@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.
- package/CHANGELOG.md +21 -0
- package/package.json +2 -2
- package/templates/claude-code/_claude/skills/tos-settings-ui/SKILL.md +128 -101
- package/templates/vite-react-typescript/index.html +1 -1
- package/templates/vite-react-typescript/public/assets/telemetryos-wordmark-dark.svg +11 -0
- package/templates/vite-react-typescript/src/components/FlickeringGrid.tsx +150 -0
- package/templates/vite-react-typescript/src/hooks/store.ts +32 -0
- package/templates/vite-react-typescript/src/themes.ts +226 -0
- package/templates/vite-react-typescript/src/views/Render.css +943 -5
- package/templates/vite-react-typescript/src/views/Render.tsx +139 -10
- package/templates/vite-react-typescript/src/views/Settings.tsx +145 -34
- package/templates/vite-react-typescript-web/index.html +1 -1
- package/templates/vite-react-typescript-web/public/assets/telemetryos-wordmark-dark.svg +11 -0
- package/templates/vite-react-typescript-web/src/components/FlickeringGrid.tsx +150 -0
- package/templates/vite-react-typescript-web/src/hooks/store.ts +35 -2
- package/templates/vite-react-typescript-web/src/themes.ts +226 -0
- package/templates/vite-react-typescript-web/src/views/Render.css +943 -5
- package/templates/vite-react-typescript-web/src/views/Render.tsx +139 -11
- package/templates/vite-react-typescript-web/src/views/Settings.tsx +156 -47
- /package/templates/vite-react-typescript-web/{assets → public/assets}/telemetryos-wordmark.svg +0 -0
- /package/templates/vite-react-typescript-web/{assets → public/assets}/tos-app.svg +0 -0
|
@@ -1,24 +1,152 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
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=
|
|
13
|
-
|
|
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
|
-
{
|
|
142
|
+
{density !== 'minimal' && (
|
|
16
143
|
<div className="render__hero-title">Welcome to TelemetryOS SDK</div>
|
|
17
144
|
)}
|
|
18
|
-
<div className="render__hero-subtitle">{
|
|
145
|
+
<div className="render__hero-subtitle">{subtitle}</div>
|
|
19
146
|
</div>
|
|
147
|
+
|
|
20
148
|
<div className="render__docs-information">
|
|
21
|
-
{
|
|
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
|
-
{
|
|
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
|
-
|
|
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 {
|
|
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 [
|
|
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
|
-
|
|
79
|
+
{/* ── Appearance (common to all apps) ────────────────────────────── */}
|
|
21
80
|
|
|
22
|
-
<
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
<
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
/package/templates/vite-react-typescript-web/{assets → public/assets}/telemetryos-wordmark.svg
RENAMED
|
File without changes
|
|
File without changes
|