@telemetryos/cli 1.7.0 → 1.7.2
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 +16 -0
- package/package.json +2 -2
- package/templates/vite-react-typescript/CLAUDE.md +657 -151
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @telemetryos/cli
|
|
2
2
|
|
|
3
|
+
## 1.7.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Add more React components for Settings, show the canvas ascpect ratio in the dev host
|
|
8
|
+
- Updated dependencies
|
|
9
|
+
- @telemetryos/development-application-host-ui@1.7.2
|
|
10
|
+
|
|
11
|
+
## 1.7.1
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- do not write workers to public dir
|
|
16
|
+
- Updated dependencies
|
|
17
|
+
- @telemetryos/development-application-host-ui@1.7.1
|
|
18
|
+
|
|
3
19
|
## 1.7.0
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telemetryos/cli",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.2",
|
|
4
4
|
"description": "The official TelemetryOS application CLI package. Use it to build applications that run on the TelemetryOS platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"license": "",
|
|
26
26
|
"repository": "github:TelemetryTV/Application-API",
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@telemetryos/development-application-host-ui": "^1.7.
|
|
28
|
+
"@telemetryos/development-application-host-ui": "^1.7.2",
|
|
29
29
|
"@types/serve-handler": "^6.1.4",
|
|
30
30
|
"commander": "^14.0.0",
|
|
31
31
|
"inquirer": "^12.9.6",
|
|
@@ -40,7 +40,8 @@ project-root/
|
|
|
40
40
|
│ ├── Settings.tsx # /settings mount point
|
|
41
41
|
│ └── Render.tsx # /render mount point
|
|
42
42
|
├── components/ # Reusable components
|
|
43
|
-
├── hooks/
|
|
43
|
+
├── hooks/
|
|
44
|
+
│ └── store.ts # Store state hooks (createUseStoreState)
|
|
44
45
|
├── types/ # TypeScript interfaces
|
|
45
46
|
└── utils/ # Helper functions
|
|
46
47
|
```
|
|
@@ -128,159 +129,145 @@ export default function App() {
|
|
|
128
129
|
}
|
|
129
130
|
```
|
|
130
131
|
|
|
131
|
-
### src/
|
|
132
|
+
### src/hooks/store.ts (Store Hooks)
|
|
132
133
|
```typescript
|
|
133
|
-
import {
|
|
134
|
-
import { store } from '@telemetryos/sdk';
|
|
134
|
+
import { createUseStoreState } from '@telemetryos/sdk/react'
|
|
135
135
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
export default function Settings() {
|
|
142
|
-
const [config, setConfig] = useState<Config>({ city: '', units: 'celsius' });
|
|
143
|
-
const [loading, setLoading] = useState(false);
|
|
144
|
-
const [error, setError] = useState<string | null>(null);
|
|
145
|
-
|
|
146
|
-
// Load existing config on mount
|
|
147
|
-
useEffect(() => {
|
|
148
|
-
store().instance.get<Config>('config')
|
|
149
|
-
.then(saved => { if (saved) setConfig(saved); })
|
|
150
|
-
.catch(err => setError(err.message));
|
|
151
|
-
}, []);
|
|
136
|
+
// Create typed hooks for each store key
|
|
137
|
+
export const useTeamStoreState = createUseStoreState<string>('team', '')
|
|
138
|
+
export const useLeagueStoreState = createUseStoreState<string>('league', 'nfl')
|
|
139
|
+
```
|
|
152
140
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
141
|
+
### src/views/Settings.tsx (Complete Reference)
|
|
142
|
+
```typescript
|
|
143
|
+
import { store } from '@telemetryos/sdk'
|
|
144
|
+
import {
|
|
145
|
+
SettingsContainer,
|
|
146
|
+
SettingsField,
|
|
147
|
+
SettingsLabel,
|
|
148
|
+
SettingsInputFrame,
|
|
149
|
+
SettingsSelectFrame,
|
|
150
|
+
} from '@telemetryos/sdk/react'
|
|
151
|
+
import { useTeamStoreState, useLeagueStoreState } from '../hooks/store'
|
|
157
152
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
} catch (err) {
|
|
162
|
-
setError(err instanceof Error ? err.message : 'Unknown error');
|
|
163
|
-
} finally {
|
|
164
|
-
setLoading(false);
|
|
165
|
-
}
|
|
166
|
-
};
|
|
153
|
+
export default function Settings() {
|
|
154
|
+
const [isLoadingTeam, team, setTeam] = useTeamStoreState(store().instance)
|
|
155
|
+
const [isLoadingLeague, league, setLeague] = useLeagueStoreState(store().instance)
|
|
167
156
|
|
|
168
157
|
return (
|
|
169
|
-
<
|
|
170
|
-
<
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
<div>
|
|
174
|
-
<label htmlFor="city">City:</label>
|
|
158
|
+
<SettingsContainer>
|
|
159
|
+
<SettingsField>
|
|
160
|
+
<SettingsLabel>Team Name</SettingsLabel>
|
|
161
|
+
<SettingsInputFrame>
|
|
175
162
|
<input
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
163
|
+
type="text"
|
|
164
|
+
placeholder="Enter team name..."
|
|
165
|
+
disabled={isLoadingTeam}
|
|
166
|
+
value={team}
|
|
167
|
+
onChange={(e) => setTeam(e.target.value)}
|
|
180
168
|
/>
|
|
181
|
-
</
|
|
182
|
-
|
|
183
|
-
|
|
169
|
+
</SettingsInputFrame>
|
|
170
|
+
</SettingsField>
|
|
171
|
+
|
|
172
|
+
<SettingsField>
|
|
173
|
+
<SettingsLabel>League</SettingsLabel>
|
|
174
|
+
<SettingsSelectFrame>
|
|
184
175
|
<select
|
|
185
|
-
|
|
186
|
-
value={
|
|
187
|
-
onChange={(e) =>
|
|
176
|
+
disabled={isLoadingLeague}
|
|
177
|
+
value={league}
|
|
178
|
+
onChange={(e) => setLeague(e.target.value)}
|
|
188
179
|
>
|
|
189
|
-
<option value="
|
|
190
|
-
<option value="
|
|
180
|
+
<option value="nfl">NFL</option>
|
|
181
|
+
<option value="nba">NBA</option>
|
|
182
|
+
<option value="mlb">MLB</option>
|
|
183
|
+
<option value="nhl">NHL</option>
|
|
191
184
|
</select>
|
|
192
|
-
</
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
</form>
|
|
197
|
-
</div>
|
|
198
|
-
);
|
|
185
|
+
</SettingsSelectFrame>
|
|
186
|
+
</SettingsField>
|
|
187
|
+
</SettingsContainer>
|
|
188
|
+
)
|
|
199
189
|
}
|
|
200
190
|
```
|
|
201
191
|
|
|
202
192
|
### src/views/Render.tsx (Complete Reference)
|
|
203
193
|
```typescript
|
|
204
|
-
import { useEffect, useState } from 'react'
|
|
205
|
-
import {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
194
|
+
import { useEffect, useState } from 'react'
|
|
195
|
+
import { proxy } from '@telemetryos/sdk'
|
|
196
|
+
import { store } from '@telemetryos/sdk'
|
|
197
|
+
import { useTeamStoreState, useLeagueStoreState } from '../hooks/store'
|
|
198
|
+
|
|
199
|
+
interface GameScore {
|
|
200
|
+
homeTeam: string
|
|
201
|
+
awayTeam: string
|
|
202
|
+
homeScore: number
|
|
203
|
+
awayScore: number
|
|
204
|
+
status: string
|
|
215
205
|
}
|
|
216
206
|
|
|
217
207
|
export default function Render() {
|
|
218
|
-
|
|
219
|
-
const [
|
|
220
|
-
const [
|
|
221
|
-
const [error, setError] = useState<string | null>(null);
|
|
208
|
+
// Use same hooks as Settings - automatically syncs when Settings changes
|
|
209
|
+
const [isLoadingTeam, team] = useTeamStoreState(store().instance)
|
|
210
|
+
const [isLoadingLeague, league] = useLeagueStoreState(store().instance)
|
|
222
211
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
212
|
+
const [score, setScore] = useState<GameScore | null>(null)
|
|
213
|
+
const [loading, setLoading] = useState(false)
|
|
214
|
+
const [error, setError] = useState<string | null>(null)
|
|
226
215
|
|
|
227
|
-
|
|
228
|
-
if (newConfig) setConfig(newConfig);
|
|
229
|
-
};
|
|
230
|
-
store().instance.subscribe('config', handler);
|
|
231
|
-
|
|
232
|
-
return () => {
|
|
233
|
-
store().instance.unsubscribe('config', handler);
|
|
234
|
-
};
|
|
235
|
-
}, []);
|
|
236
|
-
|
|
237
|
-
// Fetch weather when config changes
|
|
216
|
+
// Fetch scores when config changes
|
|
238
217
|
useEffect(() => {
|
|
239
|
-
if (!
|
|
218
|
+
if (!team) return
|
|
240
219
|
|
|
241
|
-
const
|
|
242
|
-
setLoading(true)
|
|
243
|
-
setError(null)
|
|
220
|
+
const fetchScore = async () => {
|
|
221
|
+
setLoading(true)
|
|
222
|
+
setError(null)
|
|
244
223
|
|
|
245
224
|
try {
|
|
225
|
+
// Platform handles caching automatically - no manual cache needed
|
|
246
226
|
const response = await proxy().fetch(
|
|
247
|
-
`https://api.
|
|
248
|
-
)
|
|
249
|
-
|
|
250
|
-
if (!response.ok) throw new Error(`API error: ${response.status}`)
|
|
251
|
-
|
|
252
|
-
const data = await response.json()
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
227
|
+
`https://api.sportsdata.io/v3/${league}/scores/json/GamesByTeam/${team}`
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
if (!response.ok) throw new Error(`API error: ${response.status}`)
|
|
231
|
+
|
|
232
|
+
const data = await response.json()
|
|
233
|
+
if (data.length > 0) {
|
|
234
|
+
const game = data[0]
|
|
235
|
+
setScore({
|
|
236
|
+
homeTeam: game.HomeTeam,
|
|
237
|
+
awayTeam: game.AwayTeam,
|
|
238
|
+
homeScore: game.HomeScore,
|
|
239
|
+
awayScore: game.AwayScore,
|
|
240
|
+
status: game.Status,
|
|
241
|
+
})
|
|
242
|
+
}
|
|
257
243
|
} catch (err) {
|
|
258
|
-
setError(err instanceof Error ? err.message : 'Unknown error')
|
|
259
|
-
|
|
260
|
-
// Try cached data
|
|
261
|
-
const cached = await store().device.get<any>('cached');
|
|
262
|
-
if (cached) setWeather(cached.data);
|
|
244
|
+
setError(err instanceof Error ? err.message : 'Unknown error')
|
|
263
245
|
} finally {
|
|
264
|
-
setLoading(false)
|
|
246
|
+
setLoading(false)
|
|
265
247
|
}
|
|
266
|
-
}
|
|
248
|
+
}
|
|
267
249
|
|
|
268
|
-
|
|
269
|
-
}, [
|
|
250
|
+
fetchScore()
|
|
251
|
+
}, [team, league])
|
|
270
252
|
|
|
271
|
-
//
|
|
272
|
-
if (
|
|
273
|
-
if (
|
|
274
|
-
if (
|
|
253
|
+
// Loading state
|
|
254
|
+
if (isLoadingTeam || isLoadingLeague) return <div>Loading config...</div>
|
|
255
|
+
if (!team) return <div>Configure team in Settings</div>
|
|
256
|
+
if (loading && !score) return <div>Loading scores...</div>
|
|
257
|
+
if (error && !score) return <div>Error: {error}</div>
|
|
275
258
|
|
|
276
259
|
return (
|
|
277
260
|
<div>
|
|
278
|
-
<h1>{
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
261
|
+
<h1>{team} - {league.toUpperCase()}</h1>
|
|
262
|
+
{score && (
|
|
263
|
+
<div>
|
|
264
|
+
<div>{score.awayTeam} @ {score.homeTeam}</div>
|
|
265
|
+
<div>{score.awayScore} - {score.homeScore}</div>
|
|
266
|
+
<div>{score.status}</div>
|
|
267
|
+
</div>
|
|
268
|
+
)}
|
|
282
269
|
</div>
|
|
283
|
-
)
|
|
270
|
+
)
|
|
284
271
|
}
|
|
285
272
|
```
|
|
286
273
|
|
|
@@ -361,9 +348,9 @@ proxy().fetch(url: string, options?: RequestInit): Promise<Response>
|
|
|
361
348
|
```
|
|
362
349
|
|
|
363
350
|
- Same interface as standard fetch()
|
|
364
|
-
- Use
|
|
351
|
+
- Use when external APIs don't include CORS headers
|
|
365
352
|
- Returns standard Response object
|
|
366
|
-
-
|
|
353
|
+
- Regular `fetch()` works fine when CORS is not an issue (and has advanced caching in the player)
|
|
367
354
|
|
|
368
355
|
**Example:**
|
|
369
356
|
```typescript
|
|
@@ -535,6 +522,362 @@ if (result.ready.includes('app-specifier-hash')) {
|
|
|
535
522
|
}
|
|
536
523
|
```
|
|
537
524
|
|
|
525
|
+
## React Hooks for Store
|
|
526
|
+
|
|
527
|
+
Import from `@telemetryos/sdk/react`. These hooks simplify store interactions by handling subscriptions, loading states, and cleanup automatically.
|
|
528
|
+
|
|
529
|
+
### useStoreState
|
|
530
|
+
|
|
531
|
+
Syncs React state with a store key. Handles subscription/unsubscription automatically.
|
|
532
|
+
|
|
533
|
+
```typescript
|
|
534
|
+
import { useStoreState } from '@telemetryos/sdk/react'
|
|
535
|
+
import { store } from '@telemetryos/sdk'
|
|
536
|
+
|
|
537
|
+
function MyComponent() {
|
|
538
|
+
const [isLoading, value, setValue] = useStoreState<string>(
|
|
539
|
+
store().instance, // Store slice
|
|
540
|
+
'myKey', // Key name
|
|
541
|
+
'default value', // Initial state (optional)
|
|
542
|
+
300 // Debounce delay in ms (optional)
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
return (
|
|
546
|
+
<input
|
|
547
|
+
disabled={isLoading}
|
|
548
|
+
value={value}
|
|
549
|
+
onChange={(e) => setValue(e.target.value)}
|
|
550
|
+
/>
|
|
551
|
+
)
|
|
552
|
+
}
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
**Returns:** `[isLoading: boolean, value: T, setValue: Dispatch<SetStateAction<T>>]`
|
|
556
|
+
|
|
557
|
+
- `isLoading` - `true` until first value received from store
|
|
558
|
+
- `value` - Current value (from store or local optimistic update)
|
|
559
|
+
- `setValue` - Updates both local state and store (with optional debounce)
|
|
560
|
+
|
|
561
|
+
### createUseStoreState
|
|
562
|
+
|
|
563
|
+
Factory function to create reusable, typed hooks for specific store keys. **This is the recommended pattern.**
|
|
564
|
+
|
|
565
|
+
```typescript
|
|
566
|
+
// hooks/store.ts
|
|
567
|
+
import { createUseStoreState } from '@telemetryos/sdk/react'
|
|
568
|
+
|
|
569
|
+
// Create typed hooks for each store key
|
|
570
|
+
export const useTeamStoreState = createUseStoreState<string>('team', '')
|
|
571
|
+
export const useLeagueStoreState = createUseStoreState<string>('league', 'nfl')
|
|
572
|
+
export const useRefreshIntervalStoreState = createUseStoreState<number>('refreshInterval', 30)
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
```typescript
|
|
576
|
+
// views/Settings.tsx
|
|
577
|
+
import { store } from '@telemetryos/sdk'
|
|
578
|
+
import { useTeamStoreState, useLeagueStoreState } from '../hooks/store'
|
|
579
|
+
|
|
580
|
+
function Settings() {
|
|
581
|
+
const [isLoadingTeam, team, setTeam] = useTeamStoreState(store().instance)
|
|
582
|
+
const [isLoadingLeague, league, setLeague] = useLeagueStoreState(store().instance)
|
|
583
|
+
// ... use in components
|
|
584
|
+
}
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
**Benefits:**
|
|
588
|
+
- Type-safe: TypeScript knows the exact type of each store key
|
|
589
|
+
- Reusable: Same hook works in Settings and Render
|
|
590
|
+
- Automatic cleanup: No manual subscribe/unsubscribe needed
|
|
591
|
+
- Immediate sync: Changes sync to store automatically (no save button needed)
|
|
592
|
+
|
|
593
|
+
### Store Data Patterns
|
|
594
|
+
|
|
595
|
+
**Recommended: Separate store entry per field**
|
|
596
|
+
```typescript
|
|
597
|
+
// hooks/store.ts
|
|
598
|
+
export const useTeamStoreState = createUseStoreState<string>('team', '')
|
|
599
|
+
export const useLeagueStoreState = createUseStoreState<string>('league', 'nfl')
|
|
600
|
+
export const useShowScoresStoreState = createUseStoreState<boolean>('showScores', true)
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
**Alternative: Rich data object** (for tightly related data like slideshow items)
|
|
604
|
+
```typescript
|
|
605
|
+
// hooks/store.ts
|
|
606
|
+
interface SportsSlide {
|
|
607
|
+
team: string
|
|
608
|
+
league: string
|
|
609
|
+
displaySeconds: number
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
export const useSlidesStoreState = createUseStoreState<SportsSlide[]>('slides', [])
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
## Settings Components
|
|
616
|
+
|
|
617
|
+
Import from `@telemetryos/sdk/react`. These components ensure your Settings UI matches the Studio design system.
|
|
618
|
+
|
|
619
|
+
**Important:** Always use these components for Settings. Raw HTML won't look correct in Studio.
|
|
620
|
+
|
|
621
|
+
### Container & Layout
|
|
622
|
+
|
|
623
|
+
#### SettingsContainer
|
|
624
|
+
|
|
625
|
+
Root wrapper for all Settings content. Handles color scheme synchronization with Studio.
|
|
626
|
+
|
|
627
|
+
```typescript
|
|
628
|
+
import { SettingsContainer } from '@telemetryos/sdk/react'
|
|
629
|
+
|
|
630
|
+
function Settings() {
|
|
631
|
+
return (
|
|
632
|
+
<SettingsContainer>
|
|
633
|
+
{/* All settings content goes here */}
|
|
634
|
+
</SettingsContainer>
|
|
635
|
+
)
|
|
636
|
+
}
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
#### SettingsBox
|
|
640
|
+
|
|
641
|
+
Container with border for grouping related settings.
|
|
642
|
+
|
|
643
|
+
```typescript
|
|
644
|
+
import { SettingsBox } from '@telemetryos/sdk/react'
|
|
645
|
+
|
|
646
|
+
<SettingsBox>
|
|
647
|
+
{/* Group of related fields */}
|
|
648
|
+
</SettingsBox>
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
#### SettingsDivider
|
|
652
|
+
|
|
653
|
+
Horizontal rule separator between sections.
|
|
654
|
+
|
|
655
|
+
```typescript
|
|
656
|
+
import { SettingsDivider } from '@telemetryos/sdk/react'
|
|
657
|
+
|
|
658
|
+
<SettingsDivider />
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
### Field Structure
|
|
662
|
+
|
|
663
|
+
#### SettingsField, SettingsLabel
|
|
664
|
+
|
|
665
|
+
Wrapper for each form field with its label.
|
|
666
|
+
|
|
667
|
+
```typescript
|
|
668
|
+
import { SettingsField, SettingsLabel } from '@telemetryos/sdk/react'
|
|
669
|
+
|
|
670
|
+
<SettingsField>
|
|
671
|
+
<SettingsLabel>Field Label</SettingsLabel>
|
|
672
|
+
{/* Input component goes here */}
|
|
673
|
+
</SettingsField>
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
### Text Inputs
|
|
677
|
+
|
|
678
|
+
#### SettingsInputFrame (Text Input)
|
|
679
|
+
|
|
680
|
+
```typescript
|
|
681
|
+
import { store } from '@telemetryos/sdk'
|
|
682
|
+
import {
|
|
683
|
+
SettingsContainer,
|
|
684
|
+
SettingsField,
|
|
685
|
+
SettingsLabel,
|
|
686
|
+
SettingsInputFrame,
|
|
687
|
+
} from '@telemetryos/sdk/react'
|
|
688
|
+
import { useTeamStoreState } from '../hooks/store'
|
|
689
|
+
|
|
690
|
+
function Settings() {
|
|
691
|
+
const [isLoading, team, setTeam] = useTeamStoreState(store().instance)
|
|
692
|
+
|
|
693
|
+
return (
|
|
694
|
+
<SettingsContainer>
|
|
695
|
+
<SettingsField>
|
|
696
|
+
<SettingsLabel>Team Name</SettingsLabel>
|
|
697
|
+
<SettingsInputFrame>
|
|
698
|
+
<input
|
|
699
|
+
type="text"
|
|
700
|
+
placeholder="Enter team name..."
|
|
701
|
+
disabled={isLoading}
|
|
702
|
+
value={team}
|
|
703
|
+
onChange={(e) => setTeam(e.target.value)}
|
|
704
|
+
/>
|
|
705
|
+
</SettingsInputFrame>
|
|
706
|
+
</SettingsField>
|
|
707
|
+
</SettingsContainer>
|
|
708
|
+
)
|
|
709
|
+
}
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
#### SettingsTextAreaFrame (Multiline Text)
|
|
713
|
+
|
|
714
|
+
```typescript
|
|
715
|
+
import { SettingsTextAreaFrame } from '@telemetryos/sdk/react'
|
|
716
|
+
import { useDescriptionStoreState } from '../hooks/store'
|
|
717
|
+
|
|
718
|
+
const [isLoading, description, setDescription] = useDescriptionStoreState(store().instance)
|
|
719
|
+
|
|
720
|
+
<SettingsField>
|
|
721
|
+
<SettingsLabel>Description</SettingsLabel>
|
|
722
|
+
<SettingsTextAreaFrame>
|
|
723
|
+
<textarea
|
|
724
|
+
placeholder="Enter description..."
|
|
725
|
+
disabled={isLoading}
|
|
726
|
+
value={description}
|
|
727
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
728
|
+
rows={4}
|
|
729
|
+
/>
|
|
730
|
+
</SettingsTextAreaFrame>
|
|
731
|
+
</SettingsField>
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
### Selection Inputs
|
|
735
|
+
|
|
736
|
+
#### SettingsSelectFrame (Dropdown)
|
|
737
|
+
|
|
738
|
+
```typescript
|
|
739
|
+
import { SettingsSelectFrame } from '@telemetryos/sdk/react'
|
|
740
|
+
import { useLeagueStoreState } from '../hooks/store'
|
|
741
|
+
|
|
742
|
+
const [isLoading, league, setLeague] = useLeagueStoreState(store().instance)
|
|
743
|
+
|
|
744
|
+
<SettingsField>
|
|
745
|
+
<SettingsLabel>League</SettingsLabel>
|
|
746
|
+
<SettingsSelectFrame>
|
|
747
|
+
<select
|
|
748
|
+
disabled={isLoading}
|
|
749
|
+
value={league}
|
|
750
|
+
onChange={(e) => setLeague(e.target.value)}
|
|
751
|
+
>
|
|
752
|
+
<option value="nfl">NFL</option>
|
|
753
|
+
<option value="nba">NBA</option>
|
|
754
|
+
<option value="mlb">MLB</option>
|
|
755
|
+
<option value="nhl">NHL</option>
|
|
756
|
+
</select>
|
|
757
|
+
</SettingsSelectFrame>
|
|
758
|
+
</SettingsField>
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
#### SettingsSliderFrame (Range Slider)
|
|
762
|
+
|
|
763
|
+
```typescript
|
|
764
|
+
import { SettingsSliderFrame } from '@telemetryos/sdk/react'
|
|
765
|
+
import { useVolumeStoreState } from '../hooks/store'
|
|
766
|
+
|
|
767
|
+
const [isLoading, volume, setVolume] = useVolumeStoreState(store().instance)
|
|
768
|
+
|
|
769
|
+
<SettingsField>
|
|
770
|
+
<SettingsLabel>Volume</SettingsLabel>
|
|
771
|
+
<SettingsSliderFrame>
|
|
772
|
+
<input
|
|
773
|
+
type="range"
|
|
774
|
+
min="0"
|
|
775
|
+
max="100"
|
|
776
|
+
disabled={isLoading}
|
|
777
|
+
value={volume}
|
|
778
|
+
onChange={(e) => setVolume(Number(e.target.value))}
|
|
779
|
+
/>
|
|
780
|
+
<span>{volume}%</span> {/* Optional value label */}
|
|
781
|
+
</SettingsSliderFrame>
|
|
782
|
+
</SettingsField>
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
The frame uses flexbox layout, so you can optionally add a `<span>` after the input to display the current value.
|
|
786
|
+
|
|
787
|
+
### Toggle Inputs
|
|
788
|
+
|
|
789
|
+
#### SettingsSwitchFrame, SettingsSwitchLabel (Toggle Switch)
|
|
790
|
+
|
|
791
|
+
```typescript
|
|
792
|
+
import { SettingsSwitchFrame, SettingsSwitchLabel } from '@telemetryos/sdk/react'
|
|
793
|
+
import { useShowScoresStoreState } from '../hooks/store'
|
|
794
|
+
|
|
795
|
+
const [isLoading, showScores, setShowScores] = useShowScoresStoreState(store().instance)
|
|
796
|
+
|
|
797
|
+
<SettingsField>
|
|
798
|
+
<SettingsSwitchFrame>
|
|
799
|
+
<input
|
|
800
|
+
type="checkbox"
|
|
801
|
+
role="switch"
|
|
802
|
+
disabled={isLoading}
|
|
803
|
+
checked={showScores}
|
|
804
|
+
onChange={(e) => setShowScores(e.target.checked)}
|
|
805
|
+
/>
|
|
806
|
+
<SettingsSwitchLabel>Show Live Scores</SettingsSwitchLabel>
|
|
807
|
+
</SettingsSwitchFrame>
|
|
808
|
+
</SettingsField>
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
#### SettingsCheckboxFrame, SettingsCheckboxLabel (Checkbox)
|
|
812
|
+
|
|
813
|
+
```typescript
|
|
814
|
+
import { SettingsCheckboxFrame, SettingsCheckboxLabel } from '@telemetryos/sdk/react'
|
|
815
|
+
import { useAutoRefreshStoreState } from '../hooks/store'
|
|
816
|
+
|
|
817
|
+
const [isLoading, autoRefresh, setAutoRefresh] = useAutoRefreshStoreState(store().instance)
|
|
818
|
+
|
|
819
|
+
<SettingsField>
|
|
820
|
+
<SettingsCheckboxFrame>
|
|
821
|
+
<input
|
|
822
|
+
type="checkbox"
|
|
823
|
+
disabled={isLoading}
|
|
824
|
+
checked={autoRefresh}
|
|
825
|
+
onChange={(e) => setAutoRefresh(e.target.checked)}
|
|
826
|
+
/>
|
|
827
|
+
<SettingsCheckboxLabel>Enable Auto-Refresh</SettingsCheckboxLabel>
|
|
828
|
+
</SettingsCheckboxFrame>
|
|
829
|
+
</SettingsField>
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
#### SettingsRadioFrame, SettingsRadioLabel (Radio Buttons)
|
|
833
|
+
|
|
834
|
+
```typescript
|
|
835
|
+
import { SettingsRadioFrame, SettingsRadioLabel } from '@telemetryos/sdk/react'
|
|
836
|
+
import { useDisplayModeStoreState } from '../hooks/store'
|
|
837
|
+
|
|
838
|
+
const [isLoading, displayMode, setDisplayMode] = useDisplayModeStoreState(store().instance)
|
|
839
|
+
|
|
840
|
+
<SettingsField>
|
|
841
|
+
<SettingsLabel>Display Mode</SettingsLabel>
|
|
842
|
+
<SettingsRadioFrame>
|
|
843
|
+
<input
|
|
844
|
+
type="radio"
|
|
845
|
+
name="displayMode"
|
|
846
|
+
value="compact"
|
|
847
|
+
disabled={isLoading}
|
|
848
|
+
checked={displayMode === 'compact'}
|
|
849
|
+
onChange={(e) => setDisplayMode(e.target.value)}
|
|
850
|
+
/>
|
|
851
|
+
<SettingsRadioLabel>Compact</SettingsRadioLabel>
|
|
852
|
+
</SettingsRadioFrame>
|
|
853
|
+
<SettingsRadioFrame>
|
|
854
|
+
<input
|
|
855
|
+
type="radio"
|
|
856
|
+
name="displayMode"
|
|
857
|
+
value="expanded"
|
|
858
|
+
disabled={isLoading}
|
|
859
|
+
checked={displayMode === 'expanded'}
|
|
860
|
+
onChange={(e) => setDisplayMode(e.target.value)}
|
|
861
|
+
/>
|
|
862
|
+
<SettingsRadioLabel>Expanded</SettingsRadioLabel>
|
|
863
|
+
</SettingsRadioFrame>
|
|
864
|
+
</SettingsField>
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
### Actions
|
|
868
|
+
|
|
869
|
+
#### SettingsButtonFrame
|
|
870
|
+
|
|
871
|
+
```typescript
|
|
872
|
+
import { SettingsButtonFrame } from '@telemetryos/sdk/react'
|
|
873
|
+
|
|
874
|
+
<SettingsField>
|
|
875
|
+
<SettingsButtonFrame>
|
|
876
|
+
<button onClick={handleReset}>Reset to Defaults</button>
|
|
877
|
+
</SettingsButtonFrame>
|
|
878
|
+
</SettingsField>
|
|
879
|
+
```
|
|
880
|
+
|
|
538
881
|
## Hard Constraints
|
|
539
882
|
|
|
540
883
|
**These cause runtime errors:**
|
|
@@ -544,9 +887,10 @@ if (result.ready.includes('app-specifier-hash')) {
|
|
|
544
887
|
- `store().device.*` throws Error in Settings
|
|
545
888
|
- Use `store().instance` or `store().application` instead
|
|
546
889
|
|
|
547
|
-
2. **
|
|
548
|
-
-
|
|
549
|
-
-
|
|
890
|
+
2. **CORS errors on external APIs**
|
|
891
|
+
- Some external APIs don't include CORS headers
|
|
892
|
+
- Use `proxy().fetch()` when you encounter CORS errors
|
|
893
|
+
- Regular `fetch()` is fine when CORS is not an issue (and has advanced caching in the player)
|
|
550
894
|
|
|
551
895
|
3. **Missing configure()**
|
|
552
896
|
- SDK methods throw "SDK not configured" Error
|
|
@@ -592,50 +936,205 @@ export default function WeatherCard({ data, onRefresh }: Props) {
|
|
|
592
936
|
|
|
593
937
|
## React Patterns
|
|
594
938
|
|
|
939
|
+
**Prefer SDK hooks over manual store operations:**
|
|
940
|
+
```typescript
|
|
941
|
+
// RECOMMENDED: Use SDK hooks
|
|
942
|
+
import { useTeamStoreState } from '../hooks/store'
|
|
943
|
+
const [isLoading, team, setTeam] = useTeamStoreState(store().instance)
|
|
944
|
+
|
|
945
|
+
// AVOID: Manual subscription (only for special cases)
|
|
946
|
+
const [team, setTeam] = useState('')
|
|
947
|
+
useEffect(() => {
|
|
948
|
+
const handler = (value) => setTeam(value)
|
|
949
|
+
store().instance.subscribe('team', handler)
|
|
950
|
+
return () => store().instance.unsubscribe('team', handler)
|
|
951
|
+
}, [])
|
|
952
|
+
```
|
|
953
|
+
|
|
954
|
+
**Subscription behavior:**
|
|
955
|
+
When you call `subscribe()`, the handler is immediately called with the current value. No separate `get()` call is needed:
|
|
956
|
+
```typescript
|
|
957
|
+
// WRONG - unnecessary get() call
|
|
958
|
+
useEffect(() => {
|
|
959
|
+
store().instance.get('config').then(setConfig) // Not needed!
|
|
960
|
+
store().instance.subscribe('config', handler)
|
|
961
|
+
// ...
|
|
962
|
+
}, [])
|
|
963
|
+
|
|
964
|
+
// CORRECT - subscribe handles initial value
|
|
965
|
+
useEffect(() => {
|
|
966
|
+
const handler = (value) => setConfig(value)
|
|
967
|
+
store().instance.subscribe('config', handler) // Immediately receives current value
|
|
968
|
+
return () => store().instance.unsubscribe('config', handler)
|
|
969
|
+
}, [])
|
|
970
|
+
```
|
|
971
|
+
|
|
972
|
+
**No manual caching needed:**
|
|
973
|
+
The platform automatically caches SDK API calls, `fetch()`, and `proxy().fetch()` requests. Don't implement your own cache:
|
|
974
|
+
```typescript
|
|
975
|
+
// WRONG - manual caching
|
|
976
|
+
const response = await fetch(url)
|
|
977
|
+
await store().device.set('cached', data) // Don't do this!
|
|
978
|
+
|
|
979
|
+
// CORRECT - just fetch, platform handles caching
|
|
980
|
+
const response = await fetch(url)
|
|
981
|
+
```
|
|
982
|
+
|
|
595
983
|
**Error handling:**
|
|
596
984
|
```typescript
|
|
597
|
-
const [error, setError] = useState<string | null>(null)
|
|
985
|
+
const [error, setError] = useState<string | null>(null)
|
|
598
986
|
|
|
599
987
|
try {
|
|
600
|
-
await store().instance.set('key', value)
|
|
988
|
+
await store().instance.set('key', value)
|
|
601
989
|
} catch (err) {
|
|
602
|
-
setError(err instanceof Error ? err.message : 'Unknown error')
|
|
990
|
+
setError(err instanceof Error ? err.message : 'Unknown error')
|
|
603
991
|
}
|
|
604
992
|
```
|
|
605
993
|
|
|
606
994
|
**Loading states:**
|
|
607
995
|
```typescript
|
|
608
|
-
const [loading, setLoading] = useState(false)
|
|
996
|
+
const [loading, setLoading] = useState(false)
|
|
609
997
|
|
|
610
998
|
const handleAction = async () => {
|
|
611
|
-
setLoading(true)
|
|
999
|
+
setLoading(true)
|
|
612
1000
|
try {
|
|
613
|
-
await someAsyncOperation()
|
|
1001
|
+
await someAsyncOperation()
|
|
614
1002
|
} finally {
|
|
615
|
-
setLoading(false)
|
|
1003
|
+
setLoading(false)
|
|
616
1004
|
}
|
|
617
|
-
}
|
|
1005
|
+
}
|
|
618
1006
|
```
|
|
619
1007
|
|
|
620
|
-
|
|
1008
|
+
## Low-Level Store API (Alternative)
|
|
1009
|
+
|
|
1010
|
+
For special cases where SDK hooks don't fit, you can use the store API directly. This requires manual subscription management.
|
|
1011
|
+
|
|
1012
|
+
**Manual subscription pattern:**
|
|
621
1013
|
```typescript
|
|
622
|
-
useEffect
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
1014
|
+
import { useEffect, useState } from 'react'
|
|
1015
|
+
import { store } from '@telemetryos/sdk'
|
|
1016
|
+
import {
|
|
1017
|
+
SettingsContainer,
|
|
1018
|
+
SettingsField,
|
|
1019
|
+
SettingsLabel,
|
|
1020
|
+
SettingsInputFrame,
|
|
1021
|
+
} from '@telemetryos/sdk/react'
|
|
1022
|
+
|
|
1023
|
+
interface Config {
|
|
1024
|
+
team: string
|
|
1025
|
+
league: string
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
export default function Settings() {
|
|
1029
|
+
const [config, setConfig] = useState<Config>({ team: '', league: 'nfl' })
|
|
1030
|
+
const [isLoading, setIsLoading] = useState(true)
|
|
1031
|
+
|
|
1032
|
+
// Subscribe on mount - subscribe() immediately sends current value
|
|
1033
|
+
useEffect(() => {
|
|
1034
|
+
const handler = (value: Config | undefined) => {
|
|
1035
|
+
if (value) setConfig(value)
|
|
1036
|
+
setIsLoading(false)
|
|
1037
|
+
}
|
|
1038
|
+
store().instance.subscribe<Config>('config', handler)
|
|
1039
|
+
|
|
1040
|
+
return () => {
|
|
1041
|
+
store().instance.unsubscribe('config', handler)
|
|
1042
|
+
}
|
|
1043
|
+
}, [])
|
|
1044
|
+
|
|
1045
|
+
// Update store on change
|
|
1046
|
+
const updateConfig = (updates: Partial<Config>) => {
|
|
1047
|
+
const newConfig = { ...config, ...updates }
|
|
1048
|
+
setConfig(newConfig)
|
|
1049
|
+
store().instance.set('config', newConfig)
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
return (
|
|
1053
|
+
<SettingsContainer>
|
|
1054
|
+
<SettingsField>
|
|
1055
|
+
<SettingsLabel>Team Name</SettingsLabel>
|
|
1056
|
+
<SettingsInputFrame>
|
|
1057
|
+
<input
|
|
1058
|
+
type="text"
|
|
1059
|
+
value={config.team}
|
|
1060
|
+
onChange={(e) => updateConfig({ team: e.target.value })}
|
|
1061
|
+
disabled={isLoading}
|
|
1062
|
+
/>
|
|
1063
|
+
</SettingsInputFrame>
|
|
1064
|
+
</SettingsField>
|
|
1065
|
+
</SettingsContainer>
|
|
1066
|
+
)
|
|
1067
|
+
}
|
|
629
1068
|
```
|
|
630
1069
|
|
|
631
|
-
**
|
|
1070
|
+
**Form submission pattern** (for cases requiring validation before save):
|
|
632
1071
|
```typescript
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
1072
|
+
import { useState, FormEvent } from 'react'
|
|
1073
|
+
import { store } from '@telemetryos/sdk'
|
|
1074
|
+
import {
|
|
1075
|
+
SettingsContainer,
|
|
1076
|
+
SettingsField,
|
|
1077
|
+
SettingsLabel,
|
|
1078
|
+
SettingsInputFrame,
|
|
1079
|
+
} from '@telemetryos/sdk/react'
|
|
1080
|
+
|
|
1081
|
+
export default function Settings() {
|
|
1082
|
+
const [team, setTeam] = useState('')
|
|
1083
|
+
const [saving, setSaving] = useState(false)
|
|
1084
|
+
const [error, setError] = useState<string | null>(null)
|
|
1085
|
+
|
|
1086
|
+
const handleSubmit = async (e: FormEvent) => {
|
|
1087
|
+
e.preventDefault()
|
|
1088
|
+
setSaving(true)
|
|
1089
|
+
setError(null)
|
|
1090
|
+
|
|
1091
|
+
try {
|
|
1092
|
+
// Validate before saving
|
|
1093
|
+
if (team.length < 2) throw new Error('Team name too short')
|
|
1094
|
+
|
|
1095
|
+
const success = await store().instance.set('team', team)
|
|
1096
|
+
if (!success) throw new Error('Storage operation failed')
|
|
1097
|
+
} catch (err) {
|
|
1098
|
+
setError(err instanceof Error ? err.message : 'Unknown error')
|
|
1099
|
+
} finally {
|
|
1100
|
+
setSaving(false)
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
return (
|
|
1105
|
+
<SettingsContainer>
|
|
1106
|
+
<form onSubmit={handleSubmit}>
|
|
1107
|
+
{error && <div style={{ color: 'red' }}>{error}</div>}
|
|
1108
|
+
<SettingsField>
|
|
1109
|
+
<SettingsLabel>Team Name</SettingsLabel>
|
|
1110
|
+
<SettingsInputFrame>
|
|
1111
|
+
<input
|
|
1112
|
+
type="text"
|
|
1113
|
+
value={team}
|
|
1114
|
+
onChange={(e) => setTeam(e.target.value)}
|
|
1115
|
+
/>
|
|
1116
|
+
</SettingsInputFrame>
|
|
1117
|
+
</SettingsField>
|
|
1118
|
+
<button type="submit" disabled={saving}>
|
|
1119
|
+
{saving ? 'Saving...' : 'Save'}
|
|
1120
|
+
</button>
|
|
1121
|
+
</form>
|
|
1122
|
+
</SettingsContainer>
|
|
1123
|
+
)
|
|
1124
|
+
}
|
|
637
1125
|
```
|
|
638
1126
|
|
|
1127
|
+
**When to use low-level API:**
|
|
1128
|
+
- Complex validation logic before saving
|
|
1129
|
+
- Batching multiple store operations
|
|
1130
|
+
- Custom debouncing behavior
|
|
1131
|
+
- Integration with form libraries
|
|
1132
|
+
|
|
1133
|
+
**When to use SDK hooks (recommended):**
|
|
1134
|
+
- Most Settings components
|
|
1135
|
+
- Simple form fields
|
|
1136
|
+
- Real-time sync between Settings and Render
|
|
1137
|
+
|
|
639
1138
|
## Code Style
|
|
640
1139
|
|
|
641
1140
|
**Naming:**
|
|
@@ -647,14 +1146,21 @@ useEffect(() => {
|
|
|
647
1146
|
**Imports order:**
|
|
648
1147
|
```typescript
|
|
649
1148
|
// 1. SDK imports
|
|
650
|
-
import { configure, store, proxy } from '@telemetryos/sdk'
|
|
1149
|
+
import { configure, store, proxy } from '@telemetryos/sdk'
|
|
1150
|
+
import {
|
|
1151
|
+
SettingsContainer,
|
|
1152
|
+
SettingsField,
|
|
1153
|
+
SettingsLabel,
|
|
1154
|
+
SettingsInputFrame,
|
|
1155
|
+
} from '@telemetryos/sdk/react'
|
|
651
1156
|
|
|
652
1157
|
// 2. React imports
|
|
653
|
-
import { useEffect, useState } from 'react'
|
|
1158
|
+
import { useEffect, useState } from 'react'
|
|
654
1159
|
|
|
655
|
-
// 3. Local imports
|
|
656
|
-
import
|
|
657
|
-
import
|
|
1160
|
+
// 3. Local imports (hooks, components, types)
|
|
1161
|
+
import { useTeamStoreState } from '../hooks/store'
|
|
1162
|
+
import ScoreCard from '@/components/ScoreCard'
|
|
1163
|
+
import type { GameScore } from '@/types'
|
|
658
1164
|
```
|
|
659
1165
|
|
|
660
1166
|
**TypeScript:**
|
|
@@ -707,7 +1213,7 @@ git push origin main
|
|
|
707
1213
|
→ Using `store().device` in Settings - use `store().instance` instead
|
|
708
1214
|
|
|
709
1215
|
**CORS error**
|
|
710
|
-
→
|
|
1216
|
+
→ External API doesn't include CORS headers - use `proxy().fetch()` for that API
|
|
711
1217
|
|
|
712
1218
|
**"Request timeout"**
|
|
713
1219
|
→ SDK operation exceeded 30 seconds - handle with try/catch
|