@telemetryos/cli 1.14.0 → 1.15.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.
Files changed (21) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/package.json +2 -2
  3. package/templates/claude-code/_claude/skills/tos-settings-ui/SKILL.md +128 -101
  4. package/templates/vite-react-typescript/index.html +1 -1
  5. package/templates/vite-react-typescript/public/assets/telemetryos-wordmark-dark.svg +11 -0
  6. package/templates/vite-react-typescript/src/components/FlickeringGrid.tsx +150 -0
  7. package/templates/vite-react-typescript/src/hooks/store.ts +32 -0
  8. package/templates/vite-react-typescript/src/themes.ts +226 -0
  9. package/templates/vite-react-typescript/src/views/Render.css +943 -5
  10. package/templates/vite-react-typescript/src/views/Render.tsx +139 -10
  11. package/templates/vite-react-typescript/src/views/Settings.tsx +145 -34
  12. package/templates/vite-react-typescript-web/index.html +1 -1
  13. package/templates/vite-react-typescript-web/public/assets/telemetryos-wordmark-dark.svg +11 -0
  14. package/templates/vite-react-typescript-web/src/components/FlickeringGrid.tsx +150 -0
  15. package/templates/vite-react-typescript-web/src/hooks/store.ts +35 -2
  16. package/templates/vite-react-typescript-web/src/themes.ts +226 -0
  17. package/templates/vite-react-typescript-web/src/views/Render.css +943 -5
  18. package/templates/vite-react-typescript-web/src/views/Render.tsx +139 -11
  19. package/templates/vite-react-typescript-web/src/views/Settings.tsx +156 -47
  20. /package/templates/vite-react-typescript-web/{assets → public/assets}/telemetryos-wordmark.svg +0 -0
  21. /package/templates/vite-react-typescript-web/{assets → public/assets}/tos-app.svg +0 -0
@@ -1,24 +1,152 @@
1
- import { useUiScaleToSetRem } from '@telemetryos/sdk/react'
2
- import wordMarkPath from '../../assets/telemetryos-wordmark.svg'
3
- import { useSubtitleStoreState, useUiScaleStoreState } from '../hooks/store'
1
+ /**
2
+ * Render view — the display content shown on physical signage devices.
3
+ *
4
+ * This template provides the shared scaffolding that every app needs:
5
+ * - Theme system (8 themes with CSS custom properties)
6
+ * - Responsive density tiers (full / comfortable / compact / minimal)
7
+ * - Entrance animations (14 presets)
8
+ * - Page padding and UI scale controls
9
+ * - Background toggle (gradient or transparent for layering)
10
+ * - Portrait/landscape detection
11
+ *
12
+ * Layout building blocks (CSS classes with entrance animation support):
13
+ * .content-header — top-level header bar (title + subtitle)
14
+ * .content-card — primary hero card with gradient background
15
+ * .detail-grid — grid container for detail cards
16
+ * .detail-card — small info cards (icon + value + label)
17
+ * .content-section — secondary section (footer bar, list, etc.)
18
+ *
19
+ * To build your app:
20
+ * 1. Add store hooks in hooks/store.ts and import them here.
21
+ * 2. Add their isLoading flags to `isStoreLoading`.
22
+ * 3. Replace the welcome content below with your layout using the classes above.
23
+ * 4. Use `density` and `isPortrait` to adapt your layout to available space.
24
+ */
25
+
26
+ import { useUiScaleToSetRem, useUiAspectRatio } from '@telemetryos/sdk/react'
27
+ import {
28
+ useThemeStoreState,
29
+ useUiScaleStoreState,
30
+ usePagePaddingStoreState,
31
+ useAnimationStoreState,
32
+ useShowBackgroundStoreState,
33
+ useSubtitleStoreState,
34
+ } from '../hooks/store'
35
+ import { themes, type ThemeName, type Theme } from '../themes'
4
36
  import './Render.css'
5
37
 
38
+ /** Maps a Theme's color tokens to CSS custom properties consumed by Render.css. */
39
+ function applyThemeVars(theme: Theme) {
40
+ const c = theme.colors
41
+ return {
42
+ '--bg-start': c.bgStart,
43
+ '--bg-mid': c.bgMid,
44
+ '--bg-end': c.bgEnd,
45
+ '--card-start': c.cardStart,
46
+ '--card-mid1': c.cardMid1,
47
+ '--card-mid2': c.cardMid2,
48
+ '--card-end': c.cardEnd,
49
+ '--card-shadow': c.cardShadow,
50
+ '--primary': c.primary,
51
+ '--secondary': c.secondary,
52
+ '--muted': c.muted,
53
+ '--accent': c.accent,
54
+ '--card-bg': c.cardBg,
55
+ '--card-border': c.cardBorder,
56
+ '--status-good': c.statusGood,
57
+ '--font-family': theme.fontFamily,
58
+ } as React.CSSProperties
59
+ }
60
+
61
+ /**
62
+ * Density tiers control spacing, font sizes, and which elements are visible.
63
+ * Determined by UI scale combined with aspect ratio (portrait needs more room).
64
+ */
65
+ type Density = 'full' | 'comfortable' | 'compact' | 'minimal'
66
+
67
+ function getDensity(uiScale: number, aspectRatio: number): Density {
68
+ const isPortrait = aspectRatio < 1
69
+ const pressure = uiScale * (isPortrait ? 1.2 : 1)
70
+ if (pressure < 1.4) return 'full'
71
+ if (pressure < 1.8) return 'comfortable'
72
+ if (pressure < 2.3) return 'compact'
73
+ return 'minimal'
74
+ }
75
+
6
76
  export function Render() {
7
- const [, uiScale] = useUiScaleStoreState()
77
+ // ── Store state ──────────────────────────────────────────────────────────
78
+ const [isLoadingScale, uiScale] = useUiScaleStoreState()
79
+ const [isLoadingPadding, pagePadding] = usePagePaddingStoreState()
80
+ const [isLoadingTheme, themeName] = useThemeStoreState()
81
+ const [isLoadingAnim, animation] = useAnimationStoreState()
82
+ const [isLoadingBg, showBackground] = useShowBackgroundStoreState()
83
+ const [isLoadingSubtitle, subtitle] = useSubtitleStoreState()
84
+ const aspectRatio = useUiAspectRatio()
85
+
86
+ // Drives rem scaling: html font-size = base * uiScale
8
87
  useUiScaleToSetRem(uiScale)
9
- const [isLoading, subtitle] = useSubtitleStoreState()
10
88
 
89
+ // ── Loading gate — render nothing until all store values are available ───
90
+ const isStoreLoading = isLoadingScale || isLoadingPadding || isLoadingTheme || isLoadingAnim || isLoadingBg || isLoadingSubtitle
91
+ if (isStoreLoading) return null
92
+
93
+ // ── Derived layout state ─────────────────────────────────────────────────
94
+ const resolvedName = (Object.prototype.hasOwnProperty.call(themes, themeName) ? themeName : 'telemetryos') as ThemeName
95
+ const theme = themes[resolvedName]
96
+ const isPortrait = aspectRatio < 1
97
+ const density = getDensity(uiScale, aspectRatio)
98
+
99
+ // Themes that need special CSS overlays register their modifier class here.
100
+ const themeModifier: Record<string, string> = {
101
+ 'telemetryos': 'render--telemetryos',
102
+ 'neon-pulse': 'render--neon-pulse',
103
+ 'solar-flare': 'render--solar-flare',
104
+ 'emerald-matrix': 'render--emerald-matrix',
105
+ 'arctic-aurora': 'render--arctic-aurora',
106
+ 'the-matrix': 'render--the-matrix',
107
+ 'plain-light': 'render--plain',
108
+ 'plain-dark': 'render--plain',
109
+ }
110
+
111
+ const animClass = `anim--${animation}`
112
+ const layoutClasses = [
113
+ 'render',
114
+ themeModifier[resolvedName] ?? '',
115
+ isPortrait ? 'render--portrait' : '',
116
+ `render--${density}`,
117
+ animClass,
118
+ showBackground ? '' : 'render--no-bg',
119
+ ].filter(Boolean).join(' ')
120
+
121
+ // ── Render ───────────────────────────────────────────────────────────────
11
122
  return (
12
- <div className="render">
13
- <img src={wordMarkPath} alt="TelemetryOS" className="render__logo" />
123
+ <div key={animation} className={layoutClasses} style={{ ...applyThemeVars(theme), '--page-padding': pagePadding } as React.CSSProperties}>
124
+ {/* Theme-specific background effects */}
125
+ {showBackground && resolvedName === 'telemetryos' && <div className="tos-sweep" />}
126
+ {showBackground && resolvedName === 'neon-pulse' && (
127
+ <div className="neon-pulse-bg">
128
+ <div className="neon-pulse-bg__orb" />
129
+ <div className="neon-pulse-bg__orb" />
130
+ <div className="neon-pulse-bg__orb" />
131
+ </div>
132
+ )}
133
+ {showBackground && resolvedName === 'solar-flare' && <div className="solar-flare-bg" />}
134
+ {showBackground && resolvedName === 'arctic-aurora' && <div className="arctic-aurora-bg" />}
135
+ {showBackground && resolvedName === 'the-matrix' && <div className="matrix-scanlines" />}
136
+
137
+ {/* ── Welcome content (replace with your app) ─────────────────────── */}
138
+
139
+ <img src={resolvedName === 'plain-light' ? '/assets/telemetryos-wordmark-dark.svg' : '/assets/telemetryos-wordmark.svg'} alt="TelemetryOS" className="render__logo" />
140
+
14
141
  <div className="render__hero">
15
- {uiScale < 1.5 && (
142
+ {density !== 'minimal' && (
16
143
  <div className="render__hero-title">Welcome to TelemetryOS SDK</div>
17
144
  )}
18
- <div className="render__hero-subtitle">{isLoading ? 'Loading...' : subtitle}</div>
145
+ <div className="render__hero-subtitle">{subtitle}</div>
19
146
  </div>
147
+
20
148
  <div className="render__docs-information">
21
- {uiScale < 1.2 && (
149
+ {density === 'full' && (
22
150
  <>
23
151
  <div className="render__docs-information-title">
24
152
  To get started, edit the Render.tsx and Settings.tsx files
@@ -28,7 +156,7 @@ export function Render() {
28
156
  </div>
29
157
  </>
30
158
  )}
31
- {uiScale < 1.35 && (
159
+ {(density === 'full' || density === 'comfortable') && (
32
160
  <a
33
161
  className="render__docs-information-button"
34
162
  href="https://docs.telemetryos.com/docs/sdk-getting-started"
@@ -1,72 +1,181 @@
1
+ /**
2
+ * Settings view — configuration UI rendered inside the Studio admin portal.
3
+ *
4
+ * This view provides the standard appearance controls (theme, scale, padding,
5
+ * animation, background toggle) that ship with every app.
6
+ *
7
+ * To add app-specific settings:
8
+ * 1. Create store hooks in hooks/store.ts.
9
+ * 2. Import and destructure them here alongside the existing hooks.
10
+ * 3. Add their isLoading flags to the `isLoading` expression.
11
+ * 4. Wrap your controls in a <SettingsSection title="Your Section">.
12
+ *
13
+ * Available SDK settings components:
14
+ * SettingsContainer, SettingsSection, SettingsDivider, SettingsHint,
15
+ * SettingsField, SettingsLabel,
16
+ * SettingsInputFrame, SettingsSelectFrame, SettingsSliderFrame,
17
+ * SettingsSwitchFrame, SettingsSwitchLabel,
18
+ * SettingsCheckboxFrame, SettingsCheckboxLabel
19
+ *
20
+ * SettingsSection is a collapsible accordion — use it to group related
21
+ * controls under a heading. It manages its own open/closed state.
22
+ * Always wrap new groups of settings in a SettingsSection.
23
+ */
24
+
1
25
  import {
2
26
  SettingsContainer,
3
- SettingsDivider,
4
27
  SettingsField,
5
- SettingsHeading,
28
+ SettingsHint,
6
29
  SettingsInputFrame,
7
30
  SettingsLabel,
31
+ SettingsSection,
32
+ SettingsSelectFrame,
8
33
  SettingsSliderFrame,
34
+ SettingsCheckboxFrame,
35
+ SettingsCheckboxLabel,
9
36
  } from '@telemetryos/sdk/react'
10
- import { useSubtitleStoreState, useUiScaleStoreState, useWelcomeMessageStoreState } from '../hooks/store'
37
+ import {
38
+ useThemeStoreState,
39
+ useUiScaleStoreState,
40
+ usePagePaddingStoreState,
41
+ useAnimationStoreState,
42
+ useShowBackgroundStoreState,
43
+ useSubtitleStoreState,
44
+ useWelcomeMessageStoreState,
45
+ } from '../hooks/store'
46
+ import { themes } from '../themes'
11
47
 
12
48
  export function Settings() {
13
- const [isLoadingUiScale, uiScale, setUiScale] = useUiScaleStoreState(5)
49
+ const [isLoadingTheme, themeName, setThemeName] = useThemeStoreState()
50
+ const [isLoadingScale, uiScale, setUiScale] = useUiScaleStoreState(5)
51
+ const [isLoadingPadding, pagePadding, setPagePadding] = usePagePaddingStoreState()
52
+ const [isLoadingAnim, animation, setAnimation] = useAnimationStoreState()
53
+ const [isLoadingBg, showBackground, setShowBackground] = useShowBackgroundStoreState()
14
54
  const [isLoadingSubtitle, subtitle, setSubtitle] = useSubtitleStoreState(250)
15
55
  const [isLoadingWelcome, welcomeMessage, setWelcomeMessage] = useWelcomeMessageStoreState(250)
16
56
 
57
+ const isLoading = isLoadingTheme || isLoadingScale || isLoadingPadding || isLoadingAnim || isLoadingBg
58
+
17
59
  return (
18
60
  <SettingsContainer>
61
+ {/* ── App-specific sections go here ─────────────────────────────── */}
62
+
63
+ <SettingsSection title="Content">
64
+ <SettingsField>
65
+ <SettingsLabel>Subtitle Text</SettingsLabel>
66
+ <SettingsInputFrame>
67
+ <input
68
+ type="text"
69
+ placeholder="Enter a subtitle..."
70
+ value={subtitle}
71
+ onChange={(e) => setSubtitle(e.target.value)}
72
+ disabled={isLoadingSubtitle}
73
+ />
74
+ </SettingsInputFrame>
75
+ <SettingsHint>Displayed below the welcome title on the render view</SettingsHint>
76
+ </SettingsField>
77
+ </SettingsSection>
19
78
 
20
- <SettingsHeading>Render</SettingsHeading>
79
+ {/* ── Appearance (common to all apps) ────────────────────────────── */}
21
80
 
22
- <SettingsField>
23
- <SettingsLabel>UI Scale</SettingsLabel>
24
- <SettingsSliderFrame>
25
- <input
26
- type="range"
27
- min={1}
28
- max={3}
29
- step={0.01}
30
- disabled={isLoadingUiScale}
31
- value={uiScale}
32
- onChange={(e) => setUiScale(parseFloat(e.target.value))}
33
- />
34
- <span>{uiScale}x</span>
35
- </SettingsSliderFrame>
36
- </SettingsField>
81
+ <SettingsSection title="Appearance">
82
+ <SettingsField>
83
+ <SettingsLabel>Theme</SettingsLabel>
84
+ <SettingsSelectFrame>
85
+ <select
86
+ disabled={isLoading}
87
+ value={themeName}
88
+ onChange={(e) => setThemeName(e.target.value)}
89
+ >
90
+ {Object.entries(themes).map(([key, theme]) => (
91
+ <option key={key} value={key}>{theme.label}</option>
92
+ ))}
93
+ </select>
94
+ </SettingsSelectFrame>
95
+ </SettingsField>
37
96
 
38
- <SettingsDivider />
97
+ <SettingsField>
98
+ <SettingsLabel>UI Scale</SettingsLabel>
99
+ <SettingsSliderFrame>
100
+ <input
101
+ type="range"
102
+ min={1}
103
+ max={3}
104
+ step={0.01}
105
+ disabled={isLoading}
106
+ value={uiScale}
107
+ onChange={(e) => setUiScale(parseFloat(e.target.value))}
108
+ />
109
+ <span>{uiScale}x</span>
110
+ </SettingsSliderFrame>
111
+ </SettingsField>
39
112
 
40
- <SettingsField>
41
- <SettingsLabel>Subtitle Text</SettingsLabel>
42
- <SettingsInputFrame>
43
- <input
44
- type="text"
45
- placeholder='Some text for the subtitle...'
46
- value={subtitle}
47
- onChange={(e) => setSubtitle(e.target.value)}
48
- disabled={isLoadingSubtitle}
49
- />
50
- </SettingsInputFrame>
51
- </SettingsField>
113
+ <SettingsField>
114
+ <SettingsLabel>Padding</SettingsLabel>
115
+ <SettingsSliderFrame>
116
+ <input
117
+ type="range"
118
+ min={0}
119
+ max={3}
120
+ step={0.01}
121
+ disabled={isLoading}
122
+ value={pagePadding}
123
+ onChange={(e) => setPagePadding(parseFloat(e.target.value))}
124
+ />
125
+ <span>{pagePadding}x</span>
126
+ </SettingsSliderFrame>
127
+ </SettingsField>
52
128
 
53
- <SettingsDivider />
129
+ <SettingsField>
130
+ <SettingsLabel>Entrance Animation</SettingsLabel>
131
+ <SettingsSelectFrame>
132
+ <select
133
+ disabled={isLoading}
134
+ value={animation}
135
+ onChange={(e) => setAnimation(e.target.value)}
136
+ >
137
+ <option value="fade-in">Fade</option>
138
+ <option value="fade">Fade Up</option>
139
+ <option value="flip">Flip</option>
140
+ <option value="unfold">Unfold</option>
141
+ <option value="scale">Scale</option>
142
+ <option value="zoom">Zoom</option>
143
+ <option value="slide">Slide</option>
144
+ <option value="drop">Drop</option>
145
+ <option value="bounce">Bounce</option>
146
+ <option value="rise">Rise</option>
147
+ <option value="blur">Blur</option>
148
+ <option value="glitch">Glitch</option>
149
+ <option value="none">None</option>
150
+ </select>
151
+ </SettingsSelectFrame>
152
+ </SettingsField>
54
153
 
55
- <SettingsHeading>Web</SettingsHeading>
154
+ <SettingsField>
155
+ <SettingsCheckboxFrame>
156
+ <input type="checkbox" disabled={isLoading} checked={showBackground} onChange={(e) => setShowBackground(e.target.checked)} />
157
+ <SettingsCheckboxLabel>Show Background</SettingsCheckboxLabel>
158
+ </SettingsCheckboxFrame>
159
+ <SettingsHint>Uncheck for a transparent background</SettingsHint>
160
+ </SettingsField>
161
+ </SettingsSection>
56
162
 
57
- <SettingsField>
58
- <SettingsLabel>Welcome Message</SettingsLabel>
59
- <SettingsInputFrame>
60
- <input
61
- type="text"
62
- placeholder='A welcome message for the web view...'
63
- value={welcomeMessage}
64
- onChange={(e) => setWelcomeMessage(e.target.value)}
65
- disabled={isLoadingWelcome}
66
- />
67
- </SettingsInputFrame>
68
- </SettingsField>
163
+ {/* ── Web ────────────────────────────────────────────────────────── */}
69
164
 
165
+ <SettingsSection title="Web">
166
+ <SettingsField>
167
+ <SettingsLabel>Welcome Message</SettingsLabel>
168
+ <SettingsInputFrame>
169
+ <input
170
+ type="text"
171
+ placeholder='A welcome message for the web view...'
172
+ value={welcomeMessage}
173
+ onChange={(e) => setWelcomeMessage(e.target.value)}
174
+ disabled={isLoadingWelcome}
175
+ />
176
+ </SettingsInputFrame>
177
+ </SettingsField>
178
+ </SettingsSection>
70
179
  </SettingsContainer>
71
180
  )
72
181
  }