@telemetryos/cli 1.11.0 → 1.13.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/commands/claude-code.d.ts +2 -0
  3. package/dist/commands/claude-code.js +29 -0
  4. package/dist/commands/init.js +22 -9
  5. package/dist/index.js +2 -0
  6. package/dist/services/create-project.d.ts +13 -0
  7. package/dist/services/create-project.js +188 -0
  8. package/dist/services/project-config.d.ts +3 -0
  9. package/dist/services/project-config.js +3 -0
  10. package/dist/services/run-server.js +186 -60
  11. package/dist/utils/ansi.d.ts +1 -0
  12. package/dist/utils/ansi.js +1 -0
  13. package/dist/utils/template.d.ts +2 -0
  14. package/dist/utils/template.js +30 -0
  15. package/package.json +4 -4
  16. package/templates/{vite-react-typescript → claude-code}/CLAUDE.md +10 -3
  17. package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-architecture/SKILL.md +138 -61
  18. package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-debugging/SKILL.md +2 -2
  19. package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-media-api/SKILL.md +97 -10
  20. package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-multi-mode/SKILL.md +97 -4
  21. package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-requirements/SKILL.md +70 -5
  22. package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-store-sync/SKILL.md +4 -2
  23. package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-weather-api/SKILL.md +7 -6
  24. package/templates/claude-code/_claude/skills/tos-web-ui-design/SKILL.md +373 -0
  25. package/templates/vite-react-typescript/_gitignore +4 -2
  26. package/templates/vite-react-typescript/telemetry.config.json +2 -1
  27. package/templates/vite-react-typescript-web/_gitignore +32 -0
  28. package/templates/vite-react-typescript-web/assets/telemetryos-wordmark.svg +11 -0
  29. package/templates/vite-react-typescript-web/assets/tos-app.svg +12 -0
  30. package/templates/vite-react-typescript-web/index.html +15 -0
  31. package/templates/vite-react-typescript-web/package.json +24 -0
  32. package/templates/vite-react-typescript-web/src/App.tsx +25 -0
  33. package/templates/vite-react-typescript-web/src/hooks/store.ts +8 -0
  34. package/templates/vite-react-typescript-web/src/index.css +24 -0
  35. package/templates/vite-react-typescript-web/src/index.tsx +11 -0
  36. package/templates/vite-react-typescript-web/src/views/Render.css +67 -0
  37. package/templates/vite-react-typescript-web/src/views/Render.tsx +44 -0
  38. package/templates/vite-react-typescript-web/src/views/Settings.tsx +72 -0
  39. package/templates/vite-react-typescript-web/src/views/Web.css +105 -0
  40. package/templates/vite-react-typescript-web/src/views/Web.tsx +52 -0
  41. package/templates/vite-react-typescript-web/telemetry.config.json +16 -0
  42. package/templates/vite-react-typescript-web/tsconfig.json +19 -0
  43. package/templates/vite-react-typescript-web/vite.config.ts +18 -0
  44. package/templates/vite-react-typescript/_claude/skills/tos-render-design/SKILL.md +0 -624
  45. /package/templates/{vite-react-typescript → claude-code}/AGENTS.md +0 -0
  46. /package/templates/{vite-react-typescript → claude-code}/_claude/settings.local.json +0 -0
  47. /package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-proxy-fetch/SKILL.md +0 -0
  48. /package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-render-kiosk-design/SKILL.md +0 -0
  49. /package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-render-signage-design/SKILL.md +0 -0
  50. /package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-render-ui-design/SKILL.md +0 -0
  51. /package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-settings-ui/SKILL.md +0 -0
@@ -1,624 +0,0 @@
1
- ---
2
- name: tos-render-design
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
- ---
5
-
6
- # Render View Design
7
-
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
-
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
-
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.
43
-
44
- ### No User Interaction
45
-
46
- Assume **no mouse, keyboard, or touch input**:
47
-
48
- ```css
49
- /* WRONG - No one will hover on digital signage */
50
- .button:hover {
51
- background: blue;
52
- }
53
-
54
- /* WRONG - No one will focus elements */
55
- .input:focus {
56
- outline: 2px solid blue;
57
- }
58
- ```
59
-
60
- Avoid `:hover`, `:focus`, `:active`, and similar interaction pseudo-classes for display-only apps.
61
-
62
- ### No Scrolling
63
-
64
- Content **must fit the viewport**. There's no user to scroll:
65
-
66
- ```css
67
- /* WRONG - Creates scrollbar no one can use */
68
- .container {
69
- overflow-y: scroll;
70
- }
71
-
72
- /* WRONG - Content disappears off-screen */
73
- .content {
74
- height: 150vh;
75
- }
76
- ```
77
-
78
- ```css
79
- /* CORRECT - Content contained */
80
- .container {
81
- height: 100vh;
82
- overflow: hidden;
83
- }
84
- ```
85
-
86
- If content might overflow, truncate it or conditionally hide elements—never show a scrollbar.
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
-
228
- ## UI Scale Hooks
229
-
230
- **Applies to both digital signage and interactive kiosks.**
231
-
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:
233
-
234
- ### useUiScaleToSetRem(uiScale)
235
-
236
- Sets the document's root font-size based on viewport. **Call once in your Render view:**
237
-
238
- ```typescript
239
- import { useUiScaleToSetRem } from '@telemetryos/sdk/react'
240
- import { useUiScaleStoreState } from '../hooks/store'
241
-
242
- export function Render() {
243
- const [_isLoading, uiScale] = useUiScaleStoreState()
244
- useUiScaleToSetRem(uiScale)
245
-
246
- return <div className="content">...</div>
247
- }
248
- ```
249
-
250
- **How it works:**
251
- - At scale 1: `1rem` = 1% of viewport's longest dimension
252
- - At scale 2: `1rem` = 2% of viewport's longest dimension
253
- - A 2rem font occupies identical screen percentage on Full HD and 4K
254
-
255
- ### useUiAspectRatio()
256
-
257
- Returns current aspect ratio, updating on resize:
258
-
259
- ```typescript
260
- import { useUiAspectRatio } from '@telemetryos/sdk/react'
261
-
262
- export function Render() {
263
- const aspectRatio = useUiAspectRatio()
264
-
265
- // > 1 = landscape, < 1 = portrait, = 1 = square
266
- const isPortrait = aspectRatio < 1
267
-
268
- return (
269
- <div className={isPortrait ? 'portrait-layout' : 'landscape-layout'}>
270
- ...
271
- </div>
272
- )
273
- }
274
- ```
275
-
276
- ---
277
-
278
- ## Best Practices
279
-
280
- **Applies to both digital signage and interactive kiosks.**
281
-
282
- ### Use rem for Everything
283
-
284
- All sizing should use `rem` to scale with the UI scale setting:
285
-
286
- ```css
287
- /* CORRECT - Scales with viewport */
288
- .title {
289
- font-size: 4rem;
290
- margin-bottom: 1rem;
291
- }
292
-
293
- .card {
294
- padding: 2rem;
295
- border-radius: 0.5rem;
296
- }
297
- ```
298
-
299
- ```css
300
- /* WRONG - Fixed pixels don't scale */
301
- .title {
302
- font-size: 48px;
303
- margin-bottom: 12px;
304
- }
305
- ```
306
-
307
- ### Title Safe Zone
308
-
309
- The init project's `.render` class already applies ~3rem padding from screen edges (SMPTE ST 2046-1 standard for avoiding bezel cutoff). Keep this padding when building your layout.
310
-
311
- ### Minimum Text Size
312
-
313
- Text should be no smaller than ~2rem for comfortable viewing at typical distances (approximately 4% of screen height):
314
-
315
- ```css
316
- .body-text {
317
- font-size: 2rem; /* Minimum readable size */
318
- }
319
-
320
- .headline {
321
- font-size: 4rem;
322
- }
323
-
324
- .small-label {
325
- font-size: 1.5rem; /* Use sparingly */
326
- }
327
- ```
328
-
329
- ### Constrain Layouts
330
-
331
- The init project's `index.css` and `.render` class already set up the base layout with `overflow: hidden` and flexbox. When adding child elements, use `min-height: 0` or `min-width: 0` on flex children to allow them to shrink:
332
-
333
- ```css
334
- .my-content {
335
- flex: 1;
336
- min-height: 0; /* Allows flex children to shrink below content size */
337
- }
338
- ```
339
-
340
- ### Text Truncation
341
-
342
- When text might overflow, truncate gracefully:
343
-
344
- ```css
345
- .title {
346
- white-space: nowrap;
347
- overflow: hidden;
348
- text-overflow: ellipsis;
349
- }
350
-
351
- /* Multi-line truncation */
352
- .description {
353
- display: -webkit-box;
354
- -webkit-line-clamp: 3;
355
- -webkit-box-orient: vertical;
356
- overflow: hidden;
357
- }
358
- ```
359
-
360
- ### Adaptive Content
361
-
362
- Use `useUiAspectRatio()` to adapt layouts for portrait vs landscape:
363
-
364
- ```typescript
365
- function Dashboard() {
366
- const aspectRatio = useUiAspectRatio()
367
- const isPortrait = aspectRatio < 1
368
-
369
- return (
370
- <div className={`dashboard ${isPortrait ? 'dashboard--portrait' : ''}`}>
371
- <PrimaryContent />
372
- {/* Hide sidebar in portrait mode */}
373
- {!isPortrait && <Sidebar />}
374
- </div>
375
- )
376
- }
377
- ```
378
-
379
- ---
380
-
381
- ## Complete Examples
382
-
383
- ### Digital Signage Example (Display-Only)
384
-
385
- ```typescript
386
- // Render.tsx - Display-only dashboard
387
- import { useUiScaleToSetRem, useUiAspectRatio } from '@telemetryos/sdk/react'
388
- import { useUiScaleStoreState } from '../hooks/store'
389
- import './Render.css'
390
-
391
- export function Render() {
392
- const [isLoading, uiScale] = useUiScaleStoreState()
393
- const aspectRatio = useUiAspectRatio()
394
-
395
- useUiScaleToSetRem(uiScale)
396
-
397
- if (isLoading) return null
398
-
399
- const isPortrait = aspectRatio < 1
400
-
401
- return (
402
- <div className="render">
403
- <header className="render__header">
404
- <h1 className="render__title">Dashboard</h1>
405
- </header>
406
-
407
- <main className={`render__content ${isPortrait ? 'render__content--portrait' : ''}`}>
408
- <div className="render__primary">
409
- <MainDisplay />
410
- </div>
411
-
412
- {!isPortrait && (
413
- <aside className="render__sidebar">
414
- <SecondaryInfo />
415
- </aside>
416
- )}
417
- </main>
418
- </div>
419
- )
420
- }
421
- ```
422
-
423
- ```css
424
- /* Render.css - Display-only styles */
425
- .render__header {
426
- flex-shrink: 0;
427
- margin-bottom: 2rem;
428
- }
429
-
430
- .render__title {
431
- font-size: 4rem;
432
- margin: 0;
433
- white-space: nowrap;
434
- overflow: hidden;
435
- text-overflow: ellipsis;
436
- }
437
-
438
- .render__content {
439
- flex: 1;
440
- min-height: 0;
441
- display: flex;
442
- gap: 2rem;
443
- }
444
-
445
- .render__content--portrait {
446
- flex-direction: column;
447
- }
448
-
449
- .render__primary {
450
- flex: 1;
451
- min-width: 0;
452
- min-height: 0;
453
- }
454
-
455
- .render__sidebar {
456
- width: 25rem;
457
- flex-shrink: 0;
458
- }
459
- ```
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
-
567
- ## Store Hook for UI Scale
568
-
569
- Create a store hook to let admins adjust the UI scale:
570
-
571
- ```typescript
572
- // hooks/store.ts
573
- import { createUseInstanceStoreState } from '@telemetryos/sdk/react'
574
-
575
- export const useUiScaleStoreState = createUseInstanceStoreState<number>('ui-scale', 1)
576
- ```
577
-
578
- ```typescript
579
- // Settings.tsx - Add slider control
580
- import { SettingsSliderFrame, SettingsField, SettingsLabel } from '@telemetryos/sdk/react'
581
- import { useUiScaleStoreState } from '../hooks/store'
582
-
583
- export function Settings() {
584
- // Pass 0 debounce for instant slider updates
585
- const [isLoading, uiScale, setUiScale] = useUiScaleStoreState(0)
586
-
587
- return (
588
- <SettingsField>
589
- <SettingsLabel>UI Scale</SettingsLabel>
590
- <SettingsSliderFrame>
591
- <input
592
- type="range"
593
- min={1}
594
- max={3}
595
- step={0.01}
596
- disabled={isLoading}
597
- value={uiScale}
598
- onChange={(e) => setUiScale(parseFloat(e.target.value))}
599
- />
600
- <span>{uiScale}x</span>
601
- </SettingsSliderFrame>
602
- </SettingsField>
603
- )
604
- }
605
- ```
606
-
607
- ---
608
-
609
- ## Common Mistakes
610
-
611
- | Mistake | Problem | Fix |
612
- |---------|---------|-----|
613
- | Using `px` units | Won't scale across resolutions | Use `rem` everywhere |
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 |
616
- | Using `overflow: scroll` | No user to scroll | Use `overflow: hidden`, truncate content |
617
- | Fixed heights in `px` | Breaks on different aspect ratios | Use `vh`, `%`, or flex |
618
- | Forgetting `useUiScaleToSetRem()` | `rem` units won't scale properly | Call it once in Render view with uiScale |
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 |
623
- | Removing `.render` padding | Content cut off by bezels | Keep the ~3rem padding from init project |
624
- | Overriding `index.css` base styles | Breaks viewport scaling | Add new styles, don't modify base setup |