@telemetryos/cli 1.9.0 → 1.11.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.
- package/CHANGELOG.md +25 -0
- package/dist/commands/auth.js +8 -15
- package/dist/commands/init.js +131 -68
- package/dist/commands/publish.d.ts +22 -0
- package/dist/commands/publish.js +238 -0
- package/dist/index.js +2 -0
- package/dist/plugins/math-tools.d.ts +2 -0
- package/dist/plugins/math-tools.js +18 -0
- package/dist/services/api-client.d.ts +18 -0
- package/dist/services/api-client.js +70 -0
- package/dist/services/archiver.d.ts +4 -0
- package/dist/services/archiver.js +65 -0
- package/dist/services/build-poller.d.ts +10 -0
- package/dist/services/build-poller.js +63 -0
- package/dist/services/cli-config.d.ts +10 -0
- package/dist/services/cli-config.js +45 -0
- package/dist/services/generate-application.d.ts +2 -1
- package/dist/services/generate-application.js +31 -32
- package/dist/services/project-config.d.ts +24 -0
- package/dist/services/project-config.js +51 -0
- package/dist/services/run-server.js +29 -73
- package/dist/types/api.d.ts +44 -0
- package/dist/types/api.js +1 -0
- package/dist/types/applications.d.ts +44 -0
- package/dist/types/applications.js +1 -0
- package/dist/utils/ansi.d.ts +10 -0
- package/dist/utils/ansi.js +10 -0
- package/dist/utils/path-utils.d.ts +55 -0
- package/dist/utils/path-utils.js +99 -0
- package/package.json +4 -2
- package/templates/vite-react-typescript/CLAUDE.md +14 -6
- package/templates/vite-react-typescript/_claude/skills/tos-architecture/SKILL.md +4 -28
- package/templates/vite-react-typescript/_claude/skills/tos-multi-mode/SKILL.md +359 -0
- package/templates/vite-react-typescript/_claude/skills/tos-render-design/SKILL.md +304 -12
- package/templates/vite-react-typescript/_claude/skills/tos-render-kiosk-design/SKILL.md +384 -0
- package/templates/vite-react-typescript/_claude/skills/tos-render-signage-design/SKILL.md +515 -0
- package/templates/vite-react-typescript/_claude/skills/tos-render-ui-design/SKILL.md +325 -0
- package/templates/vite-react-typescript/_claude/skills/tos-requirements/SKILL.md +405 -125
- package/templates/vite-react-typescript/_claude/skills/tos-store-sync/SKILL.md +96 -5
- package/templates/vite-react-typescript/_claude/skills/tos-weather-api/SKILL.md +443 -269
- package/templates/vite-react-typescript/index.html +1 -1
|
@@ -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)
|