@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.
- package/CHANGELOG.md +37 -0
- package/dist/commands/claude-code.d.ts +2 -0
- package/dist/commands/claude-code.js +29 -0
- package/dist/commands/init.js +22 -9
- package/dist/index.js +2 -0
- package/dist/services/create-project.d.ts +13 -0
- package/dist/services/create-project.js +188 -0
- package/dist/services/project-config.d.ts +3 -0
- package/dist/services/project-config.js +3 -0
- package/dist/services/run-server.js +186 -60
- package/dist/utils/ansi.d.ts +1 -0
- package/dist/utils/ansi.js +1 -0
- package/dist/utils/template.d.ts +2 -0
- package/dist/utils/template.js +30 -0
- package/package.json +4 -4
- package/templates/{vite-react-typescript → claude-code}/CLAUDE.md +10 -3
- package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-architecture/SKILL.md +138 -61
- package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-debugging/SKILL.md +2 -2
- package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-media-api/SKILL.md +97 -10
- package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-multi-mode/SKILL.md +97 -4
- package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-requirements/SKILL.md +70 -5
- package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-store-sync/SKILL.md +4 -2
- package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-weather-api/SKILL.md +7 -6
- package/templates/claude-code/_claude/skills/tos-web-ui-design/SKILL.md +373 -0
- package/templates/vite-react-typescript/_gitignore +4 -2
- package/templates/vite-react-typescript/telemetry.config.json +2 -1
- package/templates/vite-react-typescript-web/_gitignore +32 -0
- package/templates/vite-react-typescript-web/assets/telemetryos-wordmark.svg +11 -0
- package/templates/vite-react-typescript-web/assets/tos-app.svg +12 -0
- package/templates/vite-react-typescript-web/index.html +15 -0
- package/templates/vite-react-typescript-web/package.json +24 -0
- package/templates/vite-react-typescript-web/src/App.tsx +25 -0
- package/templates/vite-react-typescript-web/src/hooks/store.ts +8 -0
- package/templates/vite-react-typescript-web/src/index.css +24 -0
- package/templates/vite-react-typescript-web/src/index.tsx +11 -0
- package/templates/vite-react-typescript-web/src/views/Render.css +67 -0
- package/templates/vite-react-typescript-web/src/views/Render.tsx +44 -0
- package/templates/vite-react-typescript-web/src/views/Settings.tsx +72 -0
- package/templates/vite-react-typescript-web/src/views/Web.css +105 -0
- package/templates/vite-react-typescript-web/src/views/Web.tsx +52 -0
- package/templates/vite-react-typescript-web/telemetry.config.json +16 -0
- package/templates/vite-react-typescript-web/tsconfig.json +19 -0
- package/templates/vite-react-typescript-web/vite.config.ts +18 -0
- package/templates/vite-react-typescript/_claude/skills/tos-render-design/SKILL.md +0 -624
- /package/templates/{vite-react-typescript → claude-code}/AGENTS.md +0 -0
- /package/templates/{vite-react-typescript → claude-code}/_claude/settings.local.json +0 -0
- /package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-proxy-fetch/SKILL.md +0 -0
- /package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-render-kiosk-design/SKILL.md +0 -0
- /package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-render-signage-design/SKILL.md +0 -0
- /package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-render-ui-design/SKILL.md +0 -0
- /package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-settings-ui/SKILL.md +0 -0
package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-architecture/SKILL.md
RENAMED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: tos-architecture
|
|
3
|
-
description: Understand TelemetryOS
|
|
3
|
+
description: Understand TelemetryOS mount point architecture (Render, Settings, and optional Web). Use when debugging mount-point issues or understanding how mount points communicate through the store.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# TelemetryOS Architecture
|
|
7
7
|
|
|
8
8
|
TelemetryOS applications run on digital signage devices (TVs, kiosks, displays). Understanding the architecture is key to building effective apps.
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## Mount Points
|
|
11
11
|
|
|
12
|
-
Every
|
|
12
|
+
Every TOS app has at least two entry points (Render and Settings). Apps that need a browser-accessible interface add a third: the Web mount point.
|
|
13
13
|
|
|
14
14
|
### /render - Device Display
|
|
15
15
|
|
|
@@ -19,7 +19,7 @@ Every standard TOS app has two entry points:
|
|
|
19
19
|
│ (TV, Kiosk, Digital Sign) │
|
|
20
20
|
│ │
|
|
21
21
|
│ ┌───────────────────────────┐ │
|
|
22
|
-
│ │
|
|
22
|
+
│ │ TelemetryOS Electron App │ │
|
|
23
23
|
│ │ ┌─────────────────────┐ │ │
|
|
24
24
|
│ │ │ Your App │ │ │
|
|
25
25
|
│ │ │ /render route │ │ │
|
|
@@ -33,7 +33,7 @@ Every standard TOS app has two entry points:
|
|
|
33
33
|
|
|
34
34
|
**Characteristics:**
|
|
35
35
|
- Runs on physical device (TelemetryOS player)
|
|
36
|
-
-
|
|
36
|
+
- Iframe sandbox in Electron app
|
|
37
37
|
- Has access to `store().device` (device-local storage)
|
|
38
38
|
- Displays content to viewers/customers
|
|
39
39
|
- Full screen, optimized for viewing from distance
|
|
@@ -66,6 +66,38 @@ Every standard TOS app has two entry points:
|
|
|
66
66
|
- Side panel in Studio editor
|
|
67
67
|
- Form-based controls for settings
|
|
68
68
|
|
|
69
|
+
### /web - Browser-Accessible Interface (Optional)
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
┌─────────────────────────────────┐
|
|
73
|
+
│ Any Browser (Phone/Tablet/ │
|
|
74
|
+
│ Desktop — Staff or Public) │
|
|
75
|
+
│ │
|
|
76
|
+
│ ┌───────────────────────────┐ │
|
|
77
|
+
│ │ TelemetryOS Web Host │ │
|
|
78
|
+
│ │ ┌─────────────────────┐ │ │
|
|
79
|
+
│ │ │ Your App │ │ │
|
|
80
|
+
│ │ │ /app-name route │ │ │
|
|
81
|
+
│ │ │ │ │ │
|
|
82
|
+
│ │ │ Operator desk, │ │ │
|
|
83
|
+
│ │ │ public form, staff │ │ │
|
|
84
|
+
│ │ │ dashboard │ │ │
|
|
85
|
+
│ │ └─────────────────────┘ │ │
|
|
86
|
+
│ └───────────────────────────┘ │
|
|
87
|
+
└─────────────────────────────────┘
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Characteristics:**
|
|
91
|
+
- Runs in any browser, accessible via URL
|
|
92
|
+
- Can be staff-facing (private) or public-facing
|
|
93
|
+
- Application and dynamic namespace store scopes only
|
|
94
|
+
- **NO access to `store().instance` or `store().device`**
|
|
95
|
+
- Multi-level navigation with React Router
|
|
96
|
+
- Standard HTML/CSS (no SDK settings components, no UI scaling)
|
|
97
|
+
- Always pairs with multi-mode architecture and entity-driven routing
|
|
98
|
+
|
|
99
|
+
See `tos-web-ui-design` for full design patterns.
|
|
100
|
+
|
|
69
101
|
## Communication Flow
|
|
70
102
|
|
|
71
103
|
```
|
|
@@ -78,10 +110,31 @@ Every standard TOS app has two entry points:
|
|
|
78
110
|
└─────────────┘ └─────────────────┘ └─────────────┘
|
|
79
111
|
```
|
|
80
112
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
113
|
+
With a Web mount point, communication extends through shared namespaces:
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
┌─────────────┐ ┌─────────────────┐ ┌─────────────┐
|
|
117
|
+
│ Settings │ ───▶ │ store(). │ ◀─── │ Web │
|
|
118
|
+
│ /settings │ WRITE │ shared() │ READ │ /app-name │
|
|
119
|
+
│ │ │ namespace │ +WRITE │ │
|
|
120
|
+
│ Admin │ │ │ │ Operator │
|
|
121
|
+
│ configures │ │ { queues, │ │ manages │
|
|
122
|
+
│ entities │ │ counters } │ │ live data │
|
|
123
|
+
└─────────────┘ └─────────────────┘ └─────────────┘
|
|
124
|
+
│
|
|
125
|
+
▼ READ + SUB
|
|
126
|
+
┌─────────────┐
|
|
127
|
+
│ Render │
|
|
128
|
+
│ /render │
|
|
129
|
+
│ Displays │
|
|
130
|
+
│ live data │
|
|
131
|
+
└─────────────┘
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
1. **Settings writes** configuration to instance and application store
|
|
135
|
+
2. **Web reads and writes** entity-scoped data via dynamic namespace store
|
|
136
|
+
3. **Render subscribes** and displays changes in real-time
|
|
137
|
+
4. Same store hooks work across all mount points (scoped by what each can access)
|
|
85
138
|
|
|
86
139
|
## Project Structure
|
|
87
140
|
|
|
@@ -91,9 +144,12 @@ src/
|
|
|
91
144
|
├── App.tsx # Router - directs to correct view
|
|
92
145
|
├── views/
|
|
93
146
|
│ ├── Settings.tsx # /settings mount point
|
|
94
|
-
│
|
|
147
|
+
│ ├── Render.tsx # /render mount point
|
|
148
|
+
│ ├── Render.css # /render styles
|
|
149
|
+
│ ├── Web.tsx # /app-name mount point (if using web)
|
|
150
|
+
│ └── Web.css # Web view styles (if using web)
|
|
95
151
|
├── hooks/
|
|
96
|
-
│ └── store.ts # Shared store hooks (used
|
|
152
|
+
│ └── store.ts # Shared store hooks (used across all mount points)
|
|
97
153
|
├── components/ # Reusable UI components
|
|
98
154
|
├── types/ # TypeScript interfaces
|
|
99
155
|
└── utils/ # Helper functions
|
|
@@ -103,29 +159,34 @@ src/
|
|
|
103
159
|
|
|
104
160
|
```typescript
|
|
105
161
|
// App.tsx
|
|
162
|
+
import { createBrowserRouter, RouterProvider } from 'react-router'
|
|
106
163
|
import Settings from './views/Settings'
|
|
107
164
|
import Render from './views/Render'
|
|
108
165
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (path === '/render') return <Render />
|
|
166
|
+
const router = createBrowserRouter([
|
|
167
|
+
{ path: '/render', element: <Render /> },
|
|
168
|
+
{ path: '/settings', element: <Settings /> },
|
|
169
|
+
])
|
|
114
170
|
|
|
115
|
-
|
|
171
|
+
export function App() {
|
|
172
|
+
return <RouterProvider router={router} />
|
|
116
173
|
}
|
|
117
174
|
```
|
|
118
175
|
|
|
119
|
-
|
|
176
|
+
With a web mount point, add routes under the app name:
|
|
120
177
|
|
|
121
178
|
```typescript
|
|
122
179
|
import { createBrowserRouter, RouterProvider } from 'react-router'
|
|
123
|
-
import
|
|
124
|
-
import
|
|
180
|
+
import { Render } from './views/Render'
|
|
181
|
+
import { Settings } from './views/Settings'
|
|
182
|
+
import { WebEntities, WebDetail, WebOperator } from './views/Web'
|
|
125
183
|
|
|
126
184
|
const router = createBrowserRouter([
|
|
127
|
-
{ path: '/render',
|
|
128
|
-
{ path: '/settings',
|
|
185
|
+
{ path: '/render', Component: Render },
|
|
186
|
+
{ path: '/settings', Component: Settings },
|
|
187
|
+
{ path: '/my-app', Component: WebEntities },
|
|
188
|
+
{ path: '/my-app/locations/:locationName', Component: WebDetail },
|
|
189
|
+
{ path: '/my-app/locations/:locationName/counters/:counterId', Component: WebOperator },
|
|
129
190
|
])
|
|
130
191
|
|
|
131
192
|
export function App() {
|
|
@@ -157,6 +218,15 @@ export function App() {
|
|
|
157
218
|
- Form-based interaction
|
|
158
219
|
- Preview pane integration
|
|
159
220
|
|
|
221
|
+
### Web Only
|
|
222
|
+
|
|
223
|
+
- Standard HTML/CSS (no SDK settings components)
|
|
224
|
+
- Runs in any browser (phone, tablet, desktop)
|
|
225
|
+
- `store().application` and `store().shared(namespace)` only
|
|
226
|
+
- NO access to `store().instance` or `store().device`
|
|
227
|
+
- Multi-level React Router navigation
|
|
228
|
+
- No UI scaling (`useUiScaleToSetRem` not used)
|
|
229
|
+
|
|
160
230
|
## telemetry.config.json
|
|
161
231
|
|
|
162
232
|
```json
|
|
@@ -174,12 +244,32 @@ export function App() {
|
|
|
174
244
|
}
|
|
175
245
|
```
|
|
176
246
|
|
|
247
|
+
With a web mount point:
|
|
248
|
+
|
|
249
|
+
```json
|
|
250
|
+
{
|
|
251
|
+
"name": "my-app",
|
|
252
|
+
"version": "1.0.0",
|
|
253
|
+
"useSpaRouting": true,
|
|
254
|
+
"mountPoints": {
|
|
255
|
+
"render": "/render",
|
|
256
|
+
"settings": "/settings",
|
|
257
|
+
"web": "/my-app"
|
|
258
|
+
},
|
|
259
|
+
"devServer": {
|
|
260
|
+
"runCommand": "vite --port 3000",
|
|
261
|
+
"url": "http://localhost:3000"
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
177
266
|
### Mount Point Configuration
|
|
178
267
|
|
|
179
|
-
| Mount Point | Purpose | Route |
|
|
180
|
-
|
|
181
|
-
| render | Device display | /render |
|
|
182
|
-
| settings | Admin config UI | /settings |
|
|
268
|
+
| Mount Point | Purpose | Route | Store Access |
|
|
269
|
+
|-------------|---------|-------|--------------|
|
|
270
|
+
| render | Device display | /render | instance, application, device, shared |
|
|
271
|
+
| settings | Admin config UI | /settings | instance, application, shared |
|
|
272
|
+
| web (optional) | Browser interface | /app-name | application, shared |
|
|
183
273
|
|
|
184
274
|
### Optional: Workers
|
|
185
275
|
|
|
@@ -198,34 +288,32 @@ export function App() {
|
|
|
198
288
|
|
|
199
289
|
```
|
|
200
290
|
┌────────────────────────────────────────────────────────────────┐
|
|
201
|
-
│
|
|
291
|
+
│ Account │
|
|
292
|
+
│ │
|
|
202
293
|
│ ┌──────────────────────────────────────────────────────────┐ │
|
|
203
|
-
│ │ store().
|
|
204
|
-
│ │
|
|
205
|
-
│ │
|
|
294
|
+
│ │ store().shared('namespace') │ │
|
|
295
|
+
│ │ Dynamic namespace — scoped by entity │ │
|
|
296
|
+
│ │ Accessible from: Settings, Render, Web │ │
|
|
206
297
|
│ └──────────────────────────────────────────────────────────┘ │
|
|
207
|
-
│
|
|
208
|
-
│ ┌─────────────────────┐ ┌─────────────────────┐ │
|
|
209
|
-
│ │ App Instance 1 │ │ App Instance 2 │ │
|
|
210
|
-
│ │ instance store │ │ instance store │ │
|
|
211
|
-
│ │ (Settings↔Render) │ │ (Settings↔Render) │ │
|
|
212
|
-
│ └─────────────────────┘ └─────────────────────┘ │
|
|
213
|
-
└────────────────────────────────────────────────────────────────┘
|
|
214
|
-
|
|
215
|
-
┌────────────────────────────────────────────────────────────────┐
|
|
216
|
-
│ Physical Device │
|
|
298
|
+
│ │
|
|
217
299
|
│ ┌──────────────────────────────────────────────────────────┐ │
|
|
218
|
-
│ │
|
|
219
|
-
│ │
|
|
220
|
-
│ │
|
|
300
|
+
│ │ Application │ │
|
|
301
|
+
│ │ │ │
|
|
302
|
+
│ │ ┌────────────────────────────────────────────────────┐ │ │
|
|
303
|
+
│ │ │ store().application │ │ │
|
|
304
|
+
│ │ │ Shared across ALL instances of this app │ │ │
|
|
305
|
+
│ │ │ (API keys, account-wide settings) │ │ │
|
|
306
|
+
│ │ │ Accessible from: Settings, Render, Web │ │ │
|
|
307
|
+
│ │ └────────────────────────────────────────────────────┘ │ │
|
|
308
|
+
│ │ │ │
|
|
309
|
+
│ │ ┌──────────────────────┐ ┌──────────────────────┐ │ │
|
|
310
|
+
│ │ │ Device │ │ Instance │ │ │
|
|
311
|
+
│ │ │ store().device │ │ store().instance │ │ │
|
|
312
|
+
│ │ │ Render only! │ │ Settings ↔ Render │ │ │
|
|
313
|
+
│ │ │ (Cache, calibration)│ │ NOT in Web │ │ │
|
|
314
|
+
│ │ └──────────────────────┘ └──────────────────────┘ │ │
|
|
221
315
|
│ └──────────────────────────────────────────────────────────┘ │
|
|
222
316
|
└────────────────────────────────────────────────────────────────┘
|
|
223
|
-
|
|
224
|
-
┌────────────────────────────────────────────────────────────────┐
|
|
225
|
-
│ store().shared('namespace') │
|
|
226
|
-
│ Inter-app communication (any app can read/write) │
|
|
227
|
-
│ (Weather data sharing, event broadcasting) │
|
|
228
|
-
└────────────────────────────────────────────────────────────────┘
|
|
229
317
|
```
|
|
230
318
|
|
|
231
319
|
## Development Workflow
|
|
@@ -236,6 +324,7 @@ export function App() {
|
|
|
236
324
|
Both the render and settings mounts points are visible in the development host.
|
|
237
325
|
The Render mount point is presented in a resizable pane.
|
|
238
326
|
The Settings mount point shows in the right sidebar.
|
|
327
|
+
The Web mount point (if configured) appears as a tab in the development host, displayed in the same area as the other mount points.
|
|
239
328
|
|
|
240
329
|
|
|
241
330
|
### Build & Deploy
|
|
@@ -243,10 +332,6 @@ The Settings mount point shows in the right sidebar.
|
|
|
243
332
|
```bash
|
|
244
333
|
# Build production
|
|
245
334
|
npm run build
|
|
246
|
-
|
|
247
|
-
# Deploy via Git
|
|
248
|
-
git add . && git commit -m "Update" && git push
|
|
249
|
-
# GitHub integration auto-deploys
|
|
250
335
|
```
|
|
251
336
|
|
|
252
337
|
## Common Patterns
|
|
@@ -265,14 +350,6 @@ return <WeatherDisplay city={city} />
|
|
|
265
350
|
|
|
266
351
|
## Debugging
|
|
267
352
|
|
|
268
|
-
### Check Current Path
|
|
269
|
-
|
|
270
|
-
```typescript
|
|
271
|
-
console.log('Current path:', window.location.pathname)
|
|
272
|
-
console.log('Is Settings:', window.location.pathname === '/settings')
|
|
273
|
-
console.log('Is Render:', window.location.pathname === '/render')
|
|
274
|
-
```
|
|
275
|
-
|
|
276
353
|
### Verify SDK Configuration
|
|
277
354
|
|
|
278
355
|
```typescript
|
package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-debugging/SKILL.md
RENAMED
|
@@ -39,10 +39,10 @@ ReactDOM.createRoot(document.getElementById('root')!).render(<App />)
|
|
|
39
39
|
**Solution:**
|
|
40
40
|
```typescript
|
|
41
41
|
// WRONG - in Settings.tsx
|
|
42
|
-
const [, value, setValue] = useMyState() // using createUseDeviceStoreState
|
|
42
|
+
const [isLoading, value, setValue] = useMyState() // using createUseDeviceStoreState
|
|
43
43
|
|
|
44
44
|
// CORRECT - use instance scope
|
|
45
|
-
const [, value, setValue] = useMyState() // using createUseInstanceStoreState
|
|
45
|
+
const [isLoading, value, setValue] = useMyState() // using createUseInstanceStoreState
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
**Scope guide:**
|
package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-media-api/SKILL.md
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: tos-media-api
|
|
3
|
-
description: Access TelemetryOS Media Library for images, videos, and files. Use when building apps that display user-uploaded media content.
|
|
3
|
+
description: Access TelemetryOS Media Library for images, videos, and files. Use when building apps that display or select user-uploaded media content.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# TelemetryOS Media API
|
|
@@ -23,6 +23,10 @@ const tagged = await media().getAllByTag('banner')
|
|
|
23
23
|
|
|
24
24
|
// Get single item by ID
|
|
25
25
|
const item = await media().getById('content-id')
|
|
26
|
+
|
|
27
|
+
// Open media picker (user selects from full-screen dialog)
|
|
28
|
+
const selection = await media().openPicker()
|
|
29
|
+
const imagesOnly = await media().openPicker({ accept: ['image/*'] })
|
|
26
30
|
```
|
|
27
31
|
|
|
28
32
|
## Response Types
|
|
@@ -61,6 +65,29 @@ interface MediaContent {
|
|
|
61
65
|
}
|
|
62
66
|
```
|
|
63
67
|
|
|
68
|
+
### MediaSelection
|
|
69
|
+
|
|
70
|
+
Returned by `openPicker()` and used by `SettingsMediaSelect`.
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
interface MediaSelection {
|
|
74
|
+
id: string
|
|
75
|
+
name: string
|
|
76
|
+
thumbnailUrl: string // Thumbnail for preview
|
|
77
|
+
url: string // Full content URL
|
|
78
|
+
contentType: string // MIME type
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### MediaPickerOptions
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
interface MediaPickerOptions {
|
|
86
|
+
accept?: string[] // Content type filters: ['image/*', 'video/*']
|
|
87
|
+
currentValue?: MediaSelection // Pre-select current value in picker
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
64
91
|
## Common Patterns
|
|
65
92
|
|
|
66
93
|
### Folder Picker in Settings
|
|
@@ -123,6 +150,64 @@ export default function Settings() {
|
|
|
123
150
|
}
|
|
124
151
|
```
|
|
125
152
|
|
|
153
|
+
### Media Picker in Settings (SettingsMediaSelect)
|
|
154
|
+
|
|
155
|
+
Use the built-in `SettingsMediaSelect` component to let users pick a media item. Opens a full-screen picker dialog in the host window.
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
// hooks/store.ts
|
|
159
|
+
import { createUseInstanceStoreState } from '@telemetryos/sdk/react'
|
|
160
|
+
import type { MediaSelection } from '@telemetryos/sdk/react'
|
|
161
|
+
|
|
162
|
+
export const useMediaState = createUseInstanceStoreState<MediaSelection | null>('selectedMedia', null)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
// views/Settings.tsx
|
|
167
|
+
import {
|
|
168
|
+
SettingsContainer,
|
|
169
|
+
SettingsField,
|
|
170
|
+
SettingsLabel,
|
|
171
|
+
SettingsMediaSelect,
|
|
172
|
+
} from '@telemetryos/sdk/react'
|
|
173
|
+
import { useMediaState } from '../hooks/store'
|
|
174
|
+
|
|
175
|
+
export default function Settings() {
|
|
176
|
+
const [isLoading, selectedMedia, setSelectedMedia] = useMediaState()
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<SettingsContainer>
|
|
180
|
+
<SettingsField>
|
|
181
|
+
<SettingsLabel>Background Image</SettingsLabel>
|
|
182
|
+
<SettingsMediaSelect
|
|
183
|
+
value={selectedMedia}
|
|
184
|
+
onChange={setSelectedMedia}
|
|
185
|
+
accept={['image/*']}
|
|
186
|
+
disabled={isLoading}
|
|
187
|
+
/>
|
|
188
|
+
</SettingsField>
|
|
189
|
+
</SettingsContainer>
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Using openPicker() Directly
|
|
195
|
+
|
|
196
|
+
For custom UI, call `media().openPicker()` directly instead of using the component.
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
import { media } from '@telemetryos/sdk'
|
|
200
|
+
|
|
201
|
+
const handleSelectMedia = async () => {
|
|
202
|
+
const selection = await media().openPicker({ accept: ['image/*'] })
|
|
203
|
+
if (selection) {
|
|
204
|
+
// selection has: id, name, thumbnailUrl, url, contentType
|
|
205
|
+
console.log('Selected:', selection.name, selection.url)
|
|
206
|
+
}
|
|
207
|
+
// selection is null if user cancelled
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
126
211
|
### Image Gallery in Render
|
|
127
212
|
|
|
128
213
|
```typescript
|
|
@@ -249,15 +334,15 @@ import { media } from '@telemetryos/sdk'
|
|
|
249
334
|
import { useFolderIdState, useIntervalState } from '../hooks/store'
|
|
250
335
|
|
|
251
336
|
export default function Render() {
|
|
252
|
-
const [, folderId] = useFolderIdState()
|
|
253
|
-
const [, interval] = useIntervalState() // seconds
|
|
337
|
+
const [isLoadingFolder, folderId] = useFolderIdState()
|
|
338
|
+
const [isLoadingInterval, interval] = useIntervalState() // seconds
|
|
254
339
|
|
|
255
340
|
const [images, setImages] = useState<string[]>([])
|
|
256
341
|
const [currentIndex, setCurrentIndex] = useState(0)
|
|
257
342
|
|
|
258
343
|
// Load images
|
|
259
344
|
useEffect(() => {
|
|
260
|
-
if (!folderId) return
|
|
345
|
+
if (isLoadingFolder || !folderId) return
|
|
261
346
|
|
|
262
347
|
media().getAllByFolderId(folderId).then(content => {
|
|
263
348
|
const urls = content
|
|
@@ -327,9 +412,11 @@ const activeContent = content.filter(item => {
|
|
|
327
412
|
|
|
328
413
|
## Tips
|
|
329
414
|
|
|
330
|
-
1. **Use
|
|
331
|
-
2. **Use
|
|
332
|
-
3. **
|
|
333
|
-
4. **
|
|
334
|
-
5. **
|
|
335
|
-
6. **
|
|
415
|
+
1. **Use `SettingsMediaSelect` for settings** - Preferred way to let users pick media in Settings views
|
|
416
|
+
2. **Use `openPicker()` for custom UI** - When you need full control over the selection flow
|
|
417
|
+
3. **Use publicUrls[0]** - First URL is the primary CDN URL
|
|
418
|
+
4. **Use thumbnailUrl for previews** - Smaller, faster loading
|
|
419
|
+
5. **Filter by contentType** - Ensure you're displaying compatible content
|
|
420
|
+
6. **Handle empty folders** - Show appropriate message when no content
|
|
421
|
+
7. **Lazy load images** - Use `loading="lazy"` for galleries
|
|
422
|
+
8. **Respect hidden flag** - Filter out hidden items unless intentional
|
package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-multi-mode/SKILL.md
RENAMED
|
@@ -15,8 +15,10 @@ Listen for these indicators during requirements gathering:
|
|
|
15
15
|
- "Kiosk and display" or "control panel and viewer"
|
|
16
16
|
- "Different devices need different views of the same data"
|
|
17
17
|
- Data that's organized by location, department, topic, or similar grouping
|
|
18
|
+
- "Staff need to manage/control from their phone or browser"
|
|
19
|
+
- "Public signup form" or "public status page" for the same data
|
|
18
20
|
|
|
19
|
-
If ANY of these come up, this is a multi-mode app.
|
|
21
|
+
If ANY of these come up, this is a multi-mode app. If staff or the public need browser access outside the device, it also needs a **web mount point**.
|
|
20
22
|
|
|
21
23
|
---
|
|
22
24
|
|
|
@@ -80,12 +82,26 @@ Multi-mode apps use a 3-tier store pattern:
|
|
|
80
82
|
└─────────────────────────────────────────────────────────────┘
|
|
81
83
|
```
|
|
82
84
|
|
|
85
|
+
With a web mount point, add a fourth access path:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
89
|
+
│ Web Mount Point (browser — staff or public) │
|
|
90
|
+
│ Entity from URL params, NOT instance store │
|
|
91
|
+
│ ┌──────────────┐ ┌──────────────┐ ┌─────────────────────┐ │
|
|
92
|
+
│ │ Reads/writes │ │ Same │ │ Same namespace │ │
|
|
93
|
+
│ │ namespace │ │ store hooks │ │ helper function │ │
|
|
94
|
+
│ └──────────────┘ └──────────────┘ └─────────────────────┘ │
|
|
95
|
+
└─────────────────────────────────────────────────────────────┘
|
|
96
|
+
```
|
|
97
|
+
|
|
83
98
|
**How it connects:**
|
|
84
99
|
|
|
85
|
-
1. Admin creates
|
|
86
|
-
2. Each device selects a mode AND
|
|
87
|
-
3. All devices on the same
|
|
100
|
+
1. Admin creates entities in Settings (application scope)
|
|
101
|
+
2. Each device selects a mode AND an entity (instance scope)
|
|
102
|
+
3. All devices on the same entity share the same data (dynamic namespace scope)
|
|
88
103
|
4. A kiosk at "Location A" and a display at "Location A" see the same queues
|
|
104
|
+
5. Web views navigate to entities via URL and read/write the same namespace data
|
|
89
105
|
|
|
90
106
|
---
|
|
91
107
|
|
|
@@ -240,6 +256,75 @@ export function Render() {
|
|
|
240
256
|
|
|
241
257
|
---
|
|
242
258
|
|
|
259
|
+
## Web Mount Point Integration
|
|
260
|
+
|
|
261
|
+
When your multi-mode app needs a browser-accessible interface (operator desk, public form, staff dashboard), add a web mount point. The web view accesses the **same entity-scoped data** as Render, but gets the entity from URL params instead of instance store.
|
|
262
|
+
|
|
263
|
+
### Entity from URL vs Instance Store
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
// Render — entity from instance store (admin picks which entity this device shows)
|
|
267
|
+
const [isLoading, selectedLocation] = useSelectedLocationStoreState()
|
|
268
|
+
if (isLoading) return <div>Loading...</div>
|
|
269
|
+
const ns = locationNamespace(selectedLocation || 'Location A')
|
|
270
|
+
|
|
271
|
+
// Web — entity from URL params (user navigates via links)
|
|
272
|
+
const { locationName } = useParams()
|
|
273
|
+
const ns = locationNamespace(decodeURIComponent(locationName || ''))
|
|
274
|
+
|
|
275
|
+
// Same namespace → same data → real-time sync
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Web Routing Maps to Entities
|
|
279
|
+
|
|
280
|
+
Web routes use the app name as base path, with entity-driven sub-routes. Choose path segments that match your domain:
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
// App.tsx — add web routes alongside render and settings
|
|
284
|
+
const router = createBrowserRouter([
|
|
285
|
+
{ path: '/render', Component: Render },
|
|
286
|
+
{ path: '/settings', Component: Settings },
|
|
287
|
+
{ path: '/my-app', Component: WebEntities },
|
|
288
|
+
{ path: '/my-app/locations/:locationName', Component: WebDetail },
|
|
289
|
+
{ path: '/my-app/locations/:locationName/counters/:counterId', Component: WebOperator },
|
|
290
|
+
])
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Store Access in Web
|
|
294
|
+
|
|
295
|
+
Web views can only use **application** and **dynamic namespace** store hooks. No instance or device store (there's no device/instance context in the browser):
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
// ✅ Works in web
|
|
299
|
+
const [isLoading, locations] = useLocationsStoreState() // application scope
|
|
300
|
+
const [isLoadingCounters, counters, setCounters] = useCountersStoreState(ns, 250) // dynamic namespace
|
|
301
|
+
if (isLoading || isLoadingCounters) return <div>Loading...</div>
|
|
302
|
+
|
|
303
|
+
// ❌ NOT available in web
|
|
304
|
+
const [isLoading, mode] = useModeStoreState() // instance scope — no context
|
|
305
|
+
const [isLoading, uiScale] = useUiScaleStoreState() // instance scope — no context
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Config
|
|
309
|
+
|
|
310
|
+
Add the web mount point to `telemetry.config.json`:
|
|
311
|
+
|
|
312
|
+
```json
|
|
313
|
+
{
|
|
314
|
+
"name": "my-app",
|
|
315
|
+
"useSpaRouting": true,
|
|
316
|
+
"mountPoints": {
|
|
317
|
+
"render": "/render",
|
|
318
|
+
"settings": "/settings",
|
|
319
|
+
"web": "/my-app"
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
See `tos-web-ui-design` for complete web view design patterns.
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
243
328
|
## Settings Organization
|
|
244
329
|
|
|
245
330
|
Order Settings sections intentionally:
|
|
@@ -357,3 +442,11 @@ Before implementing a multi-mode app, confirm:
|
|
|
357
442
|
- [ ] Render view loads all hooks before branching on mode
|
|
358
443
|
- [ ] Settings ordered: mode → entity selector → config → entity management
|
|
359
444
|
- [ ] Entity management has add/rename/remove with at-least-one validation
|
|
445
|
+
|
|
446
|
+
If using a web mount point, also confirm:
|
|
447
|
+
|
|
448
|
+
- [ ] `telemetry.config.json` has `"web": "/app-name"` and `useSpaRouting: true`
|
|
449
|
+
- [ ] Web routes registered in App.tsx under the app name
|
|
450
|
+
- [ ] Web views get entity from URL params (not instance store)
|
|
451
|
+
- [ ] Web views only use application and dynamic namespace store hooks
|
|
452
|
+
- [ ] Entity names are URL-encoded in links and decoded in components
|