even-toolkit 1.3.2 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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"}
@@ -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.0",
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"