@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,54 @@
|
|
|
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
|
+
|
|
10
|
+
const deriveScreen = createScreenMapper([
|
|
11
|
+
{ pattern: '/', screen: 'home' },
|
|
12
|
+
], 'home')
|
|
13
|
+
|
|
14
|
+
const homeTiles = getHomeTiles(appSplash)
|
|
15
|
+
|
|
16
|
+
export function AppGlasses() {
|
|
17
|
+
const navigate = useNavigate()
|
|
18
|
+
const location = useLocation()
|
|
19
|
+
const flashPhase = useFlashPhase(deriveScreen(location.pathname) === 'home')
|
|
20
|
+
|
|
21
|
+
const snapshotRef = useMemo(() => ({
|
|
22
|
+
current: null as AppSnapshot | null,
|
|
23
|
+
}), [])
|
|
24
|
+
|
|
25
|
+
const snapshot: AppSnapshot = {
|
|
26
|
+
items: ['Welcome to {{DISPLAY_NAME}}', 'Edit App.tsx to get started'],
|
|
27
|
+
flashPhase,
|
|
28
|
+
}
|
|
29
|
+
snapshotRef.current = snapshot
|
|
30
|
+
|
|
31
|
+
const getSnapshot = useCallback(() => snapshotRef.current!, [snapshotRef])
|
|
32
|
+
|
|
33
|
+
const ctxRef = useRef<AppActions>({ navigate })
|
|
34
|
+
ctxRef.current = { navigate }
|
|
35
|
+
|
|
36
|
+
const handleGlassAction = useCallback(
|
|
37
|
+
(action: Parameters<typeof onGlassAction>[0], nav: Parameters<typeof onGlassAction>[1], snap: AppSnapshot) =>
|
|
38
|
+
onGlassAction(action, nav, snap, ctxRef.current),
|
|
39
|
+
[],
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
useGlasses({
|
|
43
|
+
getSnapshot,
|
|
44
|
+
toDisplayData,
|
|
45
|
+
onGlassAction: handleGlassAction,
|
|
46
|
+
deriveScreen,
|
|
47
|
+
appName: '{{DISPLAY_NAME_UPPER}}',
|
|
48
|
+
splash: appSplash,
|
|
49
|
+
getPageMode: (screen) => screen === 'home' ? 'home' : 'text',
|
|
50
|
+
homeImageTiles: homeTiles,
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
@@ -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
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
@@ -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,25 @@
|
|
|
1
|
+
import { Routes, Route } from 'react-router'
|
|
2
|
+
import { Shell } from './layouts/shell'
|
|
3
|
+
import { NotesProvider } from './contexts/NotesContext'
|
|
4
|
+
import { NoteList } from './screens/NoteList'
|
|
5
|
+
import { NoteDetail } from './screens/NoteDetail'
|
|
6
|
+
import { NoteForm } from './screens/NoteForm'
|
|
7
|
+
import { Settings } from './screens/Settings'
|
|
8
|
+
import { AppGlasses } from './glass/AppGlasses'
|
|
9
|
+
|
|
10
|
+
export function App() {
|
|
11
|
+
return (
|
|
12
|
+
<NotesProvider>
|
|
13
|
+
<Routes>
|
|
14
|
+
<Route element={<Shell />}>
|
|
15
|
+
<Route path="/" element={<NoteList />} />
|
|
16
|
+
<Route path="/note/:id" element={<NoteDetail />} />
|
|
17
|
+
<Route path="/new" element={<NoteForm />} />
|
|
18
|
+
<Route path="/note/:id/edit" element={<NoteForm />} />
|
|
19
|
+
<Route path="/settings" element={<Settings />} />
|
|
20
|
+
</Route>
|
|
21
|
+
</Routes>
|
|
22
|
+
<AppGlasses />
|
|
23
|
+
</NotesProvider>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
@@ -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,140 @@
|
|
|
1
|
+
import { createContext, useContext, useState, useCallback, useEffect, type ReactNode } from 'react'
|
|
2
|
+
import type { Note, NoteCategory, CategoryFilter } from '../types'
|
|
3
|
+
import { ALL_FILTER } from '../types'
|
|
4
|
+
|
|
5
|
+
interface NotesContextValue {
|
|
6
|
+
notes: Note[]
|
|
7
|
+
addNote: (title: string, content: string, category: NoteCategory) => Note
|
|
8
|
+
updateNote: (id: string, title: string, content: string, category: NoteCategory) => void
|
|
9
|
+
deleteNote: (id: string) => void
|
|
10
|
+
searchQuery: string
|
|
11
|
+
setSearchQuery: (q: string) => void
|
|
12
|
+
selectedCategory: CategoryFilter
|
|
13
|
+
setSelectedCategory: (c: CategoryFilter) => void
|
|
14
|
+
filteredNotes: Note[]
|
|
15
|
+
compactView: boolean
|
|
16
|
+
setCompactView: (v: boolean) => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const NotesContext = createContext<NotesContextValue | null>(null)
|
|
20
|
+
|
|
21
|
+
const STORAGE_KEY = '{{APP_NAME}}-notes'
|
|
22
|
+
const SETTINGS_KEY = '{{APP_NAME}}-settings'
|
|
23
|
+
|
|
24
|
+
function generateId(): string {
|
|
25
|
+
return Date.now().toString(36) + Math.random().toString(36).slice(2, 8)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const SAMPLE_NOTES: Note[] = [
|
|
29
|
+
{
|
|
30
|
+
id: 'sample-1',
|
|
31
|
+
title: 'Meeting Notes',
|
|
32
|
+
content: 'Discussed Q2 roadmap with the team. Key decisions:\n\n- Launch new dashboard by end of April\n- Prioritize mobile experience improvements\n- Schedule design review for next Wednesday\n- Allocate two sprints for performance optimization',
|
|
33
|
+
category: 'Work',
|
|
34
|
+
createdAt: Date.now() - 86400000 * 2,
|
|
35
|
+
updatedAt: Date.now() - 86400000 * 2,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: 'sample-2',
|
|
39
|
+
title: 'Recipe Ideas',
|
|
40
|
+
content: 'Try making sourdough bread this weekend. Need to get a starter going by Thursday at the latest.\n\nIngredients to pick up:\n- Bread flour (King Arthur)\n- Sea salt\n- Dutch oven if I can find one on sale',
|
|
41
|
+
category: 'Personal',
|
|
42
|
+
createdAt: Date.now() - 86400000,
|
|
43
|
+
updatedAt: Date.now() - 86400000,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 'sample-3',
|
|
47
|
+
title: 'App Feature',
|
|
48
|
+
content: 'What if we added voice-to-text note creation using the G2 glasses microphone? Could use the even-toolkit STT module with web-speech provider for real-time transcription.\n\nWould need:\n- Mic permission handling\n- Visual feedback on glasses display\n- Auto-punctuation',
|
|
49
|
+
category: 'Ideas',
|
|
50
|
+
createdAt: Date.now() - 3600000,
|
|
51
|
+
updatedAt: Date.now() - 3600000,
|
|
52
|
+
},
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
function loadNotes(): Note[] {
|
|
56
|
+
try {
|
|
57
|
+
const raw = localStorage.getItem(STORAGE_KEY)
|
|
58
|
+
if (raw) {
|
|
59
|
+
const parsed = JSON.parse(raw)
|
|
60
|
+
if (Array.isArray(parsed) && parsed.length > 0) return parsed
|
|
61
|
+
}
|
|
62
|
+
} catch { /* ignore */ }
|
|
63
|
+
return SAMPLE_NOTES
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function loadSettings(): { compactView: boolean } {
|
|
67
|
+
try {
|
|
68
|
+
const raw = localStorage.getItem(SETTINGS_KEY)
|
|
69
|
+
if (raw) return JSON.parse(raw)
|
|
70
|
+
} catch { /* ignore */ }
|
|
71
|
+
return { compactView: false }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function NotesProvider({ children }: { children: ReactNode }) {
|
|
75
|
+
const [notes, setNotes] = useState<Note[]>(loadNotes)
|
|
76
|
+
const [searchQuery, setSearchQuery] = useState('')
|
|
77
|
+
const [selectedCategory, setSelectedCategory] = useState<CategoryFilter>(ALL_FILTER)
|
|
78
|
+
const [compactView, setCompactView] = useState(() => loadSettings().compactView)
|
|
79
|
+
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(notes))
|
|
82
|
+
}, [notes])
|
|
83
|
+
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
localStorage.setItem(SETTINGS_KEY, JSON.stringify({ compactView }))
|
|
86
|
+
}, [compactView])
|
|
87
|
+
|
|
88
|
+
const addNote = useCallback((title: string, content: string, category: NoteCategory): Note => {
|
|
89
|
+
const now = Date.now()
|
|
90
|
+
const note: Note = { id: generateId(), title, content, category, createdAt: now, updatedAt: now }
|
|
91
|
+
setNotes((prev) => [note, ...prev])
|
|
92
|
+
return note
|
|
93
|
+
}, [])
|
|
94
|
+
|
|
95
|
+
const updateNote = useCallback((id: string, title: string, content: string, category: NoteCategory) => {
|
|
96
|
+
setNotes((prev) =>
|
|
97
|
+
prev.map((n) => (n.id === id ? { ...n, title, content, category, updatedAt: Date.now() } : n)),
|
|
98
|
+
)
|
|
99
|
+
}, [])
|
|
100
|
+
|
|
101
|
+
const deleteNote = useCallback((id: string) => {
|
|
102
|
+
setNotes((prev) => prev.filter((n) => n.id !== id))
|
|
103
|
+
}, [])
|
|
104
|
+
|
|
105
|
+
const filteredNotes = notes.filter((note) => {
|
|
106
|
+
const matchesCategory = selectedCategory === ALL_FILTER || note.category === selectedCategory
|
|
107
|
+
const query = searchQuery.toLowerCase()
|
|
108
|
+
const matchesSearch =
|
|
109
|
+
!query ||
|
|
110
|
+
note.title.toLowerCase().includes(query) ||
|
|
111
|
+
note.content.toLowerCase().includes(query)
|
|
112
|
+
return matchesCategory && matchesSearch
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<NotesContext.Provider
|
|
117
|
+
value={{
|
|
118
|
+
notes,
|
|
119
|
+
addNote,
|
|
120
|
+
updateNote,
|
|
121
|
+
deleteNote,
|
|
122
|
+
searchQuery,
|
|
123
|
+
setSearchQuery,
|
|
124
|
+
selectedCategory,
|
|
125
|
+
setSelectedCategory,
|
|
126
|
+
filteredNotes,
|
|
127
|
+
compactView,
|
|
128
|
+
setCompactView,
|
|
129
|
+
}}
|
|
130
|
+
>
|
|
131
|
+
{children}
|
|
132
|
+
</NotesContext.Provider>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function useNotes() {
|
|
137
|
+
const ctx = useContext(NotesContext)
|
|
138
|
+
if (!ctx) throw new Error('useNotes must be used within NotesProvider')
|
|
139
|
+
return ctx
|
|
140
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
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 { useNotes } from '../contexts/NotesContext'
|
|
10
|
+
|
|
11
|
+
const deriveScreen = createScreenMapper([
|
|
12
|
+
{ pattern: '/', screen: 'home' },
|
|
13
|
+
], 'home')
|
|
14
|
+
|
|
15
|
+
const homeTiles = getHomeTiles(appSplash)
|
|
16
|
+
|
|
17
|
+
export function AppGlasses() {
|
|
18
|
+
const navigate = useNavigate()
|
|
19
|
+
const location = useLocation()
|
|
20
|
+
const { notes } = useNotes()
|
|
21
|
+
const flashPhase = useFlashPhase(deriveScreen(location.pathname) === 'home')
|
|
22
|
+
|
|
23
|
+
const snapshotRef = useMemo(() => ({
|
|
24
|
+
current: null as AppSnapshot | null,
|
|
25
|
+
}), [])
|
|
26
|
+
|
|
27
|
+
const snapshot: AppSnapshot = {
|
|
28
|
+
items: notes.length > 0
|
|
29
|
+
? notes.map((n) => n.title)
|
|
30
|
+
: ['No notes yet'],
|
|
31
|
+
flashPhase,
|
|
32
|
+
}
|
|
33
|
+
snapshotRef.current = snapshot
|
|
34
|
+
|
|
35
|
+
const getSnapshot = useCallback(() => snapshotRef.current!, [snapshotRef])
|
|
36
|
+
|
|
37
|
+
const ctxRef = useRef<AppActions>({ navigate })
|
|
38
|
+
ctxRef.current = { navigate }
|
|
39
|
+
|
|
40
|
+
const handleGlassAction = useCallback(
|
|
41
|
+
(action: Parameters<typeof onGlassAction>[0], nav: Parameters<typeof onGlassAction>[1], snap: AppSnapshot) =>
|
|
42
|
+
onGlassAction(action, nav, snap, ctxRef.current),
|
|
43
|
+
[],
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
useGlasses({
|
|
47
|
+
getSnapshot,
|
|
48
|
+
toDisplayData,
|
|
49
|
+
onGlassAction: handleGlassAction,
|
|
50
|
+
deriveScreen,
|
|
51
|
+
appName: '{{DISPLAY_NAME_UPPER}}',
|
|
52
|
+
splash: appSplash,
|
|
53
|
+
getPageMode: (screen) => screen === 'home' ? 'home' : 'text',
|
|
54
|
+
homeImageTiles: homeTiles,
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
return null
|
|
58
|
+
}
|
|
@@ -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,24 @@
|
|
|
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
|
+
ctx.fillStyle = fg
|
|
9
|
+
ctx.font = `bold ${14 * s}px "Courier New", monospace`
|
|
10
|
+
ctx.textAlign = 'center'
|
|
11
|
+
ctx.fillText('{{DISPLAY_NAME_UPPER}}', cx, 50 * s)
|
|
12
|
+
ctx.textAlign = 'left'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const appSplash = createSplash({
|
|
16
|
+
tiles: 1,
|
|
17
|
+
tileLayout: 'vertical',
|
|
18
|
+
tilePositions: TILE_PRESETS.topCenter1,
|
|
19
|
+
canvasSize: { w: 200, h: 200 },
|
|
20
|
+
minTimeMs: 0,
|
|
21
|
+
maxTimeMs: 0,
|
|
22
|
+
menuText: '',
|
|
23
|
+
render: renderSplash,
|
|
24
|
+
})
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { DrawerShell } from 'even-toolkit/web'
|
|
2
|
+
import type { SideDrawerItem } from 'even-toolkit/web'
|
|
3
|
+
|
|
4
|
+
const MENU_ITEMS: SideDrawerItem[] = [
|
|
5
|
+
{ id: '/', label: 'Notes', section: 'Notes' },
|
|
6
|
+
]
|
|
7
|
+
|
|
8
|
+
const BOTTOM_ITEMS: SideDrawerItem[] = [
|
|
9
|
+
{ id: '/settings', label: 'Settings', section: 'App' },
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
function getPageTitle(pathname: string): string {
|
|
13
|
+
if (pathname === '/') return '{{DISPLAY_NAME}}'
|
|
14
|
+
if (pathname === '/new') return 'New Note'
|
|
15
|
+
if (pathname.includes('/edit')) return 'Edit Note'
|
|
16
|
+
if (pathname.startsWith('/note/')) return 'Note'
|
|
17
|
+
if (pathname === '/settings') return 'Settings'
|
|
18
|
+
return 'Notes'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function deriveActiveId(pathname: string): string {
|
|
22
|
+
if (pathname === '/settings') return '/settings'
|
|
23
|
+
return '/'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function Shell() {
|
|
27
|
+
return (
|
|
28
|
+
<DrawerShell
|
|
29
|
+
items={MENU_ITEMS}
|
|
30
|
+
bottomItems={BOTTOM_ITEMS}
|
|
31
|
+
title="{{DISPLAY_NAME}}"
|
|
32
|
+
getPageTitle={getPageTitle}
|
|
33
|
+
deriveActiveId={deriveActiveId}
|
|
34
|
+
/>
|
|
35
|
+
)
|
|
36
|
+
}
|