@guuey/agent-layout 0.16.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/LICENSE +21 -0
- package/README.md +60 -0
- package/dist/bind.d.ts +56 -0
- package/dist/bind.d.ts.map +1 -0
- package/dist/bind.js +31 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/machine.d.ts +59 -0
- package/dist/machine.d.ts.map +1 -0
- package/dist/machine.js +52 -0
- package/dist/react.d.ts +109 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +121 -0
- package/dist/tones.d.ts +79 -0
- package/dist/tones.d.ts.map +1 -0
- package/dist/tones.js +131 -0
- package/package.json +55 -0
- package/src/bind.ts +73 -0
- package/src/index.ts +54 -0
- package/src/machine.ts +76 -0
- package/src/react.tsx +305 -0
- package/src/tones.ts +156 -0
- package/styles.css +180 -0
package/dist/tones.d.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tone math (guuey#403 §2) — the two-panel tone pair, its perceptibility
|
|
3
|
+
* floor, and the chat-palette derivation.
|
|
4
|
+
*
|
|
5
|
+
* The floor is CALIBRATED, not invented (ggui#633's scars): the console's
|
|
6
|
+
* first cut at ΔL* 3.5 measured below perception; their shipped pair
|
|
7
|
+
* (#F4F3ED warm paper vs #E4E4E2 neutral chrome) reads clearly at ΔL* 5.2
|
|
8
|
+
* because the warm→neutral TEMPERATURE step does perceptual work lightness
|
|
9
|
+
* alone doesn't. Hence the OR-form floor:
|
|
10
|
+
*
|
|
11
|
+
* ΔL* ≥ 6 — OR — a hue-temperature step (Δab ≥ 2) with ΔL* ≥ 5
|
|
12
|
+
*
|
|
13
|
+
* Both arms reject the failed 3.5 cut; the strict arm alone would have
|
|
14
|
+
* rejected the founder-certified shipped pair. The platform's theme-doc
|
|
15
|
+
* gate applies its own mirror of this rule server-side; this module is the
|
|
16
|
+
* lib's own honesty (derived defaults satisfy the floor BY CONSTRUCTION —
|
|
17
|
+
* pairs that land under it get the documented nudge apart, never a silent
|
|
18
|
+
* pass). Calibration may RAISE the floor; it does not lower it.
|
|
19
|
+
*/
|
|
20
|
+
/** A resolved tone pair for one mode: two backgrounds + their foregrounds. */
|
|
21
|
+
export interface TonePair {
|
|
22
|
+
/** Upper panel (app menus) background. */
|
|
23
|
+
upper: string;
|
|
24
|
+
/** Foreground on the upper tone. */
|
|
25
|
+
upperOn: string;
|
|
26
|
+
/** Lower panel (agent) background. */
|
|
27
|
+
lower: string;
|
|
28
|
+
/** Foreground on the lower tone. */
|
|
29
|
+
lowerOn: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The lib base defaults — the ggui#633 SHIPPED pair for light, and its
|
|
33
|
+
* temperature-step mirror for dark (warm umber menu vs neutral chrome,
|
|
34
|
+
* ΔL* 6.5 — the strict arm). Founder-certified "two-toned and
|
|
35
|
+
* well-distinguished" in light; the dark pair keeps the identical warm→
|
|
36
|
+
* neutral grammar so a mode flip changes brightness, not the layout's
|
|
37
|
+
* character. Both pairs assert the floor in this package's tests.
|
|
38
|
+
*/
|
|
39
|
+
export declare const DEFAULT_TONES: Record<"light" | "dark", TonePair>;
|
|
40
|
+
/** CIELAB (D65) from a `#rrggbb` hex. Throws on a malformed color. */
|
|
41
|
+
export declare function hexToLab(hex: string): {
|
|
42
|
+
L: number;
|
|
43
|
+
a: number;
|
|
44
|
+
b: number;
|
|
45
|
+
};
|
|
46
|
+
/** The perceptibility deltas between two backgrounds. */
|
|
47
|
+
export declare function toneDelta(upper: string, lower: string): {
|
|
48
|
+
dL: number;
|
|
49
|
+
dab: number;
|
|
50
|
+
};
|
|
51
|
+
/** The OR-form floor (module header). */
|
|
52
|
+
export declare function meetsToneFloor(upper: string, lower: string): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Reject-under-floor with the explanatory message (the brandAccent
|
|
55
|
+
* posture): overrides that fail perceptibility throw HERE, at wiring time,
|
|
56
|
+
* never render as an invisible seam.
|
|
57
|
+
*/
|
|
58
|
+
export declare function assertToneFloor(pair: TonePair): void;
|
|
59
|
+
/**
|
|
60
|
+
* Base-tier derivation (§2, platform's pick): tones DERIVE from the chat
|
|
61
|
+
* palette's surface/canvas pair, so an already-themed app gets coherent
|
|
62
|
+
* tones for free — theming chat themes the layout, no second authoring
|
|
63
|
+
* step. `surface` grounds the AGENT (lower) panel — it is the chat's own
|
|
64
|
+
* ground — and `canvas` grounds the MENU (upper) panel. Pairs that land
|
|
65
|
+
* under the floor get the documented nudge: the upper tone walks away from
|
|
66
|
+
* the lower along the lightness axis (toward white below L* 50, toward
|
|
67
|
+
* black above — always INTO the pair's existing contrast direction) until
|
|
68
|
+
* the strict arm passes. Deterministic, bounded, and asserted by tests.
|
|
69
|
+
*/
|
|
70
|
+
export declare function deriveTones(palette: {
|
|
71
|
+
surface: string;
|
|
72
|
+
canvas: string;
|
|
73
|
+
ink: string;
|
|
74
|
+
}): Pick<TonePair, "upper" | "lower"> & {
|
|
75
|
+
nudged: boolean;
|
|
76
|
+
};
|
|
77
|
+
/** Linear sRGB-space hex mix (`t` toward `target`). Exported for tests. */
|
|
78
|
+
export declare function mixHex(base: string, target: string, t: number): string;
|
|
79
|
+
//# sourceMappingURL=tones.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tones.d.ts","sourceRoot":"","sources":["../src/tones.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,8EAA8E;AAC9E,MAAM,WAAW,QAAQ;IACvB,0CAA0C;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,oCAAoC;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,sCAAsC;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,oCAAoC;IACpC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,OAAO,GAAG,MAAM,EAAE,QAAQ,CAa5D,CAAC;AAEF,sEAAsE;AACtE,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAsBzE;AAED,yDAAyD;AACzD,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAInF;AAED,yCAAyC;AACzC,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAGpE;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,CAQpD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;CACb,GAAG,IAAI,CAAC,QAAQ,EAAE,OAAO,GAAG,OAAO,CAAC,GAAG;IAAE,MAAM,EAAE,OAAO,CAAA;CAAE,CAe1D;AAED,2EAA2E;AAC3E,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CActE"}
|
package/dist/tones.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tone math (guuey#403 §2) — the two-panel tone pair, its perceptibility
|
|
3
|
+
* floor, and the chat-palette derivation.
|
|
4
|
+
*
|
|
5
|
+
* The floor is CALIBRATED, not invented (ggui#633's scars): the console's
|
|
6
|
+
* first cut at ΔL* 3.5 measured below perception; their shipped pair
|
|
7
|
+
* (#F4F3ED warm paper vs #E4E4E2 neutral chrome) reads clearly at ΔL* 5.2
|
|
8
|
+
* because the warm→neutral TEMPERATURE step does perceptual work lightness
|
|
9
|
+
* alone doesn't. Hence the OR-form floor:
|
|
10
|
+
*
|
|
11
|
+
* ΔL* ≥ 6 — OR — a hue-temperature step (Δab ≥ 2) with ΔL* ≥ 5
|
|
12
|
+
*
|
|
13
|
+
* Both arms reject the failed 3.5 cut; the strict arm alone would have
|
|
14
|
+
* rejected the founder-certified shipped pair. The platform's theme-doc
|
|
15
|
+
* gate applies its own mirror of this rule server-side; this module is the
|
|
16
|
+
* lib's own honesty (derived defaults satisfy the floor BY CONSTRUCTION —
|
|
17
|
+
* pairs that land under it get the documented nudge apart, never a silent
|
|
18
|
+
* pass). Calibration may RAISE the floor; it does not lower it.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* The lib base defaults — the ggui#633 SHIPPED pair for light, and its
|
|
22
|
+
* temperature-step mirror for dark (warm umber menu vs neutral chrome,
|
|
23
|
+
* ΔL* 6.5 — the strict arm). Founder-certified "two-toned and
|
|
24
|
+
* well-distinguished" in light; the dark pair keeps the identical warm→
|
|
25
|
+
* neutral grammar so a mode flip changes brightness, not the layout's
|
|
26
|
+
* character. Both pairs assert the floor in this package's tests.
|
|
27
|
+
*/
|
|
28
|
+
export const DEFAULT_TONES = {
|
|
29
|
+
light: {
|
|
30
|
+
upper: "#F4F3ED",
|
|
31
|
+
upperOn: "#1F1E1B",
|
|
32
|
+
lower: "#E4E4E2",
|
|
33
|
+
lowerOn: "#1A1A1C",
|
|
34
|
+
},
|
|
35
|
+
dark: {
|
|
36
|
+
upper: "#302B24",
|
|
37
|
+
upperOn: "#ECE9E2",
|
|
38
|
+
lower: "#1E1E21",
|
|
39
|
+
lowerOn: "#E3E3E5",
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
/** CIELAB (D65) from a `#rrggbb` hex. Throws on a malformed color. */
|
|
43
|
+
export function hexToLab(hex) {
|
|
44
|
+
const m = /^#([0-9a-fA-F]{6})$/.exec(hex.trim());
|
|
45
|
+
if (m === null) {
|
|
46
|
+
throw new Error(`agent-layout tones: "${hex}" is not a #rrggbb color — tone math needs resolvable hex (CSS keywords and var() belong to the stylesheet tier, not the derivation door).`);
|
|
47
|
+
}
|
|
48
|
+
const h = m[1];
|
|
49
|
+
const chan = (i) => parseInt(h.slice(i, i + 2), 16) / 255;
|
|
50
|
+
const lin = (c) => (c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4));
|
|
51
|
+
const R = lin(chan(0));
|
|
52
|
+
const G = lin(chan(2));
|
|
53
|
+
const B = lin(chan(4));
|
|
54
|
+
const X = (0.4124 * R + 0.3576 * G + 0.1805 * B) / 0.95047;
|
|
55
|
+
const Y = 0.2126 * R + 0.7152 * G + 0.0722 * B;
|
|
56
|
+
const Z = (0.0193 * R + 0.1192 * G + 0.9505 * B) / 1.08883;
|
|
57
|
+
const f = (t) => (t > 0.008856 ? Math.cbrt(t) : 7.787 * t + 16 / 116);
|
|
58
|
+
return {
|
|
59
|
+
L: 116 * f(Y) - 16,
|
|
60
|
+
a: 500 * (f(X) - f(Y)),
|
|
61
|
+
b: 200 * (f(Y) - f(Z)),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** The perceptibility deltas between two backgrounds. */
|
|
65
|
+
export function toneDelta(upper, lower) {
|
|
66
|
+
const A = hexToLab(upper);
|
|
67
|
+
const B = hexToLab(lower);
|
|
68
|
+
return { dL: Math.abs(A.L - B.L), dab: Math.hypot(A.a - B.a, A.b - B.b) };
|
|
69
|
+
}
|
|
70
|
+
/** The OR-form floor (module header). */
|
|
71
|
+
export function meetsToneFloor(upper, lower) {
|
|
72
|
+
const { dL, dab } = toneDelta(upper, lower);
|
|
73
|
+
return dL >= 6 || (dab >= 2 && dL >= 5);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Reject-under-floor with the explanatory message (the brandAccent
|
|
77
|
+
* posture): overrides that fail perceptibility throw HERE, at wiring time,
|
|
78
|
+
* never render as an invisible seam.
|
|
79
|
+
*/
|
|
80
|
+
export function assertToneFloor(pair) {
|
|
81
|
+
if (meetsToneFloor(pair.upper, pair.lower))
|
|
82
|
+
return;
|
|
83
|
+
const { dL, dab } = toneDelta(pair.upper, pair.lower);
|
|
84
|
+
throw new Error(`agent-layout tones: the upper/lower pair ${pair.upper}/${pair.lower} is below the perceptibility floor ` +
|
|
85
|
+
`(ΔL* ${dL.toFixed(1)}, Δab ${dab.toFixed(1)}; needs ΔL* ≥ 6, or a temperature step Δab ≥ 2 with ΔL* ≥ 5). ` +
|
|
86
|
+
`Two tones that measure alike defeat the category's defining behavior — pick a stronger pair.`);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Base-tier derivation (§2, platform's pick): tones DERIVE from the chat
|
|
90
|
+
* palette's surface/canvas pair, so an already-themed app gets coherent
|
|
91
|
+
* tones for free — theming chat themes the layout, no second authoring
|
|
92
|
+
* step. `surface` grounds the AGENT (lower) panel — it is the chat's own
|
|
93
|
+
* ground — and `canvas` grounds the MENU (upper) panel. Pairs that land
|
|
94
|
+
* under the floor get the documented nudge: the upper tone walks away from
|
|
95
|
+
* the lower along the lightness axis (toward white below L* 50, toward
|
|
96
|
+
* black above — always INTO the pair's existing contrast direction) until
|
|
97
|
+
* the strict arm passes. Deterministic, bounded, and asserted by tests.
|
|
98
|
+
*/
|
|
99
|
+
export function deriveTones(palette) {
|
|
100
|
+
const lower = palette.surface;
|
|
101
|
+
let upper = palette.canvas;
|
|
102
|
+
let nudged = false;
|
|
103
|
+
if (!meetsToneFloor(upper, lower)) {
|
|
104
|
+
nudged = true;
|
|
105
|
+
const lowerL = hexToLab(lower).L;
|
|
106
|
+
// Walk upper away from lower in sRGB mix steps until ΔL* ≥ 6. The mix
|
|
107
|
+
// target keeps the walk inside the pair's own contrast direction.
|
|
108
|
+
const towardWhite = hexToLab(upper).L >= lowerL;
|
|
109
|
+
for (let i = 1; i <= 20 && !(toneDelta(upper, lower).dL >= 6); i++) {
|
|
110
|
+
upper = mixHex(palette.canvas, towardWhite ? "#FFFFFF" : "#000000", i * 0.05);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return { upper, lower, nudged };
|
|
114
|
+
}
|
|
115
|
+
/** Linear sRGB-space hex mix (`t` toward `target`). Exported for tests. */
|
|
116
|
+
export function mixHex(base, target, t) {
|
|
117
|
+
const pb = /^#([0-9a-fA-F]{6})$/.exec(base.trim());
|
|
118
|
+
const pt = /^#([0-9a-fA-F]{6})$/.exec(target.trim());
|
|
119
|
+
if (pb === null || pt === null)
|
|
120
|
+
throw new Error(`agent-layout tones: mixHex needs #rrggbb inputs`);
|
|
121
|
+
const out = [0, 2, 4]
|
|
122
|
+
.map((i) => {
|
|
123
|
+
const b = parseInt(pb[1].slice(i, i + 2), 16);
|
|
124
|
+
const g = parseInt(pt[1].slice(i, i + 2), 16);
|
|
125
|
+
return Math.round(b + (g - b) * Math.min(1, Math.max(0, t)))
|
|
126
|
+
.toString(16)
|
|
127
|
+
.padStart(2, "0");
|
|
128
|
+
})
|
|
129
|
+
.join("");
|
|
130
|
+
return `#${out.toUpperCase()}`;
|
|
131
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@guuey/agent-layout",
|
|
3
|
+
"version": "0.16.0",
|
|
4
|
+
"description": "Agent-mode layout: the two-tone shell where an app and its agent share one surface — the pane follows attention, the transport owns the stream.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/withguuey/guuey-sdks.git",
|
|
9
|
+
"directory": "packages/agent-layout"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./react": {
|
|
19
|
+
"types": "./dist/react.d.ts",
|
|
20
|
+
"import": "./dist/react.js",
|
|
21
|
+
"default": "./dist/react.js"
|
|
22
|
+
},
|
|
23
|
+
"./styles.css": "./styles.css"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"src",
|
|
28
|
+
"!src/**/*.test.ts",
|
|
29
|
+
"!src/**/*.test.tsx",
|
|
30
|
+
"styles.css",
|
|
31
|
+
"README.md"
|
|
32
|
+
],
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"react": ">=18"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@testing-library/dom": "^10.0.0",
|
|
38
|
+
"@testing-library/react": "^16.3.2",
|
|
39
|
+
"@types/node": "^24.0.0",
|
|
40
|
+
"@types/react": "^19.0.0",
|
|
41
|
+
"@types/react-dom": "^19.0.0",
|
|
42
|
+
"jsdom": "^25.0.1",
|
|
43
|
+
"react": "^19.0.0",
|
|
44
|
+
"react-dom": "^19.0.0",
|
|
45
|
+
"typescript": "^5.0.0",
|
|
46
|
+
"vitest": "^3.2.7"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsc -p tsconfig.build.json",
|
|
50
|
+
"dev": "tsc --watch",
|
|
51
|
+
"typecheck": "tsc --noEmit",
|
|
52
|
+
"test": "vitest run",
|
|
53
|
+
"test:watch": "vitest"
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/bind.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `@guuey/chat` bridge (guuey#403 §4) — pairs with the kit WITHOUT
|
|
3
|
+
* importing it. This package deliberately has NO dependency on
|
|
4
|
+
* `@guuey/chat` (the layout stays consumable by a surface that brings its
|
|
5
|
+
* own agent — ggui console's path calls `agentSubmit()` from
|
|
6
|
+
* `useAgentMode()` directly), so the kit's shapes are mirrored here
|
|
7
|
+
* STRUCTURALLY, the fs-contract discipline:
|
|
8
|
+
*
|
|
9
|
+
* - {@link GuueyChatActivityEventShape} mirrors `GuueyChatActivityEvent`
|
|
10
|
+
* (oss/packages/chat/src/react/guuey-chat.tsx — sync comment on the
|
|
11
|
+
* definition points back here);
|
|
12
|
+
* - {@link PlanViewSummaryShape} mirrors the two fields of
|
|
13
|
+
* `PlanViewSummary` this bridge reads (`key`, `origin`).
|
|
14
|
+
*
|
|
15
|
+
* The template's typecheck is the seam prover: it passes GuueyChat's real
|
|
16
|
+
* callbacks into these structural slots — a drift on either side breaks
|
|
17
|
+
* the template build, never silently.
|
|
18
|
+
*/
|
|
19
|
+
import type { AgentModeInput } from "./machine.js";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Structural mirror of `GuueyChatActivityEvent` (@guuey/chat). `submit`
|
|
23
|
+
* fires on every successful send — composer, `handle.send`, and the
|
|
24
|
+
* `ui/message` doorbell paths alike; `settled` fires when the invoke
|
|
25
|
+
* returns to `ready` (success, error, and abort all settle).
|
|
26
|
+
*/
|
|
27
|
+
export interface GuueyChatActivityEventShape {
|
|
28
|
+
type: "submit" | "settled";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The two roster fields the bridge reads (structural `PlanViewSummary`). */
|
|
32
|
+
export interface PlanViewSummaryShape {
|
|
33
|
+
key: string;
|
|
34
|
+
origin: "live" | "history";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** What the bridge needs from the layout side — `useAgentMode().dispatch`. */
|
|
38
|
+
export type AgentModeBinding = (input: AgentModeInput) => void;
|
|
39
|
+
|
|
40
|
+
/** The GuueyChat-shaped props {@link bindGuueyChat} returns. */
|
|
41
|
+
export interface GuueyChatBindingProps {
|
|
42
|
+
onActivity: (event: GuueyChatActivityEventShape) => void;
|
|
43
|
+
onViewsChange: (views: readonly PlanViewSummaryShape[]) => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Wire a GuueyChat surface into the active-panel machine: spread the
|
|
48
|
+
* returned props onto `<GuueyChat>` (or compose them inside your own
|
|
49
|
+
* handlers — each is a plain function).
|
|
50
|
+
*
|
|
51
|
+
* - `submit` → `agentSubmit` (tone flips ON SUBMIT — the §3 law);
|
|
52
|
+
* - `settled` → `agentSettled` (no tone transition; ends the working
|
|
53
|
+
* state and the streaming flag);
|
|
54
|
+
* - a roster change whose LIVE view count GREW → `agentViewMounted`
|
|
55
|
+
* (the founder's (d): content replaced the working state). Counting
|
|
56
|
+
* growth — not presence — keeps earlier turns' lingering live views
|
|
57
|
+
* and rehydrated history views from clearing a fresh submit's pending
|
|
58
|
+
* window spuriously.
|
|
59
|
+
*/
|
|
60
|
+
export function bindGuueyChat(dispatch: AgentModeBinding): GuueyChatBindingProps {
|
|
61
|
+
let liveCount = 0;
|
|
62
|
+
return {
|
|
63
|
+
onActivity: (event) => {
|
|
64
|
+
if (event.type === "submit") dispatch({ type: "agentSubmit" });
|
|
65
|
+
else dispatch({ type: "agentSettled" });
|
|
66
|
+
},
|
|
67
|
+
onViewsChange: (views) => {
|
|
68
|
+
const next = views.filter((v) => v.origin === "live").length;
|
|
69
|
+
if (next > liveCount) dispatch({ type: "agentViewMounted" });
|
|
70
|
+
liveCount = next;
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@guuey/agent-layout` — the agent-mode layout category (guuey#403).
|
|
3
|
+
*
|
|
4
|
+
* Root entry: the pure machine, the tone math, the token names, and the
|
|
5
|
+
* `@guuey/chat` bridge — everything React-free. Components live under
|
|
6
|
+
* `@guuey/agent-layout/react`; the stylesheet under
|
|
7
|
+
* `@guuey/agent-layout/styles.css`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
agentModeReduce,
|
|
12
|
+
INITIAL_AGENT_MODE_STATE,
|
|
13
|
+
type ActivePanel,
|
|
14
|
+
type AgentModeInput,
|
|
15
|
+
type AgentModeState,
|
|
16
|
+
} from "./machine.js";
|
|
17
|
+
export {
|
|
18
|
+
assertToneFloor,
|
|
19
|
+
DEFAULT_TONES,
|
|
20
|
+
deriveTones,
|
|
21
|
+
hexToLab,
|
|
22
|
+
meetsToneFloor,
|
|
23
|
+
mixHex,
|
|
24
|
+
toneDelta,
|
|
25
|
+
type TonePair,
|
|
26
|
+
} from "./tones.js";
|
|
27
|
+
export {
|
|
28
|
+
bindGuueyChat,
|
|
29
|
+
type AgentModeBinding,
|
|
30
|
+
type GuueyChatActivityEventShape,
|
|
31
|
+
type GuueyChatBindingProps,
|
|
32
|
+
type PlanViewSummaryShape,
|
|
33
|
+
} from "./bind.js";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The token vocabulary (§2) — one prefix, kebab-case, `-on` suffix keeps
|
|
37
|
+
* each tone's pair adjacent in sorted listings (platform-ruled naming).
|
|
38
|
+
* `--guuey-layout-pane-tone` is LIB-WRITTEN state: apps read it, never set
|
|
39
|
+
* it — overriding it silently breaks the category's defining behavior.
|
|
40
|
+
*/
|
|
41
|
+
export const LAYOUT_TOKENS = {
|
|
42
|
+
toneUpper: "--guuey-layout-tone-upper",
|
|
43
|
+
toneUpperOn: "--guuey-layout-tone-upper-on",
|
|
44
|
+
toneLower: "--guuey-layout-tone-lower",
|
|
45
|
+
toneLowerOn: "--guuey-layout-tone-lower-on",
|
|
46
|
+
paneTone: "--guuey-layout-pane-tone",
|
|
47
|
+
toneTransition: "--guuey-layout-tone-transition",
|
|
48
|
+
} as const;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The follow fade — founder-certified on the ggui#633 calibration: the
|
|
52
|
+
* eased ~150ms color fade ("arrives fast, lands soft" at panel scale).
|
|
53
|
+
*/
|
|
54
|
+
export const DEFAULT_TONE_TRANSITION_MS = 150;
|
package/src/machine.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The active-panel machine (guuey#403 §3) — pure data-in/data-out, no React.
|
|
3
|
+
*
|
|
4
|
+
* ONE law (platform's words): **layout follows attention; transport owns
|
|
5
|
+
* the stream.** The tone tracks the user's locus of action — submit flips
|
|
6
|
+
* it ON SUBMIT (not first token, not completion), navigation flips it back,
|
|
7
|
+
* and stream end changes nothing.
|
|
8
|
+
*
|
|
9
|
+
* State beyond the panel itself (the ggui#633 calibration additions):
|
|
10
|
+
*
|
|
11
|
+
* - `streaming` — a turn is in flight. Read by ONE rule only: closing an
|
|
12
|
+
* agent view returns to the menu UNLESS still streaming (the console's
|
|
13
|
+
* shipped behavior).
|
|
14
|
+
* - `pending` — the founder's (d) requirement: on flip to the agent tone
|
|
15
|
+
* with no view mounted yet, the pane must NEVER hold prior page content
|
|
16
|
+
* on the agent ground — it presents the working state immediately.
|
|
17
|
+
* `pending` arms on submit and clears on view-mount, on settle (the
|
|
18
|
+
* answer is in the log even when no view came), and on return-to-menu.
|
|
19
|
+
*
|
|
20
|
+
* The machine is consumed through `useAgentMode()` / the Provider; this
|
|
21
|
+
* module is exported for tests and for non-React hosts that want the exact
|
|
22
|
+
* transition table.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export type ActivePanel = "app" | "agent";
|
|
26
|
+
|
|
27
|
+
export interface AgentModeState {
|
|
28
|
+
activePanel: ActivePanel;
|
|
29
|
+
/** A turn is in flight (submit → settled). */
|
|
30
|
+
streaming: boolean;
|
|
31
|
+
/** Working-state window: submitted, nothing presented yet (founder (d)). */
|
|
32
|
+
pending: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const INITIAL_AGENT_MODE_STATE: AgentModeState = {
|
|
36
|
+
// A surface opens as the app; the agent earns the room by being addressed.
|
|
37
|
+
activePanel: "app",
|
|
38
|
+
streaming: false,
|
|
39
|
+
pending: false,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type AgentModeInput =
|
|
43
|
+
/** App-side chrome interaction (NavLink click, panel focus) OR a route change. */
|
|
44
|
+
| { type: "menuInteraction" }
|
|
45
|
+
/** The user sent a message — the agent bridge fired. */
|
|
46
|
+
| { type: "agentSubmit" }
|
|
47
|
+
/** The turn reached `ready`. NO panel transition (platform-ruled: no bounce-back). */
|
|
48
|
+
| { type: "agentSettled" }
|
|
49
|
+
/** A live view mounted — the working state has been replaced by content. */
|
|
50
|
+
| { type: "agentViewMounted" }
|
|
51
|
+
/** The user closed an agent view (canvas "back" affordance). */
|
|
52
|
+
| { type: "agentViewClosed" };
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The transition table (§3, verbatim rules). Pure; last input wins — the
|
|
56
|
+
* transition duration is the only debounce (no timers, no queue).
|
|
57
|
+
*/
|
|
58
|
+
export function agentModeReduce(state: AgentModeState, input: AgentModeInput): AgentModeState {
|
|
59
|
+
switch (input.type) {
|
|
60
|
+
case "menuInteraction":
|
|
61
|
+
// Re-follows the user WITHOUT touching the stream (mid-stream click
|
|
62
|
+
// included — interruption semantics live in transport, never here).
|
|
63
|
+
return { ...state, activePanel: "app", pending: false };
|
|
64
|
+
case "agentSubmit":
|
|
65
|
+
return { activePanel: "agent", streaming: true, pending: true };
|
|
66
|
+
case "agentSettled":
|
|
67
|
+
// Stream end changes nothing about the panel — the tone tracks the
|
|
68
|
+
// user's locus of action, not the agent's state.
|
|
69
|
+
return { ...state, streaming: false, pending: false };
|
|
70
|
+
case "agentViewMounted":
|
|
71
|
+
return { ...state, pending: false };
|
|
72
|
+
case "agentViewClosed":
|
|
73
|
+
// Closing a view returns to the menu UNLESS still streaming.
|
|
74
|
+
return state.streaming ? state : { ...state, activePanel: "app" };
|
|
75
|
+
}
|
|
76
|
+
}
|