even-toolkit 1.3.2 → 1.4.1
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/README.md +162 -22
- package/dist/glasses/glass-display-builders.d.ts +47 -0
- package/dist/glasses/glass-display-builders.d.ts.map +1 -0
- package/dist/glasses/glass-display-builders.js +54 -0
- package/dist/glasses/glass-display-builders.js.map +1 -0
- package/dist/glasses/glass-mode.d.ts +34 -0
- package/dist/glasses/glass-mode.d.ts.map +1 -0
- package/dist/glasses/glass-mode.js +48 -0
- package/dist/glasses/glass-mode.js.map +1 -0
- package/dist/glasses/glass-nav.d.ts +15 -0
- package/dist/glasses/glass-nav.d.ts.map +1 -0
- package/dist/glasses/glass-nav.js +28 -0
- package/dist/glasses/glass-nav.js.map +1 -0
- package/dist/glasses/glass-router.d.ts +37 -0
- package/dist/glasses/glass-router.d.ts.map +1 -0
- package/dist/glasses/glass-router.js +46 -0
- package/dist/glasses/glass-router.js.map +1 -0
- package/dist/glasses/glass-screen-router.d.ts +28 -0
- package/dist/glasses/glass-screen-router.d.ts.map +1 -0
- package/dist/glasses/glass-screen-router.js +30 -0
- package/dist/glasses/glass-screen-router.js.map +1 -0
- package/dist/glasses/index.d.ts +5 -0
- package/dist/glasses/index.d.ts.map +1 -1
- package/dist/glasses/index.js +5 -0
- package/dist/glasses/index.js.map +1 -1
- package/glasses/glass-display-builders.ts +110 -0
- package/glasses/glass-mode.ts +65 -0
- package/glasses/glass-nav.ts +31 -0
- package/glasses/glass-router.ts +59 -0
- package/glasses/glass-screen-router.ts +43 -0
- package/glasses/index.ts +5 -0
- package/package.json +21 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Design system & component library for **Even Realities G2** smart glasses apps.
|
|
4
4
|
|
|
5
|
-
55+ web components, 191 pixel-art icons, glasses SDK bridge, light/dark themes, and design tokens — all following the Even Realities 2025 UIUX Design Guidelines.
|
|
5
|
+
55+ web components, 191 pixel-art icons, glasses SDK bridge with per-screen architecture, speech-to-text module, light/dark themes, and design tokens — all following the Even Realities 2025 UIUX Design Guidelines.
|
|
6
6
|
|
|
7
7
|
**[Live Demo → even-demo.vercel.app](https://even-demo.vercel.app)**
|
|
8
8
|
|
|
@@ -12,6 +12,12 @@ Design system & component library for **Even Realities G2** smart glasses apps.
|
|
|
12
12
|
npm install even-toolkit
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
+
Scaffold a new app instantly:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx create-even-app my-app
|
|
19
|
+
```
|
|
20
|
+
|
|
15
21
|
## What's Inside
|
|
16
22
|
|
|
17
23
|
### `/web` — Web Component Library
|
|
@@ -44,38 +50,173 @@ import { IcChevronBack, IcTrash, IcSettings } from 'even-toolkit/web/icons/svg-i
|
|
|
44
50
|
|
|
45
51
|
**Categories:** Edit & Settings (32), Feature & Function (50), Guide System (20), Menu Bar (8), Navigate (23), Status (54), Health (12)
|
|
46
52
|
|
|
47
|
-
|
|
53
|
+
---
|
|
48
54
|
|
|
49
|
-
|
|
55
|
+
## Glasses SDK
|
|
50
56
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
57
|
+
Everything needed to build G2 glasses apps with a clean, per-screen architecture.
|
|
58
|
+
|
|
59
|
+
### Per-Screen Architecture (v1.4)
|
|
60
|
+
|
|
61
|
+
Each glasses screen lives in its own file with co-located display + action logic:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
src/glass/
|
|
65
|
+
shared.ts — Snapshot type + actions interface
|
|
66
|
+
selectors.ts — Screen router (3 lines of wiring)
|
|
67
|
+
splash.ts — Splash image + loading text
|
|
68
|
+
AppGlasses.tsx — useGlasses hook setup
|
|
69
|
+
screens/
|
|
70
|
+
home.ts — { display, action }
|
|
71
|
+
detail.ts — { display, action }
|
|
72
|
+
active.ts — { display, action }
|
|
55
73
|
```
|
|
56
74
|
|
|
57
|
-
|
|
75
|
+
#### Define a screen
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import type { GlassScreen } from 'even-toolkit/glass-screen-router';
|
|
79
|
+
import { buildScrollableList } from 'even-toolkit/glass-display-builders';
|
|
80
|
+
import { moveHighlight } from 'even-toolkit/glass-nav';
|
|
81
|
+
|
|
82
|
+
export const homeScreen: GlassScreen<MySnapshot, MyActions> = {
|
|
83
|
+
display(snapshot, nav) {
|
|
84
|
+
return {
|
|
85
|
+
lines: buildScrollableList({
|
|
86
|
+
items: snapshot.items,
|
|
87
|
+
highlightedIndex: nav.highlightedIndex,
|
|
88
|
+
maxVisible: 5,
|
|
89
|
+
formatter: (item) => item.title,
|
|
90
|
+
}),
|
|
91
|
+
};
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
action(action, nav, snapshot, ctx) {
|
|
95
|
+
if (action.type === 'HIGHLIGHT_MOVE') {
|
|
96
|
+
return { ...nav, highlightedIndex: moveHighlight(nav.highlightedIndex, action.direction, snapshot.items.length - 1) };
|
|
97
|
+
}
|
|
98
|
+
if (action.type === 'SELECT_HIGHLIGHTED') {
|
|
99
|
+
ctx.navigate(`/item/${snapshot.items[nav.highlightedIndex].id}`);
|
|
100
|
+
return nav;
|
|
101
|
+
}
|
|
102
|
+
return nav;
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
```
|
|
58
106
|
|
|
59
|
-
|
|
107
|
+
#### Wire screens together
|
|
60
108
|
|
|
61
|
-
|
|
109
|
+
```ts
|
|
110
|
+
import { createGlassScreenRouter } from 'even-toolkit/glass-screen-router';
|
|
111
|
+
import { homeScreen } from './screens/home';
|
|
112
|
+
import { detailScreen } from './screens/detail';
|
|
62
113
|
|
|
63
|
-
|
|
114
|
+
export const { toDisplayData, onGlassAction } = createGlassScreenRouter({
|
|
115
|
+
'home': homeScreen,
|
|
116
|
+
'detail': detailScreen,
|
|
117
|
+
}, 'home');
|
|
118
|
+
```
|
|
64
119
|
|
|
65
|
-
|
|
120
|
+
### Navigation Helpers (`glass-nav`)
|
|
66
121
|
|
|
67
|
-
|
|
122
|
+
```ts
|
|
123
|
+
import { moveHighlight, clampIndex, calcMaxScroll, wrapIndex } from 'even-toolkit/glass-nav';
|
|
68
124
|
|
|
69
|
-
|
|
70
|
-
|
|
125
|
+
// Clamped movement (0 to max)
|
|
126
|
+
moveHighlight(current, 'up', max) // Math.max(0, Math.min(max, current - 1))
|
|
127
|
+
moveHighlight(current, 'down', max) // Math.max(0, Math.min(max, current + 1))
|
|
128
|
+
|
|
129
|
+
// Clamp index to button count
|
|
130
|
+
clampIndex(index, buttonCount) // Math.min(Math.max(0, index), count - 1)
|
|
131
|
+
|
|
132
|
+
// Max scroll offset
|
|
133
|
+
calcMaxScroll(totalLines, slots) // Math.max(0, totalLines - slots)
|
|
134
|
+
|
|
135
|
+
// Wrapping movement (loops around)
|
|
136
|
+
wrapIndex(current, 'down', count) // (current + 1) % count
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Display Builders (`glass-display-builders`)
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
import {
|
|
143
|
+
buildScrollableList,
|
|
144
|
+
buildScrollableContent,
|
|
145
|
+
slidingWindowStart,
|
|
146
|
+
G2_TEXT_LINES, // 10
|
|
147
|
+
DEFAULT_CONTENT_SLOTS, // 7 (below glassHeader)
|
|
148
|
+
} from 'even-toolkit/glass-display-builders';
|
|
149
|
+
|
|
150
|
+
// Scrollable highlighted list with scroll indicators
|
|
151
|
+
const lines = buildScrollableList({
|
|
152
|
+
items: recipes,
|
|
153
|
+
highlightedIndex: nav.highlightedIndex,
|
|
154
|
+
maxVisible: 5,
|
|
155
|
+
formatter: (r) => r.title,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Header + scrollable content with indicators
|
|
159
|
+
const display = buildScrollableContent({
|
|
160
|
+
title: 'Recipe Detail',
|
|
161
|
+
actionBar: buildStaticActionBar(['Start'], 0),
|
|
162
|
+
contentLines: ['Line 1', 'Line 2', ...],
|
|
163
|
+
scrollPos: nav.highlightedIndex,
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Mode Encoding (`glass-mode`)
|
|
168
|
+
|
|
169
|
+
Pack multiple navigation modes into a single `highlightedIndex`:
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
import { createModeEncoder } from 'even-toolkit/glass-mode';
|
|
173
|
+
|
|
174
|
+
const mode = createModeEncoder({
|
|
175
|
+
buttons: 0, // 0-99: button selection
|
|
176
|
+
scroll: 100, // 100+: scroll mode (offset = index - 100)
|
|
177
|
+
links: 200, // 200+: link navigation
|
|
178
|
+
});
|
|
71
179
|
|
|
72
|
-
//
|
|
73
|
-
|
|
180
|
+
mode.getMode(150) // 'scroll'
|
|
181
|
+
mode.getOffset(150) // 50
|
|
182
|
+
mode.encode('scroll', 25) // 125
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Route Mapping (`glass-router`)
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
import { createScreenMapper, createIdExtractor, getHomeTiles } from 'even-toolkit/glass-router';
|
|
74
189
|
|
|
75
|
-
|
|
76
|
-
|
|
190
|
+
const deriveScreen = createScreenMapper([
|
|
191
|
+
{ pattern: '/', screen: 'home' },
|
|
192
|
+
{ pattern: /^\/item\/[^/]+$/, screen: 'detail' },
|
|
193
|
+
], 'home');
|
|
194
|
+
|
|
195
|
+
const extractId = createIdExtractor(/^\/item\/([^/]+)/);
|
|
196
|
+
const homeTiles = getHomeTiles(appSplash);
|
|
77
197
|
```
|
|
78
198
|
|
|
199
|
+
### Core Glasses Modules
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
import { useGlasses } from 'even-toolkit/useGlasses';
|
|
203
|
+
import { useFlashPhase } from 'even-toolkit/useFlashPhase';
|
|
204
|
+
import { EvenHubBridge } from 'even-toolkit/bridge';
|
|
205
|
+
import { line, separator, glassHeader } from 'even-toolkit/types';
|
|
206
|
+
import { buildActionBar, buildStaticActionBar } from 'even-toolkit/action-bar';
|
|
207
|
+
import { truncate, applyScrollIndicators } from 'even-toolkit/text-utils';
|
|
208
|
+
import { renderTimerLines } from 'even-toolkit/timer-display';
|
|
209
|
+
import { createSplash, TILE_PRESETS } from 'even-toolkit/splash';
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**Display:** 576x288px, 10 text lines, text/columns/chart/home page modes, image tiles (max 288x144)
|
|
213
|
+
|
|
214
|
+
**Input:** action-map (tap/double-tap/scroll events), gestures (debounce), keyboard bindings
|
|
215
|
+
|
|
216
|
+
**Utilities:** splash screens, PNG encoding, text cleaning, pagination, keep-alive
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
79
220
|
## Speech-to-Text (STT)
|
|
80
221
|
|
|
81
222
|
Provider-agnostic speech-to-text module for voice input in G2 glasses apps.
|
|
@@ -130,9 +271,10 @@ Automatically detects the best audio source:
|
|
|
130
271
|
- **Browser mic** — via `getUserMedia` (desktop)
|
|
131
272
|
- Custom `AudioSource` — pass your own
|
|
132
273
|
|
|
274
|
+
---
|
|
275
|
+
|
|
133
276
|
## SDK 0.0.9 Support
|
|
134
277
|
|
|
135
|
-
- Container limit increased to 12 (8 text + 4 image)
|
|
136
278
|
- Max image size: 288x144
|
|
137
279
|
- IMU control: `bridge.imuEnable()` / `bridge.imuDisable()`
|
|
138
280
|
- Launch source detection: `LaunchSource` type
|
|
@@ -177,7 +319,6 @@ Light theme following Even Realities 2025 guidelines:
|
|
|
177
319
|
## Quick Start
|
|
178
320
|
|
|
179
321
|
```tsx
|
|
180
|
-
// App.tsx
|
|
181
322
|
import { AppShell, NavBar, ScreenHeader, Button, Card } from 'even-toolkit/web';
|
|
182
323
|
import type { NavItem } from 'even-toolkit/web';
|
|
183
324
|
|
|
@@ -200,7 +341,6 @@ export function App() {
|
|
|
200
341
|
```
|
|
201
342
|
|
|
202
343
|
```css
|
|
203
|
-
/* app.css */
|
|
204
344
|
@import "tailwindcss";
|
|
205
345
|
@import "even-toolkit/web/theme-light.css";
|
|
206
346
|
@import "even-toolkit/web/typography.css";
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Display composition utilities for G2 glasses.
|
|
3
|
+
* Builds common screen layouts: scrollable lists, scrollable content with headers.
|
|
4
|
+
*/
|
|
5
|
+
import type { DisplayLine, DisplayData } from './types';
|
|
6
|
+
/** G2 display fits 10 lines of text */
|
|
7
|
+
export declare const G2_TEXT_LINES = 10;
|
|
8
|
+
/** glassHeader() produces 2 DisplayLines that occupy 3 visual lines (title + separator + gap) */
|
|
9
|
+
export declare const HEADER_LINES = 3;
|
|
10
|
+
/** Default content slots below a glassHeader */
|
|
11
|
+
export declare const DEFAULT_CONTENT_SLOTS: number;
|
|
12
|
+
/**
|
|
13
|
+
* Calculate the start index for a centered sliding window.
|
|
14
|
+
* Keeps the highlighted item roughly centered in the visible area.
|
|
15
|
+
*/
|
|
16
|
+
export declare function slidingWindowStart(highlightedIndex: number, totalItems: number, maxVisible: number): number;
|
|
17
|
+
export interface ScrollableListOptions<T> {
|
|
18
|
+
items: T[];
|
|
19
|
+
highlightedIndex: number;
|
|
20
|
+
maxVisible: number;
|
|
21
|
+
/** Format an item into a display string */
|
|
22
|
+
formatter: (item: T, index: number) => string;
|
|
23
|
+
/** Line style for list items. Default: 'normal' */
|
|
24
|
+
style?: 'normal' | 'meta';
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Build a scrollable highlighted list with ▲/▼ scroll indicators.
|
|
28
|
+
* Returns an array of DisplayLines ready to use as DisplayData.lines.
|
|
29
|
+
*/
|
|
30
|
+
export declare function buildScrollableList<T>(opts: ScrollableListOptions<T>): DisplayLine[];
|
|
31
|
+
export interface ScrollableContentOptions {
|
|
32
|
+
title: string;
|
|
33
|
+
actionBar: string;
|
|
34
|
+
contentLines: string[];
|
|
35
|
+
scrollPos: number;
|
|
36
|
+
/** Number of visible content lines. Default: DEFAULT_CONTENT_SLOTS (7) */
|
|
37
|
+
contentSlots?: number;
|
|
38
|
+
/** Style for content lines. Default: 'meta' */
|
|
39
|
+
contentStyle?: 'normal' | 'meta';
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Build a header + windowed content display with scroll indicators.
|
|
43
|
+
* Produces a complete DisplayData with glassHeader at the top,
|
|
44
|
+
* followed by a scrollable window of content lines.
|
|
45
|
+
*/
|
|
46
|
+
export declare function buildScrollableContent(opts: ScrollableContentOptions): DisplayData;
|
|
47
|
+
//# sourceMappingURL=glass-display-builders.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"glass-display-builders.d.ts","sourceRoot":"","sources":["../../glasses/glass-display-builders.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAIxD,uCAAuC;AACvC,eAAO,MAAM,aAAa,KAAK,CAAC;AAEhC,iGAAiG;AACjG,eAAO,MAAM,YAAY,IAAI,CAAC;AAE9B,gDAAgD;AAChD,eAAO,MAAM,qBAAqB,QAA+B,CAAC;AAElE;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,gBAAgB,EAAE,MAAM,EACxB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,MAAM,CAMR;AAED,MAAM,WAAW,qBAAqB,CAAC,CAAC;IACtC,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,gBAAgB,EAAE,MAAM,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,2CAA2C;IAC3C,SAAS,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IAC9C,mDAAmD;IACnD,KAAK,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;CAC3B;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,IAAI,EAAE,qBAAqB,CAAC,CAAC,CAAC,GAAG,WAAW,EAAE,CAYpF;AAED,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,+CAA+C;IAC/C,YAAY,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;CAClC;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,wBAAwB,GAAG,WAAW,CA+BlF"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Display composition utilities for G2 glasses.
|
|
3
|
+
* Builds common screen layouts: scrollable lists, scrollable content with headers.
|
|
4
|
+
*/
|
|
5
|
+
import { line, glassHeader } from './types';
|
|
6
|
+
import { applyScrollIndicators } from './text-utils';
|
|
7
|
+
/** G2 display fits 10 lines of text */
|
|
8
|
+
export const G2_TEXT_LINES = 10;
|
|
9
|
+
/** glassHeader() produces 2 DisplayLines that occupy 3 visual lines (title + separator + gap) */
|
|
10
|
+
export const HEADER_LINES = 3;
|
|
11
|
+
/** Default content slots below a glassHeader */
|
|
12
|
+
export const DEFAULT_CONTENT_SLOTS = G2_TEXT_LINES - HEADER_LINES;
|
|
13
|
+
/**
|
|
14
|
+
* Calculate the start index for a centered sliding window.
|
|
15
|
+
* Keeps the highlighted item roughly centered in the visible area.
|
|
16
|
+
*/
|
|
17
|
+
export function slidingWindowStart(highlightedIndex, totalItems, maxVisible) {
|
|
18
|
+
if (totalItems <= maxVisible)
|
|
19
|
+
return 0;
|
|
20
|
+
return Math.max(0, Math.min(highlightedIndex - Math.floor(maxVisible / 2), totalItems - maxVisible));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Build a scrollable highlighted list with ▲/▼ scroll indicators.
|
|
24
|
+
* Returns an array of DisplayLines ready to use as DisplayData.lines.
|
|
25
|
+
*/
|
|
26
|
+
export function buildScrollableList(opts) {
|
|
27
|
+
const { items, highlightedIndex, maxVisible, formatter, style = 'normal' } = opts;
|
|
28
|
+
const start = slidingWindowStart(highlightedIndex, items.length, maxVisible);
|
|
29
|
+
const visible = items.slice(start, start + maxVisible).map((item, i) => {
|
|
30
|
+
const idx = start + i;
|
|
31
|
+
return line(formatter(item, idx), style, idx === highlightedIndex);
|
|
32
|
+
});
|
|
33
|
+
applyScrollIndicators(visible, start, items.length, maxVisible, (t) => line(t, 'meta', false));
|
|
34
|
+
return visible;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Build a header + windowed content display with scroll indicators.
|
|
38
|
+
* Produces a complete DisplayData with glassHeader at the top,
|
|
39
|
+
* followed by a scrollable window of content lines.
|
|
40
|
+
*/
|
|
41
|
+
export function buildScrollableContent(opts) {
|
|
42
|
+
const { title, actionBar, contentLines, scrollPos, contentSlots = DEFAULT_CONTENT_SLOTS, contentStyle = 'meta', } = opts;
|
|
43
|
+
const lines = [...glassHeader(title, actionBar)];
|
|
44
|
+
const start = Math.max(0, Math.min(scrollPos, contentLines.length - contentSlots));
|
|
45
|
+
const visible = contentLines.slice(start, start + contentSlots);
|
|
46
|
+
const contentDisplayLines = [];
|
|
47
|
+
for (const text of visible) {
|
|
48
|
+
contentDisplayLines.push(line(text, contentStyle, false));
|
|
49
|
+
}
|
|
50
|
+
applyScrollIndicators(contentDisplayLines, start, contentLines.length, contentSlots, (t) => line(t, 'meta', false));
|
|
51
|
+
lines.push(...contentDisplayLines);
|
|
52
|
+
return { lines };
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=glass-display-builders.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"glass-display-builders.js","sourceRoot":"","sources":["../../glasses/glass-display-builders.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAErD,uCAAuC;AACvC,MAAM,CAAC,MAAM,aAAa,GAAG,EAAE,CAAC;AAEhC,iGAAiG;AACjG,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAC;AAE9B,gDAAgD;AAChD,MAAM,CAAC,MAAM,qBAAqB,GAAG,aAAa,GAAG,YAAY,CAAC;AAElE;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAChC,gBAAwB,EACxB,UAAkB,EAClB,UAAkB;IAElB,IAAI,UAAU,IAAI,UAAU;QAAE,OAAO,CAAC,CAAC;IACvC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CACzB,gBAAgB,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,GAAG,CAAC,CAAC,EAC7C,UAAU,GAAG,UAAU,CACxB,CAAC,CAAC;AACL,CAAC;AAYD;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CAAI,IAA8B;IACnE,MAAM,EAAE,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,SAAS,EAAE,KAAK,GAAG,QAAQ,EAAE,GAAG,IAAI,CAAC;IAElF,MAAM,KAAK,GAAG,kBAAkB,CAAC,gBAAgB,EAAE,KAAK,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAC7E,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,UAAU,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE;QACrE,MAAM,GAAG,GAAG,KAAK,GAAG,CAAC,CAAC;QACtB,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,KAAK,EAAE,GAAG,KAAK,gBAAgB,CAAC,CAAC;IACrE,CAAC,CAAC,CAAC;IAEH,qBAAqB,CAAC,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC;IAE/F,OAAO,OAAO,CAAC;AACjB,CAAC;AAaD;;;;GAIG;AACH,MAAM,UAAU,sBAAsB,CAAC,IAA8B;IACnE,MAAM,EACJ,KAAK,EACL,SAAS,EACT,YAAY,EACZ,SAAS,EACT,YAAY,GAAG,qBAAqB,EACpC,YAAY,GAAG,MAAM,GACtB,GAAG,IAAI,CAAC;IAET,MAAM,KAAK,GAAG,CAAC,GAAG,WAAW,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC;IAEjD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,YAAY,CAAC,MAAM,GAAG,YAAY,CAAC,CAAC,CAAC;IACnF,MAAM,OAAO,GAAG,YAAY,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,YAAY,CAAC,CAAC;IAEhE,MAAM,mBAAmB,GAAkB,EAAE,CAAC;IAC9C,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;QAC3B,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,CAAC,CAAC,CAAC;IAC5D,CAAC;IAED,qBAAqB,CACnB,mBAAmB,EACnB,KAAK,EACL,YAAY,CAAC,MAAM,EACnB,YAAY,EACZ,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,CAC9B,CAAC;IAEF,KAAK,CAAC,IAAI,CAAC,GAAG,mBAAmB,CAAC,CAAC;IAEnC,OAAO,EAAE,KAAK,EAAE,CAAC;AACnB,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mode encoding for G2 glasses navigation.
|
|
3
|
+
*
|
|
4
|
+
* Packs a mode identifier + offset into a single highlightedIndex number.
|
|
5
|
+
* E.g. { buttons: 0, read: 100, links: 200 } means:
|
|
6
|
+
* - index 0-99: buttons mode, offset = index
|
|
7
|
+
* - index 100-199: read mode, offset = index - 100
|
|
8
|
+
* - index 200+: links mode, offset = index - 200
|
|
9
|
+
*/
|
|
10
|
+
export interface ModeEncoder<M extends string> {
|
|
11
|
+
/** Encode a mode + offset into a single highlightedIndex value */
|
|
12
|
+
encode(mode: M, offset?: number): number;
|
|
13
|
+
/** Decode an index back to { mode, offset } */
|
|
14
|
+
decode(index: number): {
|
|
15
|
+
mode: M;
|
|
16
|
+
offset: number;
|
|
17
|
+
};
|
|
18
|
+
/** Get just the mode name from an encoded index */
|
|
19
|
+
getMode(index: number): M;
|
|
20
|
+
/** Get just the offset from an encoded index */
|
|
21
|
+
getOffset(index: number): number;
|
|
22
|
+
/** Get the base value for a mode */
|
|
23
|
+
getBase(mode: M): number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Create a mode encoder from a mapping of mode names to base values.
|
|
27
|
+
* Modes are matched by checking the index against bases in descending order.
|
|
28
|
+
*
|
|
29
|
+
* @param modes Record mapping mode names to their base values.
|
|
30
|
+
* All bases must be unique non-negative integers.
|
|
31
|
+
* E.g. { buttons: 0, scroll: 100, steps: 200 }
|
|
32
|
+
*/
|
|
33
|
+
export declare function createModeEncoder<M extends string>(modes: Record<M, number>): ModeEncoder<M>;
|
|
34
|
+
//# sourceMappingURL=glass-mode.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"glass-mode.d.ts","sourceRoot":"","sources":["../../glasses/glass-mode.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,WAAW,WAAW,CAAC,CAAC,SAAS,MAAM;IAC3C,kEAAkE;IAClE,MAAM,CAAC,IAAI,EAAE,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzC,+CAA+C;IAC/C,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG;QAAE,IAAI,EAAE,CAAC,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACnD,mDAAmD;IACnD,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,CAAC,CAAC;IAC1B,gDAAgD;IAChD,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IACjC,oCAAoC;IACpC,OAAO,CAAC,IAAI,EAAE,CAAC,GAAG,MAAM,CAAC;CAC1B;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,MAAM,EAChD,KAAK,EAAE,MAAM,CAAC,CAAC,EAAE,MAAM,CAAC,GACvB,WAAW,CAAC,CAAC,CAAC,CA+BhB"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mode encoding for G2 glasses navigation.
|
|
3
|
+
*
|
|
4
|
+
* Packs a mode identifier + offset into a single highlightedIndex number.
|
|
5
|
+
* E.g. { buttons: 0, read: 100, links: 200 } means:
|
|
6
|
+
* - index 0-99: buttons mode, offset = index
|
|
7
|
+
* - index 100-199: read mode, offset = index - 100
|
|
8
|
+
* - index 200+: links mode, offset = index - 200
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Create a mode encoder from a mapping of mode names to base values.
|
|
12
|
+
* Modes are matched by checking the index against bases in descending order.
|
|
13
|
+
*
|
|
14
|
+
* @param modes Record mapping mode names to their base values.
|
|
15
|
+
* All bases must be unique non-negative integers.
|
|
16
|
+
* E.g. { buttons: 0, scroll: 100, steps: 200 }
|
|
17
|
+
*/
|
|
18
|
+
export function createModeEncoder(modes) {
|
|
19
|
+
// Sort entries by base value descending for decode matching
|
|
20
|
+
const entries = Object.entries(modes)
|
|
21
|
+
.sort((a, b) => b[1] - a[1]);
|
|
22
|
+
function decode(index) {
|
|
23
|
+
for (const [mode, base] of entries) {
|
|
24
|
+
if (index >= base) {
|
|
25
|
+
return { mode, offset: index - base };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// Fallback to the lowest base mode
|
|
29
|
+
const last = entries[entries.length - 1];
|
|
30
|
+
return { mode: last[0], offset: index - last[1] };
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
encode(mode, offset = 0) {
|
|
34
|
+
return modes[mode] + offset;
|
|
35
|
+
},
|
|
36
|
+
decode,
|
|
37
|
+
getMode(index) {
|
|
38
|
+
return decode(index).mode;
|
|
39
|
+
},
|
|
40
|
+
getOffset(index) {
|
|
41
|
+
return decode(index).offset;
|
|
42
|
+
},
|
|
43
|
+
getBase(mode) {
|
|
44
|
+
return modes[mode];
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=glass-mode.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"glass-mode.js","sourceRoot":"","sources":["../../glasses/glass-mode.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAeH;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAC/B,KAAwB;IAExB,4DAA4D;IAC5D,MAAM,OAAO,GAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAmB;SACrD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAE/B,SAAS,MAAM,CAAC,KAAa;QAC3B,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,OAAO,EAAE,CAAC;YACnC,IAAI,KAAK,IAAI,IAAI,EAAE,CAAC;gBAClB,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,GAAG,IAAI,EAAE,CAAC;YACxC,CAAC;QACH,CAAC;QACD,mCAAmC;QACnC,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAE,CAAC;QAC1C,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;IACpD,CAAC;IAED,OAAO;QACL,MAAM,CAAC,IAAO,EAAE,MAAM,GAAG,CAAC;YACxB,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC;QAC9B,CAAC;QACD,MAAM;QACN,OAAO,CAAC,KAAa;YACnB,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC;QAC5B,CAAC;QACD,SAAS,CAAC,KAAa;YACrB,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC;QAC9B,CAAC;QACD,OAAO,CAAC,IAAO;YACb,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC;QACrB,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Navigation helpers for G2 glasses display.
|
|
3
|
+
* Pure functions for cursor movement, index clamping, and scroll math.
|
|
4
|
+
*/
|
|
5
|
+
/** Convert gesture direction to numeric delta */
|
|
6
|
+
export declare function directionDelta(direction: 'up' | 'down'): -1 | 1;
|
|
7
|
+
/** Move a highlight index by one step, clamped to [0, max] */
|
|
8
|
+
export declare function moveHighlight(current: number, direction: 'up' | 'down', max: number): number;
|
|
9
|
+
/** Clamp an index to a valid range [0, count - 1] */
|
|
10
|
+
export declare function clampIndex(index: number, count: number): number;
|
|
11
|
+
/** Calculate maximum scroll offset for windowed content */
|
|
12
|
+
export declare function calcMaxScroll(totalLines: number, visibleSlots: number): number;
|
|
13
|
+
/** Move an index with wrapping (loops around) */
|
|
14
|
+
export declare function wrapIndex(current: number, direction: 'up' | 'down', count: number): number;
|
|
15
|
+
//# sourceMappingURL=glass-nav.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"glass-nav.d.ts","sourceRoot":"","sources":["../../glasses/glass-nav.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,iDAAiD;AACjD,wBAAgB,cAAc,CAAC,SAAS,EAAE,IAAI,GAAG,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC,CAE/D;AAED,8DAA8D;AAC9D,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,GAAG,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAG5F;AAED,qDAAqD;AACrD,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED,2DAA2D;AAC3D,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAE9E;AAED,iDAAiD;AACjD,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,GAAG,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAG1F"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Navigation helpers for G2 glasses display.
|
|
3
|
+
* Pure functions for cursor movement, index clamping, and scroll math.
|
|
4
|
+
*/
|
|
5
|
+
/** Convert gesture direction to numeric delta */
|
|
6
|
+
export function directionDelta(direction) {
|
|
7
|
+
return direction === 'up' ? -1 : 1;
|
|
8
|
+
}
|
|
9
|
+
/** Move a highlight index by one step, clamped to [0, max] */
|
|
10
|
+
export function moveHighlight(current, direction, max) {
|
|
11
|
+
const next = current + directionDelta(direction);
|
|
12
|
+
return Math.max(0, Math.min(max, next));
|
|
13
|
+
}
|
|
14
|
+
/** Clamp an index to a valid range [0, count - 1] */
|
|
15
|
+
export function clampIndex(index, count) {
|
|
16
|
+
return Math.min(Math.max(0, index), count - 1);
|
|
17
|
+
}
|
|
18
|
+
/** Calculate maximum scroll offset for windowed content */
|
|
19
|
+
export function calcMaxScroll(totalLines, visibleSlots) {
|
|
20
|
+
return Math.max(0, totalLines - visibleSlots);
|
|
21
|
+
}
|
|
22
|
+
/** Move an index with wrapping (loops around) */
|
|
23
|
+
export function wrapIndex(current, direction, count) {
|
|
24
|
+
if (count <= 0)
|
|
25
|
+
return 0;
|
|
26
|
+
return (current + directionDelta(direction) + count) % count;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=glass-nav.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"glass-nav.js","sourceRoot":"","sources":["../../glasses/glass-nav.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,iDAAiD;AACjD,MAAM,UAAU,cAAc,CAAC,SAAwB;IACrD,OAAO,SAAS,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACrC,CAAC;AAED,8DAA8D;AAC9D,MAAM,UAAU,aAAa,CAAC,OAAe,EAAE,SAAwB,EAAE,GAAW;IAClF,MAAM,IAAI,GAAG,OAAO,GAAG,cAAc,CAAC,SAAS,CAAC,CAAC;IACjD,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC;AAC1C,CAAC;AAED,qDAAqD;AACrD,MAAM,UAAU,UAAU,CAAC,KAAa,EAAE,KAAa;IACrD,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;AACjD,CAAC;AAED,2DAA2D;AAC3D,MAAM,UAAU,aAAa,CAAC,UAAkB,EAAE,YAAoB;IACpE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,UAAU,GAAG,YAAY,CAAC,CAAC;AAChD,CAAC;AAED,iDAAiD;AACjD,MAAM,UAAU,SAAS,CAAC,OAAe,EAAE,SAAwB,EAAE,KAAa;IAChF,IAAI,KAAK,IAAI,CAAC;QAAE,OAAO,CAAC,CAAC;IACzB,OAAO,CAAC,OAAO,GAAG,cAAc,CAAC,SAAS,CAAC,GAAG,KAAK,CAAC,GAAG,KAAK,CAAC;AAC/D,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route-to-screen mapping utilities for G2 glasses.
|
|
3
|
+
* Maps React Router URL paths to glasses screen names.
|
|
4
|
+
*/
|
|
5
|
+
import type { SplashHandle } from './splash';
|
|
6
|
+
export interface ScreenPattern {
|
|
7
|
+
pattern: RegExp | string;
|
|
8
|
+
screen: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Create a deriveScreen function from a list of URL patterns.
|
|
12
|
+
* Patterns are tested in order; first match wins.
|
|
13
|
+
* String patterns are matched exactly. RegExp patterns use .test().
|
|
14
|
+
*
|
|
15
|
+
* @param patterns Array of { pattern, screen } rules
|
|
16
|
+
* @param fallback Screen name to return if no pattern matches
|
|
17
|
+
*/
|
|
18
|
+
export declare function createScreenMapper(patterns: ScreenPattern[], fallback: string): (path: string) => string;
|
|
19
|
+
/**
|
|
20
|
+
* Create a function that extracts an ID from a URL path.
|
|
21
|
+
* @param pattern RegExp with one capture group for the ID
|
|
22
|
+
*/
|
|
23
|
+
export declare function createIdExtractor(pattern: RegExp): (path: string) => string | null;
|
|
24
|
+
/**
|
|
25
|
+
* Extract the first tile from a splash handle for home screen use.
|
|
26
|
+
* Returns an array with a single tile, or empty if no tiles available.
|
|
27
|
+
*/
|
|
28
|
+
export declare function getHomeTiles(splash: SplashHandle): {
|
|
29
|
+
id: number;
|
|
30
|
+
name: string;
|
|
31
|
+
bytes: Uint8Array;
|
|
32
|
+
x: number;
|
|
33
|
+
y: number;
|
|
34
|
+
w: number;
|
|
35
|
+
h: number;
|
|
36
|
+
}[];
|
|
37
|
+
//# sourceMappingURL=glass-router.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"glass-router.d.ts","sourceRoot":"","sources":["../../glasses/glass-router.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAE7C,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,aAAa,EAAE,EACzB,QAAQ,EAAE,MAAM,GACf,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAW1B;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,GACd,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAKjC;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,YAAY,GACnB;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,UAAU,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,CAG/F"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route-to-screen mapping utilities for G2 glasses.
|
|
3
|
+
* Maps React Router URL paths to glasses screen names.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Create a deriveScreen function from a list of URL patterns.
|
|
7
|
+
* Patterns are tested in order; first match wins.
|
|
8
|
+
* String patterns are matched exactly. RegExp patterns use .test().
|
|
9
|
+
*
|
|
10
|
+
* @param patterns Array of { pattern, screen } rules
|
|
11
|
+
* @param fallback Screen name to return if no pattern matches
|
|
12
|
+
*/
|
|
13
|
+
export function createScreenMapper(patterns, fallback) {
|
|
14
|
+
return (path) => {
|
|
15
|
+
for (const { pattern, screen } of patterns) {
|
|
16
|
+
if (typeof pattern === 'string') {
|
|
17
|
+
if (path === pattern)
|
|
18
|
+
return screen;
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
if (pattern.test(path))
|
|
22
|
+
return screen;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return fallback;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Create a function that extracts an ID from a URL path.
|
|
30
|
+
* @param pattern RegExp with one capture group for the ID
|
|
31
|
+
*/
|
|
32
|
+
export function createIdExtractor(pattern) {
|
|
33
|
+
return (path) => {
|
|
34
|
+
const match = path.match(pattern);
|
|
35
|
+
return match ? match[1] ?? null : null;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Extract the first tile from a splash handle for home screen use.
|
|
40
|
+
* Returns an array with a single tile, or empty if no tiles available.
|
|
41
|
+
*/
|
|
42
|
+
export function getHomeTiles(splash) {
|
|
43
|
+
const allTiles = splash.getTiles();
|
|
44
|
+
return allTiles.length > 0 ? [allTiles[0]] : [];
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=glass-router.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"glass-router.js","sourceRoot":"","sources":["../../glasses/glass-router.ts"],"names":[],"mappings":"AAAA;;;GAGG;AASH;;;;;;;GAOG;AACH,MAAM,UAAU,kBAAkB,CAChC,QAAyB,EACzB,QAAgB;IAEhB,OAAO,CAAC,IAAY,EAAU,EAAE;QAC9B,KAAK,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,QAAQ,EAAE,CAAC;YAC3C,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;gBAChC,IAAI,IAAI,KAAK,OAAO;oBAAE,OAAO,MAAM,CAAC;YACtC,CAAC;iBAAM,CAAC;gBACN,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC;oBAAE,OAAO,MAAM,CAAC;YACxC,CAAC;QACH,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAC/B,OAAe;IAEf,OAAO,CAAC,IAAY,EAAiB,EAAE;QACrC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAClC,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;IACzC,CAAC,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAC1B,MAAoB;IAEpB,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAC;IACnC,OAAO,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AACnD,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Screen router for G2 glasses apps.
|
|
3
|
+
*
|
|
4
|
+
* Each screen is a self-contained module with display + action logic.
|
|
5
|
+
* The router composes them into a single toDisplayData + onGlassAction pair
|
|
6
|
+
* that switches on nav.screen automatically.
|
|
7
|
+
*
|
|
8
|
+
* @typeParam S Snapshot type (app state)
|
|
9
|
+
* @typeParam C Context type for side effects (navigate, actions, etc.)
|
|
10
|
+
*/
|
|
11
|
+
import type { DisplayData, GlassNavState, GlassAction } from './types';
|
|
12
|
+
export interface GlassScreen<S, C> {
|
|
13
|
+
/** Render the display for this screen */
|
|
14
|
+
display: (snapshot: S, nav: GlassNavState) => DisplayData;
|
|
15
|
+
/** Handle a glass action. ctx provides side effects like navigate. */
|
|
16
|
+
action: (action: GlassAction, nav: GlassNavState, snapshot: S, ctx: C) => GlassNavState;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Create a screen router from a map of screen definitions.
|
|
20
|
+
*
|
|
21
|
+
* @param screens Record mapping screen names to their display + action handlers
|
|
22
|
+
* @param fallback Screen name to use when nav.screen doesn't match any key
|
|
23
|
+
*/
|
|
24
|
+
export declare function createGlassScreenRouter<S, C>(screens: Record<string, GlassScreen<S, C>>, fallback: string): {
|
|
25
|
+
toDisplayData(snapshot: S, nav: GlassNavState): DisplayData;
|
|
26
|
+
onGlassAction(action: GlassAction, nav: GlassNavState, snapshot: S, ctx: C): GlassNavState;
|
|
27
|
+
};
|
|
28
|
+
//# sourceMappingURL=glass-screen-router.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"glass-screen-router.d.ts","sourceRoot":"","sources":["../../glasses/glass-screen-router.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAEvE,MAAM,WAAW,WAAW,CAAC,CAAC,EAAE,CAAC;IAC/B,yCAAyC;IACzC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,EAAE,GAAG,EAAE,aAAa,KAAK,WAAW,CAAC;IAC1D,sEAAsE;IACtE,MAAM,EAAE,CAAC,MAAM,EAAE,WAAW,EAAE,GAAG,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,KAAK,aAAa,CAAC;CACzF;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,EAAE,CAAC,EAC1C,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAC1C,QAAQ,EAAE,MAAM;4BAOU,CAAC,OAAO,aAAa,GAAG,WAAW;0BAGrC,WAAW,OAAO,aAAa,YAAY,CAAC,OAAO,CAAC,GAAG,aAAa;EAI7F"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Screen router for G2 glasses apps.
|
|
3
|
+
*
|
|
4
|
+
* Each screen is a self-contained module with display + action logic.
|
|
5
|
+
* The router composes them into a single toDisplayData + onGlassAction pair
|
|
6
|
+
* that switches on nav.screen automatically.
|
|
7
|
+
*
|
|
8
|
+
* @typeParam S Snapshot type (app state)
|
|
9
|
+
* @typeParam C Context type for side effects (navigate, actions, etc.)
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Create a screen router from a map of screen definitions.
|
|
13
|
+
*
|
|
14
|
+
* @param screens Record mapping screen names to their display + action handlers
|
|
15
|
+
* @param fallback Screen name to use when nav.screen doesn't match any key
|
|
16
|
+
*/
|
|
17
|
+
export function createGlassScreenRouter(screens, fallback) {
|
|
18
|
+
const getScreen = (name) => {
|
|
19
|
+
return screens[name] ?? screens[fallback];
|
|
20
|
+
};
|
|
21
|
+
return {
|
|
22
|
+
toDisplayData(snapshot, nav) {
|
|
23
|
+
return getScreen(nav.screen).display(snapshot, nav);
|
|
24
|
+
},
|
|
25
|
+
onGlassAction(action, nav, snapshot, ctx) {
|
|
26
|
+
return getScreen(nav.screen).action(action, nav, snapshot, ctx);
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=glass-screen-router.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"glass-screen-router.js","sourceRoot":"","sources":["../../glasses/glass-screen-router.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAWH;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CACrC,OAA0C,EAC1C,QAAgB;IAEhB,MAAM,SAAS,GAAG,CAAC,IAAY,EAAqB,EAAE;QACpD,OAAO,OAAO,CAAC,IAAI,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAE,CAAC;IAC7C,CAAC,CAAC;IAEF,OAAO;QACL,aAAa,CAAC,QAAW,EAAE,GAAkB;YAC3C,OAAO,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;QACtD,CAAC;QACD,aAAa,CAAC,MAAmB,EAAE,GAAkB,EAAE,QAAW,EAAE,GAAM;YACxE,OAAO,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,CAAC,CAAC;QAClE,CAAC;KACF,CAAC;AACJ,CAAC"}
|
package/dist/glasses/index.d.ts
CHANGED
|
@@ -5,4 +5,9 @@ export * from './timer-display';
|
|
|
5
5
|
export * from './gestures';
|
|
6
6
|
export * from './text-clean';
|
|
7
7
|
export * from './paginate-text';
|
|
8
|
+
export * from './glass-nav';
|
|
9
|
+
export * from './glass-display-builders';
|
|
10
|
+
export * from './glass-mode';
|
|
11
|
+
export * from './glass-router';
|
|
12
|
+
export * from './glass-screen-router';
|
|
8
13
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../glasses/index.ts"],"names":[],"mappings":"AAGA,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../glasses/index.ts"],"names":[],"mappings":"AAGA,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,0BAA0B,CAAC;AACzC,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,uBAAuB,CAAC"}
|
package/dist/glasses/index.js
CHANGED
|
@@ -7,4 +7,9 @@ export * from './timer-display';
|
|
|
7
7
|
export * from './gestures';
|
|
8
8
|
export * from './text-clean';
|
|
9
9
|
export * from './paginate-text';
|
|
10
|
+
export * from './glass-nav';
|
|
11
|
+
export * from './glass-display-builders';
|
|
12
|
+
export * from './glass-mode';
|
|
13
|
+
export * from './glass-router';
|
|
14
|
+
export * from './glass-screen-router';
|
|
10
15
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../glasses/index.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,4FAA4F;AAE5F,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../glasses/index.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,4FAA4F;AAE5F,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,0BAA0B,CAAC;AACzC,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,uBAAuB,CAAC"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Display composition utilities for G2 glasses.
|
|
3
|
+
* Builds common screen layouts: scrollable lists, scrollable content with headers.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { DisplayLine, DisplayData } from './types';
|
|
7
|
+
import { line, glassHeader } from './types';
|
|
8
|
+
import { applyScrollIndicators } from './text-utils';
|
|
9
|
+
|
|
10
|
+
/** G2 display fits 10 lines of text */
|
|
11
|
+
export const G2_TEXT_LINES = 10;
|
|
12
|
+
|
|
13
|
+
/** glassHeader() produces 2 DisplayLines that occupy 3 visual lines (title + separator + gap) */
|
|
14
|
+
export const HEADER_LINES = 3;
|
|
15
|
+
|
|
16
|
+
/** Default content slots below a glassHeader */
|
|
17
|
+
export const DEFAULT_CONTENT_SLOTS = G2_TEXT_LINES - HEADER_LINES;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Calculate the start index for a centered sliding window.
|
|
21
|
+
* Keeps the highlighted item roughly centered in the visible area.
|
|
22
|
+
*/
|
|
23
|
+
export function slidingWindowStart(
|
|
24
|
+
highlightedIndex: number,
|
|
25
|
+
totalItems: number,
|
|
26
|
+
maxVisible: number,
|
|
27
|
+
): number {
|
|
28
|
+
if (totalItems <= maxVisible) return 0;
|
|
29
|
+
return Math.max(0, Math.min(
|
|
30
|
+
highlightedIndex - Math.floor(maxVisible / 2),
|
|
31
|
+
totalItems - maxVisible,
|
|
32
|
+
));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ScrollableListOptions<T> {
|
|
36
|
+
items: T[];
|
|
37
|
+
highlightedIndex: number;
|
|
38
|
+
maxVisible: number;
|
|
39
|
+
/** Format an item into a display string */
|
|
40
|
+
formatter: (item: T, index: number) => string;
|
|
41
|
+
/** Line style for list items. Default: 'normal' */
|
|
42
|
+
style?: 'normal' | 'meta';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Build a scrollable highlighted list with ▲/▼ scroll indicators.
|
|
47
|
+
* Returns an array of DisplayLines ready to use as DisplayData.lines.
|
|
48
|
+
*/
|
|
49
|
+
export function buildScrollableList<T>(opts: ScrollableListOptions<T>): DisplayLine[] {
|
|
50
|
+
const { items, highlightedIndex, maxVisible, formatter, style = 'normal' } = opts;
|
|
51
|
+
|
|
52
|
+
const start = slidingWindowStart(highlightedIndex, items.length, maxVisible);
|
|
53
|
+
const visible = items.slice(start, start + maxVisible).map((item, i) => {
|
|
54
|
+
const idx = start + i;
|
|
55
|
+
return line(formatter(item, idx), style, idx === highlightedIndex);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
applyScrollIndicators(visible, start, items.length, maxVisible, (t) => line(t, 'meta', false));
|
|
59
|
+
|
|
60
|
+
return visible;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface ScrollableContentOptions {
|
|
64
|
+
title: string;
|
|
65
|
+
actionBar: string;
|
|
66
|
+
contentLines: string[];
|
|
67
|
+
scrollPos: number;
|
|
68
|
+
/** Number of visible content lines. Default: DEFAULT_CONTENT_SLOTS (7) */
|
|
69
|
+
contentSlots?: number;
|
|
70
|
+
/** Style for content lines. Default: 'meta' */
|
|
71
|
+
contentStyle?: 'normal' | 'meta';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Build a header + windowed content display with scroll indicators.
|
|
76
|
+
* Produces a complete DisplayData with glassHeader at the top,
|
|
77
|
+
* followed by a scrollable window of content lines.
|
|
78
|
+
*/
|
|
79
|
+
export function buildScrollableContent(opts: ScrollableContentOptions): DisplayData {
|
|
80
|
+
const {
|
|
81
|
+
title,
|
|
82
|
+
actionBar,
|
|
83
|
+
contentLines,
|
|
84
|
+
scrollPos,
|
|
85
|
+
contentSlots = DEFAULT_CONTENT_SLOTS,
|
|
86
|
+
contentStyle = 'meta',
|
|
87
|
+
} = opts;
|
|
88
|
+
|
|
89
|
+
const lines = [...glassHeader(title, actionBar)];
|
|
90
|
+
|
|
91
|
+
const start = Math.max(0, Math.min(scrollPos, contentLines.length - contentSlots));
|
|
92
|
+
const visible = contentLines.slice(start, start + contentSlots);
|
|
93
|
+
|
|
94
|
+
const contentDisplayLines: DisplayLine[] = [];
|
|
95
|
+
for (const text of visible) {
|
|
96
|
+
contentDisplayLines.push(line(text, contentStyle, false));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
applyScrollIndicators(
|
|
100
|
+
contentDisplayLines,
|
|
101
|
+
start,
|
|
102
|
+
contentLines.length,
|
|
103
|
+
contentSlots,
|
|
104
|
+
(t) => line(t, 'meta', false),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
lines.push(...contentDisplayLines);
|
|
108
|
+
|
|
109
|
+
return { lines };
|
|
110
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mode encoding for G2 glasses navigation.
|
|
3
|
+
*
|
|
4
|
+
* Packs a mode identifier + offset into a single highlightedIndex number.
|
|
5
|
+
* E.g. { buttons: 0, read: 100, links: 200 } means:
|
|
6
|
+
* - index 0-99: buttons mode, offset = index
|
|
7
|
+
* - index 100-199: read mode, offset = index - 100
|
|
8
|
+
* - index 200+: links mode, offset = index - 200
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface ModeEncoder<M extends string> {
|
|
12
|
+
/** Encode a mode + offset into a single highlightedIndex value */
|
|
13
|
+
encode(mode: M, offset?: number): number;
|
|
14
|
+
/** Decode an index back to { mode, offset } */
|
|
15
|
+
decode(index: number): { mode: M; offset: number };
|
|
16
|
+
/** Get just the mode name from an encoded index */
|
|
17
|
+
getMode(index: number): M;
|
|
18
|
+
/** Get just the offset from an encoded index */
|
|
19
|
+
getOffset(index: number): number;
|
|
20
|
+
/** Get the base value for a mode */
|
|
21
|
+
getBase(mode: M): number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Create a mode encoder from a mapping of mode names to base values.
|
|
26
|
+
* Modes are matched by checking the index against bases in descending order.
|
|
27
|
+
*
|
|
28
|
+
* @param modes Record mapping mode names to their base values.
|
|
29
|
+
* All bases must be unique non-negative integers.
|
|
30
|
+
* E.g. { buttons: 0, scroll: 100, steps: 200 }
|
|
31
|
+
*/
|
|
32
|
+
export function createModeEncoder<M extends string>(
|
|
33
|
+
modes: Record<M, number>,
|
|
34
|
+
): ModeEncoder<M> {
|
|
35
|
+
// Sort entries by base value descending for decode matching
|
|
36
|
+
const entries = (Object.entries(modes) as [M, number][])
|
|
37
|
+
.sort((a, b) => b[1] - a[1]);
|
|
38
|
+
|
|
39
|
+
function decode(index: number): { mode: M; offset: number } {
|
|
40
|
+
for (const [mode, base] of entries) {
|
|
41
|
+
if (index >= base) {
|
|
42
|
+
return { mode, offset: index - base };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Fallback to the lowest base mode
|
|
46
|
+
const last = entries[entries.length - 1]!;
|
|
47
|
+
return { mode: last[0], offset: index - last[1] };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
encode(mode: M, offset = 0): number {
|
|
52
|
+
return modes[mode] + offset;
|
|
53
|
+
},
|
|
54
|
+
decode,
|
|
55
|
+
getMode(index: number): M {
|
|
56
|
+
return decode(index).mode;
|
|
57
|
+
},
|
|
58
|
+
getOffset(index: number): number {
|
|
59
|
+
return decode(index).offset;
|
|
60
|
+
},
|
|
61
|
+
getBase(mode: M): number {
|
|
62
|
+
return modes[mode];
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Navigation helpers for G2 glasses display.
|
|
3
|
+
* Pure functions for cursor movement, index clamping, and scroll math.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Convert gesture direction to numeric delta */
|
|
7
|
+
export function directionDelta(direction: 'up' | 'down'): -1 | 1 {
|
|
8
|
+
return direction === 'up' ? -1 : 1;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Move a highlight index by one step, clamped to [0, max] */
|
|
12
|
+
export function moveHighlight(current: number, direction: 'up' | 'down', max: number): number {
|
|
13
|
+
const next = current + directionDelta(direction);
|
|
14
|
+
return Math.max(0, Math.min(max, next));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Clamp an index to a valid range [0, count - 1] */
|
|
18
|
+
export function clampIndex(index: number, count: number): number {
|
|
19
|
+
return Math.min(Math.max(0, index), count - 1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Calculate maximum scroll offset for windowed content */
|
|
23
|
+
export function calcMaxScroll(totalLines: number, visibleSlots: number): number {
|
|
24
|
+
return Math.max(0, totalLines - visibleSlots);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Move an index with wrapping (loops around) */
|
|
28
|
+
export function wrapIndex(current: number, direction: 'up' | 'down', count: number): number {
|
|
29
|
+
if (count <= 0) return 0;
|
|
30
|
+
return (current + directionDelta(direction) + count) % count;
|
|
31
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route-to-screen mapping utilities for G2 glasses.
|
|
3
|
+
* Maps React Router URL paths to glasses screen names.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { SplashHandle } from './splash';
|
|
7
|
+
|
|
8
|
+
export interface ScreenPattern {
|
|
9
|
+
pattern: RegExp | string;
|
|
10
|
+
screen: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Create a deriveScreen function from a list of URL patterns.
|
|
15
|
+
* Patterns are tested in order; first match wins.
|
|
16
|
+
* String patterns are matched exactly. RegExp patterns use .test().
|
|
17
|
+
*
|
|
18
|
+
* @param patterns Array of { pattern, screen } rules
|
|
19
|
+
* @param fallback Screen name to return if no pattern matches
|
|
20
|
+
*/
|
|
21
|
+
export function createScreenMapper(
|
|
22
|
+
patterns: ScreenPattern[],
|
|
23
|
+
fallback: string,
|
|
24
|
+
): (path: string) => string {
|
|
25
|
+
return (path: string): string => {
|
|
26
|
+
for (const { pattern, screen } of patterns) {
|
|
27
|
+
if (typeof pattern === 'string') {
|
|
28
|
+
if (path === pattern) return screen;
|
|
29
|
+
} else {
|
|
30
|
+
if (pattern.test(path)) return screen;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return fallback;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create a function that extracts an ID from a URL path.
|
|
39
|
+
* @param pattern RegExp with one capture group for the ID
|
|
40
|
+
*/
|
|
41
|
+
export function createIdExtractor(
|
|
42
|
+
pattern: RegExp,
|
|
43
|
+
): (path: string) => string | null {
|
|
44
|
+
return (path: string): string | null => {
|
|
45
|
+
const match = path.match(pattern);
|
|
46
|
+
return match ? match[1] ?? null : null;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Extract the first tile from a splash handle for home screen use.
|
|
52
|
+
* Returns an array with a single tile, or empty if no tiles available.
|
|
53
|
+
*/
|
|
54
|
+
export function getHomeTiles(
|
|
55
|
+
splash: SplashHandle,
|
|
56
|
+
): { id: number; name: string; bytes: Uint8Array; x: number; y: number; w: number; h: number }[] {
|
|
57
|
+
const allTiles = splash.getTiles();
|
|
58
|
+
return allTiles.length > 0 ? [allTiles[0]!] : [];
|
|
59
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Screen router for G2 glasses apps.
|
|
3
|
+
*
|
|
4
|
+
* Each screen is a self-contained module with display + action logic.
|
|
5
|
+
* The router composes them into a single toDisplayData + onGlassAction pair
|
|
6
|
+
* that switches on nav.screen automatically.
|
|
7
|
+
*
|
|
8
|
+
* @typeParam S Snapshot type (app state)
|
|
9
|
+
* @typeParam C Context type for side effects (navigate, actions, etc.)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { DisplayData, GlassNavState, GlassAction } from './types';
|
|
13
|
+
|
|
14
|
+
export interface GlassScreen<S, C> {
|
|
15
|
+
/** Render the display for this screen */
|
|
16
|
+
display: (snapshot: S, nav: GlassNavState) => DisplayData;
|
|
17
|
+
/** Handle a glass action. ctx provides side effects like navigate. */
|
|
18
|
+
action: (action: GlassAction, nav: GlassNavState, snapshot: S, ctx: C) => GlassNavState;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Create a screen router from a map of screen definitions.
|
|
23
|
+
*
|
|
24
|
+
* @param screens Record mapping screen names to their display + action handlers
|
|
25
|
+
* @param fallback Screen name to use when nav.screen doesn't match any key
|
|
26
|
+
*/
|
|
27
|
+
export function createGlassScreenRouter<S, C>(
|
|
28
|
+
screens: Record<string, GlassScreen<S, C>>,
|
|
29
|
+
fallback: string,
|
|
30
|
+
) {
|
|
31
|
+
const getScreen = (name: string): GlassScreen<S, C> => {
|
|
32
|
+
return screens[name] ?? screens[fallback]!;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
toDisplayData(snapshot: S, nav: GlassNavState): DisplayData {
|
|
37
|
+
return getScreen(nav.screen).display(snapshot, nav);
|
|
38
|
+
},
|
|
39
|
+
onGlassAction(action: GlassAction, nav: GlassNavState, snapshot: S, ctx: C): GlassNavState {
|
|
40
|
+
return getScreen(nav.screen).action(action, nav, snapshot, ctx);
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
package/glasses/index.ts
CHANGED
|
@@ -8,3 +8,8 @@ export * from './timer-display';
|
|
|
8
8
|
export * from './gestures';
|
|
9
9
|
export * from './text-clean';
|
|
10
10
|
export * from './paginate-text';
|
|
11
|
+
export * from './glass-nav';
|
|
12
|
+
export * from './glass-display-builders';
|
|
13
|
+
export * from './glass-mode';
|
|
14
|
+
export * from './glass-router';
|
|
15
|
+
export * from './glass-screen-router';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "even-toolkit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"description": "Design system & component library for Even Realities G2 smart glasses apps — 55+ web components, 191 pixel-art icons, glasses SDK bridge, and design tokens.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/glasses/index.js",
|
|
@@ -82,6 +82,26 @@
|
|
|
82
82
|
"types": "./dist/glasses/paginate-text.d.ts",
|
|
83
83
|
"import": "./dist/glasses/paginate-text.js"
|
|
84
84
|
},
|
|
85
|
+
"./glass-nav": {
|
|
86
|
+
"types": "./dist/glasses/glass-nav.d.ts",
|
|
87
|
+
"import": "./dist/glasses/glass-nav.js"
|
|
88
|
+
},
|
|
89
|
+
"./glass-display-builders": {
|
|
90
|
+
"types": "./dist/glasses/glass-display-builders.d.ts",
|
|
91
|
+
"import": "./dist/glasses/glass-display-builders.js"
|
|
92
|
+
},
|
|
93
|
+
"./glass-mode": {
|
|
94
|
+
"types": "./dist/glasses/glass-mode.d.ts",
|
|
95
|
+
"import": "./dist/glasses/glass-mode.js"
|
|
96
|
+
},
|
|
97
|
+
"./glass-router": {
|
|
98
|
+
"types": "./dist/glasses/glass-router.d.ts",
|
|
99
|
+
"import": "./dist/glasses/glass-router.js"
|
|
100
|
+
},
|
|
101
|
+
"./glass-screen-router": {
|
|
102
|
+
"types": "./dist/glasses/glass-screen-router.d.ts",
|
|
103
|
+
"import": "./dist/glasses/glass-screen-router.js"
|
|
104
|
+
},
|
|
85
105
|
"./web": {
|
|
86
106
|
"types": "./dist/web/index.d.ts",
|
|
87
107
|
"import": "./dist/web/index.js"
|