@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,384 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tos-render-kiosk-design
|
|
3
|
+
description: Design patterns for interactive TelemetryOS kiosks. Use AFTER reading tos-render-ui-design. Covers touch interaction, onClick handlers, idle timeout, touch targets, and navigation state.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Interactive Kiosk Design (Touch-Enabled)
|
|
7
|
+
|
|
8
|
+
For TelemetryOS render views where users **interact with the screen** via touch or click.
|
|
9
|
+
|
|
10
|
+
> **Prerequisites:** Read `tos-render-ui-design` first for foundation concepts (rem scaling, safe zones, text sizing, responsive layouts).
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## What is an Interactive Kiosk?
|
|
15
|
+
|
|
16
|
+
**Use for:** Wayfinding, directories, check-in systems, search interfaces
|
|
17
|
+
|
|
18
|
+
Interactive kiosks allow user interaction with these characteristics:
|
|
19
|
+
|
|
20
|
+
- Users can touch/click elements on screen
|
|
21
|
+
- onClick handlers for buttons, navigation, forms
|
|
22
|
+
- Idle timeout returns to home screen after inactivity
|
|
23
|
+
- Touch feedback with `:active` states (not `:hover`)
|
|
24
|
+
- Manages interaction state (current screen, navigation history)
|
|
25
|
+
|
|
26
|
+
**Examples:**
|
|
27
|
+
- Wayfinding kiosks in shopping malls
|
|
28
|
+
- Hotel check-in systems
|
|
29
|
+
- Building directory search
|
|
30
|
+
- Product catalog browsers
|
|
31
|
+
- Information lookup stations
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Interaction Patterns
|
|
36
|
+
|
|
37
|
+
### onClick Handlers
|
|
38
|
+
|
|
39
|
+
Add click handlers to interactive elements:
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
import { useState } from 'react'
|
|
43
|
+
|
|
44
|
+
function Render() {
|
|
45
|
+
const [screen, setScreen] = useState('home')
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="render">
|
|
49
|
+
{screen === 'home' && (
|
|
50
|
+
<button
|
|
51
|
+
className="kiosk-button"
|
|
52
|
+
onClick={() => setScreen('search')}
|
|
53
|
+
>
|
|
54
|
+
Search Directory
|
|
55
|
+
</button>
|
|
56
|
+
)}
|
|
57
|
+
{screen === 'search' && (
|
|
58
|
+
<SearchScreen onBack={() => setScreen('home')} />
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Key points:**
|
|
66
|
+
- Use standard React `onClick` handlers
|
|
67
|
+
- Manage screen/navigation state with `useState` or device store
|
|
68
|
+
- Provide clear navigation (back buttons, home button)
|
|
69
|
+
- Keep navigation shallow (avoid deep nested screens)
|
|
70
|
+
|
|
71
|
+
### Touch Feedback (:active states)
|
|
72
|
+
|
|
73
|
+
Use `:active` pseudo-class for touch feedback (NOT `:hover`):
|
|
74
|
+
|
|
75
|
+
```css
|
|
76
|
+
.kiosk-button {
|
|
77
|
+
padding: 2rem 4rem;
|
|
78
|
+
font-size: 3rem;
|
|
79
|
+
background: blue;
|
|
80
|
+
color: white;
|
|
81
|
+
border: none;
|
|
82
|
+
border-radius: 1rem;
|
|
83
|
+
transition: transform 0.1s, background 0.1s;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* Touch feedback - user sees visual response when tapping */
|
|
87
|
+
.kiosk-button:active {
|
|
88
|
+
transform: scale(0.95);
|
|
89
|
+
background: darkblue;
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Why :active instead of :hover?**
|
|
94
|
+
- Touch devices don't have hover
|
|
95
|
+
- `:active` triggers on touch/click
|
|
96
|
+
- Provides immediate visual feedback
|
|
97
|
+
- Shows user their tap was registered
|
|
98
|
+
|
|
99
|
+
**Feedback strategies:**
|
|
100
|
+
- Scale down slightly (0.95-0.98)
|
|
101
|
+
- Darken background color
|
|
102
|
+
- Change border/shadow
|
|
103
|
+
- Keep transition fast (0.1s)
|
|
104
|
+
|
|
105
|
+
### Touch Target Sizing
|
|
106
|
+
|
|
107
|
+
Make touch targets large enough to tap accurately:
|
|
108
|
+
|
|
109
|
+
```css
|
|
110
|
+
/* Minimum sizes for touch targets */
|
|
111
|
+
.kiosk-button {
|
|
112
|
+
min-width: 15rem; /* Large enough to tap */
|
|
113
|
+
min-height: 8rem;
|
|
114
|
+
padding: 2rem 4rem;
|
|
115
|
+
font-size: 3rem; /* Large, readable text */
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* Gap between interactive elements */
|
|
119
|
+
.button-group {
|
|
120
|
+
display: flex;
|
|
121
|
+
gap: 1rem; /* Minimum 1rem spacing */
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Touch target guidelines:**
|
|
126
|
+
|
|
127
|
+
| Element | Min Height | Min Padding | Font Size | Gap |
|
|
128
|
+
|---------|-----------|-------------|-----------|-----|
|
|
129
|
+
| Button | 8rem | 2rem | 3rem | 1rem |
|
|
130
|
+
| Nav item | 8rem | 2rem | 3rem | 1rem |
|
|
131
|
+
| Input field | 8rem | 2rem | 3rem | 1rem |
|
|
132
|
+
|
|
133
|
+
**Why large targets?**
|
|
134
|
+
- Users tap with fingers, not precise mouse cursor
|
|
135
|
+
- Prevents accidental taps on adjacent elements
|
|
136
|
+
- Reduces user frustration
|
|
137
|
+
- Improves accessibility
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## State Management
|
|
142
|
+
|
|
143
|
+
### Idle Timeout Pattern
|
|
144
|
+
|
|
145
|
+
Return to home screen after inactivity to prevent kiosks from staying on user's screen:
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
import { useState, useEffect } from 'react'
|
|
149
|
+
|
|
150
|
+
function Render() {
|
|
151
|
+
const [screen, setScreen] = useState('home')
|
|
152
|
+
const [lastInteraction, setLastInteraction] = useState(Date.now())
|
|
153
|
+
|
|
154
|
+
// Return to home after 30 seconds of inactivity
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
const timeout = setTimeout(() => {
|
|
157
|
+
const elapsed = Date.now() - lastInteraction
|
|
158
|
+
if (elapsed > 30000 && screen !== 'home') {
|
|
159
|
+
setScreen('home')
|
|
160
|
+
}
|
|
161
|
+
}, 30000)
|
|
162
|
+
|
|
163
|
+
return () => clearTimeout(timeout)
|
|
164
|
+
}, [lastInteraction, screen])
|
|
165
|
+
|
|
166
|
+
const handleInteraction = (newScreen: string) => {
|
|
167
|
+
setScreen(newScreen)
|
|
168
|
+
setLastInteraction(Date.now())
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<div className="render">
|
|
173
|
+
{screen === 'home' && (
|
|
174
|
+
<button onClick={() => handleInteraction('search')}>
|
|
175
|
+
Search
|
|
176
|
+
</button>
|
|
177
|
+
)}
|
|
178
|
+
</div>
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Key points:**
|
|
184
|
+
- Track last interaction timestamp
|
|
185
|
+
- Clear timeout on cleanup
|
|
186
|
+
- Configurable timeout duration (typically 15-60 seconds)
|
|
187
|
+
- Update timestamp on any interaction
|
|
188
|
+
- Only reset if not already on home screen
|
|
189
|
+
|
|
190
|
+
### Device Store for Navigation
|
|
191
|
+
|
|
192
|
+
Use device store to persist state across composition changes:
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
// hooks/store.ts
|
|
196
|
+
import { createUseDeviceStoreState } from '@telemetryos/sdk/react'
|
|
197
|
+
|
|
198
|
+
export const useKioskScreenState = createUseDeviceStoreState<string>(
|
|
199
|
+
'kiosk-screen',
|
|
200
|
+
'home'
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
// Render.tsx
|
|
204
|
+
import { useKioskScreenState } from '../hooks/store'
|
|
205
|
+
|
|
206
|
+
export function Render() {
|
|
207
|
+
const [_isLoading, screen, setScreen] = useKioskScreenState()
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<div className="render">
|
|
211
|
+
{screen === 'home' && <HomeScreen />}
|
|
212
|
+
{screen === 'search' && <SearchScreen />}
|
|
213
|
+
</div>
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**Why device store for kiosk navigation?**
|
|
219
|
+
- Persists state on the device (survives composition changes)
|
|
220
|
+
- Doesn't sync to other devices (screen state is device-local)
|
|
221
|
+
- Maintains kiosk state during playlist rotation
|
|
222
|
+
- Each physical device has independent navigation
|
|
223
|
+
|
|
224
|
+
**Don't use instance store** - That syncs across all devices. For kiosk navigation, use device store so each kiosk has independent state.
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Complete Example
|
|
229
|
+
|
|
230
|
+
### Interactive Kiosk with Navigation
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
// Render.tsx - Interactive kiosk with navigation
|
|
234
|
+
import { useState, useEffect } from 'react'
|
|
235
|
+
import { useUiScaleToSetRem } from '@telemetryos/sdk/react'
|
|
236
|
+
import { useUiScaleStoreState } from '../hooks/store'
|
|
237
|
+
import './Render.css'
|
|
238
|
+
|
|
239
|
+
export function Render() {
|
|
240
|
+
const [isLoading, uiScale] = useUiScaleStoreState()
|
|
241
|
+
const [screen, setScreen] = useState('home')
|
|
242
|
+
const [lastInteraction, setLastInteraction] = useState(Date.now())
|
|
243
|
+
|
|
244
|
+
useUiScaleToSetRem(uiScale)
|
|
245
|
+
|
|
246
|
+
// Idle timeout - return to home after 30 seconds
|
|
247
|
+
useEffect(() => {
|
|
248
|
+
const timeout = setTimeout(() => {
|
|
249
|
+
const elapsed = Date.now() - lastInteraction
|
|
250
|
+
if (elapsed > 30000 && screen !== 'home') {
|
|
251
|
+
setScreen('home')
|
|
252
|
+
}
|
|
253
|
+
}, 30000)
|
|
254
|
+
|
|
255
|
+
return () => clearTimeout(timeout)
|
|
256
|
+
}, [lastInteraction, screen])
|
|
257
|
+
|
|
258
|
+
const handleInteraction = (newScreen: string) => {
|
|
259
|
+
setScreen(newScreen)
|
|
260
|
+
setLastInteraction(Date.now())
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (isLoading) return null
|
|
264
|
+
|
|
265
|
+
return (
|
|
266
|
+
<div className="render">
|
|
267
|
+
{screen === 'home' && (
|
|
268
|
+
<div className="kiosk-home">
|
|
269
|
+
<h1 className="kiosk-home__title">Welcome</h1>
|
|
270
|
+
<button
|
|
271
|
+
className="kiosk-button"
|
|
272
|
+
onClick={() => handleInteraction('search')}
|
|
273
|
+
>
|
|
274
|
+
Search Directory
|
|
275
|
+
</button>
|
|
276
|
+
<button
|
|
277
|
+
className="kiosk-button"
|
|
278
|
+
onClick={() => handleInteraction('map')}
|
|
279
|
+
>
|
|
280
|
+
View Map
|
|
281
|
+
</button>
|
|
282
|
+
</div>
|
|
283
|
+
)}
|
|
284
|
+
|
|
285
|
+
{screen === 'search' && (
|
|
286
|
+
<SearchScreen onBack={() => handleInteraction('home')} />
|
|
287
|
+
)}
|
|
288
|
+
|
|
289
|
+
{screen === 'map' && (
|
|
290
|
+
<MapScreen onBack={() => handleInteraction('home')} />
|
|
291
|
+
)}
|
|
292
|
+
</div>
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
```css
|
|
298
|
+
/* Render.css - Interactive kiosk styles */
|
|
299
|
+
.kiosk-home {
|
|
300
|
+
display: flex;
|
|
301
|
+
flex-direction: column;
|
|
302
|
+
align-items: center;
|
|
303
|
+
justify-content: center;
|
|
304
|
+
gap: 3rem;
|
|
305
|
+
height: 100%;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.kiosk-home__title {
|
|
309
|
+
font-size: 6rem;
|
|
310
|
+
margin: 0;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/* Touch-friendly button with :active feedback */
|
|
314
|
+
.kiosk-button {
|
|
315
|
+
min-width: 20rem;
|
|
316
|
+
min-height: 8rem;
|
|
317
|
+
padding: 2rem 4rem;
|
|
318
|
+
font-size: 3rem;
|
|
319
|
+
background: #0066cc;
|
|
320
|
+
color: white;
|
|
321
|
+
border: none;
|
|
322
|
+
border-radius: 1rem;
|
|
323
|
+
cursor: pointer;
|
|
324
|
+
transition: transform 0.1s, background 0.1s;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/* Touch feedback - scales down when tapped */
|
|
328
|
+
.kiosk-button:active {
|
|
329
|
+
transform: scale(0.95);
|
|
330
|
+
background: #0052a3;
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## Touch Interaction Guidelines
|
|
337
|
+
|
|
338
|
+
### Visual Feedback Requirements
|
|
339
|
+
|
|
340
|
+
Every interactive element should provide visual feedback:
|
|
341
|
+
|
|
342
|
+
✅ Use `:active` state (not `:hover`)
|
|
343
|
+
✅ Visual change must be immediate (< 100ms)
|
|
344
|
+
✅ Change should be obvious (scale, color, shadow)
|
|
345
|
+
✅ Feedback on tap down, not just tap up
|
|
346
|
+
|
|
347
|
+
### Touch Target Checklist
|
|
348
|
+
|
|
349
|
+
- [ ] All buttons are at least 8rem height
|
|
350
|
+
- [ ] Padding is at least 2rem
|
|
351
|
+
- [ ] Font size is at least 3rem
|
|
352
|
+
- [ ] Gap between buttons is at least 1rem
|
|
353
|
+
- [ ] Touch targets don't overlap
|
|
354
|
+
- [ ] `:active` states provide visual feedback
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
## Common Mistakes
|
|
359
|
+
|
|
360
|
+
| Mistake | Problem | Fix |
|
|
361
|
+
|---------|---------|-----|
|
|
362
|
+
| Using `:hover` on interactive kiosks | No hover on touch devices | Use `:active` instead for touch feedback |
|
|
363
|
+
| Small touch targets on kiosks | Hard to tap accurately | Minimum 8rem height, 2rem padding, 3rem font |
|
|
364
|
+
| No idle timeout on kiosks | Kiosk stays on user's screen | Add useEffect timeout logic |
|
|
365
|
+
| No touch feedback on kiosks | User unsure if tap registered | Add `:active` state animations |
|
|
366
|
+
| Using instance store for navigation | State syncs to other devices | Use device store for local navigation |
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## Testing Your Kiosk
|
|
371
|
+
|
|
372
|
+
✅ **Test on touch devices** - Not just mouse, verify touch works
|
|
373
|
+
✅ **Verify idle timeout** - Confirm it returns to home after inactivity
|
|
374
|
+
✅ **Check touch targets** - Ensure all buttons are large enough
|
|
375
|
+
✅ **Ensure :active feedback** - Visual response should be obvious
|
|
376
|
+
✅ **Test navigation flow** - Complete user journeys should work
|
|
377
|
+
✅ **Verify state persistence** - State should survive composition rotation
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## See Also
|
|
382
|
+
|
|
383
|
+
- `tos-render-ui-design` - Foundation concepts for all render views (UI scaling, rem usage, responsive layouts)
|
|
384
|
+
- `tos-render-signage-design` - For display-only patterns without interaction
|