@runtypelabs/persona 2.3.0 → 3.0.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 +221 -4
- package/dist/index.cjs +42 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +832 -571
- package/dist/index.d.ts +832 -571
- package/dist/index.global.js +87 -87
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +42 -42
- package/dist/index.js.map +1 -1
- package/dist/widget.css +205 -15
- package/package.json +2 -2
- package/src/components/artifact-card.ts +39 -5
- package/src/components/artifact-pane.ts +67 -126
- package/src/components/composer-builder.ts +3 -23
- package/src/components/header-builder.ts +29 -34
- package/src/components/header-layouts.ts +109 -41
- package/src/components/launcher.ts +10 -7
- package/src/components/message-bubble.ts +7 -11
- package/src/components/panel.ts +4 -4
- package/src/defaults.ts +22 -93
- package/src/index.ts +20 -7
- package/src/presets.ts +66 -51
- package/src/runtime/host-layout.test.ts +196 -0
- package/src/runtime/host-layout.ts +265 -27
- package/src/runtime/init.test.ts +77 -7
- package/src/styles/widget.css +205 -15
- package/src/types/theme.ts +76 -0
- package/src/types.ts +86 -97
- package/src/ui.docked.test.ts +203 -7
- package/src/ui.ts +129 -88
- package/src/utils/buttons.ts +417 -0
- package/src/utils/code-generators.test.ts +43 -7
- package/src/utils/code-generators.ts +9 -25
- package/src/utils/deep-merge.ts +26 -0
- package/src/utils/dock.ts +18 -5
- package/src/utils/dropdown.ts +178 -0
- package/src/utils/sanitize.ts +1 -1
- package/src/utils/theme.test.ts +90 -15
- package/src/utils/theme.ts +20 -46
- package/src/utils/tokens.ts +108 -11
- package/src/utils/migration.ts +0 -220
package/dist/widget.css
CHANGED
|
@@ -2067,7 +2067,7 @@
|
|
|
2067
2067
|
padding: var(--persona-artifact-toolbar-icon-padding, 0.25rem);
|
|
2068
2068
|
border-radius: var(--persona-artifact-toolbar-icon-radius, var(--persona-radius-md, 0.375rem));
|
|
2069
2069
|
border: var(--persona-artifact-toolbar-icon-border, 1px solid var(--persona-border, #e5e7eb));
|
|
2070
|
-
background: var(--persona-surface, #ffffff);
|
|
2070
|
+
background: var(--persona-artifact-toolbar-icon-bg, var(--persona-surface, #ffffff));
|
|
2071
2071
|
color: var(--persona-artifact-doc-toolbar-icon-color, var(--persona-text, #111827));
|
|
2072
2072
|
cursor: pointer;
|
|
2073
2073
|
line-height: 1;
|
|
@@ -2105,18 +2105,6 @@
|
|
|
2105
2105
|
font-weight: 500;
|
|
2106
2106
|
}
|
|
2107
2107
|
|
|
2108
|
-
/* Copy menu dropdown theming */
|
|
2109
|
-
#persona-root .persona-artifact-doc-copy-menu {
|
|
2110
|
-
background: var(--persona-artifact-toolbar-copy-menu-bg, var(--persona-surface, #fff));
|
|
2111
|
-
border: var(--persona-artifact-toolbar-copy-menu-border, 1px solid var(--persona-border, #e5e7eb));
|
|
2112
|
-
box-shadow: var(--persona-artifact-toolbar-copy-menu-shadow, 0 4px 6px -1px rgba(0,0,0,.1));
|
|
2113
|
-
border-radius: var(--persona-artifact-toolbar-copy-menu-radius, 0.375rem);
|
|
2114
|
-
}
|
|
2115
|
-
|
|
2116
|
-
#persona-root .persona-artifact-doc-copy-menu button:hover {
|
|
2117
|
-
background: var(--persona-artifact-toolbar-copy-menu-item-hover-bg, var(--persona-container, #f3f4f6));
|
|
2118
|
-
}
|
|
2119
|
-
|
|
2120
2108
|
/* Artifact tab theming */
|
|
2121
2109
|
#persona-root .persona-artifact-tab {
|
|
2122
2110
|
background: var(--persona-artifact-tab-bg, transparent);
|
|
@@ -2134,6 +2122,200 @@
|
|
|
2134
2122
|
background: var(--persona-artifact-toolbar-bg, var(--persona-surface, #fff));
|
|
2135
2123
|
}
|
|
2136
2124
|
|
|
2125
|
+
/* Toggle group gap */
|
|
2126
|
+
#persona-root .persona-artifact-toggle-group {
|
|
2127
|
+
gap: var(--persona-artifact-toolbar-toggle-group-gap, 0.25rem);
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
/* Toggle button border-radius (view/code buttons) */
|
|
2131
|
+
#persona-root .persona-artifact-toolbar-document .persona-artifact-view-btn,
|
|
2132
|
+
#persona-root .persona-artifact-toolbar-document .persona-artifact-code-btn {
|
|
2133
|
+
border-radius: var(--persona-artifact-toolbar-toggle-radius, var(--persona-artifact-toolbar-icon-radius, var(--persona-radius-md, 0.375rem)));
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
/* Tab hover */
|
|
2137
|
+
#persona-root .persona-artifact-tab:hover {
|
|
2138
|
+
background: var(--persona-artifact-tab-hover-bg, var(--persona-container, #f3f4f6));
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
/* Tab list container */
|
|
2142
|
+
#persona-root .persona-artifact-list {
|
|
2143
|
+
background: var(--persona-artifact-tab-list-bg, transparent);
|
|
2144
|
+
border-bottom-color: var(--persona-artifact-tab-list-border-color, var(--persona-border, #e5e7eb));
|
|
2145
|
+
padding: var(--persona-artifact-tab-list-padding, 0.5rem);
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
/* Toolbar border override */
|
|
2149
|
+
#persona-root .persona-artifact-toolbar-document {
|
|
2150
|
+
border-bottom: var(--persona-artifact-toolbar-border, 1px solid var(--persona-border, #e5e7eb));
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
/* ── Composable button utilities ── */
|
|
2154
|
+
|
|
2155
|
+
/* Icon button — base for all icon-only buttons created by createIconButton() */
|
|
2156
|
+
#persona-root .persona-icon-btn {
|
|
2157
|
+
display: inline-flex;
|
|
2158
|
+
align-items: center;
|
|
2159
|
+
justify-content: center;
|
|
2160
|
+
padding: var(--persona-icon-btn-padding, 0.25rem);
|
|
2161
|
+
border-radius: var(--persona-icon-btn-radius, var(--persona-radius-md, 0.375rem));
|
|
2162
|
+
border: var(--persona-icon-btn-border, 1px solid var(--persona-border, #e5e7eb));
|
|
2163
|
+
background: var(--persona-icon-btn-bg, var(--persona-surface, #ffffff));
|
|
2164
|
+
color: var(--persona-icon-btn-color, var(--persona-text, #111827));
|
|
2165
|
+
cursor: pointer;
|
|
2166
|
+
line-height: 1;
|
|
2167
|
+
transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease;
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
#persona-root .persona-icon-btn:hover {
|
|
2171
|
+
background: var(--persona-icon-btn-hover-bg, var(--persona-container, #f3f4f6));
|
|
2172
|
+
color: var(--persona-icon-btn-hover-color, inherit);
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
#persona-root .persona-icon-btn:focus-visible {
|
|
2176
|
+
outline: 2px solid var(--persona-accent, #3b82f6);
|
|
2177
|
+
outline-offset: 2px;
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
#persona-root .persona-icon-btn[aria-pressed="true"] {
|
|
2181
|
+
background: var(--persona-icon-btn-active-bg, var(--persona-container, #f3f4f6));
|
|
2182
|
+
border-color: var(--persona-icon-btn-active-border, var(--persona-border, #e5e7eb));
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
/* Label button — icon + text button created by createLabelButton() */
|
|
2186
|
+
#persona-root .persona-label-btn {
|
|
2187
|
+
display: inline-flex;
|
|
2188
|
+
align-items: center;
|
|
2189
|
+
gap: var(--persona-label-btn-gap, 0.35rem);
|
|
2190
|
+
padding: var(--persona-label-btn-padding, 0.25rem 0.5rem);
|
|
2191
|
+
border-radius: var(--persona-label-btn-radius, var(--persona-radius-md, 0.375rem));
|
|
2192
|
+
border: var(--persona-label-btn-border, 1px solid var(--persona-border, #e5e7eb));
|
|
2193
|
+
background: var(--persona-label-btn-bg, var(--persona-surface, #ffffff));
|
|
2194
|
+
color: var(--persona-label-btn-color, var(--persona-text, #111827));
|
|
2195
|
+
cursor: pointer;
|
|
2196
|
+
font-size: var(--persona-label-btn-font-size, 0.75rem);
|
|
2197
|
+
font-weight: 500;
|
|
2198
|
+
line-height: 1.25;
|
|
2199
|
+
white-space: nowrap;
|
|
2200
|
+
transition: background-color 0.15s ease, color 0.15s ease;
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
#persona-root .persona-label-btn:hover {
|
|
2204
|
+
background: var(--persona-label-btn-hover-bg, var(--persona-container, #f3f4f6));
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
#persona-root .persona-label-btn:focus-visible {
|
|
2208
|
+
outline: 2px solid var(--persona-accent, #3b82f6);
|
|
2209
|
+
outline-offset: 2px;
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
#persona-root .persona-label-btn--sm {
|
|
2213
|
+
padding: var(--persona-label-btn-padding, 0.25rem 0.5rem);
|
|
2214
|
+
font-size: var(--persona-label-btn-font-size, 0.75rem);
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
#persona-root .persona-label-btn--md {
|
|
2218
|
+
padding: 0.375rem 0.75rem;
|
|
2219
|
+
font-size: 0.8125rem;
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
#persona-root .persona-label-btn--primary {
|
|
2223
|
+
background: var(--persona-primary, #3b82f6);
|
|
2224
|
+
color: var(--persona-text-inverse, #ffffff);
|
|
2225
|
+
border-color: transparent;
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
#persona-root .persona-label-btn--primary:hover {
|
|
2229
|
+
opacity: 0.9;
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
#persona-root .persona-label-btn--destructive {
|
|
2233
|
+
color: var(--persona-label-btn-destructive-color, #ef4444);
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
#persona-root .persona-label-btn--ghost {
|
|
2237
|
+
border: none;
|
|
2238
|
+
background: transparent;
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
#persona-root .persona-label-btn--ghost:hover {
|
|
2242
|
+
background: var(--persona-label-btn-hover-bg, var(--persona-container, #f3f4f6));
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
/* Toggle group — mutually exclusive button set created by createToggleGroup() */
|
|
2246
|
+
#persona-root .persona-toggle-group {
|
|
2247
|
+
display: inline-flex;
|
|
2248
|
+
gap: var(--persona-toggle-group-gap, 0);
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
#persona-root .persona-toggle-group > .persona-icon-btn {
|
|
2252
|
+
border-radius: 0;
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
#persona-root .persona-toggle-group > .persona-icon-btn:first-child {
|
|
2256
|
+
border-top-left-radius: var(--persona-toggle-group-radius, var(--persona-icon-btn-radius, var(--persona-radius-md, 0.375rem)));
|
|
2257
|
+
border-bottom-left-radius: var(--persona-toggle-group-radius, var(--persona-icon-btn-radius, var(--persona-radius-md, 0.375rem)));
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
#persona-root .persona-toggle-group > .persona-icon-btn:last-child {
|
|
2261
|
+
border-top-right-radius: var(--persona-toggle-group-radius, var(--persona-icon-btn-radius, var(--persona-radius-md, 0.375rem)));
|
|
2262
|
+
border-bottom-right-radius: var(--persona-toggle-group-radius, var(--persona-icon-btn-radius, var(--persona-radius-md, 0.375rem)));
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
/* Combo button — label + chevron with dropdown */
|
|
2266
|
+
#persona-root .persona-combo-btn {
|
|
2267
|
+
font-size: var(--persona-combo-btn-font-size, 0.8125rem);
|
|
2268
|
+
font-weight: var(--persona-combo-btn-font-weight, 600);
|
|
2269
|
+
color: var(--persona-combo-btn-color, var(--persona-text, #111827));
|
|
2270
|
+
user-select: none;
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
#persona-root .persona-combo-btn-label {
|
|
2274
|
+
white-space: nowrap;
|
|
2275
|
+
overflow: hidden;
|
|
2276
|
+
text-overflow: ellipsis;
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
/* Dropdown menu utility */
|
|
2280
|
+
#persona-root .persona-dropdown-menu {
|
|
2281
|
+
z-index: 100;
|
|
2282
|
+
min-width: 160px;
|
|
2283
|
+
background: var(--persona-dropdown-bg, var(--persona-surface, #fff));
|
|
2284
|
+
border: var(--persona-dropdown-border, 1px solid var(--persona-border, #e5e7eb));
|
|
2285
|
+
border-radius: var(--persona-dropdown-radius, 0.625rem);
|
|
2286
|
+
padding: 0.25rem 0;
|
|
2287
|
+
box-shadow: var(--persona-dropdown-shadow, 0 4px 16px rgba(0,0,0,0.12));
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
#persona-root .persona-dropdown-menu button {
|
|
2291
|
+
display: flex;
|
|
2292
|
+
align-items: center;
|
|
2293
|
+
gap: 0.625rem;
|
|
2294
|
+
width: 100%;
|
|
2295
|
+
padding: 0.5rem 0.875rem;
|
|
2296
|
+
border: none;
|
|
2297
|
+
background: transparent;
|
|
2298
|
+
color: var(--persona-dropdown-item-color, var(--persona-text, #1f2937));
|
|
2299
|
+
font-size: 0.8125rem;
|
|
2300
|
+
cursor: pointer;
|
|
2301
|
+
text-align: left;
|
|
2302
|
+
white-space: nowrap;
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
#persona-root .persona-dropdown-menu button:hover {
|
|
2306
|
+
background: var(--persona-dropdown-item-hover-bg, var(--persona-container, #f3f4f6));
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
#persona-root .persona-dropdown-menu button[data-destructive] {
|
|
2310
|
+
color: var(--persona-dropdown-destructive-color, #ef4444);
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
#persona-root .persona-dropdown-menu hr {
|
|
2314
|
+
border: none;
|
|
2315
|
+
border-top: 1px solid var(--persona-dropdown-hr, var(--persona-border, #e5e7eb));
|
|
2316
|
+
margin: 0.25rem 0;
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2137
2319
|
/* Draggable split handle (desktop split only; hidden in drawer / narrow host / small viewport) */
|
|
2138
2320
|
#persona-root .persona-artifact-split-handle {
|
|
2139
2321
|
width: 6px;
|
|
@@ -2178,7 +2360,11 @@
|
|
|
2178
2360
|
#persona-root .persona-artifact-pane {
|
|
2179
2361
|
border-radius: var(--persona-artifact-pane-radius, 0);
|
|
2180
2362
|
overflow: hidden;
|
|
2181
|
-
|
|
2363
|
+
/* Layout paneBackground → theme components.artifact.pane.background → semantic surface */
|
|
2364
|
+
background-color: var(
|
|
2365
|
+
--persona-artifact-pane-bg,
|
|
2366
|
+
var(--persona-components-artifact-pane-background, var(--persona-surface, #ffffff))
|
|
2367
|
+
);
|
|
2182
2368
|
}
|
|
2183
2369
|
|
|
2184
2370
|
/* paneAppearance: 'seamless' — flush with chat, no border/shadow/gap */
|
|
@@ -2192,7 +2378,11 @@
|
|
|
2192
2378
|
border-left-width: 0 !important;
|
|
2193
2379
|
border-left-color: transparent !important;
|
|
2194
2380
|
box-shadow: none !important;
|
|
2195
|
-
|
|
2381
|
+
/* Same token chain; final fallback stays container for flush split chrome */
|
|
2382
|
+
background-color: var(
|
|
2383
|
+
--persona-artifact-pane-bg,
|
|
2384
|
+
var(--persona-components-artifact-pane-background, var(--persona-container, #f8fafc))
|
|
2385
|
+
);
|
|
2196
2386
|
}
|
|
2197
2387
|
|
|
2198
2388
|
/* layout.paneBorder / paneBorderLeft — theme overrides (after appearance defaults) */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runtypelabs/persona",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Themeable, pluggable streaming agent widget for websites, in plain JS with support for voice input and reasoning / tool output.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"vitest": "^4.0.9"
|
|
47
47
|
},
|
|
48
48
|
"engines": {
|
|
49
|
-
"node": ">=
|
|
49
|
+
"node": ">=20.0.0"
|
|
50
50
|
},
|
|
51
51
|
"author": "Runtype",
|
|
52
52
|
"license": "MIT",
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import type { ComponentRenderer } from "./registry";
|
|
1
|
+
import type { ComponentContext, ComponentRenderer } from "./registry";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* Uses `data-open-artifact` attribute for click delegation (handled in ui.ts).
|
|
4
|
+
* Default artifact card renderer.
|
|
5
|
+
* Builds the compact clickable card shown in the chat thread.
|
|
7
6
|
*/
|
|
8
|
-
|
|
7
|
+
function renderDefaultArtifactCard(
|
|
8
|
+
props: Record<string, unknown>,
|
|
9
|
+
_context: ComponentContext
|
|
10
|
+
): HTMLElement {
|
|
9
11
|
const title =
|
|
10
12
|
typeof props.title === "string" && props.title
|
|
11
13
|
? props.title
|
|
@@ -88,4 +90,36 @@ export const PersonaArtifactCard: ComponentRenderer = (props) => {
|
|
|
88
90
|
}
|
|
89
91
|
|
|
90
92
|
return root;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Built-in artifact reference card component.
|
|
97
|
+
* Renders a compact clickable card in the chat thread that links to an artifact.
|
|
98
|
+
* Uses `data-open-artifact` attribute for click delegation (handled in ui.ts).
|
|
99
|
+
*
|
|
100
|
+
* Supports a custom `renderCard` callback via `config.features.artifacts.renderCard`
|
|
101
|
+
* that can override the default card rendering.
|
|
102
|
+
*/
|
|
103
|
+
export const PersonaArtifactCard: ComponentRenderer = (props, context) => {
|
|
104
|
+
const customRenderer = context?.config?.features?.artifacts?.renderCard;
|
|
105
|
+
if (customRenderer) {
|
|
106
|
+
const title =
|
|
107
|
+
typeof props.title === "string" && props.title
|
|
108
|
+
? props.title
|
|
109
|
+
: "Untitled artifact";
|
|
110
|
+
const artifactId =
|
|
111
|
+
typeof props.artifactId === "string" ? props.artifactId : "";
|
|
112
|
+
const status = props.status === "streaming" ? "streaming" : "complete";
|
|
113
|
+
const artifactType =
|
|
114
|
+
typeof props.artifactType === "string" ? props.artifactType : "markdown";
|
|
115
|
+
|
|
116
|
+
const result = customRenderer({
|
|
117
|
+
artifact: { artifactId, title, artifactType, status },
|
|
118
|
+
config: context.config,
|
|
119
|
+
defaultRenderer: () => renderDefaultArtifactCard(props, context),
|
|
120
|
+
});
|
|
121
|
+
if (result) return result;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return renderDefaultArtifactCard(props, context);
|
|
91
125
|
};
|
|
@@ -4,6 +4,8 @@ import { escapeHtml, createMarkdownProcessorFromConfig } from "../postprocessors
|
|
|
4
4
|
import { resolveSanitizer } from "../utils/sanitize";
|
|
5
5
|
import { componentRegistry, type ComponentContext } from "./registry";
|
|
6
6
|
import { renderLucideIcon } from "../utils/icons";
|
|
7
|
+
import { createDropdownMenu, type DropdownMenuHandle } from "../utils/dropdown";
|
|
8
|
+
import { createIconButton, createLabelButton } from "../utils/buttons";
|
|
7
9
|
|
|
8
10
|
export type ArtifactPaneApi = {
|
|
9
11
|
element: HTMLElement;
|
|
@@ -27,65 +29,6 @@ function fallbackComponentCard(sel: PersonaArtifactRecord): HTMLElement {
|
|
|
27
29
|
return card;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
function iconButton(iconName: string, label: string, extraClass = ""): HTMLButtonElement {
|
|
31
|
-
const btn = createElement(
|
|
32
|
-
"button",
|
|
33
|
-
`persona-inline-flex persona-items-center persona-justify-center persona-rounded-md persona-border persona-border-persona-border persona-bg-persona-surface persona-p-1 persona-text-persona-primary hover:persona-bg-persona-container ${extraClass}`
|
|
34
|
-
) as HTMLButtonElement;
|
|
35
|
-
btn.type = "button";
|
|
36
|
-
btn.setAttribute("aria-label", label);
|
|
37
|
-
btn.title = label;
|
|
38
|
-
const icon = renderLucideIcon(iconName, 16, "currentColor", 2);
|
|
39
|
-
if (icon) btn.appendChild(icon);
|
|
40
|
-
return btn;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function documentToolbarIconButton(
|
|
44
|
-
iconName: string,
|
|
45
|
-
label: string,
|
|
46
|
-
extraClass: string
|
|
47
|
-
): HTMLButtonElement {
|
|
48
|
-
const btn = createElement(
|
|
49
|
-
"button",
|
|
50
|
-
`persona-artifact-doc-icon-btn ${extraClass}`.trim()
|
|
51
|
-
) as HTMLButtonElement;
|
|
52
|
-
btn.type = "button";
|
|
53
|
-
btn.setAttribute("aria-label", label);
|
|
54
|
-
btn.title = label;
|
|
55
|
-
const icon = renderLucideIcon(iconName, 16, "currentColor", 2);
|
|
56
|
-
if (icon) btn.appendChild(icon);
|
|
57
|
-
return btn;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function documentToolbarCopyMainButton(showLabel: boolean): HTMLButtonElement {
|
|
61
|
-
const btn = createElement("button", "persona-artifact-doc-copy-btn") as HTMLButtonElement;
|
|
62
|
-
btn.type = "button";
|
|
63
|
-
btn.setAttribute("aria-label", "Copy");
|
|
64
|
-
btn.title = "Copy";
|
|
65
|
-
const icon = renderLucideIcon("copy", showLabel ? 14 : 16, "currentColor", 2);
|
|
66
|
-
if (icon) btn.appendChild(icon);
|
|
67
|
-
if (showLabel) {
|
|
68
|
-
const span = createElement("span", "persona-artifact-doc-copy-label");
|
|
69
|
-
span.textContent = "Copy";
|
|
70
|
-
btn.appendChild(span);
|
|
71
|
-
}
|
|
72
|
-
return btn;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function documentToolbarChevronMenuButton(): HTMLButtonElement {
|
|
76
|
-
const btn = createElement(
|
|
77
|
-
"button",
|
|
78
|
-
"persona-artifact-doc-copy-menu-chevron persona-artifact-doc-icon-btn"
|
|
79
|
-
) as HTMLButtonElement;
|
|
80
|
-
btn.type = "button";
|
|
81
|
-
btn.setAttribute("aria-label", "More copy options");
|
|
82
|
-
btn.setAttribute("aria-haspopup", "true");
|
|
83
|
-
btn.setAttribute("aria-expanded", "false");
|
|
84
|
-
const chev = renderLucideIcon("chevron-down", 14, "currentColor", 2);
|
|
85
|
-
if (chev) btn.appendChild(chev);
|
|
86
|
-
return btn;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
32
|
/**
|
|
90
33
|
* Right-hand artifact sidebar / mobile drawer content.
|
|
91
34
|
*/
|
|
@@ -119,6 +62,8 @@ export function createArtifactPane(
|
|
|
119
62
|
const dismissLocalUi = () => {
|
|
120
63
|
backdrop?.classList.add("persona-hidden");
|
|
121
64
|
shell.classList.remove("persona-artifact-drawer-open");
|
|
65
|
+
// Hide portaled copy menu
|
|
66
|
+
copyMenuDropdown?.hide();
|
|
122
67
|
};
|
|
123
68
|
|
|
124
69
|
if (backdrop) {
|
|
@@ -162,13 +107,13 @@ export function createArtifactPane(
|
|
|
162
107
|
|
|
163
108
|
/** Document preset: view vs raw source */
|
|
164
109
|
let viewMode: "rendered" | "source" = "rendered";
|
|
165
|
-
const leftTools = createElement("div", "persona-flex persona-items-center persona-gap-1 persona-shrink-0");
|
|
110
|
+
const leftTools = createElement("div", "persona-flex persona-items-center persona-gap-1 persona-shrink-0 persona-artifact-toggle-group");
|
|
166
111
|
const viewBtn = documentChrome
|
|
167
|
-
?
|
|
168
|
-
:
|
|
112
|
+
? createIconButton({ icon: "eye", label: "Rendered view", className: "persona-artifact-doc-icon-btn persona-artifact-view-btn" })
|
|
113
|
+
: createIconButton({ icon: "eye", label: "Rendered view" });
|
|
169
114
|
const codeBtn = documentChrome
|
|
170
|
-
?
|
|
171
|
-
:
|
|
115
|
+
? createIconButton({ icon: "code-2", label: "Source", className: "persona-artifact-doc-icon-btn persona-artifact-code-btn" })
|
|
116
|
+
: createIconButton({ icon: "code-2", label: "Source" });
|
|
172
117
|
const actionsRight = createElement("div", "persona-flex persona-items-center persona-gap-1 persona-shrink-0");
|
|
173
118
|
const showCopyLabel = layout?.documentToolbarShowCopyLabel === true;
|
|
174
119
|
const showCopyChevron = layout?.documentToolbarShowCopyChevron === true;
|
|
@@ -178,10 +123,12 @@ export function createArtifactPane(
|
|
|
178
123
|
let copyWrap: HTMLElement | null = null;
|
|
179
124
|
let copyBtn: HTMLButtonElement;
|
|
180
125
|
let copyMenuChevronBtn: HTMLButtonElement | null = null;
|
|
181
|
-
let
|
|
126
|
+
let copyMenuDropdown: DropdownMenuHandle | null = null;
|
|
182
127
|
|
|
183
128
|
if (documentChrome && (showCopyLabel || showCopyChevron) && !showCopyMenu) {
|
|
184
|
-
copyBtn =
|
|
129
|
+
copyBtn = showCopyLabel
|
|
130
|
+
? createLabelButton({ icon: "copy", label: "Copy", iconSize: 14, className: "persona-artifact-doc-copy-btn" })
|
|
131
|
+
: createIconButton({ icon: "copy", label: "Copy", className: "persona-artifact-doc-copy-btn" });
|
|
185
132
|
if (showCopyChevron) {
|
|
186
133
|
const chev = renderLucideIcon("chevron-down", 14, "currentColor", 2);
|
|
187
134
|
if (chev) copyBtn.appendChild(chev);
|
|
@@ -191,36 +138,29 @@ export function createArtifactPane(
|
|
|
191
138
|
"div",
|
|
192
139
|
"persona-relative persona-inline-flex persona-items-center persona-gap-0 persona-rounded-md"
|
|
193
140
|
);
|
|
194
|
-
copyBtn =
|
|
195
|
-
|
|
141
|
+
copyBtn = showCopyLabel
|
|
142
|
+
? createLabelButton({ icon: "copy", label: "Copy", iconSize: 14, className: "persona-artifact-doc-copy-btn" })
|
|
143
|
+
: createIconButton({ icon: "copy", label: "Copy", className: "persona-artifact-doc-copy-btn" });
|
|
144
|
+
copyMenuChevronBtn = createIconButton({
|
|
145
|
+
icon: "chevron-down",
|
|
146
|
+
label: "More copy options",
|
|
147
|
+
size: 14,
|
|
148
|
+
className: "persona-artifact-doc-copy-menu-chevron persona-artifact-doc-icon-btn",
|
|
149
|
+
aria: { "aria-haspopup": "true", "aria-expanded": "false" }
|
|
150
|
+
});
|
|
196
151
|
copyWrap.append(copyBtn, copyMenuChevronBtn);
|
|
197
|
-
copyMenuEl = createElement(
|
|
198
|
-
"div",
|
|
199
|
-
"persona-artifact-doc-copy-menu persona-absolute persona-right-0 persona-top-full persona-z-20 persona-mt-1 persona-min-w-[10rem] persona-rounded-md persona-border persona-border-persona-border persona-bg-persona-surface persona-py-1 persona-shadow-md persona-hidden"
|
|
200
|
-
);
|
|
201
|
-
copyWrap.appendChild(copyMenuEl);
|
|
202
|
-
for (const item of copyMenuItems!) {
|
|
203
|
-
const opt = createElement(
|
|
204
|
-
"button",
|
|
205
|
-
"persona-block persona-w-full persona-text-left persona-px-3 persona-py-2 persona-text-xs persona-text-persona-primary hover:persona-bg-persona-container"
|
|
206
|
-
) as HTMLButtonElement;
|
|
207
|
-
opt.type = "button";
|
|
208
|
-
opt.textContent = item.label;
|
|
209
|
-
opt.dataset.copyMenuId = item.id;
|
|
210
|
-
copyMenuEl.appendChild(opt);
|
|
211
|
-
}
|
|
212
152
|
} else if (documentChrome) {
|
|
213
|
-
copyBtn =
|
|
153
|
+
copyBtn = createIconButton({ icon: "copy", label: "Copy", className: "persona-artifact-doc-icon-btn" });
|
|
214
154
|
} else {
|
|
215
|
-
copyBtn =
|
|
155
|
+
copyBtn = createIconButton({ icon: "copy", label: "Copy" });
|
|
216
156
|
}
|
|
217
157
|
|
|
218
158
|
const refreshBtn = documentChrome
|
|
219
|
-
?
|
|
220
|
-
:
|
|
159
|
+
? createIconButton({ icon: "refresh-cw", label: "Refresh", className: "persona-artifact-doc-icon-btn" })
|
|
160
|
+
: createIconButton({ icon: "refresh-cw", label: "Refresh" });
|
|
221
161
|
const closeIconBtn = documentChrome
|
|
222
|
-
?
|
|
223
|
-
:
|
|
162
|
+
? createIconButton({ icon: "x", label: "Close", className: "persona-artifact-doc-icon-btn" })
|
|
163
|
+
: createIconButton({ icon: "x", label: "Close" });
|
|
224
164
|
|
|
225
165
|
const getSelectedArtifactText = (): { markdown: string; jsonPayload: string; id: string | null } => {
|
|
226
166
|
const sel = records.find((r) => r.id === selectedId) ?? records[records.length - 1];
|
|
@@ -262,45 +202,46 @@ export function createArtifactPane(
|
|
|
262
202
|
await defaultCopy();
|
|
263
203
|
});
|
|
264
204
|
|
|
265
|
-
if (copyMenuChevronBtn &&
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
205
|
+
if (copyMenuChevronBtn && copyMenuItems?.length) {
|
|
206
|
+
// Resolve the portal target — #persona-root for CSS var inheritance, escaping overflow: hidden
|
|
207
|
+
const resolvePortal = (): HTMLElement => shell.closest("#persona-root") as HTMLElement ?? document.body;
|
|
208
|
+
|
|
209
|
+
const initDropdown = () => {
|
|
210
|
+
copyMenuDropdown = createDropdownMenu({
|
|
211
|
+
items: copyMenuItems.map((item) => ({ id: item.id, label: item.label })),
|
|
212
|
+
onSelect: async (actionId) => {
|
|
213
|
+
const { markdown, jsonPayload, id } = getSelectedArtifactText();
|
|
214
|
+
const handler = layout?.onDocumentToolbarCopyMenuSelect;
|
|
215
|
+
try {
|
|
216
|
+
if (handler) {
|
|
217
|
+
await handler({ actionId, artifactId: id, markdown, jsonPayload });
|
|
218
|
+
} else if (actionId === "markdown" || actionId === "md") {
|
|
219
|
+
await navigator.clipboard.writeText(markdown);
|
|
220
|
+
} else if (actionId === "json" || actionId === "source") {
|
|
221
|
+
await navigator.clipboard.writeText(jsonPayload);
|
|
222
|
+
} else {
|
|
223
|
+
await navigator.clipboard.writeText(markdown || jsonPayload);
|
|
224
|
+
}
|
|
225
|
+
} catch {
|
|
226
|
+
/* ignore */
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
anchor: copyWrap ?? copyMenuChevronBtn!,
|
|
230
|
+
position: 'bottom-right',
|
|
231
|
+
portal: resolvePortal(),
|
|
232
|
+
});
|
|
269
233
|
};
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
} else {
|
|
277
|
-
closeMenu();
|
|
278
|
-
}
|
|
279
|
-
});
|
|
280
|
-
if (typeof document !== "undefined") {
|
|
281
|
-
document.addEventListener("click", closeMenu);
|
|
234
|
+
|
|
235
|
+
// Defer init until shell is in the DOM (may not be attached yet)
|
|
236
|
+
if (shell.isConnected) {
|
|
237
|
+
initDropdown();
|
|
238
|
+
} else {
|
|
239
|
+
requestAnimationFrame(initDropdown);
|
|
282
240
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
if (!t?.dataset.copyMenuId) return;
|
|
241
|
+
|
|
242
|
+
copyMenuChevronBtn.addEventListener("click", (e) => {
|
|
286
243
|
e.stopPropagation();
|
|
287
|
-
|
|
288
|
-
const { markdown, jsonPayload, id } = getSelectedArtifactText();
|
|
289
|
-
const handler = layout?.onDocumentToolbarCopyMenuSelect;
|
|
290
|
-
try {
|
|
291
|
-
if (handler) {
|
|
292
|
-
await handler({ actionId, artifactId: id, markdown, jsonPayload });
|
|
293
|
-
} else if (actionId === "markdown" || actionId === "md") {
|
|
294
|
-
await navigator.clipboard.writeText(markdown);
|
|
295
|
-
} else if (actionId === "json" || actionId === "source") {
|
|
296
|
-
await navigator.clipboard.writeText(jsonPayload);
|
|
297
|
-
} else {
|
|
298
|
-
await navigator.clipboard.writeText(markdown || jsonPayload);
|
|
299
|
-
}
|
|
300
|
-
} catch {
|
|
301
|
-
/* ignore */
|
|
302
|
-
}
|
|
303
|
-
closeMenu();
|
|
244
|
+
copyMenuDropdown?.toggle();
|
|
304
245
|
});
|
|
305
246
|
}
|
|
306
247
|
|
|
@@ -388,7 +329,7 @@ export function createArtifactPane(
|
|
|
388
329
|
for (const r of records) {
|
|
389
330
|
const tab = createElement(
|
|
390
331
|
"button",
|
|
391
|
-
"persona-artifact-tab persona-shrink-0 persona-rounded-lg persona-px-2 persona-py-1 persona-text-xs persona-border persona-border-transparent persona-text-persona-primary
|
|
332
|
+
"persona-artifact-tab persona-shrink-0 persona-rounded-lg persona-px-2 persona-py-1 persona-text-xs persona-border persona-border-transparent persona-text-persona-primary"
|
|
392
333
|
);
|
|
393
334
|
tab.type = "button";
|
|
394
335
|
tab.textContent = r.title || r.id.slice(0, 8);
|
|
@@ -40,23 +40,6 @@ export interface ComposerBuildContext {
|
|
|
40
40
|
disabled?: boolean;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
/**
|
|
44
|
-
* Helper to get font family CSS value from config preset
|
|
45
|
-
*/
|
|
46
|
-
const getFontFamilyValue = (
|
|
47
|
-
family: "sans-serif" | "serif" | "mono"
|
|
48
|
-
): string => {
|
|
49
|
-
switch (family) {
|
|
50
|
-
case "serif":
|
|
51
|
-
return 'Georgia, "Times New Roman", Times, serif';
|
|
52
|
-
case "mono":
|
|
53
|
-
return '"Courier New", Courier, "Lucida Console", Monaco, monospace';
|
|
54
|
-
case "sans-serif":
|
|
55
|
-
default:
|
|
56
|
-
return '-apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif';
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
|
|
60
43
|
/**
|
|
61
44
|
* Build the composer/footer section of the panel.
|
|
62
45
|
* Extracted for reuse and plugin override support.
|
|
@@ -91,12 +74,9 @@ export const buildComposer = (context: ComposerBuildContext): ComposerElements =
|
|
|
91
74
|
"persona-w-full persona-min-h-[24px] persona-resize-none persona-border-none persona-bg-transparent persona-text-sm persona-text-persona-primary focus:persona-outline-none focus:persona-border-none persona-composer-textarea";
|
|
92
75
|
textarea.rows = 1;
|
|
93
76
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
textarea.style.fontFamily = getFontFamilyValue(fontFamily);
|
|
99
|
-
textarea.style.fontWeight = fontWeight;
|
|
77
|
+
textarea.style.fontFamily =
|
|
78
|
+
'var(--persona-input-font-family, var(--persona-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif))';
|
|
79
|
+
textarea.style.fontWeight = "var(--persona-input-font-weight, var(--persona-font-weight, 400))";
|
|
100
80
|
|
|
101
81
|
// Set up auto-resize: expand up to 3 lines, then scroll
|
|
102
82
|
// Line height is ~20px for text-sm (14px * 1.25 line-height), so 3 lines ≈ 60px
|