@even-toolkit/create-even-app 1.1.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/index.js +159 -0
- package/package.json +28 -0
- package/templates/chat/README.md +27 -0
- package/templates/chat/index.html +12 -0
- package/templates/chat/package.json +34 -0
- package/templates/chat/src/App.tsx +61 -0
- package/templates/chat/src/app.css +54 -0
- package/templates/chat/src/contexts/ChatContext.tsx +99 -0
- package/templates/chat/src/glass/AppGlasses.tsx +70 -0
- package/templates/chat/src/glass/screens/home.ts +24 -0
- package/templates/chat/src/glass/selectors.ts +9 -0
- package/templates/chat/src/glass/shared.ts +8 -0
- package/templates/chat/src/glass/splash.ts +25 -0
- package/templates/chat/src/main.tsx +13 -0
- package/templates/chat/src/screens/ChatScreen.tsx +69 -0
- package/templates/chat/src/screens/Settings.tsx +88 -0
- package/templates/chat/src/types.ts +13 -0
- package/templates/chat/src/vite-env.d.ts +1 -0
- package/templates/chat/template.json +7 -0
- package/templates/chat/tsconfig.json +20 -0
- package/templates/chat/tsconfig.node.json +13 -0
- package/templates/chat/vite.config.ts +12 -0
- package/templates/dashboard/README.md +17 -0
- package/templates/dashboard/index.html +12 -0
- package/templates/dashboard/package.json +34 -0
- package/templates/dashboard/src/App.tsx +27 -0
- package/templates/dashboard/src/app.css +54 -0
- package/templates/dashboard/src/glass/AppGlasses.tsx +53 -0
- package/templates/dashboard/src/glass/screens/home.ts +23 -0
- package/templates/dashboard/src/glass/selectors.ts +9 -0
- package/templates/dashboard/src/glass/shared.ts +8 -0
- package/templates/dashboard/src/glass/splash.ts +22 -0
- package/templates/dashboard/src/main.tsx +13 -0
- package/templates/dashboard/src/screens/ChartsScreen.tsx +99 -0
- package/templates/dashboard/src/screens/OverviewScreen.tsx +102 -0
- package/templates/dashboard/src/screens/SettingsScreen.tsx +60 -0
- package/templates/dashboard/src/vite-env.d.ts +1 -0
- package/templates/dashboard/template.json +7 -0
- package/templates/dashboard/tsconfig.json +20 -0
- package/templates/dashboard/tsconfig.node.json +13 -0
- package/templates/dashboard/vite.config.ts +12 -0
- package/templates/media/README.md +27 -0
- package/templates/media/index.html +12 -0
- package/templates/media/package.json +34 -0
- package/templates/media/src/App.tsx +24 -0
- package/templates/media/src/app.css +54 -0
- package/templates/media/src/contexts/MediaContext.tsx +108 -0
- package/templates/media/src/glass/AppGlasses.tsx +59 -0
- package/templates/media/src/glass/screens/home.ts +24 -0
- package/templates/media/src/glass/selectors.ts +9 -0
- package/templates/media/src/glass/shared.ts +8 -0
- package/templates/media/src/glass/splash.ts +25 -0
- package/templates/media/src/layouts/shell.tsx +39 -0
- package/templates/media/src/main.tsx +13 -0
- package/templates/media/src/screens/AudioScreen.tsx +78 -0
- package/templates/media/src/screens/GalleryScreen.tsx +98 -0
- package/templates/media/src/screens/Settings.tsx +86 -0
- package/templates/media/src/screens/UploadScreen.tsx +95 -0
- package/templates/media/src/types.ts +29 -0
- package/templates/media/src/vite-env.d.ts +1 -0
- package/templates/media/template.json +7 -0
- package/templates/media/tsconfig.json +20 -0
- package/templates/media/tsconfig.node.json +13 -0
- package/templates/media/vite.config.ts +12 -0
- package/templates/minimal/README.md +27 -0
- package/templates/minimal/index.html +12 -0
- package/templates/minimal/package.json +34 -0
- package/templates/minimal/src/App.tsx +50 -0
- package/templates/minimal/src/app.css +54 -0
- package/templates/minimal/src/glass/AppGlasses.tsx +54 -0
- package/templates/minimal/src/glass/screens/home.ts +24 -0
- package/templates/minimal/src/glass/selectors.ts +9 -0
- package/templates/minimal/src/glass/shared.ts +8 -0
- package/templates/minimal/src/glass/splash.ts +25 -0
- package/templates/minimal/src/main.tsx +13 -0
- package/templates/minimal/src/vite-env.d.ts +1 -0
- package/templates/minimal/template.json +7 -0
- package/templates/minimal/tsconfig.json +20 -0
- package/templates/minimal/tsconfig.node.json +13 -0
- package/templates/minimal/vite.config.ts +12 -0
- package/templates/notes/README.md +27 -0
- package/templates/notes/index.html +12 -0
- package/templates/notes/package.json +34 -0
- package/templates/notes/src/App.tsx +25 -0
- package/templates/notes/src/app.css +54 -0
- package/templates/notes/src/contexts/NotesContext.tsx +140 -0
- package/templates/notes/src/glass/AppGlasses.tsx +58 -0
- package/templates/notes/src/glass/screens/home.ts +24 -0
- package/templates/notes/src/glass/selectors.ts +9 -0
- package/templates/notes/src/glass/shared.ts +8 -0
- package/templates/notes/src/glass/splash.ts +24 -0
- package/templates/notes/src/layouts/shell.tsx +36 -0
- package/templates/notes/src/main.tsx +13 -0
- package/templates/notes/src/screens/NoteDetail.tsx +104 -0
- package/templates/notes/src/screens/NoteForm.tsx +84 -0
- package/templates/notes/src/screens/NoteList.tsx +108 -0
- package/templates/notes/src/screens/Settings.tsx +88 -0
- package/templates/notes/src/types.ts +14 -0
- package/templates/notes/src/vite-env.d.ts +1 -0
- package/templates/notes/template.json +7 -0
- package/templates/notes/tsconfig.json +20 -0
- package/templates/notes/tsconfig.node.json +13 -0
- package/templates/notes/vite.config.ts +12 -0
- package/templates/tracker/README.md +27 -0
- package/templates/tracker/index.html +12 -0
- package/templates/tracker/package.json +34 -0
- package/templates/tracker/src/App.tsx +24 -0
- package/templates/tracker/src/app.css +54 -0
- package/templates/tracker/src/contexts/TrackerContext.tsx +193 -0
- package/templates/tracker/src/glass/AppGlasses.tsx +64 -0
- package/templates/tracker/src/glass/screens/home.ts +24 -0
- package/templates/tracker/src/glass/selectors.ts +9 -0
- package/templates/tracker/src/glass/shared.ts +8 -0
- package/templates/tracker/src/glass/splash.ts +24 -0
- package/templates/tracker/src/layouts/shell.tsx +37 -0
- package/templates/tracker/src/main.tsx +13 -0
- package/templates/tracker/src/screens/HistoryScreen.tsx +106 -0
- package/templates/tracker/src/screens/NewEntryScreen.tsx +135 -0
- package/templates/tracker/src/screens/Settings.tsx +135 -0
- package/templates/tracker/src/screens/TodayScreen.tsx +147 -0
- package/templates/tracker/src/types.ts +34 -0
- package/templates/tracker/src/vite-env.d.ts +1 -0
- package/templates/tracker/template.json +7 -0
- package/templates/tracker/tsconfig.json +20 -0
- package/templates/tracker/tsconfig.node.json +13 -0
- package/templates/tracker/vite.config.ts +12 -0
package/index.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import readline from 'readline';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const TEMPLATES_DIR = path.join(__dirname, 'templates');
|
|
10
|
+
|
|
11
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
12
|
+
const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
13
|
+
|
|
14
|
+
// ── Load available templates ──
|
|
15
|
+
|
|
16
|
+
function loadTemplates() {
|
|
17
|
+
const dirs = fs.readdirSync(TEMPLATES_DIR).filter((d) =>
|
|
18
|
+
fs.statSync(path.join(TEMPLATES_DIR, d)).isDirectory() &&
|
|
19
|
+
fs.existsSync(path.join(TEMPLATES_DIR, d, 'template.json'))
|
|
20
|
+
);
|
|
21
|
+
return dirs.map((d) => {
|
|
22
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(TEMPLATES_DIR, d, 'template.json'), 'utf8'));
|
|
23
|
+
return { dir: d, ...manifest };
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ── Interactive template picker ──
|
|
28
|
+
|
|
29
|
+
async function pickTemplate(templates, preselected) {
|
|
30
|
+
if (preselected) {
|
|
31
|
+
const match = templates.find((t) => t.name === preselected || t.dir === preselected);
|
|
32
|
+
if (match) return match;
|
|
33
|
+
console.error(`Template "${preselected}" not found. Available: ${templates.map((t) => t.name).join(', ')}`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log('\nAvailable templates:\n');
|
|
38
|
+
templates.forEach((t, i) => {
|
|
39
|
+
console.log(` ${i + 1}. ${t.name.padEnd(14)} — ${t.description}`);
|
|
40
|
+
});
|
|
41
|
+
console.log();
|
|
42
|
+
|
|
43
|
+
const choice = await ask(`Choose a template [1-${templates.length}] (default 1): `);
|
|
44
|
+
const idx = (parseInt(choice, 10) || 1) - 1;
|
|
45
|
+
if (idx < 0 || idx >= templates.length) {
|
|
46
|
+
console.error('Invalid choice.');
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
return templates[idx];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ── Copy directory recursively, replacing tokens ──
|
|
53
|
+
|
|
54
|
+
const TOKEN_FILES = new Set([
|
|
55
|
+
'package.json', 'app.json', 'index.html', 'README.md',
|
|
56
|
+
'App.tsx', 'main.tsx', 'shell.tsx', 'splash.ts', 'AppGlasses.tsx', 'selectors.ts',
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
function copyDir(src, dest, tokens) {
|
|
60
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
61
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
62
|
+
if (entry.name === 'template.json' || entry.name === 'node_modules') continue;
|
|
63
|
+
const srcPath = path.join(src, entry.name);
|
|
64
|
+
const destPath = path.join(dest, entry.name);
|
|
65
|
+
if (entry.isDirectory()) {
|
|
66
|
+
copyDir(srcPath, destPath, tokens);
|
|
67
|
+
} else {
|
|
68
|
+
let content = fs.readFileSync(srcPath, 'utf8');
|
|
69
|
+
if (TOKEN_FILES.has(entry.name) || entry.name.endsWith('.tsx') || entry.name.endsWith('.ts') || entry.name.endsWith('.json') || entry.name.endsWith('.html') || entry.name.endsWith('.md')) {
|
|
70
|
+
for (const [key, value] of Object.entries(tokens)) {
|
|
71
|
+
content = content.replaceAll(key, value);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
fs.writeFileSync(destPath, content);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Main ──
|
|
80
|
+
|
|
81
|
+
async function main() {
|
|
82
|
+
const args = process.argv.slice(2);
|
|
83
|
+
let appName = null;
|
|
84
|
+
let templateName = null;
|
|
85
|
+
|
|
86
|
+
// Parse args
|
|
87
|
+
for (let i = 0; i < args.length; i++) {
|
|
88
|
+
if (args[i] === '--template' || args[i] === '-t') {
|
|
89
|
+
templateName = args[++i];
|
|
90
|
+
} else if (!args[i].startsWith('-')) {
|
|
91
|
+
appName = args[i];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log(`
|
|
96
|
+
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
|
|
97
|
+
░ @even-toolkit/create-even-app ░
|
|
98
|
+
░ G2 Smart Glasses App Scaffold ░
|
|
99
|
+
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
|
|
100
|
+
`);
|
|
101
|
+
|
|
102
|
+
// App name
|
|
103
|
+
if (!appName) {
|
|
104
|
+
appName = await ask('App name (e.g. my-app): ');
|
|
105
|
+
}
|
|
106
|
+
if (!appName) {
|
|
107
|
+
console.error('App name is required.');
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const dir = path.resolve(process.cwd(), appName);
|
|
112
|
+
if (fs.existsSync(dir)) {
|
|
113
|
+
console.error(`Directory "${appName}" already exists.`);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Template selection
|
|
118
|
+
const templates = loadTemplates();
|
|
119
|
+
if (templates.length === 0) {
|
|
120
|
+
console.error('No templates found in templates/ directory.');
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
const template = await pickTemplate(templates, templateName);
|
|
124
|
+
|
|
125
|
+
// Package ID
|
|
126
|
+
const defaultId = `com.${appName.replace(/[^a-z0-9]/g, '')}.g2`;
|
|
127
|
+
const packageId = await ask(`Package ID (${defaultId}): `) || defaultId;
|
|
128
|
+
|
|
129
|
+
const displayName = appName
|
|
130
|
+
.split(/[-_]/)
|
|
131
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
132
|
+
.join(' ');
|
|
133
|
+
|
|
134
|
+
rl.close();
|
|
135
|
+
|
|
136
|
+
console.log(`\nScaffolding "${displayName}" with "${template.name}" template...\n`);
|
|
137
|
+
|
|
138
|
+
// Copy template with token replacement
|
|
139
|
+
const tokens = {
|
|
140
|
+
'{{APP_NAME}}': appName,
|
|
141
|
+
'{{DISPLAY_NAME}}': displayName,
|
|
142
|
+
'{{DISPLAY_NAME_UPPER}}': displayName.toUpperCase(),
|
|
143
|
+
'{{PACKAGE_ID}}': packageId,
|
|
144
|
+
};
|
|
145
|
+
copyDir(path.join(TEMPLATES_DIR, template.dir), dir, tokens);
|
|
146
|
+
|
|
147
|
+
console.log(`Done! Next steps:\n`);
|
|
148
|
+
console.log(` cd ${appName}`);
|
|
149
|
+
console.log(` npm install`);
|
|
150
|
+
console.log(` npm run dev\n`);
|
|
151
|
+
console.log(` # Build for Even Hub:`);
|
|
152
|
+
console.log(` npm run build`);
|
|
153
|
+
console.log(` npx @evenrealities/evenhub-cli pack app.json dist\n`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
main().catch((err) => {
|
|
157
|
+
console.error(err);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@even-toolkit/create-even-app",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Scaffold a new Even Realities G2 glasses app from a template gallery",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-even-app": "./index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.js",
|
|
11
|
+
"templates/"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"even-realities",
|
|
15
|
+
"even-toolkit",
|
|
16
|
+
"g2-glasses",
|
|
17
|
+
"smart-glasses",
|
|
18
|
+
"scaffold",
|
|
19
|
+
"create-app",
|
|
20
|
+
"template"
|
|
21
|
+
],
|
|
22
|
+
"author": "fabioglimb",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/fabioglimb/create-even-app"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# {{DISPLAY_NAME}}
|
|
2
|
+
|
|
3
|
+
A G2 smart glasses app built with [even-toolkit](https://www.npmjs.com/package/even-toolkit).
|
|
4
|
+
|
|
5
|
+
## Development
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
npm run dev
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Open [http://localhost:5173](http://localhost:5173) in your browser.
|
|
13
|
+
|
|
14
|
+
## Test with Simulator
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npx @evenrealities/evenhub-simulator@latest http://localhost:5173
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Build for Even Hub
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm run build
|
|
24
|
+
npx @evenrealities/evenhub-cli pack app.json dist
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Upload the generated `.ehpk` file to the Even Hub.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
|
6
|
+
<title>{{DISPLAY_NAME}}</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{APP_NAME}}",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite --port 5173 --host",
|
|
8
|
+
"build": "tsc -b && vite build",
|
|
9
|
+
"preview": "vite preview"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@evenrealities/even_hub_sdk": "^0.0.9",
|
|
13
|
+
"@jappyjan/even-better-sdk": "^0.0.11",
|
|
14
|
+
"class-variance-authority": "^0.7.1",
|
|
15
|
+
"clsx": "^2.1.0",
|
|
16
|
+
"even-toolkit": "^1.5.0",
|
|
17
|
+
"react": "^19.0.0",
|
|
18
|
+
"react-dom": "^19.0.0",
|
|
19
|
+
"react-is": "^19.2.4",
|
|
20
|
+
"react-router": "^7.0.0",
|
|
21
|
+
"tailwind-merge": "^3.0.0",
|
|
22
|
+
"upng-js": "^2.1.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@tailwindcss/vite": "^4.0.0",
|
|
26
|
+
"@types/react": "^19.0.0",
|
|
27
|
+
"@types/react-dom": "^19.0.0",
|
|
28
|
+
"@types/node": "^22.0.0",
|
|
29
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
30
|
+
"tailwindcss": "^4.0.0",
|
|
31
|
+
"typescript": "~5.4.0",
|
|
32
|
+
"vite": "^5.4.0"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Routes, Route, useNavigate, useLocation } from 'react-router'
|
|
2
|
+
import { AppShell, NavHeader, Button } from 'even-toolkit/web'
|
|
3
|
+
import { IcSettings, IcChevronBack } from 'even-toolkit/web/icons/svg-icons'
|
|
4
|
+
import { ChatProvider } from './contexts/ChatContext'
|
|
5
|
+
import { ChatScreen } from './screens/ChatScreen'
|
|
6
|
+
import { Settings } from './screens/Settings'
|
|
7
|
+
import { AppGlasses } from './glass/AppGlasses'
|
|
8
|
+
|
|
9
|
+
function ChatLayout() {
|
|
10
|
+
const navigate = useNavigate()
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<AppShell
|
|
14
|
+
header={
|
|
15
|
+
<NavHeader
|
|
16
|
+
title="{{DISPLAY_NAME}}"
|
|
17
|
+
right={
|
|
18
|
+
<Button variant="ghost" size="icon" onClick={() => navigate('/settings')}>
|
|
19
|
+
<IcSettings className="w-5 h-5" />
|
|
20
|
+
</Button>
|
|
21
|
+
}
|
|
22
|
+
/>
|
|
23
|
+
}
|
|
24
|
+
>
|
|
25
|
+
<ChatScreen />
|
|
26
|
+
<AppGlasses />
|
|
27
|
+
</AppShell>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function SettingsLayout() {
|
|
32
|
+
const navigate = useNavigate()
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<AppShell
|
|
36
|
+
header={
|
|
37
|
+
<NavHeader
|
|
38
|
+
title="Settings"
|
|
39
|
+
left={
|
|
40
|
+
<Button variant="ghost" size="icon" onClick={() => navigate('/')}>
|
|
41
|
+
<IcChevronBack className="w-5 h-5" />
|
|
42
|
+
</Button>
|
|
43
|
+
}
|
|
44
|
+
/>
|
|
45
|
+
}
|
|
46
|
+
>
|
|
47
|
+
<Settings />
|
|
48
|
+
</AppShell>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function App() {
|
|
53
|
+
return (
|
|
54
|
+
<ChatProvider>
|
|
55
|
+
<Routes>
|
|
56
|
+
<Route path="/" element={<ChatLayout />} />
|
|
57
|
+
<Route path="/settings" element={<SettingsLayout />} />
|
|
58
|
+
</Routes>
|
|
59
|
+
</ChatProvider>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@import "even-toolkit/web/theme-light.css";
|
|
3
|
+
@import "even-toolkit/web/typography.css";
|
|
4
|
+
@import "even-toolkit/web/utilities.css";
|
|
5
|
+
@source "../src";
|
|
6
|
+
@source "../node_modules/even-toolkit/web";
|
|
7
|
+
@source "../node_modules/even-toolkit/dist";
|
|
8
|
+
|
|
9
|
+
@theme {
|
|
10
|
+
--color-bg: var(--color-bg);
|
|
11
|
+
--color-surface: var(--color-surface);
|
|
12
|
+
--color-surface-light: var(--color-surface-light);
|
|
13
|
+
--color-surface-lighter: var(--color-surface-lighter);
|
|
14
|
+
--color-border: var(--color-border);
|
|
15
|
+
--color-border-light: var(--color-border-light);
|
|
16
|
+
--color-text: var(--color-text);
|
|
17
|
+
--color-text-dim: var(--color-text-dim);
|
|
18
|
+
--color-text-muted: var(--color-text-muted);
|
|
19
|
+
--color-text-highlight: var(--color-text-highlight);
|
|
20
|
+
--color-accent: var(--color-accent);
|
|
21
|
+
--color-accent-alpha: var(--color-accent-alpha);
|
|
22
|
+
--color-accent-warning: var(--color-accent-warning);
|
|
23
|
+
--color-positive: var(--color-positive);
|
|
24
|
+
--color-positive-alpha: var(--color-positive-alpha);
|
|
25
|
+
--color-negative: var(--color-negative);
|
|
26
|
+
--color-negative-alpha: var(--color-negative-alpha);
|
|
27
|
+
--color-overlay: var(--color-overlay);
|
|
28
|
+
--color-input-bg: var(--color-input-bg);
|
|
29
|
+
--radius-default: var(--radius-default);
|
|
30
|
+
--font-display: var(--font-display);
|
|
31
|
+
--font-body: var(--font-body);
|
|
32
|
+
--font-mono: var(--font-mono);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
html, body {
|
|
36
|
+
background: var(--color-bg);
|
|
37
|
+
color: var(--color-text);
|
|
38
|
+
min-height: 100dvh;
|
|
39
|
+
font-family: var(--font-display);
|
|
40
|
+
-webkit-font-smoothing: antialiased;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#root {
|
|
44
|
+
max-width: 430px;
|
|
45
|
+
margin: 0 auto;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.scrollbar-hide {
|
|
49
|
+
-ms-overflow-style: none;
|
|
50
|
+
scrollbar-width: none;
|
|
51
|
+
}
|
|
52
|
+
.scrollbar-hide::-webkit-scrollbar {
|
|
53
|
+
display: none;
|
|
54
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { createContext, useContext, useState, useCallback, type ReactNode } from 'react'
|
|
2
|
+
import type { Message, ChatSettings } from '../types'
|
|
3
|
+
|
|
4
|
+
interface ChatContextValue {
|
|
5
|
+
messages: Message[]
|
|
6
|
+
isLoading: boolean
|
|
7
|
+
settings: ChatSettings
|
|
8
|
+
setSettings: (settings: ChatSettings) => void
|
|
9
|
+
sendMessage: (content: string) => void
|
|
10
|
+
clearMessages: () => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const ChatContext = createContext<ChatContextValue | null>(null)
|
|
14
|
+
|
|
15
|
+
const SAMPLE_MESSAGES: Message[] = [
|
|
16
|
+
{
|
|
17
|
+
id: '1',
|
|
18
|
+
role: 'user',
|
|
19
|
+
content: 'What can you help me with?',
|
|
20
|
+
timestamp: Date.now() - 120000,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: '2',
|
|
24
|
+
role: 'assistant',
|
|
25
|
+
content: 'I can help you with a wide range of tasks — answering questions, writing, analysis, coding, brainstorming, and more. I can also display key information on your G2 glasses for quick, hands-free reference. What would you like to work on?',
|
|
26
|
+
timestamp: Date.now() - 110000,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: '3',
|
|
30
|
+
role: 'user',
|
|
31
|
+
content: 'Show me the weather forecast for today.',
|
|
32
|
+
timestamp: Date.now() - 60000,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: '4',
|
|
36
|
+
role: 'assistant',
|
|
37
|
+
content: 'Here is today\'s forecast:\n\nMostly sunny, high of 22C / 72F with light winds from the northwest. UV index is moderate — sunglasses recommended (you\'re already wearing them). No rain expected until Thursday.',
|
|
38
|
+
timestamp: Date.now() - 50000,
|
|
39
|
+
},
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
const SIMULATED_RESPONSES = [
|
|
43
|
+
'That\'s a great question. Let me think about it for a moment.\n\nBased on what I know, the answer involves a few key considerations. Would you like me to go deeper into any particular aspect?',
|
|
44
|
+
'I\'ve processed your request. Here is what I found:\n\nThe information you\'re looking for can be broken down into three main areas. I\'ll summarize the key points and push them to your glasses display for quick reference.',
|
|
45
|
+
'Interesting thought. Here\'s my perspective on it:\n\nThere are multiple angles to consider here. The most important factor is understanding the context and constraints involved. Let me know if you\'d like more detail on any specific part.',
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
export function ChatProvider({ children }: { children: ReactNode }) {
|
|
49
|
+
const [messages, setMessages] = useState<Message[]>(SAMPLE_MESSAGES)
|
|
50
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
51
|
+
const [settings, setSettings] = useState<ChatSettings>({
|
|
52
|
+
provider: 'openai',
|
|
53
|
+
apiKey: '',
|
|
54
|
+
voiceEnabled: true,
|
|
55
|
+
darkMode: false,
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const sendMessage = useCallback((content: string) => {
|
|
59
|
+
const userMessage: Message = {
|
|
60
|
+
id: crypto.randomUUID(),
|
|
61
|
+
role: 'user',
|
|
62
|
+
content,
|
|
63
|
+
timestamp: Date.now(),
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
setMessages((prev) => [...prev, userMessage])
|
|
67
|
+
setIsLoading(true)
|
|
68
|
+
|
|
69
|
+
// Simulate AI response
|
|
70
|
+
const delay = 1200 + Math.random() * 1800
|
|
71
|
+
setTimeout(() => {
|
|
72
|
+
const response = SIMULATED_RESPONSES[Math.floor(Math.random() * SIMULATED_RESPONSES.length)]
|
|
73
|
+
const assistantMessage: Message = {
|
|
74
|
+
id: crypto.randomUUID(),
|
|
75
|
+
role: 'assistant',
|
|
76
|
+
content: response,
|
|
77
|
+
timestamp: Date.now(),
|
|
78
|
+
}
|
|
79
|
+
setMessages((prev) => [...prev, assistantMessage])
|
|
80
|
+
setIsLoading(false)
|
|
81
|
+
}, delay)
|
|
82
|
+
}, [])
|
|
83
|
+
|
|
84
|
+
const clearMessages = useCallback(() => {
|
|
85
|
+
setMessages([])
|
|
86
|
+
}, [])
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<ChatContext.Provider value={{ messages, isLoading, settings, setSettings, sendMessage, clearMessages }}>
|
|
90
|
+
{children}
|
|
91
|
+
</ChatContext.Provider>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function useChat() {
|
|
96
|
+
const ctx = useContext(ChatContext)
|
|
97
|
+
if (!ctx) throw new Error('useChat must be used within ChatProvider')
|
|
98
|
+
return ctx
|
|
99
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { useCallback, useMemo, useRef } from 'react'
|
|
2
|
+
import { useNavigate, useLocation } from 'react-router'
|
|
3
|
+
import { useGlasses } from 'even-toolkit/useGlasses'
|
|
4
|
+
import { useFlashPhase } from 'even-toolkit/useFlashPhase'
|
|
5
|
+
import { createScreenMapper, getHomeTiles } from 'even-toolkit/glass-router'
|
|
6
|
+
import { appSplash } from './splash'
|
|
7
|
+
import { toDisplayData, onGlassAction, type AppSnapshot } from './selectors'
|
|
8
|
+
import type { AppActions } from './shared'
|
|
9
|
+
import { useChat } from '../contexts/ChatContext'
|
|
10
|
+
|
|
11
|
+
const deriveScreen = createScreenMapper([
|
|
12
|
+
{ pattern: '/', screen: 'home' },
|
|
13
|
+
{ pattern: '/settings', screen: 'home' },
|
|
14
|
+
], 'home')
|
|
15
|
+
|
|
16
|
+
const homeTiles = getHomeTiles(appSplash)
|
|
17
|
+
|
|
18
|
+
export function AppGlasses() {
|
|
19
|
+
const navigate = useNavigate()
|
|
20
|
+
const location = useLocation()
|
|
21
|
+
const { messages } = useChat()
|
|
22
|
+
const flashPhase = useFlashPhase(deriveScreen(location.pathname) === 'home')
|
|
23
|
+
|
|
24
|
+
const snapshotRef = useMemo(() => ({
|
|
25
|
+
current: null as AppSnapshot | null,
|
|
26
|
+
}), [])
|
|
27
|
+
|
|
28
|
+
// Show recent messages on the glasses display
|
|
29
|
+
const recentItems = messages
|
|
30
|
+
.slice(-6)
|
|
31
|
+
.map((msg) => {
|
|
32
|
+
const prefix = msg.role === 'user' ? 'You' : 'AI'
|
|
33
|
+
const text = msg.content.length > 60 ? msg.content.slice(0, 57) + '...' : msg.content
|
|
34
|
+
return `${prefix}: ${text}`
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
if (recentItems.length === 0) {
|
|
38
|
+
recentItems.push('{{DISPLAY_NAME}} ready', 'Send a message to begin')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const snapshot: AppSnapshot = {
|
|
42
|
+
items: recentItems,
|
|
43
|
+
flashPhase,
|
|
44
|
+
}
|
|
45
|
+
snapshotRef.current = snapshot
|
|
46
|
+
|
|
47
|
+
const getSnapshot = useCallback(() => snapshotRef.current!, [snapshotRef])
|
|
48
|
+
|
|
49
|
+
const ctxRef = useRef<AppActions>({ navigate })
|
|
50
|
+
ctxRef.current = { navigate }
|
|
51
|
+
|
|
52
|
+
const handleGlassAction = useCallback(
|
|
53
|
+
(action: Parameters<typeof onGlassAction>[0], nav: Parameters<typeof onGlassAction>[1], snap: AppSnapshot) =>
|
|
54
|
+
onGlassAction(action, nav, snap, ctxRef.current),
|
|
55
|
+
[],
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
useGlasses({
|
|
59
|
+
getSnapshot,
|
|
60
|
+
toDisplayData,
|
|
61
|
+
onGlassAction: handleGlassAction,
|
|
62
|
+
deriveScreen,
|
|
63
|
+
appName: '{{DISPLAY_NAME_UPPER}}',
|
|
64
|
+
splash: appSplash,
|
|
65
|
+
getPageMode: (screen) => screen === 'home' ? 'home' : 'text',
|
|
66
|
+
homeImageTiles: homeTiles,
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
return null
|
|
70
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { GlassScreen } from 'even-toolkit/glass-screen-router'
|
|
2
|
+
import { buildScrollableList } from 'even-toolkit/glass-display-builders'
|
|
3
|
+
import { moveHighlight } from 'even-toolkit/glass-nav'
|
|
4
|
+
import type { AppSnapshot, AppActions } from '../shared'
|
|
5
|
+
|
|
6
|
+
export const homeScreen: GlassScreen<AppSnapshot, AppActions> = {
|
|
7
|
+
display(snapshot, nav) {
|
|
8
|
+
return {
|
|
9
|
+
lines: buildScrollableList({
|
|
10
|
+
items: snapshot.items,
|
|
11
|
+
highlightedIndex: nav.highlightedIndex,
|
|
12
|
+
maxVisible: 5,
|
|
13
|
+
formatter: (item) => item,
|
|
14
|
+
}),
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
action(action, nav, snapshot) {
|
|
19
|
+
if (action.type === 'HIGHLIGHT_MOVE') {
|
|
20
|
+
return { ...nav, highlightedIndex: moveHighlight(nav.highlightedIndex, action.direction, snapshot.items.length - 1) }
|
|
21
|
+
}
|
|
22
|
+
return nav
|
|
23
|
+
},
|
|
24
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createGlassScreenRouter } from 'even-toolkit/glass-screen-router'
|
|
2
|
+
import type { AppSnapshot, AppActions } from './shared'
|
|
3
|
+
import { homeScreen } from './screens/home'
|
|
4
|
+
|
|
5
|
+
export type { AppSnapshot, AppActions }
|
|
6
|
+
|
|
7
|
+
export const { toDisplayData, onGlassAction } = createGlassScreenRouter<AppSnapshot, AppActions>({
|
|
8
|
+
'home': homeScreen,
|
|
9
|
+
}, 'home')
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createSplash, TILE_PRESETS } from 'even-toolkit/splash'
|
|
2
|
+
|
|
3
|
+
export function renderSplash(ctx: CanvasRenderingContext2D, w: number, h: number) {
|
|
4
|
+
const fg = '#e0e0e0'
|
|
5
|
+
const cx = w / 2
|
|
6
|
+
const s = Math.min(w / 200, h / 200)
|
|
7
|
+
|
|
8
|
+
// Simple text-based splash — replace with pixel-art icon for your app
|
|
9
|
+
ctx.fillStyle = fg
|
|
10
|
+
ctx.font = `bold ${14 * s}px "Courier New", monospace`
|
|
11
|
+
ctx.textAlign = 'center'
|
|
12
|
+
ctx.fillText('{{DISPLAY_NAME_UPPER}}', cx, 50 * s)
|
|
13
|
+
ctx.textAlign = 'left'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const appSplash = createSplash({
|
|
17
|
+
tiles: 1,
|
|
18
|
+
tileLayout: 'vertical',
|
|
19
|
+
tilePositions: TILE_PRESETS.topCenter1,
|
|
20
|
+
canvasSize: { w: 200, h: 200 },
|
|
21
|
+
minTimeMs: 0,
|
|
22
|
+
maxTimeMs: 0,
|
|
23
|
+
menuText: '',
|
|
24
|
+
render: renderSplash,
|
|
25
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { StrictMode } from 'react'
|
|
2
|
+
import { createRoot } from 'react-dom/client'
|
|
3
|
+
import { BrowserRouter } from 'react-router'
|
|
4
|
+
import { App } from './App'
|
|
5
|
+
import './app.css'
|
|
6
|
+
|
|
7
|
+
createRoot(document.getElementById('root')!).render(
|
|
8
|
+
<StrictMode>
|
|
9
|
+
<BrowserRouter>
|
|
10
|
+
<App />
|
|
11
|
+
</BrowserRouter>
|
|
12
|
+
</StrictMode>,
|
|
13
|
+
)
|