@crisp-ui-kit/crisp 0.2.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/base.css +25 -0
- package/dist/button.css +1 -1
- package/dist/command.css +1 -1
- package/dist/contextmenu.css +1 -1
- package/dist/datatable.css +60 -0
- package/dist/datatable.d.ts +7 -0
- package/dist/datepicker.css +1 -1
- package/dist/dialog.css +1 -1
- package/dist/esm/components/accordion/accordion.js +32 -0
- package/dist/esm/components/accordion/accordion.recipe.js +23 -0
- package/dist/esm/components/accordion/index.js +2 -0
- package/dist/esm/components/avatar/avatar.js +8 -0
- package/dist/esm/components/avatar/avatar.recipe.js +39 -0
- package/dist/esm/components/avatar/index.js +2 -0
- package/dist/esm/components/avatargroup/avatargroup.js +16 -0
- package/dist/esm/components/avatargroup/avatargroup.recipe.js +15 -0
- package/dist/esm/components/avatargroup/index.js +2 -0
- package/dist/esm/components/badge/badge.js +8 -0
- package/dist/esm/components/badge/badge.recipe.js +65 -0
- package/dist/esm/components/badge/index.js +2 -0
- package/dist/esm/components/barchart/barchart.js +29 -0
- package/dist/esm/components/barchart/barchart.recipe.js +26 -0
- package/dist/esm/components/barchart/index.js +2 -0
- package/dist/esm/components/breadcrumb/breadcrumb.js +17 -0
- package/dist/esm/components/breadcrumb/breadcrumb.recipe.js +18 -0
- package/dist/esm/components/breadcrumb/index.js +2 -0
- package/dist/esm/components/bulkbar/bulkbar.js +16 -0
- package/dist/esm/components/bulkbar/bulkbar.recipe.js +28 -0
- package/dist/esm/components/bulkbar/index.js +2 -0
- package/dist/esm/components/button/button.js +15 -0
- package/dist/esm/components/button/button.recipe.js +39 -0
- package/dist/esm/components/button/index.js +2 -0
- package/dist/esm/components/callout/callout.js +22 -0
- package/dist/esm/components/callout/callout.recipe.js +26 -0
- package/dist/esm/components/callout/index.js +2 -0
- package/dist/esm/components/card/card.js +15 -0
- package/dist/esm/components/card/card.recipe.js +28 -0
- package/dist/esm/components/card/index.js +2 -0
- package/dist/esm/components/checkbox/checkbox.js +20 -0
- package/dist/esm/components/checkbox/checkbox.recipe.js +23 -0
- package/dist/esm/components/checkbox/index.js +2 -0
- package/dist/esm/components/contextmenu/contextmenu.js +42 -0
- package/dist/esm/components/contextmenu/contextmenu.recipe.js +21 -0
- package/dist/esm/components/contextmenu/index.js +2 -0
- package/dist/esm/components/datatable/datatable.js +245 -0
- package/dist/esm/components/datatable/datatable.recipe.js +72 -0
- package/dist/esm/components/datatable/index.js +2 -0
- package/dist/esm/components/datepicker/datepicker.js +57 -0
- package/dist/esm/components/datepicker/datepicker.recipe.js +32 -0
- package/dist/esm/components/datepicker/index.js +2 -0
- package/dist/esm/components/dialog/dialog.js +48 -0
- package/dist/esm/components/dialog/dialog.recipe.js +32 -0
- package/dist/esm/components/dialog/index.js +2 -0
- package/dist/esm/components/divider/divider.js +7 -0
- package/dist/esm/components/divider/divider.recipe.js +14 -0
- package/dist/esm/components/divider/index.js +2 -0
- package/dist/esm/components/donutchart/donutchart.js +38 -0
- package/dist/esm/components/donutchart/donutchart.recipe.js +27 -0
- package/dist/esm/components/donutchart/index.js +2 -0
- package/dist/esm/components/drawer/drawer.js +42 -0
- package/dist/esm/components/drawer/drawer.recipe.js +27 -0
- package/dist/esm/components/drawer/index.js +2 -0
- package/dist/esm/components/emptystate/emptystate.js +7 -0
- package/dist/esm/components/emptystate/emptystate.recipe.js +31 -0
- package/dist/esm/components/emptystate/index.js +2 -0
- package/dist/esm/components/formfield/formfield.js +14 -0
- package/dist/esm/components/formfield/formfield.recipe.js +19 -0
- package/dist/esm/components/formfield/index.js +2 -0
- package/dist/esm/components/input/index.js +2 -0
- package/dist/esm/components/input/input.js +8 -0
- package/dist/esm/components/input/input.recipe.js +23 -0
- package/dist/esm/components/kbd/index.js +2 -0
- package/dist/esm/components/kbd/kbd.js +8 -0
- package/dist/esm/components/kbd/kbd.recipe.js +20 -0
- package/dist/esm/components/linechart/index.js +2 -0
- package/dist/esm/components/linechart/linechart.js +41 -0
- package/dist/esm/components/linechart/linechart.recipe.js +27 -0
- package/dist/esm/components/menu/index.js +2 -0
- package/dist/esm/components/menu/menu.js +137 -0
- package/dist/esm/components/menu/menu.recipe.js +51 -0
- package/dist/esm/components/multiselect/index.js +2 -0
- package/dist/esm/components/multiselect/multiselect.js +118 -0
- package/dist/esm/components/multiselect/multiselect.recipe.js +68 -0
- package/dist/esm/components/popover/index.js +2 -0
- package/dist/esm/components/popover/popover.js +70 -0
- package/dist/esm/components/popover/popover.recipe.js +18 -0
- package/dist/esm/components/progress/index.js +2 -0
- package/dist/esm/components/progress/progress.js +23 -0
- package/dist/esm/components/progress/progress.recipe.js +17 -0
- package/dist/esm/components/radiogroup/index.js +2 -0
- package/dist/esm/components/radiogroup/radiogroup.js +46 -0
- package/dist/esm/components/radiogroup/radiogroup.recipe.js +37 -0
- package/dist/esm/components/segmented/index.js +2 -0
- package/dist/esm/components/segmented/segmented.js +59 -0
- package/dist/esm/components/segmented/segmented.recipe.js +20 -0
- package/dist/esm/components/select/index.js +2 -0
- package/dist/esm/components/select/select.js +144 -0
- package/dist/esm/components/select/select.recipe.js +76 -0
- package/dist/esm/components/skeleton/index.js +2 -0
- package/dist/esm/components/skeleton/skeleton.js +7 -0
- package/dist/esm/components/skeleton/skeleton.recipe.js +14 -0
- package/dist/esm/components/spinner/index.js +2 -0
- package/dist/esm/components/spinner/spinner.js +11 -0
- package/dist/esm/components/spinner/spinner.recipe.js +13 -0
- package/dist/esm/components/stagebar/index.js +2 -0
- package/dist/esm/components/stagebar/stagebar.js +23 -0
- package/dist/esm/components/stagebar/stagebar.recipe.js +14 -0
- package/dist/esm/components/statcard/index.js +2 -0
- package/dist/esm/components/statcard/statcard.js +12 -0
- package/dist/esm/components/statcard/statcard.recipe.js +34 -0
- package/dist/esm/components/switch/index.js +2 -0
- package/dist/esm/components/switch/switch.js +22 -0
- package/dist/esm/components/switch/switch.recipe.js +26 -0
- package/dist/esm/components/table/index.js +2 -0
- package/dist/esm/components/table/table.js +30 -0
- package/dist/esm/components/table/table.recipe.js +40 -0
- package/dist/esm/components/tabletoolbar/index.js +2 -0
- package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +79 -0
- package/dist/esm/components/tabletoolbar/tabletoolbar.js +191 -0
- package/dist/esm/components/tabletoolbar/tabletoolbar.recipe.js +23 -0
- package/dist/esm/components/tabs/index.js +2 -0
- package/dist/esm/components/tabs/tabs.js +70 -0
- package/dist/esm/components/tabs/tabs.recipe.js +33 -0
- package/dist/esm/components/textarea/index.js +2 -0
- package/dist/esm/components/textarea/textarea.js +20 -0
- package/dist/esm/components/textarea/textarea.recipe.js +31 -0
- package/dist/esm/components/toast/index.js +2 -0
- package/dist/esm/components/toast/toast.js +58 -0
- package/dist/esm/components/toast/toast.recipe.js +28 -0
- package/dist/esm/components/tooltip/index.js +2 -0
- package/dist/esm/components/tooltip/tooltip.js +45 -0
- package/dist/esm/components/tooltip/tooltip.recipe.js +13 -0
- package/dist/esm/core/cx.js +1 -0
- package/dist/esm/core/define.js +3 -0
- package/dist/esm/core/index.js +4 -0
- package/dist/esm/core/primitive.js +13 -0
- package/dist/esm/core/resolve.js +41 -0
- package/dist/esm/core/types.js +1 -0
- package/dist/esm/index.js +71 -0
- package/dist/esm/product/appshell/appshell.js +44 -0
- package/dist/esm/product/appshell/appshell.recipe.js +27 -0
- package/dist/esm/product/appshell/index.js +2 -0
- package/dist/esm/product/board/board.js +42 -0
- package/dist/esm/product/board/board.recipe.js +28 -0
- package/dist/esm/product/board/index.js +2 -0
- package/dist/esm/product/command/command.js +188 -0
- package/dist/esm/product/command/command.recipe.js +19 -0
- package/dist/esm/product/command/index.js +2 -0
- package/dist/esm/product/entitycard/entitycard.js +11 -0
- package/dist/esm/product/entitycard/entitycard.recipe.js +29 -0
- package/dist/esm/product/entitycard/index.js +2 -0
- package/dist/esm/product/flowcanvas/flowcanvas.js +85 -0
- package/dist/esm/product/flowcanvas/flowcanvas.recipe.js +25 -0
- package/dist/esm/product/flowcanvas/index.js +2 -0
- package/dist/esm/product/inbox/inbox.js +13 -0
- package/dist/esm/product/inbox/inbox.recipe.js +43 -0
- package/dist/esm/product/inbox/index.js +2 -0
- package/dist/esm/product/listpage/index.js +2 -0
- package/dist/esm/product/listpage/listpage.js +21 -0
- package/dist/esm/product/listpage/listpage.recipe.js +43 -0
- package/dist/esm/product/navitem/index.js +1 -0
- package/dist/esm/product/navitem/navitem.js +8 -0
- package/dist/esm/product/navitem/navitem.recipe.js +21 -0
- package/dist/esm/product/onboarding/index.js +2 -0
- package/dist/esm/product/onboarding/onboarding.js +14 -0
- package/dist/esm/product/onboarding/onboarding.recipe.js +41 -0
- package/dist/esm/product/recordpage/index.js +2 -0
- package/dist/esm/product/recordpage/recordpage.js +286 -0
- package/dist/esm/product/recordpage/recordpage.recipe.js +157 -0
- package/dist/esm/product/recordpanel/index.js +2 -0
- package/dist/esm/product/recordpanel/recordpanel.js +47 -0
- package/dist/esm/product/recordpanel/recordpanel.recipe.js +52 -0
- package/dist/esm/product/recordstable/company-logos.js +26 -0
- package/dist/esm/product/recordstable/index.js +2 -0
- package/dist/esm/product/recordstable/recordstable.js +963 -0
- package/dist/esm/product/recordstable/recordstable.recipe.js +149 -0
- package/dist/esm/product/richtext/index.js +2 -0
- package/dist/esm/product/richtext/richtext.js +111 -0
- package/dist/esm/product/richtext/richtext.recipe.js +40 -0
- package/dist/esm/product/settingssection/index.js +2 -0
- package/dist/esm/product/settingssection/settingssection.js +11 -0
- package/dist/esm/product/settingssection/settingssection.recipe.js +20 -0
- package/dist/esm/product/settingsshell/index.js +2 -0
- package/dist/esm/product/settingsshell/settingsshell.js +23 -0
- package/dist/esm/product/settingsshell/settingsshell.recipe.js +38 -0
- package/dist/esm/product/sidebar/index.js +2 -0
- package/dist/esm/product/sidebar/sidebar.js +112 -0
- package/dist/esm/product/sidebar/sidebar.recipe.js +18 -0
- package/dist/esm/product/tasklist/index.js +2 -0
- package/dist/esm/product/tasklist/tasklist.js +16 -0
- package/dist/esm/product/tasklist/tasklist.recipe.js +35 -0
- package/dist/esm/product/trialbanner/index.js +2 -0
- package/dist/esm/product/trialbanner/trialbanner.js +10 -0
- package/dist/esm/product/trialbanner/trialbanner.recipe.js +29 -0
- package/dist/esm/product/viewtoggle/index.js +2 -0
- package/dist/esm/product/viewtoggle/viewtoggle.js +11 -0
- package/dist/esm/product/viewtoggle/viewtoggle.recipe.js +24 -0
- package/dist/esm/tokens/contract.js +18 -0
- package/dist/esm/tokens/index.js +4 -0
- package/dist/esm/tokens/palette.js +30 -0
- package/dist/esm/tokens/scale.js +60 -0
- package/dist/esm/tokens/semantic.js +30 -0
- package/dist/menu.css +2 -2
- package/dist/multiselect.css +3 -3
- package/dist/popover.css +1 -1
- package/dist/recordpage.css +5 -5
- package/dist/recordstable.css +4 -4
- package/dist/select.css +10 -4
- package/dist/select.d.ts +2 -1
- package/dist/sidebar.css +1 -1
- package/dist/statcard.css +6 -0
- package/dist/statcard.d.ts +2 -2
- package/dist/styles.css +222 -27
- package/dist/table.css +26 -0
- package/dist/table.d.ts +8 -0
- package/dist/tabletoolbar.css +70 -0
- package/dist/tabletoolbar.d.ts +7 -0
- package/dist/tokens.css +24 -0
- package/dist/types/components/datatable/datatable.d.ts +98 -0
- package/dist/types/components/datatable/datatable.recipe.d.ts +3 -0
- package/dist/types/components/datatable/index.d.ts +2 -0
- package/dist/types/components/select/select.d.ts +3 -1
- package/dist/types/components/select/select.recipe.d.ts +12 -0
- package/dist/types/components/statcard/statcard.d.ts +6 -1
- package/dist/types/components/statcard/statcard.recipe.d.ts +12 -2
- package/dist/types/components/table/index.d.ts +2 -0
- package/dist/types/components/table/table.d.ts +53 -0
- package/dist/types/components/table/table.recipe.d.ts +21 -0
- package/dist/types/components/tabletoolbar/index.d.ts +2 -0
- package/dist/types/components/tabletoolbar/tabletoolbar.chrome.d.ts +2 -0
- package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +63 -0
- package/dist/types/components/tabletoolbar/tabletoolbar.recipe.d.ts +3 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/product/recordstable/company-logos.d.ts +1 -0
- package/dist/types/product/recordstable/recordstable.d.ts +42 -1
- package/dist/types/product/sidebar/index.d.ts +1 -1
- package/dist/types/product/sidebar/sidebar.d.ts +85 -0
- package/dist/types/tokens/contract.d.ts +12 -0
- package/dist/types/tokens/palette.d.ts +48 -0
- package/dist/types/tokens/semantic.d.ts +12 -0
- package/dist/viewtoggle.css +2 -2
- package/package.json +259 -9
- package/dist/index.js +0 -9690
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { segmentedRecipe } from "./segmented.recipe.js";
|
|
4
|
+
/**
|
|
5
|
+
* A segmented control (single-choice toggle) — measured from Attio's view
|
|
6
|
+
* switcher. `role="radiogroup"` over `role="radio"` buttons with roving
|
|
7
|
+
* tabindex and ←/→ (Home/End) keyboard selection.
|
|
8
|
+
*/
|
|
9
|
+
export function Segmented({ options, value, defaultValue, onValueChange, "aria-label": ariaLabel, className, }) {
|
|
10
|
+
const [internal, setInternal] = React.useState(defaultValue ?? options[0]?.value);
|
|
11
|
+
const selected = value ?? internal;
|
|
12
|
+
const rootRef = React.useRef(null);
|
|
13
|
+
const [thumb, setThumb] = React.useState(null);
|
|
14
|
+
// Measure the selected item and slide the thumb to it (before paint, so the
|
|
15
|
+
// first render lands in place and only later selections animate).
|
|
16
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: re-measure when the value or option set changes
|
|
17
|
+
React.useLayoutEffect(() => {
|
|
18
|
+
const root = rootRef.current;
|
|
19
|
+
const el = root?.querySelector(".segmented-item[data-selected]");
|
|
20
|
+
if (!el) {
|
|
21
|
+
setThumb(null);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
setThumb({ left: el.offsetLeft, width: el.offsetWidth });
|
|
25
|
+
}, [selected, options]);
|
|
26
|
+
const select = (v) => {
|
|
27
|
+
if (value === undefined)
|
|
28
|
+
setInternal(v);
|
|
29
|
+
onValueChange?.(v);
|
|
30
|
+
};
|
|
31
|
+
const onKeyDown = (e, i) => {
|
|
32
|
+
const enabled = options.filter((o) => !o.disabled);
|
|
33
|
+
const pos = enabled.findIndex((o) => o.value === options[i].value);
|
|
34
|
+
let next = -1;
|
|
35
|
+
if (e.key === "ArrowRight" || e.key === "ArrowDown")
|
|
36
|
+
next = (pos + 1) % enabled.length;
|
|
37
|
+
else if (e.key === "ArrowLeft" || e.key === "ArrowUp")
|
|
38
|
+
next = (pos - 1 + enabled.length) % enabled.length;
|
|
39
|
+
else if (e.key === "Home")
|
|
40
|
+
next = 0;
|
|
41
|
+
else if (e.key === "End")
|
|
42
|
+
next = enabled.length - 1;
|
|
43
|
+
if (next < 0)
|
|
44
|
+
return;
|
|
45
|
+
e.preventDefault();
|
|
46
|
+
select(enabled[next].value);
|
|
47
|
+
};
|
|
48
|
+
return (_jsxs("div", { ref: rootRef, className: segmentedRecipe({ className }), role: "radiogroup", "aria-label": ariaLabel, children: [thumb && (_jsx("span", { className: "segmented-thumb", "aria-hidden": "true", style: {
|
|
49
|
+
width: thumb.width,
|
|
50
|
+
transform: `translateX(${thumb.left}px)`,
|
|
51
|
+
} })), options.map((opt, i) => {
|
|
52
|
+
const on = opt.value === selected;
|
|
53
|
+
const iconOnly = opt.label == null && opt.icon != null;
|
|
54
|
+
return (_jsxs("button", { type: "button", role: "radio", "aria-checked": on, "aria-label": opt["aria-label"], disabled: opt.disabled, "data-selected": on ? "" : undefined, tabIndex: on ? 0 : -1, className: iconOnly
|
|
55
|
+
? "segmented-item segmented-item--icon"
|
|
56
|
+
: "segmented-item", onClick: () => select(opt.value), onKeyDown: (e) => onKeyDown(e, i), children: [opt.icon, opt.label] }, opt.value));
|
|
57
|
+
})] }));
|
|
58
|
+
}
|
|
59
|
+
Segmented.displayName = "Segmented";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { defineRecipe, resolveRecipe } from "../../core/index.js";
|
|
2
|
+
import { contract as c } from "../../tokens/index.js";
|
|
3
|
+
// A segmented control — Attio's view switcher: a subtle track holding options,
|
|
4
|
+
// the selected one raised on a white pill.
|
|
5
|
+
const config = defineRecipe({
|
|
6
|
+
name: "segmented",
|
|
7
|
+
base: `position: relative; display: inline-flex; align-items: center; gap: ${c.space["0.5"]}; box-sizing: border-box; padding: ${c.space["0.5"]}; border-radius: ${c.radius.lg}; background: color-mix(in srgb, ${c.fgPrimary} 5%, transparent); font-family: ${c.font.sans};
|
|
8
|
+
& .segmented-thumb { position: absolute; top: ${c.space["0.5"]}; left: 0; height: 26px; border-radius: ${c.radius.md}; background: ${c.bgSurface}; box-shadow: inset 0 0 0 1px ${c.borderSubtle}, 0 1px 2px rgba(0,0,0,0.06); pointer-events: none; z-index: 0; transition: transform .2s cubic-bezier(.4,0,.2,1), width .2s cubic-bezier(.4,0,.2,1); }
|
|
9
|
+
& .segmented-item { position: relative; z-index: 1; display: inline-flex; align-items: center; justify-content: center; gap: ${c.space["1.5"]}; height: 26px; padding: 0 ${c.space["2.5"]}; margin: 0; border: none; border-radius: ${c.radius.md}; background: none; color: ${c.fgMuted}; font-family: inherit; font-size: ${c.text.md}; font-weight: ${c.weight.medium}; white-space: nowrap; cursor: pointer; transition: color 0.15s ease; }
|
|
10
|
+
& .segmented-item svg { flex-shrink: 0; }
|
|
11
|
+
& .segmented-item:hover { color: ${c.fgPrimary}; }
|
|
12
|
+
& .segmented-item[data-selected] { color: ${c.fgPrimary}; }
|
|
13
|
+
& .segmented-item[disabled] { opacity: 0.4; cursor: default; }
|
|
14
|
+
& .segmented-item--icon { width: 26px; padding: 0; }`,
|
|
15
|
+
variants: {},
|
|
16
|
+
defaultVariants: {},
|
|
17
|
+
compoundVariants: [],
|
|
18
|
+
});
|
|
19
|
+
export const segmentedRecipe = resolveRecipe(config);
|
|
20
|
+
export default config;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Check, ChevronDown, Search } from "lucide-react";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { selectRecipe } from "./select.recipe.js";
|
|
5
|
+
function optionText(o) {
|
|
6
|
+
if (o.keywords)
|
|
7
|
+
return o.keywords;
|
|
8
|
+
return typeof o.label === "string" ? o.label : o.value;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A select / dropdown — the settings control, measured from Attio, with an
|
|
12
|
+
* optional Linear-style search. A `role="combobox"` trigger opens a
|
|
13
|
+
* `role="listbox"`; pass `searchable` to add a filter field at the top (with a
|
|
14
|
+
* "No results" empty state). Full keyboard model — ↑/↓ move, Enter/Space pick,
|
|
15
|
+
* Esc close, Home/End jump, opens upward when tight. Controlled or uncontrolled.
|
|
16
|
+
*/
|
|
17
|
+
export function Select({ options, value, defaultValue, onValueChange, placeholder = "Select…", variant = "default", width = "full", searchable, searchPlaceholder = "Search…", disabled, className, "aria-label": ariaLabel, id, }) {
|
|
18
|
+
const [internal, setInternal] = React.useState(defaultValue);
|
|
19
|
+
const selected = value ?? internal;
|
|
20
|
+
const [open, setOpen] = React.useState(false);
|
|
21
|
+
const [up, setUp] = React.useState(false);
|
|
22
|
+
const [pos, setPos] = React.useState({});
|
|
23
|
+
const [highlight, setHighlight] = React.useState(0);
|
|
24
|
+
const [query, setQuery] = React.useState("");
|
|
25
|
+
const rootRef = React.useRef(null);
|
|
26
|
+
const triggerRef = React.useRef(null);
|
|
27
|
+
const searchRef = React.useRef(null);
|
|
28
|
+
const selectedOption = options.find((o) => o.value === selected);
|
|
29
|
+
// The options shown, after applying the search filter.
|
|
30
|
+
const q = query.trim().toLowerCase();
|
|
31
|
+
const filtered = searchable && q
|
|
32
|
+
? options.filter((o) => optionText(o).toLowerCase().includes(q))
|
|
33
|
+
: options;
|
|
34
|
+
const enabledIndexes = filtered
|
|
35
|
+
.map((o, i) => (o.disabled ? -1 : i))
|
|
36
|
+
.filter((i) => i >= 0);
|
|
37
|
+
const openMenu = () => {
|
|
38
|
+
if (disabled)
|
|
39
|
+
return;
|
|
40
|
+
const rect = triggerRef.current?.getBoundingClientRect();
|
|
41
|
+
if (rect) {
|
|
42
|
+
const flipUp = window.innerHeight - rect.bottom < 260;
|
|
43
|
+
setUp(flipUp);
|
|
44
|
+
setPos({
|
|
45
|
+
left: rect.left,
|
|
46
|
+
minWidth: rect.width,
|
|
47
|
+
...(flipUp
|
|
48
|
+
? { bottom: window.innerHeight - rect.top + 4 }
|
|
49
|
+
: { top: rect.bottom + 4 }),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
setQuery("");
|
|
53
|
+
const sel = options.findIndex((o) => o.value === selected);
|
|
54
|
+
setHighlight(sel >= 0 ? sel : (enabledIndexes[0] ?? 0));
|
|
55
|
+
setOpen(true);
|
|
56
|
+
if (searchable)
|
|
57
|
+
requestAnimationFrame(() => searchRef.current?.focus({ preventScroll: true }));
|
|
58
|
+
};
|
|
59
|
+
const close = () => {
|
|
60
|
+
setOpen(false);
|
|
61
|
+
triggerRef.current?.focus({ preventScroll: true });
|
|
62
|
+
};
|
|
63
|
+
const pick = (opt) => {
|
|
64
|
+
if (!opt || opt.disabled)
|
|
65
|
+
return;
|
|
66
|
+
if (value === undefined)
|
|
67
|
+
setInternal(opt.value);
|
|
68
|
+
onValueChange?.(opt.value);
|
|
69
|
+
setOpen(false);
|
|
70
|
+
triggerRef.current?.focus({ preventScroll: true });
|
|
71
|
+
};
|
|
72
|
+
const moveHighlight = (dir) => {
|
|
73
|
+
if (enabledIndexes.length === 0)
|
|
74
|
+
return;
|
|
75
|
+
const pos = enabledIndexes.indexOf(highlight);
|
|
76
|
+
const nextPos = pos < 0
|
|
77
|
+
? dir > 0
|
|
78
|
+
? 0
|
|
79
|
+
: enabledIndexes.length - 1
|
|
80
|
+
: (pos + dir + enabledIndexes.length) % enabledIndexes.length;
|
|
81
|
+
setHighlight(enabledIndexes[nextPos] ?? 0);
|
|
82
|
+
};
|
|
83
|
+
const onNavKeys = (e) => {
|
|
84
|
+
switch (e.key) {
|
|
85
|
+
case "ArrowDown":
|
|
86
|
+
e.preventDefault();
|
|
87
|
+
moveHighlight(1);
|
|
88
|
+
break;
|
|
89
|
+
case "ArrowUp":
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
moveHighlight(-1);
|
|
92
|
+
break;
|
|
93
|
+
case "Home":
|
|
94
|
+
e.preventDefault();
|
|
95
|
+
setHighlight(enabledIndexes[0] ?? 0);
|
|
96
|
+
break;
|
|
97
|
+
case "End":
|
|
98
|
+
e.preventDefault();
|
|
99
|
+
setHighlight(enabledIndexes[enabledIndexes.length - 1] ?? 0);
|
|
100
|
+
break;
|
|
101
|
+
case "Enter":
|
|
102
|
+
e.preventDefault();
|
|
103
|
+
pick(filtered[highlight]);
|
|
104
|
+
break;
|
|
105
|
+
case "Escape":
|
|
106
|
+
e.preventDefault();
|
|
107
|
+
close();
|
|
108
|
+
break;
|
|
109
|
+
case "Tab":
|
|
110
|
+
setOpen(false);
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const onTriggerKeyDown = (e) => {
|
|
115
|
+
if (!open) {
|
|
116
|
+
if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") {
|
|
117
|
+
e.preventDefault();
|
|
118
|
+
openMenu();
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
// when searchable, navigation is handled on the search input
|
|
123
|
+
if (!searchable)
|
|
124
|
+
onNavKeys(e);
|
|
125
|
+
};
|
|
126
|
+
React.useEffect(() => {
|
|
127
|
+
if (!open)
|
|
128
|
+
return;
|
|
129
|
+
const onDown = (e) => {
|
|
130
|
+
if (!rootRef.current?.contains(e.target))
|
|
131
|
+
setOpen(false);
|
|
132
|
+
};
|
|
133
|
+
document.addEventListener("pointerdown", onDown);
|
|
134
|
+
return () => document.removeEventListener("pointerdown", onDown);
|
|
135
|
+
}, [open]);
|
|
136
|
+
return (_jsxs("div", { ref: rootRef, className: selectRecipe({ variant, width, className }), children: [_jsxs("button", { ref: triggerRef, type: "button", id: id, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-label": ariaLabel, disabled: disabled, "data-open": open ? "" : undefined, className: "select-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: onTriggerKeyDown, children: [_jsx("span", { className: "select-value", children: selectedOption ? (_jsxs(_Fragment, { children: [selectedOption.color && (_jsx("span", { className: "select-option-dot", style: { background: selectedOption.color } })), selectedOption.label] })) : (_jsx("span", { className: "select-placeholder", children: placeholder })) }), _jsx(ChevronDown, { className: "select-chevron", size: 16, strokeWidth: 1.75 })] }), open && (_jsxs("div", { className: "select-listbox", role: "listbox", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && (_jsxs("div", { className: "select-search", children: [_jsx(Search, { size: 15, strokeWidth: 1.75 }), _jsx("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, value: query, onChange: (e) => {
|
|
137
|
+
setQuery(e.target.value);
|
|
138
|
+
setHighlight(0);
|
|
139
|
+
}, onKeyDown: onNavKeys })] })), filtered.length === 0 ? (_jsx("div", { className: "select-empty", children: "No results" })) : (filtered.map((opt, i) => {
|
|
140
|
+
const isSel = opt.value === selected;
|
|
141
|
+
return (_jsxs("button", { type: "button", role: "option", "aria-selected": isSel, disabled: opt.disabled, "data-disabled": opt.disabled ? "" : undefined, "data-highlighted": i === highlight ? "" : undefined, className: "select-option", onClick: () => pick(opt), onMouseEnter: () => setHighlight(i), children: [opt.color && (_jsx("span", { className: "select-option-dot", style: { background: opt.color } })), _jsx("span", { className: "select-option-label", children: opt.label }), isSel && (_jsx(Check, { className: "select-option-check", size: 15, strokeWidth: 2 }))] }, opt.value));
|
|
142
|
+
}))] }))] }));
|
|
143
|
+
}
|
|
144
|
+
Select.displayName = "Select";
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { defineRecipe, resolveRecipe } from "../../core/index.js";
|
|
2
|
+
import { contract as c } from "../../tokens/index.js";
|
|
3
|
+
// A select / dropdown, measured from Attio's settings: a 32px combobox trigger
|
|
4
|
+
// (radius 9, value + a 16px chevron) over a listbox — radius 12, 4px padding, a
|
|
5
|
+
// layered soft shadow — of 32px options (radius 8, 6/8 padding) that show a
|
|
6
|
+
// check on the selected one and highlight on hover / keyboard. The listbox is
|
|
7
|
+
// portaled into this root (which is position:relative) so it escapes overflow
|
|
8
|
+
// while keeping these nested styles.
|
|
9
|
+
const config = defineRecipe({
|
|
10
|
+
name: "select",
|
|
11
|
+
base: `
|
|
12
|
+
position: relative; display: inline-flex; font-family: ${c.font.sans};
|
|
13
|
+
-webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
14
|
+
|
|
15
|
+
& .select-trigger {
|
|
16
|
+
box-sizing: border-box; display: inline-flex; align-items: center; gap: ${c.space["1.5"]};
|
|
17
|
+
height: 32px; width: 100%; min-width: 0; margin: 0; padding: 0 ${c.space["1.5"]} 0 ${c.space["2"]};
|
|
18
|
+
border: none; border-radius: ${c.radius.md}; background: transparent; box-shadow: inset 0 0 0 1px color-mix(in srgb, ${c.fgPrimary} 7%, transparent);
|
|
19
|
+
font-family: inherit; font-size: ${c.text.lg}; color: ${c.fgPrimary}; cursor: pointer;
|
|
20
|
+
transition: background-color .12s ease, box-shadow .12s ease;
|
|
21
|
+
}
|
|
22
|
+
& .select-trigger:hover { background: ${c.bgRaised}; }
|
|
23
|
+
& .select-trigger[data-open], & .select-trigger:focus-visible { outline: none; box-shadow: inset 0 0 0 1px ${c.bgBrandSolid}; background: ${c.bgSurface}; }
|
|
24
|
+
& .select-trigger:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
25
|
+
& .select-value { flex: 1; min-width: 0; display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: left; }
|
|
26
|
+
& .select-placeholder { color: ${c.fgMuted}; }
|
|
27
|
+
& .select-chevron { flex-shrink: 0; color: ${c.fgMuted}; transition: transform .15s ease; }
|
|
28
|
+
& .select-trigger[data-open] .select-chevron { transform: rotate(180deg); }
|
|
29
|
+
|
|
30
|
+
& .select-listbox {
|
|
31
|
+
position: fixed; z-index: 60; box-sizing: border-box; display: flex; flex-direction: column; gap: ${c.space.px};
|
|
32
|
+
max-height: 280px; overflow-y: auto; padding: ${c.space["1"]};
|
|
33
|
+
border-radius: ${c.radius.xl}; background: ${c.bgPopover};
|
|
34
|
+
box-shadow: inset 0 0 0 1px ${c.ringPopover}, 0 0 0 1px ${c.shadowPopover1}, 0 4px 8px -4px ${c.shadowPopover2}, 0 4px 12px -2px ${c.shadowPopover3};
|
|
35
|
+
transform-origin: top left; opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease;
|
|
36
|
+
}
|
|
37
|
+
& .select-listbox[data-up] { transform-origin: bottom; }
|
|
38
|
+
& .select-listbox { @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
|
|
39
|
+
& .select-search { display: flex; align-items: center; gap: ${c.space["2"]}; height: 34px; flex-shrink: 0; margin: calc(-1 * ${c.space["1"]}) calc(-1 * ${c.space["1"]}) ${c.space["1"]}; padding: 0 ${c.space["2.5"]}; border-bottom: 1px solid ${c.borderSubtle}; color: ${c.fgMuted}; }
|
|
40
|
+
& .select-search svg { flex-shrink: 0; }
|
|
41
|
+
& .select-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: ${c.text.lg}; color: ${c.fgPrimary}; }
|
|
42
|
+
& .select-search input::placeholder { color: ${c.fgMuted}; }
|
|
43
|
+
& .select-empty { padding: ${c.space["3"]} ${c.space["2"]}; text-align: center; font-size: ${c.text.md}; color: ${c.fgMuted}; }
|
|
44
|
+
& .select-option {
|
|
45
|
+
box-sizing: border-box; display: flex; align-items: center; gap: ${c.space["2"]}; height: 32px; flex-shrink: 0;
|
|
46
|
+
padding: 0 ${c.space["2"]}; margin: 0; border: none; background: none; border-radius: ${c.radius.md};
|
|
47
|
+
font-family: inherit; font-size: ${c.text.lg}; color: ${c.fgPrimary}; text-align: left; cursor: pointer; white-space: nowrap;
|
|
48
|
+
}
|
|
49
|
+
& .select-option[data-highlighted] { background: ${c.bgRaised}; }
|
|
50
|
+
& .select-option[data-disabled] { opacity: 0.5; cursor: not-allowed; }
|
|
51
|
+
& .select-option-dot { flex-shrink: 0; width: 8px; height: 8px; border-radius: ${c.radius.full}; }
|
|
52
|
+
& .select-option-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
53
|
+
& .select-option-check { flex-shrink: 0; color: ${c.bgBrandSolid}; }
|
|
54
|
+
`,
|
|
55
|
+
variants: {
|
|
56
|
+
// ghost: borderless at rest (transparent), reveals a subtle fill on hover —
|
|
57
|
+
// Attio's inline selects (e.g. the sort configurator).
|
|
58
|
+
variant: {
|
|
59
|
+
default: "",
|
|
60
|
+
ghost: `
|
|
61
|
+
& .select-trigger { background: transparent; box-shadow: none; }
|
|
62
|
+
& .select-trigger:hover { background: ${c.bgRaised}; box-shadow: none; }
|
|
63
|
+
& .select-trigger[data-open], & .select-trigger:focus-visible { background: transparent; box-shadow: inset 0 0 0 1px ${c.bgBrandSolid}; }
|
|
64
|
+
`,
|
|
65
|
+
},
|
|
66
|
+
// full (default): fills its container, as before. auto: sizes to content.
|
|
67
|
+
width: {
|
|
68
|
+
full: `width: 100%;`,
|
|
69
|
+
auto: `width: auto;`,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
defaultVariants: { variant: "default", width: "full" },
|
|
73
|
+
compoundVariants: [],
|
|
74
|
+
});
|
|
75
|
+
export const selectRecipe = Object.assign(resolveRecipe(config), { config });
|
|
76
|
+
export default config;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { skeletonRecipe } from "./skeleton.recipe.js";
|
|
3
|
+
/** A pulsing placeholder block sized to the content it stands in for. */
|
|
4
|
+
export function Skeleton({ width, height = 16, circle, radius, className, }) {
|
|
5
|
+
return (_jsx("span", { "aria-hidden": "true", "data-circle": circle ? "" : undefined, className: skeletonRecipe({ className }), style: { width, height, borderRadius: radius } }));
|
|
6
|
+
}
|
|
7
|
+
Skeleton.displayName = "Skeleton";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { defineRecipe, resolveRecipe } from "../../core/index.js";
|
|
2
|
+
import { contract as c } from "../../tokens/index.js";
|
|
3
|
+
// A pulsing placeholder block for loading states.
|
|
4
|
+
const config = defineRecipe({
|
|
5
|
+
name: "skeleton",
|
|
6
|
+
globalCss: `@keyframes crisp-skeleton { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }`,
|
|
7
|
+
base: `display: block; background: color-mix(in srgb, ${c.fgPrimary} 8%, transparent); border-radius: ${c.radius.md}; animation: crisp-skeleton 1.4s ease-in-out infinite;
|
|
8
|
+
&[data-circle] { border-radius: ${c.radius.full}; }`,
|
|
9
|
+
variants: {},
|
|
10
|
+
defaultVariants: {},
|
|
11
|
+
compoundVariants: [],
|
|
12
|
+
});
|
|
13
|
+
export const skeletonRecipe = resolveRecipe(config);
|
|
14
|
+
export default config;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { spinnerRecipe } from "./spinner.recipe.js";
|
|
3
|
+
/** A spinning ring indicator. Reports `role="status"` for assistive tech. */
|
|
4
|
+
export function Spinner({ size = 16, "aria-label": ariaLabel = "Loading", className, }) {
|
|
5
|
+
return (_jsx("span", { role: "status", "aria-label": ariaLabel, className: spinnerRecipe({ className }), style: {
|
|
6
|
+
width: size,
|
|
7
|
+
height: size,
|
|
8
|
+
borderWidth: Math.max(2, Math.round(size / 8)),
|
|
9
|
+
} }));
|
|
10
|
+
}
|
|
11
|
+
Spinner.displayName = "Spinner";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineRecipe, resolveRecipe } from "../../core/index.js";
|
|
2
|
+
import { contract as c } from "../../tokens/index.js";
|
|
3
|
+
// A spinning ring loading indicator.
|
|
4
|
+
const config = defineRecipe({
|
|
5
|
+
name: "spinner",
|
|
6
|
+
globalCss: `@keyframes crisp-spin { to { transform: rotate(360deg); } }`,
|
|
7
|
+
base: `display: inline-block; box-sizing: border-box; width: 16px; height: 16px; border-radius: ${c.radius.full}; border: 2px solid color-mix(in srgb, ${c.fgPrimary} 15%, transparent); border-top-color: ${c.fgMuted}; animation: crisp-spin 0.6s linear infinite;`,
|
|
8
|
+
variants: {},
|
|
9
|
+
defaultVariants: {},
|
|
10
|
+
compoundVariants: [],
|
|
11
|
+
});
|
|
12
|
+
export const spinnerRecipe = resolveRecipe(config);
|
|
13
|
+
export default config;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { stagebarRecipe } from "./stagebar.recipe.js";
|
|
3
|
+
const DEFAULT = "#266df0";
|
|
4
|
+
/**
|
|
5
|
+
* A pipeline stage bar — measured from Attio's Deal-stage Highlight. Each stage
|
|
6
|
+
* is a segment tinted with its colour; the current stage is at full strength.
|
|
7
|
+
*/
|
|
8
|
+
export function StageBar({ stages, current, "aria-label": ariaLabel, className, }) {
|
|
9
|
+
const currentIndex = typeof current === "number"
|
|
10
|
+
? current
|
|
11
|
+
: stages.findIndex((s) => s.label === current);
|
|
12
|
+
const currentStage = stages[currentIndex];
|
|
13
|
+
return (_jsx("div", { className: stagebarRecipe({ className }), role: "progressbar", "aria-valuemin": 0, "aria-valuemax": stages.length, "aria-valuenow": currentIndex + 1, "aria-valuetext": currentStage?.label, "aria-label": ariaLabel, children: stages.map((stage, i) => {
|
|
14
|
+
const color = stage.color ?? DEFAULT;
|
|
15
|
+
const on = i === currentIndex;
|
|
16
|
+
return (_jsx("span", { className: "stagebar-seg", style: {
|
|
17
|
+
background: on
|
|
18
|
+
? color
|
|
19
|
+
: `color-mix(in srgb, ${color} 18%, transparent)`,
|
|
20
|
+
} }, stage.label));
|
|
21
|
+
}) }));
|
|
22
|
+
}
|
|
23
|
+
StageBar.displayName = "StageBar";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { defineRecipe, resolveRecipe } from "../../core/index.js";
|
|
2
|
+
import { contract as c } from "../../tokens/index.js";
|
|
3
|
+
// A pipeline stage indicator — Attio's Deal-stage Highlight bar: one segment per
|
|
4
|
+
// stage, each tinted with its stage colour, the current stage at full strength.
|
|
5
|
+
const config = defineRecipe({
|
|
6
|
+
name: "stagebar",
|
|
7
|
+
base: `display: flex; align-items: center; gap: ${c.space["0.5"]}; width: 100%; height: 6px; font-family: ${c.font.sans};
|
|
8
|
+
& .stagebar-seg { flex: 1; height: 100%; border-radius: ${c.radius.full}; transition: background 0.2s ease; }`,
|
|
9
|
+
variants: {},
|
|
10
|
+
defaultVariants: {},
|
|
11
|
+
compoundVariants: [],
|
|
12
|
+
});
|
|
13
|
+
export const stagebarRecipe = resolveRecipe(config);
|
|
14
|
+
export default config;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ArrowDown, ArrowUp } from "lucide-react";
|
|
3
|
+
import { statcardRecipe } from "./statcard.recipe.js";
|
|
4
|
+
/**
|
|
5
|
+
* A single-metric KPI tile — measured from Attio's Reports number/insight cards.
|
|
6
|
+
* Label, a large value, and an optional up/down delta pill with a caption.
|
|
7
|
+
*/
|
|
8
|
+
export function StatCard({ label, icon, value, delta, deltaSuffix = "%", caption, card, className, }) {
|
|
9
|
+
const trend = delta == null ? null : delta > 0 ? "up" : delta < 0 ? "down" : "flat";
|
|
10
|
+
return (_jsxs("div", { className: statcardRecipe({ card, className }), children: [_jsxs("span", { className: "statcard-label", children: [icon, label] }), _jsx("span", { className: "statcard-value", children: value }), (trend != null || caption != null) && (_jsxs("span", { className: "statcard-foot", children: [trend != null && (_jsxs("span", { className: "statcard-delta", "data-trend": trend, children: [trend === "up" && _jsx(ArrowUp, { size: 12, strokeWidth: 2.5 }), trend === "down" && _jsx(ArrowDown, { size: 12, strokeWidth: 2.5 }), Math.abs(delta), deltaSuffix] })), caption] }))] }));
|
|
11
|
+
}
|
|
12
|
+
StatCard.displayName = "StatCard";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { defineRecipe, resolveRecipe } from "../../core/index.js";
|
|
2
|
+
import { contract as c } from "../../tokens/index.js";
|
|
3
|
+
// A single-metric KPI tile — measured from Attio's Reports "Insight" number
|
|
4
|
+
// report: a small muted label, a large semibold value, and an optional delta
|
|
5
|
+
// pill (green up / red down) with a caption. Fills its container.
|
|
6
|
+
const config = defineRecipe({
|
|
7
|
+
name: "statcard",
|
|
8
|
+
base: `display: flex; flex-direction: column; gap: ${c.space["1.5"]}; box-sizing: border-box; font-family: ${c.font.sans};
|
|
9
|
+
-webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
10
|
+
& .statcard-label { display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: ${c.fgMuted}; }
|
|
11
|
+
& .statcard-label svg { color: ${c.fgMuted}; }
|
|
12
|
+
& .statcard-value { font-size: 28px; line-height: 32px; font-weight: ${c.weight.semibold}; letter-spacing: -0.4px; color: ${c.fgPrimary}; }
|
|
13
|
+
& .statcard-foot { display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; font-size: ${c.text.sm}; color: ${c.fgMuted}; }
|
|
14
|
+
& .statcard-delta { display: inline-flex; align-items: center; gap: 2px; height: 20px; padding: 0 ${c.space["1"]}; border-radius: ${c.radius.sm}; font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; }
|
|
15
|
+
& .statcard-delta svg { flex-shrink: 0; }
|
|
16
|
+
& .statcard-delta[data-trend="up"] { background: color-mix(in srgb, ${c.bgSuccess} 16%, transparent); color: color-mix(in srgb, ${c.bgSuccess} 80%, ${c.fgPrimary}); }
|
|
17
|
+
& .statcard-delta[data-trend="down"] { background: color-mix(in srgb, ${c.bgDanger} 15%, transparent); color: color-mix(in srgb, ${c.bgDanger} 80%, ${c.fgPrimary}); }
|
|
18
|
+
& .statcard-delta[data-trend="flat"] { background: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); color: ${c.fgMuted}; }`,
|
|
19
|
+
variants: {
|
|
20
|
+
// Opt-in chrome for consumers who want the tile self-contained instead of
|
|
21
|
+
// hand-wrapping it in <Card><CardBody>. Not measured as part of the Attio
|
|
22
|
+
// report tile itself (the tile sits bare inside the report's own Card —
|
|
23
|
+
// see the docs page) — reuses Card's exact border/radius/bg rather than
|
|
24
|
+
// inventing a new value, since it is only ever an explicit opt-in.
|
|
25
|
+
card: {
|
|
26
|
+
true: `padding: ${c.space["4"]}; border-radius: ${c.radius.xl}; background: ${c.bgSurface}; box-shadow: inset 0 0 0 1px ${c.borderSubtle};`,
|
|
27
|
+
false: ``,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
defaultVariants: { card: false },
|
|
31
|
+
compoundVariants: [],
|
|
32
|
+
});
|
|
33
|
+
export const statcardRecipe = resolveRecipe(config);
|
|
34
|
+
export default config;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { switchRecipe } from "./switch.recipe.js";
|
|
4
|
+
/**
|
|
5
|
+
* An on/off toggle — the settings switch, measured from Attio. Renders a
|
|
6
|
+
* `role="switch"` button (a native checkbox's semantics without its box), so
|
|
7
|
+
* label association and keyboard (Space/Enter) come for free. Controlled via
|
|
8
|
+
* `checked`/`onCheckedChange`, or uncontrolled from `defaultChecked`.
|
|
9
|
+
*/
|
|
10
|
+
export const Switch = React.forwardRef(({ checked, defaultChecked = false, onCheckedChange, disabled, className, onClick, ...props }, ref) => {
|
|
11
|
+
const [internal, setInternal] = React.useState(defaultChecked);
|
|
12
|
+
const on = checked ?? internal;
|
|
13
|
+
return (_jsx("button", { ref: ref, type: "button", role: "switch", "aria-checked": on, disabled: disabled, "data-checked": on ? "" : undefined, className: switchRecipe({ className }), onClick: (e) => {
|
|
14
|
+
onClick?.(e);
|
|
15
|
+
if (e.defaultPrevented)
|
|
16
|
+
return;
|
|
17
|
+
if (checked === undefined)
|
|
18
|
+
setInternal((v) => !v);
|
|
19
|
+
onCheckedChange?.(!on);
|
|
20
|
+
}, ...props, children: _jsx("span", { className: "switch-thumb" }) }));
|
|
21
|
+
});
|
|
22
|
+
Switch.displayName = "Switch";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { defineRecipe, resolveRecipe } from "../../core/index.js";
|
|
2
|
+
import { contract as c } from "../../tokens/index.js";
|
|
3
|
+
// Attio's settings toggle, measured from the live app: a 24×16 pill track that
|
|
4
|
+
// flips from a muted grey to brand blue (#266df0), with a 12px white thumb
|
|
5
|
+
// (2px inset) that slides 8px across. background + transform both animate .2s.
|
|
6
|
+
const config = defineRecipe({
|
|
7
|
+
name: "switch",
|
|
8
|
+
base: `
|
|
9
|
+
display: inline-flex; align-items: center; flex-shrink: 0; width: 24px; height: 16px; padding: 0; margin: 0;
|
|
10
|
+
border: none; border-radius: ${c.radius.full}; background: color-mix(in srgb, ${c.fgPrimary} 9%, transparent);
|
|
11
|
+
cursor: pointer; transition: background-color .2s ease; -webkit-tap-highlight-color: transparent;
|
|
12
|
+
& .switch-thumb { width: 12px; height: 12px; margin: 0 2px; border-radius: ${c.radius.full}; background: #fff; transition: transform .2s ease; }
|
|
13
|
+
&[data-checked] { background: ${c.bgBrandSolid}; }
|
|
14
|
+
&[data-checked] .switch-thumb { transform: translateX(8px); }
|
|
15
|
+
&:focus-visible { outline: 2px solid ${c.bgBrandSolid}; outline-offset: 2px; }
|
|
16
|
+
&:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
17
|
+
`,
|
|
18
|
+
variants: {},
|
|
19
|
+
defaultVariants: {},
|
|
20
|
+
compoundVariants: [],
|
|
21
|
+
});
|
|
22
|
+
export const switchRecipe = Object.assign(resolveRecipe(config), {
|
|
23
|
+
config,
|
|
24
|
+
meta: { track: { width: "24px", height: "16px" }, thumb: { size: "12px" } },
|
|
25
|
+
});
|
|
26
|
+
export default config;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { tableRecipe } from "./table.recipe.js";
|
|
4
|
+
/**
|
|
5
|
+
* A composable data table. Wraps `<table>` in a horizontally scrollable
|
|
6
|
+
* surface — `surface="card"` (default) bordered + rounded, `surface="bleed"`
|
|
7
|
+
* edge-to-edge like RecordsTable's full-page grid. Compose with
|
|
8
|
+
* `TableHeader`, `TableBody`, `TableFooter`, `TableRow`, `TableHead`,
|
|
9
|
+
* `TableCell`, and `TableCaption`.
|
|
10
|
+
*/
|
|
11
|
+
export const Table = React.forwardRef(({ interactive, surface, className, wrapperClassName, scrollProps, children, ...props }, ref) => (_jsx("div", { className: tableRecipe({
|
|
12
|
+
interactive,
|
|
13
|
+
surface,
|
|
14
|
+
className: wrapperClassName,
|
|
15
|
+
}), children: _jsx("div", { ...scrollProps, className: `table-scroll${scrollProps?.className ? ` ${scrollProps.className}` : ""}`, children: _jsx("table", { ref: ref, className: className, ...props, children: children }) }) })));
|
|
16
|
+
Table.displayName = "Table";
|
|
17
|
+
export const TableHeader = React.forwardRef((props, ref) => _jsx("thead", { ref: ref, ...props }));
|
|
18
|
+
TableHeader.displayName = "TableHeader";
|
|
19
|
+
export const TableBody = React.forwardRef((props, ref) => _jsx("tbody", { ref: ref, ...props }));
|
|
20
|
+
TableBody.displayName = "TableBody";
|
|
21
|
+
export const TableFooter = React.forwardRef((props, ref) => _jsx("tfoot", { ref: ref, ...props }));
|
|
22
|
+
TableFooter.displayName = "TableFooter";
|
|
23
|
+
export const TableRow = React.forwardRef((props, ref) => _jsx("tr", { ref: ref, ...props }));
|
|
24
|
+
TableRow.displayName = "TableRow";
|
|
25
|
+
export const TableHead = React.forwardRef(({ align, ...props }, ref) => _jsx("th", { ref: ref, "data-align": align, ...props }));
|
|
26
|
+
TableHead.displayName = "TableHead";
|
|
27
|
+
export const TableCell = React.forwardRef(({ align, ...props }, ref) => _jsx("td", { ref: ref, "data-align": align, ...props }));
|
|
28
|
+
TableCell.displayName = "TableCell";
|
|
29
|
+
export const TableCaption = React.forwardRef((props, ref) => _jsx("caption", { ref: ref, ...props }));
|
|
30
|
+
TableCaption.displayName = "TableCaption";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { defineRecipe, resolveRecipe } from "../../core/index.js";
|
|
2
|
+
import { contract as c } from "../../tokens/index.js";
|
|
3
|
+
// A general-purpose, composable data table — the lightweight primitive for
|
|
4
|
+
// arbitrary tabular data (as opposed to `product/recordstable`, which is a
|
|
5
|
+
// faithful rebuild of Attio's heavy records grid). It carries the SAME measured
|
|
6
|
+
// grid styling as RecordsTable — 36px rows, a 40px header, hairline vertical +
|
|
7
|
+
// horizontal grid lines — so `DataTable` (built on this) matches that polish.
|
|
8
|
+
const config = defineRecipe({
|
|
9
|
+
name: "table",
|
|
10
|
+
base: `display: block; box-sizing: border-box; background: ${c.bgSurface};
|
|
11
|
+
& .table-scroll { overflow-x: auto; }
|
|
12
|
+
& table { width: 100%; border-collapse: separate; border-spacing: 0; font-family: ${c.font.sans}; font-size: ${c.text.lg}; color: ${c.fgPrimary}; }
|
|
13
|
+
& th, & td { position: relative; height: ${c.size.tableRow}; padding: 0 ${c.space["2"]}; border-bottom: 1px solid ${c.borderSubtle}; border-right: 1px solid ${c.borderSubtle}; text-align: left; vertical-align: middle; white-space: nowrap; overflow: hidden; }
|
|
14
|
+
& th { height: 40px; background: ${c.bgSurface}; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; letter-spacing: -0.14px; color: ${c.fgPrimary}; user-select: none; }
|
|
15
|
+
& td { color: ${c.fgPrimary}; background: ${c.bgSurface}; }
|
|
16
|
+
& th:last-child, & td:last-child { border-right: none; }
|
|
17
|
+
& tbody tr:last-child td { border-bottom: none; }
|
|
18
|
+
& tfoot td { border-top: 1px solid ${c.borderSubtle}; border-bottom: none; font-weight: ${c.weight.medium}; color: ${c.fgMuted}; }
|
|
19
|
+
& caption { padding: ${c.space["2"]} ${c.space["2"]}; text-align: left; font-size: ${c.text.sm}; color: ${c.fgMuted}; caption-side: bottom; }
|
|
20
|
+
& td[data-align="right"], & th[data-align="right"] { text-align: right; }
|
|
21
|
+
& td[data-align="center"], & th[data-align="center"] { text-align: center; }`,
|
|
22
|
+
variants: {
|
|
23
|
+
// subtle row hover — matches RecordsTable's 3% tint
|
|
24
|
+
interactive: {
|
|
25
|
+
true: `& tbody tr { transition: background-color .12s; } & tbody tr:hover td { background: color-mix(in srgb, ${c.fgPrimary} 3%, ${c.bgSurface}); }`,
|
|
26
|
+
false: ``,
|
|
27
|
+
},
|
|
28
|
+
// card (default): the bordered rounded surface, unchanged for existing
|
|
29
|
+
// consumers. bleed: drops the outer border/radius/inset so the grid runs
|
|
30
|
+
// edge-to-edge, like RecordsTable's real full-page records grid.
|
|
31
|
+
surface: {
|
|
32
|
+
card: `border: 1px solid ${c.borderSubtle}; border-radius: ${c.radius.xl}; overflow: hidden;`,
|
|
33
|
+
bleed: `border: none; border-radius: 0;`,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
defaultVariants: { interactive: false, surface: "card" },
|
|
37
|
+
compoundVariants: [],
|
|
38
|
+
});
|
|
39
|
+
export const tableRecipe = resolveRecipe(config);
|
|
40
|
+
export default config;
|