@telemetryos/cli 1.10.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 +11 -0
- package/dist/commands/auth.js +6 -13
- package/dist/commands/init.js +64 -49
- package/dist/commands/publish.d.ts +20 -0
- package/dist/commands/publish.js +68 -38
- package/dist/services/api-client.js +1 -1
- package/dist/services/cli-config.d.ts +9 -5
- package/dist/services/cli-config.js +28 -6
- package/package.json +2 -2
- package/templates/vite-react-typescript/CLAUDE.md +9 -2
- 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-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 +72 -29
- package/templates/vite-react-typescript/_claude/skills/tos-store-sync/SKILL.md +96 -5
- package/templates/vite-react-typescript/index.html +1 -1
|
@@ -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-
|
|
49
|
-
- "one
|
|
50
|
-
- "
|
|
51
|
-
- "
|
|
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
|
|
64
|
-
> "Got it -
|
|
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-
|
|
308
|
+
## Phase 5: Multi-Mode Design (If Applicable)
|
|
308
309
|
|
|
309
|
-
**Only if Phase 1 identified
|
|
310
|
+
**Only if Phase 1 identified a multi-mode app.**
|
|
310
311
|
|
|
311
|
-
###
|
|
312
|
+
### Step 1: Identify the Modes
|
|
312
313
|
|
|
313
|
-
|
|
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
|
-
|
|
318
|
-
-
|
|
319
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
329
|
-
|
|
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
|
-
//
|
|
332
|
-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
//
|
|
48
|
-
const [,
|
|
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>
|