@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
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { ScreenHeader, StatGrid, StatCard, Timeline, Card } from 'even-toolkit/web'
|
|
2
|
+
import type { TimelineEvent } from 'even-toolkit/web'
|
|
3
|
+
|
|
4
|
+
/* ── Stat grid data ────────────────────────────────────────────── */
|
|
5
|
+
|
|
6
|
+
const kpiStats = [
|
|
7
|
+
{ label: 'Revenue', value: '$48.2K', detail: '+12.4% MoM' },
|
|
8
|
+
{ label: 'Users', value: '2,847', detail: '+340 this week' },
|
|
9
|
+
{ label: 'Growth', value: '18.3%', detail: 'vs 14.1% prior' },
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
/* ── Stat cards with sparklines ────────────────────────────────── */
|
|
13
|
+
|
|
14
|
+
const revenueSparkline = [32, 35, 33, 38, 42, 40, 44, 47, 45, 50, 48, 52]
|
|
15
|
+
const usersSparkline = [180, 210, 195, 240, 260, 255, 290, 310, 305, 340, 350, 380]
|
|
16
|
+
const conversionSparkline = [3.2, 3.4, 3.1, 3.6, 3.8, 3.5, 3.9, 4.1, 3.8, 4.2, 4.0, 4.3]
|
|
17
|
+
|
|
18
|
+
/* ── Timeline events ───────────────────────────────────────────── */
|
|
19
|
+
|
|
20
|
+
const recentActivity: TimelineEvent[] = [
|
|
21
|
+
{
|
|
22
|
+
id: '1',
|
|
23
|
+
title: 'Revenue milestone reached',
|
|
24
|
+
subtitle: 'Monthly recurring revenue crossed $48K',
|
|
25
|
+
timestamp: '2m ago',
|
|
26
|
+
color: 'var(--color-positive)',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: '2',
|
|
30
|
+
title: 'New user cohort onboarded',
|
|
31
|
+
subtitle: '124 users joined from campaign #47',
|
|
32
|
+
timestamp: '18m ago',
|
|
33
|
+
color: 'var(--color-accent)',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: '3',
|
|
37
|
+
title: 'Conversion rate improved',
|
|
38
|
+
subtitle: 'A/B test variant B outperformed by 0.8pp',
|
|
39
|
+
timestamp: '1h ago',
|
|
40
|
+
color: 'var(--color-positive)',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: '4',
|
|
44
|
+
title: 'API latency spike detected',
|
|
45
|
+
subtitle: 'P95 latency exceeded 400ms for 12 minutes',
|
|
46
|
+
timestamp: '3h ago',
|
|
47
|
+
color: 'var(--color-negative)',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: '5',
|
|
51
|
+
title: 'Weekly report generated',
|
|
52
|
+
subtitle: 'Sent to 14 stakeholders via email',
|
|
53
|
+
timestamp: '6h ago',
|
|
54
|
+
},
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
/* ── Component ─────────────────────────────────────────────────── */
|
|
58
|
+
|
|
59
|
+
export function OverviewScreen() {
|
|
60
|
+
return (
|
|
61
|
+
<div className="flex flex-col gap-6">
|
|
62
|
+
<ScreenHeader title="Overview" />
|
|
63
|
+
|
|
64
|
+
{/* KPI summary row */}
|
|
65
|
+
<StatGrid stats={kpiStats} columns={3} />
|
|
66
|
+
|
|
67
|
+
{/* Trend cards */}
|
|
68
|
+
<div className="flex flex-col gap-3">
|
|
69
|
+
<div className="text-[13px] tracking-[-0.13px] text-text-dim">Trends</div>
|
|
70
|
+
<StatCard
|
|
71
|
+
label="Monthly Revenue"
|
|
72
|
+
value="$48.2K"
|
|
73
|
+
change="+12.4%"
|
|
74
|
+
trend="up"
|
|
75
|
+
sparklineData={revenueSparkline}
|
|
76
|
+
/>
|
|
77
|
+
<StatCard
|
|
78
|
+
label="Active Users"
|
|
79
|
+
value="2,847"
|
|
80
|
+
change="+340"
|
|
81
|
+
trend="up"
|
|
82
|
+
sparklineData={usersSparkline}
|
|
83
|
+
/>
|
|
84
|
+
<StatCard
|
|
85
|
+
label="Conversion Rate"
|
|
86
|
+
value="4.3%"
|
|
87
|
+
change="+0.3pp"
|
|
88
|
+
trend="up"
|
|
89
|
+
sparklineData={conversionSparkline}
|
|
90
|
+
/>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
{/* Recent activity */}
|
|
94
|
+
<div className="flex flex-col gap-3">
|
|
95
|
+
<div className="text-[13px] tracking-[-0.13px] text-text-dim">Recent Activity</div>
|
|
96
|
+
<Card className="p-4">
|
|
97
|
+
<Timeline events={recentActivity} />
|
|
98
|
+
</Card>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { ScreenHeader, SettingsGroup, Toggle, ListItem } from 'even-toolkit/web'
|
|
3
|
+
import { IcGuideChevronSmallDrillIn } from 'even-toolkit/web/icons/svg-icons'
|
|
4
|
+
|
|
5
|
+
export function SettingsScreen() {
|
|
6
|
+
const [darkMode, setDarkMode] = useState(false)
|
|
7
|
+
const [autoRefresh, setAutoRefresh] = useState(true)
|
|
8
|
+
const [notifications, setNotifications] = useState(true)
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<div className="flex flex-col gap-6">
|
|
12
|
+
<ScreenHeader title="Settings" />
|
|
13
|
+
|
|
14
|
+
{/* Display settings */}
|
|
15
|
+
<SettingsGroup label="Display">
|
|
16
|
+
<ListItem
|
|
17
|
+
title="Dark Mode"
|
|
18
|
+
subtitle="Use dark color scheme"
|
|
19
|
+
trailing={<Toggle checked={darkMode} onChange={setDarkMode} />}
|
|
20
|
+
/>
|
|
21
|
+
<ListItem
|
|
22
|
+
title="Auto-refresh"
|
|
23
|
+
subtitle="Update data every 30 seconds"
|
|
24
|
+
trailing={<Toggle checked={autoRefresh} onChange={setAutoRefresh} />}
|
|
25
|
+
/>
|
|
26
|
+
<ListItem
|
|
27
|
+
title="Notifications"
|
|
28
|
+
subtitle="Alert on threshold breaches"
|
|
29
|
+
trailing={<Toggle checked={notifications} onChange={setNotifications} />}
|
|
30
|
+
/>
|
|
31
|
+
</SettingsGroup>
|
|
32
|
+
|
|
33
|
+
{/* Data settings */}
|
|
34
|
+
<SettingsGroup label="Data">
|
|
35
|
+
<ListItem
|
|
36
|
+
title="Refresh Interval"
|
|
37
|
+
subtitle="30 seconds"
|
|
38
|
+
trailing={<IcGuideChevronSmallDrillIn width={20} height={20} className="text-text-dim" />}
|
|
39
|
+
/>
|
|
40
|
+
<ListItem
|
|
41
|
+
title="Data Source"
|
|
42
|
+
subtitle="Production API"
|
|
43
|
+
trailing={<IcGuideChevronSmallDrillIn width={20} height={20} className="text-text-dim" />}
|
|
44
|
+
/>
|
|
45
|
+
</SettingsGroup>
|
|
46
|
+
|
|
47
|
+
{/* About */}
|
|
48
|
+
<SettingsGroup label="About">
|
|
49
|
+
<ListItem
|
|
50
|
+
title="{{DISPLAY_NAME}}"
|
|
51
|
+
subtitle="Version 1.0.0"
|
|
52
|
+
/>
|
|
53
|
+
<ListItem
|
|
54
|
+
title="Even Toolkit"
|
|
55
|
+
subtitle="v1.5.0"
|
|
56
|
+
/>
|
|
57
|
+
</SettingsGroup>
|
|
58
|
+
</div>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dashboard",
|
|
3
|
+
"displayName": "Data Dashboard",
|
|
4
|
+
"description": "Analytics dashboard with charts, stats, and timeline — built for G2 smart glasses",
|
|
5
|
+
"tags": ["dashboard", "charts", "analytics"],
|
|
6
|
+
"components": ["AppShell", "NavBar", "ScreenHeader", "StatGrid", "StatCard", "LineChart", "BarChart", "PieChart", "Timeline", "SettingsGroup", "Toggle", "ListItem", "Card"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"isolatedModules": true,
|
|
15
|
+
"baseUrl": ".",
|
|
16
|
+
"paths": { "@/*": ["./src/*"] }
|
|
17
|
+
},
|
|
18
|
+
"include": ["src"],
|
|
19
|
+
"references": [{ "path": "./tsconfig.node.json" }]
|
|
20
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineConfig } from 'vite'
|
|
2
|
+
import react from '@vitejs/plugin-react'
|
|
3
|
+
import tailwindcss from '@tailwindcss/vite'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
plugins: [react(), tailwindcss()],
|
|
8
|
+
resolve: {
|
|
9
|
+
alias: { '@': path.resolve(__dirname, './src') },
|
|
10
|
+
dedupe: ['react', 'react-dom', 'react-router', '@evenrealities/even_hub_sdk', '@jappyjan/even-better-sdk', 'upng-js'],
|
|
11
|
+
},
|
|
12
|
+
})
|
|
@@ -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,24 @@
|
|
|
1
|
+
import { Routes, Route } from 'react-router'
|
|
2
|
+
import { Shell } from './layouts/shell'
|
|
3
|
+
import { MediaProvider } from './contexts/MediaContext'
|
|
4
|
+
import { GalleryScreen } from './screens/GalleryScreen'
|
|
5
|
+
import { AudioScreen } from './screens/AudioScreen'
|
|
6
|
+
import { UploadScreen } from './screens/UploadScreen'
|
|
7
|
+
import { Settings } from './screens/Settings'
|
|
8
|
+
import { AppGlasses } from './glass/AppGlasses'
|
|
9
|
+
|
|
10
|
+
export function App() {
|
|
11
|
+
return (
|
|
12
|
+
<MediaProvider>
|
|
13
|
+
<Routes>
|
|
14
|
+
<Route element={<Shell />}>
|
|
15
|
+
<Route path="/" element={<GalleryScreen />} />
|
|
16
|
+
<Route path="/audio" element={<AudioScreen />} />
|
|
17
|
+
<Route path="/upload" element={<UploadScreen />} />
|
|
18
|
+
<Route path="/settings" element={<Settings />} />
|
|
19
|
+
</Route>
|
|
20
|
+
</Routes>
|
|
21
|
+
<AppGlasses />
|
|
22
|
+
</MediaProvider>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
@@ -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,108 @@
|
|
|
1
|
+
import { createContext, useContext, useState, useCallback, type ReactNode } from 'react'
|
|
2
|
+
import type { GalleryItem, AudioTrack, UploadItem, CategoryFilter } from '../types'
|
|
3
|
+
import { ALL_FILTER } from '../types'
|
|
4
|
+
|
|
5
|
+
interface MediaContextValue {
|
|
6
|
+
galleryItems: GalleryItem[]
|
|
7
|
+
audioTracks: AudioTrack[]
|
|
8
|
+
uploads: UploadItem[]
|
|
9
|
+
addUpload: (name: string, size: string, type: string) => void
|
|
10
|
+
removeUpload: (id: string) => void
|
|
11
|
+
clearUploads: () => void
|
|
12
|
+
selectedCategory: CategoryFilter
|
|
13
|
+
setSelectedCategory: (c: CategoryFilter) => void
|
|
14
|
+
filteredGallery: GalleryItem[]
|
|
15
|
+
selectedTrackId: string | null
|
|
16
|
+
setSelectedTrackId: (id: string | null) => void
|
|
17
|
+
gridColumns: 2 | 3
|
|
18
|
+
setGridColumns: (cols: 2 | 3) => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const MediaContext = createContext<MediaContextValue | null>(null)
|
|
22
|
+
|
|
23
|
+
function generateId(): string {
|
|
24
|
+
return Date.now().toString(36) + Math.random().toString(36).slice(2, 8)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const GRADIENTS = [
|
|
28
|
+
'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
29
|
+
'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
|
|
30
|
+
'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
|
|
31
|
+
'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)',
|
|
32
|
+
'linear-gradient(135deg, #fa709a 0%, #fee140 100%)',
|
|
33
|
+
'linear-gradient(135deg, #a18cd1 0%, #fbc2eb 100%)',
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
const SAMPLE_GALLERY: GalleryItem[] = [
|
|
37
|
+
{ id: 'g1', title: 'Mountain Sunrise', category: 'Photos', date: '2026-03-28', gradient: GRADIENTS[0] },
|
|
38
|
+
{ id: 'g2', title: 'City Skyline', category: 'Photos', date: '2026-03-27', gradient: GRADIENTS[1] },
|
|
39
|
+
{ id: 'g3', title: 'Abstract Waves', category: 'Artwork', date: '2026-03-26', gradient: GRADIENTS[2] },
|
|
40
|
+
{ id: 'g4', title: 'Digital Garden', category: 'Artwork', date: '2026-03-25', gradient: GRADIENTS[3] },
|
|
41
|
+
{ id: 'g5', title: 'App Dashboard', category: 'Screenshots', date: '2026-03-24', gradient: GRADIENTS[4] },
|
|
42
|
+
{ id: 'g6', title: 'Ocean Breeze', category: 'Photos', date: '2026-03-23', gradient: GRADIENTS[5] },
|
|
43
|
+
{ id: 'g7', title: 'Neon Portrait', category: 'Artwork', date: '2026-03-22', gradient: GRADIENTS[0] },
|
|
44
|
+
{ id: 'g8', title: 'Settings Panel', category: 'Screenshots', date: '2026-03-21', gradient: GRADIENTS[1] },
|
|
45
|
+
{ id: 'g9', title: 'Forest Trail', category: 'Photos', date: '2026-03-20', gradient: GRADIENTS[3] },
|
|
46
|
+
{ id: 'g10', title: 'Pixel Mosaic', category: 'Artwork', date: '2026-03-19', gradient: GRADIENTS[4] },
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
const SAMPLE_TRACKS: AudioTrack[] = [
|
|
50
|
+
{ id: 't1', title: 'Morning Light', artist: 'Ambient Waves', duration: '3:42', durationSeconds: 222 },
|
|
51
|
+
{ id: 't2', title: 'Deep Focus', artist: 'Lo-Fi Beats', duration: '4:15', durationSeconds: 255 },
|
|
52
|
+
{ id: 't3', title: 'Evening Calm', artist: 'Nature Sounds', duration: '5:08', durationSeconds: 308 },
|
|
53
|
+
{ id: 't4', title: 'City Rain', artist: 'White Noise Co', duration: '2:56', durationSeconds: 176 },
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
export function MediaProvider({ children }: { children: ReactNode }) {
|
|
57
|
+
const [galleryItems] = useState<GalleryItem[]>(SAMPLE_GALLERY)
|
|
58
|
+
const [audioTracks] = useState<AudioTrack[]>(SAMPLE_TRACKS)
|
|
59
|
+
const [uploads, setUploads] = useState<UploadItem[]>([])
|
|
60
|
+
const [selectedCategory, setSelectedCategory] = useState<CategoryFilter>(ALL_FILTER)
|
|
61
|
+
const [selectedTrackId, setSelectedTrackId] = useState<string | null>(null)
|
|
62
|
+
const [gridColumns, setGridColumns] = useState<2 | 3>(3)
|
|
63
|
+
|
|
64
|
+
const addUpload = useCallback((name: string, size: string, type: string) => {
|
|
65
|
+
const item: UploadItem = { id: generateId(), name, size, type, timestamp: Date.now() }
|
|
66
|
+
setUploads((prev) => [item, ...prev])
|
|
67
|
+
}, [])
|
|
68
|
+
|
|
69
|
+
const removeUpload = useCallback((id: string) => {
|
|
70
|
+
setUploads((prev) => prev.filter((u) => u.id !== id))
|
|
71
|
+
}, [])
|
|
72
|
+
|
|
73
|
+
const clearUploads = useCallback(() => {
|
|
74
|
+
setUploads([])
|
|
75
|
+
}, [])
|
|
76
|
+
|
|
77
|
+
const filteredGallery = galleryItems.filter((item) => {
|
|
78
|
+
return selectedCategory === ALL_FILTER || item.category === selectedCategory
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<MediaContext.Provider
|
|
83
|
+
value={{
|
|
84
|
+
galleryItems,
|
|
85
|
+
audioTracks,
|
|
86
|
+
uploads,
|
|
87
|
+
addUpload,
|
|
88
|
+
removeUpload,
|
|
89
|
+
clearUploads,
|
|
90
|
+
selectedCategory,
|
|
91
|
+
setSelectedCategory,
|
|
92
|
+
filteredGallery,
|
|
93
|
+
selectedTrackId,
|
|
94
|
+
setSelectedTrackId,
|
|
95
|
+
gridColumns,
|
|
96
|
+
setGridColumns,
|
|
97
|
+
}}
|
|
98
|
+
>
|
|
99
|
+
{children}
|
|
100
|
+
</MediaContext.Provider>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function useMedia() {
|
|
105
|
+
const ctx = useContext(MediaContext)
|
|
106
|
+
if (!ctx) throw new Error('useMedia must be used within MediaProvider')
|
|
107
|
+
return ctx
|
|
108
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
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 { useMedia } from '../contexts/MediaContext'
|
|
10
|
+
|
|
11
|
+
const deriveScreen = createScreenMapper([
|
|
12
|
+
{ pattern: '/', screen: 'home' },
|
|
13
|
+
{ pattern: '/audio', screen: 'home' },
|
|
14
|
+
{ pattern: '/upload', screen: 'home' },
|
|
15
|
+
{ pattern: '/settings', screen: 'home' },
|
|
16
|
+
], 'home')
|
|
17
|
+
|
|
18
|
+
const homeTiles = getHomeTiles(appSplash)
|
|
19
|
+
|
|
20
|
+
export function AppGlasses() {
|
|
21
|
+
const navigate = useNavigate()
|
|
22
|
+
const location = useLocation()
|
|
23
|
+
const flashPhase = useFlashPhase(deriveScreen(location.pathname) === 'home')
|
|
24
|
+
const { galleryItems } = useMedia()
|
|
25
|
+
|
|
26
|
+
const snapshotRef = useMemo(() => ({
|
|
27
|
+
current: null as AppSnapshot | null,
|
|
28
|
+
}), [])
|
|
29
|
+
|
|
30
|
+
const snapshot: AppSnapshot = {
|
|
31
|
+
items: galleryItems.map((item) => `${item.title} — ${item.category}`),
|
|
32
|
+
flashPhase,
|
|
33
|
+
}
|
|
34
|
+
snapshotRef.current = snapshot
|
|
35
|
+
|
|
36
|
+
const getSnapshot = useCallback(() => snapshotRef.current!, [snapshotRef])
|
|
37
|
+
|
|
38
|
+
const ctxRef = useRef<AppActions>({ navigate })
|
|
39
|
+
ctxRef.current = { navigate }
|
|
40
|
+
|
|
41
|
+
const handleGlassAction = useCallback(
|
|
42
|
+
(action: Parameters<typeof onGlassAction>[0], nav: Parameters<typeof onGlassAction>[1], snap: AppSnapshot) =>
|
|
43
|
+
onGlassAction(action, nav, snap, ctxRef.current),
|
|
44
|
+
[],
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
useGlasses({
|
|
48
|
+
getSnapshot,
|
|
49
|
+
toDisplayData,
|
|
50
|
+
onGlassAction: handleGlassAction,
|
|
51
|
+
deriveScreen,
|
|
52
|
+
appName: '{{DISPLAY_NAME_UPPER}}',
|
|
53
|
+
splash: appSplash,
|
|
54
|
+
getPageMode: (screen) => screen === 'home' ? 'home' : 'text',
|
|
55
|
+
homeImageTiles: homeTiles,
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
@@ -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,39 @@
|
|
|
1
|
+
import { DrawerShell } from 'even-toolkit/web'
|
|
2
|
+
import type { SideDrawerItem } from 'even-toolkit/web'
|
|
3
|
+
|
|
4
|
+
const MENU_ITEMS: SideDrawerItem[] = [
|
|
5
|
+
{ id: '/', label: 'Gallery', section: 'Media' },
|
|
6
|
+
{ id: '/audio', label: 'Audio', section: 'Media' },
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
const BOTTOM_ITEMS: SideDrawerItem[] = [
|
|
10
|
+
{ id: '/upload', label: 'Upload', section: 'App' },
|
|
11
|
+
{ id: '/settings', label: 'Settings', section: 'App' },
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
function getPageTitle(pathname: string): string {
|
|
15
|
+
if (pathname === '/') return '{{DISPLAY_NAME}}'
|
|
16
|
+
if (pathname === '/audio') return 'Audio'
|
|
17
|
+
if (pathname === '/upload') return 'Upload'
|
|
18
|
+
if (pathname === '/settings') return 'Settings'
|
|
19
|
+
return '{{DISPLAY_NAME}}'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function deriveActiveId(pathname: string): string {
|
|
23
|
+
if (pathname === '/audio') return '/audio'
|
|
24
|
+
if (pathname === '/upload') return '/upload'
|
|
25
|
+
if (pathname === '/settings') return '/settings'
|
|
26
|
+
return '/'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function Shell() {
|
|
30
|
+
return (
|
|
31
|
+
<DrawerShell
|
|
32
|
+
items={MENU_ITEMS}
|
|
33
|
+
bottomItems={BOTTOM_ITEMS}
|
|
34
|
+
title="{{DISPLAY_NAME}}"
|
|
35
|
+
getPageTitle={getPageTitle}
|
|
36
|
+
deriveActiveId={deriveActiveId}
|
|
37
|
+
/>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -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
|
+
)
|