rift-ds 1.16.0 → 1.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/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 +26 -7
- package/chat/ChatLauncher.d.ts +11 -1
- package/chat/ChatLauncher.js +7 -1
- package/chat/ChatThreads.d.ts +3 -1
- package/chat/ChatThreads.js +3 -1
- package/chat/ChatWidget.css +21 -0
- package/components/AppLayout/AppLayout.css +90 -2
- package/components/AppLayout/AppLayout.d.ts +10 -2
- package/components/AppLayout/AppLayout.js +20 -2
- package/components/AppSidebar/AppSidebar.css +104 -38
- package/components/AppSidebar/AppSidebar.d.ts +15 -4
- package/components/AppSidebar/AppSidebar.js +8 -2
- package/components/DataTable/DataTable.js +29 -9
- package/components/DataTable/FilterEditor.js +1 -2
- package/components/DataTable/filterModel.js +4 -3
- package/components/DataTable/views.d.ts +9 -0
- package/components/DataTable/views.js +10 -1
- package/components/SegmentedControl/SegmentedControl.css +69 -0
- package/components/SegmentedControl/SegmentedControl.d.ts +4 -3
- package/components/SegmentedControl/SegmentedControl.js +90 -48
- 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 +6 -0
- package/components/TableToolbar/TableToolbar.js +52 -16
- package/components/ThemeToggle/ThemeToggle.css +2 -72
- package/components/ThemeToggle/ThemeToggle.d.ts +1 -1
- package/components/ThemeToggle/ThemeToggle.js +4 -40
- package/components/registry.json +1 -1
- package/components/registry.json.d.ts +1 -1
- package/components/registry.json.js +1 -1
- package/package.json +1 -1
- package/tokens/registry.json +2 -0
- package/tokens/registry.json.d.ts +2 -0
- package/tokens/registry.json.js +1 -1
- package/tokens/tokens-dark.css +7 -0
- package/tokens/tokens-light.css +7 -0
|
@@ -5,6 +5,7 @@ import "./SegmentedControl.css";
|
|
|
5
5
|
import "../../behaviors/visually-hidden.css";
|
|
6
6
|
import "../../fonts/material-symbols.css";
|
|
7
7
|
import { mirrorClass, logicalArrowKey } from "../../behaviors/direction.js";
|
|
8
|
+
import { useAnchoredPosition } from "../../behaviors/useAnchoredPosition.js";
|
|
8
9
|
const SegmentedControl = React.forwardRef(
|
|
9
10
|
({
|
|
10
11
|
segments,
|
|
@@ -70,6 +71,7 @@ const SegmentedControl = React.forwardRef(
|
|
|
70
71
|
variantClass,
|
|
71
72
|
fullWidthClass,
|
|
72
73
|
pillReady ? `${baseClass}--pill-ready` : "",
|
|
74
|
+
!showLabels ? `${baseClass}--icons` : "",
|
|
73
75
|
className
|
|
74
76
|
].filter(Boolean).join(" ");
|
|
75
77
|
const positionPill = useCallback(
|
|
@@ -182,6 +184,28 @@ const SegmentedControl = React.forwardRef(
|
|
|
182
184
|
}
|
|
183
185
|
};
|
|
184
186
|
}, []);
|
|
187
|
+
const tipSegmentRef = useRef(null);
|
|
188
|
+
const tipLabelRef = useRef(null);
|
|
189
|
+
const [tip, setTip] = useState(null);
|
|
190
|
+
const showTip = (target) => {
|
|
191
|
+
if (showLabels) return;
|
|
192
|
+
const segment = target?.closest(`.${baseClass}__segment`);
|
|
193
|
+
const strip = containerRef.current;
|
|
194
|
+
const label = segment?.querySelector(`.${baseClass}__label--tip`);
|
|
195
|
+
if (!segment || !strip || !label) return;
|
|
196
|
+
tipSegmentRef.current = segment;
|
|
197
|
+
tipLabelRef.current = label;
|
|
198
|
+
setTip([...strip.querySelectorAll(`.${baseClass}__segment`)].indexOf(segment));
|
|
199
|
+
};
|
|
200
|
+
useAnchoredPosition({
|
|
201
|
+
open: tip !== null,
|
|
202
|
+
target: tip,
|
|
203
|
+
anchorRef: tipSegmentRef,
|
|
204
|
+
panelRef: tipLabelRef,
|
|
205
|
+
side: "bottom",
|
|
206
|
+
align: "center",
|
|
207
|
+
gapProperty: "--gap-100"
|
|
208
|
+
});
|
|
185
209
|
const handleKeyDown = (e, idx) => {
|
|
186
210
|
const enabledSegments = segments.filter((s) => !s.disabled);
|
|
187
211
|
const currentEnabledIdx = enabledSegments.findIndex((s) => s.value === segments[idx].value);
|
|
@@ -213,54 +237,72 @@ const SegmentedControl = React.forwardRef(
|
|
|
213
237
|
});
|
|
214
238
|
}
|
|
215
239
|
};
|
|
216
|
-
return /* @__PURE__ */ jsxs(
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
"
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
240
|
+
return /* @__PURE__ */ jsxs(
|
|
241
|
+
"div",
|
|
242
|
+
{
|
|
243
|
+
"aria-label": ariaLabel,
|
|
244
|
+
...rest,
|
|
245
|
+
ref: setRef,
|
|
246
|
+
className: classes,
|
|
247
|
+
role: "tablist",
|
|
248
|
+
onPointerOver: (event) => {
|
|
249
|
+
rest.onPointerOver?.(event);
|
|
250
|
+
showTip(event.target);
|
|
251
|
+
},
|
|
252
|
+
onFocus: (event) => {
|
|
253
|
+
rest.onFocus?.(event);
|
|
254
|
+
showTip(event.target);
|
|
255
|
+
},
|
|
256
|
+
children: [
|
|
257
|
+
/* @__PURE__ */ jsx("span", { className: `${baseClass}__pill`, "aria-hidden": "true", ref: pillRef }),
|
|
258
|
+
segments.map((segment, idx) => {
|
|
259
|
+
const isActive = segment.value === activeSegment;
|
|
260
|
+
const btnClasses = [
|
|
261
|
+
`${baseClass}__segment`,
|
|
262
|
+
isActive ? `${baseClass}__segment--active` : "",
|
|
263
|
+
segment.disabled ? `${baseClass}__segment--disabled` : ""
|
|
264
|
+
].filter(Boolean).join(" ");
|
|
265
|
+
return /* @__PURE__ */ jsxs(
|
|
266
|
+
"button",
|
|
267
|
+
{
|
|
268
|
+
type: "button",
|
|
269
|
+
role: "tab",
|
|
270
|
+
className: btnClasses,
|
|
271
|
+
"aria-selected": isActive,
|
|
272
|
+
"aria-disabled": segment.disabled || void 0,
|
|
273
|
+
disabled: segment.disabled,
|
|
274
|
+
tabIndex: isActive ? 0 : -1,
|
|
275
|
+
"data-segment-value": segment.value,
|
|
276
|
+
onClick: () => {
|
|
277
|
+
if (!segment.disabled) {
|
|
278
|
+
select(segment.value);
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
onKeyDown: (e) => handleKeyDown(e, idx),
|
|
282
|
+
children: [
|
|
283
|
+
segment.icon && collapseMode !== "labels" && /* @__PURE__ */ jsx(
|
|
284
|
+
"span",
|
|
285
|
+
{
|
|
286
|
+
className: `${baseClass}__icon${typeof segment.icon === "string" ? ` material-symbols-rounded ${mirrorClass(segment.icon)}` : ""}`,
|
|
287
|
+
"aria-hidden": "true",
|
|
288
|
+
children: segment.icon
|
|
289
|
+
}
|
|
290
|
+
),
|
|
291
|
+
/* @__PURE__ */ jsx(
|
|
292
|
+
"span",
|
|
293
|
+
{
|
|
294
|
+
className: `${baseClass}__label${!segment.icon ? "" : !showLabels ? ` ${baseClass}__label--tip` : collapseMode === "icons" ? ` ${baseClass}__label--hidden ds-visually-hidden` : ""}`,
|
|
295
|
+
children: segment.label
|
|
296
|
+
}
|
|
297
|
+
)
|
|
298
|
+
]
|
|
299
|
+
},
|
|
300
|
+
segment.value
|
|
301
|
+
);
|
|
302
|
+
})
|
|
303
|
+
]
|
|
304
|
+
}
|
|
305
|
+
);
|
|
264
306
|
}
|
|
265
307
|
);
|
|
266
308
|
SegmentedControl.displayName = "SegmentedControl";
|
package/components/Stat/Stat.css
CHANGED
|
@@ -111,6 +111,63 @@
|
|
|
111
111
|
margin-bottom: var(--gap-100);
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/* ============================================
|
|
115
|
+
AUTO DELTA — inline while the stat has room,
|
|
116
|
+
stacked under the label once it is narrow.
|
|
117
|
+
The root fills its parent and is the size
|
|
118
|
+
container; the body inside it re-templates.
|
|
119
|
+
============================================ */
|
|
120
|
+
|
|
121
|
+
.ds-stat--delta-auto {
|
|
122
|
+
display: block;
|
|
123
|
+
width: 100%;
|
|
124
|
+
min-width: 0;
|
|
125
|
+
container: ds-stat / inline-size;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.ds-stat__body {
|
|
129
|
+
display: grid;
|
|
130
|
+
grid-template-columns: auto 1fr;
|
|
131
|
+
grid-template-areas:
|
|
132
|
+
'value delta'
|
|
133
|
+
'label label';
|
|
134
|
+
justify-items: start;
|
|
135
|
+
column-gap: var(--gap-200);
|
|
136
|
+
row-gap: var(--gap-050);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.ds-stat__body .ds-stat__value {
|
|
140
|
+
grid-area: value;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.ds-stat__body .ds-stat__label {
|
|
144
|
+
grid-area: label;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.ds-stat__body .ds-stat__delta {
|
|
148
|
+
grid-area: delta;
|
|
149
|
+
align-self: end;
|
|
150
|
+
/* The inline placement's optical baseline. */
|
|
151
|
+
margin-bottom: var(--gap-100);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* Too narrow for the value and its delta side by side: the delta drops
|
|
155
|
+
under the label, the stacked placement's order. */
|
|
156
|
+
@container ds-stat (max-width: 180px) {
|
|
157
|
+
.ds-stat__body {
|
|
158
|
+
grid-template-columns: minmax(0, 1fr);
|
|
159
|
+
grid-template-areas:
|
|
160
|
+
'value'
|
|
161
|
+
'label'
|
|
162
|
+
'delta';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.ds-stat__body .ds-stat__delta {
|
|
166
|
+
align-self: start;
|
|
167
|
+
margin-bottom: 0;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
114
171
|
/* Icons take the colour of the text around them. Explicit, not just
|
|
115
172
|
inherited by default: a consumer stylesheet that sets a colour on
|
|
116
173
|
.material-symbols-rounded itself (a common global default) would
|
|
@@ -9,8 +9,14 @@ type StatOwnProps = {
|
|
|
9
9
|
delta?: string;
|
|
10
10
|
/** Direction of the delta — colours it and adds an arrow */
|
|
11
11
|
trend?: 'up' | 'down' | 'neutral';
|
|
12
|
-
/**
|
|
13
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Where the delta sits: stacked below the label, inline to the right of the
|
|
14
|
+
* value (bottom-aligned), or `auto`: inline while the stat has room and
|
|
15
|
+
* stacked once it is narrow, such as a KPI tile in a two-up phone grid.
|
|
16
|
+
* `auto` fills its parent's width and reads its own, so it restacks
|
|
17
|
+
* wherever it sits, whatever the viewport.
|
|
18
|
+
*/
|
|
19
|
+
deltaPlacement?: 'stacked' | 'inline' | 'auto';
|
|
14
20
|
/** Stat size */
|
|
15
21
|
size?: 'default' | 'large';
|
|
16
22
|
/** The BCP 47 locale a number `value` is written in, such as `ar-EG`. Defaults to `en-US`. This component can render from a Server Component, so it takes the prop alone and reads no LocaleProvider. */
|
package/components/Stat/Stat.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsxs, jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsxs, Fragment, jsx } from "react/jsx-runtime";
|
|
2
2
|
import React from "react";
|
|
3
3
|
import "./Stat.css";
|
|
4
4
|
import "../../fonts/material-symbols.css";
|
|
@@ -25,9 +25,10 @@ const Stat = React.forwardRef(
|
|
|
25
25
|
baseClass,
|
|
26
26
|
`${baseClass}--${size}`,
|
|
27
27
|
deltaPlacement === "inline" ? `${baseClass}--delta-inline` : "",
|
|
28
|
+
deltaPlacement === "auto" ? `${baseClass}--delta-auto` : "",
|
|
28
29
|
className
|
|
29
30
|
].filter(Boolean).join(" ");
|
|
30
|
-
|
|
31
|
+
const parts = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
31
32
|
/* @__PURE__ */ jsx("span", { className: `${baseClass}__value`, children: typeof value === "number" ? formatNumber(value, locale) : value }),
|
|
32
33
|
/* @__PURE__ */ jsx("span", { className: `${baseClass}__label`, children: label }),
|
|
33
34
|
delta && /* @__PURE__ */ jsxs("span", { className: `${baseClass}__delta ${baseClass}__delta--${trend}`, children: [
|
|
@@ -35,6 +36,7 @@ const Stat = React.forwardRef(
|
|
|
35
36
|
delta
|
|
36
37
|
] })
|
|
37
38
|
] });
|
|
39
|
+
return /* @__PURE__ */ jsx("div", { ...rest, ref, className: classes, children: deltaPlacement === "auto" ? /* @__PURE__ */ jsx("span", { className: `${baseClass}__body`, children: parts }) : parts });
|
|
38
40
|
}
|
|
39
41
|
);
|
|
40
42
|
Stat.displayName = "Stat";
|
|
@@ -393,6 +393,12 @@
|
|
|
393
393
|
min-width: 0;
|
|
394
394
|
}
|
|
395
395
|
|
|
396
|
+
/* The count and Select all N never give up width: the actions' fit is
|
|
397
|
+
measured against what they need, and what does not fit folds into More. */
|
|
398
|
+
.ds-table-toolbar__bulk-start {
|
|
399
|
+
flex-shrink: 0;
|
|
400
|
+
}
|
|
401
|
+
|
|
396
402
|
/* Actions that do not fit stay laid out out of sight, so they can be measured. */
|
|
397
403
|
.ds-table-toolbar__bulk-end {
|
|
398
404
|
position: relative;
|
|
@@ -408,12 +408,15 @@ const TableToolbar = React.forwardRef(
|
|
|
408
408
|
}, [narrow, filterIds, size]);
|
|
409
409
|
const bulkRef = useRef(null);
|
|
410
410
|
const bulkId = useId();
|
|
411
|
-
const [actionFit, setActionFit] = useState(
|
|
411
|
+
const [actionFit, setActionFit] = useState({
|
|
412
|
+
icons: false,
|
|
413
|
+
shown: Number.MAX_SAFE_INTEGER
|
|
414
|
+
});
|
|
412
415
|
const bulkOn = Boolean(table?.selectable && table.selectedIds.length > 0);
|
|
413
416
|
const actionIds = bulkActions.map((action) => action.id + action.label).join("|");
|
|
414
417
|
useLayoutEffect(() => {
|
|
415
418
|
const bar = bulkRef.current;
|
|
416
|
-
if (!bar ||
|
|
419
|
+
if (!bar || !bulkOn) return;
|
|
417
420
|
const measure = () => {
|
|
418
421
|
const gap = parseFloat(getComputedStyle(bar).columnGap) || 0;
|
|
419
422
|
const start = bar.querySelector(".ds-table-toolbar__bulk-start");
|
|
@@ -422,17 +425,25 @@ const TableToolbar = React.forwardRef(
|
|
|
422
425
|
const endGap = parseFloat(getComputedStyle(end).columnGap) || 0;
|
|
423
426
|
const room = bar.clientWidth - start.getBoundingClientRect().width - gap;
|
|
424
427
|
const buttons = [...end.querySelectorAll(":scope > [data-action]")];
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
let
|
|
428
|
-
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
428
|
+
const square = bar.getBoundingClientRect().height;
|
|
429
|
+
const full = buttons.reduce((sum, button, i) => sum + button.getBoundingClientRect().width + (i > 0 ? endGap : 0), 0);
|
|
430
|
+
let next = { icons: false, shown: buttons.length };
|
|
431
|
+
if (narrow || full > room) {
|
|
432
|
+
const iconable = buttons.filter((button) => button.hasAttribute("data-iconable")).length;
|
|
433
|
+
let used = 0;
|
|
434
|
+
let n = 0;
|
|
435
|
+
for (let i = 0; i < iconable; i += 1) {
|
|
436
|
+
const width = square + (i > 0 ? endGap : 0);
|
|
437
|
+
const reserve = i < buttons.length - 1 ? square + endGap : 0;
|
|
438
|
+
if (used + width + reserve > room) break;
|
|
439
|
+
used += width;
|
|
440
|
+
n += 1;
|
|
441
|
+
}
|
|
442
|
+
next = { icons: true, shown: n };
|
|
434
443
|
}
|
|
435
|
-
setActionFit(
|
|
444
|
+
setActionFit(
|
|
445
|
+
(previous) => previous.icons === next.icons && previous.shown === next.shown ? previous : next
|
|
446
|
+
);
|
|
436
447
|
};
|
|
437
448
|
measure();
|
|
438
449
|
const observer = new ResizeObserver(measure);
|
|
@@ -460,14 +471,19 @@ const TableToolbar = React.forwardRef(
|
|
|
460
471
|
...bulkActions.filter((action) => !action.destructive),
|
|
461
472
|
...bulkActions.filter((action) => action.destructive)
|
|
462
473
|
];
|
|
463
|
-
const
|
|
464
|
-
|
|
474
|
+
const iconFirst = [
|
|
475
|
+
...orderedActions.filter((action) => action.icon != null),
|
|
476
|
+
...orderedActions.filter((action) => action.icon == null)
|
|
477
|
+
];
|
|
478
|
+
const shownActions = Math.min(actionFit.shown, orderedActions.length);
|
|
479
|
+
const folded = (actionFit.icons ? iconFirst : orderedActions).slice(shownActions);
|
|
465
480
|
const bulkButton = (action, index) => {
|
|
466
|
-
const out = index >= shownActions;
|
|
481
|
+
const out = actionFit.icons || index >= shownActions;
|
|
467
482
|
return /* @__PURE__ */ jsx(
|
|
468
483
|
Button,
|
|
469
484
|
{
|
|
470
485
|
"data-action": "",
|
|
486
|
+
"data-iconable": action.icon != null || void 0,
|
|
471
487
|
className: out ? "ds-table-toolbar__chip--overflow" : void 0,
|
|
472
488
|
"aria-hidden": out || void 0,
|
|
473
489
|
inert: out || void 0,
|
|
@@ -523,7 +539,21 @@ const TableToolbar = React.forwardRef(
|
|
|
523
539
|
)
|
|
524
540
|
] }),
|
|
525
541
|
/* @__PURE__ */ jsxs("div", { className: "ds-table-toolbar__bulk-end", children: [
|
|
526
|
-
|
|
542
|
+
orderedActions.map(bulkButton),
|
|
543
|
+
actionFit.icons && iconFirst.slice(0, shownActions).map((action) => /* @__PURE__ */ jsx(
|
|
544
|
+
CircularButton,
|
|
545
|
+
{
|
|
546
|
+
icon: action.icon,
|
|
547
|
+
variant: "secondary",
|
|
548
|
+
tone: action.destructive ? "destructive" : "neutral",
|
|
549
|
+
size,
|
|
550
|
+
ariaLabel: action.label,
|
|
551
|
+
tooltip: action.label,
|
|
552
|
+
disabled: action.disabled,
|
|
553
|
+
onClick: () => action.onSelect(selection)
|
|
554
|
+
},
|
|
555
|
+
action.id
|
|
556
|
+
)),
|
|
527
557
|
folded.length > 0 && /* @__PURE__ */ jsx(
|
|
528
558
|
DropdownMenu,
|
|
529
559
|
{
|
|
@@ -902,6 +932,12 @@ const TableToolbar = React.forwardRef(
|
|
|
902
932
|
);
|
|
903
933
|
const active = views.find((v) => v.id === activeViewId) ?? views[0];
|
|
904
934
|
const dirty = Boolean(table && active && !table.viewMatches(active.view));
|
|
935
|
+
const appliedStart = useRef(false);
|
|
936
|
+
useEffect(() => {
|
|
937
|
+
if (appliedStart.current || !table) return;
|
|
938
|
+
appliedStart.current = true;
|
|
939
|
+
if (active && !table.viewMatches(active.view)) table.applyView(active.view);
|
|
940
|
+
}, [table]);
|
|
905
941
|
const [renaming, setRenaming] = useState(null);
|
|
906
942
|
const commitRename = () => {
|
|
907
943
|
const name = renaming?.trim();
|
|
@@ -3,80 +3,10 @@
|
|
|
3
3
|
The colour-mode control. It is a
|
|
4
4
|
SegmentedControl, which draws every pixel;
|
|
5
5
|
this root keeps the strip at its own width
|
|
6
|
-
in a squeezed row
|
|
7
|
-
|
|
6
|
+
in a squeezed row. The icon-only form, its
|
|
7
|
+
tooltips included, is SegmentedControl's.
|
|
8
8
|
============================================ */
|
|
9
9
|
|
|
10
10
|
.ds-theme-toggle {
|
|
11
11
|
flex-shrink: 0;
|
|
12
12
|
}
|
|
13
|
-
|
|
14
|
-
/* ============================================
|
|
15
|
-
ICON STRIP (showLabels={false})
|
|
16
|
-
The glyphs alone, with each label turned
|
|
17
|
-
into the segment's tooltip: it appears below
|
|
18
|
-
the segment on hover and keyboard focus, on
|
|
19
|
-
Tooltip's surface, so "routine" is never
|
|
20
|
-
left to be guessed as System. The component
|
|
21
|
-
places the label with the shared
|
|
22
|
-
anchored-position behaviour, so a seat that
|
|
23
|
-
clips cannot cut it off, and it flips above
|
|
24
|
-
the strip when there is no room below. The label
|
|
25
|
-
stays in the accessibility tree as the
|
|
26
|
-
segment's name. This is the documentation
|
|
27
|
-
site's header toggle, moved into the
|
|
28
|
-
package so a consumer's strip behaves the
|
|
29
|
-
same.
|
|
30
|
-
============================================ */
|
|
31
|
-
|
|
32
|
-
.ds-theme-toggle--icons .ds-segmented-control__label {
|
|
33
|
-
/* Out of flow at rest; the behaviour makes it fixed once it shows. */
|
|
34
|
-
position: absolute;
|
|
35
|
-
z-index: var(--layer-floating);
|
|
36
|
-
padding: var(--padding-150) var(--padding-200);
|
|
37
|
-
border-radius: var(--radius-100);
|
|
38
|
-
border: var(--border-025) solid var(--color-bg-container-border);
|
|
39
|
-
background-color: var(--color-bg-page-primary);
|
|
40
|
-
color: var(--color-text-primary);
|
|
41
|
-
box-shadow: var(--shadow-floating);
|
|
42
|
-
font-family: var(--font-paragraph-sm-family);
|
|
43
|
-
font-size: var(--font-paragraph-sm-size);
|
|
44
|
-
font-weight: var(--font-paragraph-sm-weight);
|
|
45
|
-
line-height: var(--font-paragraph-sm-line-height);
|
|
46
|
-
letter-spacing: var(--font-paragraph-sm-letter-spacing);
|
|
47
|
-
white-space: nowrap;
|
|
48
|
-
pointer-events: none;
|
|
49
|
-
opacity: 0;
|
|
50
|
-
transform: translateY(calc(-1 * var(--gap-100)));
|
|
51
|
-
transition:
|
|
52
|
-
opacity var(--motion-duration-fast) var(--motion-ease-standard),
|
|
53
|
-
transform var(--motion-duration-fast) var(--motion-ease-standard);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/* --motion-duration-slow matches MOTION_HOVER_SHOW_DELAY_MS, the hover-intent
|
|
57
|
-
delay Tooltip uses in JS, so the label does not flash while the pointer
|
|
58
|
-
passes through. Hiding takes no delay, like a tooltip leaving. */
|
|
59
|
-
.ds-theme-toggle--icons .ds-segmented-control__segment:hover .ds-segmented-control__label,
|
|
60
|
-
.ds-theme-toggle--icons .ds-segmented-control__segment:focus-visible .ds-segmented-control__label {
|
|
61
|
-
opacity: 1;
|
|
62
|
-
transform: none;
|
|
63
|
-
transition-delay: var(--motion-duration-slow);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/* Flipped above the strip, it slides down into place instead. */
|
|
67
|
-
.ds-theme-toggle--icons .ds-segmented-control__label[data-side='top'] {
|
|
68
|
-
transform: translateY(var(--gap-100));
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/* Without an inline label a segment's asymmetric padding reads lopsided:
|
|
72
|
-
square each one into an even pill around its glyph. */
|
|
73
|
-
.ds-theme-toggle--icons .ds-segmented-control__segment {
|
|
74
|
-
padding-inline: var(--padding-200);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/* ds-allow(motion): drops the label tooltip's slide outright rather than playing it at 0.01ms. */
|
|
78
|
-
@media (prefers-reduced-motion: reduce) {
|
|
79
|
-
.ds-theme-toggle--icons .ds-segmented-control__label {
|
|
80
|
-
transition: none;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
@@ -13,7 +13,7 @@ type ThemeToggleOwnProps = {
|
|
|
13
13
|
defaultValue?: ColorModeSetting;
|
|
14
14
|
/** Fires with the setting after every pick, controlled or not. */
|
|
15
15
|
onValueChange?: (value: ColorModeSetting) => void;
|
|
16
|
-
/** Control size, on SegmentedControl's scale
|
|
16
|
+
/** Control size, on SegmentedControl's scale: `default` (the default), the height of the CommandPaletteTrigger and AiButton beside it in a top bar, or `compact`. */
|
|
17
17
|
size?: 'default' | 'compact';
|
|
18
18
|
/** Show each segment's label beside its icon. Set `false` for an icon-only strip: each label becomes the segment's tooltip on hover and keyboard focus, and stays its accessible name. */
|
|
19
19
|
showLabels?: boolean;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
|
-
import React, { useEffect
|
|
3
|
+
import React, { useEffect } from "react";
|
|
4
4
|
import { SegmentedControl } from "../SegmentedControl/SegmentedControl.js";
|
|
5
|
-
import { useAnchoredPosition } from "../../behaviors/useAnchoredPosition.js";
|
|
6
5
|
import { DEFAULT_COLOR_MODE_STORAGE_KEY } from "./colorMode.js";
|
|
7
6
|
import { useColorMode } from "./useColorMode.js";
|
|
8
7
|
import "./ThemeToggle.css";
|
|
@@ -11,7 +10,7 @@ const ThemeToggle = React.forwardRef(
|
|
|
11
10
|
value,
|
|
12
11
|
defaultValue,
|
|
13
12
|
onValueChange,
|
|
14
|
-
size = "
|
|
13
|
+
size = "default",
|
|
15
14
|
showLabels = true,
|
|
16
15
|
storageKey,
|
|
17
16
|
systemLabel = "System",
|
|
@@ -32,33 +31,6 @@ const ThemeToggle = React.forwardRef(
|
|
|
32
31
|
}
|
|
33
32
|
if (stored === null) setSetting(defaultValue);
|
|
34
33
|
}, []);
|
|
35
|
-
const stripRef = useRef(null);
|
|
36
|
-
const segmentRef = useRef(null);
|
|
37
|
-
const labelRef = useRef(null);
|
|
38
|
-
const [tip, setTip] = useState(null);
|
|
39
|
-
const setStripRef = (node) => {
|
|
40
|
-
stripRef.current = node;
|
|
41
|
-
if (typeof ref === "function") ref(node);
|
|
42
|
-
else if (ref) ref.current = node;
|
|
43
|
-
};
|
|
44
|
-
const showTip = (target) => {
|
|
45
|
-
if (showLabels) return;
|
|
46
|
-
const segment = target?.closest(".ds-segmented-control__segment");
|
|
47
|
-
const strip = stripRef.current;
|
|
48
|
-
if (!segment || !strip) return;
|
|
49
|
-
segmentRef.current = segment;
|
|
50
|
-
labelRef.current = segment.querySelector(".ds-segmented-control__label");
|
|
51
|
-
setTip([...strip.querySelectorAll(".ds-segmented-control__segment")].indexOf(segment));
|
|
52
|
-
};
|
|
53
|
-
useAnchoredPosition({
|
|
54
|
-
open: tip !== null,
|
|
55
|
-
target: tip,
|
|
56
|
-
anchorRef: segmentRef,
|
|
57
|
-
panelRef: labelRef,
|
|
58
|
-
side: "bottom",
|
|
59
|
-
align: "center",
|
|
60
|
-
gapProperty: "--gap-100"
|
|
61
|
-
});
|
|
62
34
|
const segments = [
|
|
63
35
|
{ value: "system", label: systemLabel, icon: "routine" },
|
|
64
36
|
{ value: "light", label: lightLabel, icon: "light_mode" },
|
|
@@ -69,15 +41,7 @@ const ThemeToggle = React.forwardRef(
|
|
|
69
41
|
{
|
|
70
42
|
"aria-label": ariaLabel,
|
|
71
43
|
...rest,
|
|
72
|
-
ref
|
|
73
|
-
onPointerOver: (event) => {
|
|
74
|
-
rest.onPointerOver?.(event);
|
|
75
|
-
showTip(event.target);
|
|
76
|
-
},
|
|
77
|
-
onFocus: (event) => {
|
|
78
|
-
rest.onFocus?.(event);
|
|
79
|
-
showTip(event.target);
|
|
80
|
-
},
|
|
44
|
+
ref,
|
|
81
45
|
className: ["ds-theme-toggle", !showLabels && "ds-theme-toggle--icons", className].filter(Boolean).join(" "),
|
|
82
46
|
segments,
|
|
83
47
|
value: setting,
|
|
@@ -87,7 +51,7 @@ const ThemeToggle = React.forwardRef(
|
|
|
87
51
|
onValueChange?.(next);
|
|
88
52
|
},
|
|
89
53
|
size,
|
|
90
|
-
showLabels
|
|
54
|
+
showLabels
|
|
91
55
|
}
|
|
92
56
|
);
|
|
93
57
|
}
|
package/components/registry.json
CHANGED
|
@@ -139,7 +139,7 @@
|
|
|
139
139
|
"name": "AppLayout",
|
|
140
140
|
"label": "App layout",
|
|
141
141
|
"slug": "app-layout",
|
|
142
|
-
"description": "Full-page template pairing the collapsible App sidebar with a centred
|
|
142
|
+
"description": "Full-page template pairing the collapsible App sidebar with a content area, centred or set in an inset surface.",
|
|
143
143
|
"category": "layout",
|
|
144
144
|
"client": true
|
|
145
145
|
},
|
|
@@ -139,7 +139,7 @@ declare const _default: {
|
|
|
139
139
|
"name": "AppLayout",
|
|
140
140
|
"label": "App layout",
|
|
141
141
|
"slug": "app-layout",
|
|
142
|
-
"description": "Full-page template pairing the collapsible App sidebar with a centred
|
|
142
|
+
"description": "Full-page template pairing the collapsible App sidebar with a content area, centred or set in an inset surface.",
|
|
143
143
|
"category": "layout",
|
|
144
144
|
"client": true
|
|
145
145
|
},
|