even-toolkit 1.3.1 → 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.
- package/dist/glasses/action-bar.d.ts +8 -8
- package/dist/glasses/action-bar.d.ts.map +1 -1
- package/dist/glasses/action-bar.js +11 -13
- package/dist/glasses/action-bar.js.map +1 -1
- 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/action-bar.ts +11 -13
- 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
|
@@ -2,23 +2,23 @@
|
|
|
2
2
|
* Shared action button bar for G2 glasses display.
|
|
3
3
|
*
|
|
4
4
|
* Renders a row of named buttons with triangle indicators:
|
|
5
|
-
* ▶Timer◀ Scroll
|
|
5
|
+
* ▶Timer◀ ▷Scroll◁ Steps
|
|
6
6
|
*
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
7
|
+
* - Active button (mode entered): filled triangles ▶Name◀
|
|
8
|
+
* - Selected button (hovering in button-select mode): empty triangles ▷Name◁
|
|
9
9
|
* - Inactive button: plain Name
|
|
10
10
|
*/
|
|
11
11
|
/**
|
|
12
12
|
* Build an action bar string from a list of button names.
|
|
13
13
|
*
|
|
14
14
|
* @param buttons Array of button label strings, e.g. ['Timer', 'Scroll', 'Steps']
|
|
15
|
-
* @param selectedIndex Index of the currently highlighted button
|
|
16
|
-
* @param activeLabel Label of the currently active mode button
|
|
17
|
-
* @param
|
|
15
|
+
* @param selectedIndex Index of the currently highlighted button
|
|
16
|
+
* @param activeLabel Label of the currently active mode button, or null if in button-select mode
|
|
17
|
+
* @param _flashPhase Unused, kept for API compatibility
|
|
18
18
|
*/
|
|
19
|
-
export declare function buildActionBar(buttons: string[], selectedIndex: number, activeLabel: string | null,
|
|
19
|
+
export declare function buildActionBar(buttons: string[], selectedIndex: number, activeLabel: string | null, _flashPhase?: boolean): string;
|
|
20
20
|
/**
|
|
21
|
-
* Build a static action bar (
|
|
21
|
+
* Build a static action bar (empty triangles on selected).
|
|
22
22
|
* Useful for screens like recipe detail or completion where there's no mode switching.
|
|
23
23
|
*/
|
|
24
24
|
export declare function buildStaticActionBar(buttons: string[], selectedIndex: number): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action-bar.d.ts","sourceRoot":"","sources":["../../glasses/action-bar.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EAAE,EACjB,aAAa,EAAE,MAAM,EACrB,WAAW,EAAE,MAAM,GAAG,IAAI,EAC1B,
|
|
1
|
+
{"version":3,"file":"action-bar.d.ts","sourceRoot":"","sources":["../../glasses/action-bar.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EAAE,EACjB,aAAa,EAAE,MAAM,EACrB,WAAW,EAAE,MAAM,GAAG,IAAI,EAC1B,WAAW,CAAC,EAAE,OAAO,GACpB,MAAM,CAcR;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EAAE,EACjB,aAAa,EAAE,MAAM,GACpB,MAAM,CAOR"}
|
|
@@ -2,38 +2,36 @@
|
|
|
2
2
|
* Shared action button bar for G2 glasses display.
|
|
3
3
|
*
|
|
4
4
|
* Renders a row of named buttons with triangle indicators:
|
|
5
|
-
* ▶Timer◀ Scroll
|
|
5
|
+
* ▶Timer◀ ▷Scroll◁ Steps
|
|
6
6
|
*
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
7
|
+
* - Active button (mode entered): filled triangles ▶Name◀
|
|
8
|
+
* - Selected button (hovering in button-select mode): empty triangles ▷Name◁
|
|
9
9
|
* - Inactive button: plain Name
|
|
10
10
|
*/
|
|
11
11
|
/**
|
|
12
12
|
* Build an action bar string from a list of button names.
|
|
13
13
|
*
|
|
14
14
|
* @param buttons Array of button label strings, e.g. ['Timer', 'Scroll', 'Steps']
|
|
15
|
-
* @param selectedIndex Index of the currently highlighted button
|
|
16
|
-
* @param activeLabel Label of the currently active mode button
|
|
17
|
-
* @param
|
|
15
|
+
* @param selectedIndex Index of the currently highlighted button
|
|
16
|
+
* @param activeLabel Label of the currently active mode button, or null if in button-select mode
|
|
17
|
+
* @param _flashPhase Unused, kept for API compatibility
|
|
18
18
|
*/
|
|
19
|
-
export function buildActionBar(buttons, selectedIndex, activeLabel,
|
|
19
|
+
export function buildActionBar(buttons, selectedIndex, activeLabel, _flashPhase) {
|
|
20
20
|
const activeIdx = activeLabel ? buttons.indexOf(activeLabel) : -1;
|
|
21
21
|
return buttons.map((name, i) => {
|
|
22
22
|
if (activeIdx === i) {
|
|
23
|
-
// Active mode:
|
|
24
|
-
|
|
25
|
-
const R = flashPhase ? '\u25C0' : '\u25C1'; // ◀ / ◁
|
|
26
|
-
return `${L}${name}${R}`;
|
|
23
|
+
// Active mode: filled triangles
|
|
24
|
+
return `\u25B6${name}\u25C0`;
|
|
27
25
|
}
|
|
28
26
|
if (activeIdx < 0 && i === selectedIndex) {
|
|
29
|
-
//
|
|
27
|
+
// Hovering in button-select mode: empty triangles
|
|
30
28
|
return `\u25B7${name}\u25C1`;
|
|
31
29
|
}
|
|
32
30
|
return ` ${name} `;
|
|
33
31
|
}).join(' ');
|
|
34
32
|
}
|
|
35
33
|
/**
|
|
36
|
-
* Build a static action bar (
|
|
34
|
+
* Build a static action bar (empty triangles on selected).
|
|
37
35
|
* Useful for screens like recipe detail or completion where there's no mode switching.
|
|
38
36
|
*/
|
|
39
37
|
export function buildStaticActionBar(buttons, selectedIndex) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action-bar.js","sourceRoot":"","sources":["../../glasses/action-bar.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAC5B,OAAiB,EACjB,aAAqB,EACrB,WAA0B,EAC1B,
|
|
1
|
+
{"version":3,"file":"action-bar.js","sourceRoot":"","sources":["../../glasses/action-bar.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAC5B,OAAiB,EACjB,aAAqB,EACrB,WAA0B,EAC1B,WAAqB;IAErB,MAAM,SAAS,GAAG,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAElE,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE;QAC7B,IAAI,SAAS,KAAK,CAAC,EAAE,CAAC;YACpB,gCAAgC;YAChC,OAAO,SAAS,IAAI,QAAQ,CAAC;QAC/B,CAAC;QACD,IAAI,SAAS,GAAG,CAAC,IAAI,CAAC,KAAK,aAAa,EAAE,CAAC;YACzC,kDAAkD;YAClD,OAAO,SAAS,IAAI,QAAQ,CAAC;QAC/B,CAAC;QACD,OAAO,IAAI,IAAI,GAAG,CAAC;IACrB,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACf,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAClC,OAAiB,EACjB,aAAqB;IAErB,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE;QAC7B,IAAI,CAAC,KAAK,aAAa,EAAE,CAAC;YACxB,OAAO,SAAS,IAAI,QAAQ,CAAC;QAC/B,CAAC;QACD,OAAO,IAAI,IAAI,GAAG,CAAC;IACrB,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACf,CAAC"}
|
|
@@ -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"}
|
package/glasses/action-bar.ts
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* Shared action button bar for G2 glasses display.
|
|
3
3
|
*
|
|
4
4
|
* Renders a row of named buttons with triangle indicators:
|
|
5
|
-
* ▶Timer◀ Scroll
|
|
5
|
+
* ▶Timer◀ ▷Scroll◁ Steps
|
|
6
6
|
*
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
7
|
+
* - Active button (mode entered): filled triangles ▶Name◀
|
|
8
|
+
* - Selected button (hovering in button-select mode): empty triangles ▷Name◁
|
|
9
9
|
* - Inactive button: plain Name
|
|
10
10
|
*/
|
|
11
11
|
|
|
@@ -13,27 +13,25 @@
|
|
|
13
13
|
* Build an action bar string from a list of button names.
|
|
14
14
|
*
|
|
15
15
|
* @param buttons Array of button label strings, e.g. ['Timer', 'Scroll', 'Steps']
|
|
16
|
-
* @param selectedIndex Index of the currently highlighted button
|
|
17
|
-
* @param activeLabel Label of the currently active mode button
|
|
18
|
-
* @param
|
|
16
|
+
* @param selectedIndex Index of the currently highlighted button
|
|
17
|
+
* @param activeLabel Label of the currently active mode button, or null if in button-select mode
|
|
18
|
+
* @param _flashPhase Unused, kept for API compatibility
|
|
19
19
|
*/
|
|
20
20
|
export function buildActionBar(
|
|
21
21
|
buttons: string[],
|
|
22
22
|
selectedIndex: number,
|
|
23
23
|
activeLabel: string | null,
|
|
24
|
-
|
|
24
|
+
_flashPhase?: boolean,
|
|
25
25
|
): string {
|
|
26
26
|
const activeIdx = activeLabel ? buttons.indexOf(activeLabel) : -1;
|
|
27
27
|
|
|
28
28
|
return buttons.map((name, i) => {
|
|
29
29
|
if (activeIdx === i) {
|
|
30
|
-
// Active mode:
|
|
31
|
-
|
|
32
|
-
const R = flashPhase ? '\u25C0' : '\u25C1'; // ◀ / ◁
|
|
33
|
-
return `${L}${name}${R}`;
|
|
30
|
+
// Active mode: filled triangles
|
|
31
|
+
return `\u25B6${name}\u25C0`;
|
|
34
32
|
}
|
|
35
33
|
if (activeIdx < 0 && i === selectedIndex) {
|
|
36
|
-
//
|
|
34
|
+
// Hovering in button-select mode: empty triangles
|
|
37
35
|
return `\u25B7${name}\u25C1`;
|
|
38
36
|
}
|
|
39
37
|
return ` ${name} `;
|
|
@@ -41,7 +39,7 @@ export function buildActionBar(
|
|
|
41
39
|
}
|
|
42
40
|
|
|
43
41
|
/**
|
|
44
|
-
* Build a static action bar (
|
|
42
|
+
* Build a static action bar (empty triangles on selected).
|
|
45
43
|
* Useful for screens like recipe detail or completion where there's no mode switching.
|
|
46
44
|
*/
|
|
47
45
|
export function buildStaticActionBar(
|
|
@@ -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.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"
|