Skip to content

feat(simple-message-app): redesign and refactor#711

Open
nicomiguelino wants to merge 5 commits intomasterfrom
feat/redesign-simple-message-app
Open

feat(simple-message-app): redesign and refactor#711
nicomiguelino wants to merge 5 commits intomasterfrom
feat/redesign-simple-message-app

Conversation

@nicomiguelino
Copy link
Contributor

Summary

  • Redesign the Simple Message App using plain TypeScript and @screenly/edge-apps web components, replacing the previous Vue 3 implementation
  • Implement layout based on the approved Figma design with a blurred background, left-panel heading/badge, and frosted glass message card
  • Add responsive portrait layout with scaled typography, adjusted card border-radius and padding

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions
Copy link

github-actions bot commented Feb 26, 2026

PR Reviewer Guide 🔍

(Review updated until commit bd19b5f)

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Robustness

signalReady() is only reached if all awaited operations succeed. If getLocale()/getTimeZone() (or formatLocalizedDate) throws/rejects, the app may never signal readiness. Consider wrapping the async badge formatting in try/catch (or try/finally) so readiness is always signaled and failures degrade gracefully (e.g., omit badge text).

document.addEventListener('DOMContentLoaded', async () => {
  setupErrorHandling()
  setupTheme()

  const messageHeader = getSettingWithDefault<string>(
    'message_header',
    'Simple Message App',
  )
  const messageHeaderEl =
    document.querySelector<HTMLHeadingElement>('#message-header')
  if (messageHeaderEl) {
    messageHeaderEl.textContent = messageHeader
  }

  const messageBody = getSettingWithDefault<string>(
    'message_body',
    'A simple message app allows users to display text on a screen, making it a\nbasic tool for digital signage. Users can input and edit both the heading\nand message body directly from the Screenly dashboard.\n',
  )
  const messageBodyEl = document.querySelector<HTMLDivElement>('#message-body')
  if (messageBodyEl) {
    messageBodyEl.textContent = messageBody
  }

  const dateBadgeEl = document.querySelector<HTMLDivElement>('#date-badge')
  if (dateBadgeEl) {
    const [locale, timezone] = await Promise.all([getLocale(), getTimeZone()])
    const now = new Date()
    dateBadgeEl.textContent = formatLocalizedDate(now, locale, {
      month: 'long',
      year: 'numeric',
      timeZone: timezone,
    })
  }

  signalReady()
})
Test Flakiness

Using page.waitForLoadState('networkidle') can be flaky/hang in the presence of analytics/tag-manager requests (even mocked) or long-polling. For more deterministic screenshots, consider waiting for specific UI selectors (e.g., header/card elements) and/or using a less strict load state (domcontentloaded/load) before capturing.

for (const { width, height } of RESOLUTIONS) {
  test(`screenshot ${width}x${height}`, async ({ browser }) => {
    const screenshotsDir = getScreenshotsDir()

    const context = await browser.newContext({ viewport: { width, height } })
    const page = await context.newPage()

    await setupScreenlyJsMock(page, screenlyJsContent)

    await page.goto('/')
    await page.waitForLoadState('networkidle')

    await page.screenshot({
      path: path.join(screenshotsDir, `${width}x${height}.png`),
      fullPage: false,
    })

    await context.close()
  })
Performance

The full-screen blurred overlay (body::before with backdrop-filter: blur(7px) over a large background image) can be expensive on low-power signage devices and may impact frame rate. Consider validating performance on target hardware and providing a fallback (e.g., reduced blur or no blur) if needed.

body::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: var(--z-index-backdrop);
  pointer-events: none;
  background: rgba(0, 0, 0, 0.2);
  backdrop-filter: blur(7px);
}

@github-actions
Copy link

github-actions bot commented Feb 26, 2026

PR Code Suggestions ✨

Latest suggestions up to bd19b5f
Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Always signal readiness on errors

Ensure signalReady() always runs even if getLocale()/getTimeZone() fail; otherwise
the app may never signal readiness and stay blank. Wrap the initialization in
try/finally and guard the date formatting with a nested try/catch (or default
fallback values).

edge-apps/simple-message-app/src/main.ts [13-48]

 document.addEventListener('DOMContentLoaded', async () => {
   setupErrorHandling()
   setupTheme()
 
-  const messageHeader = getSettingWithDefault<string>(
-    'message_header',
-    'Simple Message App',
-  )
-  const messageHeaderEl =
-    document.querySelector<HTMLHeadingElement>('#message-header')
-  if (messageHeaderEl) {
-    messageHeaderEl.textContent = messageHeader
+  try {
+    const messageHeader = getSettingWithDefault<string>(
+      'message_header',
+      'Simple Message App',
+    )
+    const messageHeaderEl =
+      document.querySelector<HTMLHeadingElement>('#message-header')
+    if (messageHeaderEl) {
+      messageHeaderEl.textContent = messageHeader
+    }
+
+    const messageBody = getSettingWithDefault<string>(
+      'message_body',
+      'A simple message app allows users to display text on a screen, making it a\nbasic tool for digital signage. Users can input and edit both the heading\nand message body directly from the Screenly dashboard.\n',
+    )
+    const messageBodyEl =
+      document.querySelector<HTMLDivElement>('#message-body')
+    if (messageBodyEl) {
+      messageBodyEl.textContent = messageBody
+    }
+
+    const dateBadgeEl = document.querySelector<HTMLDivElement>('#date-badge')
+    if (dateBadgeEl) {
+      try {
+        const [locale, timezone] = await Promise.all([
+          getLocale(),
+          getTimeZone(),
+        ])
+        dateBadgeEl.textContent = formatLocalizedDate(new Date(), locale, {
+          month: 'long',
+          year: 'numeric',
+          timeZone: timezone,
+        })
+      } catch {
+        dateBadgeEl.textContent = ''
+      }
+    }
+  } finally {
+    signalReady()
   }
-
-  const messageBody = getSettingWithDefault<string>(
-    'message_body',
-    'A simple message app allows users to display text on a screen, making it a\nbasic tool for digital signage. Users can input and edit both the heading\nand message body directly from the Screenly dashboard.\n',
-  )
-  const messageBodyEl = document.querySelector<HTMLDivElement>('#message-body')
-  if (messageBodyEl) {
-    messageBodyEl.textContent = messageBody
-  }
-
-  const dateBadgeEl = document.querySelector<HTMLDivElement>('#date-badge')
-  if (dateBadgeEl) {
-    const [locale, timezone] = await Promise.all([getLocale(), getTimeZone()])
-    const now = new Date()
-    dateBadgeEl.textContent = formatLocalizedDate(now, locale, {
-      month: 'long',
-      year: 'numeric',
-      timeZone: timezone,
-    })
-  }
-
-  signalReady()
 })
Suggestion importance[1-10]: 7

__

Why: If getLocale()/getTimeZone() (or formatLocalizedDate()) throws, the current code would skip signalReady(), potentially leaving the device waiting indefinitely. Wrapping the handler body in try/finally correctly guarantees signalReady() runs and the proposed change matches the shown existing_code.

Medium
Make screenshot tests deterministic

Avoid networkidle here and disable analytics in the mock; analytics/tag manager can
trigger background requests that make networkidle flaky or hang. Also close the
browser context in a finally so resources are cleaned up even when a screenshot step
fails.

edge-apps/simple-message-app/e2e/screenshots.spec.ts [10-42]

 const { screenlyJsContent } = createMockScreenlyForScreenshots(
   { coordinates: [40.7128, -74.006], location: 'New York, NY' },
   {
-    enable_analytics: 'true',
+    enable_analytics: 'false',
     message_body: 'A simple message app...',
     message_header: 'Simple Message App',
     override_locale: 'en',
     override_timezone: 'America/New_York',
-    tag_manager_id: 'GTM-P98SPZ9Z',
     theme: 'light',
   },
 )
 
 for (const { width, height } of RESOLUTIONS) {
   test(`screenshot ${width}x${height}`, async ({ browser }) => {
     const screenshotsDir = getScreenshotsDir()
 
     const context = await browser.newContext({ viewport: { width, height } })
-    const page = await context.newPage()
+    try {
+      const page = await context.newPage()
 
-    await setupScreenlyJsMock(page, screenlyJsContent)
+      await setupScreenlyJsMock(page, screenlyJsContent)
 
-    await page.goto('/')
-    await page.waitForLoadState('networkidle')
+      await page.goto('/', { waitUntil: 'domcontentloaded' })
+      await page.waitForFunction(() => {
+        const h = document.querySelector('#message-header')?.textContent?.trim()
+        const b = document.querySelector('#message-body')?.textContent?.trim()
+        return Boolean(h) && Boolean(b)
+      })
 
-    await page.screenshot({
-      path: path.join(screenshotsDir, `${width}x${height}.png`),
-      fullPage: false,
-    })
-
-    await context.close()
+      await page.screenshot({
+        path: path.join(screenshotsDir, `${width}x${height}.png`),
+        fullPage: false,
+      })
+    } finally {
+      await context.close()
+    }
   })
 }
Suggestion importance[1-10]: 7

__

Why: Replacing page.waitForLoadState('networkidle') with a DOM/content-based readiness check reduces flakiness, especially if enable_analytics/tag_manager_id triggers background requests. Adding a try/finally around context.close() is also a solid reliability improvement for the Playwright run.

Medium

Previous suggestions

Suggestions up to commit 69ab72e
CategorySuggestion                                                                                                                                    Impact
Possible issue
Always send ready signal

Ensure signalReady() always executes even if a runtime error occurs while reading
settings or updating the DOM; otherwise the player may wait indefinitely for the
ready signal. Wrap the app initialization logic in a try/finally and call
signalReady() in the finally block.

edge-apps/simple-message-app-new/src/main.ts [10-52]

 document.addEventListener('DOMContentLoaded', () => {
   setupErrorHandling()
   setupTheme()
 
-  const theme = getSettingWithDefault<string>('theme', 'light')
-  document.body.setAttribute('data-theme', theme)
+  try {
+    const theme = getSettingWithDefault<string>('theme', 'light')
+    document.body.setAttribute('data-theme', theme)
 
-  const messageHeader = getSettingWithDefault<string>(
-    'message_header',
-    'Simple Message App',
-  )
-  const messageHeaderEl =
-    document.querySelector<HTMLHeadingElement>('#message-header')
-  if (messageHeaderEl) {
-    messageHeaderEl.textContent = messageHeader
+    const messageHeader = getSettingWithDefault<string>(
+      'message_header',
+      'Simple Message App',
+    )
+    const messageHeaderEl =
+      document.querySelector<HTMLHeadingElement>('#message-header')
+    if (messageHeaderEl) {
+      messageHeaderEl.textContent = messageHeader
+    }
+
+    const messageBody = getSettingWithDefault<string>('message_body', '')
+    const messageBodyEl =
+      document.querySelector<HTMLDivElement>('#message-body')
+    if (messageBodyEl && messageBody) {
+      messageBodyEl.textContent = messageBody
+    }
+
+    const overrideLocale = getSettingWithDefault<string>('override_locale', 'en')
+    const overrideTimezone = getSettingWithDefault<string>(
+      'override_timezone',
+      '',
+    )
+    const dateBadgeEl = document.querySelector<HTMLDivElement>('#date-badge')
+    if (dateBadgeEl) {
+      const now = new Date()
+      const options: Intl.DateTimeFormatOptions = {
+        month: 'long',
+        year: 'numeric',
+      }
+      if (overrideTimezone) {
+        options.timeZone = overrideTimezone
+      }
+      dateBadgeEl.textContent = now.toLocaleDateString(overrideLocale, options)
+    }
+  } finally {
+    signalReady()
   }
-
-  const messageBody = getSettingWithDefault<string>('message_body', '')
-  const messageBodyEl = document.querySelector<HTMLDivElement>('#message-body')
-  if (messageBodyEl && messageBody) {
-    messageBodyEl.textContent = messageBody
-  }
-
-  const overrideLocale = getSettingWithDefault<string>('override_locale', 'en')
-  const overrideTimezone = getSettingWithDefault<string>(
-    'override_timezone',
-    '',
-  )
-  const dateBadgeEl = document.querySelector<HTMLDivElement>('#date-badge')
-  if (dateBadgeEl) {
-    const now = new Date()
-    const options: Intl.DateTimeFormatOptions = {
-      month: 'long',
-      year: 'numeric',
-    }
-    if (overrideTimezone) {
-      options.timeZone = overrideTimezone
-    }
-    dateBadgeEl.textContent = now.toLocaleDateString(overrideLocale, options)
-  }
-
-  signalReady()
 })
Suggestion importance[1-10]: 7

__

Why: Calling signalReady() in a finally block prevents the player from waiting indefinitely if an exception occurs while reading settings or updating the DOM. This is a meaningful reliability improvement given ready_signal: true, though it may also mask initialization failures by still signaling readiness.

Medium
Guard invalid locale/timezone values

toLocaleDateString() can throw a RangeError if override_locale or override_timezone
is invalid, which can break rendering. Validate the locale and guard the formatting
with a fallback to safe defaults.

edge-apps/simple-message-app-new/src/main.ts [39-49]

 if (dateBadgeEl) {
   const now = new Date()
   const options: Intl.DateTimeFormatOptions = {
     month: 'long',
     year: 'numeric',
   }
+
   if (overrideTimezone) {
     options.timeZone = overrideTimezone
   }
-  dateBadgeEl.textContent = now.toLocaleDateString(overrideLocale, options)
+
+  const safeLocale =
+    Intl.DateTimeFormat.supportedLocalesOf(overrideLocale).length > 0
+      ? overrideLocale
+      : 'en'
+
+  try {
+    dateBadgeEl.textContent = now.toLocaleDateString(safeLocale, options)
+  } catch {
+    dateBadgeEl.textContent = now.toLocaleDateString('en', {
+      month: 'long',
+      year: 'numeric',
+    })
+  }
 }
Suggestion importance[1-10]: 5

__

Why: toLocaleDateString() can throw (notably for an invalid timeZone), so adding a guarded fallback avoids the whole app failing to render the date badge. The change is correct but relatively defensive, and setupErrorHandling() may already cover some runtime failures.

Low
General
Always close test resources

If navigation or screenshotting fails, the browser context may not close and can
cause flakiness/leaks across resolutions. Use try/finally to always close page and
context.

edge-apps/simple-message-app-new/e2e/screenshots.spec.ts [27-40]

 const context = await browser.newContext({ viewport: { width, height } })
 const page = await context.newPage()
 
-await setupScreenlyJsMock(page, screenlyJsContent)
+try {
+  await setupScreenlyJsMock(page, screenlyJsContent)
 
-await page.goto('/')
-await page.waitForLoadState('networkidle')
+  await page.goto('/')
+  await page.waitForLoadState('networkidle')
 
-await page.screenshot({
-  path: path.join(screenshotsDir, `${width}x${height}.png`),
-  fullPage: false,
-})
+  await page.screenshot({
+    path: path.join(screenshotsDir, `${width}x${height}.png`),
+    fullPage: false,
+  })
+} finally {
+  await page.close().catch(() => {})
+  await context.close().catch(() => {})
+}
 
-await context.close()
-
Suggestion importance[1-10]: 6

__

Why: Using try/finally to ensure context.close() runs reduces leaks and flakiness across iterations when a navigation or screenshot step fails. Closing page explicitly is optional since context.close() will close pages, but the suggestion is still safe and improves robustness.

Low

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a redesigned “Simple Message App” implemented in plain TypeScript using @screenly/edge-apps web components, replacing the prior Vue-based approach and adding responsive portrait styling.

Changes:

  • Add a new TypeScript entrypoint that reads Screenly settings, applies theme, renders header/body text, and signals readiness.
  • Implement the new frosted-glass / blurred-background layout + portrait responsive CSS.
  • Add manifests, Playwright screenshot coverage, and commit generated screenshots for multiple resolutions.

Reviewed changes

Copilot reviewed 10 out of 22 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
edge-apps/simple-message-app-new/src/main.ts Initializes app, reads settings, formats date badge, applies theme, signals ready
edge-apps/simple-message-app-new/src/css/style.css Implements Figma-based layout, blur overlay, and portrait responsive adjustments
edge-apps/simple-message-app-new/index.html New HTML shell using auto-scaler and app-header web component
edge-apps/simple-message-app-new/screenly.yml App manifest defining settings used by the new TS implementation
edge-apps/simple-message-app-new/screenly_qc.yml QC manifest for the same settings schema
edge-apps/simple-message-app-new/e2e/screenshots.spec.ts Playwright-based screenshot generation across standard resolutions
edge-apps/simple-message-app-new/package.json Bun-based scripts for dev/build/lint/typecheck/screenshots/deploy
edge-apps/simple-message-app-new/README.md Setup, deployment, settings, and screenshot instructions
edge-apps/simple-message-app-new/.gitignore Ignores build output and local artifacts
edge-apps/simple-message-app-new/.ignore Ignores node_modules/
edge-apps/simple-message-app-new/screenshots/*.webp Generated visual baselines for multiple resolutions

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

nicomiguelino and others added 4 commits February 25, 2026 19:24
- Align `message_body` default with manifest and remove falsy guard
- Remove manual `theme` setting read and `data-theme` attribute assignment
- Replace raw locale/timezone handling with `getLocale`, `getTimeZone`, and `formatLocalizedDate` from the edge-apps library

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ttings

- Replace Moment.js locale link with BCP 47 / MDN Intl documentation
- Replace Moment.js timezone link with IANA time zones reference

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Update package name in package.json
- Update title and create command in README.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…Script app

- Remove old Vue-based implementation and all associated files
- Replace with new TypeScript/Vite implementation using the edge-apps library
- Update manifest files, screenshots, and static assets

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@nicomiguelino nicomiguelino marked this pull request as ready for review February 26, 2026 05:04
@github-actions
Copy link

Persistent review updated to latest commit bd19b5f

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants