@runtypelabs/persona 4.17.0 → 4.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-BpWGm7_6.d.cts → types-DsmTD8TO.d.cts} +12 -0
- package/dist/animations/{types-BpWGm7_6.d.ts → types-DsmTD8TO.d.ts} +12 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/chunk-3I2S3I5P.js +1 -0
- package/dist/chunk-76IELU7F.js +1 -0
- package/dist/chunk-EFJBUH5F.js +1 -0
- package/dist/chunk-PDPVORUF.js +1 -0
- package/dist/chunk-Q735PSZL.js +1 -0
- package/dist/chunk-VK6JOIYJ.js +1 -0
- package/dist/codegen.cjs +4 -4
- package/dist/codegen.js +4 -4
- package/dist/{context-mentions-BTSTIBFC.js → context-mentions-MJL6S6P2.js} +1 -1
- package/dist/context-mentions.cjs +9 -8
- package/dist/context-mentions.d.cts +1224 -14
- package/dist/context-mentions.d.ts +1224 -14
- package/dist/context-mentions.js +9 -8
- package/dist/event-stream-view-4EZ7BGFK.js +1 -0
- package/dist/event-stream-view.cjs +129 -0
- package/dist/event-stream-view.d.cts +8384 -0
- package/dist/event-stream-view.d.ts +8384 -0
- package/dist/event-stream-view.js +129 -0
- package/dist/history-view-BD6OXLNA.js +898 -0
- package/dist/history-view.cjs +898 -0
- package/dist/history-view.d.cts +1461 -0
- package/dist/history-view.d.ts +1461 -0
- package/dist/history-view.js +898 -0
- package/dist/index.cjs +77 -77
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2040 -21
- package/dist/index.d.ts +2040 -21
- package/dist/index.global.js +67 -66
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +77 -77
- package/dist/index.js.map +1 -1
- package/dist/launcher.global.js +5 -4
- package/dist/launcher.global.js.map +1 -1
- package/dist/plugin-kit.cjs +1 -1
- package/dist/plugin-kit.d.cts +0 -8
- package/dist/plugin-kit.d.ts +0 -8
- package/dist/plugin-kit.js +1 -1
- package/dist/smart-dom-reader.d.cts +1224 -14
- package/dist/smart-dom-reader.d.ts +1224 -14
- package/dist/theme-editor-preview.cjs +970 -73
- package/dist/theme-editor-preview.d.cts +1343 -15
- package/dist/theme-editor-preview.d.ts +1343 -15
- package/dist/theme-editor-preview.js +81 -81
- package/dist/theme-editor.cjs +7 -7
- package/dist/theme-editor.d.cts +1227 -15
- package/dist/theme-editor.d.ts +1227 -15
- package/dist/theme-editor.js +9 -9
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +226 -7
- package/dist/theme-reference.d.ts +226 -7
- package/dist/theme-reference.js +1 -1
- package/dist/webmcp-polyfill.js +2 -2
- package/dist/widget.css +1 -1
- package/package.json +18 -6
- package/src/client-history-rest.test.ts +1034 -0
- package/src/client-visitor-history.test.ts +596 -0
- package/src/client.ts +1040 -24
- package/src/components/artifact-inline.ts +8 -1
- package/src/components/artifact-pane.test.ts +278 -1
- package/src/components/artifact-pane.ts +135 -67
- package/src/components/artifact-preview.test.ts +14 -1
- package/src/components/artifact-preview.ts +9 -0
- package/src/components/header-builder.ts +3 -0
- package/src/components/header-layouts.test.ts +171 -7
- package/src/components/header-layouts.ts +83 -53
- package/src/components/header-parts.test.ts +154 -1
- package/src/components/header-parts.ts +266 -138
- package/src/components/history-confirm-dialog.ts +177 -0
- package/src/components/history-shell-copy.ts +99 -0
- package/src/components/history-view/announcer.ts +40 -0
- package/src/components/history-view/copy.ts +105 -0
- package/src/components/history-view/css.ts +1078 -0
- package/src/components/history-view/grouping.test.ts +142 -0
- package/src/components/history-view/grouping.ts +142 -0
- package/src/components/history-view/icons.ts +73 -0
- package/src/components/history-view/rows.ts +341 -0
- package/src/components/history-view/state.ts +309 -0
- package/src/components/history-view.test.ts +2660 -0
- package/src/components/history-view.ts +1891 -0
- package/src/components/launcher-surface.test.ts +133 -0
- package/src/components/launcher.test.ts +18 -0
- package/src/components/launcher.ts +7 -7
- package/src/components/panel.ts +17 -7
- package/src/components/welcome.ts +72 -2
- package/src/defaults.ts +36 -3
- package/src/event-stream-view-bundle.test.ts +69 -0
- package/src/event-stream-view-entry.ts +19 -0
- package/src/event-stream-view-loader.ts +31 -0
- package/src/event-stream-view.ts +19 -0
- package/src/generated/runtype-openapi-contract.ts +54 -4
- package/src/history-render.ts +325 -0
- package/src/history-view-bundle.test.ts +78 -0
- package/src/history-view-entry.ts +41 -0
- package/src/history-view-loader.test.ts +30 -0
- package/src/history-view-loader.ts +32 -0
- package/src/history-view.ts +28 -0
- package/src/index-core.ts +50 -2
- package/src/index-global.ts +55 -0
- package/src/internal/demo-history-provider.test.ts +402 -0
- package/src/internal/demo-history-provider.ts +689 -0
- package/src/internal/history-provider-registry.ts +25 -0
- package/src/internal/history-provider.ts +150 -0
- package/src/internal/runtype-history-provider.test.ts +313 -0
- package/src/internal/runtype-history-provider.ts +240 -0
- package/src/plugin-kit.test.ts +16 -0
- package/src/plugin-kit.ts +13 -5
- package/src/plugins/types.ts +177 -1
- package/src/session-history.test.ts +1397 -0
- package/src/session.ts +1200 -26
- package/src/styles/widget-styles.test.ts +133 -0
- package/src/styles/widget.css +227 -8
- package/src/theme-editor/preview-utils.test.ts +19 -0
- package/src/theme-editor/preview-utils.ts +15 -1
- package/src/theme-editor/preview.ts +7 -1
- package/src/theme-editor/role-mappings.ts +4 -1
- package/src/theme-editor/sections.test.ts +168 -1
- package/src/theme-editor/sections.ts +136 -7
- package/src/theme-editor/webmcp/summary.ts +16 -2
- package/src/theme-reference.ts +20 -6
- package/src/types/theme.ts +213 -7
- package/src/types.ts +927 -7
- package/src/ui.artifact-rail-resize.test.ts +235 -0
- package/src/ui.attachments-drop.test.ts +1 -0
- package/src/ui.header-update-stability.test.ts +2 -2
- package/src/ui.history-chunk.test.ts +72 -0
- package/src/ui.history-config-provider.test.ts +318 -0
- package/src/ui.history-controller.test.ts +711 -0
- package/src/ui.history-lifecycle.test.ts +456 -0
- package/src/ui.history-render-hooks.test.ts +904 -0
- package/src/ui.history-shell.test.ts +1887 -0
- package/src/ui.rail-overlay.test.ts +573 -0
- package/src/ui.rail-resize.test.ts +284 -0
- package/src/ui.rebuild-composer.test.ts +21 -0
- package/src/ui.render-welcome.test.ts +42 -0
- package/src/ui.shortcuts.test.ts +166 -0
- package/src/ui.ts +3071 -240
- package/src/utils/__fixtures__/client-conversation-history-wire.json +108 -0
- package/src/utils/artifact-file.ts +15 -0
- package/src/utils/artifact-gate.test.ts +4 -0
- package/src/utils/artifact-gate.ts +13 -17
- package/src/utils/artifact-resize.test.ts +28 -0
- package/src/utils/artifact-resize.ts +9 -2
- package/src/utils/buttons.ts +188 -1
- package/src/utils/config-merge.test.ts +91 -0
- package/src/utils/dropdown.ts +10 -1
- package/src/utils/history-messages.test.ts +284 -0
- package/src/utils/history-messages.ts +284 -0
- package/src/utils/icons.ts +3 -0
- package/src/utils/shortcuts.test.ts +288 -0
- package/src/utils/shortcuts.ts +221 -0
- package/src/utils/theme.test.ts +501 -4
- package/src/utils/theme.ts +42 -3
- package/src/utils/tokens.ts +153 -15
- package/src/utils/tooltip-timing.ts +4 -0
- package/src/utils/tooltip.test.ts +249 -7
- package/src/utils/tooltip.ts +146 -4
- package/src/utils/visitor-store.test.ts +425 -0
- package/src/utils/visitor-store.ts +337 -0
- package/dist/chunk-QJ4LV2KW.js +0 -1
package/src/utils/tokens.ts
CHANGED
|
@@ -280,9 +280,10 @@ export const DEFAULT_COMPONENTS: ComponentTokens = {
|
|
|
280
280
|
},
|
|
281
281
|
},
|
|
282
282
|
launcher: {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
283
|
+
// Launcher pill role: shared surface/primary tones until a host overrides.
|
|
284
|
+
background: 'semantic.colors.surface',
|
|
285
|
+
foreground: 'semantic.colors.primary',
|
|
286
|
+
border: 'semantic.colors.border',
|
|
286
287
|
size: '60px',
|
|
287
288
|
iconSize: '28px',
|
|
288
289
|
borderRadius: 'palette.radius.full',
|
|
@@ -299,16 +300,14 @@ export const DEFAULT_COMPONENTS: ComponentTokens = {
|
|
|
299
300
|
canvasBackground: DEFAULT_PANEL_CANVAS_BACKGROUND,
|
|
300
301
|
},
|
|
301
302
|
header: {
|
|
302
|
-
// Header role: solid primary
|
|
303
|
+
// Header role: solid primary. Border, subtitle, and action icons stay
|
|
304
|
+
// unset so they derive from this background/foreground pair.
|
|
303
305
|
background: 'palette.colors.primary.500',
|
|
304
|
-
|
|
306
|
+
foreground: 'palette.colors.primary.50',
|
|
305
307
|
borderRadius: 'palette.radius.xl palette.radius.xl 0 0',
|
|
306
308
|
padding: 'semantic.spacing.md',
|
|
307
309
|
iconBackground: 'palette.colors.primary.600',
|
|
308
310
|
iconForeground: 'palette.colors.primary.50',
|
|
309
|
-
titleForeground: 'palette.colors.primary.50',
|
|
310
|
-
subtitleForeground: 'palette.colors.primary.200',
|
|
311
|
-
actionIconForeground: 'palette.colors.primary.200',
|
|
312
311
|
},
|
|
313
312
|
message: {
|
|
314
313
|
user: {
|
|
@@ -729,6 +728,9 @@ export function createTheme(
|
|
|
729
728
|
return theme;
|
|
730
729
|
}
|
|
731
730
|
|
|
731
|
+
/** Coerce a unitless "0" to "0px" for vars consumed inside length calc()s. */
|
|
732
|
+
const zeroLength = (value: string): string => (value.trim() === '0' ? '0px' : value);
|
|
733
|
+
|
|
732
734
|
export function themeToCssVariables(theme: PersonaTheme): Record<string, string> {
|
|
733
735
|
const resolved = resolveTokens(theme);
|
|
734
736
|
const cssVars: Record<string, string> = {};
|
|
@@ -787,10 +789,20 @@ export function themeToCssVariables(theme: PersonaTheme): Record<string, string>
|
|
|
787
789
|
'9999px';
|
|
788
790
|
cssVars['--persona-launcher-bg'] =
|
|
789
791
|
cssVars['--persona-components-launcher-background'] ??
|
|
790
|
-
cssVars['--persona-
|
|
792
|
+
cssVars['--persona-surface'];
|
|
791
793
|
cssVars['--persona-launcher-fg'] =
|
|
792
794
|
cssVars['--persona-components-launcher-foreground'] ??
|
|
793
|
-
cssVars['--persona-
|
|
795
|
+
cssVars['--persona-primary'];
|
|
796
|
+
// Subtitle keeps the shared muted tone while the foreground is stock (it
|
|
797
|
+
// resolves to primary); a custom foreground washes to 70% so both pill
|
|
798
|
+
// text lines recolor together. Compared by resolved value, not key
|
|
799
|
+
// presence: DEFAULT_COMPONENTS materializes launcher.foreground into
|
|
800
|
+
// every theme.
|
|
801
|
+
const launcherFg = cssVars['--persona-components-launcher-foreground'];
|
|
802
|
+
cssVars['--persona-launcher-fg-muted'] =
|
|
803
|
+
launcherFg && launcherFg !== cssVars['--persona-primary']
|
|
804
|
+
? `color-mix(in srgb, ${launcherFg} 70%, transparent)`
|
|
805
|
+
: cssVars['--persona-muted'];
|
|
794
806
|
cssVars['--persona-launcher-border'] =
|
|
795
807
|
cssVars['--persona-components-launcher-border'] ??
|
|
796
808
|
cssVars['--persona-border'];
|
|
@@ -853,8 +865,15 @@ export function themeToCssVariables(theme: PersonaTheme): Record<string, string>
|
|
|
853
865
|
// dedicated CSS variables that the widget CSS reads for individual elements.
|
|
854
866
|
cssVars['--persona-header-bg'] =
|
|
855
867
|
cssVars['--persona-components-header-background'] ?? cssVars['--persona-surface'];
|
|
868
|
+
// The background/foreground pair anchors every other header color: unset
|
|
869
|
+
// text and border keys mix the two rather than falling back to page chrome.
|
|
870
|
+
const headerBg = cssVars['--persona-header-bg'];
|
|
871
|
+
const headerFg = cssVars['--persona-components-header-foreground'];
|
|
872
|
+
const headerMutedFg = headerFg ? `color-mix(in srgb, ${headerFg} 72%, ${headerBg})` : undefined;
|
|
856
873
|
cssVars['--persona-header-border'] =
|
|
857
|
-
cssVars['--persona-components-header-border'] ??
|
|
874
|
+
cssVars['--persona-components-header-border'] ??
|
|
875
|
+
(headerFg ? `color-mix(in srgb, ${headerFg} 14%, ${headerBg})` : undefined) ??
|
|
876
|
+
cssVars['--persona-divider'];
|
|
858
877
|
cssVars['--persona-header-icon-bg'] =
|
|
859
878
|
cssVars['--persona-components-header-iconBackground'] ?? cssVars['--persona-primary'];
|
|
860
879
|
cssVars['--persona-header-icon-fg'] =
|
|
@@ -863,17 +882,133 @@ export function themeToCssVariables(theme: PersonaTheme): Record<string, string>
|
|
|
863
882
|
cssVars['--persona-header-title-fg'] =
|
|
864
883
|
cssVars['--persona-components-header-title-color'] ??
|
|
865
884
|
cssVars['--persona-components-header-titleForeground'] ??
|
|
885
|
+
headerFg ??
|
|
866
886
|
cssVars['--persona-primary'];
|
|
867
887
|
cssVars['--persona-header-subtitle-fg'] =
|
|
868
888
|
cssVars['--persona-components-header-subtitle-color'] ??
|
|
869
889
|
cssVars['--persona-components-header-subtitleForeground'] ??
|
|
890
|
+
headerMutedFg ??
|
|
870
891
|
cssVars['--persona-text-muted'];
|
|
871
892
|
cssVars['--persona-header-action-icon-fg'] =
|
|
872
|
-
cssVars['--persona-components-header-actionIconForeground'] ??
|
|
893
|
+
cssVars['--persona-components-header-actionIconForeground'] ??
|
|
894
|
+
headerMutedFg ??
|
|
895
|
+
cssVars['--persona-muted'];
|
|
896
|
+
// Unified header control box. Every header icon button reads these two from
|
|
897
|
+
// the stylesheet; per-control config keys stay inline and win. The third
|
|
898
|
+
// control token, `header.controlStrokeWidth`, needs no alias: widget.css
|
|
899
|
+
// reads its auto-emitted `--persona-components-header-controlStrokeWidth`
|
|
900
|
+
// directly and carries the 1.5 default in the var() fallback.
|
|
901
|
+
cssVars['--persona-header-control-size'] =
|
|
902
|
+
cssVars['--persona-components-header-controlSize'] ?? '32px';
|
|
903
|
+
cssVars['--persona-header-control-icon-size'] =
|
|
904
|
+
cssVars['--persona-components-header-controlIconSize'] ?? '20px';
|
|
873
905
|
|
|
874
906
|
const headerTokens = theme.components?.header;
|
|
875
907
|
if (headerTokens?.shadow) cssVars['--persona-header-shadow'] = headerTokens.shadow;
|
|
876
908
|
if (headerTokens?.borderBottom) cssVars['--persona-header-border-bottom'] = headerTokens.borderBottom;
|
|
909
|
+
// Conditional: the stylesheet carries `auto` / the rail's own fallback chain,
|
|
910
|
+
// so an unset token must leave the variable undefined.
|
|
911
|
+
if (headerTokens?.minHeight) cssVars['--persona-header-min-height'] = headerTokens.minHeight;
|
|
912
|
+
|
|
913
|
+
// Messages rail header strip. Same conditional rule: the chunk's CSS carries
|
|
914
|
+
// the surface/border/height fallback chain when a theme says nothing.
|
|
915
|
+
const railHeaderBg = cssVars['--persona-components-history-railHeader-background'];
|
|
916
|
+
if (railHeaderBg) cssVars['--persona-history-rail-header-bg'] = railHeaderBg;
|
|
917
|
+
const railHeaderBorder = cssVars['--persona-components-history-railHeader-border'];
|
|
918
|
+
if (railHeaderBorder) cssVars['--persona-history-rail-header-border'] = railHeaderBorder;
|
|
919
|
+
const railHeaderMinHeight = cssVars['--persona-components-history-railHeader-minHeight'];
|
|
920
|
+
if (railHeaderMinHeight)
|
|
921
|
+
cssVars['--persona-history-rail-header-min-height'] = railHeaderMinHeight;
|
|
922
|
+
|
|
923
|
+
// Floating (overlay-collapsed) rail. Same conditional rule: the host's
|
|
924
|
+
// inline styles carry every default in their var() fallback.
|
|
925
|
+
for (const [token, alias] of [
|
|
926
|
+
['margin', 'margin'],
|
|
927
|
+
['borderRadius', 'radius'],
|
|
928
|
+
['shadow', 'shadow'],
|
|
929
|
+
['background', 'bg'],
|
|
930
|
+
] as const) {
|
|
931
|
+
const value = cssVars[`--persona-components-history-overlay-${token}`];
|
|
932
|
+
if (value) cssVars[`--persona-history-overlay-${alias}`] = value;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// Row overflow menu. Same conditional rule: the chunk's CSS carries the
|
|
936
|
+
// elevated color-mix default in its var() fallback.
|
|
937
|
+
for (const [token, alias] of [
|
|
938
|
+
['background', 'bg'],
|
|
939
|
+
['borderRadius', 'radius'],
|
|
940
|
+
] as const) {
|
|
941
|
+
const value = cssVars[`--persona-components-history-menu-${token}`];
|
|
942
|
+
if (value) cssVars[`--persona-history-menu-${alias}`] = value;
|
|
943
|
+
}
|
|
944
|
+
// Messages enter/exit motion. Read straight off the theme: durations are
|
|
945
|
+
// authored as bare millisecond NUMBERS, and resolveTokens collects only
|
|
946
|
+
// string leaves, so these never reach the flattened vars. A unitless value
|
|
947
|
+
// gains "ms" for the CSS animation shorthand; 0 emits "0ms" (leg disabled).
|
|
948
|
+
const historyMotion = theme.components?.history?.motion;
|
|
949
|
+
if (historyMotion) {
|
|
950
|
+
const asTime = (value: number | string | undefined): string | undefined => {
|
|
951
|
+
if (value === undefined) return undefined;
|
|
952
|
+
const s = String(value).trim();
|
|
953
|
+
return /^\d+(\.\d+)?$/.test(s) ? `${s}ms` : s;
|
|
954
|
+
};
|
|
955
|
+
const enterMs = asTime(historyMotion.enterDurationMs);
|
|
956
|
+
if (enterMs !== undefined) cssVars['--persona-history-enter-ms'] = enterMs;
|
|
957
|
+
const exitMs = asTime(historyMotion.exitDurationMs);
|
|
958
|
+
if (exitMs !== undefined) cssVars['--persona-history-exit-ms'] = exitMs;
|
|
959
|
+
if (historyMotion.enterEasing)
|
|
960
|
+
cssVars['--persona-history-enter-easing'] = historyMotion.enterEasing;
|
|
961
|
+
if (historyMotion.exitEasing)
|
|
962
|
+
cssVars['--persona-history-exit-easing'] = historyMotion.exitEasing;
|
|
963
|
+
}
|
|
964
|
+
// Destructive action text. The chunk falls back to the light-surface
|
|
965
|
+
// error-600; the built-in dark theme sets this to a passing lighter red.
|
|
966
|
+
const historyDangerFg = cssVars['--persona-components-history-dangerForeground'];
|
|
967
|
+
if (historyDangerFg) cssVars['--persona-history-danger-fg'] = historyDangerFg;
|
|
968
|
+
// Confirmation dialog. The danger fill/label pair is always emitted so the
|
|
969
|
+
// dialog follows the error palette; scrim and shadow stay conditional with
|
|
970
|
+
// the dialog's inline fallbacks as the defaults.
|
|
971
|
+
cssVars['--persona-danger'] =
|
|
972
|
+
cssVars['--persona-components-history-confirm-dangerBackground'] ??
|
|
973
|
+
cssVars['--persona-palette-colors-error-700'] ??
|
|
974
|
+
'#b42318';
|
|
975
|
+
cssVars['--persona-danger-fg'] =
|
|
976
|
+
cssVars['--persona-components-history-confirm-dangerForeground'] ?? '#ffffff';
|
|
977
|
+
for (const token of ['scrim', 'shadow'] as const) {
|
|
978
|
+
const value = cssVars[`--persona-components-history-confirm-${token}`];
|
|
979
|
+
if (value) cssVars[`--persona-history-confirm-${token}`] = value;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// Portaled control tooltip. Same conditional rule: widget.css carries the
|
|
983
|
+
// built-in look in every var() fallback, so an unset token stays undefined.
|
|
984
|
+
for (const [token, alias] of [
|
|
985
|
+
['background', 'background'],
|
|
986
|
+
['foreground', 'foreground'],
|
|
987
|
+
['hintForeground', 'hint-fg'],
|
|
988
|
+
['borderRadius', 'radius'],
|
|
989
|
+
['fontSize', 'font-size'],
|
|
990
|
+
['padding', 'padding'],
|
|
991
|
+
['maxWidth', 'max-width'],
|
|
992
|
+
['shadow', 'shadow'],
|
|
993
|
+
]) {
|
|
994
|
+
const value = cssVars[`--persona-components-tooltip-${token}`];
|
|
995
|
+
if (value) cssVars[`--persona-tooltip-${alias}`] = value;
|
|
996
|
+
}
|
|
997
|
+
// Booleans never resolve into a token, so the arrow reads off the theme.
|
|
998
|
+
if (theme.components?.tooltip?.arrow === false)
|
|
999
|
+
cssVars['--persona-tooltip-arrow-display'] = 'none';
|
|
1000
|
+
|
|
1001
|
+
// Per-message action row (copy, vote, read aloud). Same conditional rule:
|
|
1002
|
+
// widget.css falls back to the scheme-aware ghost wash and semantic text, so
|
|
1003
|
+
// an unset token must leave the variable undefined.
|
|
1004
|
+
for (const [token, alias] of [
|
|
1005
|
+
['hoverBackground', 'hover-bg'],
|
|
1006
|
+
['hoverForeground', 'hover-fg'],
|
|
1007
|
+
['borderRadius', 'radius'],
|
|
1008
|
+
] as const) {
|
|
1009
|
+
const value = cssVars[`--persona-components-messageActions-${token}`];
|
|
1010
|
+
if (value) cssVars[`--persona-message-action-${alias}`] = value;
|
|
1011
|
+
}
|
|
877
1012
|
|
|
878
1013
|
// Intro card aliases: short names the panel inline-styles read directly.
|
|
879
1014
|
// The full-path `--persona-components-introCard-*` variables auto-emit above.
|
|
@@ -1102,16 +1237,19 @@ export function themeToCssVariables(theme: PersonaTheme): Record<string, string>
|
|
|
1102
1237
|
if (t.iconPadding) cssVars['--persona-artifact-toolbar-icon-padding'] = t.iconPadding;
|
|
1103
1238
|
if (t.iconBorderRadius) cssVars['--persona-artifact-toolbar-icon-radius'] = t.iconBorderRadius;
|
|
1104
1239
|
if (t.iconBorder) cssVars['--persona-artifact-toolbar-icon-border'] = t.iconBorder;
|
|
1105
|
-
|
|
1240
|
+
// A bare "0" is a <number> in calc(), not a <length>; gap/padding feed the
|
|
1241
|
+
// toggle thumb's width calc, so a unitless zero would zero out the thumb.
|
|
1242
|
+
if (t.toggleGroupGap) cssVars['--persona-artifact-toolbar-toggle-group-gap'] = zeroLength(t.toggleGroupGap);
|
|
1106
1243
|
if (t.toggleBorderRadius) cssVars['--persona-artifact-toolbar-toggle-radius'] = t.toggleBorderRadius;
|
|
1107
|
-
if (t.toggleGroupPadding) cssVars['--persona-artifact-toolbar-toggle-group-padding'] = t.toggleGroupPadding;
|
|
1244
|
+
if (t.toggleGroupPadding) cssVars['--persona-artifact-toolbar-toggle-group-padding'] = zeroLength(t.toggleGroupPadding);
|
|
1108
1245
|
if (t.toggleGroupBorder) cssVars['--persona-artifact-toolbar-toggle-group-border'] = t.toggleGroupBorder;
|
|
1109
1246
|
if (t.toggleGroupBorderRadius) cssVars['--persona-artifact-toolbar-toggle-group-radius'] = t.toggleGroupBorderRadius;
|
|
1110
1247
|
if (t.toggleGroupBackground) cssVars['--persona-artifact-toolbar-toggle-group-bg'] = resolveTokenValue(theme, t.toggleGroupBackground) ?? t.toggleGroupBackground;
|
|
1111
1248
|
if (t.copyBackground) cssVars['--persona-artifact-toolbar-copy-bg'] = t.copyBackground;
|
|
1112
1249
|
if (t.copyBorder) cssVars['--persona-artifact-toolbar-copy-border'] = t.copyBorder;
|
|
1113
1250
|
if (t.copyColor) cssVars['--persona-artifact-toolbar-copy-color'] = t.copyColor;
|
|
1114
|
-
|
|
1251
|
+
// Feeds the split-button halves' inner-radius calc; must stay a <length>.
|
|
1252
|
+
if (t.copyBorderRadius) cssVars['--persona-artifact-toolbar-copy-radius'] = zeroLength(t.copyBorderRadius);
|
|
1115
1253
|
if (t.copyPadding) cssVars['--persona-artifact-toolbar-copy-padding'] = t.copyPadding;
|
|
1116
1254
|
if (t.copyMenuBackground) {
|
|
1117
1255
|
cssVars['--persona-artifact-toolbar-copy-menu-bg'] = t.copyMenuBackground;
|
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
|
|
3
3
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
attachTooltip,
|
|
6
|
+
DEFAULT_TOOLTIP_DELAY_MS,
|
|
7
|
+
resetTooltipTiming,
|
|
8
|
+
} from "./tooltip";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Focus with a pinned keyboard-visible verdict. jsdom implements the spec's
|
|
12
|
+
* focus-visible propagation, so a bare focus() verdict depends on what ran
|
|
13
|
+
* before it; tests that open via focus must not be order-dependent.
|
|
14
|
+
*/
|
|
15
|
+
const focusVisible = (button: HTMLElement): void => {
|
|
16
|
+
vi.spyOn(button, "matches").mockImplementation(
|
|
17
|
+
(selector: string) => selector === ":focus-visible"
|
|
18
|
+
);
|
|
19
|
+
button.focus();
|
|
20
|
+
};
|
|
5
21
|
|
|
6
22
|
const rect = (
|
|
7
23
|
left: number,
|
|
@@ -98,7 +114,7 @@ describe("attachTooltip", () => {
|
|
|
98
114
|
);
|
|
99
115
|
|
|
100
116
|
attachTooltip({ anchor: button, text: "Close chat" });
|
|
101
|
-
button
|
|
117
|
+
focusVisible(button);
|
|
102
118
|
|
|
103
119
|
const tooltip = document.body.querySelector<HTMLElement>(
|
|
104
120
|
".persona-control-tooltip"
|
|
@@ -110,12 +126,26 @@ describe("attachTooltip", () => {
|
|
|
110
126
|
expect(document.body.querySelector(".persona-control-tooltip")).toBeNull();
|
|
111
127
|
});
|
|
112
128
|
|
|
113
|
-
it("
|
|
129
|
+
it("stays closed on focus that is not focus-visible", () => {
|
|
114
130
|
const button = document.createElement("button");
|
|
115
131
|
document.body.appendChild(button);
|
|
132
|
+
// jsdom reports every focus as :focus-visible; model a browser's
|
|
133
|
+
// programmatic-focus-after-mouse verdict instead.
|
|
134
|
+
vi.spyOn(button, "matches").mockImplementation(
|
|
135
|
+
(selector: string) => selector !== ":focus-visible"
|
|
136
|
+
);
|
|
116
137
|
|
|
117
138
|
attachTooltip({ anchor: button, text: "Close chat" });
|
|
118
139
|
button.focus();
|
|
140
|
+
expect(document.body.querySelector(".persona-control-tooltip")).toBeNull();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("dismisses an open tooltip with Escape", () => {
|
|
144
|
+
const button = document.createElement("button");
|
|
145
|
+
document.body.appendChild(button);
|
|
146
|
+
|
|
147
|
+
attachTooltip({ anchor: button, text: "Close chat" });
|
|
148
|
+
focusVisible(button);
|
|
119
149
|
expect(document.body.querySelector(".persona-control-tooltip")).not.toBeNull();
|
|
120
150
|
|
|
121
151
|
button.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }));
|
|
@@ -123,6 +153,47 @@ describe("attachTooltip", () => {
|
|
|
123
153
|
expect(document.body.querySelector(".persona-control-tooltip")).toBeNull();
|
|
124
154
|
});
|
|
125
155
|
|
|
156
|
+
it("dismisses on activation and reopens with the label the control now has", () => {
|
|
157
|
+
const button = document.createElement("button");
|
|
158
|
+
button.setAttribute("aria-label", "Collapse conversation list");
|
|
159
|
+
document.body.appendChild(button);
|
|
160
|
+
const tip = () =>
|
|
161
|
+
document.body.querySelector<HTMLElement>(".persona-control-tooltip");
|
|
162
|
+
|
|
163
|
+
attachTooltip({
|
|
164
|
+
anchor: button,
|
|
165
|
+
text: () => button.getAttribute("aria-label") ?? "",
|
|
166
|
+
});
|
|
167
|
+
button.dispatchEvent(new MouseEvent("mouseenter"));
|
|
168
|
+
expect(tip()?.textContent).toContain("Collapse conversation list");
|
|
169
|
+
|
|
170
|
+
// The pointer never left, so nothing but the activation closes it.
|
|
171
|
+
button.click();
|
|
172
|
+
expect(tip()).toBeNull();
|
|
173
|
+
|
|
174
|
+
button.setAttribute("aria-label", "Expand conversation list");
|
|
175
|
+
button.dispatchEvent(new MouseEvent("mouseenter"));
|
|
176
|
+
expect(tip()?.textContent).toContain("Expand conversation list");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("dismisses a keyboard-opened tooltip on activation until focus returns", () => {
|
|
180
|
+
const button = document.createElement("button");
|
|
181
|
+
document.body.appendChild(button);
|
|
182
|
+
const tip = () => document.body.querySelector(".persona-control-tooltip");
|
|
183
|
+
|
|
184
|
+
attachTooltip({ anchor: button, text: "Close chat" });
|
|
185
|
+
focusVisible(button);
|
|
186
|
+
expect(tip()).not.toBeNull();
|
|
187
|
+
|
|
188
|
+
// Space/Enter activates without moving focus off the control.
|
|
189
|
+
button.click();
|
|
190
|
+
expect(tip()).toBeNull();
|
|
191
|
+
|
|
192
|
+
button.blur();
|
|
193
|
+
focusVisible(button);
|
|
194
|
+
expect(tip()).not.toBeNull();
|
|
195
|
+
});
|
|
196
|
+
|
|
126
197
|
it("suppresses hover tooltips when the device has no hover capability", () => {
|
|
127
198
|
vi.stubGlobal(
|
|
128
199
|
"matchMedia",
|
|
@@ -166,7 +237,7 @@ describe("attachTooltip", () => {
|
|
|
166
237
|
);
|
|
167
238
|
|
|
168
239
|
attachTooltip({ anchor: button, text: "Add context" });
|
|
169
|
-
button
|
|
240
|
+
focusVisible(button);
|
|
170
241
|
const tooltip = document.body.querySelector<HTMLElement>(
|
|
171
242
|
".persona-control-tooltip"
|
|
172
243
|
);
|
|
@@ -186,7 +257,7 @@ describe("attachTooltip", () => {
|
|
|
186
257
|
document.body.appendChild(button);
|
|
187
258
|
|
|
188
259
|
attachTooltip({ anchor: button, text: "Add context" });
|
|
189
|
-
button
|
|
260
|
+
focusVisible(button);
|
|
190
261
|
expect(document.body.querySelector(".persona-control-tooltip")).not.toBeNull();
|
|
191
262
|
|
|
192
263
|
button.remove();
|
|
@@ -203,11 +274,101 @@ describe("attachTooltip", () => {
|
|
|
203
274
|
const next = attachTooltip({ anchor: button, text: "New", enabled: false });
|
|
204
275
|
|
|
205
276
|
expect(old.isOpen).toBe(false);
|
|
206
|
-
button
|
|
277
|
+
focusVisible(button);
|
|
207
278
|
expect(next.isOpen).toBe(false);
|
|
208
279
|
expect(document.body.querySelector(".persona-control-tooltip")).toBeNull();
|
|
209
280
|
});
|
|
210
281
|
|
|
282
|
+
it("renders a shortcut hint chip and refreshes it alongside the label", () => {
|
|
283
|
+
const button = document.createElement("button");
|
|
284
|
+
button.setAttribute("aria-label", "New conversation");
|
|
285
|
+
document.body.appendChild(button);
|
|
286
|
+
let hint = "⌘K";
|
|
287
|
+
|
|
288
|
+
attachTooltip({
|
|
289
|
+
anchor: button,
|
|
290
|
+
text: () => button.getAttribute("aria-label") ?? "",
|
|
291
|
+
hint: () => hint,
|
|
292
|
+
});
|
|
293
|
+
focusVisible(button);
|
|
294
|
+
|
|
295
|
+
const chip = () =>
|
|
296
|
+
document.body.querySelector<HTMLElement>(
|
|
297
|
+
".persona-control-tooltip__hint"
|
|
298
|
+
);
|
|
299
|
+
expect(chip()?.textContent).toBe("⌘K");
|
|
300
|
+
|
|
301
|
+
hint = "⌘⇧K";
|
|
302
|
+
window.dispatchEvent(new Event("resize"));
|
|
303
|
+
expect(chip()?.textContent).toBe("⌘⇧K");
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("renders no hint chip when the hint is absent or empty", () => {
|
|
307
|
+
const bare = document.createElement("button");
|
|
308
|
+
const blank = document.createElement("button");
|
|
309
|
+
document.body.append(bare, blank);
|
|
310
|
+
|
|
311
|
+
attachTooltip({ anchor: bare, text: "Close chat" });
|
|
312
|
+
focusVisible(bare);
|
|
313
|
+
expect(
|
|
314
|
+
document.body.querySelector(".persona-control-tooltip__hint")
|
|
315
|
+
).toBeNull();
|
|
316
|
+
bare.blur();
|
|
317
|
+
|
|
318
|
+
attachTooltip({ anchor: blank, text: "Close chat", hint: () => " " });
|
|
319
|
+
focusVisible(blank);
|
|
320
|
+
expect(
|
|
321
|
+
document.body.querySelector(".persona-control-tooltip__hint")
|
|
322
|
+
).toBeNull();
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("copies the anchor's resolved theme variables onto a body-portaled tooltip", () => {
|
|
326
|
+
const mount = document.createElement("div");
|
|
327
|
+
const button = document.createElement("button");
|
|
328
|
+
mount.appendChild(button);
|
|
329
|
+
document.body.appendChild(mount);
|
|
330
|
+
|
|
331
|
+
// The mount carries the theme; jsdom does not inherit custom properties,
|
|
332
|
+
// so the anchor's computed style is stubbed the way a browser resolves it.
|
|
333
|
+
vi.spyOn(window, "getComputedStyle").mockImplementation(
|
|
334
|
+
(element: Element) =>
|
|
335
|
+
({
|
|
336
|
+
fontFamily: element === button ? "Geist, sans-serif" : "serif",
|
|
337
|
+
getPropertyValue: (name: string) =>
|
|
338
|
+
element === button
|
|
339
|
+
? ({
|
|
340
|
+
"--persona-tooltip-background": "#1d4ed8",
|
|
341
|
+
"--persona-tooltip-hint-fg": "rgba(255,255,255,0.6)",
|
|
342
|
+
"--persona-tooltip-arrow-display": "none",
|
|
343
|
+
"--persona-radius-sm": "0.5rem",
|
|
344
|
+
})[name] ?? ""
|
|
345
|
+
: "",
|
|
346
|
+
}) as CSSStyleDeclaration
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
attachTooltip({ anchor: button, text: "Close chat", hint: "Esc" });
|
|
350
|
+
focusVisible(button);
|
|
351
|
+
|
|
352
|
+
const tooltip = document.body.querySelector<HTMLElement>(
|
|
353
|
+
".persona-control-tooltip"
|
|
354
|
+
);
|
|
355
|
+
expect(tooltip!.style.getPropertyValue("--persona-tooltip-background")).toBe(
|
|
356
|
+
"#1d4ed8"
|
|
357
|
+
);
|
|
358
|
+
expect(tooltip!.style.getPropertyValue("--persona-tooltip-hint-fg")).toBe(
|
|
359
|
+
"rgba(255,255,255,0.6)"
|
|
360
|
+
);
|
|
361
|
+
// arrow: false rides along, so a themed tooltip hides the caret off-mount.
|
|
362
|
+
expect(
|
|
363
|
+
tooltip!.style.getPropertyValue("--persona-tooltip-arrow-display")
|
|
364
|
+
).toBe("none");
|
|
365
|
+
expect(tooltip!.style.getPropertyValue("--persona-radius-sm")).toBe("0.5rem");
|
|
366
|
+
// The body portal inherits the page font; the anchor's family wins instead.
|
|
367
|
+
expect(tooltip!.style.fontFamily).toBe("Geist, sans-serif");
|
|
368
|
+
// Unresolved variables are never written as empty declarations.
|
|
369
|
+
expect(tooltip!.style.getPropertyValue("--persona-tooltip-padding")).toBe("");
|
|
370
|
+
});
|
|
371
|
+
|
|
211
372
|
it("portals into the anchor's shadow root so widget styles still apply", () => {
|
|
212
373
|
const host = document.createElement("div");
|
|
213
374
|
const shadow = host.attachShadow({ mode: "open" });
|
|
@@ -216,7 +377,7 @@ describe("attachTooltip", () => {
|
|
|
216
377
|
document.body.appendChild(host);
|
|
217
378
|
|
|
218
379
|
attachTooltip({ anchor: button, text: "Shadow tooltip" });
|
|
219
|
-
button
|
|
380
|
+
focusVisible(button);
|
|
220
381
|
|
|
221
382
|
expect(shadow.querySelector(".persona-control-tooltip")?.textContent).toContain(
|
|
222
383
|
"Shadow tooltip"
|
|
@@ -224,3 +385,84 @@ describe("attachTooltip", () => {
|
|
|
224
385
|
expect(document.body.querySelector(".persona-control-tooltip")).toBeNull();
|
|
225
386
|
});
|
|
226
387
|
});
|
|
388
|
+
|
|
389
|
+
describe("attachTooltip hover delay", () => {
|
|
390
|
+
afterEach(() => {
|
|
391
|
+
document.body.innerHTML = "";
|
|
392
|
+
vi.useRealTimers();
|
|
393
|
+
vi.restoreAllMocks();
|
|
394
|
+
resetTooltipTiming({ delayMs: 0, skipDelayMs: 0 });
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
const tip = () => document.body.querySelector(".persona-control-tooltip");
|
|
398
|
+
|
|
399
|
+
const hoverButton = (label: string): HTMLButtonElement => {
|
|
400
|
+
const button = document.createElement("button");
|
|
401
|
+
button.setAttribute("aria-label", label);
|
|
402
|
+
document.body.appendChild(button);
|
|
403
|
+
attachTooltip({
|
|
404
|
+
anchor: button,
|
|
405
|
+
text: () => button.getAttribute("aria-label") ?? "",
|
|
406
|
+
});
|
|
407
|
+
return button;
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
it("waits the product default before showing on hover", () => {
|
|
411
|
+
resetTooltipTiming();
|
|
412
|
+
vi.useFakeTimers();
|
|
413
|
+
const button = hoverButton("Close chat");
|
|
414
|
+
|
|
415
|
+
button.dispatchEvent(new MouseEvent("mouseenter"));
|
|
416
|
+
expect(tip()).toBeNull();
|
|
417
|
+
|
|
418
|
+
vi.advanceTimersByTime(DEFAULT_TOOLTIP_DELAY_MS - 1);
|
|
419
|
+
expect(tip()).toBeNull();
|
|
420
|
+
|
|
421
|
+
vi.advanceTimersByTime(1);
|
|
422
|
+
expect(tip()?.textContent).toContain("Close chat");
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it("cancels a pending hover if the pointer leaves before the delay", () => {
|
|
426
|
+
resetTooltipTiming();
|
|
427
|
+
vi.useFakeTimers();
|
|
428
|
+
const button = hoverButton("Close chat");
|
|
429
|
+
|
|
430
|
+
button.dispatchEvent(new MouseEvent("mouseenter"));
|
|
431
|
+
vi.advanceTimersByTime(100);
|
|
432
|
+
button.dispatchEvent(new MouseEvent("mouseleave"));
|
|
433
|
+
vi.advanceTimersByTime(DEFAULT_TOOLTIP_DELAY_MS);
|
|
434
|
+
expect(tip()).toBeNull();
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it("opens the next tooltip immediately inside the skip-delay window", () => {
|
|
438
|
+
resetTooltipTiming();
|
|
439
|
+
vi.useFakeTimers();
|
|
440
|
+
const first = hoverButton("New conversation");
|
|
441
|
+
const second = hoverButton("Close chat");
|
|
442
|
+
|
|
443
|
+
first.dispatchEvent(new MouseEvent("mouseenter"));
|
|
444
|
+
vi.advanceTimersByTime(DEFAULT_TOOLTIP_DELAY_MS);
|
|
445
|
+
expect(tip()?.textContent).toContain("New conversation");
|
|
446
|
+
|
|
447
|
+
first.dispatchEvent(new MouseEvent("mouseleave"));
|
|
448
|
+
expect(tip()).toBeNull();
|
|
449
|
+
|
|
450
|
+
second.dispatchEvent(new MouseEvent("mouseenter"));
|
|
451
|
+
expect(tip()?.textContent).toContain("Close chat");
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it("opens immediately on keyboard-visible focus even when hover is delayed", () => {
|
|
455
|
+
resetTooltipTiming();
|
|
456
|
+
const button = hoverButton("Close chat");
|
|
457
|
+
focusVisible(button);
|
|
458
|
+
expect(tip()?.textContent).toContain("Close chat");
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it("honors delayMs: 0 for instant hover", () => {
|
|
462
|
+
const button = document.createElement("button");
|
|
463
|
+
document.body.appendChild(button);
|
|
464
|
+
attachTooltip({ anchor: button, text: "Close chat", delayMs: 0 });
|
|
465
|
+
button.dispatchEvent(new MouseEvent("mouseenter"));
|
|
466
|
+
expect(tip()?.textContent).toContain("Close chat");
|
|
467
|
+
});
|
|
468
|
+
});
|