@runtypelabs/persona 4.8.0 → 4.9.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/README.md +1 -0
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-BsZtXPKK.d.cts → types-4ROVJ1gA.d.cts} +42 -0
- package/dist/animations/{types-BsZtXPKK.d.ts → types-4ROVJ1gA.d.ts} +42 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/chunk-IO5VVUKP.js +3 -0
- package/dist/chunk-IPVK3KOM.js +1 -0
- package/dist/chunk-UPO4GUFC.js +1 -0
- package/dist/codegen.cjs +6 -6
- package/dist/codegen.js +8 -8
- package/dist/context-mentions-7S5KVUTG.js +169 -0
- package/dist/context-mentions-inline-TTCN7ZM2.js +4 -0
- package/dist/context-mentions-inline.cjs +4 -0
- package/dist/context-mentions-inline.d.cts +203 -0
- package/dist/context-mentions-inline.d.ts +203 -0
- package/dist/context-mentions-inline.js +4 -0
- package/dist/context-mentions.cjs +295 -0
- package/dist/context-mentions.d.cts +7025 -0
- package/dist/context-mentions.d.ts +7025 -0
- package/dist/context-mentions.js +295 -0
- package/dist/index.cjs +59 -51
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +622 -2
- package/dist/index.d.ts +622 -2
- package/dist/index.global.js +54 -46
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +59 -51
- package/dist/index.js.map +1 -1
- package/dist/launcher.global.js +2 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/plugin-kit.cjs +1 -1
- package/dist/plugin-kit.d.cts +17 -0
- package/dist/plugin-kit.d.ts +17 -0
- package/dist/plugin-kit.js +1 -1
- package/dist/smart-dom-reader.cjs +17 -16
- package/dist/smart-dom-reader.d.cts +507 -1
- package/dist/smart-dom-reader.d.ts +507 -1
- package/dist/smart-dom-reader.js +17 -16
- package/dist/theme-editor-preview.cjs +234 -55
- package/dist/theme-editor-preview.d.cts +473 -0
- package/dist/theme-editor-preview.d.ts +473 -0
- package/dist/theme-editor-preview.js +48 -42
- package/dist/theme-editor.cjs +7 -7
- package/dist/theme-editor.d.cts +473 -0
- package/dist/theme-editor.d.ts +473 -0
- package/dist/theme-editor.js +5 -5
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +2 -0
- package/dist/theme-reference.d.ts +2 -0
- package/dist/widget.css +1 -1
- package/package.json +15 -3
- package/src/client.test.ts +69 -0
- package/src/client.ts +65 -51
- package/src/components/composer-parts.ts +3 -12
- package/src/components/context-mention-button.test.ts +70 -0
- package/src/components/context-mention-button.ts +82 -0
- package/src/components/context-mention-chip.ts +134 -0
- package/src/components/context-mention-menu.test.ts +508 -0
- package/src/components/context-mention-menu.ts +0 -0
- package/src/components/message-bubble.test.ts +175 -0
- package/src/components/message-bubble.ts +177 -19
- package/src/components/panel.ts +7 -10
- package/src/context-mentions-bundle.test.ts +163 -0
- package/src/context-mentions-entry.ts +185 -0
- package/src/context-mentions-inline-entry.test.ts +136 -0
- package/src/context-mentions-inline-entry.ts +226 -0
- package/src/context-mentions-inline-loader.test.ts +30 -0
- package/src/context-mentions-inline-loader.ts +36 -0
- package/src/context-mentions-inline.ts +15 -0
- package/src/context-mentions-loader.ts +32 -0
- package/src/context-mentions.ts +16 -0
- package/src/index-core.ts +26 -0
- package/src/index-global.ts +51 -0
- package/src/markdown-parsers-loader.ts +35 -44
- package/src/plugin-kit.test.ts +40 -0
- package/src/plugin-kit.ts +39 -5
- package/src/session.mentions.test.ts +175 -0
- package/src/session.test.ts +52 -4
- package/src/session.ts +121 -5
- package/src/smart-dom-reader.test.ts +129 -2
- package/src/smart-dom-reader.ts +127 -1
- package/src/styles/context-mention-menu-css.ts +176 -0
- package/src/styles/widget.css +229 -126
- package/src/theme-editor/sections.ts +3 -3
- package/src/types/theme.ts +2 -0
- package/src/types.ts +525 -0
- package/src/ui.mention-submit.test.ts +235 -0
- package/src/ui.ts +301 -51
- package/src/utils/chunk-loader.test.ts +97 -0
- package/src/utils/chunk-loader.ts +88 -0
- package/src/utils/composer-contenteditable.test.ts +507 -0
- package/src/utils/composer-contenteditable.ts +626 -0
- package/src/utils/composer-document.test.ts +280 -0
- package/src/utils/composer-document.ts +293 -0
- package/src/utils/composer-history.test.ts +35 -7
- package/src/utils/composer-history.ts +30 -20
- package/src/utils/composer-input.ts +159 -0
- package/src/utils/context-mention-controller.test.ts +1215 -0
- package/src/utils/context-mention-controller.ts +1186 -0
- package/src/utils/context-mention-manager.test.ts +422 -0
- package/src/utils/context-mention-manager.ts +410 -0
- package/src/utils/context-mention-orchestrator.test.ts +538 -0
- package/src/utils/context-mention-orchestrator.ts +348 -0
- package/src/utils/live-region.test.ts +108 -0
- package/src/utils/live-region.ts +94 -0
- package/src/utils/mention-channels.ts +63 -0
- package/src/utils/mention-llm-format.test.ts +91 -0
- package/src/utils/mention-llm-format.ts +79 -0
- package/src/utils/mention-matcher.test.ts +86 -0
- package/src/utils/mention-matcher.ts +221 -0
- package/src/utils/mention-token.ts +72 -0
- package/src/utils/mention-trigger.test.ts +155 -0
- package/src/utils/mention-trigger.ts +156 -0
- package/src/utils/theme.test.ts +28 -4
- package/src/utils/tokens.ts +27 -11
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
parseMentionTrigger,
|
|
4
|
+
parseAnyTrigger,
|
|
5
|
+
isMenuOpeningInput,
|
|
6
|
+
stripMentionQuery,
|
|
7
|
+
MENTION_PLACEHOLDER,
|
|
8
|
+
} from "./mention-trigger";
|
|
9
|
+
|
|
10
|
+
describe("parseMentionTrigger", () => {
|
|
11
|
+
it("activates on @ at the start of input", () => {
|
|
12
|
+
const m = parseMentionTrigger("@App", 4);
|
|
13
|
+
expect(m).toEqual({ triggerIndex: 0, query: "App" });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("activates on @ after whitespace mid-sentence", () => {
|
|
17
|
+
const m = parseMentionTrigger("check @fo", 9);
|
|
18
|
+
expect(m).toEqual({ triggerIndex: 6, query: "fo" });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("returns an empty query right after a bare @", () => {
|
|
22
|
+
const m = parseMentionTrigger("hi @", 4);
|
|
23
|
+
expect(m).toEqual({ triggerIndex: 3, query: "" });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("does NOT activate for an email (@ glued to a word char)", () => {
|
|
27
|
+
expect(parseMentionTrigger("user@example.com", 16)).toBeNull();
|
|
28
|
+
expect(parseMentionTrigger("user@", 5)).toBeNull();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("closes once a space follows the trigger", () => {
|
|
32
|
+
// caret after "fo bar" — whitespace between query and caret ends the mention
|
|
33
|
+
expect(parseMentionTrigger("@fo bar", 7)).toBeNull();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("tracks the nearest active trigger when caret is mid-query", () => {
|
|
37
|
+
const m = parseMentionTrigger("a @one @two", 11);
|
|
38
|
+
expect(m).toEqual({ triggerIndex: 7, query: "two" });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("returns null at caret 0 or out of range", () => {
|
|
42
|
+
expect(parseMentionTrigger("@x", 0)).toBeNull();
|
|
43
|
+
expect(parseMentionTrigger("@x", 99)).toBeNull();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("honors a custom trigger character", () => {
|
|
47
|
+
expect(parseMentionTrigger("see #iss", 8, "#")).toEqual({
|
|
48
|
+
triggerIndex: 4,
|
|
49
|
+
query: "iss",
|
|
50
|
+
});
|
|
51
|
+
expect(parseMentionTrigger("see #iss", 8, "@")).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("handles multi-byte characters in the query", () => {
|
|
55
|
+
const m = parseMentionTrigger("@café", 5);
|
|
56
|
+
expect(m).toEqual({ triggerIndex: 0, query: "café" });
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("exports MENTION_PLACEHOLDER as the U+FFFC object-replacement char", () => {
|
|
60
|
+
expect(MENTION_PLACEHOLDER).toBe("");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("treats U+FFFC (inline mention token placeholder) as a query terminator", () => {
|
|
64
|
+
// In inline-mode logical text an existing token is a single `` char. A new
|
|
65
|
+
// `@query` must not scan back THROUGH that token: "@ab" with caret after "b"
|
|
66
|
+
// has no active trigger (the `` ends the backscan like whitespace/newline).
|
|
67
|
+
expect(parseMentionTrigger(`@a${MENTION_PLACEHOLDER}b`, 4)).toBeNull();
|
|
68
|
+
// A fresh `@` typed AFTER a token still activates (scan stops at ``, not the @).
|
|
69
|
+
expect(parseMentionTrigger(" @fo", 5)).toEqual({
|
|
70
|
+
triggerIndex: 2,
|
|
71
|
+
query: "fo",
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("position gating (for / slash-commands)", () => {
|
|
76
|
+
it("line-start: activates at input start and after a newline", () => {
|
|
77
|
+
expect(parseMentionTrigger("/dep", 4, "/", "line-start")).toEqual({
|
|
78
|
+
triggerIndex: 0,
|
|
79
|
+
query: "dep",
|
|
80
|
+
});
|
|
81
|
+
expect(parseMentionTrigger("hi\n/dep", 7, "/", "line-start")).toEqual({
|
|
82
|
+
triggerIndex: 3,
|
|
83
|
+
query: "dep",
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("line-start: does NOT activate mid-line (after a space)", () => {
|
|
88
|
+
expect(parseMentionTrigger("hi /dep", 7, "/", "line-start")).toBeNull();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("input-start: only at index 0", () => {
|
|
92
|
+
expect(parseMentionTrigger("/x", 2, "/", "input-start")).toEqual({
|
|
93
|
+
triggerIndex: 0,
|
|
94
|
+
query: "x",
|
|
95
|
+
});
|
|
96
|
+
expect(parseMentionTrigger("hi\n/x", 5, "/", "input-start")).toBeNull();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("parseAnyTrigger", () => {
|
|
102
|
+
const channels = [
|
|
103
|
+
{ trigger: "@", position: "anywhere" as const },
|
|
104
|
+
{ trigger: "/", position: "line-start" as const },
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
it("picks the @ channel for a mid-sentence mention", () => {
|
|
108
|
+
const hit = parseAnyTrigger("hey @fo", 7, channels);
|
|
109
|
+
expect(hit?.channel.trigger).toBe("@");
|
|
110
|
+
expect(hit?.match).toEqual({ triggerIndex: 4, query: "fo" });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("picks the / channel only at line-start", () => {
|
|
114
|
+
const hit = parseAnyTrigger("/dep", 4, channels);
|
|
115
|
+
expect(hit?.channel.trigger).toBe("/");
|
|
116
|
+
expect(hit?.match).toEqual({ triggerIndex: 0, query: "dep" });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("does not match / mid-line even though @ is anywhere", () => {
|
|
120
|
+
expect(parseAnyTrigger("hi /dep", 7, channels)).toBeNull();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("returns null when no channel is active", () => {
|
|
124
|
+
expect(parseAnyTrigger("plain text", 10, channels)).toBeNull();
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("isMenuOpeningInput", () => {
|
|
129
|
+
it("opens on typed text and unknown input types", () => {
|
|
130
|
+
expect(isMenuOpeningInput("insertText")).toBe(true);
|
|
131
|
+
expect(isMenuOpeningInput(undefined)).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("does not open on paste or drop", () => {
|
|
135
|
+
expect(isMenuOpeningInput("insertFromPaste")).toBe(false);
|
|
136
|
+
expect(isMenuOpeningInput("insertFromDrop")).toBe(false);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe("stripMentionQuery", () => {
|
|
141
|
+
it("removes the @query span and moves the caret to the trigger", () => {
|
|
142
|
+
const value = "check @App for bugs";
|
|
143
|
+
const match = parseMentionTrigger("check @App", 10)!;
|
|
144
|
+
// caret sits right after "@App" (index 10)
|
|
145
|
+
const out = stripMentionQuery(value, match, 10);
|
|
146
|
+
expect(out.value).toBe("check for bugs");
|
|
147
|
+
expect(out.caret).toBe(6);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("strips a bare trigger with empty query", () => {
|
|
151
|
+
const out = stripMentionQuery("hi @", { triggerIndex: 3, query: "" }, 4);
|
|
152
|
+
expect(out.value).toBe("hi ");
|
|
153
|
+
expect(out.caret).toBe(3);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure `@`-trigger detection for the composer mention menu.
|
|
3
|
+
*
|
|
4
|
+
* No DOM, no contenteditable — a small function over `(value, caret)` mirroring
|
|
5
|
+
* the Slack/Discord approach. Shared by the core orchestrator's lightweight
|
|
6
|
+
* "should I open?" pre-check and the lazy-loaded controller's full parse, so the
|
|
7
|
+
* trigger rule lives in exactly one place.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type MentionTriggerMatch = {
|
|
11
|
+
/** Index of the trigger character in `value`. */
|
|
12
|
+
triggerIndex: number;
|
|
13
|
+
/** The text typed after the trigger, up to the caret (no leading trigger). */
|
|
14
|
+
query: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Where a trigger may open. `"anywhere"` is the `@` rule (after any whitespace/
|
|
19
|
+
* start); `"line-start"` and `"input-start"` are the natural rules for `/`
|
|
20
|
+
* slash-commands. Kept as a local string union so the parser stays free of the
|
|
21
|
+
* public config types.
|
|
22
|
+
*/
|
|
23
|
+
export type MentionTriggerPosition = "anywhere" | "line-start" | "input-start";
|
|
24
|
+
|
|
25
|
+
/** One trigger channel for `parseAnyTrigger` — a char plus its position rule. */
|
|
26
|
+
export type MentionTriggerSpec = {
|
|
27
|
+
trigger: string;
|
|
28
|
+
position?: MentionTriggerPosition;
|
|
29
|
+
/**
|
|
30
|
+
* Allow the query to span spaces/tabs (a newline still ends it). Needed for
|
|
31
|
+
* slash-command ARGS (`/deploy staging` → query `"deploy staging"`); leave
|
|
32
|
+
* false for single-token `@` mentions (a space ends the mention).
|
|
33
|
+
*/
|
|
34
|
+
allowSpaces?: boolean;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const WHITESPACE = /\s/;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* OBJECT REPLACEMENT CHAR (U+FFFC) — one inline mention token in the composer's
|
|
41
|
+
* *logical* text (see `toLogicalText` in composer-document.ts). Defined HERE, in
|
|
42
|
+
* the dependency-free trigger module, and re-exported from composer-document.ts so
|
|
43
|
+
* the constant has a single source without dragging the document-model runtime
|
|
44
|
+
* into the chip chunk (which imports this module but not composer-document).
|
|
45
|
+
*/
|
|
46
|
+
export const MENTION_PLACEHOLDER = "";
|
|
47
|
+
|
|
48
|
+
/** Does `triggerIndex` satisfy the channel's position rule? */
|
|
49
|
+
function positionAllowed(
|
|
50
|
+
value: string,
|
|
51
|
+
triggerIndex: number,
|
|
52
|
+
position: MentionTriggerPosition
|
|
53
|
+
): boolean {
|
|
54
|
+
if (position === "input-start") return triggerIndex === 0;
|
|
55
|
+
const before = triggerIndex > 0 ? value[triggerIndex - 1] : "";
|
|
56
|
+
if (position === "line-start") return before === "" || before === "\n";
|
|
57
|
+
// "anywhere": preceded by whitespace or start (also excludes `user@example.com`).
|
|
58
|
+
return before === "" || WHITESPACE.test(before);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Detect an active mention trigger ending at `caret`.
|
|
63
|
+
*
|
|
64
|
+
* Active when, scanning back from the caret, a trigger char is reached with no
|
|
65
|
+
* intervening whitespace AND the trigger satisfies `position`. Returns `null`
|
|
66
|
+
* otherwise — notably for `user@example.com` (trigger glued to a word char) and
|
|
67
|
+
* once a space follows the trigger.
|
|
68
|
+
*
|
|
69
|
+
* @param value Full textarea value.
|
|
70
|
+
* @param caret Caret offset (selectionStart). Out-of-range → `null`.
|
|
71
|
+
* @param trigger Single trigger character. @default "@"
|
|
72
|
+
* @param position Where the trigger may open. @default "anywhere"
|
|
73
|
+
* @param allowSpaces Let the query span spaces/tabs (for command args). @default false
|
|
74
|
+
*/
|
|
75
|
+
export function parseMentionTrigger(
|
|
76
|
+
value: string,
|
|
77
|
+
caret: number,
|
|
78
|
+
trigger = "@",
|
|
79
|
+
position: MentionTriggerPosition = "anywhere",
|
|
80
|
+
allowSpaces = false
|
|
81
|
+
): MentionTriggerMatch | null {
|
|
82
|
+
if (!trigger) return null;
|
|
83
|
+
if (caret <= 0 || caret > value.length) return null;
|
|
84
|
+
|
|
85
|
+
let i = caret - 1;
|
|
86
|
+
while (i >= 0) {
|
|
87
|
+
const ch = value[i];
|
|
88
|
+
if (ch === trigger) {
|
|
89
|
+
if (positionAllowed(value, i, position)) {
|
|
90
|
+
return { triggerIndex: i, query: value.slice(i + 1, caret) };
|
|
91
|
+
}
|
|
92
|
+
// Trigger present but disallowed here (glued word char / not line-start).
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
// A newline always ends the query. Spaces/tabs end it too, unless the
|
|
96
|
+
// channel allows multi-word queries (slash-command args).
|
|
97
|
+
if (ch === "\n") return null;
|
|
98
|
+
// The mention-token placeholder always terminates the backscan so a query
|
|
99
|
+
// never spans an existing token (`@a<token>b` must not report a query through
|
|
100
|
+
// the token). Never appears in a plain textarea value, so chip mode is
|
|
101
|
+
// unaffected.
|
|
102
|
+
if (ch === MENTION_PLACEHOLDER) return null;
|
|
103
|
+
if (!allowSpaces && WHITESPACE.test(ch)) return null;
|
|
104
|
+
i--;
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Resolve which of several trigger channels is active at `caret`. Channels are
|
|
111
|
+
* tested in order; the FIRST with an active match wins (put `@` first, `/`
|
|
112
|
+
* next). At any caret at most one channel can match — the scan stops at the
|
|
113
|
+
* nearest trigger/whitespace — so ordering only matters if two channels share a
|
|
114
|
+
* trigger char (don't do that). Returns the winning channel + its match.
|
|
115
|
+
*/
|
|
116
|
+
export function parseAnyTrigger<T extends MentionTriggerSpec>(
|
|
117
|
+
value: string,
|
|
118
|
+
caret: number,
|
|
119
|
+
channels: readonly T[]
|
|
120
|
+
): { channel: T; match: MentionTriggerMatch } | null {
|
|
121
|
+
for (const channel of channels) {
|
|
122
|
+
const match = parseMentionTrigger(
|
|
123
|
+
value,
|
|
124
|
+
caret,
|
|
125
|
+
channel.trigger,
|
|
126
|
+
channel.position ?? "anywhere",
|
|
127
|
+
channel.allowSpaces ?? false
|
|
128
|
+
);
|
|
129
|
+
if (match) return { channel, match };
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Whether a given `inputType` (from an `InputEvent`) is one that may OPEN the
|
|
136
|
+
* menu. Paste never opens it (`insertFromPaste`); only real typing does. The
|
|
137
|
+
* caller separately guards `event.isComposing` for IME safety.
|
|
138
|
+
*/
|
|
139
|
+
export function isMenuOpeningInput(inputType: string | undefined): boolean {
|
|
140
|
+
if (!inputType) return true; // older browsers omit inputType; treat as typing
|
|
141
|
+
return inputType !== "insertFromPaste" && inputType !== "insertFromDrop";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Remove the `@query` span (trigger char through the caret) from `value`,
|
|
146
|
+
* returning the new value + the caret position after removal. Used on select to
|
|
147
|
+
* strip the typed query while leaving the rest of the prose intact.
|
|
148
|
+
*/
|
|
149
|
+
export function stripMentionQuery(
|
|
150
|
+
value: string,
|
|
151
|
+
match: MentionTriggerMatch,
|
|
152
|
+
caret: number
|
|
153
|
+
): { value: string; caret: number } {
|
|
154
|
+
const next = value.slice(0, match.triggerIndex) + value.slice(caret);
|
|
155
|
+
return { value: next, caret: match.triggerIndex };
|
|
156
|
+
}
|
package/src/utils/theme.test.ts
CHANGED
|
@@ -169,6 +169,31 @@ describe('theme utils', () => {
|
|
|
169
169
|
expect(customVars['--persona-header-action-icon-fg']).toBe('#9ca3af');
|
|
170
170
|
});
|
|
171
171
|
|
|
172
|
+
it('maps button.ghost tokens to the composer ghost icon-button CSS variables', () => {
|
|
173
|
+
const cssVars = themeToCssVariables(createTheme());
|
|
174
|
+
|
|
175
|
+
// The composer's transparent icon buttons (.persona-mention-button /
|
|
176
|
+
// .persona-attachment-button) read these instead of hardcoded inline styles.
|
|
177
|
+
expect(cssVars['--persona-button-ghost-bg']).toBe('transparent');
|
|
178
|
+
expect(cssVars['--persona-button-ghost-fg']).toBe('#111827'); // semantic.text → gray.900
|
|
179
|
+
expect(cssVars['--persona-button-ghost-radius']).toBe('0.375rem'); // radius.md
|
|
180
|
+
expect(cssVars['--persona-button-ghost-hover-bg']).toBe('rgba(0, 0, 0, 0.05)');
|
|
181
|
+
|
|
182
|
+
const custom = createTheme({
|
|
183
|
+
components: {
|
|
184
|
+
button: {
|
|
185
|
+
ghost: {
|
|
186
|
+
foreground: 'palette.colors.accent.500',
|
|
187
|
+
hoverBackground: 'palette.colors.gray.100',
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
} as any);
|
|
192
|
+
const customVars = themeToCssVariables(custom);
|
|
193
|
+
expect(customVars['--persona-button-ghost-fg']).toBe('#06b6d4'); // accent.500
|
|
194
|
+
expect(customVars['--persona-button-ghost-hover-bg']).toBe('#f3f4f6'); // gray.100
|
|
195
|
+
});
|
|
196
|
+
|
|
172
197
|
it('defaults artifact pane fill from semantic container and resolves toolbar background token refs', () => {
|
|
173
198
|
const theme = createTheme();
|
|
174
199
|
const cssVars = themeToCssVariables(theme);
|
|
@@ -312,12 +337,11 @@ describe('theme utils', () => {
|
|
|
312
337
|
);
|
|
313
338
|
});
|
|
314
339
|
|
|
315
|
-
it('
|
|
340
|
+
it('defaults the intro card to flat (transparent, no shadow) when no token is set', () => {
|
|
316
341
|
const theme = createTheme({});
|
|
317
342
|
const cssVars = themeToCssVariables(theme);
|
|
318
|
-
expect(cssVars['--persona-intro-card-shadow']).toBe(
|
|
319
|
-
|
|
320
|
-
);
|
|
343
|
+
expect(cssVars['--persona-intro-card-shadow']).toBe('none');
|
|
344
|
+
expect(cssVars['--persona-intro-card-bg']).toBe('transparent');
|
|
321
345
|
});
|
|
322
346
|
|
|
323
347
|
it('drives --persona-tool-bubble-shadow from the theme token (config.toolCall.shadow is applied inline on the bubble, not the root var)', () => {
|
package/src/utils/tokens.ts
CHANGED
|
@@ -263,6 +263,9 @@ export const DEFAULT_COMPONENTS: ComponentTokens = {
|
|
|
263
263
|
foreground: 'semantic.colors.text',
|
|
264
264
|
borderRadius: 'palette.radius.md',
|
|
265
265
|
padding: 'semantic.spacing.sm',
|
|
266
|
+
// Subtle neutral tint on hover — the composer's transparent icon buttons
|
|
267
|
+
// (attachment, mention) read this via `--persona-button-ghost-hover-bg`.
|
|
268
|
+
hoverBackground: 'rgba(0, 0, 0, 0.05)',
|
|
266
269
|
},
|
|
267
270
|
},
|
|
268
271
|
input: {
|
|
@@ -326,12 +329,12 @@ export const DEFAULT_COMPONENTS: ComponentTokens = {
|
|
|
326
329
|
border: 'semantic.colors.border',
|
|
327
330
|
},
|
|
328
331
|
introCard: {
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
background: '
|
|
332
|
+
// Flat by default: the greeting reads as plain text on the transcript
|
|
333
|
+
// background (the industry norm), not an elevated card.
|
|
334
|
+
background: 'transparent',
|
|
332
335
|
borderRadius: 'palette.radius.2xl',
|
|
333
336
|
padding: 'semantic.spacing.lg',
|
|
334
|
-
shadow: '
|
|
337
|
+
shadow: 'palette.shadows.none',
|
|
335
338
|
},
|
|
336
339
|
toolBubble: {
|
|
337
340
|
shadow: 'palette.shadows.sm',
|
|
@@ -739,6 +742,21 @@ export function themeToCssVariables(theme: PersonaTheme): Record<string, string>
|
|
|
739
742
|
cssVars['--persona-components-button-primary-borderRadius'] ??
|
|
740
743
|
cssVars['--persona-palette-radius-full'] ??
|
|
741
744
|
'9999px';
|
|
745
|
+
// Ghost variant: transparent, neutral-foreground icon buttons (the composer's
|
|
746
|
+
// attachment + mention affordances). Wired to `components.button.ghost.*`.
|
|
747
|
+
cssVars['--persona-button-ghost-bg'] =
|
|
748
|
+
cssVars['--persona-components-button-ghost-background'] ??
|
|
749
|
+
'transparent';
|
|
750
|
+
cssVars['--persona-button-ghost-fg'] =
|
|
751
|
+
cssVars['--persona-components-button-ghost-foreground'] ??
|
|
752
|
+
cssVars['--persona-text'];
|
|
753
|
+
cssVars['--persona-button-ghost-radius'] =
|
|
754
|
+
cssVars['--persona-components-button-ghost-borderRadius'] ??
|
|
755
|
+
cssVars['--persona-radius-md'] ??
|
|
756
|
+
'0.375rem';
|
|
757
|
+
cssVars['--persona-button-ghost-hover-bg'] =
|
|
758
|
+
cssVars['--persona-components-button-ghost-hoverBackground'] ??
|
|
759
|
+
'rgba(0, 0, 0, 0.05)';
|
|
742
760
|
cssVars['--persona-panel-radius'] =
|
|
743
761
|
cssVars['--persona-components-panel-borderRadius'] ??
|
|
744
762
|
cssVars['--persona-radius-xl'] ??
|
|
@@ -791,19 +809,17 @@ export function themeToCssVariables(theme: PersonaTheme): Record<string, string>
|
|
|
791
809
|
if (headerTokens?.borderBottom) cssVars['--persona-header-border-bottom'] = headerTokens.borderBottom;
|
|
792
810
|
|
|
793
811
|
// Intro card aliases: short names the panel inline-styles read directly.
|
|
794
|
-
// The full-path `--persona-components-introCard-*` variables auto-emit above
|
|
795
|
-
//
|
|
796
|
-
|
|
812
|
+
// The full-path `--persona-components-introCard-*` variables auto-emit above.
|
|
813
|
+
// Default is flat (transparent, no shadow): the greeting renders as plain
|
|
814
|
+
// text on the transcript background; set introCard tokens for a card look.
|
|
797
815
|
cssVars['--persona-intro-card-bg'] =
|
|
798
|
-
cssVars['--persona-components-introCard-background'] ??
|
|
816
|
+
cssVars['--persona-components-introCard-background'] ?? 'transparent';
|
|
799
817
|
cssVars['--persona-intro-card-radius'] =
|
|
800
818
|
cssVars['--persona-components-introCard-borderRadius'] ?? '1rem';
|
|
801
819
|
cssVars['--persona-intro-card-padding'] =
|
|
802
820
|
cssVars['--persona-components-introCard-padding'] ?? '1.5rem';
|
|
803
821
|
cssVars['--persona-intro-card-shadow'] =
|
|
804
|
-
|
|
805
|
-
?? cssVars['--persona-components-introCard-shadow']
|
|
806
|
-
?? '0 5px 15px rgba(15, 23, 42, 0.08)';
|
|
822
|
+
cssVars['--persona-components-introCard-shadow'] ?? 'none';
|
|
807
823
|
|
|
808
824
|
cssVars['--persona-input-background'] =
|
|
809
825
|
cssVars['--persona-components-input-background'] ?? cssVars['--persona-surface'];
|