@telemetryos/cli 1.11.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/dist/commands/claude-code.d.ts +2 -0
- package/dist/commands/claude-code.js +29 -0
- package/dist/commands/init.js +22 -9
- package/dist/index.js +2 -0
- package/dist/services/create-project.d.ts +13 -0
- package/dist/services/create-project.js +188 -0
- package/dist/services/project-config.d.ts +3 -0
- package/dist/services/project-config.js +3 -0
- package/dist/services/run-server.js +186 -60
- package/dist/utils/ansi.d.ts +1 -0
- package/dist/utils/ansi.js +1 -0
- package/dist/utils/template.d.ts +2 -0
- package/dist/utils/template.js +30 -0
- package/package.json +4 -4
- package/templates/{vite-react-typescript → claude-code}/CLAUDE.md +10 -3
- package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-architecture/SKILL.md +138 -61
- package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-debugging/SKILL.md +2 -2
- package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-media-api/SKILL.md +97 -10
- package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-multi-mode/SKILL.md +97 -4
- package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-requirements/SKILL.md +70 -5
- package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-store-sync/SKILL.md +4 -2
- package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-weather-api/SKILL.md +7 -6
- package/templates/claude-code/_claude/skills/tos-web-ui-design/SKILL.md +373 -0
- package/templates/vite-react-typescript/_gitignore +4 -2
- package/templates/vite-react-typescript/telemetry.config.json +2 -1
- package/templates/vite-react-typescript-web/_gitignore +32 -0
- package/templates/vite-react-typescript-web/assets/telemetryos-wordmark.svg +11 -0
- package/templates/vite-react-typescript-web/assets/tos-app.svg +12 -0
- package/templates/vite-react-typescript-web/index.html +15 -0
- package/templates/vite-react-typescript-web/package.json +24 -0
- package/templates/vite-react-typescript-web/src/App.tsx +25 -0
- package/templates/vite-react-typescript-web/src/hooks/store.ts +8 -0
- package/templates/vite-react-typescript-web/src/index.css +24 -0
- package/templates/vite-react-typescript-web/src/index.tsx +11 -0
- package/templates/vite-react-typescript-web/src/views/Render.css +67 -0
- package/templates/vite-react-typescript-web/src/views/Render.tsx +44 -0
- package/templates/vite-react-typescript-web/src/views/Settings.tsx +72 -0
- package/templates/vite-react-typescript-web/src/views/Web.css +105 -0
- package/templates/vite-react-typescript-web/src/views/Web.tsx +52 -0
- package/templates/vite-react-typescript-web/telemetry.config.json +16 -0
- package/templates/vite-react-typescript-web/tsconfig.json +19 -0
- package/templates/vite-react-typescript-web/vite.config.ts +18 -0
- package/templates/vite-react-typescript/_claude/skills/tos-render-design/SKILL.md +0 -624
- /package/templates/{vite-react-typescript → claude-code}/AGENTS.md +0 -0
- /package/templates/{vite-react-typescript → claude-code}/_claude/settings.local.json +0 -0
- /package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-proxy-fetch/SKILL.md +0 -0
- /package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-render-kiosk-design/SKILL.md +0 -0
- /package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-render-signage-design/SKILL.md +0 -0
- /package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-render-ui-design/SKILL.md +0 -0
- /package/templates/{vite-react-typescript → claude-code}/_claude/skills/tos-settings-ui/SKILL.md +0 -0
|
@@ -3,11 +3,20 @@ import { readFile } from 'fs/promises';
|
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import http from 'http';
|
|
5
5
|
import path from 'path';
|
|
6
|
-
import
|
|
6
|
+
import { createInterface } from 'readline/promises';
|
|
7
7
|
import serveHandler from 'serve-handler';
|
|
8
8
|
import pkg from '../../package.json' with { type: 'json' };
|
|
9
9
|
import { loadProjectConfig } from './project-config.js';
|
|
10
|
-
import { ansi } from '../utils/ansi.js';
|
|
10
|
+
import { ansi, ansiRegex } from '../utils/ansi.js';
|
|
11
|
+
// import { handlePublishCommand } from '../commands/publish.js'
|
|
12
|
+
const IMAGE_MIME_TYPES = {
|
|
13
|
+
'.jpg': 'image/jpeg',
|
|
14
|
+
'.jpeg': 'image/jpeg',
|
|
15
|
+
'.png': 'image/png',
|
|
16
|
+
'.gif': 'image/gif',
|
|
17
|
+
'.svg': 'image/svg+xml',
|
|
18
|
+
'.webp': 'image/webp',
|
|
19
|
+
};
|
|
11
20
|
export async function runServer(projectPath, flags) {
|
|
12
21
|
printSplashScreen();
|
|
13
22
|
projectPath = path.resolve(process.cwd(), projectPath);
|
|
@@ -21,6 +30,8 @@ export async function runServer(projectPath, flags) {
|
|
|
21
30
|
}
|
|
22
31
|
await serveDevelopmentApplicationHostUI(projectPath, flags.port, projectConfig);
|
|
23
32
|
await serveTelemetryApplication(projectPath, projectConfig);
|
|
33
|
+
// Print ready message after Vite is confirmed ready
|
|
34
|
+
printServerInfo(flags.port);
|
|
24
35
|
}
|
|
25
36
|
async function serveDevelopmentApplicationHostUI(projectPath, port, projectConfig) {
|
|
26
37
|
const hostUiPath = await import.meta.resolve('@telemetryos/development-application-host-ui/dist');
|
|
@@ -34,32 +45,12 @@ async function serveDevelopmentApplicationHostUI(projectPath, port, projectConfi
|
|
|
34
45
|
res.end(JSON.stringify(projectConfig));
|
|
35
46
|
return;
|
|
36
47
|
}
|
|
48
|
+
if (url.pathname === '/__tos-logo__') {
|
|
49
|
+
await serveImageFile(res, projectPath, projectConfig.logoPath, 'logo');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
37
52
|
if (url.pathname === '/__tos-thumbnail__') {
|
|
38
|
-
|
|
39
|
-
res.statusCode = 404;
|
|
40
|
-
res.end('No thumbnail configured');
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
const thumbnailFullPath = path.join(projectPath, projectConfig.thumbnailPath);
|
|
44
|
-
try {
|
|
45
|
-
const imageData = await readFile(thumbnailFullPath);
|
|
46
|
-
const ext = path.extname(projectConfig.thumbnailPath).toLowerCase();
|
|
47
|
-
const mimeTypes = {
|
|
48
|
-
'.jpg': 'image/jpeg',
|
|
49
|
-
'.jpeg': 'image/jpeg',
|
|
50
|
-
'.png': 'image/png',
|
|
51
|
-
'.gif': 'image/gif',
|
|
52
|
-
'.svg': 'image/svg+xml',
|
|
53
|
-
'.webp': 'image/webp',
|
|
54
|
-
};
|
|
55
|
-
const contentType = mimeTypes[ext] || 'application/octet-stream';
|
|
56
|
-
res.setHeader('Content-Type', contentType);
|
|
57
|
-
res.end(imageData);
|
|
58
|
-
}
|
|
59
|
-
catch {
|
|
60
|
-
res.statusCode = 404;
|
|
61
|
-
res.end('Thumbnail file not found');
|
|
62
|
-
}
|
|
53
|
+
await serveImageFile(res, projectPath, projectConfig.thumbnailPath, 'thumbnail');
|
|
63
54
|
return;
|
|
64
55
|
}
|
|
65
56
|
if (url.pathname === '/__dev_proxy__' && req.method === 'POST' && res) {
|
|
@@ -122,49 +113,184 @@ async function serveDevelopmentApplicationHostUI(projectPath, port, projectConfi
|
|
|
122
113
|
}
|
|
123
114
|
return;
|
|
124
115
|
}
|
|
116
|
+
// TODO: Publish endpoint disabled until auth is working
|
|
117
|
+
// if (url.pathname === '/__publish__') {
|
|
118
|
+
// if (req.method !== 'GET') {
|
|
119
|
+
// res.statusCode = 405
|
|
120
|
+
// res.end('Method not allowed')
|
|
121
|
+
// return
|
|
122
|
+
// }
|
|
123
|
+
//
|
|
124
|
+
// // Set SSE headers
|
|
125
|
+
// res.setHeader('Content-Type', 'text/event-stream')
|
|
126
|
+
// res.setHeader('Cache-Control', 'no-cache')
|
|
127
|
+
// res.setHeader('Connection', 'keep-alive')
|
|
128
|
+
//
|
|
129
|
+
// const sendEvent = (event: string, data: any) => {
|
|
130
|
+
// res.write(`event: ${event}\n`)
|
|
131
|
+
// res.write(`data: ${JSON.stringify(data)}\n\n`)
|
|
132
|
+
// }
|
|
133
|
+
//
|
|
134
|
+
// try {
|
|
135
|
+
// sendEvent('state', { state: 'starting' })
|
|
136
|
+
//
|
|
137
|
+
// await handlePublishCommand(
|
|
138
|
+
// projectPath,
|
|
139
|
+
// {},
|
|
140
|
+
// {
|
|
141
|
+
// onLog: (line: string) => {
|
|
142
|
+
// sendEvent('log', { message: line })
|
|
143
|
+
// },
|
|
144
|
+
// onStateChange: (state: string) => {
|
|
145
|
+
// sendEvent('state', { state })
|
|
146
|
+
// },
|
|
147
|
+
// onComplete: (data: { success: boolean; buildIndex?: number; duration?: string }) => {
|
|
148
|
+
// sendEvent('complete', data)
|
|
149
|
+
// res.end()
|
|
150
|
+
// },
|
|
151
|
+
// onError: (error: Error) => {
|
|
152
|
+
// const isAuthError =
|
|
153
|
+
// error.message.includes('authenticate') ||
|
|
154
|
+
// error.message.includes('Not authenticated')
|
|
155
|
+
// sendEvent('error', {
|
|
156
|
+
// error: error.message,
|
|
157
|
+
// code: isAuthError ? 'AUTH_REQUIRED' : 'PUBLISH_FAILED',
|
|
158
|
+
// message: isAuthError
|
|
159
|
+
// ? "Run 'tos auth' in the terminal to authenticate"
|
|
160
|
+
// : error.message,
|
|
161
|
+
// })
|
|
162
|
+
// res.end()
|
|
163
|
+
// },
|
|
164
|
+
// },
|
|
165
|
+
// )
|
|
166
|
+
// } catch (error) {
|
|
167
|
+
// sendEvent('error', {
|
|
168
|
+
// error: (error as Error).message,
|
|
169
|
+
// code: 'UNEXPECTED_ERROR',
|
|
170
|
+
// message: (error as Error).message,
|
|
171
|
+
// })
|
|
172
|
+
// res.end()
|
|
173
|
+
// }
|
|
174
|
+
// return
|
|
175
|
+
// }
|
|
125
176
|
serveHandler(req, res, serveConfig).catch((err) => {
|
|
126
177
|
console.error('Error handling request:', err);
|
|
127
178
|
res.statusCode = 500;
|
|
128
179
|
res.end('Internal Server Error');
|
|
129
180
|
});
|
|
130
181
|
});
|
|
131
|
-
printServerInfo(port);
|
|
132
182
|
server.listen(port);
|
|
133
183
|
}
|
|
134
|
-
async function
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
184
|
+
async function serveImageFile(res, projectPath, filePath, label) {
|
|
185
|
+
if (!filePath) {
|
|
186
|
+
res.statusCode = 404;
|
|
187
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
188
|
+
res.end(`No ${label} configured`);
|
|
138
189
|
return;
|
|
139
190
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
|
|
191
|
+
try {
|
|
192
|
+
const fullPath = path.resolve(projectPath, filePath);
|
|
193
|
+
if (!fullPath.startsWith(path.resolve(projectPath) + path.sep) &&
|
|
194
|
+
fullPath !== path.resolve(projectPath)) {
|
|
195
|
+
res.statusCode = 403;
|
|
196
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
197
|
+
res.end('Forbidden: path escapes project root');
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const imageData = await readFile(fullPath);
|
|
201
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
202
|
+
const contentType = IMAGE_MIME_TYPES[ext] || 'application/octet-stream';
|
|
203
|
+
res.setHeader('Content-Type', contentType);
|
|
204
|
+
res.end(imageData);
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
208
|
+
if ((error === null || error === void 0 ? void 0 : error.code) === 'ENOENT') {
|
|
209
|
+
res.statusCode = 404;
|
|
210
|
+
res.end(`${label.charAt(0).toUpperCase() + label.slice(1)} file not found`);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
res.statusCode = 500;
|
|
214
|
+
res.end(`Error reading ${label}: ${(error === null || error === void 0 ? void 0 : error.message) || 'unknown error'}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async function serveTelemetryApplication(rootPath, projectConfig) {
|
|
219
|
+
return new Promise((resolve, reject) => {
|
|
220
|
+
var _a;
|
|
221
|
+
if (!((_a = projectConfig === null || projectConfig === void 0 ? void 0 : projectConfig.devServer) === null || _a === void 0 ? void 0 : _a.runCommand)) {
|
|
222
|
+
console.log('No value in config at devServer.runCommand');
|
|
223
|
+
resolve();
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const runCommand = projectConfig.devServer.runCommand;
|
|
227
|
+
const binPath = path.join(rootPath, 'node_modules', '.bin');
|
|
228
|
+
const childProcess = spawn(runCommand, {
|
|
229
|
+
shell: true,
|
|
230
|
+
env: {
|
|
231
|
+
...process.env,
|
|
232
|
+
FORCE_COLOR: '1',
|
|
233
|
+
PATH: `${binPath}${path.delimiter}${process.env.PATH}`,
|
|
234
|
+
},
|
|
235
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
236
|
+
cwd: rootPath,
|
|
237
|
+
});
|
|
238
|
+
const stdoutReadline = createInterface({
|
|
239
|
+
input: childProcess.stdout,
|
|
240
|
+
crlfDelay: Infinity,
|
|
241
|
+
});
|
|
242
|
+
const stderrReadline = createInterface({
|
|
243
|
+
input: childProcess.stderr,
|
|
244
|
+
crlfDelay: Infinity,
|
|
245
|
+
});
|
|
246
|
+
let cleaned = false;
|
|
247
|
+
const cleanup = () => {
|
|
248
|
+
if (cleaned)
|
|
249
|
+
return;
|
|
250
|
+
cleaned = true;
|
|
251
|
+
process.removeListener('exit', onParentExit);
|
|
252
|
+
clearTimeout(timeoutHandle);
|
|
253
|
+
childProcess.kill();
|
|
254
|
+
stdoutReadline.close();
|
|
255
|
+
stderrReadline.close();
|
|
256
|
+
};
|
|
257
|
+
const onParentExit = () => {
|
|
258
|
+
console.log('Shutting down development server...');
|
|
259
|
+
cleanup();
|
|
260
|
+
};
|
|
261
|
+
process.on('exit', onParentExit);
|
|
262
|
+
const timeoutHandle = setTimeout(() => {
|
|
263
|
+
cleanup();
|
|
264
|
+
reject(new Error('Vite dev server did not start within 30 seconds'));
|
|
265
|
+
}, 30000);
|
|
266
|
+
let viteReady = false;
|
|
267
|
+
stdoutReadline.on('line', (line) => {
|
|
268
|
+
console.log(`[application]: ${line}`);
|
|
269
|
+
// Detect Vite ready signal
|
|
270
|
+
if (!viteReady) {
|
|
271
|
+
const cleanLine = line.replace(ansiRegex, '');
|
|
272
|
+
if (cleanLine.includes('VITE') && cleanLine.includes('ready in')) {
|
|
273
|
+
viteReady = true;
|
|
274
|
+
clearTimeout(timeoutHandle);
|
|
275
|
+
resolve();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
stderrReadline.on('line', (line) => {
|
|
280
|
+
console.error(`[application]: ${line}`);
|
|
281
|
+
});
|
|
282
|
+
childProcess.on('error', (error) => {
|
|
283
|
+
if (!viteReady) {
|
|
284
|
+
cleanup();
|
|
285
|
+
reject(error);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
childProcess.on('close', (code) => {
|
|
289
|
+
if (!viteReady) {
|
|
290
|
+
cleanup();
|
|
291
|
+
reject(new Error(`Dev server process exited with code ${code} before becoming ready`));
|
|
292
|
+
}
|
|
293
|
+
});
|
|
168
294
|
});
|
|
169
295
|
}
|
|
170
296
|
function printSplashScreen() {
|
package/dist/utils/ansi.d.ts
CHANGED
package/dist/utils/ansi.js
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
export const templatesDir = path.join(import.meta.dirname, '../../templates');
|
|
4
|
+
const ignoredTemplateFiles = ['.DS_Store', 'thumbs.db', 'node_modules', '.git', 'dist'];
|
|
5
|
+
const dotfileNames = ['_gitignore', '_claude'];
|
|
6
|
+
export async function copyDir(source, destination, replacements, progressFn) {
|
|
7
|
+
const dirListing = await fs.readdir(source);
|
|
8
|
+
for (const dirEntry of dirListing) {
|
|
9
|
+
if (ignoredTemplateFiles.includes(dirEntry))
|
|
10
|
+
continue;
|
|
11
|
+
const sourcePath = path.join(source, dirEntry);
|
|
12
|
+
const destinationPath = path.join(destination, dotfileNames.includes(dirEntry) ? `.${dirEntry.slice(1)}` : dirEntry);
|
|
13
|
+
const stats = await fs.stat(sourcePath);
|
|
14
|
+
if (stats.isDirectory()) {
|
|
15
|
+
await fs.mkdir(destinationPath, { recursive: true });
|
|
16
|
+
await copyDir(sourcePath, destinationPath, replacements, progressFn);
|
|
17
|
+
}
|
|
18
|
+
else if (stats.isFile()) {
|
|
19
|
+
await copyFile(sourcePath, destinationPath, replacements, progressFn);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function copyFile(source, destination, replacements, progressFn) {
|
|
24
|
+
let contents = await fs.readFile(source, 'utf-8');
|
|
25
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
26
|
+
contents = contents.replace(new RegExp(`{{${key}}}`, 'g'), value);
|
|
27
|
+
}
|
|
28
|
+
await fs.writeFile(destination, contents, 'utf-8');
|
|
29
|
+
progressFn(destination);
|
|
30
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telemetryos/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.0",
|
|
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.
|
|
28
|
+
"@telemetryos/development-application-host-ui": "^1.13.0",
|
|
29
29
|
"@types/serve-handler": "^6.1.4",
|
|
30
30
|
"commander": "^14.0.0",
|
|
31
31
|
"ignore": "^6.0.2",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"eslint-plugin-prettier": "^5.4.0",
|
|
44
44
|
"globals": "^16.0.0",
|
|
45
45
|
"prettier": "^3.5.3",
|
|
46
|
-
"typescript
|
|
47
|
-
"typescript": "^
|
|
46
|
+
"typescript": "^5.8.3",
|
|
47
|
+
"typescript-eslint": "^8.49.0"
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
50
|
"tos": "pnpm build && node dist/index.js",
|
|
@@ -16,19 +16,21 @@ tos serve # Start dev server (or: npm run dev)
|
|
|
16
16
|
Both the render and settings mounts points are visible in the development host.
|
|
17
17
|
The Render mount point is presented in a resizable pane.
|
|
18
18
|
The Settings mount point shows in the right sidebar.
|
|
19
|
+
The Web mount point (if configured) appears as a tab in the development host, displayed in the same area as the other mount points.
|
|
19
20
|
|
|
20
21
|
**The development host is already running!** The user has already started it and the agent doesn't need to run it
|
|
21
22
|
|
|
22
23
|
## Architecture
|
|
23
24
|
|
|
24
|
-
TelemetryOS apps have two mount points:
|
|
25
|
+
TelemetryOS apps have at least two mount points. Apps that need a browser-accessible interface add a third: the Web mount point.
|
|
25
26
|
|
|
26
27
|
| Mount | Purpose | Runs On |
|
|
27
28
|
|-------|---------|---------|
|
|
28
29
|
| `/render` | Content displayed on devices | Physical device (TV, kiosk) |
|
|
29
30
|
| `/settings` | Configuration UI | Studio admin portal |
|
|
31
|
+
| `/web` (optional) | Browser-accessible interface | Any browser (phone, tablet, desktop) |
|
|
30
32
|
|
|
31
|
-
Settings
|
|
33
|
+
Instance store hooks sync Settings ↔ Render. Application store hooks are accessible from Settings, Render, and Web. Web views typically use application and dynamic namespace store hooks for their data.
|
|
32
34
|
|
|
33
35
|
## Project Structure
|
|
34
36
|
|
|
@@ -38,7 +40,10 @@ src/
|
|
|
38
40
|
├── App.tsx # Mount point routing
|
|
39
41
|
├── views/
|
|
40
42
|
│ ├── Settings.tsx # Configuration UI
|
|
41
|
-
│
|
|
43
|
+
│ ├── Render.tsx # Display content
|
|
44
|
+
│ ├── Render.css # Render styles
|
|
45
|
+
│ ├── Web.tsx # Browser interface (if using web)
|
|
46
|
+
│ └── Web.css # Web view styles (if using web)
|
|
42
47
|
└── hooks/
|
|
43
48
|
└── store.ts # Store state hooks
|
|
44
49
|
```
|
|
@@ -60,6 +65,7 @@ export const useUnitsState = createUseInstanceStoreState<'imperial' | 'metric'>(
|
|
|
60
65
|
import { useCityState } from '../hooks/store'
|
|
61
66
|
|
|
62
67
|
const [isLoading, city, setCity] = useCityState()
|
|
68
|
+
if (isLoading) return <div>Loading...</div>
|
|
63
69
|
```
|
|
64
70
|
|
|
65
71
|
### Settings Components
|
|
@@ -113,6 +119,7 @@ const response = await proxy().fetch('https://api.example.com/data')
|
|
|
113
119
|
| Adding ANY Settings UI | `tos-settings-ui` | SDK components are required - raw HTML won't work |
|
|
114
120
|
| Adding store keys | `tos-store-sync` | Hook patterns ensure Settings↔Render sync |
|
|
115
121
|
| Building multi-mode apps | `tos-multi-mode` | Entity-scoped data, mode switching, namespace patterns |
|
|
122
|
+
| Building web views | `tos-web-ui-design` | Web mount point routing, styling, store access |
|
|
116
123
|
| Calling external APIs | `tos-proxy-fetch` | Proxy patterns prevent CORS errors |
|
|
117
124
|
| Media library access | `tos-media-api` | SDK media methods and types |
|
|
118
125
|
| Weather integration | `tos-weather-api` | API-specific patterns and credentials |
|