@telemetryos/cli 1.10.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,515 @@
1
+ ---
2
+ name: tos-render-signage-design
3
+ description: Design patterns for display-only TelemetryOS digital signage. Use AFTER reading tos-render-ui-design. Covers no-interaction constraints, no-scrolling requirements, and auto-rotation patterns.
4
+ ---
5
+
6
+ # Digital Signage Design (Display-Only)
7
+
8
+ For TelemetryOS render views where content is **viewed from a distance** with **no user interaction**.
9
+
10
+ > **Prerequisites:** Read `tos-render-ui-design` first for foundation concepts (rem scaling, safe zones, text sizing, responsive layouts).
11
+
12
+ ---
13
+
14
+ ## What is Digital Signage?
15
+
16
+ **Use for:** Information displays, dashboards, menu boards, announcements
17
+
18
+ Digital signage is display-only content with these characteristics:
19
+
20
+ - Content updates automatically via store subscriptions
21
+ - No user interaction (no mouse, keyboard, touch input)
22
+ - No onClick handlers in render view
23
+ - Viewed from a distance
24
+ - Updates driven by timers, external data, or Settings changes
25
+
26
+ **Examples:**
27
+ - Restaurant menu boards that show daily specials
28
+ - Airport flight status displays
29
+ - Office lobby dashboards showing company metrics
30
+ - Retail price displays
31
+ - Weather and news displays
32
+
33
+ ---
34
+
35
+ ## Core Constraints
36
+
37
+ ### No User Interaction
38
+
39
+ Assume **no mouse, keyboard, or touch input**:
40
+
41
+ ```css
42
+ /* WRONG - No one will hover on digital signage */
43
+ .button:hover {
44
+ background: blue;
45
+ }
46
+
47
+ /* WRONG - No one will focus elements */
48
+ .input:focus {
49
+ outline: 2px solid blue;
50
+ }
51
+ ```
52
+
53
+ **Why avoid interaction states?**
54
+ - Display-only apps have no input devices
55
+ - `:hover`, `:focus`, `:active` pseudo-classes will never trigger
56
+ - CSS for these states wastes bytes and creates confusion
57
+
58
+ Avoid `:hover`, `:focus`, `:active`, and similar interaction pseudo-classes for display-only apps.
59
+
60
+ ### No Scrolling
61
+
62
+ Content **must fit the viewport**. There's no user to scroll:
63
+
64
+ ```css
65
+ /* WRONG - Creates scrollbar no one can use */
66
+ .container {
67
+ overflow-y: scroll;
68
+ }
69
+
70
+ /* WRONG - Content disappears off-screen */
71
+ .content {
72
+ height: 150vh;
73
+ }
74
+ ```
75
+
76
+ ```css
77
+ /* CORRECT - Content contained */
78
+ .container {
79
+ height: 100vh;
80
+ overflow: hidden;
81
+ }
82
+ ```
83
+
84
+ **What to do instead:**
85
+ - Use text truncation (ellipsis, line-clamp)
86
+ - Conditionally hide less important elements
87
+ - Paginate content with timer-based rotation
88
+ - Adapt layout for portrait/landscape (see `tos-render-ui-design`)
89
+
90
+ If content might overflow, truncate it or conditionally hide elements—never show a scrollbar.
91
+
92
+ > **See "Layout Philosophy: Outside-In Design"** below for the architectural approach that prevents overflow by design.
93
+
94
+ ---
95
+
96
+ ## Layout Philosophy: Outside-In Design
97
+
98
+ ### The Signage Layout Mindset
99
+
100
+ Digital signage requires a fundamentally different layout approach than typical web development.
101
+
102
+ **Typical Web (Inside-Out):**
103
+ - Content determines its size first
104
+ - Elements demand the space they require
105
+ - Containers grow to fit content
106
+ - Overflow is handled with scrollbars
107
+
108
+ **Digital Signage (Outside-In):**
109
+ - Viewport dimensions are fixed and known upfront
110
+ - Available space is divided among components
111
+ - Components size themselves to fit allocated space
112
+ - Overflow is prevented by design
113
+
114
+ ### Why No Overflow Is Acceptable
115
+
116
+ In signage, **overflow is NOT ACCEPTABLE** because:
117
+ - There's no user to scroll
118
+ - Content cut off looks unprofessional and broken
119
+ - You cannot add scrollbars (violates no-scrolling constraint)
120
+ - Prevention through design is the only solution
121
+
122
+ **Solution:** Combine outside-in space allocation with graceful degradation techniques (text truncation, conditional hiding) to ensure content always fits elegantly.
123
+
124
+ ### The Outside-In Pattern
125
+
126
+ Build layouts in three steps:
127
+
128
+ 1. **Start with viewport** - Your container is `100vw × 100vh` (fixed)
129
+ 2. **Divide space** - Use flexbox/grid to partition available real estate among top-level components
130
+ 3. **Fill allocations** - Inside components size themselves to use the space they've been given
131
+
132
+ This is opposite from typical web development where elements size themselves based on content first.
133
+
134
+ ### CSS Techniques for Space Division
135
+
136
+ | Technique | Purpose | Example |
137
+ |-----------|---------|---------|
138
+ | `flex: 1` | Component takes proportional share of remaining space | Main content area |
139
+ | `fr` units | Grid tracks divide available space | `grid-template-columns: 2fr 1fr` |
140
+ | `min-height: 0` | Allow flex children to shrink below content size | Prevents overflow |
141
+ | `min-width: 0` | Allow flex children to shrink below content size | Prevents overflow |
142
+ | Container queries | Text sizes itself to fit allocated container space | `font-size: min(10cqh, 6cqw)` |
143
+ | Text truncation | Gracefully handle text that might not fit | `text-overflow: ellipsis`, `line-clamp` |
144
+ | Conditional rendering | Hide less important elements when space is tight | `{isLandscape && <Sidebar />}` |
145
+
146
+ ### Dynamic Text Sizing with Container Queries
147
+
148
+ Use CSS container queries with `cqw` (container query width) and `cqh` (container query height) units to size text based on its container dimensions. This is useful for two scenarios:
149
+
150
+ 1. **Maximizing text size** - Make text as large as possible while fitting the container
151
+ 2. **Calculated space allocation** - Divide container space among parts by calculating cq values based on known content
152
+
153
+ The key advantage: cq values are **calculated based on your content** (number of lines, characters) and how much container space you want to allocate.
154
+
155
+ **How it works:**
156
+ 1. Container receives allocated space from flexbox/grid (outside-in)
157
+ 2. Set `container-type: size` on the container
158
+ 3. Content uses `cqw`/`cqh` units for font sizing
159
+ 4. Use `min()` to constrain by both dimensions
160
+
161
+ **Calculating cq values based on content:**
162
+
163
+ Container query values should be calculated based on your content and space allocation goals:
164
+
165
+ | Content | Goal | Calculation | Result |
166
+ |---------|------|-------------|--------|
167
+ | 2 lines of text | Occupy 50% of container height | 50% ÷ 2 lines | `25cqh` |
168
+ | 8 characters | Occupy 100% of container width | 100% ÷ 8 chars | `12.5cqw` |
169
+ | Result | Fit both dimensions | Use smaller value | `min(25cqh, 12.5cqw)` |
170
+
171
+ **Example calculation:**
172
+ ```css
173
+ /* Content: "CODE-123" (8 characters, 1 line) */
174
+ /* Goal: Full width, 40% of height */
175
+ .code {
176
+ font-size: min(
177
+ 40cqh, /* 40% height ÷ 1 line */
178
+ 12.5cqw /* 100% width ÷ 8 characters */
179
+ );
180
+ }
181
+ ```
182
+
183
+ This makes container queries a **precise space allocation tool**, not just a "make it big" tool.
184
+
185
+ **Example: Grid of cards with large text**
186
+
187
+ ```css
188
+ .card {
189
+ container-type: size; /* Enable container queries */
190
+ flex: 1; /* Outside-in: Accept allocated space */
191
+ min-height: 0;
192
+ display: flex;
193
+ flex-direction: column;
194
+ padding: min(3cqh, 2cqw); /* Padding scales with container */
195
+ gap: min(1.5cqh, 1cqw);
196
+ }
197
+
198
+ .card-title {
199
+ font-size: min(10cqh, 6cqw); /* Constrained by container dimensions */
200
+ flex-shrink: 0;
201
+ }
202
+
203
+ .card-number {
204
+ font-size: min(55cqh, 25cqw); /* Large text that fits container */
205
+ font-weight: 900;
206
+ line-height: 1;
207
+ flex-shrink: 1;
208
+ min-height: 0;
209
+ }
210
+
211
+ .card-subtitle {
212
+ font-size: min(8cqh, 5cqw);
213
+ flex-shrink: 0;
214
+ }
215
+ ```
216
+
217
+ **How these values were chosen:**
218
+ - `card-title`: 10cqh = ~10 characters width, 1 line height
219
+ - `card-number`: 55cqh = ~4 characters width, 1 line at ~55% height
220
+ - `card-subtitle`: 8cqh = ~12 characters width, 1 line height
221
+
222
+ These are calculated based on expected content and desired space allocation.
223
+
224
+ **Why `min(cqh, cqw)`?**
225
+ - Takes the smaller of the two values
226
+ - Ensures text fits both width AND height
227
+ - Prevents overflow in either dimension
228
+
229
+ **When to use:**
230
+ - Large display text that should fill its space (numbers, codes, headlines)
231
+ - Grids/cards where text prominence is important
232
+ - Known content length (works best with predictable text)
233
+ - **Precise space allocation** - When you want to divide container space proportionally based on content
234
+
235
+ **When NOT to use:**
236
+ - Body text or paragraphs (use rem + line-clamp instead)
237
+ - Unknown/variable content length (truncation is safer)
238
+ - Complex text layouts with multiple font sizes
239
+
240
+ **Browser support:** Container queries are widely supported in modern browsers (Chrome 105+, Safari 16+, Firefox 110+). For TelemetryOS signage on updated devices, this is safe to use.
241
+
242
+ ### Outside-In Example
243
+
244
+ ```typescript
245
+ // Render.tsx - Outside-in layout pattern
246
+ export function Render() {
247
+ return (
248
+ <div className="render"> {/* Fixed: 100vh */}
249
+ <header className="render__header"> {/* Allocated: fixed height */}
250
+ <h1 className="render__title">Dashboard</h1>
251
+ </header>
252
+
253
+ <main className="render__content"> {/* Allocated: remaining space */}
254
+ <PrimaryPanel /> {/* Works within allocated space */}
255
+ </main>
256
+ </div>
257
+ )
258
+ }
259
+ ```
260
+
261
+ ```css
262
+ /* Outside-in: Divide the viewport's available space */
263
+ .render {
264
+ height: 100vh;
265
+ display: flex;
266
+ flex-direction: column;
267
+ }
268
+
269
+ .render__header {
270
+ flex-shrink: 0; /* Fixed allocation */
271
+ height: 10rem;
272
+ }
273
+
274
+ .render__title {
275
+ font-size: 4rem;
276
+ white-space: nowrap;
277
+ overflow: hidden;
278
+ text-overflow: ellipsis; /* Graceful degradation for long titles */
279
+ }
280
+
281
+ .render__content {
282
+ flex: 1; /* Takes remaining space after header */
283
+ min-height: 0; /* Critical: allows shrinking if needed */
284
+ }
285
+ ```
286
+
287
+ ### Anti-Patterns (Inside-Out Thinking)
288
+
289
+ ```css
290
+ /* WRONG - Content dictates size */
291
+ .content {
292
+ height: auto; /* Size based on content */
293
+ min-height: 800px; /* Fixed demand that might overflow */
294
+ }
295
+
296
+ /* WRONG - No space division */
297
+ .container {
298
+ display: block; /* Elements stack naturally */
299
+ /* Content grows unbounded */
300
+ }
301
+
302
+ /* CORRECT - Outside-in approach */
303
+ .content {
304
+ flex: 1; /* Accept allocated space */
305
+ min-height: 0; /* Can shrink if needed */
306
+ }
307
+
308
+ .container {
309
+ display: flex; /* Divide space */
310
+ flex-direction: column;
311
+ }
312
+
313
+ /* CORRECT - Graceful degradation for content that might not fit */
314
+ .text {
315
+ overflow: hidden;
316
+ text-overflow: ellipsis; /* Show ... instead of cutting off */
317
+ white-space: nowrap;
318
+ }
319
+
320
+ .optional-element {
321
+ /* Use conditional rendering: {hasSpace && <OptionalElement />} */
322
+ }
323
+ ```
324
+
325
+ **Key principle:** Build outside-in. Use flexbox and grid to divide available real estate, not to accommodate content demands. When content might not fit, use graceful degradation (truncation, conditional hiding) instead of hard cutoff.
326
+
327
+ ---
328
+
329
+ ## Layout Patterns
330
+
331
+ ### Full Viewport Content
332
+
333
+ Digital signage layouts should fill the viewport completely:
334
+
335
+ ```typescript
336
+ import { useUiScaleToSetRem, useUiAspectRatio } from '@telemetryos/sdk/react'
337
+ import { useUiScaleStoreState } from '../hooks/store'
338
+
339
+ export function Render() {
340
+ const [isLoading, uiScale] = useUiScaleStoreState()
341
+ const aspectRatio = useUiAspectRatio()
342
+
343
+ useUiScaleToSetRem(uiScale)
344
+
345
+ if (isLoading) return null
346
+
347
+ const isPortrait = aspectRatio < 1
348
+
349
+ return (
350
+ <div className="render">
351
+ {/* Content that fills viewport */}
352
+ <header>...</header>
353
+ <main>...</main>
354
+ </div>
355
+ )
356
+ }
357
+ ```
358
+
359
+ ```css
360
+ /* The init project's .render class handles this */
361
+ .render {
362
+ /* Already set: width: 100vw, height: 100vh, overflow: hidden */
363
+ /* Just add your content layout */
364
+ display: flex;
365
+ flex-direction: column;
366
+ }
367
+ ```
368
+
369
+ ### Auto-Rotation Considerations
370
+
371
+ Digital signage content may rotate in/out of playlists or compositions:
372
+
373
+ ```typescript
374
+ export function Render() {
375
+ const [isLoading, uiScale] = useUiScaleStoreState()
376
+
377
+ useUiScaleToSetRem(uiScale)
378
+
379
+ // Clean up timers/subscriptions on unmount
380
+ useEffect(() => {
381
+ const interval = setInterval(() => {
382
+ // Refresh data
383
+ }, 60000)
384
+
385
+ return () => clearInterval(interval)
386
+ }, [])
387
+
388
+ if (isLoading) return null
389
+
390
+ return <div className="render">...</div>
391
+ }
392
+ ```
393
+
394
+ **Best practices for auto-rotation:**
395
+ - Use `useEffect` cleanup to clear timers/subscriptions
396
+ - Don't rely on user interaction to trigger updates
397
+ - Content should be meaningful from the moment it appears
398
+ - Use store subscriptions for real-time data updates
399
+
400
+ ---
401
+
402
+ ## Complete Example
403
+
404
+ ### Digital Signage Dashboard
405
+
406
+ ```typescript
407
+ // Render.tsx - Display-only dashboard
408
+ import { useUiScaleToSetRem, useUiAspectRatio } from '@telemetryos/sdk/react'
409
+ import { useUiScaleStoreState } from '../hooks/store'
410
+ import './Render.css'
411
+
412
+ export function Render() {
413
+ const [isLoading, uiScale] = useUiScaleStoreState()
414
+ const aspectRatio = useUiAspectRatio()
415
+
416
+ useUiScaleToSetRem(uiScale)
417
+
418
+ if (isLoading) return null
419
+
420
+ const isPortrait = aspectRatio < 1
421
+
422
+ return (
423
+ <div className="render">
424
+ <header className="render__header">
425
+ <h1 className="render__title">Dashboard</h1>
426
+ </header>
427
+
428
+ <main className={`render__content ${isPortrait ? 'render__content--portrait' : ''}`}>
429
+ <div className="render__primary">
430
+ <MainDisplay />
431
+ </div>
432
+
433
+ {!isPortrait && (
434
+ <aside className="render__sidebar">
435
+ <SecondaryInfo />
436
+ </aside>
437
+ )}
438
+ </main>
439
+ </div>
440
+ )
441
+ }
442
+ ```
443
+
444
+ ```css
445
+ /* Render.css - Display-only styles */
446
+ .render__header {
447
+ flex-shrink: 0;
448
+ margin-bottom: 2rem;
449
+ }
450
+
451
+ .render__title {
452
+ font-size: 4rem;
453
+ margin: 0;
454
+ white-space: nowrap;
455
+ overflow: hidden;
456
+ text-overflow: ellipsis;
457
+ }
458
+
459
+ .render__content {
460
+ flex: 1; /* Outside-in: Takes remaining space after header */
461
+ min-height: 0; /* Critical: prevents overflow */
462
+ display: flex;
463
+ gap: 2rem;
464
+ }
465
+
466
+ .render__content--portrait {
467
+ flex-direction: column;
468
+ }
469
+
470
+ .render__primary {
471
+ flex: 1; /* Outside-in: Proportional space allocation */
472
+ min-width: 0; /* Allows shrinking */
473
+ min-height: 0; /* Allows shrinking */
474
+ }
475
+
476
+ .render__sidebar {
477
+ width: 25rem; /* Outside-in: Fixed allocation */
478
+ flex-shrink: 0; /* Maintains fixed size */
479
+ }
480
+ ```
481
+
482
+ ---
483
+
484
+ ## Common Mistakes
485
+
486
+ | Mistake | Problem | Fix |
487
+ |---------|---------|-----|
488
+ | Adding `:hover` styles on digital signage | No mouse on display-only apps | Remove interaction states for display-only |
489
+ | Using `overflow: scroll` | No user to scroll | Use `overflow: hidden`, truncate content |
490
+ | Creating scrollbars | No one can use them | Fit content to viewport, hide overflow |
491
+ | Fixed content height > 100vh | Content disappears off-screen | Use `flex: 1` and `min-height: 0` |
492
+ | Using `height: auto` on containers | Content dictates size (inside-out) | Use `flex: 1` to accept allocated space |
493
+ | Forgetting `min-height: 0` on flex children | Prevents shrinking, causes overflow | Always add `min-height: 0` to flex children |
494
+ | Setting `min-height` in px on main containers | Creates fixed demand that can overflow viewport | Use `flex: 1` instead of fixed minimums |
495
+
496
+ ---
497
+
498
+ ## Tips for Digital Signage
499
+
500
+ ✅ **Think outside-in** - Start with viewport dimensions, divide space with flex/grid, let components fill their allocations
501
+ ✅ **Use container queries for dynamic text sizing** - `min(cqh, cqw)` makes text as large as possible while fitting the container
502
+ ✅ **Preview at multiple aspect ratios** - Test portrait and landscape in dev host
503
+ ✅ **Test text truncation** - Verify ellipsis works correctly
504
+ ✅ **Ensure content fits** - Nothing should require scrolling
505
+ ✅ **Use timer-based updates** - Content can rotate/refresh automatically
506
+ ✅ **Subscribe to store changes** - Content updates when Settings change
507
+ ✅ **Clean up effects** - Clear timers/subscriptions on unmount
508
+ ✅ **No interaction states** - Remove `:hover`, `:focus`, `:active` CSS
509
+
510
+ ---
511
+
512
+ ## See Also
513
+
514
+ - `tos-render-ui-design` - Foundation concepts for all render views (UI scaling, rem usage, responsive layouts)
515
+ - `tos-render-kiosk-design` - If you need to add interaction later (touch, onClick, idle timeout)