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 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
- ### `/glasses` — G2 Glasses SDK Bridge
53
+ ---
48
54
 
49
- Everything needed to render content on the G2 glasses display.
55
+ ## Glasses SDK
50
56
 
51
- ```tsx
52
- import { useGlasses } from 'even-toolkit/useGlasses';
53
- import { line, separator } from 'even-toolkit/types';
54
- import { buildActionBar } from 'even-toolkit/action-bar';
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
- **Core:** EvenHubBridge, useGlasses hook, useFlashPhase hook
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
- **Display:** DisplayData, DisplayLine, line(), separator(), text-utils, timer-display, canvas-renderer
107
+ #### Wire screens together
60
108
 
61
- **Input:** action-map, gestures, keyboard bindings
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
- **Layout:** 576x288px display, text/columns/chart/home page modes, image tiles
114
+ export const { toDisplayData, onGlassAction } = createGlassScreenRouter({
115
+ 'home': homeScreen,
116
+ 'detail': detailScreen,
117
+ }, 'home');
118
+ ```
64
119
 
65
- **Utilities:** splash screens, PNG encoding, text cleaning, pagination, keep-alive
120
+ ### Navigation Helpers (`glass-nav`)
66
121
 
67
- ### Glasses Display Helpers
122
+ ```ts
123
+ import { moveHighlight, clampIndex, calcMaxScroll, wrapIndex } from 'even-toolkit/glass-nav';
68
124
 
69
- ```tsx
70
- import { glassHeader, line, separator } from 'even-toolkit/types';
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
- // Header with separator
73
- lines.push(...glassHeader('MY APP'));
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
- // Header with action bar
76
- lines.push(...glassHeader('STEP 1/4', buildActionBar([...], 0, null, true)));
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":"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"}
@@ -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"}
@@ -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"}
@@ -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.2",
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"