@crisp-ui-kit/crisp 0.57.0 → 0.57.2
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/accentpicker.css +5 -0
- package/dist/accentpicker.d.ts +7 -0
- package/dist/accentpicker.layered.css +7 -0
- package/dist/authshell.css +9 -4
- package/dist/authshell.layered.css +9 -4
- package/dist/avatar.css +1 -1
- package/dist/avatar.layered.css +1 -1
- package/dist/badge.css +5 -5
- package/dist/badge.layered.css +5 -5
- package/dist/board.css +28 -8
- package/dist/board.layered.css +28 -8
- package/dist/bottomsheet.css +26 -0
- package/dist/bottomsheet.d.ts +7 -0
- package/dist/bottomsheet.layered.css +28 -0
- package/dist/button.css +9 -8
- package/dist/button.layered.css +9 -8
- package/dist/checkbox.css +7 -3
- package/dist/checkbox.layered.css +7 -3
- package/dist/chip.css +15 -0
- package/dist/chip.d.ts +3 -1
- package/dist/chip.layered.css +15 -0
- package/dist/chipgroup.css +5 -0
- package/dist/chipgroup.d.ts +7 -0
- package/dist/chipgroup.layered.css +7 -0
- package/dist/cjs/components/accentpicker/accentpicker.js +66 -0
- package/dist/cjs/components/accentpicker/accentpicker.recipe.js +25 -0
- package/dist/cjs/components/accentpicker/index.js +7 -0
- package/dist/cjs/components/avatar/avatar.recipe.js +4 -1
- package/dist/cjs/components/badge/badge.recipe.js +38 -10
- package/dist/cjs/components/bottomsheet/bottomsheet.js +90 -0
- package/dist/cjs/components/bottomsheet/bottomsheet.recipe.js +77 -0
- package/dist/cjs/components/bottomsheet/index.js +9 -0
- package/dist/cjs/components/button/button.recipe.js +55 -21
- package/dist/cjs/components/checkbox/checkbox.recipe.js +61 -9
- package/dist/cjs/components/chip/chip.js +14 -2
- package/dist/cjs/components/chip/chip.recipe.js +69 -1
- package/dist/cjs/components/chipgroup/chipgroup.js +51 -0
- package/dist/cjs/components/chipgroup/chipgroup.recipe.js +36 -0
- package/dist/cjs/components/chipgroup/index.js +7 -0
- package/dist/cjs/components/datatable/datatable.js +5 -1
- package/dist/cjs/components/datatable/datatable.recipe.js +11 -2
- package/dist/cjs/components/dialog/dialog.recipe.js +21 -10
- package/dist/cjs/components/emptystate/emptystate.recipe.js +7 -2
- package/dist/cjs/components/fileupload/fileupload.js +21 -4
- package/dist/cjs/components/input/index.js +2 -1
- package/dist/cjs/components/input/input.js +33 -3
- package/dist/cjs/components/input/input.recipe.js +22 -7
- package/dist/cjs/components/menu/menu.recipe.js +21 -7
- package/dist/cjs/components/menusheet/index.js +9 -0
- package/dist/cjs/components/menusheet/menusheet.js +133 -0
- package/dist/cjs/components/menusheet/menusheet.recipe.js +103 -0
- package/dist/cjs/components/multiselect/multiselect.js +2 -2
- package/dist/cjs/components/multiselect/multiselect.recipe.js +45 -12
- package/dist/cjs/components/progress/progress.js +21 -4
- package/dist/cjs/components/progress/progress.recipe.js +30 -0
- package/dist/cjs/components/select/select.js +3 -3
- package/dist/cjs/components/select/select.recipe.js +63 -10
- package/dist/cjs/components/snackbar/index.js +8 -0
- package/dist/cjs/components/snackbar/snackbar.js +121 -0
- package/dist/cjs/components/snackbar/snackbar.recipe.js +71 -0
- package/dist/cjs/components/tabletoolbar/tabletoolbar.chrome.js +7 -2
- package/dist/cjs/components/tabletoolbar/tabletoolbar.js +37 -12
- package/dist/cjs/components/tabs/tabs.js +10 -4
- package/dist/cjs/components/tabs/tabs.recipe.js +23 -6
- package/dist/cjs/components/text/text.recipe.js +8 -7
- package/dist/cjs/components/textarea/textarea.js +3 -2
- package/dist/cjs/components/textarea/textarea.recipe.js +13 -9
- package/dist/cjs/components/textlink/textlink.recipe.js +6 -3
- package/dist/cjs/index.js +8 -0
- package/dist/cjs/product/authshell/authshell.recipe.js +5 -1
- package/dist/cjs/product/command/command.recipe.js +4 -1
- package/dist/cjs/product/comments/comments.js +7 -2
- package/dist/cjs/product/comments/comments.recipe.js +25 -7
- package/dist/cjs/product/memberssettings/index.js +6 -0
- package/dist/cjs/product/memberssettings/memberssettings.js +99 -0
- package/dist/cjs/product/memberssettings/memberssettings.recipe.js +83 -0
- package/dist/cjs/product/notificationssettings/index.js +5 -0
- package/dist/cjs/product/notificationssettings/notificationssettings.js +19 -0
- package/dist/cjs/product/notificationssettings/notificationssettings.recipe.js +44 -0
- package/dist/cjs/product/recordpage/recordpage.js +101 -9
- package/dist/cjs/product/recordpage/recordpage.recipe.js +43 -6
- package/dist/cjs/product/workspacegeneralsettings/index.js +5 -0
- package/dist/cjs/product/workspacegeneralsettings/workspacegeneralsettings.js +21 -0
- package/dist/cjs/product/workspacegeneralsettings/workspacegeneralsettings.recipe.js +51 -0
- package/dist/cjs/tokens/contract.js +1 -0
- package/dist/cjs/tokens/elevation.js +8 -5
- package/dist/cjs/tokens/palette.js +107 -33
- package/dist/cjs/tokens/scale.js +45 -2
- package/dist/cjs/tokens/semantic.js +26 -0
- package/dist/columncalc.css +15 -7
- package/dist/columncalc.layered.css +15 -7
- package/dist/command.css +23 -19
- package/dist/command.layered.css +23 -19
- package/dist/comments.css +28 -19
- package/dist/comments.d.ts +2 -2
- package/dist/comments.layered.css +28 -19
- package/dist/datatable.css +161 -67
- package/dist/datatable.layered.css +161 -67
- package/dist/datepicker.css +13 -41
- package/dist/datepicker.layered.css +13 -41
- package/dist/datepickerfield.css +4 -1
- package/dist/datepickerfield.layered.css +4 -1
- package/dist/dialog.css +12 -7
- package/dist/dialog.layered.css +12 -7
- package/dist/emptystate.css +5 -3
- package/dist/emptystate.layered.css +5 -3
- package/dist/esm/components/accentpicker/accentpicker.js +30 -0
- package/dist/esm/components/accentpicker/accentpicker.recipe.js +22 -0
- package/dist/esm/components/accentpicker/index.js +2 -0
- package/dist/esm/components/avatar/avatar.recipe.js +4 -1
- package/dist/esm/components/badge/badge.recipe.js +38 -10
- package/dist/esm/components/bottomsheet/bottomsheet.js +52 -0
- package/dist/esm/components/bottomsheet/bottomsheet.recipe.js +74 -0
- package/dist/esm/components/bottomsheet/index.js +2 -0
- package/dist/esm/components/button/button.recipe.js +56 -22
- package/dist/esm/components/checkbox/checkbox.recipe.js +61 -9
- package/dist/esm/components/chip/chip.js +14 -2
- package/dist/esm/components/chip/chip.recipe.js +69 -1
- package/dist/esm/components/chipgroup/chipgroup.js +15 -0
- package/dist/esm/components/chipgroup/chipgroup.recipe.js +33 -0
- package/dist/esm/components/chipgroup/index.js +2 -0
- package/dist/esm/components/datatable/datatable.js +5 -1
- package/dist/esm/components/datatable/datatable.recipe.js +11 -2
- package/dist/esm/components/dialog/dialog.recipe.js +22 -11
- package/dist/esm/components/emptystate/emptystate.recipe.js +7 -2
- package/dist/esm/components/fileupload/fileupload.js +20 -5
- package/dist/esm/components/input/index.js +1 -1
- package/dist/esm/components/input/input.js +33 -3
- package/dist/esm/components/input/input.recipe.js +23 -8
- package/dist/esm/components/menu/menu.recipe.js +21 -7
- package/dist/esm/components/menusheet/index.js +2 -0
- package/dist/esm/components/menusheet/menusheet.js +95 -0
- package/dist/esm/components/menusheet/menusheet.recipe.js +100 -0
- package/dist/esm/components/multiselect/multiselect.js +2 -2
- package/dist/esm/components/multiselect/multiselect.recipe.js +45 -12
- package/dist/esm/components/progress/progress.js +22 -5
- package/dist/esm/components/progress/progress.recipe.js +30 -0
- package/dist/esm/components/select/select.js +3 -3
- package/dist/esm/components/select/select.recipe.js +63 -10
- package/dist/esm/components/snackbar/index.js +2 -0
- package/dist/esm/components/snackbar/snackbar.js +84 -0
- package/dist/esm/components/snackbar/snackbar.recipe.js +68 -0
- package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +7 -2
- package/dist/esm/components/tabletoolbar/tabletoolbar.js +37 -12
- package/dist/esm/components/tabs/tabs.js +10 -4
- package/dist/esm/components/tabs/tabs.recipe.js +23 -6
- package/dist/esm/components/text/text.recipe.js +8 -7
- package/dist/esm/components/textarea/textarea.js +3 -2
- package/dist/esm/components/textarea/textarea.recipe.js +14 -10
- package/dist/esm/components/textlink/textlink.recipe.js +6 -3
- package/dist/esm/index.js +8 -0
- package/dist/esm/product/authshell/authshell.recipe.js +5 -1
- package/dist/esm/product/command/command.recipe.js +4 -1
- package/dist/esm/product/comments/comments.js +7 -2
- package/dist/esm/product/comments/comments.recipe.js +25 -7
- package/dist/esm/product/memberssettings/index.js +1 -0
- package/dist/esm/product/memberssettings/memberssettings.js +62 -0
- package/dist/esm/product/memberssettings/memberssettings.recipe.js +80 -0
- package/dist/esm/product/notificationssettings/index.js +1 -0
- package/dist/esm/product/notificationssettings/notificationssettings.js +16 -0
- package/dist/esm/product/notificationssettings/notificationssettings.recipe.js +41 -0
- package/dist/esm/product/recordpage/recordpage.js +102 -10
- package/dist/esm/product/recordpage/recordpage.recipe.js +43 -6
- package/dist/esm/product/workspacegeneralsettings/index.js +1 -0
- package/dist/esm/product/workspacegeneralsettings/workspacegeneralsettings.js +18 -0
- package/dist/esm/product/workspacegeneralsettings/workspacegeneralsettings.recipe.js +48 -0
- package/dist/esm/tokens/contract.js +2 -1
- package/dist/esm/tokens/elevation.js +8 -5
- package/dist/esm/tokens/palette.js +107 -33
- package/dist/esm/tokens/scale.js +44 -1
- package/dist/esm/tokens/semantic.js +26 -0
- package/dist/fileupload.css +3 -1
- package/dist/fileupload.layered.css +3 -1
- package/dist/input.css +11 -2
- package/dist/input.layered.css +11 -2
- package/dist/listpage.css +3 -1
- package/dist/listpage.layered.css +3 -1
- package/dist/memberssettings.css +67 -0
- package/dist/memberssettings.d.ts +7 -0
- package/dist/memberssettings.layered.css +69 -0
- package/dist/menu.css +2 -2
- package/dist/menu.layered.css +2 -2
- package/dist/menusheet.css +36 -0
- package/dist/menusheet.d.ts +7 -0
- package/dist/menusheet.layered.css +38 -0
- package/dist/multiselect.css +17 -6
- package/dist/multiselect.d.ts +2 -2
- package/dist/multiselect.layered.css +17 -6
- package/dist/native/tokens.d.ts +2 -1
- package/dist/native/tokens.js +53 -25
- package/dist/notificationssettings.css +22 -0
- package/dist/notificationssettings.d.ts +7 -0
- package/dist/notificationssettings.layered.css +24 -0
- package/dist/objectsettings.css +4 -4
- package/dist/objectsettings.layered.css +4 -4
- package/dist/progress.css +3 -0
- package/dist/progress.layered.css +3 -0
- package/dist/recordpage.css +67 -23
- package/dist/recordpage.layered.css +67 -23
- package/dist/recordpanel.css +35 -16
- package/dist/recordpanel.layered.css +35 -16
- package/dist/resizablepanel.css +8 -5
- package/dist/resizablepanel.layered.css +8 -5
- package/dist/richtext.css +24 -8
- package/dist/richtext.layered.css +24 -8
- package/dist/savedviews.css +9 -2
- package/dist/savedviews.layered.css +9 -2
- package/dist/select.css +47 -7
- package/dist/select.d.ts +2 -2
- package/dist/select.layered.css +47 -7
- package/dist/settingsshell.css +2 -3
- package/dist/settingsshell.layered.css +2 -3
- package/dist/slider.css +6 -27
- package/dist/slider.layered.css +6 -27
- package/dist/snackbar.css +24 -0
- package/dist/snackbar.d.ts +7 -0
- package/dist/snackbar.layered.css +26 -0
- package/dist/statcard.css +3 -1
- package/dist/statcard.layered.css +3 -1
- package/dist/styles.css +927 -365
- package/dist/styles.layered.css +927 -365
- package/dist/table.css +28 -10
- package/dist/table.layered.css +28 -10
- package/dist/tabletoolbar.css +35 -24
- package/dist/tabletoolbar.layered.css +35 -24
- package/dist/tabs.css +18 -4
- package/dist/tabs.layered.css +18 -4
- package/dist/text.css +1 -1
- package/dist/text.layered.css +1 -1
- package/dist/textarea.css +4 -7
- package/dist/textarea.layered.css +4 -7
- package/dist/textlink.css +2 -2
- package/dist/textlink.layered.css +2 -2
- package/dist/themepicker.css +2 -1
- package/dist/themepicker.layered.css +2 -1
- package/dist/tokens.css +31 -13
- package/dist/tokens.json +53 -25
- package/dist/types/components/accentpicker/accentpicker.d.ts +23 -0
- package/dist/types/components/accentpicker/accentpicker.recipe.d.ts +3 -0
- package/dist/types/components/accentpicker/index.d.ts +2 -0
- package/dist/types/components/bottomsheet/bottomsheet.d.ts +35 -0
- package/dist/types/components/bottomsheet/bottomsheet.recipe.d.ts +3 -0
- package/dist/types/components/bottomsheet/index.d.ts +2 -0
- package/dist/types/components/button/button.recipe.d.ts +13 -8
- package/dist/types/components/checkbox/checkbox.recipe.d.ts +28 -5
- package/dist/types/components/chip/chip.d.ts +22 -0
- package/dist/types/components/chip/chip.recipe.d.ts +48 -0
- package/dist/types/components/chipgroup/chipgroup.d.ts +14 -0
- package/dist/types/components/chipgroup/chipgroup.recipe.d.ts +5 -0
- package/dist/types/components/chipgroup/index.d.ts +2 -0
- package/dist/types/components/datatable/datatable.d.ts +19 -0
- package/dist/types/components/fileupload/fileupload.d.ts +26 -0
- package/dist/types/components/input/index.d.ts +1 -1
- package/dist/types/components/input/input.d.ts +84 -0
- package/dist/types/components/menusheet/index.d.ts +2 -0
- package/dist/types/components/menusheet/menusheet.d.ts +42 -0
- package/dist/types/components/menusheet/menusheet.recipe.d.ts +3 -0
- package/dist/types/components/multiselect/multiselect.d.ts +4 -3
- package/dist/types/components/multiselect/multiselect.recipe.d.ts +3 -0
- package/dist/types/components/progress/progress.d.ts +16 -4
- package/dist/types/components/progress/progress.recipe.d.ts +22 -0
- package/dist/types/components/select/select.d.ts +6 -5
- package/dist/types/components/select/select.recipe.d.ts +3 -0
- package/dist/types/components/snackbar/index.d.ts +2 -0
- package/dist/types/components/snackbar/snackbar.d.ts +36 -0
- package/dist/types/components/snackbar/snackbar.recipe.d.ts +5 -0
- package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +28 -1
- package/dist/types/components/tabs/tabs.d.ts +11 -1
- package/dist/types/components/textarea/textarea.d.ts +16 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/product/comments/comments.d.ts +9 -3
- package/dist/types/product/comments/comments.recipe.d.ts +12 -2
- package/dist/types/product/memberssettings/index.d.ts +1 -0
- package/dist/types/product/memberssettings/memberssettings.d.ts +85 -0
- package/dist/types/product/memberssettings/memberssettings.recipe.d.ts +3 -0
- package/dist/types/product/notificationssettings/index.d.ts +1 -0
- package/dist/types/product/notificationssettings/notificationssettings.d.ts +37 -0
- package/dist/types/product/notificationssettings/notificationssettings.recipe.d.ts +3 -0
- package/dist/types/product/recordpage/recordpage.d.ts +89 -2
- package/dist/types/product/workspacegeneralsettings/index.d.ts +1 -0
- package/dist/types/product/workspacegeneralsettings/workspacegeneralsettings.d.ts +48 -0
- package/dist/types/product/workspacegeneralsettings/workspacegeneralsettings.recipe.d.ts +3 -0
- package/dist/types/tokens/contract.d.ts +7 -1
- package/dist/types/tokens/elevation.d.ts +8 -5
- package/dist/types/tokens/palette.d.ts +28 -12
- package/dist/types/tokens/scale.d.ts +12 -1
- package/dist/types/tokens/semantic.d.ts +17 -0
- package/dist/wheelpicker.css +4 -2
- package/dist/wheelpicker.layered.css +4 -2
- package/dist/workspacegeneralsettings.css +30 -0
- package/dist/workspacegeneralsettings.d.ts +7 -0
- package/dist/workspacegeneralsettings.layered.css +32 -0
- package/package.json +41 -1
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as dialog from "@zag-js/dialog";
|
|
3
|
+
import { normalizeProps, Portal, useMachine } from "@zag-js/react";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { hasOpenFloatingLayer, withDescribedBy } from "../../core/index.js";
|
|
6
|
+
import { MENU_ITEM_SELECTOR, useMenuKeyboard } from "../menu/menu-keyboard.js";
|
|
7
|
+
import { menusheetRecipe } from "./menusheet.recipe.js";
|
|
8
|
+
const MenuSheetContext = React.createContext(null);
|
|
9
|
+
let instanceCounter = 0;
|
|
10
|
+
function useMenuSheetInstanceId() {
|
|
11
|
+
const idRef = React.useRef(null);
|
|
12
|
+
if (idRef.current == null) {
|
|
13
|
+
instanceCounter += 1;
|
|
14
|
+
idRef.current = `crisp-menusheet-${instanceCounter}`;
|
|
15
|
+
}
|
|
16
|
+
return idRef.current;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A mobile menu sheet -- a focus-trapped bottom sheet (same Zag dialog
|
|
20
|
+
* machine as BottomSheet) whose body is a role=menu list, with the identical
|
|
21
|
+
* keyboard model Menu already uses: arrow keys move, Home/End jump,
|
|
22
|
+
* typeahead, Enter/Space select, Escape closes. Compose items with
|
|
23
|
+
* MenuSheetItem, and group a run of them in one rounded card with
|
|
24
|
+
* MenuSheetGroup.
|
|
25
|
+
*/
|
|
26
|
+
export function MenuSheet({ open, onOpenChange, title, description, footer, "aria-label": ariaLabel, children, className, "aria-describedby": describedBy, ...rest }) {
|
|
27
|
+
const id = useMenuSheetInstanceId();
|
|
28
|
+
const listRef = React.useRef(null);
|
|
29
|
+
const service = useMachine(dialog.machine, {
|
|
30
|
+
id,
|
|
31
|
+
open,
|
|
32
|
+
onOpenChange(details) {
|
|
33
|
+
onOpenChange(details.open);
|
|
34
|
+
},
|
|
35
|
+
"aria-label": typeof title === "string" ? title : ariaLabel,
|
|
36
|
+
onEscapeKeyDown(event) {
|
|
37
|
+
if (hasOpenFloatingLayer())
|
|
38
|
+
event.preventDefault();
|
|
39
|
+
},
|
|
40
|
+
// Without this, Zag's own focus trap parks initial focus on the dialog
|
|
41
|
+
// surface itself, which wins the race against useMenuKeyboard's rAF-
|
|
42
|
+
// scheduled "focus the first item" effect below -- the two compete for
|
|
43
|
+
// the same job and Zag's own dialog-level default was landing last.
|
|
44
|
+
// Pointing it at the first menu item directly makes Zag do that job
|
|
45
|
+
// itself instead of fighting useMenuKeyboard for it.
|
|
46
|
+
initialFocusEl() {
|
|
47
|
+
return (listRef.current?.querySelector(MENU_ITEM_SELECTOR) ?? null);
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
const api = dialog.connect(service, normalizeProps);
|
|
51
|
+
const close = React.useCallback(() => onOpenChange(false), [onOpenChange]);
|
|
52
|
+
const { onKeyDown: menuKeyDown, items } = useMenuKeyboard({
|
|
53
|
+
open: api.open,
|
|
54
|
+
close,
|
|
55
|
+
contentRef: listRef,
|
|
56
|
+
});
|
|
57
|
+
// Roving tabindex: exactly one item is a real Tab stop at a time (#237 --
|
|
58
|
+
// "selected, or first when none" -- is right in every state but the one
|
|
59
|
+
// that matters, so it is enforced imperatively here rather than trusted to
|
|
60
|
+
// a per-render default). useMenuKeyboard moves FOCUS with arrow keys; this
|
|
61
|
+
// keeps the tabindex in sync with wherever that focus actually lands,
|
|
62
|
+
// including the initial auto-focus Zag's own initialFocusEl performs.
|
|
63
|
+
const onFocus = React.useCallback((e) => {
|
|
64
|
+
for (const el of items())
|
|
65
|
+
el.tabIndex = -1;
|
|
66
|
+
e.target.tabIndex = 0;
|
|
67
|
+
}, [items]);
|
|
68
|
+
React.useEffect(() => {
|
|
69
|
+
if (!api.open)
|
|
70
|
+
return;
|
|
71
|
+
const [first] = items();
|
|
72
|
+
if (first)
|
|
73
|
+
first.tabIndex = 0;
|
|
74
|
+
}, [api.open, items]);
|
|
75
|
+
const ctx = React.useMemo(() => ({ close }), [close]);
|
|
76
|
+
if (!api.open)
|
|
77
|
+
return null;
|
|
78
|
+
return (_jsx(Portal, { children: _jsxs("div", { className: menusheetRecipe(), children: [_jsx("div", { ...api.getBackdropProps(), className: "crisp-menusheet-backdrop" }), _jsx("div", { ...api.getPositionerProps(), className: "crisp-menusheet-positioner", children: _jsxs("div", { ...rest, ...withDescribedBy(api.getContentProps(), describedBy), className: className
|
|
79
|
+
? `crisp-menusheet-content ${className}`
|
|
80
|
+
: "crisp-menusheet-content", children: [(title != null || description != null) && (_jsxs("div", { className: "crisp-menusheet-header", children: [title != null && (_jsx("span", { ...api.getTitleProps(), className: "crisp-menusheet-title", children: title })), description != null && (_jsx("span", { className: "crisp-menusheet-description", children: description }))] })), _jsx(MenuSheetContext.Provider, { value: ctx, children: _jsx("div", { ref: listRef, role: "menu", className: "crisp-menusheet-list", onKeyDown: menuKeyDown, onFocus: onFocus, children: children }) }), footer != null && (_jsx("div", { className: "crisp-menusheet-footer", children: footer }))] }) })] }) }));
|
|
81
|
+
}
|
|
82
|
+
MenuSheet.displayName = "MenuSheet";
|
|
83
|
+
export function MenuSheetGroup({ children }) {
|
|
84
|
+
return _jsx("div", { className: "crisp-menusheet-group", children: children });
|
|
85
|
+
}
|
|
86
|
+
MenuSheetGroup.displayName = "MenuSheetGroup";
|
|
87
|
+
export function MenuSheetItem({ children, icon, description, tone = "neutral", disabled, onSelect, closeOnSelect = true, }) {
|
|
88
|
+
const ctx = React.useContext(MenuSheetContext);
|
|
89
|
+
return (_jsxs("button", { type: "button", role: "menuitem", tabIndex: -1, disabled: disabled, "data-disabled": disabled ? "" : undefined, "data-tone": tone, className: "crisp-menusheet-item", onClick: () => {
|
|
90
|
+
onSelect?.();
|
|
91
|
+
if (closeOnSelect)
|
|
92
|
+
ctx?.close();
|
|
93
|
+
}, children: [icon != null && (_jsx("span", { className: "crisp-menusheet-item-icon", children: icon })), _jsxs("span", { className: "crisp-menusheet-item-content", children: [_jsx("span", { className: "crisp-menusheet-item-label", children: children }), description != null && (_jsx("span", { className: "crisp-menusheet-item-description", children: description }))] })] }));
|
|
94
|
+
}
|
|
95
|
+
MenuSheetItem.displayName = "MenuSheetItem";
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { defineRecipe, resolveRecipe } from "../../core/index.js";
|
|
2
|
+
import { contract as c } from "../../tokens/index.js";
|
|
3
|
+
// A mobile menu sheet -- daangn/seed-design components menu-sheet.yaml and
|
|
4
|
+
// menu-sheet-item.yaml (dev at e1c486b904297f7b6a8f17b8523f732c37015d60), not
|
|
5
|
+
// Attio (no such component there; crisp#911). Same @zag-js/dialog machine as
|
|
6
|
+
// BottomSheet, and the identical role=menu keyboard model Menu already uses
|
|
7
|
+
// (useMenuKeyboard, shared rather than reimplemented). Differs from
|
|
8
|
+
// BottomSheet in every slot value: r5 radius (not r6), content-level padding
|
|
9
|
+
// (not per-slot), no header alignment / close-button matrix at all -- menu
|
|
10
|
+
// sheet has neither in its own schema. Every numeric value cites its yaml
|
|
11
|
+
// line. NOTE: no apostrophes in this file -- one broke the recipe compiler
|
|
12
|
+
// silently on BottomSheet, see AUTHORING.md Lesson 64.
|
|
13
|
+
const config = defineRecipe({
|
|
14
|
+
name: "menusheet",
|
|
15
|
+
base: `display: contents; font-family: ${c.font.sans};
|
|
16
|
+
& .crisp-menusheet-backdrop {
|
|
17
|
+
position: fixed; inset: 0; z-index: 60;
|
|
18
|
+
background: ${c.bgOverlayStrong}; /* menu-sheet.yaml:95 backdrop.color = $color.bg.overlay */
|
|
19
|
+
opacity: 1; transition: opacity 100ms cubic-bezier(0,0,.15,1); /* menu-sheet.yaml:96-98 enterDuration/enterTimingFunction = $duration.d2/$timing-function.enter */
|
|
20
|
+
@starting-style { opacity: 0; }
|
|
21
|
+
}
|
|
22
|
+
& .crisp-menusheet-positioner {
|
|
23
|
+
position: fixed; inset: 0; z-index: 61;
|
|
24
|
+
display: flex; align-items: flex-end; justify-content: center;
|
|
25
|
+
}
|
|
26
|
+
& .crisp-menusheet-content {
|
|
27
|
+
-webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
28
|
+
position: relative; box-sizing: border-box; display: flex; flex-direction: column;
|
|
29
|
+
overflow: hidden; width: 100%; max-width: 480px; /* menu-sheet.yaml:104 content.maxWidth */
|
|
30
|
+
max-height: 85vh;
|
|
31
|
+
background: ${c.bgSurface}; /* menu-sheet.yaml:103 content.color = $color.bg.layer-floating */
|
|
32
|
+
padding: ${c.space["6"]} ${c.space["4"]} ${c.space["4"]}; /* menu-sheet.yaml:105-107 content.paddingX/paddingTop/paddingBottom = global-gutter/x6/x4 (16px/24px/16px) */
|
|
33
|
+
border-radius: ${c.radius["3xl"]} ${c.radius["3xl"]} 0 0; /* menu-sheet.yaml:108 content.topCornerRadius = $radius.r5 (20px) */
|
|
34
|
+
transform: translateY(0);
|
|
35
|
+
transition: transform 300ms cubic-bezier(.03,.4,.1,1); /* menu-sheet.yaml:109-110 content.enterDuration/enterTimingFunction (enter-expressive) */
|
|
36
|
+
@starting-style { transform: translateY(100%); }
|
|
37
|
+
}
|
|
38
|
+
& .crisp-menusheet-header {
|
|
39
|
+
display: flex; flex-direction: column;
|
|
40
|
+
gap: ${c.space["1"]}; /* menu-sheet.yaml:115 header.gap = $dimension.x1 (4px) */
|
|
41
|
+
padding-bottom: ${c.space["4"]}; /* menu-sheet.yaml:116 header.paddingBottom = $dimension.x4 (16px) */
|
|
42
|
+
flex-shrink: 0;
|
|
43
|
+
}
|
|
44
|
+
& .crisp-menusheet-title {
|
|
45
|
+
color: ${c.fgPrimary}; /* menu-sheet.yaml:121 title.color = $color.fg.neutral */
|
|
46
|
+
font-size: 18px; line-height: 24px; font-weight: ${c.weight.bold}; /* menu-sheet.yaml:118-120 title.fontSize/lineHeight/fontWeight = $font-size.t6/$line-height.t6/bold */
|
|
47
|
+
}
|
|
48
|
+
& .crisp-menusheet-description {
|
|
49
|
+
color: ${c.fgMuted}; /* menu-sheet.yaml:126 description.color = $color.fg.neutral-muted */
|
|
50
|
+
font-size: ${c.text.lg}; line-height: 19px; font-weight: ${c.weight.regular}; /* menu-sheet.yaml:123-125 description.fontSize/lineHeight/fontWeight = $font-size.t4/$line-height.t4/regular */
|
|
51
|
+
}
|
|
52
|
+
& .crisp-menusheet-list {
|
|
53
|
+
display: flex; flex-direction: column;
|
|
54
|
+
gap: ${c.space["2.5"]}; /* menu-sheet.yaml:129 list.gap = $dimension.x2_5 (10px) */
|
|
55
|
+
overflow-y: auto; min-height: 0;
|
|
56
|
+
}
|
|
57
|
+
& .crisp-menusheet-group {
|
|
58
|
+
display: flex; flex-direction: column;
|
|
59
|
+
border-radius: ${c.radius["2xl"]}; overflow: hidden; /* menu-sheet.yaml:131 group.cornerRadius = $radius.r4 (16px) */
|
|
60
|
+
}
|
|
61
|
+
& .crisp-menusheet-group > * + * { border-top: 1px solid ${c.strokeGroupDivider}; } /* menu-sheet.yaml:132-134 divider.strokeBottomWidth/strokeColor = 1px/$color.stroke.neutral-muted */
|
|
62
|
+
& .crisp-menusheet-footer {
|
|
63
|
+
flex-shrink: 0;
|
|
64
|
+
padding-top: ${c.space["2.5"]}; /* menu-sheet.yaml:136 footer.paddingTop = $dimension.x2_5 (10px) */
|
|
65
|
+
}
|
|
66
|
+
& .crisp-menusheet-item {
|
|
67
|
+
position: relative; box-sizing: border-box; display: flex; align-items: center;
|
|
68
|
+
width: 100%; min-height: 52px; /* menu-sheet-item.yaml:72 root.minHeight */
|
|
69
|
+
padding: ${c.space["3.5"]} ${c.space["4"]}; gap: ${c.space["3.5"]}; /* menu-sheet-item.yaml:73-75 root.paddingX/paddingY/gap = x4/x3_5/x3_5 (16px/14px/14px) */
|
|
70
|
+
border: none; margin: 0; cursor: pointer; text-align: start;
|
|
71
|
+
background: ${c.bgNeutralWeak}; /* menu-sheet-item.yaml:71 root.color = $color.bg.neutral-weak */
|
|
72
|
+
font-family: inherit;
|
|
73
|
+
}
|
|
74
|
+
& .crisp-menusheet-item:hover,
|
|
75
|
+
& .crisp-menusheet-item:active { background: color-mix(in srgb, ${c.fgPrimary} 6%, ${c.bgNeutralWeak}); } /* menu-sheet-item.yaml:90-91 pressed.root.color = $color.bg.neutral-weak-pressed, approximated as a tint over the base fill */
|
|
76
|
+
& .crisp-menusheet-item:active .crisp-menusheet-item-content { transform: scale(0.98); } /* menu-sheet-item.yaml:92 pressed.root.scaleScope = content */
|
|
77
|
+
& .crisp-menusheet-item-icon { flex-shrink: 0; display: inline-flex; width: 22px; height: 22px; } /* menu-sheet-item.yaml:77 prefixIcon.size */
|
|
78
|
+
& .crisp-menusheet-item-content {
|
|
79
|
+
flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column;
|
|
80
|
+
gap: ${c.space["0.5"]}; /* menu-sheet-item.yaml:79 content.gap = $dimension.x0_5 (2px) */
|
|
81
|
+
transition: transform 150ms cubic-bezier(0,0,.15,1);
|
|
82
|
+
}
|
|
83
|
+
& .crisp-menusheet-item-label {
|
|
84
|
+
font-size: ${c.text.xl}; line-height: 22px; font-weight: ${c.weight.regular}; /* menu-sheet-item.yaml:81-83 label.fontSize/lineHeight/fontWeight = $font-size.t5/$line-height.t5/regular */
|
|
85
|
+
}
|
|
86
|
+
& .crisp-menusheet-item-description {
|
|
87
|
+
color: ${c.fgNeutralSubtle}; /* menu-sheet-item.yaml:88 description.color = $color.fg.neutral-subtle */
|
|
88
|
+
font-size: ${c.text.md}; line-height: 18px; font-weight: ${c.weight.medium}; /* menu-sheet-item.yaml:85-87 description.fontSize/lineHeight/fontWeight = $font-size.t3/$line-height.t3/medium */
|
|
89
|
+
}
|
|
90
|
+
& .crisp-menusheet-item[data-tone="neutral"] .crisp-menusheet-item-icon,
|
|
91
|
+
& .crisp-menusheet-item[data-tone="neutral"] .crisp-menusheet-item-label { color: ${c.fgPrimary}; } /* menu-sheet-item.yaml:93-98 tone=neutral */
|
|
92
|
+
& .crisp-menusheet-item[data-tone="critical"] .crisp-menusheet-item-icon,
|
|
93
|
+
& .crisp-menusheet-item[data-tone="critical"] .crisp-menusheet-item-label { color: ${c.fgDanger}; } /* menu-sheet-item.yaml:99-104 tone=critical */
|
|
94
|
+
`,
|
|
95
|
+
variants: {},
|
|
96
|
+
defaultVariants: {},
|
|
97
|
+
compoundVariants: [],
|
|
98
|
+
});
|
|
99
|
+
export const menusheetRecipe = resolveRecipe(config);
|
|
100
|
+
export default config;
|
|
@@ -35,7 +35,7 @@ export function MultiSelect({ options, value: valueProp, defaultValue, onValueCh
|
|
|
35
35
|
// The id belongs on the trigger, not the wrapper: `<label for>` only names a
|
|
36
36
|
// LABELABLE element, so an id left on the root div meant a FormField label
|
|
37
37
|
// above this control named nothing (#247). Select already did this.
|
|
38
|
-
id, placeholder: placeholderProp, searchable = true, searchPlaceholder: searchPlaceholderProp, emptyLabel: emptyLabelProp, disabled, readOnly, invalid, size = "
|
|
38
|
+
id, placeholder: placeholderProp, searchable = true, searchPlaceholder: searchPlaceholderProp, emptyLabel: emptyLabelProp, disabled, readOnly, invalid, size = "small", variant = "default", className, "aria-label": ariaLabel, ...rest }) {
|
|
39
39
|
// crisp/#471: same resolution order as Select's own.
|
|
40
40
|
const placeholder = useCrispMessage(placeholderProp, "Select…");
|
|
41
41
|
const searchPlaceholder = useCrispMessage(searchPlaceholderProp, "Search…");
|
|
@@ -186,7 +186,7 @@ id, placeholder: placeholderProp, searchable = true, searchPlaceholder: searchPl
|
|
|
186
186
|
}, children: [_jsx("span", { className: "crisp-multiselect-chips", children: selectedOptions.length === 0 ? (_jsx("span", { className: "crisp-multiselect-placeholder", children: placeholder })) : (selectedOptions.map((o) => _jsx(Chip, { opt: o }, o.value))) }), _jsx(ChevronDown, { className: "crisp-multiselect-chevron", size: 16, strokeWidth: 1.75 })] }), open && (_jsx(FloatingPortal, { sourceRef: rootRef, children: _jsxs("div", { ref: listboxRef, id: listboxId, className: "crisp-multiselect-listbox", role: "listbox", "aria-multiselectable": "true", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && (_jsxs("div", { className: "crisp-multiselect-search", children: [_jsx(Search, { size: 16, strokeWidth: 1.75 }), _jsx("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-controls": listboxId, "aria-activedescendant": activeId, value: query, onChange: (e) => {
|
|
187
187
|
setQuery(e.target.value);
|
|
188
188
|
setHighlight(0);
|
|
189
|
-
}, onKeyDown: onNavKeys })] })), filtered.length === 0 ? (_jsx("div", { className: "crisp-multiselect-empty", children: emptyLabel })) : (filtered.map((opt, i) => {
|
|
189
|
+
}, onKeyDown: onNavKeys })] })), searchable && _jsx("div", { className: "crisp-multiselect-divider" }), filtered.length === 0 ? (_jsx("div", { className: "crisp-multiselect-empty", children: emptyLabel })) : (filtered.map((opt, i) => {
|
|
190
190
|
const sel = selectedSet.has(opt.value);
|
|
191
191
|
// #343: a header renders once per run of same-`group`
|
|
192
192
|
// options, so consecutive rows sharing a group render one
|
|
@@ -13,10 +13,14 @@ import { contract as c, elevation as e } from "../../tokens/index.js";
|
|
|
13
13
|
// styling (shared with RecordsTable/DataTable elsewhere), the checkbox-style
|
|
14
14
|
// selected indicator, and the toggle-without-closing interaction.
|
|
15
15
|
//
|
|
16
|
-
// The search row
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
16
|
+
// The search row now follows seed too (owner decision, 2026-09-26: Select
|
|
17
|
+
// AND MultiSelect both follow seed-design, superseding #481's old Attio
|
|
18
|
+
// lock on this row's geometry -- crisp/#904's MultiSelect follow-up). It
|
|
19
|
+
// reads the exact same composed @seed-design/rootage `text-input.yaml`
|
|
20
|
+
// (variant=outline,size=medium) values #913 resolved for Select's own
|
|
21
|
+
// search row -- see that recipe's matching comment for the full citation.
|
|
22
|
+
// `build/spec-check.mjs`'s guard was rewritten to assert these seed values
|
|
23
|
+
// instead of the old 36px Attio measurement.
|
|
20
24
|
//
|
|
21
25
|
// The listbox portals to `document.body` (#339), so its rules live in
|
|
22
26
|
// `globalCss`, not nested under `&` -- see Select's own recipe for the full
|
|
@@ -61,13 +65,19 @@ const config = defineRecipe({
|
|
|
61
65
|
}
|
|
62
66
|
.crisp-multiselect-listbox[data-up] { transform-origin: bottom left; }
|
|
63
67
|
.crisp-multiselect-listbox { @starting-style { opacity: 0; transform: scale(0.95); } }
|
|
64
|
-
/*
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
/* Seed geometry (build/spec-check.mjs guards these four properties
|
|
69
|
+
directly), superseding #481's old Attio lock -- see this file's
|
|
70
|
+
header comment and Select's matching .crisp-select-search comment
|
|
71
|
+
for the full citation. An ordinary flex child of the listbox, so its
|
|
72
|
+
own 8px gap provides the inset/seam on every side, same as Select. */
|
|
73
|
+
.crisp-multiselect-search { display: flex; align-items: center; gap: ${c.space["2"]}; height: ${c.size.selectTriggerMd}; flex-shrink: 0; padding: 0 ${c.space["3.5"]}; border: 1px solid ${c.strokeSelectTrigger}; border-radius: ${c.radius.md}; color: ${c.fgNeutralMuted}; }
|
|
68
74
|
.crisp-multiselect-search svg { flex-shrink: 0; }
|
|
69
|
-
.crisp-multiselect-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: ${c.text.lg}; color: ${c.fgSelectNeutral}; }
|
|
75
|
+
.crisp-multiselect-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: ${c.text.lg}; line-height: 19px; color: ${c.fgSelectNeutral}; }
|
|
70
76
|
.crisp-multiselect-search input::placeholder { color: ${c.fgSelectPlaceholder}; }
|
|
77
|
+
/* Same header/body seam as Select's own .crisp-select-divider -- see
|
|
78
|
+
that recipe's comment for the seed source (select.yaml's divider
|
|
79
|
+
slot, stroke.neutral-muted). */
|
|
80
|
+
.crisp-multiselect-divider { flex-shrink: 0; height: 1px; margin-inline: ${c.space["4"]}; background: ${c.strokeSelectDivider}; }
|
|
71
81
|
/* Same groupLabel treatment as Select's (13px/regular/neutral-subtle). */
|
|
72
82
|
.crisp-multiselect-label { padding: ${c.space["2"]} ${c.space["4"]} 0; font-size: ${c.text.md}; line-height: 18px; font-weight: ${c.weight.regular}; color: ${c.fgNeutralSubtle}; }
|
|
73
83
|
.crisp-multiselect-option {
|
|
@@ -96,9 +106,32 @@ const config = defineRecipe({
|
|
|
96
106
|
.crisp-recordstable-tag[data-tone="orange"] { background: color-mix(in srgb, ${c.bgOrange} 16%, transparent); color: color-mix(in srgb, ${c.bgOrange} 82%, ${c.fgPrimary}); box-shadow: inset 0 0 0 1px color-mix(in srgb, ${c.bgOrange} 30%, transparent); }
|
|
97
107
|
`,
|
|
98
108
|
variants: {
|
|
99
|
-
//
|
|
100
|
-
//
|
|
109
|
+
// Owner's follow-up after #867/#879/#882 (crisp/#889): SIZE is Attio's
|
|
110
|
+
// again, LOOK stays seed's -- see Select's own recipe comment for the
|
|
111
|
+
// full reasoning (measured live in GRC's "New employee" dialog: a 40px
|
|
112
|
+
// trigger next to a 32px `Input` visibly mismatched). `small` is the new
|
|
113
|
+
// default: `c.size.menuRow` (32px) and the original, pre-#882
|
|
114
|
+
// Attio-measured asymmetric padding for a chip-wrapping trigger.
|
|
115
|
+
// `medium`/`large` (seed's own 40/52px) stay, opt-in.
|
|
116
|
+
//
|
|
117
|
+
// crisp/#904 (MultiSelect follow-up, briefly moved then reverted, same
|
|
118
|
+
// ruling as Select's own): `small`'s `gap` moved from `space["1"]`
|
|
119
|
+
// (4px) to `space["2"]` (8px), matching `medium`/`large`'s seed gap --
|
|
120
|
+
// team-lead's review ruling on Select's matching change reversed it:
|
|
121
|
+
// padding AND gap are SPACING, and the owner's direction is "sizes/
|
|
122
|
+
// spacing from Attio, look from seed", not seed's literal gap number.
|
|
123
|
+
// Back to `space["1"]` (4px), the original pre-#882 Attio-measured
|
|
124
|
+
// value. `padding` was never touched either way: the pre-#882,
|
|
125
|
+
// Attio-measured asymmetric padding a chip-wrapping trigger needs
|
|
126
|
+
// (chips sit close to the start edge, the chevron gets more room at
|
|
127
|
+
// the end) -- seed has no multi-value combobox of its own to source a
|
|
128
|
+
// replacement number from (see this file's header comment) even if
|
|
129
|
+
// the ruling had gone the other way.
|
|
101
130
|
size: {
|
|
131
|
+
small: `
|
|
132
|
+
& .crisp-multiselect-trigger { min-height: ${c.size.menuRow}; border-radius: ${c.radius.md}; gap: ${c.space["1"]}; padding: ${c.space["0.5"]} ${c.space["1.5"]} ${c.space["0.5"]} ${c.space["2"]}; }
|
|
133
|
+
& .crisp-multiselect-chevron { width: ${c.size.icon}; height: ${c.size.icon}; }
|
|
134
|
+
`,
|
|
102
135
|
medium: `
|
|
103
136
|
& .crisp-multiselect-trigger { min-height: ${c.size.selectTriggerMd}; border-radius: ${c.radius.md}; gap: ${c.space["2"]}; padding: ${c.space["1"]} ${c.space["3.5"]}; }
|
|
104
137
|
& .crisp-multiselect-chevron { width: ${c.size.icon}; height: ${c.size.icon}; }
|
|
@@ -131,7 +164,7 @@ const config = defineRecipe({
|
|
|
131
164
|
`,
|
|
132
165
|
},
|
|
133
166
|
},
|
|
134
|
-
defaultVariants: { size: "
|
|
167
|
+
defaultVariants: { size: "small", invalid: false, variant: "default" },
|
|
135
168
|
compoundVariants: [],
|
|
136
169
|
});
|
|
137
170
|
export const multiselectRecipe = Object.assign(resolveRecipe(config), {
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { progressRecipe } from "./progress.recipe.js";
|
|
3
3
|
/**
|
|
4
|
-
* A progress bar — a continuous brand fill,
|
|
5
|
-
* `segments` is set (the "Getting started 3/6" pattern)
|
|
6
|
-
* `
|
|
4
|
+
* A progress bar — a continuous brand fill, a segmented row of pills when
|
|
5
|
+
* `segments` is set (the "Getting started 3/6" pattern), or a small ring
|
|
6
|
+
* (`variant="circular"`, a character-counter's own progress). Reports its
|
|
7
|
+
* state via `role="progressbar"` and the ARIA value attributes.
|
|
7
8
|
*/
|
|
8
|
-
export function Progress({ value, max = 100, segments, "aria-label": ariaLabel, className, ...rest }) {
|
|
9
|
+
export function Progress({ value, max = 100, segments, variant = "linear", size = 18, "aria-label": ariaLabel, className, ...rest }) {
|
|
9
10
|
const pct = Math.max(0, Math.min(100, (value / max) * 100));
|
|
10
11
|
const aria = {
|
|
11
12
|
role: "progressbar",
|
|
@@ -14,6 +15,22 @@ export function Progress({ value, max = 100, segments, "aria-label": ariaLabel,
|
|
|
14
15
|
"aria-valuemax": max,
|
|
15
16
|
"aria-label": ariaLabel,
|
|
16
17
|
};
|
|
18
|
+
if (variant === "circular") {
|
|
19
|
+
// Measured live: an 18x18 SVG, r=7.4, stroke-width 1.6 (a 0.6px margin on
|
|
20
|
+
// each side) — a two-circle ring (a soft-tinted track + a brand arc),
|
|
21
|
+
// the same track tint Badge/Callout's own `soft(bgBrandSolid, 14%)`
|
|
22
|
+
// formula already gives.
|
|
23
|
+
const strokeWidth = size * (1.6 / 18);
|
|
24
|
+
const r = size / 2 - strokeWidth;
|
|
25
|
+
const circumference = 2 * Math.PI * r;
|
|
26
|
+
const offset = circumference * (1 - pct / 100);
|
|
27
|
+
// The measured sample was at value=0 (a full, invisible offset either
|
|
28
|
+
// way), so the arc's start point couldn't be read off that one DOM
|
|
29
|
+
// snapshot -- 12 o'clock is the conventional start for this shape and
|
|
30
|
+
// isn't itself in tension with anything measured, but isn't independently
|
|
31
|
+
// confirmed at a nonzero value either.
|
|
32
|
+
return (_jsx("div", { ...aria, className: progressRecipe({ className }), ...rest, "data-variant": "circular", children: _jsxs("svg", { width: size, height: size, viewBox: `0 0 ${size} ${size}`, "aria-hidden": "true", children: [_jsx("circle", { className: "crisp-progress-circle-track", r: r, cx: size / 2, cy: size / 2, strokeWidth: strokeWidth, fill: "transparent" }), _jsx("circle", { className: "crisp-progress-circle-fill", r: r, cx: size / 2, cy: size / 2, strokeWidth: strokeWidth, fill: "transparent", strokeDasharray: circumference, strokeDashoffset: offset, strokeLinecap: "round", transform: `rotate(-90 ${size / 2} ${size / 2})` })] }) }));
|
|
33
|
+
}
|
|
17
34
|
if (segments && segments > 0) {
|
|
18
35
|
const filled = Math.round((pct / 100) * segments);
|
|
19
36
|
return (_jsx("div", { ...aria, className: progressRecipe({ className }), ...rest, "data-segmented": "", children: Array.from({ length: segments }, (_, i) => (_jsx("span", { className: "crisp-progress-seg", "data-filled": i < filled ? "" : undefined }, i))) }));
|
|
@@ -49,6 +49,36 @@ const spec = defineSpec({
|
|
|
49
49
|
selector: "&[data-segmented] .crisp-progress-seg[data-filled]",
|
|
50
50
|
decls: { background: c.bgBrandSolid },
|
|
51
51
|
},
|
|
52
|
+
// crisp#634: measured live on Attio's own Settings > Profile
|
|
53
|
+
// character-counter ring — the track tint is the same
|
|
54
|
+
// `soft(bgBrandSolid, 14%)` formula Badge/Callout already use (measured
|
|
55
|
+
// rgb(229,238,255), within the same noise this codebase already treats
|
|
56
|
+
// as equivalent), the arc is the plain brand fill.
|
|
57
|
+
{
|
|
58
|
+
selector: '&[data-variant="circular"]',
|
|
59
|
+
decls: {
|
|
60
|
+
display: "inline-flex",
|
|
61
|
+
height: "auto",
|
|
62
|
+
borderRadius: "0",
|
|
63
|
+
background: "transparent",
|
|
64
|
+
overflow: "visible",
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
slot: "circleTrack",
|
|
69
|
+
selector: "& .crisp-progress-circle-track",
|
|
70
|
+
decls: {
|
|
71
|
+
stroke: `color-mix(in srgb, ${c.bgBrandSolid} 14%, transparent)`,
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
slot: "circleFill",
|
|
76
|
+
selector: "& .crisp-progress-circle-fill",
|
|
77
|
+
decls: {
|
|
78
|
+
stroke: c.bgBrandSolid,
|
|
79
|
+
transition: "stroke-dashoffset 0.25s ease",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
52
82
|
],
|
|
53
83
|
});
|
|
54
84
|
const config = specToRecipe(spec);
|
|
@@ -19,7 +19,7 @@ function optionText(o) {
|
|
|
19
19
|
* move, Enter/Space pick, Esc close, Home/End jump, opens upward when tight.
|
|
20
20
|
* Controlled or uncontrolled.
|
|
21
21
|
*/
|
|
22
|
-
export function Select({ options, value, defaultValue, onValueChange, name, required, placeholder: placeholderProp, variant = "default", width = "full", size = "
|
|
22
|
+
export function Select({ options, value, defaultValue, onValueChange, name, required, placeholder: placeholderProp, variant = "default", width = "full", size = "small", searchable, searchPlaceholder: searchPlaceholderProp, onSearchChange, emptyLabel: emptyLabelProp, disabled, readOnly, invalid, className, "aria-label": ariaLabel, id, ...rest }) {
|
|
23
23
|
// crisp/#471: an explicit prop always wins; missing, it reads the nearest
|
|
24
24
|
// CrispMessagesProvider's translation of the English default, or that
|
|
25
25
|
// default itself with no provider — unchanged from before this existed.
|
|
@@ -181,11 +181,11 @@ export function Select({ options, value, defaultValue, onValueChange, name, requ
|
|
|
181
181
|
return (_jsxs("div", { ref: rootRef, className: selectRecipe({ variant, width, size, invalid, className }), ...rest, children: [name && (selected || required) && (_jsx("input", { className: "crisp-formvalue", style: formValueStyle, name: name, value: selected ?? "", required: required, tabIndex: -1, "aria-hidden": "true",
|
|
182
182
|
// controlled by the listbox above; `readOnly` would bar it from
|
|
183
183
|
// validation, which is the only reason this input is not hidden
|
|
184
|
-
onChange: () => { } })), _jsxs("button", { ref: triggerRef, type: "button", id: id, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listboxId : undefined, "aria-activedescendant": searchable ? undefined : activeId, "aria-label": ariaLabel, "aria-invalid": invalid === true || undefined, "aria-readonly": readOnly ? "true" : undefined, disabled: disabled, "data-open": open ? "" : undefined, className: "crisp-select-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: onTriggerKeyDown, children: [_jsx("span", { className: "crisp-select-value", children: selectedOption ? (_jsxs(_Fragment, { children: [selectedOption.color && (_jsx("span", { className: "crisp-select-option-dot", style: { background: selectedOption.color } })), selectedOption.triggerLabel ?? selectedOption.label] })) : (_jsx("span", { className: "crisp-select-placeholder", children: placeholder })) }), _jsx(ChevronDown, { className: "crisp-select-chevron", size: 16, strokeWidth: 1.75 })] }), open && (_jsx(FloatingPortal, { sourceRef: rootRef, children: _jsxs("div", { ref: listboxRef, id: listboxId, className: "crisp-select-listbox", role: "listbox", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && (_jsxs("div", { className: "crisp-select-search", children: [_jsx(Search, { size:
|
|
184
|
+
onChange: () => { } })), _jsxs("button", { ref: triggerRef, type: "button", id: id, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listboxId : undefined, "aria-activedescendant": searchable ? undefined : activeId, "aria-label": ariaLabel, "aria-invalid": invalid === true || undefined, "aria-readonly": readOnly ? "true" : undefined, disabled: disabled, "data-open": open ? "" : undefined, className: "crisp-select-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: onTriggerKeyDown, children: [_jsx("span", { className: "crisp-select-value", children: selectedOption ? (_jsxs(_Fragment, { children: [selectedOption.color && (_jsx("span", { className: "crisp-select-option-dot", style: { background: selectedOption.color } })), selectedOption.triggerLabel ?? selectedOption.label] })) : (_jsx("span", { className: "crisp-select-placeholder", children: placeholder })) }), _jsx(ChevronDown, { className: "crisp-select-chevron", size: 16, strokeWidth: 1.75 })] }), open && (_jsx(FloatingPortal, { sourceRef: rootRef, children: _jsxs("div", { ref: listboxRef, id: listboxId, className: "crisp-select-listbox", role: "listbox", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && (_jsxs("div", { className: "crisp-select-search", children: [_jsx(Search, { size: 16, strokeWidth: 1.75 }), _jsx("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-controls": listboxId, "aria-activedescendant": activeId, value: query, onChange: (e) => {
|
|
185
185
|
setQuery(e.target.value);
|
|
186
186
|
setHighlight(0);
|
|
187
187
|
onSearchChange?.(e.target.value);
|
|
188
|
-
}, onKeyDown: onNavKeys })] })), filtered.length === 0 ? (_jsx("div", { className: "crisp-select-empty", children: emptyLabel })) : (filtered.map((opt, i) => {
|
|
188
|
+
}, onKeyDown: onNavKeys })] })), searchable && _jsx("div", { className: "crisp-select-divider" }), filtered.length === 0 ? (_jsx("div", { className: "crisp-select-empty", children: emptyLabel })) : (filtered.map((opt, i) => {
|
|
189
189
|
const isSel = opt.value === selected;
|
|
190
190
|
// #343: a header renders once per run of same-`group`
|
|
191
191
|
// options, so consecutive rows sharing a group render one
|
|
@@ -26,11 +26,11 @@ import { contract as c, elevation as e } from "../../tokens/index.js";
|
|
|
26
26
|
// pattern with no precedent anywhere else in the repo, for one component.
|
|
27
27
|
// The enter animation (scale 0.95→1, opacity 0→1, seed's own curve) IS
|
|
28
28
|
// reproduced below.
|
|
29
|
-
// - A
|
|
30
|
-
// crisp or GRC groups options with an explicit separator
|
|
31
|
-
// AUTHORING's rule against building what nothing consumes
|
|
32
|
-
// too.
|
|
33
|
-
//
|
|
29
|
+
// - A general-purpose `divider` slot between arbitrary option groups:
|
|
30
|
+
// nothing in crisp or GRC groups options with an explicit separator
|
|
31
|
+
// today, and AUTHORING's rule against building what nothing consumes
|
|
32
|
+
// applies to CSS too. (`select.yaml`'s resolved value IS used below, just
|
|
33
|
+
// for one specific seam — see `.crisp-select-divider`'s own comment.)
|
|
34
34
|
const config = defineRecipe({
|
|
35
35
|
name: "select",
|
|
36
36
|
base: `
|
|
@@ -85,10 +85,34 @@ const config = defineRecipe({
|
|
|
85
85
|
}
|
|
86
86
|
.crisp-select-listbox[data-up] { transform-origin: bottom left; }
|
|
87
87
|
.crisp-select-listbox { @starting-style { opacity: 0; transform: scale(0.95); } }
|
|
88
|
-
|
|
88
|
+
/* Composed from @seed-design/rootage's text-input.yaml
|
|
89
|
+
(variant=outline,size=medium) -- seed's Select has no search field of
|
|
90
|
+
its own to resolve this from (see the file header), so this reuses
|
|
91
|
+
the SAME tokens #867 already resolved for the trigger at this size:
|
|
92
|
+
minHeight x10 (${c.size.selectTriggerMd}), paddingX x3_5
|
|
93
|
+
(${c.space["3.5"]}), gap x2 (${c.space["2"]}), cornerRadius r2
|
|
94
|
+
(${c.radius.md}), strokeColor stroke.neutral-weak
|
|
95
|
+
(${c.strokeSelectTrigger}) -- a real bordered field sitting flush
|
|
96
|
+
inside the listbox's own 8px padding, replacing the old bare
|
|
97
|
+
34px/no-border/bleeds-to-the-edge strip (crisp/#904). */
|
|
98
|
+
.crisp-select-search { display: flex; align-items: center; gap: ${c.space["2"]}; height: ${c.size.selectTriggerMd}; flex-shrink: 0; padding: 0 ${c.space["3.5"]}; border: 1px solid ${c.strokeSelectTrigger}; border-radius: ${c.radius.md}; color: ${c.fgNeutralMuted}; }
|
|
89
99
|
.crisp-select-search svg { flex-shrink: 0; }
|
|
90
|
-
.crisp-select-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: ${c.text.lg}; color: ${c.fgSelectNeutral}; }
|
|
100
|
+
.crisp-select-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: ${c.text.lg}; line-height: 19px; color: ${c.fgSelectNeutral}; }
|
|
91
101
|
.crisp-select-search input::placeholder { color: ${c.fgSelectPlaceholder}; }
|
|
102
|
+
/* select.yaml's own divider slot (marginX x4, height 1px, colour
|
|
103
|
+
stroke.neutral-muted -- described upstream as "the line marking a
|
|
104
|
+
boundary between semantic units: between sections, between content
|
|
105
|
+
and an action area, between a header and a body" -- exactly the
|
|
106
|
+
search-field/option-list boundary crisp#904's review flagged as
|
|
107
|
+
reading too tight). Not the general-purpose separator #867 declined
|
|
108
|
+
to build (nothing groups options with one) -- scoped to this one
|
|
109
|
+
specific header/body seam, which now exists because searchable
|
|
110
|
+
does. Sits as an ordinary flex child, so the listbox's own 8px gap
|
|
111
|
+
still applies on both sides of it -- total visual seam is
|
|
112
|
+
gap+1px+gap (17px), deliberately more than the 8px between two
|
|
113
|
+
plain option rows, so it reads as a section break, not a wider list
|
|
114
|
+
gap. */
|
|
115
|
+
.crisp-select-divider { flex-shrink: 0; height: 1px; margin-inline: ${c.space["4"]}; background: ${c.strokeSelectDivider}; }
|
|
92
116
|
.crisp-select-empty { padding: ${c.space["3"]} ${c.space["4"]}; text-align: center; font-size: ${c.text.md}; color: ${c.fgNeutralSubtle}; }
|
|
93
117
|
/* select.yaml's groupLabel -- font-size/line-height/weight are the same
|
|
94
118
|
across sizes, only paddingY differs (medium 8px, large 10px). */
|
|
@@ -121,9 +145,38 @@ const config = defineRecipe({
|
|
|
121
145
|
full: `width: 100%;`,
|
|
122
146
|
auto: `width: auto;`,
|
|
123
147
|
},
|
|
124
|
-
//
|
|
125
|
-
//
|
|
148
|
+
// Owner's follow-up after #867/#879 (crisp/#889): the LOOK stays seed's
|
|
149
|
+
// (border, listbox, radius, shadow, items, states, all unchanged below),
|
|
150
|
+
// but the HEIGHT is Attio's again -- a 40px trigger next to a 32px
|
|
151
|
+
// `Input` in the same form visibly mismatched (measured live in GRC's
|
|
152
|
+
// own "New employee" dialog). `small` is the new default: Attio's
|
|
153
|
+
// original 32px `c.size.menuRow`. `medium`/`large` (seed's own 40/52px)
|
|
154
|
+
// stay, opt-in. Item rows are IDENTICAL across all three -- only the
|
|
155
|
+
// trigger differs; only it was ever the mismatch.
|
|
156
|
+
//
|
|
157
|
+
// crisp/#904 (follow-up, then reverted): `small`'s gap/padding-inline
|
|
158
|
+
// briefly moved to seed's literal `select-trigger.yaml` `size=medium`
|
|
159
|
+
// values (8px/14px), on the theory that even the 32px trigger should
|
|
160
|
+
// read seed's numbers verbatim. Team-lead ruling on that PR's review:
|
|
161
|
+
// padding and gap are SPACING, and the owner's actual direction is
|
|
162
|
+
// "sizes/spacing from Attio, look from seed" -- Select and Input sit
|
|
163
|
+
// side by side in every form, so spacing follows Input's measured
|
|
164
|
+
// values here, same as `medium`/`large` below follow seed's. Reverted
|
|
165
|
+
// to `space["1.5"]`/`space["2.5"]` (6px/10px, `Input`'s own gap/padding
|
|
166
|
+
// at this row height) -- everything else `small` already got from seed
|
|
167
|
+
// (fill/stroke/border/radius/chevron/colours/states, see this file's
|
|
168
|
+
// other comments) is unaffected and stays seed's.
|
|
126
169
|
size: {
|
|
170
|
+
small: `
|
|
171
|
+
& .crisp-select-trigger { height: ${c.size.menuRow}; border-radius: ${c.radius.md}; gap: ${c.space["1.5"]}; padding-inline: ${c.space["2.5"]}; }
|
|
172
|
+
& .crisp-select-value, & .crisp-select-placeholder { font-size: ${c.text.lg}; line-height: 20px; }
|
|
173
|
+
& .crisp-select-chevron { width: ${c.size.icon}; height: ${c.size.icon}; }
|
|
174
|
+
.crisp-select-option { gap: ${c.space["2"]}; }
|
|
175
|
+
.crisp-select-option-label { font-size: ${c.text.lg}; line-height: 19px; }
|
|
176
|
+
.crisp-select-option-description { font-size: ${c.text.sm}; line-height: 16px; }
|
|
177
|
+
.crisp-select-option-check { width: ${c.space["3"]}; height: ${c.space["3"]}; }
|
|
178
|
+
.crisp-select-label { padding-top: ${c.space["2"]}; }
|
|
179
|
+
`,
|
|
127
180
|
medium: `
|
|
128
181
|
& .crisp-select-trigger { height: ${c.size.selectTriggerMd}; border-radius: ${c.radius.md}; gap: ${c.space["2"]}; padding-inline: ${c.space["3.5"]}; }
|
|
129
182
|
& .crisp-select-value, & .crisp-select-placeholder { font-size: ${c.text.lg}; line-height: 19px; }
|
|
@@ -175,7 +228,7 @@ const config = defineRecipe({
|
|
|
175
228
|
},
|
|
176
229
|
defaultVariants: {
|
|
177
230
|
width: "full",
|
|
178
|
-
size: "
|
|
231
|
+
size: "small",
|
|
179
232
|
invalid: false,
|
|
180
233
|
variant: "default",
|
|
181
234
|
},
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { CircleAlert, CircleCheck } from "lucide-react";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { useCrispMessage, useFocusAfterRemoval } from "../../core/index.js";
|
|
5
|
+
import { snackbarRecipe } from "./snackbar.recipe.js";
|
|
6
|
+
const SnackbarContext = React.createContext(null);
|
|
7
|
+
const ICONS = {
|
|
8
|
+
positive: CircleCheck,
|
|
9
|
+
critical: CircleAlert,
|
|
10
|
+
};
|
|
11
|
+
function SnackbarPill({ item, onClose, }) {
|
|
12
|
+
const Icon = ICONS[item.variant ?? "default"];
|
|
13
|
+
return (_jsxs("div", { className: "crisp-snackbar", "data-variant": item.variant ?? "default", children: [_jsxs("div", { className: "crisp-snackbar-content", children: [Icon != null && (_jsx("span", { className: "crisp-snackbar-icon", children: _jsx(Icon, { size: 24, strokeWidth: 1.75, "aria-hidden": "true" }) })), _jsx("span", { className: "crisp-snackbar-message", children: item.message })] }), item.action && (_jsx("button", { type: "button", className: "crisp-snackbar-action", onClick: () => {
|
|
14
|
+
item.action?.onClick();
|
|
15
|
+
onClose();
|
|
16
|
+
}, children: item.action.label }))] }));
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Wrap your app in a `SnackbarProvider`, then call `useSnackbar().show({...})`
|
|
20
|
+
* from anywhere to queue a bottom-anchored snackbar. Only one shows at a time
|
|
21
|
+
* (seed-design's mobile pattern, crisp#911) -- a second call queues behind the
|
|
22
|
+
* first and shows once it dismisses. Auto-dismisses after `duration` (default
|
|
23
|
+
* 4s; 0 keeps it until `dismiss()` or its action fires -- there is no close
|
|
24
|
+
* button, matching seed's own anatomy, which has none).
|
|
25
|
+
*/
|
|
26
|
+
export function SnackbarProvider({ children, duration = 4000, regionLabel: regionLabelProp, }) {
|
|
27
|
+
const regionLabel = useCrispMessage(regionLabelProp, "Notifications");
|
|
28
|
+
const [queue, setQueue] = React.useState([]);
|
|
29
|
+
const idRef = React.useRef(0);
|
|
30
|
+
const timer = React.useRef(null);
|
|
31
|
+
const regionRef = React.useRef(null);
|
|
32
|
+
const cameFrom = React.useRef(null);
|
|
33
|
+
const focusAfterDismiss = useFocusAfterRemoval(regionRef, ".crisp-snackbar-action", cameFrom);
|
|
34
|
+
React.useEffect(() => {
|
|
35
|
+
const onFocusIn = (e) => {
|
|
36
|
+
const target = e.target;
|
|
37
|
+
if (!target || regionRef.current?.contains(target))
|
|
38
|
+
return;
|
|
39
|
+
cameFrom.current = target;
|
|
40
|
+
};
|
|
41
|
+
document.addEventListener("focusin", onFocusIn);
|
|
42
|
+
return () => document.removeEventListener("focusin", onFocusIn);
|
|
43
|
+
}, []);
|
|
44
|
+
const dismiss = React.useCallback((id) => {
|
|
45
|
+
setQueue((list) => list.filter((s) => s.id !== id));
|
|
46
|
+
if (timer.current) {
|
|
47
|
+
clearTimeout(timer.current);
|
|
48
|
+
timer.current = null;
|
|
49
|
+
}
|
|
50
|
+
}, []);
|
|
51
|
+
const show = React.useCallback((o) => {
|
|
52
|
+
idRef.current += 1;
|
|
53
|
+
const id = idRef.current;
|
|
54
|
+
setQueue((list) => [...list, { ...o, id }]);
|
|
55
|
+
return id;
|
|
56
|
+
}, []);
|
|
57
|
+
// Only the FRONT of the queue is ever mounted, so its own auto-dismiss
|
|
58
|
+
// timer starts when it becomes visible, not when `show` was called.
|
|
59
|
+
const current = queue[0];
|
|
60
|
+
React.useEffect(() => {
|
|
61
|
+
if (!current)
|
|
62
|
+
return;
|
|
63
|
+
const d = current.duration ?? duration;
|
|
64
|
+
if (d <= 0)
|
|
65
|
+
return;
|
|
66
|
+
timer.current = setTimeout(() => dismiss(current.id), d);
|
|
67
|
+
return () => {
|
|
68
|
+
if (timer.current)
|
|
69
|
+
clearTimeout(timer.current);
|
|
70
|
+
};
|
|
71
|
+
}, [current, duration, dismiss]);
|
|
72
|
+
const api = React.useMemo(() => ({ show, dismiss }), [show, dismiss]);
|
|
73
|
+
return (_jsxs(SnackbarContext.Provider, { value: api, children: [children, _jsx("div", { className: snackbarRecipe(), children: _jsx("div", { ref: regionRef, className: "crisp-snackbar-region", role: "region", "aria-label": regionLabel, "aria-live": "polite", "aria-atomic": "false", children: current && (_jsx(SnackbarPill, { item: current, onClose: () => {
|
|
74
|
+
focusAfterDismiss(0);
|
|
75
|
+
dismiss(current.id);
|
|
76
|
+
} }, current.id)) }) })] }));
|
|
77
|
+
}
|
|
78
|
+
SnackbarProvider.displayName = "SnackbarProvider";
|
|
79
|
+
export function useSnackbar() {
|
|
80
|
+
const ctx = React.useContext(SnackbarContext);
|
|
81
|
+
if (!ctx)
|
|
82
|
+
throw new Error("useSnackbar must be used within a SnackbarProvider");
|
|
83
|
+
return ctx;
|
|
84
|
+
}
|