@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,325 @@
1
+ ---
2
+ name: tos-render-ui-design
3
+ description: Foundation for TelemetryOS Render views. MUST use FIRST before tos-render-signage-design or tos-render-kiosk-design. Covers UI scaling, rem usage, responsive layouts, and best practices for all display types.
4
+ ---
5
+
6
+ # Render View UI Design (Foundation)
7
+
8
+ This skill covers the foundational UI design patterns for ALL TelemetryOS Render views, whether building digital signage (display-only) or interactive kiosks (touch-enabled).
9
+
10
+ > **Note:** Always read this skill FIRST, then read either `tos-render-signage-design` (display-only) or `tos-render-kiosk-design` (interactive) depending on your use case.
11
+
12
+ > **Base styles:** The init project already provides base infrastructural styles in `index.css` (viewport scaling, box-sizing) and `Render.css` (`.render` class with padding, overflow, flexbox). Build on these—don't override them. But feel free to build a new visual theme.
13
+
14
+
15
+ ## Design Thinking
16
+
17
+ Before coding, understand the context and commit to a BOLD aesthetic direction:
18
+ - **Purpose**: What problem does this interface solve? Who uses it?
19
+ - **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
20
+ - **Constraints**: Technical requirements (framework, performance, accessibility).
21
+ - **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
22
+
23
+ **CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
24
+
25
+ Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is:
26
+ - Production-grade and functional
27
+ - Visually striking and memorable
28
+ - Cohesive with a clear aesthetic point-of-view
29
+ - Meticulously refined in every detail
30
+
31
+ ## Frontend Aesthetics Guidelines
32
+
33
+ Focus on:
34
+ - **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font.
35
+ - **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
36
+ - **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise.
37
+ - **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density.
38
+ - **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays.
39
+
40
+ NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character.
41
+
42
+ Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations.
43
+
44
+ **IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well.
45
+
46
+ Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.
47
+
48
+ ---
49
+
50
+ ## Quick Reference
51
+
52
+ | Concept | Key Points | Where Used |
53
+ |---------|-----------|------------|
54
+ | UI Scaling | Call `useUiScaleToSetRem(uiScale)` once in Render | All render views |
55
+ | rem Units | All sizing in rem (not px) | All styles |
56
+ | Title Safe Zone | Keep ~3rem padding from edges | All layouts |
57
+ | Text Size | Minimum 2rem for body, 4rem for headlines | All text |
58
+ | Flex Layouts | Use `min-height: 0` on flex children | All containers |
59
+ | Text Overflow | Truncate with ellipsis or line-clamp | All text that might overflow |
60
+ | Responsive | Use `useUiAspectRatio()` for portrait/landscape | Adaptive layouts |
61
+ | Outside-In Layout | Divide viewport space first, components fill allocations (see signage skill) | Signage layouts |
62
+
63
+ **Next steps after reading:**
64
+ - Display-only content? → Read `tos-render-signage-design`
65
+ - Interactive kiosk? → Read `tos-render-kiosk-design`
66
+
67
+ ---
68
+
69
+ ## UI Scale System
70
+
71
+ Displays range from tablets to 8K video walls. Standard CSS pixels create inconsistent sizing. The SDK provides hooks that redefine `rem` as viewport-relative:
72
+
73
+ ### useUiScaleToSetRem(uiScale)
74
+
75
+ Sets the document's root font-size based on viewport. **Call once in your Render view:**
76
+
77
+ ```typescript
78
+ import { useUiScaleToSetRem } from '@telemetryos/sdk/react'
79
+ import { useUiScaleStoreState } from '../hooks/store'
80
+
81
+ export function Render() {
82
+ const [_isLoading, uiScale] = useUiScaleStoreState()
83
+ useUiScaleToSetRem(uiScale)
84
+
85
+ return <div className="content">...</div>
86
+ }
87
+ ```
88
+
89
+ **How it works:**
90
+ - At scale 1: `1rem` = 1% of viewport's longest dimension
91
+ - At scale 2: `1rem` = 2% of viewport's longest dimension
92
+ - A 2rem font occupies identical screen percentage on Full HD and 4K
93
+
94
+ ### useUiAspectRatio()
95
+
96
+ Returns current aspect ratio, updating on resize:
97
+
98
+ ```typescript
99
+ import { useUiAspectRatio } from '@telemetryos/sdk/react'
100
+
101
+ export function Render() {
102
+ const aspectRatio = useUiAspectRatio()
103
+
104
+ // > 1 = landscape, < 1 = portrait, = 1 = square
105
+ const isPortrait = aspectRatio < 1
106
+
107
+ return (
108
+ <div className={isPortrait ? 'portrait-layout' : 'landscape-layout'}>
109
+ ...
110
+ </div>
111
+ )
112
+ }
113
+ ```
114
+
115
+ ---
116
+
117
+ ## Layout Fundamentals
118
+
119
+ ### Use rem for Everything
120
+
121
+ All sizing should use `rem` to scale with the UI scale setting:
122
+
123
+ ```css
124
+ /* CORRECT - Scales with viewport */
125
+ .title {
126
+ font-size: 4rem;
127
+ margin-bottom: 1rem;
128
+ }
129
+
130
+ .card {
131
+ padding: 2rem;
132
+ border-radius: 0.5rem;
133
+ }
134
+ ```
135
+
136
+ ```css
137
+ /* WRONG - Fixed pixels don't scale */
138
+ .title {
139
+ font-size: 48px;
140
+ margin-bottom: 12px;
141
+ }
142
+ ```
143
+
144
+ ### Title Safe Zone
145
+
146
+ 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.
147
+
148
+ ### Constrain Layouts
149
+
150
+ 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:
151
+
152
+ ```css
153
+ .my-content {
154
+ flex: 1;
155
+ min-height: 0; /* Allows flex children to shrink below content size */
156
+ }
157
+ ```
158
+
159
+ ### Text Truncation
160
+
161
+ When text might overflow, truncate gracefully:
162
+
163
+ ```css
164
+ /* Single line truncation */
165
+ .title {
166
+ white-space: nowrap;
167
+ overflow: hidden;
168
+ text-overflow: ellipsis;
169
+ }
170
+
171
+ /* Multi-line truncation */
172
+ .description {
173
+ display: -webkit-box;
174
+ -webkit-line-clamp: 3;
175
+ -webkit-box-orient: vertical;
176
+ overflow: hidden;
177
+ }
178
+ ```
179
+
180
+ ---
181
+
182
+ ## Typography
183
+
184
+ ### Minimum Text Size
185
+
186
+ Text should be no smaller than ~2rem for comfortable viewing at typical distances (approximately 4% of screen height):
187
+
188
+ ```css
189
+ .body-text {
190
+ font-size: 2rem; /* Minimum readable size */
191
+ }
192
+
193
+ .headline {
194
+ font-size: 4rem;
195
+ }
196
+
197
+ .small-label {
198
+ font-size: 1.5rem; /* Use sparingly */
199
+ }
200
+ ```
201
+
202
+ **Why 2rem minimum?**
203
+ - Content is viewed from a distance (not handheld)
204
+ - Smaller text becomes unreadable
205
+ - 2rem ≈ 4% of screen height at scale 1
206
+
207
+ ---
208
+
209
+ ## Responsive Design
210
+
211
+ ### Adaptive Content for Orientation
212
+
213
+ Use `useUiAspectRatio()` to adapt layouts for portrait vs landscape:
214
+
215
+ ```typescript
216
+ function Dashboard() {
217
+ const aspectRatio = useUiAspectRatio()
218
+ const isPortrait = aspectRatio < 1
219
+
220
+ return (
221
+ <div className={`dashboard ${isPortrait ? 'dashboard--portrait' : ''}`}>
222
+ <PrimaryContent />
223
+ {/* Hide sidebar in portrait mode */}
224
+ {!isPortrait && <Sidebar />}
225
+ </div>
226
+ )
227
+ }
228
+ ```
229
+
230
+ ```css
231
+ .dashboard {
232
+ display: flex;
233
+ gap: 2rem;
234
+ }
235
+
236
+ .dashboard--portrait {
237
+ flex-direction: column;
238
+ }
239
+ ```
240
+
241
+ ---
242
+
243
+ ## Store Hook for UI Scale
244
+
245
+ Create a store hook to let admins adjust the UI scale in Settings:
246
+
247
+ ```typescript
248
+ // hooks/store.ts
249
+ import { createUseInstanceStoreState } from '@telemetryos/sdk/react'
250
+
251
+ export const useUiScaleStoreState = createUseInstanceStoreState<number>('ui-scale', 1)
252
+ ```
253
+
254
+ ```typescript
255
+ // Settings.tsx - Add slider control
256
+ import { SettingsSliderFrame, SettingsField, SettingsLabel } from '@telemetryos/sdk/react'
257
+ import { useUiScaleStoreState } from '../hooks/store'
258
+
259
+ export function Settings() {
260
+ // Pass 0 debounce for instant slider updates
261
+ const [isLoading, uiScale, setUiScale] = useUiScaleStoreState(0)
262
+
263
+ return (
264
+ <SettingsField>
265
+ <SettingsLabel>UI Scale</SettingsLabel>
266
+ <SettingsSliderFrame>
267
+ <input
268
+ type="range"
269
+ min={1}
270
+ max={3}
271
+ step={0.01}
272
+ disabled={isLoading}
273
+ value={uiScale}
274
+ onChange={(e) => setUiScale(parseFloat(e.target.value))}
275
+ />
276
+ <span>{uiScale}x</span>
277
+ </SettingsSliderFrame>
278
+ </SettingsField>
279
+ )
280
+ }
281
+ ```
282
+
283
+ ---
284
+
285
+ ## Best Practices Summary
286
+
287
+ ✅ **Use rem everywhere** - All sizing should use rem units, not px
288
+ ✅ **Call useUiScaleToSetRem() once** - In Render view with uiScale from store
289
+ ✅ **Respect title safe zone** - Keep the ~3rem padding from init project
290
+ ✅ **Keep text readable** - Minimum 2rem for body, 4rem for headlines
291
+ ✅ **Constrain flex layouts** - Use `min-height: 0` on flex children
292
+ ✅ **Truncate overflow** - Use ellipsis or line-clamp for text that might overflow
293
+ ✅ **Adapt to orientation** - Use `useUiAspectRatio()` for portrait/landscape layouts
294
+ ✅ **Don't override base styles** - Build on index.css, don't replace it
295
+
296
+ ---
297
+
298
+ ## Common Mistakes
299
+
300
+ | Mistake | Problem | Fix |
301
+ |---------|---------|-----|
302
+ | Using `px` units | Won't scale across resolutions | Use `rem` everywhere |
303
+ | Fixed heights in `px` | Breaks on different aspect ratios | Use `vh`, `%`, or flex |
304
+ | Forgetting `useUiScaleToSetRem()` | `rem` units won't scale properly | Call it once in Render view with uiScale |
305
+ | Text below 2rem | Unreadable from viewing distance | Minimum 2rem for body text |
306
+ | Removing `.render` padding | Content cut off by bezels | Keep the ~3rem padding from init project |
307
+ | Overriding `index.css` base styles | Breaks viewport scaling | Add new styles, don't modify base setup |
308
+
309
+ ---
310
+
311
+ ## Next Steps
312
+
313
+ After mastering these foundational concepts, read the appropriate specialized skill:
314
+
315
+ ### Building Display-Only Content (Digital Signage)?
316
+ → Read `tos-render-signage-design`
317
+ - No user interaction patterns
318
+ - No scrolling constraints
319
+ - Auto-rotation considerations
320
+
321
+ ### Building Interactive Content (Kiosk)?
322
+ → Read `tos-render-kiosk-design`
323
+ - Touch interaction patterns
324
+ - Idle timeout behavior
325
+ - Navigation state management
@@ -45,10 +45,11 @@ Wait for their answer. Their response will tell you:
45
45
  - ANY mention of user input/interaction
46
46
  - **Implementation:** Render view WITH onClick handlers
47
47
 
48
- **Multi-App Indicators**:
49
- - "one app shows X and another shows Y"
50
- - "main display and control panel"
51
- - "shared information between apps"
48
+ **Multi-Mode Indicators** (same app, different views per device):
49
+ - "one screen shows X while another shows Y"
50
+ - "kiosk and display" or "control panel and viewer"
51
+ - "different devices need different views of the same data"
52
+ - data organized by location, department, topic, or similar grouping
52
53
 
53
54
  ### Confirm Interaction Model
54
55
 
@@ -60,8 +61,8 @@ Before proceeding to Phase 2, explicitly state your understanding:
60
61
  **For Interactive Kiosk:**
61
62
  > "Got it - we're building an interactive kiosk. Users will be able to touch/click elements on the screen. We'll use @telemetryos/sdk with onClick handlers in the render view. Does that match what you have in mind?"
62
63
 
63
- **For Multi-App System:**
64
- > "Got it - you're describing TWO separate apps: [App A description] and [App B description]. They'll communicate via shared store namespaces. We'll need to gather requirements for each app. Does that match what you have in mind?"
64
+ **For Multi-Mode App:**
65
+ > "Got it — this is a multi-mode app. Different devices will show different views: [Mode A description] and [Mode B description]. They'll share data scoped to a [entity]. Let me gather requirements for each mode."
65
66
 
66
67
  Wait for confirmation before proceeding.
67
68
 
@@ -304,35 +305,64 @@ For each setting identified, record:
304
305
 
305
306
  ---
306
307
 
307
- ## Phase 5: Multi-App Communication (If Applicable)
308
+ ## Phase 5: Multi-Mode Design (If Applicable)
308
309
 
309
- **Only if Phase 1 identified multiple apps.**
310
+ **Only if Phase 1 identified a multi-mode app.**
310
311
 
311
- ### Communication Pattern Questions
312
+ ### Step 1: Identify the Modes
312
313
 
313
- **Data Sharing:**
314
- - "How should [App A] and [App B] share data?"
315
- - Suggest: "Typically, apps share data via store namespaces. App A writes to a shared namespace, App B reads from it."
314
+ Confirm the distinct Render views:
316
315
 
317
- **Namespace Design:**
318
- - "What data needs to be shared?"
319
- - Design namespace structure: `store().shared('namespace-name')`
316
+ - "Let's name the modes. You mentioned [X] and [Y] — should we call them '[mode-a]' and '[mode-b]'?"
317
+ - Each mode becomes a value in the mode union type (e.g., `'kiosk' | 'display'`)
318
+ - Ask what each mode shows and whether it reads or writes shared data
320
319
 
321
- **Update Pattern:**
322
- - "When should App B see updates from App A - immediately (subscribe) or on-demand?"
320
+ ### Step 2: Discover the Top-Level Entity
323
321
 
324
- ### Shared Store Pattern
322
+ **The developer will almost never volunteer this on their own.** You MUST actively discover or propose the organizing entity that scopes all shared data.
323
+
324
+ Ask: "What organizes the data these modes share? For example, is this per-location, per-department, per-topic, per-event?"
325
+
326
+ If the developer doesn't have a clear answer, **propose one** based on the app domain:
327
+
328
+ | App Domain | Likely Entity |
329
+ |------------|---------------|
330
+ | Queue / service | location, branch |
331
+ | Content / editorial | topic, channel |
332
+ | Event / scheduling | event, venue |
333
+ | Organizational | department, team |
334
+ | Retail / menu | store, menu |
335
+
336
+ Explain WHY: "This lets you run the same app at multiple [locations] — each with its own independent data — without creating separate app instances. An admin just selects which [location] each device belongs to."
337
+
338
+ Confirm the entity name before proceeding.
339
+
340
+ ### Step 3: Map Shared Data
341
+
342
+ For each content element from Phase 2, determine if it's:
343
+ - **Per-device** (instance scope) — mode selection, entity selection, UI scale
344
+ - **Account-wide** (application scope) — entity list, API keys
345
+ - **Per-entity** (dynamic namespace scope) — the actual shared data between modes
346
+
347
+ ### Step 4: Mode-Specific Settings
348
+
349
+ Ask: "Are there any settings that only apply to one mode? For example, audio chime only on the display, or touch feedback only on the kiosk?"
350
+
351
+ ### Reference: Multi-Mode Store Pattern
325
352
 
326
- **Pattern: Shared Store Namespace**
327
353
  ```typescript
328
- // App A writes:
329
- await store().shared('weather-data').set('current', data)
354
+ // Instance scope — per device
355
+ const useModeStoreState = createUseInstanceStoreState<'kiosk' | 'display'>('mode', 'kiosk')
356
+ const useSelectedLocationStoreState = createUseInstanceStoreState<string>('selected-location', 'Location A')
330
357
 
331
- // App B subscribes:
332
- store().shared('weather-data').subscribe('current', callback)
358
+ // Application scope — entity management
359
+ const useLocationsStoreState = createUseApplicationStoreState<string[]>('locations', ['Location A'])
360
+
361
+ // Dynamic namespace scope — shared between modes, scoped to entity
362
+ const useQueuesStoreState = createUseDynamicNamespaceStoreState<Queue[]>('queues', [])
333
363
  ```
334
364
 
335
- Best for: Simple data sharing, pub/sub patterns between apps
365
+ See `tos-multi-mode` skill for the complete implementation pattern.
336
366
 
337
367
  ---
338
368
 
@@ -344,10 +374,10 @@ After gathering all requirements, provide a structured summary:
344
374
  # [App Name] Requirements
345
375
 
346
376
  ## Interaction Model
347
- **[Display-Only | Interactive | Multi-App System]**
377
+ **[Display-Only | Interactive | Multi-Mode]**
348
378
  - SDK: `@telemetryos/sdk`
349
379
  - Mount Points: `render` (display), `settings` (config UI)
350
- - Interaction: [Display-only with subscriptions | Interactive with onClick handlers]
380
+ - Interaction: [Display-only with subscriptions | Interactive with onClick handlers | Multi-mode with entity-scoped data]
351
381
 
352
382
  ## Vision
353
383
  [One sentence description]
@@ -381,6 +411,13 @@ After gathering all requirements, provide a structured summary:
381
411
  | Company logo | Media Library | media().getById() | Static |
382
412
  | Stock prices | External API | proxy().fetch('...') | 30s |
383
413
 
414
+ ## Multi-Mode Design (if applicable)
415
+
416
+ **Modes:** [mode-a] (description), [mode-b] (description)
417
+ **Top-Level Entity:** [entity name] (e.g., location, topic)
418
+ **Entity-Scoped Data:** [list shared data keys that use dynamic namespace]
419
+ **Mode-Specific Settings:** [list any settings that only apply to one mode]
420
+
384
421
  ## Store Keys (Settings Configuration)
385
422
 
386
423
  | Key | Category | Scope | Type | Default | UI Component | Required? |
@@ -398,7 +435,8 @@ After gathering all requirements, provide a structured summary:
398
435
  1. **Store Hooks** (hooks/store.ts)
399
436
  - Create instance-scoped hooks for [list keys]
400
437
  - Create application-scoped hooks for [list keys]
401
- - [If multi-app] Create shared namespace hooks
438
+ - [If multi-mode] Create dynamic namespace hooks for entity-scoped data
439
+ - [If multi-mode] Create namespace helper function
402
440
 
403
441
  2. **Settings UI** (views/Settings.tsx)
404
442
  - [For Digital Signage] UI Scale slider (instance scope, 1-3 range, 0.01 step)
@@ -452,7 +490,8 @@ If you need to clarify layout:
452
490
  3. **Validate assumptions** - Confirm important decisions before moving on
453
491
  4. **Be conversational** - This is a dialogue, not a form to fill out
454
492
  5. **Skip irrelevant questions** - If they already told you something, don't ask again
455
- 6. **Recognize multi-app patterns** - Watch for indicators of multiple communicating apps
493
+ 6. **Recognize multi-mode patterns** Watch for indicators of different views per device
494
+ 7. **Discover the top-level entity** — For multi-mode apps, the developer won't volunteer this. You must ask or propose one
456
495
 
457
496
  ### Common Patterns to Recognize
458
497
 
@@ -460,6 +499,7 @@ If you need to clarify layout:
460
499
  - **Menu Board** → Display-only, media library, scheduled updates
461
500
  - **Wayfinding Kiosk** → Interactive, touch navigation, search functionality
462
501
  - **Data Dashboard** → Display-only, external API, refresh interval
502
+ - **Queue Manager** → Multi-mode (kiosk + display), entity: location, shared queue data
463
503
 
464
504
  ### What to Infer vs What to Ask
465
505
 
@@ -481,9 +521,12 @@ If you need to clarify layout:
481
521
 
482
522
  After gathering requirements, use these skills to implement:
483
523
 
524
+ - **`tos-multi-mode`** - Multi-mode architecture patterns (if building multi-mode app — read first)
484
525
  - **`tos-store-sync`** - Create store hooks from the Store Keys table
485
526
  - **`tos-settings-ui`** - Build the Settings UI components
486
- - **`tos-render-design`** - Design the Render view layout
527
+ - **`tos-render-ui-design`** - Design the Render view layout (foundation - always read first)
528
+ - **`tos-render-signage-design`** - Display-only render patterns (if building digital signage)
529
+ - **`tos-render-kiosk-design`** - Interactive render patterns (if building kiosk)
487
530
  - **`tos-proxy-fetch`** - Implement external API calls (if needed)
488
531
  - **`tos-weather-api`** - Integrate weather data (if needed)
489
532
  - **`tos-media-api`** - Access media library (if needed)
@@ -26,6 +26,7 @@ import {
26
26
  createUseInstanceStoreState,
27
27
  createUseApplicationStoreState,
28
28
  createUseDeviceStoreState,
29
+ createUseDynamicNamespaceStoreState,
29
30
  } from '@telemetryos/sdk/react'
30
31
 
31
32
  // Localization (instance scope)
@@ -39,16 +40,33 @@ export const useApiKeyStoreState = createUseApplicationStoreState<string>('apiKe
39
40
  export const useBrightnessStoreState = createUseDeviceStoreState<number>('brightness', 100)
40
41
  ```
41
42
 
42
- **Usage:**
43
+ **Usage (Always Handle isLoading):**
44
+
45
+ ⚠️ **Important:** The `isLoading` boolean is `true` until the store returns the first value. During this time, the `value` is either the `initialValue` or potentially `undefined`. Always check `isLoading` before relying on the value.
46
+
43
47
  ```typescript
44
48
  // Instance-scoped (Settings + Render) - 250ms debounce for text input
45
- const [, city, setCity] = useCityStoreState(250)
49
+ const [isLoading, city, setCity] = useCityStoreState(250)
46
50
 
47
- // Application-scoped (shared across all instances) - 250ms debounce for text input
48
- const [, apiKey, setApiKey] = useApiKeyStoreState(250)
51
+ // BAD: Ignoring isLoading can cause bugs - value may not be from store yet!
52
+ // const [, city, setCity] = useCityStoreState(250)
53
+ // return <div>{city}</div> // May show default value instead of real data
54
+
55
+ // ✅ GOOD Pattern 1: Check isLoading before rendering
56
+ if (isLoading) return <Spinner />
57
+ return <div>Weather for {city}</div>
58
+
59
+ // ✅ GOOD Pattern 2: Use fallback value with nullish coalescing
60
+ return <div>Weather for {city ?? "Unknown City"}</div>
61
+
62
+ // Application-scoped (shared across all instances) - 250ms debounce
63
+ const [isLoading, apiKey, setApiKey] = useApiKeyStoreState(250)
64
+ if (isLoading) return <LoadingState />
65
+ return <APIClient apiKey={apiKey} />
49
66
 
50
67
  // Device-scoped (Render only - NOT available in Settings) - 5ms for slider
51
- const [, brightness, setBrightness] = useBrightnessStoreState(5)
68
+ const [isLoading, brightness, setBrightness] = useBrightnessStoreState(5)
69
+ const safeValue = brightness ?? 100 // Fallback to default if still loading
52
70
  ```
53
71
 
54
72
  ## Quick Pattern
@@ -179,6 +197,73 @@ const [isLoading, temp] = useTempStoreState()
179
197
  - Event broadcasting
180
198
  - Coordinated updates
181
199
 
200
+ ### createUseDynamicNamespaceStoreState
201
+
202
+ Like shared store, but the namespace is passed at **call time** instead of definition time. Use when the namespace is dynamic — derived from other data at runtime (e.g., a user-selected location, a route parameter, or any runtime key).
203
+
204
+ > **Building a multi-mode app?** For the complete architecture pattern using dynamic namespaces with mode switching and entity management, see the `tos-multi-mode` skill.
205
+
206
+ ```typescript
207
+ import { createUseDynamicNamespaceStoreState } from '@telemetryos/sdk/react'
208
+
209
+ // Define with key + default only (no namespace yet)
210
+ export const useItemsStoreState = createUseDynamicNamespaceStoreState<Item[]>('items', [])
211
+
212
+ // Usage - pass namespace AND debounceDelay at call time
213
+ const [isLoading, items, setItems] = useItemsStoreState(namespace, 250)
214
+ ```
215
+
216
+ **Key difference from `createUseSharedStoreState`:**
217
+
218
+ | | `createUseSharedStoreState` | `createUseDynamicNamespaceStoreState` |
219
+ |---|---|---|
220
+ | **Namespace** | Fixed at hook definition | Passed at call time |
221
+ | **Definition** | `(key, default, namespace)` | `(key, default)` |
222
+ | **Call signature** | `(debounceDelay?)` | `(namespace, debounceDelay)` |
223
+ | **Use when** | Namespace is always the same | Namespace depends on runtime data |
224
+
225
+ **Use cases:**
226
+ - Multi-location apps (same data structure per location)
227
+ - User-selected scopes or categories
228
+ - Data partitioned by a runtime key (route param, selected item, etc.)
229
+
230
+ **Pattern: Namespace helper + instance-scoped selector**
231
+
232
+ A common pattern is combining an instance-scoped hook (to select which namespace) with dynamic namespace hooks (to access that namespace's data):
233
+
234
+ ```typescript
235
+ // hooks/store.ts
236
+ import {
237
+ createUseInstanceStoreState,
238
+ createUseDynamicNamespaceStoreState,
239
+ } from '@telemetryos/sdk/react'
240
+
241
+ // Instance-scoped: which location this device is assigned to
242
+ export const useSelectedLocationStoreState = createUseInstanceStoreState<string>('selected-location', 'Location A')
243
+
244
+ // Dynamic namespace: per-location data
245
+ export const useQueuesStoreState = createUseDynamicNamespaceStoreState<Queue[]>('queues', [])
246
+ export const useCountersStoreState = createUseDynamicNamespaceStoreState<Counter[]>('counters', [])
247
+
248
+ // Helper to build namespace from location
249
+ export function locationNamespace(location: string): string {
250
+ return `my-app-${location}`
251
+ }
252
+ ```
253
+
254
+ ```typescript
255
+ // In a component
256
+ const [isLoadingLocation, selectedLocation] = useSelectedLocationStoreState()
257
+ const ns = locationNamespace(selectedLocation)
258
+
259
+ const [isLoadingQueues, queues, setQueues] = useQueuesStoreState(ns, 250)
260
+ const [isLoadingCounters, counters, setCounters] = useCountersStoreState(ns, 250)
261
+
262
+ if (isLoadingLocation || isLoadingQueues || isLoadingCounters) return <Spinner />
263
+ ```
264
+
265
+ When `selectedLocation` changes, the namespace changes, and the hooks automatically re-subscribe to the new namespace's data.
266
+
182
267
  ## Return Value
183
268
 
184
269
  All hooks return a tuple:
@@ -199,6 +284,12 @@ The default debounce delay is 0ms (immediate updates). Pass a value for debounce
199
284
  const [isLoading, city, setCity] = useCityStoreState(250) // 250ms debounce for text inputs
200
285
  ```
201
286
 
287
+ **Note:** `createUseDynamicNamespaceStoreState` hooks take `(namespace, debounceDelay)` — namespace is required at call time:
288
+
289
+ ```typescript
290
+ const [isLoading, items, setItems] = useItemsStoreState(namespace, 250)
291
+ ```
292
+
202
293
  ## TypeScript Types
203
294
 
204
295
  ### Primitive Types
@@ -7,7 +7,7 @@
7
7
  <link rel="preconnect" href="https://fonts.googleapis.com">
8
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
9
  <link href="https://fonts.googleapis.com/css2?family=Exo:ital,wght@0,100..900;1,100..900&family=Rubik:ital,wght@0,300..900;1,300..900&display=swap" rel="stylesheet">
10
- <script type="module" src="src/index.tsx"></script>
10
+ <script type="module" src="/src/index.tsx"></script>
11
11
  </head>
12
12
  <body>
13
13
  <div id="app"></div>