@telemetryos/cli 1.7.4 → 1.8.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 +22 -0
- package/dist/commands/init.js +11 -0
- package/dist/services/generate-application.d.ts +1 -0
- package/dist/services/generate-application.js +127 -4
- package/dist/utils/validate-project-name.d.ts +19 -0
- package/dist/utils/validate-project-name.js +44 -0
- package/package.json +2 -2
- package/templates/vite-react-typescript/CLAUDE.md +68 -1244
- package/templates/vite-react-typescript/_claude/settings.local.json +17 -0
- package/templates/vite-react-typescript/_claude/skills/tos-architecture/SKILL.md +313 -0
- package/templates/vite-react-typescript/_claude/skills/tos-debugging/SKILL.md +299 -0
- package/templates/vite-react-typescript/_claude/skills/tos-media-api/SKILL.md +335 -0
- package/templates/vite-react-typescript/_claude/skills/tos-proxy-fetch/SKILL.md +319 -0
- package/templates/vite-react-typescript/_claude/skills/tos-render-design/SKILL.md +332 -0
- package/templates/vite-react-typescript/_claude/skills/tos-requirements/SKILL.md +252 -0
- package/templates/vite-react-typescript/_claude/skills/tos-settings-ui/SKILL.md +636 -0
- package/templates/vite-react-typescript/_claude/skills/tos-store-sync/SKILL.md +359 -0
- package/templates/vite-react-typescript/_claude/skills/tos-weather-api/SKILL.md +384 -0
- package/templates/vite-react-typescript/src/hooks/store.ts +3 -3
- package/templates/vite-react-typescript/src/index.css +0 -1
- package/templates/vite-react-typescript/src/views/Render.css +2 -1
- package/templates/vite-react-typescript/src/views/Render.tsx +2 -3
- package/templates/vite-react-typescript/src/views/Settings.tsx +2 -3
- package/templates/vite-react-typescript/AGENTS.md +0 -7
- /package/templates/vite-react-typescript/{gitignore → _gitignore} +0 -0
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tos-settings-ui
|
|
3
|
+
description: REQUIRED for TelemetryOS Settings UI. MUST invoke BEFORE writing ANY Settings components - raw HTML won't work, SDK components are mandatory. Contains SettingsContainer, SettingsField, SettingsInputFrame, and all input patterns.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# TelemetryOS Settings UI Components
|
|
7
|
+
|
|
8
|
+
All Settings components are imported from `@telemetryos/sdk/react`. Always use these styled components - raw HTML won't match Studio's design system.
|
|
9
|
+
|
|
10
|
+
## Quick Reference
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
import {
|
|
14
|
+
// Layout
|
|
15
|
+
SettingsContainer,
|
|
16
|
+
SettingsHeading,
|
|
17
|
+
SettingsBox,
|
|
18
|
+
SettingsDivider,
|
|
19
|
+
// Fields
|
|
20
|
+
SettingsField,
|
|
21
|
+
SettingsLabel,
|
|
22
|
+
SettingsHint,
|
|
23
|
+
SettingsError,
|
|
24
|
+
// Inputs
|
|
25
|
+
SettingsInputFrame,
|
|
26
|
+
SettingsTextAreaFrame,
|
|
27
|
+
SettingsSelectFrame,
|
|
28
|
+
SettingsSliderFrame,
|
|
29
|
+
SettingsSliderRuler,
|
|
30
|
+
SettingsColorFrame,
|
|
31
|
+
// Toggles
|
|
32
|
+
SettingsSwitchFrame,
|
|
33
|
+
SettingsSwitchLabel,
|
|
34
|
+
SettingsCheckboxFrame,
|
|
35
|
+
SettingsCheckboxLabel,
|
|
36
|
+
SettingsRadioFrame,
|
|
37
|
+
SettingsRadioLabel,
|
|
38
|
+
// Actions
|
|
39
|
+
SettingsButtonFrame,
|
|
40
|
+
} from '@telemetryos/sdk/react'
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Debounce Guidelines
|
|
44
|
+
|
|
45
|
+
Store hooks accept an optional debounce delay (default 0ms - immediate). Choose based on input type:
|
|
46
|
+
|
|
47
|
+
| Input Type | Debounce | Reason |
|
|
48
|
+
|------------|----------|--------|
|
|
49
|
+
| Text input | 250ms | Wait for typing to pause |
|
|
50
|
+
| Textarea | 250ms | Wait for typing to pause |
|
|
51
|
+
| Select/Dropdown | 0ms (default) | Immediate feedback expected |
|
|
52
|
+
| Switch/Toggle | 0ms (default) | Immediate feedback expected |
|
|
53
|
+
| Checkbox | 0ms (default) | Immediate feedback expected |
|
|
54
|
+
| Radio | 0ms (default) | Immediate feedback expected |
|
|
55
|
+
| Slider | 5ms | Responsive feel, reduced message traffic |
|
|
56
|
+
| Color picker | 5ms | Responsive feel while dragging |
|
|
57
|
+
|
|
58
|
+
**Usage:**
|
|
59
|
+
```typescript
|
|
60
|
+
// Text input - debounce to wait for typing to pause
|
|
61
|
+
const [isLoading, city, setCity] = useCityStoreState(250)
|
|
62
|
+
|
|
63
|
+
// Dropdown - immediate (default, no argument needed)
|
|
64
|
+
const [isLoading, league, setLeague] = useLeagueStoreState()
|
|
65
|
+
|
|
66
|
+
// Slider - responsive (5ms)
|
|
67
|
+
const [isLoading, volume, setVolume] = useVolumeStoreState(5)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Component Patterns
|
|
71
|
+
|
|
72
|
+
### Container (Required)
|
|
73
|
+
|
|
74
|
+
Every Settings view must wrap content in `SettingsContainer`:
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import { SettingsContainer } from '@telemetryos/sdk/react'
|
|
78
|
+
|
|
79
|
+
export function Settings() {
|
|
80
|
+
return (
|
|
81
|
+
<SettingsContainer>
|
|
82
|
+
{/* All settings content here */}
|
|
83
|
+
</SettingsContainer>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Section Heading
|
|
89
|
+
|
|
90
|
+
Use `SettingsHeading` to divide settings into logical sections:
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
import { SettingsHeading, SettingsDivider } from '@telemetryos/sdk/react'
|
|
94
|
+
|
|
95
|
+
<SettingsHeading>Display Options</SettingsHeading>
|
|
96
|
+
{/* Fields for this section */}
|
|
97
|
+
|
|
98
|
+
<SettingsDivider />
|
|
99
|
+
|
|
100
|
+
<SettingsHeading>Advanced Settings</SettingsHeading>
|
|
101
|
+
{/* Fields for next section */}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Text Input
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
import {
|
|
108
|
+
SettingsContainer,
|
|
109
|
+
SettingsField,
|
|
110
|
+
SettingsLabel,
|
|
111
|
+
SettingsInputFrame,
|
|
112
|
+
} from '@telemetryos/sdk/react'
|
|
113
|
+
import { useTeamStoreState } from '../hooks/store'
|
|
114
|
+
|
|
115
|
+
export function Settings() {
|
|
116
|
+
const [isLoading, team, setTeam] = useTeamStoreState(250) // 250ms debounce for text input
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<SettingsContainer>
|
|
120
|
+
<SettingsField>
|
|
121
|
+
<SettingsLabel>Team Name</SettingsLabel>
|
|
122
|
+
<SettingsInputFrame>
|
|
123
|
+
<input
|
|
124
|
+
type="text"
|
|
125
|
+
placeholder="Enter team name..."
|
|
126
|
+
disabled={isLoading}
|
|
127
|
+
value={team}
|
|
128
|
+
onChange={(e) => setTeam(e.target.value)}
|
|
129
|
+
/>
|
|
130
|
+
</SettingsInputFrame>
|
|
131
|
+
</SettingsField>
|
|
132
|
+
</SettingsContainer>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Textarea (Multiline)
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
import { SettingsTextAreaFrame } from '@telemetryos/sdk/react'
|
|
141
|
+
|
|
142
|
+
<SettingsField>
|
|
143
|
+
<SettingsLabel>Description</SettingsLabel>
|
|
144
|
+
<SettingsTextAreaFrame>
|
|
145
|
+
<textarea
|
|
146
|
+
placeholder="Enter description..."
|
|
147
|
+
disabled={isLoading}
|
|
148
|
+
value={description}
|
|
149
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
150
|
+
rows={4}
|
|
151
|
+
/>
|
|
152
|
+
</SettingsTextAreaFrame>
|
|
153
|
+
</SettingsField>
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Hint Text
|
|
157
|
+
|
|
158
|
+
Add helper text below any field with `SettingsHint`:
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
import { SettingsHint } from '@telemetryos/sdk/react'
|
|
162
|
+
|
|
163
|
+
<SettingsField>
|
|
164
|
+
<SettingsLabel>API Key</SettingsLabel>
|
|
165
|
+
<SettingsInputFrame>
|
|
166
|
+
<input type="text" value={apiKey} onChange={(e) => setApiKey(e.target.value)} />
|
|
167
|
+
</SettingsInputFrame>
|
|
168
|
+
<SettingsHint>Found in your dashboard under Settings → API</SettingsHint>
|
|
169
|
+
</SettingsField>
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Error Message
|
|
173
|
+
|
|
174
|
+
Display validation errors with `SettingsError`:
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
import { SettingsError } from '@telemetryos/sdk/react'
|
|
178
|
+
|
|
179
|
+
<SettingsField>
|
|
180
|
+
<SettingsLabel>Email</SettingsLabel>
|
|
181
|
+
<SettingsInputFrame>
|
|
182
|
+
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
|
|
183
|
+
</SettingsInputFrame>
|
|
184
|
+
{error && <SettingsError>{error}</SettingsError>}
|
|
185
|
+
</SettingsField>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Dropdown Select
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
import { SettingsSelectFrame } from '@telemetryos/sdk/react'
|
|
192
|
+
import { useLeagueStoreState } from '../hooks/store'
|
|
193
|
+
|
|
194
|
+
const [isLoading, league, setLeague] = useLeagueStoreState(0) // 0ms for immediate feedback
|
|
195
|
+
|
|
196
|
+
<SettingsField>
|
|
197
|
+
<SettingsLabel>League</SettingsLabel>
|
|
198
|
+
<SettingsSelectFrame>
|
|
199
|
+
<select
|
|
200
|
+
disabled={isLoading}
|
|
201
|
+
value={league}
|
|
202
|
+
onChange={(e) => setLeague(e.target.value)}
|
|
203
|
+
>
|
|
204
|
+
<option value="nfl">NFL</option>
|
|
205
|
+
<option value="nba">NBA</option>
|
|
206
|
+
<option value="mlb">MLB</option>
|
|
207
|
+
<option value="nhl">NHL</option>
|
|
208
|
+
</select>
|
|
209
|
+
</SettingsSelectFrame>
|
|
210
|
+
</SettingsField>
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Slider
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
import { SettingsSliderFrame } from '@telemetryos/sdk/react'
|
|
217
|
+
import { useVolumeStoreState } from '../hooks/store'
|
|
218
|
+
|
|
219
|
+
const [isLoading, volume, setVolume] = useVolumeStoreState(5) // 5ms for responsive feel
|
|
220
|
+
|
|
221
|
+
<SettingsField>
|
|
222
|
+
<SettingsLabel>Volume</SettingsLabel>
|
|
223
|
+
<SettingsSliderFrame>
|
|
224
|
+
<input
|
|
225
|
+
type="range"
|
|
226
|
+
min="0"
|
|
227
|
+
max="100"
|
|
228
|
+
disabled={isLoading}
|
|
229
|
+
value={volume}
|
|
230
|
+
onChange={(e) => setVolume(Number(e.target.value))}
|
|
231
|
+
/>
|
|
232
|
+
<span>{volume}%</span>
|
|
233
|
+
</SettingsSliderFrame>
|
|
234
|
+
</SettingsField>
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
The frame uses flexbox - add a `<span>` after the input to show the current value.
|
|
238
|
+
|
|
239
|
+
### Slider with Ruler
|
|
240
|
+
|
|
241
|
+
Add tick labels below a slider with `SettingsSliderRuler`:
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
import { SettingsSliderFrame, SettingsSliderRuler } from '@telemetryos/sdk/react'
|
|
245
|
+
import { useQualityStoreState } from '../hooks/store'
|
|
246
|
+
|
|
247
|
+
const [isLoading, quality, setQuality] = useQualityStoreState(5) // 5ms for responsive feel
|
|
248
|
+
|
|
249
|
+
<SettingsField>
|
|
250
|
+
<SettingsLabel>Quality</SettingsLabel>
|
|
251
|
+
<SettingsSliderFrame>
|
|
252
|
+
<input
|
|
253
|
+
type="range"
|
|
254
|
+
min="1"
|
|
255
|
+
max="3"
|
|
256
|
+
disabled={isLoading}
|
|
257
|
+
value={quality}
|
|
258
|
+
onChange={(e) => setQuality(Number(e.target.value))}
|
|
259
|
+
/>
|
|
260
|
+
<span>{quality}</span>
|
|
261
|
+
</SettingsSliderFrame>
|
|
262
|
+
<SettingsSliderRuler>
|
|
263
|
+
<span>Low</span>
|
|
264
|
+
<span>Medium</span>
|
|
265
|
+
<span>High</span>
|
|
266
|
+
</SettingsSliderRuler>
|
|
267
|
+
</SettingsField>
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Color Picker
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
import { SettingsColorFrame } from '@telemetryos/sdk/react'
|
|
274
|
+
import { useColorStoreState } from '../hooks/store'
|
|
275
|
+
|
|
276
|
+
const [isLoading, color, setColor] = useColorStoreState(5) // 5ms for responsive feel while dragging
|
|
277
|
+
|
|
278
|
+
<SettingsField>
|
|
279
|
+
<SettingsLabel>Brand Color</SettingsLabel>
|
|
280
|
+
<SettingsColorFrame>
|
|
281
|
+
<input
|
|
282
|
+
type="color"
|
|
283
|
+
disabled={isLoading}
|
|
284
|
+
value={color}
|
|
285
|
+
onChange={(e) => setColor(e.target.value)}
|
|
286
|
+
/>
|
|
287
|
+
<span>{color}</span>
|
|
288
|
+
</SettingsColorFrame>
|
|
289
|
+
</SettingsField>
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Toggle Switch
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
import { SettingsSwitchFrame, SettingsSwitchLabel } from '@telemetryos/sdk/react'
|
|
296
|
+
import { useShowScoresStoreState } from '../hooks/store'
|
|
297
|
+
|
|
298
|
+
const [isLoading, showScores, setShowScores] = useShowScoresStoreState(0) // 0ms for immediate feedback
|
|
299
|
+
|
|
300
|
+
<SettingsField>
|
|
301
|
+
<SettingsSwitchFrame>
|
|
302
|
+
<input
|
|
303
|
+
type="checkbox"
|
|
304
|
+
role="switch"
|
|
305
|
+
disabled={isLoading}
|
|
306
|
+
checked={showScores}
|
|
307
|
+
onChange={(e) => setShowScores(e.target.checked)}
|
|
308
|
+
/>
|
|
309
|
+
<SettingsSwitchLabel>Show Live Scores</SettingsSwitchLabel>
|
|
310
|
+
</SettingsSwitchFrame>
|
|
311
|
+
</SettingsField>
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Note: Use `role="switch"` on the checkbox for proper switch styling.
|
|
315
|
+
|
|
316
|
+
### Checkbox
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
import { SettingsCheckboxFrame, SettingsCheckboxLabel } from '@telemetryos/sdk/react'
|
|
320
|
+
import { useAutoRefreshStoreState } from '../hooks/store'
|
|
321
|
+
|
|
322
|
+
const [isLoading, autoRefresh, setAutoRefresh] = useAutoRefreshStoreState(0) // 0ms for immediate feedback
|
|
323
|
+
|
|
324
|
+
<SettingsField>
|
|
325
|
+
<SettingsCheckboxFrame>
|
|
326
|
+
<input
|
|
327
|
+
type="checkbox"
|
|
328
|
+
disabled={isLoading}
|
|
329
|
+
checked={autoRefresh}
|
|
330
|
+
onChange={(e) => setAutoRefresh(e.target.checked)}
|
|
331
|
+
/>
|
|
332
|
+
<SettingsCheckboxLabel>Enable Auto-Refresh</SettingsCheckboxLabel>
|
|
333
|
+
</SettingsCheckboxFrame>
|
|
334
|
+
</SettingsField>
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Radio Group
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
import { SettingsRadioFrame, SettingsRadioLabel } from '@telemetryos/sdk/react'
|
|
341
|
+
import { useDisplayModeStoreState } from '../hooks/store'
|
|
342
|
+
|
|
343
|
+
const [isLoading, displayMode, setDisplayMode] = useDisplayModeStoreState(0) // 0ms for immediate feedback
|
|
344
|
+
|
|
345
|
+
<SettingsField>
|
|
346
|
+
<SettingsLabel>Display Mode</SettingsLabel>
|
|
347
|
+
<SettingsRadioFrame>
|
|
348
|
+
<input
|
|
349
|
+
type="radio"
|
|
350
|
+
name="displayMode"
|
|
351
|
+
value="compact"
|
|
352
|
+
disabled={isLoading}
|
|
353
|
+
checked={displayMode === 'compact'}
|
|
354
|
+
onChange={(e) => setDisplayMode(e.target.value)}
|
|
355
|
+
/>
|
|
356
|
+
<SettingsRadioLabel>Compact</SettingsRadioLabel>
|
|
357
|
+
</SettingsRadioFrame>
|
|
358
|
+
<SettingsRadioFrame>
|
|
359
|
+
<input
|
|
360
|
+
type="radio"
|
|
361
|
+
name="displayMode"
|
|
362
|
+
value="expanded"
|
|
363
|
+
disabled={isLoading}
|
|
364
|
+
checked={displayMode === 'expanded'}
|
|
365
|
+
onChange={(e) => setDisplayMode(e.target.value)}
|
|
366
|
+
/>
|
|
367
|
+
<SettingsRadioLabel>Expanded</SettingsRadioLabel>
|
|
368
|
+
</SettingsRadioFrame>
|
|
369
|
+
</SettingsField>
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Button
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
import { SettingsButtonFrame } from '@telemetryos/sdk/react'
|
|
376
|
+
|
|
377
|
+
<SettingsButtonFrame>
|
|
378
|
+
<button onClick={handleReset}>Reset to Defaults</button>
|
|
379
|
+
</SettingsButtonFrame>
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
## Layout Components
|
|
383
|
+
|
|
384
|
+
### SettingsBox (Grouping)
|
|
385
|
+
|
|
386
|
+
Bordered container, typically used for individual items in a repeatable list:
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
import { SettingsBox, SettingsHeading, SettingsButtonFrame } from '@telemetryos/sdk/react'
|
|
390
|
+
|
|
391
|
+
<SettingsHeading>Teams</SettingsHeading>
|
|
392
|
+
|
|
393
|
+
{teams.map((team, index) => (
|
|
394
|
+
<SettingsBox key={index}>
|
|
395
|
+
<SettingsHeading>Team {index + 1}</SettingsHeading>
|
|
396
|
+
{/* Team fields */}
|
|
397
|
+
<SettingsButtonFrame>
|
|
398
|
+
<button onClick={() => removeTeam(index)}>Remove</button>
|
|
399
|
+
</SettingsButtonFrame>
|
|
400
|
+
</SettingsBox>
|
|
401
|
+
))}
|
|
402
|
+
|
|
403
|
+
<SettingsButtonFrame>
|
|
404
|
+
<button onClick={addTeam}>+ Add Team</button>
|
|
405
|
+
</SettingsButtonFrame>
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### SettingsDivider (Separator)
|
|
409
|
+
|
|
410
|
+
Add a horizontal rule between sections:
|
|
411
|
+
|
|
412
|
+
```typescript
|
|
413
|
+
import { SettingsDivider } from '@telemetryos/sdk/react'
|
|
414
|
+
|
|
415
|
+
<SettingsField>...</SettingsField>
|
|
416
|
+
<SettingsDivider />
|
|
417
|
+
<SettingsField>...</SettingsField>
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
## Dynamic Lists
|
|
421
|
+
|
|
422
|
+
For settings with repeatable items (teams, locations, etc.), use store hooks with array types:
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
// hooks/store.ts defines: useTeamsStoreState
|
|
426
|
+
import { useTeamsStoreState } from '../hooks/store'
|
|
427
|
+
|
|
428
|
+
const [isLoading, teams, setTeams] = useTeamsStoreState(250) // contains text inputs
|
|
429
|
+
|
|
430
|
+
const addTeam = () => {
|
|
431
|
+
setTeams([...teams, { name: '', league: 'nfl' }])
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const removeTeam = (index: number) => {
|
|
435
|
+
setTeams(teams.filter((_, i) => i !== index))
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const updateTeam = (index: number, updates: Partial<typeof teams[0]>) => {
|
|
439
|
+
const updated = [...teams]
|
|
440
|
+
updated[index] = { ...updated[index], ...updates }
|
|
441
|
+
setTeams(updated)
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
## Complete Example
|
|
446
|
+
|
|
447
|
+
A complete Settings view with dynamic lists, sections, and multiple input types:
|
|
448
|
+
|
|
449
|
+
```typescript
|
|
450
|
+
// Store hooks - see tos-store-sync skill for how to define these in hooks/store.ts
|
|
451
|
+
import {
|
|
452
|
+
useTeamsStoreState,
|
|
453
|
+
useRefreshIntervalStoreState,
|
|
454
|
+
useShowScoresStoreState,
|
|
455
|
+
useBackgroundColorStoreState,
|
|
456
|
+
} from '../hooks/store'
|
|
457
|
+
import {
|
|
458
|
+
SettingsContainer,
|
|
459
|
+
SettingsBox,
|
|
460
|
+
SettingsHeading,
|
|
461
|
+
SettingsDivider,
|
|
462
|
+
SettingsField,
|
|
463
|
+
SettingsLabel,
|
|
464
|
+
SettingsHint,
|
|
465
|
+
SettingsInputFrame,
|
|
466
|
+
SettingsSelectFrame,
|
|
467
|
+
SettingsSliderFrame,
|
|
468
|
+
SettingsColorFrame,
|
|
469
|
+
SettingsSwitchFrame,
|
|
470
|
+
SettingsSwitchLabel,
|
|
471
|
+
SettingsButtonFrame,
|
|
472
|
+
} from '@telemetryos/sdk/react'
|
|
473
|
+
|
|
474
|
+
export function Settings() {
|
|
475
|
+
// Store hooks return [isLoading, value, setValue]
|
|
476
|
+
// Debounce: 250ms for text inputs, 0ms (default) for toggles, 5ms for sliders/colors
|
|
477
|
+
const [isLoadingTeams, teams, setTeams] = useTeamsStoreState(250) // contains text inputs
|
|
478
|
+
const [isLoadingInterval, interval, setInterval] = useRefreshIntervalStoreState(5)
|
|
479
|
+
const [isLoadingScores, showScores, setShowScores] = useShowScoresStoreState()
|
|
480
|
+
const [isLoadingBg, backgroundColor, setBackgroundColor] = useBackgroundColorStoreState(5)
|
|
481
|
+
|
|
482
|
+
const isLoading = isLoadingTeams || isLoadingInterval || isLoadingScores || isLoadingBg
|
|
483
|
+
|
|
484
|
+
const addTeam = () => {
|
|
485
|
+
setTeams([...teams, { name: '', league: 'nfl' }])
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const removeTeam = (index: number) => {
|
|
489
|
+
setTeams(teams.filter((_, i) => i !== index))
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const updateTeam = (index: number, updates: Partial<typeof teams[0]>) => {
|
|
493
|
+
const updated = [...teams]
|
|
494
|
+
updated[index] = { ...updated[index], ...updates }
|
|
495
|
+
setTeams(updated)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return (
|
|
499
|
+
<SettingsContainer>
|
|
500
|
+
<SettingsHeading>Teams</SettingsHeading>
|
|
501
|
+
|
|
502
|
+
{teams.map((team, index) => (
|
|
503
|
+
<SettingsBox key={index}>
|
|
504
|
+
<SettingsHeading>Team {index + 1}</SettingsHeading>
|
|
505
|
+
|
|
506
|
+
<SettingsField>
|
|
507
|
+
<SettingsLabel>Team Name</SettingsLabel>
|
|
508
|
+
<SettingsInputFrame>
|
|
509
|
+
<input
|
|
510
|
+
type="text"
|
|
511
|
+
placeholder="Enter team name..."
|
|
512
|
+
disabled={isLoading}
|
|
513
|
+
value={team.name}
|
|
514
|
+
onChange={(e) => updateTeam(index, { name: e.target.value })}
|
|
515
|
+
/>
|
|
516
|
+
</SettingsInputFrame>
|
|
517
|
+
<SettingsHint>This name appears in the header</SettingsHint>
|
|
518
|
+
</SettingsField>
|
|
519
|
+
|
|
520
|
+
<SettingsField>
|
|
521
|
+
<SettingsLabel>League</SettingsLabel>
|
|
522
|
+
<SettingsSelectFrame>
|
|
523
|
+
<select
|
|
524
|
+
disabled={isLoading}
|
|
525
|
+
value={team.league}
|
|
526
|
+
onChange={(e) => updateTeam(index, { league: e.target.value })}
|
|
527
|
+
>
|
|
528
|
+
<option value="nfl">NFL</option>
|
|
529
|
+
<option value="nba">NBA</option>
|
|
530
|
+
<option value="mlb">MLB</option>
|
|
531
|
+
<option value="nhl">NHL</option>
|
|
532
|
+
</select>
|
|
533
|
+
</SettingsSelectFrame>
|
|
534
|
+
</SettingsField>
|
|
535
|
+
|
|
536
|
+
<SettingsButtonFrame>
|
|
537
|
+
<button type="button" disabled={isLoading} onClick={() => removeTeam(index)}>
|
|
538
|
+
Remove Team
|
|
539
|
+
</button>
|
|
540
|
+
</SettingsButtonFrame>
|
|
541
|
+
</SettingsBox>
|
|
542
|
+
))}
|
|
543
|
+
|
|
544
|
+
<SettingsButtonFrame>
|
|
545
|
+
<button type="button" disabled={isLoading} onClick={addTeam}>+ Add Team</button>
|
|
546
|
+
</SettingsButtonFrame>
|
|
547
|
+
|
|
548
|
+
<SettingsDivider />
|
|
549
|
+
|
|
550
|
+
<SettingsHeading>Display</SettingsHeading>
|
|
551
|
+
|
|
552
|
+
<SettingsField>
|
|
553
|
+
<SettingsLabel>Refresh Interval (seconds)</SettingsLabel>
|
|
554
|
+
<SettingsSliderFrame>
|
|
555
|
+
<input
|
|
556
|
+
type="range"
|
|
557
|
+
min="10"
|
|
558
|
+
max="120"
|
|
559
|
+
disabled={isLoading}
|
|
560
|
+
value={interval}
|
|
561
|
+
onChange={(e) => setInterval(Number(e.target.value))}
|
|
562
|
+
/>
|
|
563
|
+
<span>{interval}s</span>
|
|
564
|
+
</SettingsSliderFrame>
|
|
565
|
+
</SettingsField>
|
|
566
|
+
|
|
567
|
+
<SettingsField>
|
|
568
|
+
<SettingsSwitchFrame>
|
|
569
|
+
<input
|
|
570
|
+
type="checkbox"
|
|
571
|
+
role="switch"
|
|
572
|
+
disabled={isLoading}
|
|
573
|
+
checked={showScores}
|
|
574
|
+
onChange={(e) => setShowScores(e.target.checked)}
|
|
575
|
+
/>
|
|
576
|
+
<SettingsSwitchLabel>Show Live Scores</SettingsSwitchLabel>
|
|
577
|
+
</SettingsSwitchFrame>
|
|
578
|
+
</SettingsField>
|
|
579
|
+
|
|
580
|
+
<SettingsField>
|
|
581
|
+
<SettingsLabel>Background Color</SettingsLabel>
|
|
582
|
+
<SettingsColorFrame>
|
|
583
|
+
<input
|
|
584
|
+
type="color"
|
|
585
|
+
disabled={isLoading}
|
|
586
|
+
value={backgroundColor}
|
|
587
|
+
onChange={(e) => setBackgroundColor(e.target.value)}
|
|
588
|
+
/>
|
|
589
|
+
<span>{backgroundColor}</span>
|
|
590
|
+
<SettingsButtonFrame>
|
|
591
|
+
<button type="button" disabled={isLoading} onClick={() => setBackgroundColor('transparent')}>
|
|
592
|
+
Clear
|
|
593
|
+
</button>
|
|
594
|
+
</SettingsButtonFrame>
|
|
595
|
+
</SettingsColorFrame>
|
|
596
|
+
</SettingsField>
|
|
597
|
+
</SettingsContainer>
|
|
598
|
+
)
|
|
599
|
+
}
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
## Component Reference
|
|
603
|
+
|
|
604
|
+
| Component | Purpose |
|
|
605
|
+
|-----------|---------|
|
|
606
|
+
| `SettingsContainer` | Root wrapper, handles color scheme |
|
|
607
|
+
| `SettingsHeading` | Section heading |
|
|
608
|
+
| `SettingsBox` | Container for list items |
|
|
609
|
+
| `SettingsDivider` | Horizontal separator |
|
|
610
|
+
| `SettingsField` | Wrapper for each field (renders as label) |
|
|
611
|
+
| `SettingsLabel` | Field label |
|
|
612
|
+
| `SettingsHint` | Help text below a field |
|
|
613
|
+
| `SettingsError` | Error message below a field |
|
|
614
|
+
| `SettingsInputFrame` | Text input wrapper |
|
|
615
|
+
| `SettingsTextAreaFrame` | Multiline text wrapper |
|
|
616
|
+
| `SettingsSelectFrame` | Dropdown wrapper |
|
|
617
|
+
| `SettingsSliderFrame` | Range slider wrapper |
|
|
618
|
+
| `SettingsSliderRuler` | Tick labels below a slider |
|
|
619
|
+
| `SettingsColorFrame` | Color picker wrapper |
|
|
620
|
+
| `SettingsSwitchFrame` | Toggle switch wrapper |
|
|
621
|
+
| `SettingsSwitchLabel` | Toggle switch label |
|
|
622
|
+
| `SettingsCheckboxFrame` | Checkbox wrapper |
|
|
623
|
+
| `SettingsCheckboxLabel` | Checkbox label |
|
|
624
|
+
| `SettingsRadioFrame` | Radio button wrapper |
|
|
625
|
+
| `SettingsRadioLabel` | Radio button label |
|
|
626
|
+
| `SettingsButtonFrame` | Action button wrapper |
|
|
627
|
+
|
|
628
|
+
## Common Mistakes
|
|
629
|
+
|
|
630
|
+
1. **Missing SettingsContainer** - Always wrap in SettingsContainer
|
|
631
|
+
2. **Forgetting disabled={isLoading}** - Disable inputs while loading from store
|
|
632
|
+
3. **Using raw HTML** - Always use Frame components for proper styling
|
|
633
|
+
4. **Missing role="switch"** - Required on toggle switches for proper styling
|
|
634
|
+
5. **Wrong onChange for sliders** - Use `Number(e.target.value)` for numeric values
|
|
635
|
+
6. **Missing SettingsHeading** - Use headings to organize sections
|
|
636
|
+
7. **Not using SettingsHint** - Add helpful context for complex fields
|