rift-ds 1.17.0 → 1.19.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/behaviors/useAnchoredPosition.d.ts +3 -1
- package/behaviors/useAnchoredPosition.js +1 -0
- package/behaviors/useTopLayer.d.ts +35 -0
- package/behaviors/useTopLayer.js +22 -0
- package/bin/templates/blocks/app/dashboard/page.module.css +4 -3
- package/bin/templates/blocks/app/dashboard/page.tsx +1 -1
- package/bin/templates/blocks/components/blocks/AppShell.module.css +50 -0
- package/bin/templates/blocks/components/blocks/AppShell.tsx +25 -6
- package/chat/ChatLauncher.js +23 -0
- package/chat/ChatThreads.d.ts +3 -1
- package/chat/ChatThreads.js +3 -1
- package/chat/ChatWidget.css +8 -3
- package/components/AiButton/AiButton.css +11 -0
- package/components/AiButton/AiButton.d.ts +2 -0
- package/components/AiButton/AiButton.js +3 -1
- package/components/AppSidebar/AppSidebar.css +142 -52
- package/components/AppSidebar/AppSidebar.d.ts +2 -2
- package/components/AppSidebar/AppSidebar.js +16 -9
- package/components/Chart/BarChart.js +1 -0
- package/components/Chart/Chart.css +6 -0
- package/components/Chart/ComboChart.js +1 -0
- package/components/Chart/StackedBarChart.js +1 -0
- package/components/CommandPaletteTrigger/CommandPaletteTrigger.css +13 -1
- package/components/Composer/Composer.css +72 -0
- package/components/Composer/Composer.d.ts +5 -1
- package/components/Composer/Composer.js +8 -1
- package/components/DataTable/DataTable.css +7 -3
- package/components/Popover/Popover.d.ts +2 -0
- package/components/Popover/Popover.js +2 -1
- package/components/SegmentedControl/SegmentedControl.css +3 -1
- package/components/SegmentedControl/SegmentedControl.js +2 -0
- package/components/SidebarPanel/SidebarPanel.css +10 -2
- package/components/SidebarPanel/SidebarPanel.d.ts +1 -1
- package/components/SidebarPanel/SidebarPanel.js +5 -2
- package/components/SiteHeader/SiteHeader.css +63 -5
- package/components/SiteHeader/SiteHeader.d.ts +11 -3
- package/components/SiteHeader/SiteHeader.js +51 -16
- package/components/SourceChip/SourceChip.css +5 -1
- package/components/SourceChip/SourceChip.js +3 -0
- package/components/Stat/Stat.css +57 -0
- package/components/Stat/Stat.d.ts +8 -2
- package/components/Stat/Stat.js +4 -2
- package/components/TableToolbar/TableToolbar.css +130 -1
- package/components/TableToolbar/TableToolbar.d.ts +52 -0
- package/components/TableToolbar/TableToolbar.js +232 -32
- package/components/ThemeToggle/ThemeToggle.css +26 -0
- package/components/ThemeToggle/ThemeToggle.d.ts +11 -1
- package/components/ThemeToggle/ThemeToggle.js +36 -5
- package/components/Toast/Toast.css +4 -0
- package/components/Toast/Toast.d.ts +9 -0
- package/components/Toast/Toast.js +9 -2
- package/components/Tooltip/Tooltip.css +9 -4
- package/components/Tooltip/Tooltip.d.ts +4 -2
- package/components/Tooltip/Tooltip.js +16 -4
- package/index.js +2 -1
- package/package.json +1 -1
- package/tokens/motion.d.ts +2 -0
- package/tokens/motion.js +2 -0
- package/tokens/tokens-light.css +4 -3
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* hover cards, tooltips).
|
|
5
5
|
*
|
|
6
6
|
* The panel stays where it is in the DOM, so it keeps the trigger's stacking
|
|
7
|
-
* context
|
|
7
|
+
* context (a tooltip excepted, which paints in the top layer through
|
|
8
|
+
* src/behaviors/useTopLayer) and any theme scoped above it, but is
|
|
9
|
+
* `position: fixed`, placed
|
|
8
10
|
* from the anchor's rectangle. A fixed panel is not clipped by an ancestor's
|
|
9
11
|
* `overflow`, which is what lets a menu inside a table's scroll wrapper, a
|
|
10
12
|
* rail or a card show whole. Portals would escape the same way but would
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { useRef, useLayoutEffect } from "react";
|
|
3
3
|
import { isRtl } from "./direction.js";
|
|
4
4
|
function fixedContainingBlock(element) {
|
|
5
|
+
if (element.matches(":popover-open")) return null;
|
|
5
6
|
for (let node = element.parentElement; node; node = node.parentElement) {
|
|
6
7
|
const style = getComputedStyle(node);
|
|
7
8
|
const read = (property) => style.getPropertyValue(property).trim();
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Lifts a panel into the browser's top layer while `open`, so it paints
|
|
4
|
+
* above everything on the page: every z-index, every stacking context, every
|
|
5
|
+
* `overflow` clip. Tooltips always sit here; no layer token can rank above
|
|
6
|
+
* the top layer, so nothing a page or a host does can cover one.
|
|
7
|
+
*
|
|
8
|
+
* The panel renders with `popover="manual"` and this hook calls
|
|
9
|
+
* `showPopover()` / `hidePopover()`. A panel that must stay in the
|
|
10
|
+
* accessibility tree at rest (SegmentedControl's segment labels, which are
|
|
11
|
+
* the segments' names) cannot carry the attribute, since a closed popover is
|
|
12
|
+
* `display: none`; the hook adds it for as long as the panel is shown and
|
|
13
|
+
* takes it off again. A manual popover has no light dismiss
|
|
14
|
+
* and closes no other popover, so dismissal stays with the shared layer
|
|
15
|
+
* stack (src/behaviors/useLayer). The panel keeps its place in the DOM, so
|
|
16
|
+
* it still inherits the custom properties of any theme scoped above it; only
|
|
17
|
+
* where it paints changes.
|
|
18
|
+
*
|
|
19
|
+
* Call it before useAnchoredPosition in the component: layout effects run in
|
|
20
|
+
* order, and the anchored hook must see the panel already in the top layer,
|
|
21
|
+
* where the viewport is its containing block whatever its ancestors
|
|
22
|
+
* transform (`fixedContainingBlock` returns null for an open popover).
|
|
23
|
+
*
|
|
24
|
+
* The panel's CSS adds `overlay` to its transition list with
|
|
25
|
+
* `allow-discrete`, so the exit fade still plays in the top layer, and sets
|
|
26
|
+
* `overflow: visible` over the popover default so an arrow outside the box
|
|
27
|
+
* still draws. A browser without the Popover API leaves the panel where it
|
|
28
|
+
* was, stacking at its layer token.
|
|
29
|
+
*/
|
|
30
|
+
export declare function useTopLayer({ open, panelRef, target, }: {
|
|
31
|
+
open: boolean;
|
|
32
|
+
panelRef: RefObject<HTMLElement | null>;
|
|
33
|
+
/** Changes when the ref is pointed at a different panel, as useAnchoredPosition's `target`. */
|
|
34
|
+
target?: unknown;
|
|
35
|
+
}): void;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useLayoutEffect } from "react";
|
|
3
|
+
function useTopLayer({
|
|
4
|
+
open,
|
|
5
|
+
panelRef,
|
|
6
|
+
target
|
|
7
|
+
}) {
|
|
8
|
+
useLayoutEffect(() => {
|
|
9
|
+
const panel = panelRef.current;
|
|
10
|
+
if (!open || !panel || typeof panel.showPopover !== "function") return;
|
|
11
|
+
const added = !panel.hasAttribute("popover");
|
|
12
|
+
if (added) panel.setAttribute("popover", "manual");
|
|
13
|
+
if (!panel.matches(":popover-open")) panel.showPopover();
|
|
14
|
+
return () => {
|
|
15
|
+
if (panel.isConnected && panel.matches(":popover-open")) panel.hidePopover();
|
|
16
|
+
if (added) panel.removeAttribute("popover");
|
|
17
|
+
};
|
|
18
|
+
}, [open, panelRef, target]);
|
|
19
|
+
}
|
|
20
|
+
export {
|
|
21
|
+
useTopLayer
|
|
22
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* The dashboard's grid. Four stats across on a desktop, two from the
|
|
2
|
-
laptop width down to a phone
|
|
3
|
-
|
|
2
|
+
laptop width down to a phone, where each stat drops its delta under its
|
|
3
|
+
label once its tile is narrow (deltaPlacement="auto"); the two panels sit
|
|
4
|
+
side by side until the tablet width, then stack. */
|
|
4
5
|
|
|
5
6
|
.stats {
|
|
6
7
|
display: grid;
|
|
@@ -81,7 +82,7 @@
|
|
|
81
82
|
|
|
82
83
|
@media (max-width: 600px) {
|
|
83
84
|
.stats {
|
|
84
|
-
gap: var(--gap-
|
|
85
|
+
gap: var(--gap-300);
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
.panels {
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
.content {
|
|
32
|
+
position: relative;
|
|
32
33
|
width: 100%;
|
|
33
34
|
max-width: 1440px;
|
|
34
35
|
min-width: 0;
|
|
@@ -40,12 +41,61 @@
|
|
|
40
41
|
|
|
41
42
|
/* ---------------------------------------------------------- top bar */
|
|
42
43
|
|
|
44
|
+
/* The bar holds one inset off the top of the page and the content runs
|
|
45
|
+
under it. Once the page scrolls (data-stuck), the page colour fills the
|
|
46
|
+
strip above it, fading out across the bar, and the blur behind it
|
|
47
|
+
dissolves a few rows below. */
|
|
43
48
|
.topBar {
|
|
49
|
+
position: sticky;
|
|
50
|
+
top: var(--shell-inset);
|
|
51
|
+
z-index: var(--layer-sticky);
|
|
44
52
|
display: flex;
|
|
45
53
|
align-items: center;
|
|
46
54
|
justify-content: space-between;
|
|
47
55
|
gap: var(--gap-200);
|
|
48
56
|
min-height: var(--control-height-default);
|
|
57
|
+
/* Room between the bar and the page header under it. */
|
|
58
|
+
margin-bottom: var(--gap-400);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.topBar::before {
|
|
62
|
+
content: '';
|
|
63
|
+
position: absolute;
|
|
64
|
+
top: calc(-1 * var(--shell-inset));
|
|
65
|
+
bottom: 0;
|
|
66
|
+
inset-inline: calc(-1 * var(--padding-400));
|
|
67
|
+
z-index: -1;
|
|
68
|
+
background: linear-gradient(to bottom, var(--color-bg-page-primary) var(--shell-inset), transparent);
|
|
69
|
+
pointer-events: none;
|
|
70
|
+
opacity: 0;
|
|
71
|
+
transition: opacity var(--motion-duration-base) var(--motion-ease-standard);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.topBar[data-stuck]::before {
|
|
75
|
+
opacity: 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.topBarBlur:global(.ds-progressive-blur) {
|
|
79
|
+
position: absolute;
|
|
80
|
+
top: calc(-1 * var(--shell-inset));
|
|
81
|
+
bottom: calc(-1 * var(--gap-1000));
|
|
82
|
+
inset-inline: calc(-1 * var(--padding-400));
|
|
83
|
+
height: auto;
|
|
84
|
+
z-index: -1;
|
|
85
|
+
opacity: 0;
|
|
86
|
+
transition: opacity var(--motion-duration-base) var(--motion-ease-standard);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.topBarBlur:global(.ds-progressive-blur--tinted) {
|
|
90
|
+
opacity: 1;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* A one-pixel marker at the bar's resting place. Absolute, so the
|
|
94
|
+
column's gap skips it; it keeps its static place at the column's top. */
|
|
95
|
+
.topBarMarker {
|
|
96
|
+
position: absolute;
|
|
97
|
+
width: 1px;
|
|
98
|
+
height: 1px;
|
|
49
99
|
}
|
|
50
100
|
|
|
51
101
|
.search {
|
|
@@ -7,13 +7,17 @@
|
|
|
7
7
|
* set. This file is yours: rename the product, change the navigation, and
|
|
8
8
|
* grow it with your app.
|
|
9
9
|
*
|
|
10
|
+
* The top bar sticks to the top of the page and the content scrolls under
|
|
11
|
+
* it: bare while the page is at the top, then a fill above it and the
|
|
12
|
+
* library's ProgressiveBlur behind it fade in once the page scrolls.
|
|
13
|
+
*
|
|
10
14
|
* Responsive by construction: above 768px the sidebar floats beside the
|
|
11
15
|
* page and the content moves over by its width; below it the sidebar
|
|
12
16
|
* becomes a drawer behind its own menu button, the search field folds to
|
|
13
17
|
* its icon, and the content runs edge to edge at a tighter gutter.
|
|
14
18
|
*/
|
|
15
19
|
|
|
16
|
-
import { useState, type ReactNode } from 'react';
|
|
20
|
+
import { useEffect, useRef, useState, type ReactNode } from 'react';
|
|
17
21
|
import { AiButton } from 'rift-ds/components/AiButton/AiButton';
|
|
18
22
|
import {
|
|
19
23
|
AppSidebar,
|
|
@@ -22,6 +26,7 @@ import {
|
|
|
22
26
|
} from 'rift-ds/components/AppSidebar/AppSidebar';
|
|
23
27
|
import { Breadcrumb, type BreadcrumbItem } from 'rift-ds/components/Breadcrumb/Breadcrumb';
|
|
24
28
|
import { CommandPaletteTrigger } from 'rift-ds/components/CommandPaletteTrigger/CommandPaletteTrigger';
|
|
29
|
+
import { ProgressiveBlur } from 'rift-ds/components/ProgressiveBlur/ProgressiveBlur';
|
|
25
30
|
import { ThemeToggle } from 'rift-ds/components/ThemeToggle/ThemeToggle';
|
|
26
31
|
import styles from './AppShell.module.css';
|
|
27
32
|
|
|
@@ -69,6 +74,18 @@ export interface AppShellProps {
|
|
|
69
74
|
export function AppShell({ activeKey, breadcrumb, title, actions, children }: AppShellProps) {
|
|
70
75
|
const [expanded, setExpanded] = useState(true);
|
|
71
76
|
|
|
77
|
+
// The top bar is stuck once the marker at its resting place scrolls out
|
|
78
|
+
// of view; the fill and the blur behind the bar follow.
|
|
79
|
+
const markerRef = useRef<HTMLDivElement>(null);
|
|
80
|
+
const [stuck, setStuck] = useState(false);
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
const marker = markerRef.current;
|
|
83
|
+
if (!marker) return;
|
|
84
|
+
const observer = new IntersectionObserver(([entry]) => setStuck(!entry.isIntersecting));
|
|
85
|
+
observer.observe(marker);
|
|
86
|
+
return () => observer.disconnect();
|
|
87
|
+
}, []);
|
|
88
|
+
|
|
72
89
|
return (
|
|
73
90
|
<div className={styles.shell}>
|
|
74
91
|
<AppSidebar
|
|
@@ -83,16 +100,18 @@ export function AppShell({ activeKey, breadcrumb, title, actions, children }: Ap
|
|
|
83
100
|
|
|
84
101
|
<div className={`${styles.main} ${expanded ? styles.mainExpanded : ''}`}>
|
|
85
102
|
<div className={styles.content}>
|
|
86
|
-
<
|
|
103
|
+
<div ref={markerRef} className={styles.topBarMarker} aria-hidden="true" />
|
|
104
|
+
<header className={styles.topBar} data-stuck={stuck ? '' : undefined}>
|
|
105
|
+
<ProgressiveBlur className={styles.topBarBlur} tinted={stuck} />
|
|
87
106
|
{/* Wire both to your command palette: CommandPalette from
|
|
88
107
|
rift-ds, opened from here and from Cmd+K. */}
|
|
89
|
-
<CommandPaletteTrigger
|
|
108
|
+
<CommandPaletteTrigger className={styles.search} />
|
|
90
109
|
<div className={styles.topBarActions}>
|
|
91
|
-
<CommandPaletteTrigger
|
|
110
|
+
<CommandPaletteTrigger iconOnly className={styles.searchIcon} />
|
|
92
111
|
{/* The AI entry. Point it at your chat: `npx rift-ds init chat`
|
|
93
112
|
scaffolds one, and ChatLauncher opens it. */}
|
|
94
|
-
<AiButton
|
|
95
|
-
<ThemeToggle
|
|
113
|
+
<AiButton />
|
|
114
|
+
<ThemeToggle showLabels={false} />
|
|
96
115
|
</div>
|
|
97
116
|
</header>
|
|
98
117
|
|
package/chat/ChatLauncher.js
CHANGED
|
@@ -82,6 +82,29 @@ function ChatLauncher({
|
|
|
82
82
|
}, [showPanel, isDocked, side, takeover, hostAttribute]);
|
|
83
83
|
useScrollLock(modal && !takeover);
|
|
84
84
|
const panelRef = useRef(null);
|
|
85
|
+
useLayoutEffect(() => {
|
|
86
|
+
const panel = panelRef.current;
|
|
87
|
+
const vv = window.visualViewport;
|
|
88
|
+
if (!showPanel || !takeover || !panel || !vv) return;
|
|
89
|
+
let frame = 0;
|
|
90
|
+
const sync = () => {
|
|
91
|
+
cancelAnimationFrame(frame);
|
|
92
|
+
frame = requestAnimationFrame(() => {
|
|
93
|
+
panel.style.setProperty("--ds-chat-viewport-height", `${vv.height}px`);
|
|
94
|
+
panel.style.setProperty("--ds-chat-viewport-top", `${vv.offsetTop}px`);
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
sync();
|
|
98
|
+
vv.addEventListener("resize", sync);
|
|
99
|
+
vv.addEventListener("scroll", sync);
|
|
100
|
+
return () => {
|
|
101
|
+
cancelAnimationFrame(frame);
|
|
102
|
+
vv.removeEventListener("resize", sync);
|
|
103
|
+
vv.removeEventListener("scroll", sync);
|
|
104
|
+
panel.style.removeProperty("--ds-chat-viewport-height");
|
|
105
|
+
panel.style.removeProperty("--ds-chat-viewport-top");
|
|
106
|
+
};
|
|
107
|
+
}, [showPanel, takeover]);
|
|
85
108
|
const wasOpen = useRef(false);
|
|
86
109
|
useEffect(() => {
|
|
87
110
|
if (wasOpen.current && !open) {
|
package/chat/ChatThreads.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface ChatThreadsProps {
|
|
|
11
11
|
logoText?: string;
|
|
12
12
|
/** The signed-in person, shown on the inline rail. */
|
|
13
13
|
profile?: ThreadPanelProfile;
|
|
14
|
+
/** Rendered in the rail's footer in both seats (above the profile row on the inline rail; the sheet has none): a theme toggle, a link out. Fades out while the inline rail is collapsed. */
|
|
15
|
+
footerSlot?: ReactNode;
|
|
14
16
|
/** Whether the inline rail can collapse to its icon strip. */
|
|
15
17
|
collapsible?: boolean;
|
|
16
18
|
/**
|
|
@@ -38,4 +40,4 @@ export interface ChatThreadsProps {
|
|
|
38
40
|
* thread lifecycle (`useThreads`). The widget seats it inline on a wide
|
|
39
41
|
* host and as a slide-over sheet on a narrow one, and tells it which.
|
|
40
42
|
*/
|
|
41
|
-
export declare function ChatThreads({ overlay, close, logo, logoText, profile, collapsible, defaultExpanded, historyLabel, threadPanelLabels, actionLabels, }: ChatThreadsProps): import("react").JSX.Element;
|
|
43
|
+
export declare function ChatThreads({ overlay, close, logo, logoText, profile, footerSlot, collapsible, defaultExpanded, historyLabel, threadPanelLabels, actionLabels, }: ChatThreadsProps): import("react").JSX.Element;
|
package/chat/ChatThreads.js
CHANGED
|
@@ -9,6 +9,7 @@ function ChatThreads({
|
|
|
9
9
|
logo,
|
|
10
10
|
logoText,
|
|
11
11
|
profile,
|
|
12
|
+
footerSlot,
|
|
12
13
|
collapsible = true,
|
|
13
14
|
defaultExpanded = false,
|
|
14
15
|
historyLabel = "Chat history",
|
|
@@ -65,7 +66,8 @@ function ChatThreads({
|
|
|
65
66
|
logoText: overlay ? logoText : void 0,
|
|
66
67
|
expanded: overlay || !collapsible ? true : expanded,
|
|
67
68
|
onExpandedChange: overlay || !collapsible ? void 0 : setExpanded,
|
|
68
|
-
profile: overlay ? void 0 : profile
|
|
69
|
+
profile: overlay ? void 0 : profile,
|
|
70
|
+
footerSlot
|
|
69
71
|
}
|
|
70
72
|
);
|
|
71
73
|
}
|
package/chat/ChatWidget.css
CHANGED
|
@@ -1207,13 +1207,18 @@ html[data-chat-resizing] .ds-chat-launcher__panel--floating .ds-chat-header {
|
|
|
1207
1207
|
way the browser handles any page with a field at the bottom. The launcher
|
|
1208
1208
|
reads the same threshold as TAKEOVER_QUERY; this rule covers the first
|
|
1209
1209
|
render, before a media query has been read, so the two must stay in step.
|
|
1210
|
-
No transition: there is no card form to glide from.
|
|
1210
|
+
No transition: there is no card form to glide from. Once open, the
|
|
1211
|
+
launcher writes the visual viewport's height and pan here, so the
|
|
1212
|
+
composer ends at the keyboard and the header stays on screen; 100dvh is
|
|
1213
|
+
the first paint's height and the fallback without visualViewport. */
|
|
1211
1214
|
@media (max-width: 719px) {
|
|
1212
1215
|
.ds-chat-launcher__panel {
|
|
1213
1216
|
position: relative;
|
|
1214
|
-
|
|
1217
|
+
/* A relative offset, not a transform: a transform would become the
|
|
1218
|
+
containing block of every fixed popup inside the panel. */
|
|
1219
|
+
top: var(--ds-chat-viewport-top, 0px);
|
|
1215
1220
|
left: auto;
|
|
1216
|
-
height: 100dvh;
|
|
1221
|
+
height: var(--ds-chat-viewport-height, 100dvh);
|
|
1217
1222
|
width: 100%;
|
|
1218
1223
|
max-width: none;
|
|
1219
1224
|
border: none;
|
|
@@ -279,6 +279,7 @@
|
|
|
279
279
|
focus outline is its signal, as on Button, and a widened ring inside it
|
|
280
280
|
would read as two concentric strokes. */
|
|
281
281
|
.ds-ai-button:hover::before,
|
|
282
|
+
.ds-ai-button[aria-expanded='true']::before,
|
|
282
283
|
.ds-ai-button--busy::before,
|
|
283
284
|
.ds-ai-button--done::before,
|
|
284
285
|
.ds-ai-button--error::before {
|
|
@@ -287,6 +288,7 @@
|
|
|
287
288
|
}
|
|
288
289
|
|
|
289
290
|
.ds-ai-button:hover::after,
|
|
291
|
+
.ds-ai-button[aria-expanded='true']::after,
|
|
290
292
|
.ds-ai-button--busy::after,
|
|
291
293
|
.ds-ai-button--done::after,
|
|
292
294
|
.ds-ai-button--error::after {
|
|
@@ -339,6 +341,15 @@
|
|
|
339
341
|
--icon-size: var(--icon-size-500);
|
|
340
342
|
}
|
|
341
343
|
|
|
344
|
+
.ds-ai-button--icon-only {
|
|
345
|
+
width: var(--control-height-default);
|
|
346
|
+
padding: 0;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.ds-ai-button--compact.ds-ai-button--icon-only {
|
|
350
|
+
width: var(--control-height-compact);
|
|
351
|
+
}
|
|
352
|
+
|
|
342
353
|
/* Reduced motion: the ring and glow stay, the turning stops. */
|
|
343
354
|
/* ds-allow(motion): parks a looping animation, which the collapsed duration would strobe rather than stop (the ring's turn and the busy spin), and drops the status icon's entrance. */
|
|
344
355
|
@media (prefers-reduced-motion: reduce) {
|
|
@@ -21,6 +21,8 @@ type AiButtonOwnProps = {
|
|
|
21
21
|
label?: string;
|
|
22
22
|
/** Leading icon — Material Symbol name (string) or custom element (ReactNode) */
|
|
23
23
|
icon?: string | React.ReactNode;
|
|
24
|
+
/** Draws the button as a circle holding only its icon, for a tight bar. The label stays as its accessible name. */
|
|
25
|
+
iconOnly?: boolean;
|
|
24
26
|
/** Button size */
|
|
25
27
|
size?: 'default' | 'compact';
|
|
26
28
|
/** Whether the button is disabled */
|
|
@@ -26,6 +26,7 @@ const AiButton = React.forwardRef(
|
|
|
26
26
|
({
|
|
27
27
|
label = "Ask AI",
|
|
28
28
|
icon = "auto_awesome",
|
|
29
|
+
iconOnly = false,
|
|
29
30
|
size = "default",
|
|
30
31
|
disabled,
|
|
31
32
|
state: status,
|
|
@@ -164,6 +165,7 @@ const AiButton = React.forwardRef(
|
|
|
164
165
|
}
|
|
165
166
|
};
|
|
166
167
|
const classes = [
|
|
168
|
+
iconOnly ? `${baseClass}--icon-only` : "",
|
|
167
169
|
baseClass,
|
|
168
170
|
`${baseClass}--${size}`,
|
|
169
171
|
disabled && `${baseClass}--disabled`,
|
|
@@ -183,7 +185,7 @@ const AiButton = React.forwardRef(
|
|
|
183
185
|
},
|
|
184
186
|
shownStatus
|
|
185
187
|
) }),
|
|
186
|
-
/* @__PURE__ */ jsx("span", { className: `${baseClass}__text`, children: label })
|
|
188
|
+
/* @__PURE__ */ jsx("span", { className: iconOnly ? "ds-visually-hidden" : `${baseClass}__text`, children: label })
|
|
187
189
|
] });
|
|
188
190
|
const control = href && !disabled ? /* @__PURE__ */ jsx(
|
|
189
191
|
Link,
|