@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
|
@@ -1,33 +1,63 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: tos-render-design
|
|
3
|
-
description: Design patterns for TelemetryOS Render views. Use when building
|
|
3
|
+
description: Design patterns for TelemetryOS Render views. Use when building display-only digital signage OR interactive kiosks. Covers responsive scaling, UI patterns, and interaction handling for both models.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Render View Design
|
|
7
7
|
|
|
8
|
-
TelemetryOS Render views
|
|
8
|
+
TelemetryOS Render views support both **digital signage** (display-only) and **interactive kiosks** (touch-enabled). Understanding which pattern you're building determines how you handle user interaction and state management.
|
|
9
9
|
|
|
10
10
|
> **Note:** The init project already provides base styles in `index.css` (viewport scaling, box-sizing) and `Render.css` (`.render` class with padding, overflow, flexbox). Build on these—don't override them.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Interaction Models
|
|
15
|
+
|
|
16
|
+
TelemetryOS render views support two interaction models. Both use `@telemetryos/sdk` with the same architecture—the difference is whether the render view includes onClick handlers.
|
|
17
|
+
|
|
18
|
+
### Digital Signage (Display-Only)
|
|
19
|
+
|
|
20
|
+
**Use for:** Information displays, dashboards, menu boards, announcements
|
|
21
|
+
|
|
22
|
+
- Content updates automatically via store subscriptions
|
|
23
|
+
- No user interaction (no mouse, keyboard, touch input)
|
|
24
|
+
- No onClick handlers in render view
|
|
25
|
+
- Viewed from a distance
|
|
26
|
+
- Updates driven by timers, external data, or Settings changes
|
|
27
|
+
|
|
28
|
+
### Interactive Kiosk (Touch-Enabled)
|
|
29
|
+
|
|
30
|
+
**Use for:** Wayfinding, directories, check-in systems, search interfaces
|
|
31
|
+
|
|
32
|
+
- Users can touch/click elements on screen
|
|
33
|
+
- onClick handlers for buttons, navigation, forms
|
|
34
|
+
- Idle timeout returns to home screen after inactivity
|
|
35
|
+
- Touch feedback with :active states (not :hover)
|
|
36
|
+
- Manages interaction state (current screen, navigation history)
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Digital Signage Pattern (Display-Only)
|
|
41
|
+
|
|
42
|
+
For apps where users only **view** content from a distance.
|
|
13
43
|
|
|
14
44
|
### No User Interaction
|
|
15
45
|
|
|
16
|
-
|
|
46
|
+
Assume **no mouse, keyboard, or touch input**:
|
|
17
47
|
|
|
18
48
|
```css
|
|
19
|
-
/* WRONG - No one will hover */
|
|
49
|
+
/* WRONG - No one will hover on digital signage */
|
|
20
50
|
.button:hover {
|
|
21
51
|
background: blue;
|
|
22
52
|
}
|
|
23
53
|
|
|
24
|
-
/* WRONG - No one will focus */
|
|
54
|
+
/* WRONG - No one will focus elements */
|
|
25
55
|
.input:focus {
|
|
26
56
|
outline: 2px solid blue;
|
|
27
57
|
}
|
|
28
58
|
```
|
|
29
59
|
|
|
30
|
-
Avoid `:hover`, `:focus`, `:active`, and similar interaction pseudo-classes.
|
|
60
|
+
Avoid `:hover`, `:focus`, `:active`, and similar interaction pseudo-classes for display-only apps.
|
|
31
61
|
|
|
32
62
|
### No Scrolling
|
|
33
63
|
|
|
@@ -55,8 +85,150 @@ Content **must fit the viewport**. There's no user to scroll:
|
|
|
55
85
|
|
|
56
86
|
If content might overflow, truncate it or conditionally hide elements—never show a scrollbar.
|
|
57
87
|
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Interactive Kiosk Pattern (Touch-Enabled)
|
|
91
|
+
|
|
92
|
+
For apps where users **interact** with the screen via touch or click.
|
|
93
|
+
|
|
94
|
+
### onClick Handlers
|
|
95
|
+
|
|
96
|
+
Add click handlers to interactive elements:
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
function Render() {
|
|
100
|
+
const [screen, setScreen] = useState('home')
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div className="render">
|
|
104
|
+
{screen === 'home' && (
|
|
105
|
+
<button
|
|
106
|
+
className="kiosk-button"
|
|
107
|
+
onClick={() => setScreen('search')}
|
|
108
|
+
>
|
|
109
|
+
Search Directory
|
|
110
|
+
</button>
|
|
111
|
+
)}
|
|
112
|
+
{screen === 'search' && (
|
|
113
|
+
<SearchScreen onBack={() => setScreen('home')} />
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Touch Feedback (:active states)
|
|
121
|
+
|
|
122
|
+
Use `:active` pseudo-class for touch feedback (NOT `:hover`):
|
|
123
|
+
|
|
124
|
+
```css
|
|
125
|
+
.kiosk-button {
|
|
126
|
+
padding: 2rem 4rem;
|
|
127
|
+
font-size: 3rem;
|
|
128
|
+
background: blue;
|
|
129
|
+
color: white;
|
|
130
|
+
border: none;
|
|
131
|
+
border-radius: 1rem;
|
|
132
|
+
transition: transform 0.1s, background 0.1s;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* Touch feedback - user sees visual response when tapping */
|
|
136
|
+
.kiosk-button:active {
|
|
137
|
+
transform: scale(0.95);
|
|
138
|
+
background: darkblue;
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**Why :active instead of :hover?**
|
|
143
|
+
- Touch devices don't have hover
|
|
144
|
+
- :active triggers on touch/click
|
|
145
|
+
- Provides immediate visual feedback
|
|
146
|
+
|
|
147
|
+
### Idle Timeout Pattern
|
|
148
|
+
|
|
149
|
+
Return to home screen after inactivity:
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
function Render() {
|
|
153
|
+
const [screen, setScreen] = useState('home')
|
|
154
|
+
const [lastInteraction, setLastInteraction] = useState(Date.now())
|
|
155
|
+
|
|
156
|
+
// Return to home after 30 seconds of inactivity
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
const timeout = setTimeout(() => {
|
|
159
|
+
const elapsed = Date.now() - lastInteraction
|
|
160
|
+
if (elapsed > 30000 && screen !== 'home') {
|
|
161
|
+
setScreen('home')
|
|
162
|
+
}
|
|
163
|
+
}, 30000)
|
|
164
|
+
|
|
165
|
+
return () => clearTimeout(timeout)
|
|
166
|
+
}, [lastInteraction, screen])
|
|
167
|
+
|
|
168
|
+
const handleInteraction = (newScreen: string) => {
|
|
169
|
+
setScreen(newScreen)
|
|
170
|
+
setLastInteraction(Date.now())
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<div className="render">
|
|
175
|
+
{screen === 'home' && (
|
|
176
|
+
<button onClick={() => handleInteraction('search')}>
|
|
177
|
+
Search
|
|
178
|
+
</button>
|
|
179
|
+
)}
|
|
180
|
+
</div>
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Touch Target Sizing
|
|
186
|
+
|
|
187
|
+
Make touch targets large enough to tap accurately:
|
|
188
|
+
|
|
189
|
+
```css
|
|
190
|
+
/* Minimum sizes for touch targets */
|
|
191
|
+
.kiosk-button {
|
|
192
|
+
min-width: 15rem; /* Large enough to tap */
|
|
193
|
+
min-height: 8rem;
|
|
194
|
+
padding: 2rem 4rem;
|
|
195
|
+
font-size: 3rem; /* Large, readable text */
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**Guidelines:**
|
|
200
|
+
- Minimum 8rem height for buttons
|
|
201
|
+
- 2rem padding minimum
|
|
202
|
+
- 3rem font size minimum for buttons
|
|
203
|
+
- Gap of at least 1rem between interactive elements
|
|
204
|
+
|
|
205
|
+
### Store State for Navigation
|
|
206
|
+
|
|
207
|
+
Use device store to persist state across composition changes:
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
// hooks/store.ts
|
|
211
|
+
import { createUseDeviceStoreState } from '@telemetryos/sdk/react'
|
|
212
|
+
|
|
213
|
+
export const useKioskScreenState = createUseDeviceStoreState<string>(
|
|
214
|
+
'kiosk-screen',
|
|
215
|
+
'home'
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
// Render.tsx
|
|
219
|
+
const [_isLoading, screen, setScreen] = useKioskScreenState()
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**Why device store?**
|
|
223
|
+
- Persists state on the device (survives composition changes)
|
|
224
|
+
- Doesn't sync to other devices (screen state is device-local)
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
58
228
|
## UI Scale Hooks
|
|
59
229
|
|
|
230
|
+
**Applies to both digital signage and interactive kiosks.**
|
|
231
|
+
|
|
60
232
|
Displays range from tablets to 8K video walls. Standard CSS pixels create inconsistent sizing. The SDK provides hooks that redefine `rem` as viewport-relative:
|
|
61
233
|
|
|
62
234
|
### useUiScaleToSetRem(uiScale)
|
|
@@ -101,8 +273,12 @@ export function Render() {
|
|
|
101
273
|
}
|
|
102
274
|
```
|
|
103
275
|
|
|
276
|
+
---
|
|
277
|
+
|
|
104
278
|
## Best Practices
|
|
105
279
|
|
|
280
|
+
**Applies to both digital signage and interactive kiosks.**
|
|
281
|
+
|
|
106
282
|
### Use rem for Everything
|
|
107
283
|
|
|
108
284
|
All sizing should use `rem` to scale with the UI scale setting:
|
|
@@ -200,10 +376,14 @@ function Dashboard() {
|
|
|
200
376
|
}
|
|
201
377
|
```
|
|
202
378
|
|
|
203
|
-
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## Complete Examples
|
|
382
|
+
|
|
383
|
+
### Digital Signage Example (Display-Only)
|
|
204
384
|
|
|
205
385
|
```typescript
|
|
206
|
-
// Render.tsx -
|
|
386
|
+
// Render.tsx - Display-only dashboard
|
|
207
387
|
import { useUiScaleToSetRem, useUiAspectRatio } from '@telemetryos/sdk/react'
|
|
208
388
|
import { useUiScaleStoreState } from '../hooks/store'
|
|
209
389
|
import './Render.css'
|
|
@@ -241,7 +421,7 @@ export function Render() {
|
|
|
241
421
|
```
|
|
242
422
|
|
|
243
423
|
```css
|
|
244
|
-
/* Render.css -
|
|
424
|
+
/* Render.css - Display-only styles */
|
|
245
425
|
.render__header {
|
|
246
426
|
flex-shrink: 0;
|
|
247
427
|
margin-bottom: 2rem;
|
|
@@ -278,6 +458,112 @@ export function Render() {
|
|
|
278
458
|
}
|
|
279
459
|
```
|
|
280
460
|
|
|
461
|
+
### Interactive Kiosk Example (Touch-Enabled)
|
|
462
|
+
|
|
463
|
+
```typescript
|
|
464
|
+
// Render.tsx - Interactive kiosk with navigation
|
|
465
|
+
import { useState, useEffect } from 'react'
|
|
466
|
+
import { useUiScaleToSetRem } from '@telemetryos/sdk/react'
|
|
467
|
+
import { useUiScaleStoreState } from '../hooks/store'
|
|
468
|
+
import './Render.css'
|
|
469
|
+
|
|
470
|
+
export function Render() {
|
|
471
|
+
const [isLoading, uiScale] = useUiScaleStoreState()
|
|
472
|
+
const [screen, setScreen] = useState('home')
|
|
473
|
+
const [lastInteraction, setLastInteraction] = useState(Date.now())
|
|
474
|
+
|
|
475
|
+
useUiScaleToSetRem(uiScale)
|
|
476
|
+
|
|
477
|
+
// Idle timeout - return to home after 30 seconds
|
|
478
|
+
useEffect(() => {
|
|
479
|
+
const timeout = setTimeout(() => {
|
|
480
|
+
const elapsed = Date.now() - lastInteraction
|
|
481
|
+
if (elapsed > 30000 && screen !== 'home') {
|
|
482
|
+
setScreen('home')
|
|
483
|
+
}
|
|
484
|
+
}, 30000)
|
|
485
|
+
|
|
486
|
+
return () => clearTimeout(timeout)
|
|
487
|
+
}, [lastInteraction, screen])
|
|
488
|
+
|
|
489
|
+
const handleInteraction = (newScreen: string) => {
|
|
490
|
+
setScreen(newScreen)
|
|
491
|
+
setLastInteraction(Date.now())
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (isLoading) return null
|
|
495
|
+
|
|
496
|
+
return (
|
|
497
|
+
<div className="render">
|
|
498
|
+
{screen === 'home' && (
|
|
499
|
+
<div className="kiosk-home">
|
|
500
|
+
<h1 className="kiosk-home__title">Welcome</h1>
|
|
501
|
+
<button
|
|
502
|
+
className="kiosk-button"
|
|
503
|
+
onClick={() => handleInteraction('search')}
|
|
504
|
+
>
|
|
505
|
+
Search Directory
|
|
506
|
+
</button>
|
|
507
|
+
<button
|
|
508
|
+
className="kiosk-button"
|
|
509
|
+
onClick={() => handleInteraction('map')}
|
|
510
|
+
>
|
|
511
|
+
View Map
|
|
512
|
+
</button>
|
|
513
|
+
</div>
|
|
514
|
+
)}
|
|
515
|
+
|
|
516
|
+
{screen === 'search' && (
|
|
517
|
+
<SearchScreen onBack={() => handleInteraction('home')} />
|
|
518
|
+
)}
|
|
519
|
+
|
|
520
|
+
{screen === 'map' && (
|
|
521
|
+
<MapScreen onBack={() => handleInteraction('home')} />
|
|
522
|
+
)}
|
|
523
|
+
</div>
|
|
524
|
+
)
|
|
525
|
+
}
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
```css
|
|
529
|
+
/* Render.css - Interactive kiosk styles */
|
|
530
|
+
.kiosk-home {
|
|
531
|
+
display: flex;
|
|
532
|
+
flex-direction: column;
|
|
533
|
+
align-items: center;
|
|
534
|
+
justify-content: center;
|
|
535
|
+
gap: 3rem;
|
|
536
|
+
height: 100%;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
.kiosk-home__title {
|
|
540
|
+
font-size: 6rem;
|
|
541
|
+
margin: 0;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/* Touch-friendly button with :active feedback */
|
|
545
|
+
.kiosk-button {
|
|
546
|
+
min-width: 20rem;
|
|
547
|
+
min-height: 8rem;
|
|
548
|
+
padding: 2rem 4rem;
|
|
549
|
+
font-size: 3rem;
|
|
550
|
+
background: #0066cc;
|
|
551
|
+
color: white;
|
|
552
|
+
border: none;
|
|
553
|
+
border-radius: 1rem;
|
|
554
|
+
cursor: pointer;
|
|
555
|
+
transition: transform 0.1s, background 0.1s;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/* Touch feedback - scales down when tapped */
|
|
559
|
+
.kiosk-button:active {
|
|
560
|
+
transform: scale(0.95);
|
|
561
|
+
background: #0052a3;
|
|
562
|
+
}
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
---
|
|
566
|
+
|
|
281
567
|
## Store Hook for UI Scale
|
|
282
568
|
|
|
283
569
|
Create a store hook to let admins adjust the UI scale:
|
|
@@ -318,15 +604,21 @@ export function Settings() {
|
|
|
318
604
|
}
|
|
319
605
|
```
|
|
320
606
|
|
|
607
|
+
---
|
|
608
|
+
|
|
321
609
|
## Common Mistakes
|
|
322
610
|
|
|
323
611
|
| Mistake | Problem | Fix |
|
|
324
612
|
|---------|---------|-----|
|
|
325
613
|
| Using `px` units | Won't scale across resolutions | Use `rem` everywhere |
|
|
326
|
-
| Adding `:hover` styles | No mouse on
|
|
614
|
+
| Adding `:hover` styles on digital signage | No mouse on display-only apps | Remove interaction states for display-only |
|
|
615
|
+
| Using `:hover` on interactive kiosks | No hover on touch devices | Use `:active` instead for touch feedback |
|
|
327
616
|
| Using `overflow: scroll` | No user to scroll | Use `overflow: hidden`, truncate content |
|
|
328
617
|
| Fixed heights in `px` | Breaks on different aspect ratios | Use `vh`, `%`, or flex |
|
|
329
|
-
| Forgetting `useUiScaleToSetRem()` | `rem` units won't scale properly | Call it once in Render view with
|
|
618
|
+
| Forgetting `useUiScaleToSetRem()` | `rem` units won't scale properly | Call it once in Render view with uiScale |
|
|
330
619
|
| Text below 2rem | Unreadable from viewing distance | Minimum 2rem for body text |
|
|
620
|
+
| Small touch targets on kiosks | Hard to tap accurately | Minimum 8rem height, 2rem padding, 3rem font |
|
|
621
|
+
| No idle timeout on kiosks | Kiosk stays on user's screen | Add useEffect timeout logic |
|
|
622
|
+
| No touch feedback on kiosks | User unsure if tap registered | Add :active state animations |
|
|
331
623
|
| Removing `.render` padding | Content cut off by bezels | Keep the ~3rem padding from init project |
|
|
332
624
|
| Overriding `index.css` base styles | Breaks viewport scaling | Add new styles, don't modify base setup |
|