@xenide-io/the-old-ui-theme 0.9.4 → 0.9.6
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/ai-message-markdown-AUEMCO7K.mjs +65 -0
- package/dist/suite.js +68 -13
- package/dist/suite.mjs +24 -12
- package/package.json +1 -1
- package/src/styles/suite-skin.css +158 -0
- package/src/suite/components/ai-message-markdown.test.tsx +40 -0
- package/src/suite/components/ai-message-markdown.tsx +75 -1
- package/src/suite/components/ai-panel.tsx +20 -10
- package/src/suite/components/suite-sidebar.tsx +3 -1
- package/dist/ai-message-markdown-TSPYON2F.mjs +0 -22
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import "./chunk-FWCSY2DS.mjs";
|
|
3
|
+
|
|
4
|
+
// src/suite/components/ai-message-markdown.tsx
|
|
5
|
+
import ReactMarkdown from "react-markdown";
|
|
6
|
+
import remarkGfm from "remark-gfm";
|
|
7
|
+
import { jsx } from "react/jsx-runtime";
|
|
8
|
+
var WORD_STEP_MS = 18;
|
|
9
|
+
var MAX_DELAY_MS = 600;
|
|
10
|
+
var SKIP_TAGS = /* @__PURE__ */ new Set(["code", "pre", "style", "script"]);
|
|
11
|
+
function rehypeStaggerWords() {
|
|
12
|
+
return (tree) => {
|
|
13
|
+
let wordIndex = 0;
|
|
14
|
+
const visit = (node) => {
|
|
15
|
+
if (!node.children) return;
|
|
16
|
+
if (node.tagName && SKIP_TAGS.has(node.tagName)) return;
|
|
17
|
+
const next = [];
|
|
18
|
+
for (const child of node.children) {
|
|
19
|
+
const isText = child.type === "text" && typeof child.value === "string" && /\S/.test(child.value);
|
|
20
|
+
if (!isText) {
|
|
21
|
+
visit(child);
|
|
22
|
+
next.push(child);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
for (const part of child.value.split(/(\s+)/)) {
|
|
26
|
+
if (!part) continue;
|
|
27
|
+
if (/^\s+$/.test(part)) {
|
|
28
|
+
next.push({ type: "text", value: part });
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const delay = Math.min(wordIndex * WORD_STEP_MS, MAX_DELAY_MS);
|
|
32
|
+
wordIndex += 1;
|
|
33
|
+
next.push({
|
|
34
|
+
type: "element",
|
|
35
|
+
tagName: "span",
|
|
36
|
+
properties: {
|
|
37
|
+
className: ["suite-word-in"],
|
|
38
|
+
style: `animation-delay:${delay}ms`
|
|
39
|
+
},
|
|
40
|
+
children: [{ type: "text", value: part }]
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
node.children = next;
|
|
45
|
+
};
|
|
46
|
+
visit(tree);
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function SuiteAiMarkdownMessage({
|
|
50
|
+
markdown,
|
|
51
|
+
animate = false
|
|
52
|
+
}) {
|
|
53
|
+
return /* @__PURE__ */ jsx("div", { className: "suite-ai-markdown", children: /* @__PURE__ */ jsx(
|
|
54
|
+
ReactMarkdown,
|
|
55
|
+
{
|
|
56
|
+
remarkPlugins: [remarkGfm],
|
|
57
|
+
rehypePlugins: animate ? [rehypeStaggerWords] : void 0,
|
|
58
|
+
components: { img: () => null },
|
|
59
|
+
children: markdown
|
|
60
|
+
}
|
|
61
|
+
) });
|
|
62
|
+
}
|
|
63
|
+
export {
|
|
64
|
+
SuiteAiMarkdownMessage as default
|
|
65
|
+
};
|
package/dist/suite.js
CHANGED
|
@@ -64,19 +64,59 @@ var ai_message_markdown_exports = {};
|
|
|
64
64
|
__export(ai_message_markdown_exports, {
|
|
65
65
|
default: () => SuiteAiMarkdownMessage
|
|
66
66
|
});
|
|
67
|
+
function rehypeStaggerWords() {
|
|
68
|
+
return (tree) => {
|
|
69
|
+
let wordIndex = 0;
|
|
70
|
+
const visit = (node) => {
|
|
71
|
+
if (!node.children) return;
|
|
72
|
+
if (node.tagName && SKIP_TAGS.has(node.tagName)) return;
|
|
73
|
+
const next = [];
|
|
74
|
+
for (const child of node.children) {
|
|
75
|
+
const isText = child.type === "text" && typeof child.value === "string" && /\S/.test(child.value);
|
|
76
|
+
if (!isText) {
|
|
77
|
+
visit(child);
|
|
78
|
+
next.push(child);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
for (const part of child.value.split(/(\s+)/)) {
|
|
82
|
+
if (!part) continue;
|
|
83
|
+
if (/^\s+$/.test(part)) {
|
|
84
|
+
next.push({ type: "text", value: part });
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const delay = Math.min(wordIndex * WORD_STEP_MS, MAX_DELAY_MS);
|
|
88
|
+
wordIndex += 1;
|
|
89
|
+
next.push({
|
|
90
|
+
type: "element",
|
|
91
|
+
tagName: "span",
|
|
92
|
+
properties: {
|
|
93
|
+
className: ["suite-word-in"],
|
|
94
|
+
style: `animation-delay:${delay}ms`
|
|
95
|
+
},
|
|
96
|
+
children: [{ type: "text", value: part }]
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
node.children = next;
|
|
101
|
+
};
|
|
102
|
+
visit(tree);
|
|
103
|
+
};
|
|
104
|
+
}
|
|
67
105
|
function SuiteAiMarkdownMessage({
|
|
68
|
-
markdown
|
|
106
|
+
markdown,
|
|
107
|
+
animate = false
|
|
69
108
|
}) {
|
|
70
109
|
return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { className: "suite-ai-markdown", children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
|
|
71
110
|
import_react_markdown.default,
|
|
72
111
|
{
|
|
73
112
|
remarkPlugins: [import_remark_gfm.default],
|
|
113
|
+
rehypePlugins: animate ? [rehypeStaggerWords] : void 0,
|
|
74
114
|
components: { img: () => null },
|
|
75
115
|
children: markdown
|
|
76
116
|
}
|
|
77
117
|
) });
|
|
78
118
|
}
|
|
79
|
-
var import_react_markdown, import_remark_gfm, import_jsx_runtime23;
|
|
119
|
+
var import_react_markdown, import_remark_gfm, import_jsx_runtime23, WORD_STEP_MS, MAX_DELAY_MS, SKIP_TAGS;
|
|
80
120
|
var init_ai_message_markdown = __esm({
|
|
81
121
|
"src/suite/components/ai-message-markdown.tsx"() {
|
|
82
122
|
"use strict";
|
|
@@ -84,6 +124,9 @@ var init_ai_message_markdown = __esm({
|
|
|
84
124
|
import_react_markdown = __toESM(require("react-markdown"));
|
|
85
125
|
import_remark_gfm = __toESM(require("remark-gfm"));
|
|
86
126
|
import_jsx_runtime23 = require("react/jsx-runtime");
|
|
127
|
+
WORD_STEP_MS = 18;
|
|
128
|
+
MAX_DELAY_MS = 600;
|
|
129
|
+
SKIP_TAGS = /* @__PURE__ */ new Set(["code", "pre", "style", "script"]);
|
|
87
130
|
}
|
|
88
131
|
});
|
|
89
132
|
|
|
@@ -3665,6 +3708,10 @@ function SuiteAiPanel({
|
|
|
3665
3708
|
else window.location.assign(new URL(openPath, href).toString());
|
|
3666
3709
|
}
|
|
3667
3710
|
if (!open) return null;
|
|
3711
|
+
const lastAssistantIndex = messages.reduce(
|
|
3712
|
+
(found, message, index) => message.role === "assistant" ? index : found,
|
|
3713
|
+
-1
|
|
3714
|
+
);
|
|
3668
3715
|
const Icon = BrandIcon != null ? BrandIcon : import_iconoir_react8.Sparks;
|
|
3669
3716
|
return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
|
|
3670
3717
|
"aside",
|
|
@@ -3733,7 +3780,7 @@ function SuiteAiPanel({
|
|
|
3733
3780
|
return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
3734
3781
|
"div",
|
|
3735
3782
|
{
|
|
3736
|
-
className: "flex justify-end",
|
|
3783
|
+
className: "suite-msg-in suite-msg-in--user flex justify-end",
|
|
3737
3784
|
"data-test": "ask-ai-user",
|
|
3738
3785
|
children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { className: "w-fit max-w-[85%] whitespace-pre-wrap break-words rounded-2xl rounded-br-md bg-ph-brand px-3 py-2 text-sm leading-relaxed text-[var(--ph-on-accent)] [overflow-wrap:anywhere]", children: message.content })
|
|
3739
3786
|
},
|
|
@@ -3749,12 +3796,12 @@ function SuiteAiPanel({
|
|
|
3749
3796
|
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
3750
3797
|
"span",
|
|
3751
3798
|
{
|
|
3752
|
-
className: "mb-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-ph-brand/10 text-ph-brand",
|
|
3799
|
+
className: "suite-msg-avatar mb-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-ph-brand/10 text-ph-brand",
|
|
3753
3800
|
"aria-hidden": true,
|
|
3754
3801
|
children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(Icon, { className: "h-3.5 w-3.5" })
|
|
3755
3802
|
}
|
|
3756
3803
|
),
|
|
3757
|
-
/* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "min-w-0 max-w-[85%] space-y-2", children: [
|
|
3804
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "suite-msg-bubble min-w-0 max-w-[85%] space-y-2", children: [
|
|
3758
3805
|
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { className: "rounded-2xl rounded-bl-md bg-ph-muted px-3 py-2 text-sm leading-relaxed text-ph-ink", children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
3759
3806
|
import_react12.Suspense,
|
|
3760
3807
|
{
|
|
@@ -3766,13 +3813,19 @@ function SuiteAiPanel({
|
|
|
3766
3813
|
"aria-label": "Rendering response",
|
|
3767
3814
|
children: [
|
|
3768
3815
|
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: "sr-only", children: "Rendering response\u2026" }),
|
|
3769
|
-
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { className: "h-3 w-11/12 rounded bg-ph-mutedtext/20
|
|
3770
|
-
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { className: "h-3 w-full rounded bg-ph-mutedtext/20
|
|
3771
|
-
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { className: "h-3 w-3/5 rounded bg-ph-mutedtext/20
|
|
3816
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { className: "suite-shimmer h-3 w-11/12 rounded bg-ph-mutedtext/20" }),
|
|
3817
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { className: "suite-shimmer h-3 w-full rounded bg-ph-mutedtext/20" }),
|
|
3818
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { className: "suite-shimmer h-3 w-3/5 rounded bg-ph-mutedtext/20" })
|
|
3772
3819
|
]
|
|
3773
3820
|
}
|
|
3774
3821
|
),
|
|
3775
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
3822
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
3823
|
+
SuiteAiMarkdownMessage2,
|
|
3824
|
+
{
|
|
3825
|
+
markdown: message.content,
|
|
3826
|
+
animate: index === lastAssistantIndex
|
|
3827
|
+
}
|
|
3828
|
+
)
|
|
3776
3829
|
}
|
|
3777
3830
|
) }),
|
|
3778
3831
|
((_a = message.actions) != null ? _a : []).map((action) => /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
|
|
@@ -3836,9 +3889,9 @@ function SuiteAiPanel({
|
|
|
3836
3889
|
waiting ? /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "flex items-end gap-2", "aria-hidden": true, children: [
|
|
3837
3890
|
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: "mb-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-ph-brand/10 text-ph-brand", children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(Icon, { className: "h-3.5 w-3.5" }) }),
|
|
3838
3891
|
/* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "flex items-center gap-1.5 rounded-2xl rounded-bl-md bg-ph-muted px-3 py-2.5", children: [
|
|
3839
|
-
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: "h-1.5 w-1.5 rounded-full bg-ph-mutedtext
|
|
3840
|
-
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: "h-1.5 w-1.5 rounded-full bg-ph-mutedtext
|
|
3841
|
-
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: "h-1.5 w-1.5 rounded-full bg-ph-mutedtext
|
|
3892
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: "suite-think-dot h-1.5 w-1.5 rounded-full bg-ph-mutedtext" }),
|
|
3893
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: "suite-think-dot suite-think-dot--2 h-1.5 w-1.5 rounded-full bg-ph-mutedtext" }),
|
|
3894
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: "suite-think-dot suite-think-dot--3 h-1.5 w-1.5 rounded-full bg-ph-mutedtext" })
|
|
3842
3895
|
] })
|
|
3843
3896
|
] }) : null,
|
|
3844
3897
|
waiting ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: "sr-only", "aria-live": "polite", children: "Assistant is responding\u2026" }) : null,
|
|
@@ -4730,7 +4783,9 @@ function SuiteSidebar({
|
|
|
4730
4783
|
className: cn(
|
|
4731
4784
|
"shrink-0 border-t border-ph-border",
|
|
4732
4785
|
surface ? "bg-ph-surface" : "bg-ph-canvas",
|
|
4733
|
-
|
|
4786
|
+
// Trim only the bottom inset: the 32px avatar sits inside a 40px
|
|
4787
|
+
// trigger, so even padding read top-heavy once the avatar shrank.
|
|
4788
|
+
collapsed ? "px-1.5 pt-1.5 pb-1" : "px-3 pt-3 pb-2"
|
|
4734
4789
|
),
|
|
4735
4790
|
children: /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
|
|
4736
4791
|
"div",
|
package/dist/suite.mjs
CHANGED
|
@@ -2386,7 +2386,7 @@ import {
|
|
|
2386
2386
|
WarningTriangle
|
|
2387
2387
|
} from "iconoir-react";
|
|
2388
2388
|
import { Fragment as Fragment5, jsx as jsx17, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
2389
|
-
var SuiteAiMarkdownMessage = lazy(() => import("./ai-message-markdown-
|
|
2389
|
+
var SuiteAiMarkdownMessage = lazy(() => import("./ai-message-markdown-AUEMCO7K.mjs"));
|
|
2390
2390
|
var SUITE_OPEN_ASK_AI_EVENT = "shellstack:open-ask-ai";
|
|
2391
2391
|
var SUITE_ASK_AI_OPEN_KEY = "shellstack:shelly-open";
|
|
2392
2392
|
var SUITE_ASK_AI_OPEN_QUERY = "shelly";
|
|
@@ -2685,6 +2685,10 @@ function SuiteAiPanel({
|
|
|
2685
2685
|
else window.location.assign(new URL(openPath, href).toString());
|
|
2686
2686
|
}
|
|
2687
2687
|
if (!open) return null;
|
|
2688
|
+
const lastAssistantIndex = messages.reduce(
|
|
2689
|
+
(found, message, index) => message.role === "assistant" ? index : found,
|
|
2690
|
+
-1
|
|
2691
|
+
);
|
|
2688
2692
|
const Icon = BrandIcon != null ? BrandIcon : Sparks;
|
|
2689
2693
|
return /* @__PURE__ */ jsxs14(
|
|
2690
2694
|
"aside",
|
|
@@ -2753,7 +2757,7 @@ function SuiteAiPanel({
|
|
|
2753
2757
|
return /* @__PURE__ */ jsx17(
|
|
2754
2758
|
"div",
|
|
2755
2759
|
{
|
|
2756
|
-
className: "flex justify-end",
|
|
2760
|
+
className: "suite-msg-in suite-msg-in--user flex justify-end",
|
|
2757
2761
|
"data-test": "ask-ai-user",
|
|
2758
2762
|
children: /* @__PURE__ */ jsx17("div", { className: "w-fit max-w-[85%] whitespace-pre-wrap break-words rounded-2xl rounded-br-md bg-ph-brand px-3 py-2 text-sm leading-relaxed text-[var(--ph-on-accent)] [overflow-wrap:anywhere]", children: message.content })
|
|
2759
2763
|
},
|
|
@@ -2769,12 +2773,12 @@ function SuiteAiPanel({
|
|
|
2769
2773
|
/* @__PURE__ */ jsx17(
|
|
2770
2774
|
"span",
|
|
2771
2775
|
{
|
|
2772
|
-
className: "mb-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-ph-brand/10 text-ph-brand",
|
|
2776
|
+
className: "suite-msg-avatar mb-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-ph-brand/10 text-ph-brand",
|
|
2773
2777
|
"aria-hidden": true,
|
|
2774
2778
|
children: /* @__PURE__ */ jsx17(Icon, { className: "h-3.5 w-3.5" })
|
|
2775
2779
|
}
|
|
2776
2780
|
),
|
|
2777
|
-
/* @__PURE__ */ jsxs14("div", { className: "min-w-0 max-w-[85%] space-y-2", children: [
|
|
2781
|
+
/* @__PURE__ */ jsxs14("div", { className: "suite-msg-bubble min-w-0 max-w-[85%] space-y-2", children: [
|
|
2778
2782
|
/* @__PURE__ */ jsx17("div", { className: "rounded-2xl rounded-bl-md bg-ph-muted px-3 py-2 text-sm leading-relaxed text-ph-ink", children: /* @__PURE__ */ jsx17(
|
|
2779
2783
|
Suspense,
|
|
2780
2784
|
{
|
|
@@ -2786,13 +2790,19 @@ function SuiteAiPanel({
|
|
|
2786
2790
|
"aria-label": "Rendering response",
|
|
2787
2791
|
children: [
|
|
2788
2792
|
/* @__PURE__ */ jsx17("span", { className: "sr-only", children: "Rendering response\u2026" }),
|
|
2789
|
-
/* @__PURE__ */ jsx17("div", { className: "h-3 w-11/12 rounded bg-ph-mutedtext/20
|
|
2790
|
-
/* @__PURE__ */ jsx17("div", { className: "h-3 w-full rounded bg-ph-mutedtext/20
|
|
2791
|
-
/* @__PURE__ */ jsx17("div", { className: "h-3 w-3/5 rounded bg-ph-mutedtext/20
|
|
2793
|
+
/* @__PURE__ */ jsx17("div", { className: "suite-shimmer h-3 w-11/12 rounded bg-ph-mutedtext/20" }),
|
|
2794
|
+
/* @__PURE__ */ jsx17("div", { className: "suite-shimmer h-3 w-full rounded bg-ph-mutedtext/20" }),
|
|
2795
|
+
/* @__PURE__ */ jsx17("div", { className: "suite-shimmer h-3 w-3/5 rounded bg-ph-mutedtext/20" })
|
|
2792
2796
|
]
|
|
2793
2797
|
}
|
|
2794
2798
|
),
|
|
2795
|
-
children: /* @__PURE__ */ jsx17(
|
|
2799
|
+
children: /* @__PURE__ */ jsx17(
|
|
2800
|
+
SuiteAiMarkdownMessage,
|
|
2801
|
+
{
|
|
2802
|
+
markdown: message.content,
|
|
2803
|
+
animate: index === lastAssistantIndex
|
|
2804
|
+
}
|
|
2805
|
+
)
|
|
2796
2806
|
}
|
|
2797
2807
|
) }),
|
|
2798
2808
|
((_a = message.actions) != null ? _a : []).map((action) => /* @__PURE__ */ jsxs14(
|
|
@@ -2856,9 +2866,9 @@ function SuiteAiPanel({
|
|
|
2856
2866
|
waiting ? /* @__PURE__ */ jsxs14("div", { className: "flex items-end gap-2", "aria-hidden": true, children: [
|
|
2857
2867
|
/* @__PURE__ */ jsx17("span", { className: "mb-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-ph-brand/10 text-ph-brand", children: /* @__PURE__ */ jsx17(Icon, { className: "h-3.5 w-3.5" }) }),
|
|
2858
2868
|
/* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-1.5 rounded-2xl rounded-bl-md bg-ph-muted px-3 py-2.5", children: [
|
|
2859
|
-
/* @__PURE__ */ jsx17("span", { className: "h-1.5 w-1.5 rounded-full bg-ph-mutedtext
|
|
2860
|
-
/* @__PURE__ */ jsx17("span", { className: "h-1.5 w-1.5 rounded-full bg-ph-mutedtext
|
|
2861
|
-
/* @__PURE__ */ jsx17("span", { className: "h-1.5 w-1.5 rounded-full bg-ph-mutedtext
|
|
2869
|
+
/* @__PURE__ */ jsx17("span", { className: "suite-think-dot h-1.5 w-1.5 rounded-full bg-ph-mutedtext" }),
|
|
2870
|
+
/* @__PURE__ */ jsx17("span", { className: "suite-think-dot suite-think-dot--2 h-1.5 w-1.5 rounded-full bg-ph-mutedtext" }),
|
|
2871
|
+
/* @__PURE__ */ jsx17("span", { className: "suite-think-dot suite-think-dot--3 h-1.5 w-1.5 rounded-full bg-ph-mutedtext" })
|
|
2862
2872
|
] })
|
|
2863
2873
|
] }) : null,
|
|
2864
2874
|
waiting ? /* @__PURE__ */ jsx17("span", { className: "sr-only", "aria-live": "polite", children: "Assistant is responding\u2026" }) : null,
|
|
@@ -3754,7 +3764,9 @@ function SuiteSidebar({
|
|
|
3754
3764
|
className: cn2(
|
|
3755
3765
|
"shrink-0 border-t border-ph-border",
|
|
3756
3766
|
surface ? "bg-ph-surface" : "bg-ph-canvas",
|
|
3757
|
-
|
|
3767
|
+
// Trim only the bottom inset: the 32px avatar sits inside a 40px
|
|
3768
|
+
// trigger, so even padding read top-heavy once the avatar shrank.
|
|
3769
|
+
collapsed ? "px-1.5 pt-1.5 pb-1" : "px-3 pt-3 pb-2"
|
|
3758
3770
|
),
|
|
3759
3771
|
children: /* @__PURE__ */ jsxs19(
|
|
3760
3772
|
"div",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xenide-io/the-old-ui-theme",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.6",
|
|
4
4
|
"description": "Props-driven React components and theme tokens inspired by classic interface design — optimized for Next.js apps and internal tools.",
|
|
5
5
|
"author": "Xenide",
|
|
6
6
|
"license": "MIT",
|
|
@@ -797,6 +797,164 @@ button:hover > .ph-card {
|
|
|
797
797
|
border-top: 1px solid var(--ph-border-subtle);
|
|
798
798
|
}
|
|
799
799
|
|
|
800
|
+
/* ── Ask AI conversation motion ─────────────────────────────────────────
|
|
801
|
+
Replies arrive whole (the suite chat is polled, not streamed), so the
|
|
802
|
+
motion carries the "alive" signal instead: a soft one-shot entrance per
|
|
803
|
+
message, a breathing thinking indicator, and a shimmer for the skeleton.
|
|
804
|
+
Only opacity + transform animate — height/margin would fight the
|
|
805
|
+
scroller. All of it collapses under prefers-reduced-motion. */
|
|
806
|
+
@keyframes suite-msg-in {
|
|
807
|
+
from {
|
|
808
|
+
opacity: 0;
|
|
809
|
+
transform: translateY(8px) scale(0.97);
|
|
810
|
+
filter: blur(4px);
|
|
811
|
+
}
|
|
812
|
+
60% {
|
|
813
|
+
opacity: 1;
|
|
814
|
+
}
|
|
815
|
+
to {
|
|
816
|
+
opacity: 1;
|
|
817
|
+
transform: none;
|
|
818
|
+
filter: blur(0);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
@keyframes suite-msg-in-user {
|
|
822
|
+
/* The user's own turn grows out of the composer corner, per the chat
|
|
823
|
+
scroller guidance, so sending feels anchored to where they typed. */
|
|
824
|
+
from {
|
|
825
|
+
opacity: 0;
|
|
826
|
+
transform: translateY(12px) scale(0.96);
|
|
827
|
+
filter: blur(4px);
|
|
828
|
+
}
|
|
829
|
+
60% {
|
|
830
|
+
opacity: 1;
|
|
831
|
+
}
|
|
832
|
+
to {
|
|
833
|
+
opacity: 1;
|
|
834
|
+
transform: none;
|
|
835
|
+
filter: blur(0);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
.suite-msg-in {
|
|
839
|
+
/* Slight overshoot rather than a flat ease-out: the bubble reads as
|
|
840
|
+
landing rather than fading up. Origin sits at the tail corner so it
|
|
841
|
+
appears to grow from the avatar (assistant) / composer (user). */
|
|
842
|
+
animation: suite-msg-in 280ms cubic-bezier(0.34, 1.4, 0.64, 1) both;
|
|
843
|
+
transform-origin: 0 100%;
|
|
844
|
+
}
|
|
845
|
+
.suite-msg-in--user {
|
|
846
|
+
animation-name: suite-msg-in-user;
|
|
847
|
+
animation-duration: 240ms;
|
|
848
|
+
transform-origin: 100% 100%;
|
|
849
|
+
}
|
|
850
|
+
/* Avatar lands first, bubble follows — a small layer separation. The bubble
|
|
851
|
+
only settles (no blur) because the words themselves carry the reveal. */
|
|
852
|
+
@keyframes suite-msg-settle {
|
|
853
|
+
from {
|
|
854
|
+
opacity: 0;
|
|
855
|
+
transform: translateY(4px);
|
|
856
|
+
}
|
|
857
|
+
to {
|
|
858
|
+
opacity: 1;
|
|
859
|
+
transform: none;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
.suite-msg-avatar {
|
|
863
|
+
animation: suite-msg-in 260ms cubic-bezier(0.34, 1.4, 0.64, 1) both;
|
|
864
|
+
}
|
|
865
|
+
.suite-msg-bubble {
|
|
866
|
+
animation: suite-msg-settle 200ms ease-out both;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/* ── Answer reveal ──────────────────────────────────────────────────────
|
|
870
|
+
Replies land in one payload (the suite chat is polled), so the words
|
|
871
|
+
themselves are staggered to read as if they were being written. Delay is
|
|
872
|
+
applied per word inline by the rehype plugin in ai-message-markdown.tsx
|
|
873
|
+
and capped so a long answer still finishes settling in well under a
|
|
874
|
+
second. `both` fill keeps each word hidden until its turn. */
|
|
875
|
+
@keyframes suite-word-in {
|
|
876
|
+
from {
|
|
877
|
+
opacity: 0;
|
|
878
|
+
filter: blur(2px);
|
|
879
|
+
}
|
|
880
|
+
to {
|
|
881
|
+
opacity: 1;
|
|
882
|
+
filter: blur(0);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
.suite-word-in {
|
|
886
|
+
animation: suite-word-in 240ms ease-out both;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
@keyframes suite-think-breathe {
|
|
890
|
+
0%,
|
|
891
|
+
100% {
|
|
892
|
+
opacity: 0.3;
|
|
893
|
+
transform: scale(0.8);
|
|
894
|
+
}
|
|
895
|
+
50% {
|
|
896
|
+
opacity: 1;
|
|
897
|
+
transform: scale(1);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
.suite-think-dot {
|
|
901
|
+
animation: suite-think-breathe 1.4s ease-in-out infinite;
|
|
902
|
+
}
|
|
903
|
+
.suite-think-dot--2 {
|
|
904
|
+
animation-delay: 0.16s;
|
|
905
|
+
}
|
|
906
|
+
.suite-think-dot--3 {
|
|
907
|
+
animation-delay: 0.32s;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/* Sweep a translateX band rather than background-position: the latter
|
|
911
|
+
repaints every frame, the former stays on the compositor. */
|
|
912
|
+
@keyframes suite-shimmer {
|
|
913
|
+
from {
|
|
914
|
+
transform: translateX(-100%);
|
|
915
|
+
}
|
|
916
|
+
to {
|
|
917
|
+
transform: translateX(200%);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
.suite-shimmer {
|
|
921
|
+
position: relative;
|
|
922
|
+
overflow: hidden;
|
|
923
|
+
}
|
|
924
|
+
.suite-shimmer::after {
|
|
925
|
+
content: "";
|
|
926
|
+
position: absolute;
|
|
927
|
+
inset: 0 auto 0 0;
|
|
928
|
+
width: 60%;
|
|
929
|
+
background-image: linear-gradient(
|
|
930
|
+
90deg,
|
|
931
|
+
transparent,
|
|
932
|
+
color-mix(in oklab, var(--ph-text-primary) 9%, transparent),
|
|
933
|
+
transparent
|
|
934
|
+
);
|
|
935
|
+
animation: suite-shimmer 1.6s linear infinite;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
@media (prefers-reduced-motion: reduce) {
|
|
939
|
+
.suite-msg-in,
|
|
940
|
+
.suite-msg-in--user,
|
|
941
|
+
.suite-msg-avatar,
|
|
942
|
+
.suite-msg-bubble,
|
|
943
|
+
.suite-word-in {
|
|
944
|
+
animation: none;
|
|
945
|
+
filter: none;
|
|
946
|
+
}
|
|
947
|
+
.suite-think-dot {
|
|
948
|
+
animation: none;
|
|
949
|
+
opacity: 0.7;
|
|
950
|
+
transform: none;
|
|
951
|
+
}
|
|
952
|
+
.suite-shimmer::after {
|
|
953
|
+
animation: none;
|
|
954
|
+
opacity: 0;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
800
958
|
/* ── Auth / OAuth / launch handoff ──────────────────────────────────────
|
|
801
959
|
One calm opacity fade — no stacked entrance reveals on bridge pages. */
|
|
802
960
|
.suite-auth-handoff {
|
|
@@ -38,4 +38,44 @@ describe("SuiteAiMarkdownMessage", () => {
|
|
|
38
38
|
expect(container.querySelector('[contenteditable]')).toBeNull();
|
|
39
39
|
expect(container.querySelector("img")).toBeNull();
|
|
40
40
|
});
|
|
41
|
+
|
|
42
|
+
it("leaves words untouched unless animate is set", () => {
|
|
43
|
+
const { container } = render(
|
|
44
|
+
<SuiteThemeProvider config={themeConfig}>
|
|
45
|
+
<SuiteAiMarkdownMessage markdown={"Hello there world"} />
|
|
46
|
+
</SuiteThemeProvider>,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
expect(container.querySelectorAll(".suite-word-in")).toHaveLength(0);
|
|
50
|
+
expect(container.textContent).toContain("Hello there world");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("staggers each word when animate is set", () => {
|
|
54
|
+
const { container } = render(
|
|
55
|
+
<SuiteThemeProvider config={themeConfig}>
|
|
56
|
+
<SuiteAiMarkdownMessage markdown={"Hello there world"} animate />
|
|
57
|
+
</SuiteThemeProvider>,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const words = container.querySelectorAll<HTMLElement>(".suite-word-in");
|
|
61
|
+
expect(words).toHaveLength(3);
|
|
62
|
+
expect(words[0].textContent).toBe("Hello");
|
|
63
|
+
expect(words[0].style.animationDelay).toBe("0ms");
|
|
64
|
+
expect(words[1].style.animationDelay).toBe("18ms");
|
|
65
|
+
expect(words[2].style.animationDelay).toBe("36ms");
|
|
66
|
+
// Copy still yields the original prose.
|
|
67
|
+
expect(container.textContent).toBe("Hello there world");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("does not stagger code spans", () => {
|
|
71
|
+
const { container } = render(
|
|
72
|
+
<SuiteThemeProvider config={themeConfig}>
|
|
73
|
+
<SuiteAiMarkdownMessage markdown={"Run `npm run dev` now"} animate />
|
|
74
|
+
</SuiteThemeProvider>,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const code = container.querySelector("code");
|
|
78
|
+
expect(code).toBeTruthy();
|
|
79
|
+
expect(code?.querySelector(".suite-word-in")).toBeNull();
|
|
80
|
+
});
|
|
41
81
|
});
|
|
@@ -1,17 +1,91 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import ReactMarkdown from "react-markdown";
|
|
3
|
+
import ReactMarkdown, { type Options } from "react-markdown";
|
|
4
4
|
import remarkGfm from "remark-gfm";
|
|
5
5
|
|
|
6
|
+
/** Per-word stagger step, and the ceiling so a long reply still settles fast. */
|
|
7
|
+
const WORD_STEP_MS = 18;
|
|
8
|
+
const MAX_DELAY_MS = 600;
|
|
9
|
+
|
|
10
|
+
/** Tags whose text is left alone — animating code word by word reads badly. */
|
|
11
|
+
const SKIP_TAGS = new Set(["code", "pre", "style", "script"]);
|
|
12
|
+
|
|
13
|
+
interface HastNode {
|
|
14
|
+
type: string;
|
|
15
|
+
tagName?: string;
|
|
16
|
+
value?: string;
|
|
17
|
+
properties?: Record<string, unknown>;
|
|
18
|
+
children?: HastNode[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Wrap each word of prose in a span that fades in on a stagger, so a reply
|
|
23
|
+
* that arrived in one payload reads as if it were being written.
|
|
24
|
+
*
|
|
25
|
+
* Delay lives inline on the span, so React re-rendering the same tree reuses
|
|
26
|
+
* the same elements and the animation does not replay.
|
|
27
|
+
*/
|
|
28
|
+
function rehypeStaggerWords() {
|
|
29
|
+
return (tree: HastNode) => {
|
|
30
|
+
let wordIndex = 0;
|
|
31
|
+
|
|
32
|
+
const visit = (node: HastNode) => {
|
|
33
|
+
if (!node.children) return;
|
|
34
|
+
if (node.tagName && SKIP_TAGS.has(node.tagName)) return;
|
|
35
|
+
|
|
36
|
+
const next: HastNode[] = [];
|
|
37
|
+
for (const child of node.children) {
|
|
38
|
+
const isText =
|
|
39
|
+
child.type === "text" &&
|
|
40
|
+
typeof child.value === "string" &&
|
|
41
|
+
/\S/.test(child.value);
|
|
42
|
+
|
|
43
|
+
if (!isText) {
|
|
44
|
+
visit(child);
|
|
45
|
+
next.push(child);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const part of child.value!.split(/(\s+)/)) {
|
|
50
|
+
if (!part) continue;
|
|
51
|
+
if (/^\s+$/.test(part)) {
|
|
52
|
+
next.push({ type: "text", value: part });
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const delay = Math.min(wordIndex * WORD_STEP_MS, MAX_DELAY_MS);
|
|
56
|
+
wordIndex += 1;
|
|
57
|
+
next.push({
|
|
58
|
+
type: "element",
|
|
59
|
+
tagName: "span",
|
|
60
|
+
properties: {
|
|
61
|
+
className: ["suite-word-in"],
|
|
62
|
+
style: `animation-delay:${delay}ms`,
|
|
63
|
+
},
|
|
64
|
+
children: [{ type: "text", value: part }],
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
node.children = next;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
visit(tree);
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
6
75
|
export default function SuiteAiMarkdownMessage({
|
|
7
76
|
markdown,
|
|
77
|
+
animate = false,
|
|
8
78
|
}: {
|
|
9
79
|
markdown: string;
|
|
80
|
+
animate?: boolean;
|
|
10
81
|
}) {
|
|
11
82
|
return (
|
|
12
83
|
<div className="suite-ai-markdown">
|
|
13
84
|
<ReactMarkdown
|
|
14
85
|
remarkPlugins={[remarkGfm]}
|
|
86
|
+
rehypePlugins={
|
|
87
|
+
(animate ? [rehypeStaggerWords] : undefined) as Options["rehypePlugins"]
|
|
88
|
+
}
|
|
15
89
|
components={{ img: () => null }}
|
|
16
90
|
>
|
|
17
91
|
{markdown}
|
|
@@ -425,6 +425,13 @@ export function SuiteAiPanel({
|
|
|
425
425
|
|
|
426
426
|
if (!open) return null;
|
|
427
427
|
|
|
428
|
+
// Only the newest reply gets the staggered word reveal; animating the whole
|
|
429
|
+
// transcript would replay whenever the panel remounts mid-conversation.
|
|
430
|
+
const lastAssistantIndex = messages.reduce(
|
|
431
|
+
(found, message, index) => (message.role === "assistant" ? index : found),
|
|
432
|
+
-1,
|
|
433
|
+
);
|
|
434
|
+
|
|
428
435
|
const Icon = BrandIcon ?? Sparks;
|
|
429
436
|
|
|
430
437
|
return (
|
|
@@ -505,7 +512,7 @@ export function SuiteAiPanel({
|
|
|
505
512
|
return (
|
|
506
513
|
<div
|
|
507
514
|
key={`user-${index}`}
|
|
508
|
-
className="flex justify-end"
|
|
515
|
+
className="suite-msg-in suite-msg-in--user flex justify-end"
|
|
509
516
|
data-test="ask-ai-user"
|
|
510
517
|
>
|
|
511
518
|
<div className="w-fit max-w-[85%] whitespace-pre-wrap break-words rounded-2xl rounded-br-md bg-ph-brand px-3 py-2 text-sm leading-relaxed text-[var(--ph-on-accent)] [overflow-wrap:anywhere]">
|
|
@@ -521,12 +528,12 @@ export function SuiteAiPanel({
|
|
|
521
528
|
data-test="ask-ai-assistant"
|
|
522
529
|
>
|
|
523
530
|
<span
|
|
524
|
-
className="mb-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-ph-brand/10 text-ph-brand"
|
|
531
|
+
className="suite-msg-avatar mb-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-ph-brand/10 text-ph-brand"
|
|
525
532
|
aria-hidden
|
|
526
533
|
>
|
|
527
534
|
<Icon className="h-3.5 w-3.5" />
|
|
528
535
|
</span>
|
|
529
|
-
<div className="min-w-0 max-w-[85%] space-y-2">
|
|
536
|
+
<div className="suite-msg-bubble min-w-0 max-w-[85%] space-y-2">
|
|
530
537
|
<div className="rounded-2xl rounded-bl-md bg-ph-muted px-3 py-2 text-sm leading-relaxed text-ph-ink">
|
|
531
538
|
<Suspense
|
|
532
539
|
fallback={
|
|
@@ -536,13 +543,16 @@ export function SuiteAiPanel({
|
|
|
536
543
|
aria-label="Rendering response"
|
|
537
544
|
>
|
|
538
545
|
<span className="sr-only">Rendering response…</span>
|
|
539
|
-
<div className="h-3 w-11/12 rounded bg-ph-mutedtext/20
|
|
540
|
-
<div className="h-3 w-full rounded bg-ph-mutedtext/20
|
|
541
|
-
<div className="h-3 w-3/5 rounded bg-ph-mutedtext/20
|
|
546
|
+
<div className="suite-shimmer h-3 w-11/12 rounded bg-ph-mutedtext/20" />
|
|
547
|
+
<div className="suite-shimmer h-3 w-full rounded bg-ph-mutedtext/20" />
|
|
548
|
+
<div className="suite-shimmer h-3 w-3/5 rounded bg-ph-mutedtext/20" />
|
|
542
549
|
</div>
|
|
543
550
|
}
|
|
544
551
|
>
|
|
545
|
-
<SuiteAiMarkdownMessage
|
|
552
|
+
<SuiteAiMarkdownMessage
|
|
553
|
+
markdown={message.content}
|
|
554
|
+
animate={index === lastAssistantIndex}
|
|
555
|
+
/>
|
|
546
556
|
</Suspense>
|
|
547
557
|
</div>
|
|
548
558
|
{(message.actions ?? []).map((action) => (
|
|
@@ -612,9 +622,9 @@ export function SuiteAiPanel({
|
|
|
612
622
|
<Icon className="h-3.5 w-3.5" />
|
|
613
623
|
</span>
|
|
614
624
|
<div className="flex items-center gap-1.5 rounded-2xl rounded-bl-md bg-ph-muted px-3 py-2.5">
|
|
615
|
-
<span className="h-1.5 w-1.5 rounded-full bg-ph-mutedtext
|
|
616
|
-
<span className="h-1.5 w-1.5 rounded-full bg-ph-mutedtext
|
|
617
|
-
<span className="h-1.5 w-1.5 rounded-full bg-ph-mutedtext
|
|
625
|
+
<span className="suite-think-dot h-1.5 w-1.5 rounded-full bg-ph-mutedtext" />
|
|
626
|
+
<span className="suite-think-dot suite-think-dot--2 h-1.5 w-1.5 rounded-full bg-ph-mutedtext" />
|
|
627
|
+
<span className="suite-think-dot suite-think-dot--3 h-1.5 w-1.5 rounded-full bg-ph-mutedtext" />
|
|
618
628
|
</div>
|
|
619
629
|
</div>
|
|
620
630
|
) : null}
|
|
@@ -320,7 +320,9 @@ export function SuiteSidebar({
|
|
|
320
320
|
className={cn(
|
|
321
321
|
"shrink-0 border-t border-ph-border",
|
|
322
322
|
surface ? "bg-ph-surface" : "bg-ph-canvas",
|
|
323
|
-
|
|
323
|
+
// Trim only the bottom inset: the 32px avatar sits inside a 40px
|
|
324
|
+
// trigger, so even padding read top-heavy once the avatar shrank.
|
|
325
|
+
collapsed ? "px-1.5 pt-1.5 pb-1" : "px-3 pt-3 pb-2",
|
|
324
326
|
)}
|
|
325
327
|
>
|
|
326
328
|
<div
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import "./chunk-FWCSY2DS.mjs";
|
|
3
|
-
|
|
4
|
-
// src/suite/components/ai-message-markdown.tsx
|
|
5
|
-
import ReactMarkdown from "react-markdown";
|
|
6
|
-
import remarkGfm from "remark-gfm";
|
|
7
|
-
import { jsx } from "react/jsx-runtime";
|
|
8
|
-
function SuiteAiMarkdownMessage({
|
|
9
|
-
markdown
|
|
10
|
-
}) {
|
|
11
|
-
return /* @__PURE__ */ jsx("div", { className: "suite-ai-markdown", children: /* @__PURE__ */ jsx(
|
|
12
|
-
ReactMarkdown,
|
|
13
|
-
{
|
|
14
|
-
remarkPlugins: [remarkGfm],
|
|
15
|
-
components: { img: () => null },
|
|
16
|
-
children: markdown
|
|
17
|
-
}
|
|
18
|
-
) });
|
|
19
|
-
}
|
|
20
|
-
export {
|
|
21
|
-
SuiteAiMarkdownMessage as default
|
|
22
|
-
};
|