@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,57 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { CalendarDays, ChevronLeft, ChevronRight } from "lucide-react";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Popover } from "../popover/popover.js";
|
|
5
|
+
import { datepickerRecipe } from "./datepicker.recipe.js";
|
|
6
|
+
const DOW = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
|
|
7
|
+
const MONTHS = [
|
|
8
|
+
"January",
|
|
9
|
+
"February",
|
|
10
|
+
"March",
|
|
11
|
+
"April",
|
|
12
|
+
"May",
|
|
13
|
+
"June",
|
|
14
|
+
"July",
|
|
15
|
+
"August",
|
|
16
|
+
"September",
|
|
17
|
+
"October",
|
|
18
|
+
"November",
|
|
19
|
+
"December",
|
|
20
|
+
];
|
|
21
|
+
const sameDay = (a, b) => a.getFullYear() === b.getFullYear() &&
|
|
22
|
+
a.getMonth() === b.getMonth() &&
|
|
23
|
+
a.getDate() === b.getDate();
|
|
24
|
+
const defaultFormat = (d) => `${MONTHS[d.getMonth()].slice(0, 3)} ${d.getDate()}, ${d.getFullYear()}`;
|
|
25
|
+
/**
|
|
26
|
+
* A date field with a calendar popover — composes [Popover] with a month grid.
|
|
27
|
+
* Controlled via `value`/`onChange` or self-managed from `defaultValue`.
|
|
28
|
+
*/
|
|
29
|
+
export function DatePicker({ value, defaultValue, onChange, placeholder = "Set date…", variant = "default", format = defaultFormat, "aria-label": ariaLabel, className, }) {
|
|
30
|
+
const [internal, setInternal] = React.useState(defaultValue ?? null);
|
|
31
|
+
const selected = value ?? internal;
|
|
32
|
+
const [open, setOpen] = React.useState(false);
|
|
33
|
+
const [view, setView] = React.useState(() => selected ?? new Date());
|
|
34
|
+
// when reopening, jump the view to the selected month
|
|
35
|
+
React.useEffect(() => {
|
|
36
|
+
if (open)
|
|
37
|
+
setView(selected ?? new Date());
|
|
38
|
+
}, [open, selected]);
|
|
39
|
+
const pick = (d) => {
|
|
40
|
+
if (value === undefined)
|
|
41
|
+
setInternal(d);
|
|
42
|
+
onChange?.(d);
|
|
43
|
+
setOpen(false);
|
|
44
|
+
};
|
|
45
|
+
const today = new Date();
|
|
46
|
+
const year = view.getFullYear();
|
|
47
|
+
const month = view.getMonth();
|
|
48
|
+
const firstWeekday = new Date(year, month, 1).getDay();
|
|
49
|
+
const start = new Date(year, month, 1 - firstWeekday);
|
|
50
|
+
return (_jsx("div", { className: datepickerRecipe({ className }), children: _jsx(Popover, { open: open, onOpenChange: setOpen, "aria-label": ariaLabel ?? "Choose date", trigger: _jsxs("button", { type: "button", className: "datepicker-trigger", "data-ghost": variant === "ghost" ? "" : undefined, "data-placeholder": selected ? undefined : "", "aria-label": ariaLabel, children: [_jsx(CalendarDays, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), selected ? format(selected) : placeholder] }), children: _jsxs("div", { className: "datepicker-cal", children: [_jsxs("div", { className: "datepicker-head", children: [_jsx("button", { type: "button", className: "datepicker-nav", "aria-label": "Previous month", onClick: () => setView(new Date(year, month - 1, 1)), children: _jsx(ChevronLeft, { size: 16, strokeWidth: 1.75 }) }), _jsxs("span", { className: "datepicker-title", children: [MONTHS[month], " ", year] }), _jsx("button", { type: "button", className: "datepicker-nav", "aria-label": "Next month", onClick: () => setView(new Date(year, month + 1, 1)), children: _jsx(ChevronRight, { size: 16, strokeWidth: 1.75 }) })] }), _jsxs("div", { className: "datepicker-grid", role: "grid", children: [DOW.map((d) => (_jsx("span", { className: "datepicker-dow", "aria-hidden": "true", children: d }, d))), Array.from({ length: 42 }, (_, i) => {
|
|
51
|
+
const day = new Date(start.getFullYear(), start.getMonth(), start.getDate() + i);
|
|
52
|
+
const outside = day.getMonth() !== month;
|
|
53
|
+
const isSel = selected != null && sameDay(day, selected);
|
|
54
|
+
return (_jsx("button", { type: "button", className: "datepicker-day", "aria-label": format(day), "aria-pressed": isSel, "data-outside": outside ? "" : undefined, "data-today": sameDay(day, today) ? "" : undefined, "data-selected": isSel ? "" : undefined, onClick: () => pick(day), children: day.getDate() }, day.toISOString()));
|
|
55
|
+
})] })] }) }) }));
|
|
56
|
+
}
|
|
57
|
+
DatePicker.displayName = "DatePicker";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { defineRecipe, resolveRecipe } from "../../core/index.js";
|
|
2
|
+
import { contract as c } from "../../tokens/index.js";
|
|
3
|
+
// A date field + calendar popover — measured from Attio's date cell editor: an
|
|
4
|
+
// outlined trigger showing the value, opening a month grid (weekday row, day
|
|
5
|
+
// cells, today outline, selected day brand-filled).
|
|
6
|
+
const config = defineRecipe({
|
|
7
|
+
name: "datepicker",
|
|
8
|
+
base: `display: contents; font-family: ${c.font.sans};
|
|
9
|
+
& .datepicker-trigger { display: inline-flex; align-items: center; gap: ${c.space["2"]}; box-sizing: border-box; height: 32px; padding: 0 ${c.space["2.5"]}; margin: 0; border: none; border-radius: ${c.radius.md}; background: transparent; box-shadow: inset 0 0 0 1px color-mix(in srgb, ${c.fgPrimary} 7%, transparent); color: ${c.fgPrimary}; font-family: inherit; font-size: ${c.text.lg}; white-space: nowrap; cursor: pointer; }
|
|
10
|
+
& .datepicker-trigger[data-ghost] { background: transparent; box-shadow: none; }
|
|
11
|
+
& .datepicker-trigger[data-ghost]:hover { background: color-mix(in srgb, ${c.fgPrimary} 4%, transparent); }
|
|
12
|
+
& .datepicker-trigger[data-placeholder] { color: ${c.fgMuted}; }
|
|
13
|
+
& .datepicker-trigger svg { flex-shrink: 0; color: ${c.fgMuted}; }
|
|
14
|
+
& .datepicker-cal { width: 244px; }
|
|
15
|
+
& .datepicker-head { display: flex; align-items: center; justify-content: space-between; height: 30px; padding: 0 ${c.space["1"]}; }
|
|
16
|
+
& .datepicker-title { font-size: ${c.text.md}; font-weight: ${c.weight.semibold}; color: ${c.fgPrimary}; }
|
|
17
|
+
& .datepicker-nav { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; margin: 0; padding: 0; border: none; border-radius: ${c.radius.md}; background: none; color: ${c.fgMuted}; cursor: pointer; }
|
|
18
|
+
& .datepicker-nav:hover { background: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); color: ${c.fgPrimary}; }
|
|
19
|
+
& .datepicker-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; margin-top: ${c.space["1"]}; }
|
|
20
|
+
& .datepicker-dow { display: flex; align-items: center; justify-content: center; height: 24px; font-size: ${c.text.xs}; font-weight: ${c.weight.medium}; color: ${c.fgMuted}; }
|
|
21
|
+
& .datepicker-day { display: flex; align-items: center; justify-content: center; height: 30px; margin: 0; padding: 0; border: none; border-radius: ${c.radius.md}; background: none; color: ${c.fgPrimary}; font-family: inherit; font-size: ${c.text.md}; cursor: pointer; }
|
|
22
|
+
& .datepicker-day:hover { background: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); }
|
|
23
|
+
& .datepicker-day[data-outside] { color: ${c.fgMuted}; opacity: 0.5; }
|
|
24
|
+
& .datepicker-day[data-today] { box-shadow: inset 0 0 0 1px ${c.borderDefault}; }
|
|
25
|
+
& .datepicker-day[data-selected] { background: ${c.bgBrandSolid}; color: ${c.fgOnBrand}; }
|
|
26
|
+
& .datepicker-day[data-selected]:hover { background: ${c.bgBrandSolidPressed}; }`,
|
|
27
|
+
variants: {},
|
|
28
|
+
defaultVariants: {},
|
|
29
|
+
compoundVariants: [],
|
|
30
|
+
});
|
|
31
|
+
export const datepickerRecipe = resolveRecipe(config);
|
|
32
|
+
export default config;
|
|
@@ -0,0 +1,48 @@
|
|
|
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 { X } from "lucide-react";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
import { dialogRecipe } from "./dialog.recipe.js";
|
|
7
|
+
// A globally unique id per mounted instance (see command.tsx for the rationale —
|
|
8
|
+
// the machine defers its dismissal listeners a frame, so tree-relative useId
|
|
9
|
+
// values could let a stale registration resolve to a new instance's node).
|
|
10
|
+
let instanceCounter = 0;
|
|
11
|
+
function useDialogInstanceId() {
|
|
12
|
+
const idRef = React.useRef(null);
|
|
13
|
+
if (idRef.current == null) {
|
|
14
|
+
instanceCounter += 1;
|
|
15
|
+
idRef.current = `crisp-dialog-${instanceCounter}`;
|
|
16
|
+
}
|
|
17
|
+
return idRef.current;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A modal dialog — composes Zag's dialog machine (focus trap, scroll lock,
|
|
21
|
+
* Escape / outside-click dismissal) with the measured Attio modal surface.
|
|
22
|
+
* Compose the body with `DialogBody` and actions with `DialogFooter`.
|
|
23
|
+
*/
|
|
24
|
+
export function Dialog({ open, onOpenChange, title, icon, width = 640, "aria-label": ariaLabel, children, }) {
|
|
25
|
+
const id = useDialogInstanceId();
|
|
26
|
+
const service = useMachine(dialog.machine, {
|
|
27
|
+
id,
|
|
28
|
+
open,
|
|
29
|
+
onOpenChange(details) {
|
|
30
|
+
onOpenChange(details.open);
|
|
31
|
+
},
|
|
32
|
+
// Focus the first form field on open (like the command palette focuses its
|
|
33
|
+
// search), rather than the close button; fall back to the machine default.
|
|
34
|
+
initialFocusEl: () => document.querySelector(`[data-crisp-dialog="${id}"] .dialog-body :where(input, textarea, select, [contenteditable]):not([disabled])`),
|
|
35
|
+
"aria-label": typeof title === "string" ? title : ariaLabel,
|
|
36
|
+
});
|
|
37
|
+
const api = dialog.connect(service, normalizeProps);
|
|
38
|
+
if (!api.open)
|
|
39
|
+
return null;
|
|
40
|
+
return (_jsx(Portal, { children: _jsxs("div", { className: dialogRecipe(), children: [_jsx("div", { ...api.getBackdropProps(), className: "dialog-backdrop" }), _jsx("div", { ...api.getPositionerProps(), className: "dialog-positioner", children: _jsx("div", { ...api.getContentProps(), className: "dialog-content", "data-crisp-dialog": id, style: { maxWidth: `min(${width}px, 90vw)` }, children: _jsxs("div", { className: "dialog-card", children: [title != null && (_jsxs("div", { className: "dialog-header", children: [icon, _jsx("span", { ...api.getTitleProps(), className: "dialog-title", children: title }), _jsx("button", { type: "button", ...api.getCloseTriggerProps(), className: "dialog-close", "aria-label": "Close", children: _jsx(X, { size: 16, strokeWidth: 2, "aria-hidden": "true" }) })] })), children] }) }) })] }) }));
|
|
41
|
+
}
|
|
42
|
+
Dialog.displayName = "Dialog";
|
|
43
|
+
export function DialogBody({ children, className, }) {
|
|
44
|
+
return (_jsx("div", { className: className ? `dialog-body ${className}` : "dialog-body", children: children }));
|
|
45
|
+
}
|
|
46
|
+
export function DialogFooter({ children }) {
|
|
47
|
+
return _jsx("div", { className: "dialog-footer", children: children });
|
|
48
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { defineRecipe, resolveRecipe } from "../../core/index.js";
|
|
2
|
+
import { contract as c } from "../../tokens/index.js";
|
|
3
|
+
// A modal dialog surface — measured from Attio's create-record modal: a radius-16
|
|
4
|
+
// frosted-glass card (60% tinted background + 15px backdrop blur, both re-measured
|
|
5
|
+
// per theme — light uses a blue-tinted 3-layer shadow, dark drops to a plain
|
|
6
|
+
// 2-layer black shadow) with a bordered header (title + close), a scrollable
|
|
7
|
+
// body, and a bordered footer for the primary action.
|
|
8
|
+
const config = defineRecipe({
|
|
9
|
+
name: "dialog",
|
|
10
|
+
base: `display: contents; font-family: ${c.font.sans};
|
|
11
|
+
& .dialog-backdrop { position: fixed; inset: 0; z-index: 60; background: transparent; }
|
|
12
|
+
& .dialog-positioner { position: fixed; inset: 0; z-index: 61; display: flex; align-items: flex-start; justify-content: center; padding: 15vh ${c.space["4"]} ${c.space["4"]}; }
|
|
13
|
+
& .dialog-content { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; position: relative; display: flex; flex-direction: column; box-sizing: border-box; width: 90vw; max-height: 74vh; padding: ${c.space["1"]}; border-radius: ${c.radius["2xl"]}; background: color-mix(in srgb, ${c.bgOverlayFrost} 60%, transparent); backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px); box-shadow: 0 8px 28px -6px ${c.shadowOverlay1}, 0 18px 88px -4px ${c.shadowOverlay2}, 0 0 1px ${c.shadowOverlay3}; opacity: 1; transform: scale(1); transition: opacity 0.2s ease, transform 0.2s ease; @starting-style { opacity: 0; transform: scale(0.97); } }
|
|
14
|
+
& .dialog-card { display: flex; flex-direction: column; flex: 1; min-height: 0; box-sizing: border-box; border-radius: ${c.radius.xl}; background: ${c.bgSurface}; box-shadow: 0 0 0 1px color-mix(in srgb, ${c.fgPrimary} 8%, transparent); overflow: hidden; }
|
|
15
|
+
& .dialog-header { display: flex; align-items: center; gap: ${c.space["2"]}; flex-shrink: 0; height: 52px; padding: 0 ${c.space["2"]} 0 ${c.space["4"]}; border-bottom: 1px solid ${c.borderSubtle}; }
|
|
16
|
+
& .dialog-header svg { flex-shrink: 0; color: ${c.fgMuted}; }
|
|
17
|
+
& .dialog-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: ${c.fgPrimary}; }
|
|
18
|
+
& .dialog-close { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: 28px; height: 28px; margin: 0; padding: 0; border: none; border-radius: ${c.radius.md}; background: none; color: ${c.fgMuted}; cursor: pointer; }
|
|
19
|
+
& .dialog-close:hover { background: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); color: ${c.fgPrimary}; }
|
|
20
|
+
& .dialog-body { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: ${c.space["4"]}; padding: ${c.space["5"]} ${c.space["4"]}; }
|
|
21
|
+
& .dialog-footer { display: flex; align-items: center; justify-content: flex-end; gap: ${c.space["3"]}; flex-shrink: 0; min-height: 56px; padding: 0 ${c.space["4"]}; border-top: 1px solid ${c.borderSubtle}; }
|
|
22
|
+
& .dialog-field { display: flex; flex-direction: column; gap: ${c.space["1.5"]}; }
|
|
23
|
+
& .dialog-field-label { font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: color-mix(in srgb, ${c.fgPrimary} 50%, transparent); }
|
|
24
|
+
& .dialog-more { display: inline-flex; align-items: center; gap: ${c.space["2"]}; margin-right: auto; font-size: ${c.text.md}; color: ${c.fgMuted}; }
|
|
25
|
+
& .dialog-submit { display: inline-flex; align-items: center; gap: ${c.space["2"]}; height: 28px; padding: 0 ${c.space["1"]} 0 ${c.space["2.5"]}; border: none; border-radius: ${c.radius.md}; background: ${c.bgBrandSolid}; color: ${c.fgOnBrand}; font-family: inherit; font-size: ${c.text.md}; font-weight: ${c.weight.medium}; cursor: pointer; }
|
|
26
|
+
& .dialog-submit-key { display: inline-flex; align-items: center; justify-content: center; height: 18px; padding: 0 ${c.space["1"]}; border-radius: ${c.radius.sm}; background: rgba(255,255,255,0.22); font-size: ${c.text.xs}; }`,
|
|
27
|
+
variants: {},
|
|
28
|
+
defaultVariants: {},
|
|
29
|
+
compoundVariants: [],
|
|
30
|
+
});
|
|
31
|
+
export const dialogRecipe = resolveRecipe(config);
|
|
32
|
+
export default config;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { dividerRecipe } from "./divider.recipe.js";
|
|
3
|
+
/** A hairline separator over `borderSubtle`, horizontal or vertical. */
|
|
4
|
+
export function Divider({ orientation = "horizontal", className, }) {
|
|
5
|
+
return (_jsx("hr", { "aria-orientation": orientation, "data-orientation": orientation, className: dividerRecipe({ className }) }));
|
|
6
|
+
}
|
|
7
|
+
Divider.displayName = "Divider";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { defineRecipe, resolveRecipe } from "../../core/index.js";
|
|
2
|
+
import { contract as c } from "../../tokens/index.js";
|
|
3
|
+
// A hairline separator — horizontal (default) or vertical.
|
|
4
|
+
const config = defineRecipe({
|
|
5
|
+
name: "divider",
|
|
6
|
+
base: `flex-shrink: 0; border: none; margin: 0; background: ${c.borderSubtle};
|
|
7
|
+
&[data-orientation="horizontal"] { width: 100%; height: 1px; }
|
|
8
|
+
&[data-orientation="vertical"] { width: 1px; align-self: stretch; min-height: 1em; }`,
|
|
9
|
+
variants: {},
|
|
10
|
+
defaultVariants: {},
|
|
11
|
+
compoundVariants: [],
|
|
12
|
+
});
|
|
13
|
+
export const dividerRecipe = resolveRecipe(config);
|
|
14
|
+
export default config;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { contract as c } from "../../tokens/index.js";
|
|
3
|
+
import { donutchartRecipe } from "./donutchart.recipe.js";
|
|
4
|
+
// default categorical palette (matches the tag/avatar tones)
|
|
5
|
+
const PALETTE = [
|
|
6
|
+
c.bgBrandSolid,
|
|
7
|
+
c.bgCyan,
|
|
8
|
+
c.bgPurple,
|
|
9
|
+
c.bgOrange,
|
|
10
|
+
c.bgSuccess,
|
|
11
|
+
c.bgWarning,
|
|
12
|
+
c.bgDanger,
|
|
13
|
+
];
|
|
14
|
+
/**
|
|
15
|
+
* A donut / ring chart — measured from Attio's Reports breakdown reports. SVG
|
|
16
|
+
* ring using the r=15.915 (circumference 100) trick so slices are percentages;
|
|
17
|
+
* a centred total and an optional legend. `role="img"` with a summary.
|
|
18
|
+
*/
|
|
19
|
+
export function DonutChart({ data, size = 120, thickness = 14, centerValue, centerCaption, legend = true, format = (n) => n.toLocaleString(), "aria-label": ariaLabel, className, }) {
|
|
20
|
+
const total = data.reduce((s, d) => s + Math.max(0, d.value), 0);
|
|
21
|
+
const R = 15.915; // circumference ≈ 100
|
|
22
|
+
let cursor = 0;
|
|
23
|
+
const slices = data.map((d, i) => {
|
|
24
|
+
const pct = total > 0 ? (Math.max(0, d.value) / total) * 100 : 0;
|
|
25
|
+
const slice = {
|
|
26
|
+
...d,
|
|
27
|
+
color: d.color ?? PALETTE[i % PALETTE.length],
|
|
28
|
+
pct,
|
|
29
|
+
offset: 25 - cursor, // 25 = start at 12 o'clock, clockwise
|
|
30
|
+
};
|
|
31
|
+
cursor += pct;
|
|
32
|
+
return slice;
|
|
33
|
+
});
|
|
34
|
+
const summary = ariaLabel ??
|
|
35
|
+
`Donut chart: ${data.map((d) => `${d.label} ${format(d.value)}`).join(", ")}`;
|
|
36
|
+
return (_jsxs("div", { className: donutchartRecipe({ className }), role: "img", "aria-label": summary, children: [_jsxs("div", { className: "donutchart-ring", style: { width: size, height: size }, children: [_jsxs("svg", { className: "donutchart-svg", width: size, height: size, viewBox: "0 0 42 42", "aria-hidden": "true", children: [_jsx("circle", { className: "donutchart-track", cx: 21, cy: 21, r: R, strokeWidth: (thickness / size) * 42 }), slices.map((s) => (_jsx("circle", { className: "donutchart-seg", cx: 21, cy: 21, r: R, stroke: s.color, strokeWidth: (thickness / size) * 42, strokeDasharray: `${s.pct} ${100 - s.pct}`, strokeDashoffset: s.offset, children: _jsx("title", { children: `${s.label}: ${format(s.value)}` }) }, s.label)))] }), _jsxs("div", { className: "donutchart-center", children: [_jsx("span", { className: "donutchart-total", children: centerValue ?? format(total) }), centerCaption != null && (_jsx("span", { className: "donutchart-caption", children: centerCaption }))] })] }), legend && (_jsx("div", { className: "donutchart-legend", children: slices.map((s) => (_jsxs("span", { className: "donutchart-item", children: [_jsx("span", { className: "donutchart-swatch", style: { background: s.color } }), _jsx("span", { className: "donutchart-name", children: s.label }), _jsx("span", { className: "donutchart-val", children: format(s.value) })] }, s.label))) }))] }));
|
|
37
|
+
}
|
|
38
|
+
DonutChart.displayName = "DonutChart";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { defineRecipe, resolveRecipe } from "../../core/index.js";
|
|
2
|
+
import { contract as c } from "../../tokens/index.js";
|
|
3
|
+
// A donut / ring chart — measured from Attio's Reports breakdown reports: a
|
|
4
|
+
// segmented ring with a centred total, plus a legend of coloured swatches with
|
|
5
|
+
// labels and values. Pure SVG ring; HTML legend.
|
|
6
|
+
const config = defineRecipe({
|
|
7
|
+
name: "donutchart",
|
|
8
|
+
base: `display: inline-flex; align-items: center; gap: ${c.space["4"]}; box-sizing: border-box; font-family: ${c.font.sans};
|
|
9
|
+
-webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
10
|
+
& .donutchart-ring { position: relative; flex-shrink: 0; }
|
|
11
|
+
& .donutchart-svg { display: block; }
|
|
12
|
+
& .donutchart-seg { fill: none; transition: stroke-dasharray 0.3s ease; }
|
|
13
|
+
& .donutchart-track { fill: none; stroke: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); }
|
|
14
|
+
& .donutchart-center { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; }
|
|
15
|
+
& .donutchart-total { font-size: ${c.text.xl}; font-weight: ${c.weight.semibold}; letter-spacing: -0.3px; color: ${c.fgPrimary}; }
|
|
16
|
+
& .donutchart-caption { font-size: ${c.text.xs}; color: ${c.fgMuted}; }
|
|
17
|
+
& .donutchart-legend { display: flex; flex-direction: column; gap: ${c.space["1.5"]}; min-width: 0; }
|
|
18
|
+
& .donutchart-item { display: inline-flex; align-items: center; gap: ${c.space["2"]}; font-size: ${c.text.md}; color: ${c.fgPrimary}; }
|
|
19
|
+
& .donutchart-swatch { width: 10px; height: 10px; flex-shrink: 0; border-radius: 3px; }
|
|
20
|
+
& .donutchart-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: ${c.fgMuted}; }
|
|
21
|
+
& .donutchart-val { flex-shrink: 0; font-weight: ${c.weight.medium}; color: ${c.fgPrimary}; }`,
|
|
22
|
+
variants: {},
|
|
23
|
+
defaultVariants: {},
|
|
24
|
+
compoundVariants: [],
|
|
25
|
+
});
|
|
26
|
+
export const donutchartRecipe = resolveRecipe(config);
|
|
27
|
+
export default config;
|
|
@@ -0,0 +1,42 @@
|
|
|
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 { X } from "lucide-react";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
import { drawerRecipe } from "./drawer.recipe.js";
|
|
7
|
+
let instanceCounter = 0;
|
|
8
|
+
function useDrawerInstanceId() {
|
|
9
|
+
const idRef = React.useRef(null);
|
|
10
|
+
if (idRef.current == null) {
|
|
11
|
+
instanceCounter += 1;
|
|
12
|
+
idRef.current = `crisp-drawer-${instanceCounter}`;
|
|
13
|
+
}
|
|
14
|
+
return idRef.current;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A side sheet — composes Zag's dialog machine (focus trap, scroll lock,
|
|
18
|
+
* Escape / outside-click dismissal) with a panel that slides in from the edge.
|
|
19
|
+
* Compose the body with `DrawerBody` and actions with `DrawerFooter`.
|
|
20
|
+
*/
|
|
21
|
+
export function Drawer({ open, onOpenChange, side = "right", title, icon, "aria-label": ariaLabel, children, }) {
|
|
22
|
+
const id = useDrawerInstanceId();
|
|
23
|
+
const service = useMachine(dialog.machine, {
|
|
24
|
+
id,
|
|
25
|
+
open,
|
|
26
|
+
onOpenChange(details) {
|
|
27
|
+
onOpenChange(details.open);
|
|
28
|
+
},
|
|
29
|
+
"aria-label": typeof title === "string" ? title : ariaLabel,
|
|
30
|
+
});
|
|
31
|
+
const api = dialog.connect(service, normalizeProps);
|
|
32
|
+
if (!api.open)
|
|
33
|
+
return null;
|
|
34
|
+
return (_jsx(Portal, { children: _jsxs("div", { className: drawerRecipe(), children: [_jsx("div", { ...api.getBackdropProps(), className: "drawer-backdrop" }), _jsx("div", { ...api.getPositionerProps(), className: "drawer-positioner", "data-side": side, children: _jsxs("div", { ...api.getContentProps(), className: "drawer-content", children: [title != null && (_jsxs("div", { className: "drawer-header", children: [icon, _jsx("span", { ...api.getTitleProps(), className: "drawer-title", children: title }), _jsx("button", { type: "button", ...api.getCloseTriggerProps(), className: "drawer-close", "aria-label": "Close", children: _jsx(X, { size: 16, strokeWidth: 2, "aria-hidden": "true" }) })] })), children] }) })] }) }));
|
|
35
|
+
}
|
|
36
|
+
Drawer.displayName = "Drawer";
|
|
37
|
+
export function DrawerBody({ children }) {
|
|
38
|
+
return _jsx("div", { className: "drawer-body", children: children });
|
|
39
|
+
}
|
|
40
|
+
export function DrawerFooter({ children }) {
|
|
41
|
+
return _jsx("div", { className: "drawer-footer", children: children });
|
|
42
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { defineRecipe, resolveRecipe } from "../../core/index.js";
|
|
2
|
+
import { contract as c } from "../../tokens/index.js";
|
|
3
|
+
// A side sheet — a focus-trapped panel that slides in from the right or left,
|
|
4
|
+
// over a scrim. Shares the Dialog header/body/footer language.
|
|
5
|
+
const config = defineRecipe({
|
|
6
|
+
name: "drawer",
|
|
7
|
+
base: `display: contents; font-family: ${c.font.sans};
|
|
8
|
+
& .drawer-backdrop { position: fixed; inset: 0; z-index: 60; background: color-mix(in srgb, ${c.fgPrimary} 18%, transparent); opacity: 1; transition: opacity 0.2s ease; @starting-style { opacity: 0; } }
|
|
9
|
+
& .drawer-positioner { position: fixed; inset: 0; z-index: 61; display: flex; }
|
|
10
|
+
& .drawer-positioner[data-side="right"] { justify-content: flex-end; }
|
|
11
|
+
& .drawer-positioner[data-side="left"] { justify-content: flex-start; }
|
|
12
|
+
& .drawer-content { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; position: relative; display: flex; flex-direction: column; box-sizing: border-box; width: 420px; max-width: 92vw; height: 100%; background: ${c.bgSurface}; transform: none; transition: transform 0.24s cubic-bezier(.32,.72,0,1); }
|
|
13
|
+
& .drawer-positioner[data-side="right"] .drawer-content { box-shadow: -12px 0 32px -8px rgba(28,40,64,0.18); @starting-style { transform: translateX(100%); } }
|
|
14
|
+
& .drawer-positioner[data-side="left"] .drawer-content { box-shadow: 12px 0 32px -8px rgba(28,40,64,0.18); @starting-style { transform: translateX(-100%); } }
|
|
15
|
+
& .drawer-header { display: flex; align-items: center; gap: ${c.space["2"]}; flex-shrink: 0; height: 52px; padding: 0 ${c.space["2"]} 0 ${c.space["4"]}; border-bottom: 1px solid ${c.borderSubtle}; }
|
|
16
|
+
& .drawer-header svg { flex-shrink: 0; color: ${c.fgMuted}; }
|
|
17
|
+
& .drawer-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: ${c.text.lg}; font-weight: ${c.weight.semibold}; color: ${c.fgPrimary}; }
|
|
18
|
+
& .drawer-close { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: 28px; height: 28px; margin: 0; padding: 0; border: none; border-radius: ${c.radius.md}; background: none; color: ${c.fgMuted}; cursor: pointer; }
|
|
19
|
+
& .drawer-close:hover { background: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); color: ${c.fgPrimary}; }
|
|
20
|
+
& .drawer-body { flex: 1; min-height: 0; overflow-y: auto; padding: ${c.space["4"]}; }
|
|
21
|
+
& .drawer-footer { display: flex; align-items: center; justify-content: flex-end; gap: ${c.space["3"]}; flex-shrink: 0; min-height: 56px; padding: 0 ${c.space["4"]}; border-top: 1px solid ${c.borderSubtle}; }`,
|
|
22
|
+
variants: {},
|
|
23
|
+
defaultVariants: {},
|
|
24
|
+
compoundVariants: [],
|
|
25
|
+
});
|
|
26
|
+
export const drawerRecipe = resolveRecipe(config);
|
|
27
|
+
export default config;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { emptystateRecipe } from "./emptystate.recipe.js";
|
|
3
|
+
/** A centred empty-state panel — icon, title, description, optional action. */
|
|
4
|
+
export function EmptyState({ icon, title, description, action, variant, className, }) {
|
|
5
|
+
return (_jsxs("div", { className: emptystateRecipe({ variant, className }), children: [icon && _jsx("span", { className: "emptystate-icon", children: icon }), _jsx("div", { className: "emptystate-title", children: title }), description != null && (_jsx("div", { className: "emptystate-desc", children: description })), action && _jsx("div", { className: "emptystate-action", children: action })] }));
|
|
6
|
+
}
|
|
7
|
+
EmptyState.displayName = "EmptyState";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { defineRecipe, resolveRecipe } from "../../core/index.js";
|
|
2
|
+
import { contract as c } from "../../tokens/index.js";
|
|
3
|
+
// An empty-state panel — icon + title + description + optional action.
|
|
4
|
+
const config = defineRecipe({
|
|
5
|
+
name: "emptystate",
|
|
6
|
+
base: `display: flex; flex-direction: column; align-items: center; justify-content: center; gap: ${c.space["1.5"]}; box-sizing: border-box; padding: ${c.space["8"]} ${c.space["4"]}; text-align: center; font-family: ${c.font.sans};
|
|
7
|
+
& .emptystate-icon { display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 40px; margin-bottom: ${c.space["1"]}; border-radius: ${c.radius.xl}; background: color-mix(in srgb, ${c.fgPrimary} 5%, transparent); color: ${c.fgMuted}; }
|
|
8
|
+
& .emptystate-title { font-size: ${c.text.lg}; font-weight: ${c.weight.semibold}; color: ${c.fgPrimary}; }
|
|
9
|
+
& .emptystate-desc { max-width: 320px; font-size: ${c.text.md}; color: ${c.fgMuted}; }
|
|
10
|
+
& .emptystate-action { margin-top: ${c.space["2"]}; }`,
|
|
11
|
+
variants: {
|
|
12
|
+
variant: {
|
|
13
|
+
// compact icon tile (default)
|
|
14
|
+
default: ``,
|
|
15
|
+
// a large icon on a dotted-grid backdrop — Attio's Developers/Reports empty state
|
|
16
|
+
illustrated: `& .emptystate-icon { width: 132px; height: 96px; margin-bottom: ${c.space["2"]}; border-radius: 0; color: color-mix(in srgb, ${c.fgPrimary} 22%, transparent); background: radial-gradient(circle, color-mix(in srgb, ${c.fgPrimary} 12%, transparent) 1.2px, transparent 1.2px) 0 0 / 16px 16px; }`,
|
|
17
|
+
// Attio's full-view empty state (measured from a filtered-to-zero records table):
|
|
18
|
+
// a 45px ringed circle icon, a 20/24 semibold heading, a 14/20 medium muted body,
|
|
19
|
+
// then a primary action. Gaps are 16 / 8 / 16 (icon→title→desc→action).
|
|
20
|
+
page: `gap: ${c.space["2"]};
|
|
21
|
+
& .emptystate-icon { width: 45px; height: 45px; margin-bottom: ${c.space["2"]}; border-radius: ${c.radius.full}; border: 1px solid ${c.borderSubtle}; background: none; color: ${c.fgMuted}; }
|
|
22
|
+
& .emptystate-title { font-size: 20px; line-height: 24px; }
|
|
23
|
+
& .emptystate-desc { max-width: 266px; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; line-height: 20px; }
|
|
24
|
+
& .emptystate-action { margin-top: ${c.space["2"]}; }`,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
defaultVariants: { variant: "default" },
|
|
28
|
+
compoundVariants: [],
|
|
29
|
+
});
|
|
30
|
+
export const emptystateRecipe = resolveRecipe(config);
|
|
31
|
+
export default config;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { formfieldRecipe } from "./formfield.recipe.js";
|
|
4
|
+
/**
|
|
5
|
+
* A labelled form field — measured from Attio's Settings forms. A muted label
|
|
6
|
+
* above any control, with an optional description or error line below. When
|
|
7
|
+
* `error` is set, it replaces the description and gets `aria-describedby` wiring.
|
|
8
|
+
*/
|
|
9
|
+
export function FormField({ label, htmlFor, required, optional, description, error, children, className, }) {
|
|
10
|
+
const descId = React.useId();
|
|
11
|
+
const hasMsg = error != null || description != null;
|
|
12
|
+
return (_jsxs("div", { className: formfieldRecipe({ className }), children: [_jsxs("label", { className: "formfield-label", htmlFor: htmlFor, children: [label, required && (_jsx("span", { className: "formfield-required", "aria-hidden": "true", children: "*" })), optional && !required && (_jsx("span", { className: "formfield-optional", children: "(optional)" }))] }), children, error != null ? (_jsx("span", { className: "formfield-error", id: hasMsg ? descId : undefined, children: error })) : description != null ? (_jsx("span", { className: "formfield-desc", id: hasMsg ? descId : undefined, children: description })) : null] }));
|
|
13
|
+
}
|
|
14
|
+
FormField.displayName = "FormField";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineRecipe, resolveRecipe } from "../../core/index.js";
|
|
2
|
+
import { contract as c } from "../../tokens/index.js";
|
|
3
|
+
// A labelled form field — measured from Attio's Settings forms: a 12px/500 muted
|
|
4
|
+
// label above the control, with an optional description or danger error line
|
|
5
|
+
// below. Wraps any control (Input, Select, Textarea…).
|
|
6
|
+
const config = defineRecipe({
|
|
7
|
+
name: "formfield",
|
|
8
|
+
base: `display: flex; flex-direction: column; gap: ${c.space["1.5"]}; box-sizing: border-box; font-family: ${c.font.sans};
|
|
9
|
+
& .formfield-label { display: inline-flex; align-items: center; gap: ${c.space["1"]}; font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: ${c.fgMuted}; }
|
|
10
|
+
& .formfield-required { color: ${c.bgDanger}; }
|
|
11
|
+
& .formfield-optional { color: color-mix(in srgb, ${c.fgPrimary} 40%, transparent); font-weight: ${c.weight.regular}; }
|
|
12
|
+
& .formfield-desc { font-size: ${c.text.sm}; line-height: 16px; color: ${c.fgMuted}; }
|
|
13
|
+
& .formfield-error { font-size: ${c.text.sm}; line-height: 16px; color: ${c.bgDanger}; }`,
|
|
14
|
+
variants: {},
|
|
15
|
+
defaultVariants: {},
|
|
16
|
+
compoundVariants: [],
|
|
17
|
+
});
|
|
18
|
+
export const formfieldRecipe = resolveRecipe(config);
|
|
19
|
+
export default config;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { Primitive } from "../../core/index.js";
|
|
4
|
+
import { inputRecipe } from "./input.recipe.js";
|
|
5
|
+
export const Input = React.forwardRef(({ size, invalid, className, ...props }, ref) => {
|
|
6
|
+
return (_jsx(Primitive.input, { ref: ref, className: inputRecipe({ size, invalid, className }), "aria-invalid": invalid === true || undefined, ...props }));
|
|
7
|
+
});
|
|
8
|
+
Input.displayName = "Input";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { defineRecipe, resolveRecipe } from "../../core/index.js";
|
|
2
|
+
import { contract as c } from "../../tokens/index.js";
|
|
3
|
+
const config = defineRecipe({
|
|
4
|
+
name: "input",
|
|
5
|
+
base: `box-sizing: border-box; display: inline-flex; width: 100%; font-family: ${c.font.sans}; font-size: ${c.text.lg}; line-height: 20px; color: ${c.fgPrimary}; background: ${c.bgSurface}; border: none; outline: none; box-shadow: 0 0 2px rgba(28,40,64,0.18), 0 1px 3px rgba(0,0,0,0.04); transition: box-shadow .15s; &::placeholder { color: ${c.fgMuted}; } &:focus { box-shadow: inset 0 0 0 1px ${c.bgBrandSolid}; } &:disabled { opacity: 0.5; cursor: not-allowed; }`,
|
|
6
|
+
variants: {
|
|
7
|
+
size: {
|
|
8
|
+
medium: `height: 32px; padding-inline: ${c.space["2.5"]}; border-radius: ${c.radius.md};`,
|
|
9
|
+
large: `height: 36px; padding-inline: ${c.space["3"]}; border-radius: 10px;`,
|
|
10
|
+
},
|
|
11
|
+
invalid: {
|
|
12
|
+
true: `box-shadow: inset 0 0 0 1px ${c.bgDanger}; &:focus { box-shadow: inset 0 0 0 1px ${c.bgDanger}; }`,
|
|
13
|
+
false: ``,
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
defaultVariants: { size: "medium", invalid: false },
|
|
17
|
+
compoundVariants: [],
|
|
18
|
+
});
|
|
19
|
+
export const inputRecipe = Object.assign(resolveRecipe(config), {
|
|
20
|
+
config,
|
|
21
|
+
meta: { medium: { height: "32px", borderRadius: "8px", fontSize: "14px" } },
|
|
22
|
+
});
|
|
23
|
+
export default config;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { Primitive } from "../../core/index.js";
|
|
4
|
+
import { kbdRecipe } from "./kbd.recipe.js";
|
|
5
|
+
export const Kbd = React.forwardRef(({ className, asChild, ...props }, ref) => {
|
|
6
|
+
return (_jsx(Primitive.span, { ref: ref, asChild: asChild, className: kbdRecipe({ className }), ...props }));
|
|
7
|
+
});
|
|
8
|
+
Kbd.displayName = "Kbd";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { defineRecipe, resolveRecipe } from "../../core/index.js";
|
|
2
|
+
import { contract as c } from "../../tokens/index.js";
|
|
3
|
+
const config = defineRecipe({
|
|
4
|
+
name: "kbd",
|
|
5
|
+
base: `box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; gap: ${c.space["0.5"]}; min-width: 20px; height: 20px; padding-inline: ${c.space["1"]}; border-radius: ${c.radius.sm}; font-family: ${c.font.sans}; font-size: ${c.text.xs}; font-weight: ${c.weight.regular}; color: color-mix(in srgb, ${c.fgPrimary} 50%, transparent); background: transparent; box-shadow: inset 0 0 0 1px color-mix(in srgb, ${c.fgPrimary} 6%, transparent);`,
|
|
6
|
+
variants: {},
|
|
7
|
+
defaultVariants: {},
|
|
8
|
+
compoundVariants: [],
|
|
9
|
+
});
|
|
10
|
+
export const kbdRecipe = Object.assign(resolveRecipe(config), {
|
|
11
|
+
config,
|
|
12
|
+
meta: {
|
|
13
|
+
chip: {
|
|
14
|
+
height: "20px",
|
|
15
|
+
borderRadius: "6px",
|
|
16
|
+
fontSize: "11px",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
export default config;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { linechartRecipe } from "./linechart.recipe.js";
|
|
3
|
+
function niceNum(raw) {
|
|
4
|
+
if (raw <= 0)
|
|
5
|
+
return 1;
|
|
6
|
+
const mag = 10 ** Math.floor(Math.log10(raw));
|
|
7
|
+
const norm = raw / mag;
|
|
8
|
+
const step = norm <= 1 ? 1 : norm <= 2 ? 2 : norm <= 5 ? 5 : 10;
|
|
9
|
+
return step * mag;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A responsive line chart — measured from Attio's Reports historical charts.
|
|
13
|
+
* A stretched SVG line/area (non-scaling stroke keeps it crisp) over dashed
|
|
14
|
+
* gridlines; dots are HTML so they stay round. `role="img"` with a summary.
|
|
15
|
+
*/
|
|
16
|
+
export function LineChart({ data, height = 240, max, ticks = 5, color, area = true, dots = true, format = (n) => n.toLocaleString(), xAxisTitle, "aria-label": ariaLabel, className, }) {
|
|
17
|
+
const rawMax = Math.max(1, ...data.map((d) => d.value));
|
|
18
|
+
const step = max ? max / (ticks - 1) : niceNum(rawMax / (ticks - 1));
|
|
19
|
+
const top = step * (ticks - 1);
|
|
20
|
+
const tickValues = Array.from({ length: ticks }, (_, i) => step * i);
|
|
21
|
+
// points in a 0..100 viewBox (y inverted: 0 at top)
|
|
22
|
+
const n = data.length;
|
|
23
|
+
const pts = data.map((d, i) => {
|
|
24
|
+
const x = n === 1 ? 50 : (i / (n - 1)) * 100;
|
|
25
|
+
const frac = Math.max(0, Math.min(1, d.value / top));
|
|
26
|
+
return { x, y: 100 - frac * 100, frac };
|
|
27
|
+
});
|
|
28
|
+
const line = pts.map((p) => `${p.x},${p.y}`).join(" ");
|
|
29
|
+
const areaPath = pts.length > 1
|
|
30
|
+
? `M ${pts[0].x},100 ${pts.map((p) => `L ${p.x},${p.y}`).join(" ")} L ${pts[pts.length - 1].x},100 Z`
|
|
31
|
+
: "";
|
|
32
|
+
const summary = ariaLabel ??
|
|
33
|
+
`Line chart: ${data.map((d) => `${d.label} ${format(d.value)}`).join(", ")}`;
|
|
34
|
+
return (_jsxs("div", { className: linechartRecipe({ className }), style: { height }, role: "img", "aria-label": summary, children: [_jsxs("div", { className: "linechart-body", children: [_jsx("div", { className: "linechart-yaxis", children: tickValues.map((t) => (_jsx("span", { className: "linechart-ytick", style: { bottom: `${(t / top) * 100}%` }, children: format(t) }, t))) }), _jsxs("div", { className: "linechart-plot", children: [tickValues.map((t) => (_jsx("div", { className: "linechart-grid", style: { bottom: `${(t / top) * 100}%` } }, t))), pts.length > 1 && (_jsxs("svg", { className: "linechart-svg", viewBox: "0 0 100 100", preserveAspectRatio: "none", "aria-hidden": "true", children: [area && _jsx("path", { className: "linechart-area", d: areaPath }), _jsx("polyline", { className: "linechart-line", points: line, style: { stroke: color } })] })), dots &&
|
|
35
|
+
pts.map((p, i) => (_jsx("span", { className: "linechart-dot-html", style: {
|
|
36
|
+
left: `${p.x}%`,
|
|
37
|
+
bottom: `${p.frac * 100}%`,
|
|
38
|
+
borderColor: color,
|
|
39
|
+
}, title: `${data[i].label}: ${format(data[i].value)}` }, i)))] })] }), _jsx("div", { className: "linechart-xaxis", children: data.map((d) => (_jsx("span", { className: "linechart-xlabel", children: d.label }, d.label))) }), xAxisTitle != null && (_jsx("div", { className: "linechart-axis-title", children: xAxisTitle }))] }));
|
|
40
|
+
}
|
|
41
|
+
LineChart.displayName = "LineChart";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { defineRecipe, resolveRecipe } from "../../core/index.js";
|
|
2
|
+
import { contract as c } from "../../tokens/index.js";
|
|
3
|
+
// A responsive line chart — measured from Attio's Reports historical charts: a
|
|
4
|
+
// brand line with a soft area fill over dashed horizontal gridlines, a value
|
|
5
|
+
// axis on the left, and category labels below. The plot is an SVG stretched to
|
|
6
|
+
// its box (non-scaling stroke keeps the line crisp); axes are HTML.
|
|
7
|
+
const config = defineRecipe({
|
|
8
|
+
name: "linechart",
|
|
9
|
+
base: `display: flex; flex-direction: column; box-sizing: border-box; font-family: ${c.font.sans};
|
|
10
|
+
& .linechart-body { display: flex; flex: 1; min-height: 0; }
|
|
11
|
+
& .linechart-yaxis { position: relative; flex-shrink: 0; width: 34px; }
|
|
12
|
+
& .linechart-ytick { position: absolute; right: ${c.space["2"]}; transform: translateY(50%); font-size: ${c.text.xs}; color: ${c.fgMuted}; white-space: nowrap; }
|
|
13
|
+
& .linechart-plot { position: relative; flex: 1; min-width: 0; border-bottom: 1px solid ${c.borderDefault}; }
|
|
14
|
+
& .linechart-grid { position: absolute; left: 0; right: 0; border-top: 1px dashed ${c.borderSubtle}; pointer-events: none; }
|
|
15
|
+
& .linechart-svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
|
|
16
|
+
& .linechart-area { fill: color-mix(in srgb, ${c.bgBrandSolid} 12%, transparent); stroke: none; }
|
|
17
|
+
& .linechart-line { fill: none; stroke: ${c.bgBrandSolid}; stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; vector-effect: non-scaling-stroke; }
|
|
18
|
+
& .linechart-dot-html { position: absolute; width: 8px; height: 8px; box-sizing: border-box; border-radius: ${c.radius.full}; background: ${c.bgSurface}; border: 2px solid ${c.bgBrandSolid}; transform: translate(-50%, 50%); pointer-events: auto; }
|
|
19
|
+
& .linechart-xaxis { display: flex; margin-top: ${c.space["1.5"]}; padding-left: 34px; }
|
|
20
|
+
& .linechart-xlabel { flex: 1; min-width: 0; text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: ${c.text.sm}; color: ${c.fgMuted}; }
|
|
21
|
+
& .linechart-axis-title { margin-top: ${c.space["1"]}; padding-left: 34px; text-align: center; font-size: ${c.text.sm}; color: ${c.fgMuted}; }`,
|
|
22
|
+
variants: {},
|
|
23
|
+
defaultVariants: {},
|
|
24
|
+
compoundVariants: [],
|
|
25
|
+
});
|
|
26
|
+
export const linechartRecipe = resolveRecipe(config);
|
|
27
|
+
export default config;
|