@crisp-ui-kit/crisp 0.10.0 → 0.11.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.
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./middletruncate"), exports);
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ "use client";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.MiddleTruncate = MiddleTruncate;
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ const react_middle_truncate_1 = require("@seed-design/react-middle-truncate");
7
+ /**
8
+ * Truncates in the MIDDLE rather than the end, so the part that identifies a
9
+ * string survives: `annual-report-fi…-v7-final.pdf` keeps its extension, and a
10
+ * record title keeps its suffix. An end-truncating `text-overflow: ellipsis`
11
+ * throws exactly that away.
12
+ *
13
+ * Measurement is seed-design's — it re-measures on resize with a
14
+ * `ResizeObserver` and only touches the DOM inside an effect, so it renders the
15
+ * full string on the server and narrows it on hydration.
16
+ *
17
+ * It owns no pixels, in crisp or in seed: there is no recipe here because
18
+ * seed-design ships no stylesheet for it either. It inherits the type around it
19
+ * by design, which is what makes it usable inside a table cell.
20
+ */
21
+ function MiddleTruncate({ children, title, ...props }) {
22
+ return ((0, jsx_runtime_1.jsx)(react_middle_truncate_1.MiddleTruncate, { ...props, title: title ?? children, children: children }));
23
+ }
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./quantitypicker"), exports);
18
+ __exportStar(require("./quantitypicker.recipe"), exports);
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ "use client";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.QuantityPicker = QuantityPicker;
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ const react_quantity_picker_1 = require("@seed-design/react-quantity-picker");
7
+ const lucide_react_1 = require("lucide-react");
8
+ const spinner_1 = require("../spinner");
9
+ const quantitypicker_recipe_1 = require("./quantitypicker.recipe");
10
+ /** seed's icon box per size, read from their stylesheet. */
11
+ const ICON = { small: 16, medium: 18, large: 22 };
12
+ /**
13
+ * The value, as a grid with a hidden copy of the LARGEST value stacked
14
+ * underneath it. Without that the control gets wider at 10 and wider again at
15
+ * 100, and a row of them stops lining up. `max` is the right placeholder
16
+ * because it is the widest the value can ever be, and it is run through
17
+ * `getValueText` so a formatted value reserves the width of its formatting.
18
+ */
19
+ function Value() {
20
+ // `QuantityPickerValueDisplay` renders the value as its own children and its
21
+ // props type OMITS `children`, so the two-cell grid cannot go through it.
22
+ // Taking `valueDisplayProps` directly keeps every attribute the engine puts
23
+ // there — aria-live, aria-atomic, the data-state flags — and keeps the
24
+ // rendered text the engine's own rather than a second derivation of it.
25
+ const { max, getValueText, valueDisplayProps } = (0, react_quantity_picker_1.useQuantityPickerContext)();
26
+ const { children: valueText, ...displayProps } = valueDisplayProps;
27
+ return ((0, jsx_runtime_1.jsxs)("span", { ...displayProps, className: "quantitypicker-value", children: [(0, jsx_runtime_1.jsx)("span", { className: "quantitypicker-placeholder", "aria-hidden": "true", children: getValueText(String(max), max) }), (0, jsx_runtime_1.jsx)("span", { className: "quantitypicker-text", children: valueText })] }));
28
+ }
29
+ function DecrementIcon({ size }) {
30
+ const { isRemoveButton, decrementLoading } = (0, react_quantity_picker_1.useQuantityPickerContext)();
31
+ if (decrementLoading)
32
+ return (0, jsx_runtime_1.jsx)(spinner_1.Spinner, { size: ICON[size] });
33
+ const Icon = isRemoveButton ? lucide_react_1.Trash2 : lucide_react_1.Minus;
34
+ return (0, jsx_runtime_1.jsx)(Icon, { size: ICON[size], strokeWidth: 2 });
35
+ }
36
+ function IncrementIcon({ size }) {
37
+ const { incrementLoading } = (0, react_quantity_picker_1.useQuantityPickerContext)();
38
+ if (incrementLoading)
39
+ return (0, jsx_runtime_1.jsx)(spinner_1.Spinner, { size: ICON[size] });
40
+ return (0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: ICON[size], strokeWidth: 2 });
41
+ }
42
+ /**
43
+ * A quantity stepper, driven by seed-design's headless engine so the clamping,
44
+ * the step arithmetic, the per-button loading state and the `removable` mode —
45
+ * where decrementing at the minimum becomes "remove this line" rather than a
46
+ * dead button — are theirs by construction.
47
+ *
48
+ * Two things are crisp's:
49
+ *
50
+ * The buttons carry accessible names. The engine labels the minus button only
51
+ * in remove mode, so a plain stepper would ship two buttons whose entire
52
+ * content is an SVG and whose accessible name is empty.
53
+ *
54
+ * `dir` is worth passing explicitly. The engine defaults it to `"ltr"` and
55
+ * stamps that on the root, which overrides an inherited RTL — the same trap
56
+ * Slider documents. Here it also decides child ORDER, since the engine
57
+ * reverses its visible children rather than relying on CSS.
58
+ */
59
+ function QuantityPicker({ size = "medium", fill = false, className, name, decrementLabel = "Decrease quantity", incrementLabel = "Increase quantity", ...props }) {
60
+ return ((0, jsx_runtime_1.jsxs)(react_quantity_picker_1.QuantityPickerRoot, { ...props, className: (0, quantitypicker_recipe_1.quantitypickerRecipe)({ size, fill, className }), children: [(0, jsx_runtime_1.jsx)(react_quantity_picker_1.QuantityPickerDecrementButton, { className: "quantitypicker-button", "aria-label": decrementLabel, children: (0, jsx_runtime_1.jsx)(DecrementIcon, { size: size }) }), (0, jsx_runtime_1.jsx)("span", { className: "quantitypicker-divider" }), (0, jsx_runtime_1.jsx)(Value, {}), (0, jsx_runtime_1.jsx)("span", { className: "quantitypicker-divider" }), (0, jsx_runtime_1.jsx)(react_quantity_picker_1.QuantityPickerIncrementButton, { className: "quantitypicker-button", "aria-label": incrementLabel, children: (0, jsx_runtime_1.jsx)(IncrementIcon, { size: size }) }), name ? (0, jsx_runtime_1.jsx)(react_quantity_picker_1.QuantityPickerHiddenInput, { name: name }) : null] }));
61
+ }
62
+ QuantityPicker.displayName = "QuantityPicker";
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.quantitypickerRecipe = void 0;
4
+ const core_1 = require("../../core");
5
+ const tokens_1 = require("../../tokens");
6
+ // seed-design's quantity-picker, shape for shape, in crisp's colours.
7
+ //
8
+ // Attio has no stepper — its quantity-ish fields are plain number inputs — so
9
+ // as with Slider there is nothing of Attio's to measure and the GEOMETRY is
10
+ // seed's own, read from `@seed-design/css/recipes/quantity-picker.css`:
11
+ //
12
+ // size root height / button square radius icon text
13
+ // small 36px (x9) 8px (r2) 16px 16/22 (t5)
14
+ // medium 40px (x10) 8px (r2) 18px 16/22 (t5)
15
+ // large 52px (x13) 12px (r3) 22px 18/24 (t6)
16
+ //
17
+ // plus an 8px (x2) inline padding on the value, a 1x16 (x4) divider, a 1px
18
+ // inset ring that thickens to 2px when invalid, and a 2px focus ring offset
19
+ // 2px. 16px is crisp's `text.xl`; 18/24 has no crisp token and is written as a
20
+ // literal, the way EmptyState's 20px title is.
21
+ //
22
+ // Only the COLOUR roles are re-pointed at crisp's tokens:
23
+ // seed bg-layer-default -> bgSurface
24
+ // seed stroke-neutral-weak -> borderDefault (the 1px ring)
25
+ // seed stroke-critical-solid -> bgDanger (as Input/Textarea use when invalid)
26
+ // seed stroke-neutral-subtle -> borderSubtle (the divider)
27
+ // seed bg-transparent-pressed -> bgRaised (as Button's ghost hover)
28
+ // seed fg-neutral / fg-disabled-> fgPrimary / fgDisabled
29
+ //
30
+ // Two things in seed's stylesheet are deliberately NOT copied. Their pressed
31
+ // `scale: var(--seed-feedback-scale)` resolves to 1 in the shipped CSS, so it
32
+ // is a no-op to transliterate. And their hover is split across
33
+ // `@media (hover: hover) and (pointer: fine)` / `@media not all and (...)`; no
34
+ // crisp recipe contains an `@media` at all, and neither `build.mjs`'s
35
+ // flattener nor `emit-lynx.mjs` has ever been run against one, so this uses
36
+ // the plain `:hover` every other crisp component uses.
37
+ //
38
+ // The value is a GRID with a hidden placeholder stacked on the text in the
39
+ // same cell. That is what keeps the control from resizing as the number goes
40
+ // 9 -> 10, and it is the detail a hand-built stepper always misses.
41
+ const config = (0, core_1.defineRecipe)({
42
+ name: "quantitypicker",
43
+ base: `
44
+ display: inline-flex; box-sizing: border-box; width: fit-content;
45
+ align-items: center; font-family: ${tokens_1.contract.font.sans};
46
+ background: ${tokens_1.contract.bgSurface}; box-shadow: inset 0 0 0 1px ${tokens_1.contract.borderDefault};
47
+
48
+ &[data-invalid] { box-shadow: inset 0 0 0 2px ${tokens_1.contract.bgDanger}; }
49
+
50
+ & .quantitypicker-button {
51
+ display: inline-flex; flex-shrink: 0; box-sizing: border-box;
52
+ align-items: center; justify-content: center;
53
+ border: none; padding: 0; cursor: pointer;
54
+ background: transparent; color: ${tokens_1.contract.fgPrimary};
55
+ transition: background-color .15s ease, outline-color .15s ease;
56
+ outline: 2px solid transparent; outline-offset: 2px;
57
+ }
58
+ & .quantitypicker-button:focus-visible { outline-color: ${tokens_1.contract.strokeFocusRing}; }
59
+ & .quantitypicker-button:not([data-disabled]):hover { background: ${tokens_1.contract.bgRaised}; }
60
+ & .quantitypicker-button[data-loading] { background: ${tokens_1.contract.bgRaised}; }
61
+ & .quantitypicker-button[data-disabled] { cursor: not-allowed; color: ${tokens_1.contract.fgDisabled}; }
62
+
63
+ & .quantitypicker-value {
64
+ display: grid; flex-grow: 1; flex-shrink: 0; box-sizing: border-box;
65
+ align-items: center; justify-content: center;
66
+ padding-inline: ${tokens_1.contract.space["2"]};
67
+ font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgPrimary};
68
+ }
69
+ & .quantitypicker-value[data-disabled] { color: ${tokens_1.contract.fgDisabled}; }
70
+ & .quantitypicker-placeholder {
71
+ grid-column: 1; grid-row: 1; visibility: hidden;
72
+ font-variant-numeric: tabular-nums;
73
+ }
74
+ & .quantitypicker-text {
75
+ grid-column: 1; grid-row: 1; text-align: center;
76
+ font-variant-numeric: tabular-nums;
77
+ }
78
+
79
+ & .quantitypicker-divider {
80
+ flex-shrink: 0; width: 1px; height: ${tokens_1.contract.space["4"]};
81
+ background: ${tokens_1.contract.borderSubtle};
82
+ }
83
+ `,
84
+ variants: {
85
+ size: {
86
+ small: `
87
+ height: 36px; border-radius: ${tokens_1.contract.radius.md};
88
+ & .quantitypicker-button { width: 36px; height: 36px; border-radius: ${tokens_1.contract.radius.md}; }
89
+ & .quantitypicker-value { font-size: ${tokens_1.contract.text.xl}; line-height: 22px; }
90
+ `,
91
+ medium: `
92
+ height: 40px; border-radius: ${tokens_1.contract.radius.md};
93
+ & .quantitypicker-button { width: 40px; height: 40px; border-radius: ${tokens_1.contract.radius.md}; }
94
+ & .quantitypicker-value { font-size: ${tokens_1.contract.text.xl}; line-height: 22px; }
95
+ `,
96
+ large: `
97
+ height: 52px; border-radius: ${tokens_1.contract.radius.xl};
98
+ & .quantitypicker-button { width: 52px; height: 52px; border-radius: ${tokens_1.contract.radius.xl}; }
99
+ & .quantitypicker-value { font-size: 18px; line-height: 24px; }
100
+ `,
101
+ },
102
+ /** `fill` stretches to the row it sits in; seed's `layout_fill`. */
103
+ fill: {
104
+ true: `width: auto; flex-grow: 1;`,
105
+ false: ``,
106
+ },
107
+ },
108
+ defaultVariants: { size: "medium", fill: false },
109
+ compoundVariants: [],
110
+ });
111
+ exports.quantitypickerRecipe = Object.assign((0, core_1.resolveRecipe)(config), {
112
+ config,
113
+ });
114
+ exports.default = config;
package/dist/cjs/index.js CHANGED
@@ -45,9 +45,11 @@ __exportStar(require("./components/input"), exports);
45
45
  __exportStar(require("./components/kbd"), exports);
46
46
  __exportStar(require("./components/linechart"), exports);
47
47
  __exportStar(require("./components/menu"), exports);
48
+ __exportStar(require("./components/middletruncate"), exports);
48
49
  __exportStar(require("./components/multiselect"), exports);
49
50
  __exportStar(require("./components/popover"), exports);
50
51
  __exportStar(require("./components/progress"), exports);
52
+ __exportStar(require("./components/quantitypicker"), exports);
51
53
  __exportStar(require("./components/radiogroup"), exports);
52
54
  __exportStar(require("./components/resizablepanel"), exports);
53
55
  __exportStar(require("./components/segmented"), exports);
@@ -0,0 +1 @@
1
+ export * from "./middletruncate.js";
@@ -0,0 +1,20 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { MiddleTruncate as SeedMiddleTruncate, } from "@seed-design/react-middle-truncate";
4
+ /**
5
+ * Truncates in the MIDDLE rather than the end, so the part that identifies a
6
+ * string survives: `annual-report-fi…-v7-final.pdf` keeps its extension, and a
7
+ * record title keeps its suffix. An end-truncating `text-overflow: ellipsis`
8
+ * throws exactly that away.
9
+ *
10
+ * Measurement is seed-design's — it re-measures on resize with a
11
+ * `ResizeObserver` and only touches the DOM inside an effect, so it renders the
12
+ * full string on the server and narrows it on hydration.
13
+ *
14
+ * It owns no pixels, in crisp or in seed: there is no recipe here because
15
+ * seed-design ships no stylesheet for it either. It inherits the type around it
16
+ * by design, which is what makes it usable inside a table cell.
17
+ */
18
+ export function MiddleTruncate({ children, title, ...props }) {
19
+ return (_jsx(SeedMiddleTruncate, { ...props, title: title ?? children, children: children }));
20
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./quantitypicker.js";
2
+ export * from "./quantitypicker.recipe.js";
@@ -0,0 +1,59 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { QuantityPickerDecrementButton, QuantityPickerHiddenInput, QuantityPickerIncrementButton, QuantityPickerRoot, useQuantityPickerContext, } from "@seed-design/react-quantity-picker";
4
+ import { Minus, Plus, Trash2 } from "lucide-react";
5
+ import { Spinner } from "../spinner/index.js";
6
+ import { quantitypickerRecipe } from "./quantitypicker.recipe.js";
7
+ /** seed's icon box per size, read from their stylesheet. */
8
+ const ICON = { small: 16, medium: 18, large: 22 };
9
+ /**
10
+ * The value, as a grid with a hidden copy of the LARGEST value stacked
11
+ * underneath it. Without that the control gets wider at 10 and wider again at
12
+ * 100, and a row of them stops lining up. `max` is the right placeholder
13
+ * because it is the widest the value can ever be, and it is run through
14
+ * `getValueText` so a formatted value reserves the width of its formatting.
15
+ */
16
+ function Value() {
17
+ // `QuantityPickerValueDisplay` renders the value as its own children and its
18
+ // props type OMITS `children`, so the two-cell grid cannot go through it.
19
+ // Taking `valueDisplayProps` directly keeps every attribute the engine puts
20
+ // there — aria-live, aria-atomic, the data-state flags — and keeps the
21
+ // rendered text the engine's own rather than a second derivation of it.
22
+ const { max, getValueText, valueDisplayProps } = useQuantityPickerContext();
23
+ const { children: valueText, ...displayProps } = valueDisplayProps;
24
+ return (_jsxs("span", { ...displayProps, className: "quantitypicker-value", children: [_jsx("span", { className: "quantitypicker-placeholder", "aria-hidden": "true", children: getValueText(String(max), max) }), _jsx("span", { className: "quantitypicker-text", children: valueText })] }));
25
+ }
26
+ function DecrementIcon({ size }) {
27
+ const { isRemoveButton, decrementLoading } = useQuantityPickerContext();
28
+ if (decrementLoading)
29
+ return _jsx(Spinner, { size: ICON[size] });
30
+ const Icon = isRemoveButton ? Trash2 : Minus;
31
+ return _jsx(Icon, { size: ICON[size], strokeWidth: 2 });
32
+ }
33
+ function IncrementIcon({ size }) {
34
+ const { incrementLoading } = useQuantityPickerContext();
35
+ if (incrementLoading)
36
+ return _jsx(Spinner, { size: ICON[size] });
37
+ return _jsx(Plus, { size: ICON[size], strokeWidth: 2 });
38
+ }
39
+ /**
40
+ * A quantity stepper, driven by seed-design's headless engine so the clamping,
41
+ * the step arithmetic, the per-button loading state and the `removable` mode —
42
+ * where decrementing at the minimum becomes "remove this line" rather than a
43
+ * dead button — are theirs by construction.
44
+ *
45
+ * Two things are crisp's:
46
+ *
47
+ * The buttons carry accessible names. The engine labels the minus button only
48
+ * in remove mode, so a plain stepper would ship two buttons whose entire
49
+ * content is an SVG and whose accessible name is empty.
50
+ *
51
+ * `dir` is worth passing explicitly. The engine defaults it to `"ltr"` and
52
+ * stamps that on the root, which overrides an inherited RTL — the same trap
53
+ * Slider documents. Here it also decides child ORDER, since the engine
54
+ * reverses its visible children rather than relying on CSS.
55
+ */
56
+ export function QuantityPicker({ size = "medium", fill = false, className, name, decrementLabel = "Decrease quantity", incrementLabel = "Increase quantity", ...props }) {
57
+ return (_jsxs(QuantityPickerRoot, { ...props, className: quantitypickerRecipe({ size, fill, className }), children: [_jsx(QuantityPickerDecrementButton, { className: "quantitypicker-button", "aria-label": decrementLabel, children: _jsx(DecrementIcon, { size: size }) }), _jsx("span", { className: "quantitypicker-divider" }), _jsx(Value, {}), _jsx("span", { className: "quantitypicker-divider" }), _jsx(QuantityPickerIncrementButton, { className: "quantitypicker-button", "aria-label": incrementLabel, children: _jsx(IncrementIcon, { size: size }) }), name ? _jsx(QuantityPickerHiddenInput, { name: name }) : null] }));
58
+ }
59
+ QuantityPicker.displayName = "QuantityPicker";
@@ -0,0 +1,111 @@
1
+ import { defineRecipe, resolveRecipe } from "../../core/index.js";
2
+ import { contract as c } from "../../tokens/index.js";
3
+ // seed-design's quantity-picker, shape for shape, in crisp's colours.
4
+ //
5
+ // Attio has no stepper — its quantity-ish fields are plain number inputs — so
6
+ // as with Slider there is nothing of Attio's to measure and the GEOMETRY is
7
+ // seed's own, read from `@seed-design/css/recipes/quantity-picker.css`:
8
+ //
9
+ // size root height / button square radius icon text
10
+ // small 36px (x9) 8px (r2) 16px 16/22 (t5)
11
+ // medium 40px (x10) 8px (r2) 18px 16/22 (t5)
12
+ // large 52px (x13) 12px (r3) 22px 18/24 (t6)
13
+ //
14
+ // plus an 8px (x2) inline padding on the value, a 1x16 (x4) divider, a 1px
15
+ // inset ring that thickens to 2px when invalid, and a 2px focus ring offset
16
+ // 2px. 16px is crisp's `text.xl`; 18/24 has no crisp token and is written as a
17
+ // literal, the way EmptyState's 20px title is.
18
+ //
19
+ // Only the COLOUR roles are re-pointed at crisp's tokens:
20
+ // seed bg-layer-default -> bgSurface
21
+ // seed stroke-neutral-weak -> borderDefault (the 1px ring)
22
+ // seed stroke-critical-solid -> bgDanger (as Input/Textarea use when invalid)
23
+ // seed stroke-neutral-subtle -> borderSubtle (the divider)
24
+ // seed bg-transparent-pressed -> bgRaised (as Button's ghost hover)
25
+ // seed fg-neutral / fg-disabled-> fgPrimary / fgDisabled
26
+ //
27
+ // Two things in seed's stylesheet are deliberately NOT copied. Their pressed
28
+ // `scale: var(--seed-feedback-scale)` resolves to 1 in the shipped CSS, so it
29
+ // is a no-op to transliterate. And their hover is split across
30
+ // `@media (hover: hover) and (pointer: fine)` / `@media not all and (...)`; no
31
+ // crisp recipe contains an `@media` at all, and neither `build.mjs`'s
32
+ // flattener nor `emit-lynx.mjs` has ever been run against one, so this uses
33
+ // the plain `:hover` every other crisp component uses.
34
+ //
35
+ // The value is a GRID with a hidden placeholder stacked on the text in the
36
+ // same cell. That is what keeps the control from resizing as the number goes
37
+ // 9 -> 10, and it is the detail a hand-built stepper always misses.
38
+ const config = defineRecipe({
39
+ name: "quantitypicker",
40
+ base: `
41
+ display: inline-flex; box-sizing: border-box; width: fit-content;
42
+ align-items: center; font-family: ${c.font.sans};
43
+ background: ${c.bgSurface}; box-shadow: inset 0 0 0 1px ${c.borderDefault};
44
+
45
+ &[data-invalid] { box-shadow: inset 0 0 0 2px ${c.bgDanger}; }
46
+
47
+ & .quantitypicker-button {
48
+ display: inline-flex; flex-shrink: 0; box-sizing: border-box;
49
+ align-items: center; justify-content: center;
50
+ border: none; padding: 0; cursor: pointer;
51
+ background: transparent; color: ${c.fgPrimary};
52
+ transition: background-color .15s ease, outline-color .15s ease;
53
+ outline: 2px solid transparent; outline-offset: 2px;
54
+ }
55
+ & .quantitypicker-button:focus-visible { outline-color: ${c.strokeFocusRing}; }
56
+ & .quantitypicker-button:not([data-disabled]):hover { background: ${c.bgRaised}; }
57
+ & .quantitypicker-button[data-loading] { background: ${c.bgRaised}; }
58
+ & .quantitypicker-button[data-disabled] { cursor: not-allowed; color: ${c.fgDisabled}; }
59
+
60
+ & .quantitypicker-value {
61
+ display: grid; flex-grow: 1; flex-shrink: 0; box-sizing: border-box;
62
+ align-items: center; justify-content: center;
63
+ padding-inline: ${c.space["2"]};
64
+ font-weight: ${c.weight.medium}; color: ${c.fgPrimary};
65
+ }
66
+ & .quantitypicker-value[data-disabled] { color: ${c.fgDisabled}; }
67
+ & .quantitypicker-placeholder {
68
+ grid-column: 1; grid-row: 1; visibility: hidden;
69
+ font-variant-numeric: tabular-nums;
70
+ }
71
+ & .quantitypicker-text {
72
+ grid-column: 1; grid-row: 1; text-align: center;
73
+ font-variant-numeric: tabular-nums;
74
+ }
75
+
76
+ & .quantitypicker-divider {
77
+ flex-shrink: 0; width: 1px; height: ${c.space["4"]};
78
+ background: ${c.borderSubtle};
79
+ }
80
+ `,
81
+ variants: {
82
+ size: {
83
+ small: `
84
+ height: 36px; border-radius: ${c.radius.md};
85
+ & .quantitypicker-button { width: 36px; height: 36px; border-radius: ${c.radius.md}; }
86
+ & .quantitypicker-value { font-size: ${c.text.xl}; line-height: 22px; }
87
+ `,
88
+ medium: `
89
+ height: 40px; border-radius: ${c.radius.md};
90
+ & .quantitypicker-button { width: 40px; height: 40px; border-radius: ${c.radius.md}; }
91
+ & .quantitypicker-value { font-size: ${c.text.xl}; line-height: 22px; }
92
+ `,
93
+ large: `
94
+ height: 52px; border-radius: ${c.radius.xl};
95
+ & .quantitypicker-button { width: 52px; height: 52px; border-radius: ${c.radius.xl}; }
96
+ & .quantitypicker-value { font-size: 18px; line-height: 24px; }
97
+ `,
98
+ },
99
+ /** `fill` stretches to the row it sits in; seed's `layout_fill`. */
100
+ fill: {
101
+ true: `width: auto; flex-grow: 1;`,
102
+ false: ``,
103
+ },
104
+ },
105
+ defaultVariants: { size: "medium", fill: false },
106
+ compoundVariants: [],
107
+ });
108
+ export const quantitypickerRecipe = Object.assign(resolveRecipe(config), {
109
+ config,
110
+ });
111
+ export default config;
package/dist/esm/index.js CHANGED
@@ -29,9 +29,11 @@ export * from "./components/input/index.js";
29
29
  export * from "./components/kbd/index.js";
30
30
  export * from "./components/linechart/index.js";
31
31
  export * from "./components/menu/index.js";
32
+ export * from "./components/middletruncate/index.js";
32
33
  export * from "./components/multiselect/index.js";
33
34
  export * from "./components/popover/index.js";
34
35
  export * from "./components/progress/index.js";
36
+ export * from "./components/quantitypicker/index.js";
35
37
  export * from "./components/radiogroup/index.js";
36
38
  export * from "./components/resizablepanel/index.js";
37
39
  export * from "./components/segmented/index.js";
@@ -0,0 +1,78 @@
1
+ .quantitypicker {
2
+
3
+ display: inline-flex; box-sizing: border-box; width: fit-content;
4
+ align-items: center; font-family: var(--font-sans);
5
+ background: var(--bg-surface); box-shadow: inset 0 0 0 1px var(--border-default);
6
+
7
+ &[data-invalid] { box-shadow: inset 0 0 0 2px var(--bg-danger); }
8
+
9
+ & .quantitypicker-button {
10
+ display: inline-flex; flex-shrink: 0; box-sizing: border-box;
11
+ align-items: center; justify-content: center;
12
+ border: none; padding: 0; cursor: pointer;
13
+ background: transparent; color: var(--fg-primary);
14
+ transition: background-color .15s ease, outline-color .15s ease;
15
+ outline: 2px solid transparent; outline-offset: 2px;
16
+ }
17
+ & .quantitypicker-button:focus-visible { outline-color: var(--stroke-focus-ring); }
18
+ & .quantitypicker-button:not([data-disabled]):hover { background: var(--bg-raised); }
19
+ & .quantitypicker-button[data-loading] { background: var(--bg-raised); }
20
+ & .quantitypicker-button[data-disabled] { cursor: not-allowed; color: var(--fg-disabled); }
21
+
22
+ & .quantitypicker-value {
23
+ display: grid; flex-grow: 1; flex-shrink: 0; box-sizing: border-box;
24
+ align-items: center; justify-content: center;
25
+ padding-inline: var(--space-2);
26
+ font-weight: var(--weight-medium); color: var(--fg-primary);
27
+ }
28
+ & .quantitypicker-value[data-disabled] { color: var(--fg-disabled); }
29
+ & .quantitypicker-placeholder {
30
+ grid-column: 1; grid-row: 1; visibility: hidden;
31
+ font-variant-numeric: tabular-nums;
32
+ }
33
+ & .quantitypicker-text {
34
+ grid-column: 1; grid-row: 1; text-align: center;
35
+ font-variant-numeric: tabular-nums;
36
+ }
37
+
38
+ & .quantitypicker-divider {
39
+ flex-shrink: 0; width: 1px; height: var(--space-4);
40
+ background: var(--border-subtle);
41
+ }
42
+
43
+ }
44
+ .quantitypicker--size_small {
45
+
46
+ height: 36px; border-radius: var(--radius-md);
47
+ & .quantitypicker-button { width: 36px; height: 36px; border-radius: var(--radius-md); }
48
+ & .quantitypicker-value { font-size: var(--text-xl); line-height: 22px; }
49
+
50
+ }
51
+ .quantitypicker--size_medium {
52
+
53
+ height: 40px; border-radius: var(--radius-md);
54
+ & .quantitypicker-button { width: 40px; height: 40px; border-radius: var(--radius-md); }
55
+ & .quantitypicker-value { font-size: var(--text-xl); line-height: 22px; }
56
+
57
+ }
58
+ .quantitypicker--size_large {
59
+
60
+ height: 52px; border-radius: var(--radius-xl);
61
+ & .quantitypicker-button { width: 52px; height: 52px; border-radius: var(--radius-xl); }
62
+ & .quantitypicker-value { font-size: 18px; line-height: 24px; }
63
+
64
+ }
65
+ .quantitypicker--fill_true {
66
+ width: auto; flex-grow: 1;
67
+ }
68
+ .quantitypicker--fill_false {
69
+
70
+ }
71
+
72
+ @media (prefers-reduced-motion: reduce) {
73
+ .quantitypicker, .quantitypicker * {
74
+ animation-duration: 0.01ms !important;
75
+ animation-iteration-count: 1 !important;
76
+ transition-duration: 0.01ms !important;
77
+ }
78
+ }
@@ -0,0 +1,8 @@
1
+ import type { RecipeFn } from "../core/types";
2
+
3
+ export interface QuantitypickerVariants {
4
+ size?: "small" | "medium" | "large";
5
+ fill?: boolean;
6
+ }
7
+
8
+ export declare const quantitypickerRecipe: RecipeFn<{ size: { small: string; medium: string; large: string }; fill: { true: string; false: string } }>;
package/dist/styles.css CHANGED
@@ -1175,6 +1175,85 @@
1175
1175
  }
1176
1176
  }
1177
1177
 
1178
+ .quantitypicker {
1179
+
1180
+ display: inline-flex; box-sizing: border-box; width: fit-content;
1181
+ align-items: center; font-family: var(--font-sans);
1182
+ background: var(--bg-surface); box-shadow: inset 0 0 0 1px var(--border-default);
1183
+
1184
+ &[data-invalid] { box-shadow: inset 0 0 0 2px var(--bg-danger); }
1185
+
1186
+ & .quantitypicker-button {
1187
+ display: inline-flex; flex-shrink: 0; box-sizing: border-box;
1188
+ align-items: center; justify-content: center;
1189
+ border: none; padding: 0; cursor: pointer;
1190
+ background: transparent; color: var(--fg-primary);
1191
+ transition: background-color .15s ease, outline-color .15s ease;
1192
+ outline: 2px solid transparent; outline-offset: 2px;
1193
+ }
1194
+ & .quantitypicker-button:focus-visible { outline-color: var(--stroke-focus-ring); }
1195
+ & .quantitypicker-button:not([data-disabled]):hover { background: var(--bg-raised); }
1196
+ & .quantitypicker-button[data-loading] { background: var(--bg-raised); }
1197
+ & .quantitypicker-button[data-disabled] { cursor: not-allowed; color: var(--fg-disabled); }
1198
+
1199
+ & .quantitypicker-value {
1200
+ display: grid; flex-grow: 1; flex-shrink: 0; box-sizing: border-box;
1201
+ align-items: center; justify-content: center;
1202
+ padding-inline: var(--space-2);
1203
+ font-weight: var(--weight-medium); color: var(--fg-primary);
1204
+ }
1205
+ & .quantitypicker-value[data-disabled] { color: var(--fg-disabled); }
1206
+ & .quantitypicker-placeholder {
1207
+ grid-column: 1; grid-row: 1; visibility: hidden;
1208
+ font-variant-numeric: tabular-nums;
1209
+ }
1210
+ & .quantitypicker-text {
1211
+ grid-column: 1; grid-row: 1; text-align: center;
1212
+ font-variant-numeric: tabular-nums;
1213
+ }
1214
+
1215
+ & .quantitypicker-divider {
1216
+ flex-shrink: 0; width: 1px; height: var(--space-4);
1217
+ background: var(--border-subtle);
1218
+ }
1219
+
1220
+ }
1221
+ .quantitypicker--size_small {
1222
+
1223
+ height: 36px; border-radius: var(--radius-md);
1224
+ & .quantitypicker-button { width: 36px; height: 36px; border-radius: var(--radius-md); }
1225
+ & .quantitypicker-value { font-size: var(--text-xl); line-height: 22px; }
1226
+
1227
+ }
1228
+ .quantitypicker--size_medium {
1229
+
1230
+ height: 40px; border-radius: var(--radius-md);
1231
+ & .quantitypicker-button { width: 40px; height: 40px; border-radius: var(--radius-md); }
1232
+ & .quantitypicker-value { font-size: var(--text-xl); line-height: 22px; }
1233
+
1234
+ }
1235
+ .quantitypicker--size_large {
1236
+
1237
+ height: 52px; border-radius: var(--radius-xl);
1238
+ & .quantitypicker-button { width: 52px; height: 52px; border-radius: var(--radius-xl); }
1239
+ & .quantitypicker-value { font-size: 18px; line-height: 24px; }
1240
+
1241
+ }
1242
+ .quantitypicker--fill_true {
1243
+ width: auto; flex-grow: 1;
1244
+ }
1245
+ .quantitypicker--fill_false {
1246
+
1247
+ }
1248
+
1249
+ @media (prefers-reduced-motion: reduce) {
1250
+ .quantitypicker, .quantitypicker * {
1251
+ animation-duration: 0.01ms !important;
1252
+ animation-iteration-count: 1 !important;
1253
+ transition-duration: 0.01ms !important;
1254
+ }
1255
+ }
1256
+
1178
1257
  .radiogroup {
1179
1258
 
1180
1259
  display: flex; flex-direction: column; gap: var(--space-1); font-family: var(--font-sans);
@@ -0,0 +1 @@
1
+ export * from "./middletruncate";
@@ -0,0 +1,20 @@
1
+ import { type MiddleTruncateProps as SeedProps } from "@seed-design/react-middle-truncate";
2
+ export type MiddleTruncateProps = Omit<SeedProps, "asChild"> & {
3
+ /** The full string. Kept whole in the DOM's accessible name via `title`. */
4
+ children: string;
5
+ };
6
+ /**
7
+ * Truncates in the MIDDLE rather than the end, so the part that identifies a
8
+ * string survives: `annual-report-fi…-v7-final.pdf` keeps its extension, and a
9
+ * record title keeps its suffix. An end-truncating `text-overflow: ellipsis`
10
+ * throws exactly that away.
11
+ *
12
+ * Measurement is seed-design's — it re-measures on resize with a
13
+ * `ResizeObserver` and only touches the DOM inside an effect, so it renders the
14
+ * full string on the server and narrows it on hydration.
15
+ *
16
+ * It owns no pixels, in crisp or in seed: there is no recipe here because
17
+ * seed-design ships no stylesheet for it either. It inherits the type around it
18
+ * by design, which is what makes it usable inside a table cell.
19
+ */
20
+ export declare function MiddleTruncate({ children, title, ...props }: MiddleTruncateProps): import("react").JSX.Element;
@@ -0,0 +1,2 @@
1
+ export * from "./quantitypicker";
2
+ export * from "./quantitypicker.recipe";
@@ -0,0 +1,43 @@
1
+ import { type QuantityPickerRootProps } from "@seed-design/react-quantity-picker";
2
+ /** seed's icon box per size, read from their stylesheet. */
3
+ declare const ICON: {
4
+ readonly small: 16;
5
+ readonly medium: 18;
6
+ readonly large: 22;
7
+ };
8
+ export type QuantityPickerSize = keyof typeof ICON;
9
+ export type QuantityPickerProps = QuantityPickerRootProps & {
10
+ /** 36 / 40 / 52px tall. Default `medium`. */
11
+ size?: QuantityPickerSize;
12
+ /** Stretch to the row rather than sizing to content. */
13
+ fill?: boolean;
14
+ /** Renders a hidden input under this name, so the value submits with a form. */
15
+ name?: string;
16
+ /** Accessible name for the minus button. Not used in remove mode — the
17
+ * engine substitutes `removeAriaLabel` there. */
18
+ decrementLabel?: string;
19
+ /** Accessible name for the plus button. */
20
+ incrementLabel?: string;
21
+ };
22
+ /**
23
+ * A quantity stepper, driven by seed-design's headless engine so the clamping,
24
+ * the step arithmetic, the per-button loading state and the `removable` mode —
25
+ * where decrementing at the minimum becomes "remove this line" rather than a
26
+ * dead button — are theirs by construction.
27
+ *
28
+ * Two things are crisp's:
29
+ *
30
+ * The buttons carry accessible names. The engine labels the minus button only
31
+ * in remove mode, so a plain stepper would ship two buttons whose entire
32
+ * content is an SVG and whose accessible name is empty.
33
+ *
34
+ * `dir` is worth passing explicitly. The engine defaults it to `"ltr"` and
35
+ * stamps that on the root, which overrides an inherited RTL — the same trap
36
+ * Slider documents. Here it also decides child ORDER, since the engine
37
+ * reverses its visible children rather than relying on CSS.
38
+ */
39
+ export declare function QuantityPicker({ size, fill, className, name, decrementLabel, incrementLabel, ...props }: QuantityPickerProps): import("react").JSX.Element;
40
+ export declare namespace QuantityPicker {
41
+ var displayName: string;
42
+ }
43
+ export {};
@@ -0,0 +1,38 @@
1
+ declare const config: import("../..").RecipeConfig<{
2
+ size: {
3
+ small: string;
4
+ medium: string;
5
+ large: string;
6
+ };
7
+ /** `fill` stretches to the row it sits in; seed's `layout_fill`. */
8
+ fill: {
9
+ true: string;
10
+ false: string;
11
+ };
12
+ }>;
13
+ export declare const quantitypickerRecipe: import("../..").RecipeFn<{
14
+ size: {
15
+ small: string;
16
+ medium: string;
17
+ large: string;
18
+ };
19
+ /** `fill` stretches to the row it sits in; seed's `layout_fill`. */
20
+ fill: {
21
+ true: string;
22
+ false: string;
23
+ };
24
+ }> & {
25
+ config: import("../..").RecipeConfig<{
26
+ size: {
27
+ small: string;
28
+ medium: string;
29
+ large: string;
30
+ };
31
+ /** `fill` stretches to the row it sits in; seed's `layout_fill`. */
32
+ fill: {
33
+ true: string;
34
+ false: string;
35
+ };
36
+ }>;
37
+ };
38
+ export default config;
@@ -25,9 +25,11 @@ export * from "./components/input";
25
25
  export * from "./components/kbd";
26
26
  export * from "./components/linechart";
27
27
  export * from "./components/menu";
28
+ export * from "./components/middletruncate";
28
29
  export * from "./components/multiselect";
29
30
  export * from "./components/popover";
30
31
  export * from "./components/progress";
32
+ export * from "./components/quantitypicker";
31
33
  export * from "./components/radiogroup";
32
34
  export * from "./components/resizablepanel";
33
35
  export * from "./components/segmented";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crisp-ui-kit/crisp",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "A lean React design system, measured pixel-for-pixel from Attio.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -167,6 +167,11 @@
167
167
  "import": "./dist/esm/components/menu/index.js",
168
168
  "require": "./dist/cjs/components/menu/index.js"
169
169
  },
170
+ "./middletruncate": {
171
+ "types": "./dist/types/components/middletruncate/index.d.ts",
172
+ "import": "./dist/esm/components/middletruncate/index.js",
173
+ "require": "./dist/cjs/components/middletruncate/index.js"
174
+ },
170
175
  "./multiselect": {
171
176
  "types": "./dist/types/components/multiselect/index.d.ts",
172
177
  "import": "./dist/esm/components/multiselect/index.js",
@@ -182,6 +187,11 @@
182
187
  "import": "./dist/esm/components/progress/index.js",
183
188
  "require": "./dist/cjs/components/progress/index.js"
184
189
  },
190
+ "./quantitypicker": {
191
+ "types": "./dist/types/components/quantitypicker/index.d.ts",
192
+ "import": "./dist/esm/components/quantitypicker/index.js",
193
+ "require": "./dist/cjs/components/quantitypicker/index.js"
194
+ },
185
195
  "./radiogroup": {
186
196
  "types": "./dist/types/components/radiogroup/index.d.ts",
187
197
  "import": "./dist/esm/components/radiogroup/index.js",
@@ -407,6 +417,7 @@
407
417
  "test": "bun test",
408
418
  "spec": "bun build/spec-check.mjs",
409
419
  "lynx": "bun build/lynx-lint.mjs",
420
+ "lynx:emit": "bun build/emit-lynx.mjs",
410
421
  "ssr": "bun scripts/ssr-check.tsx",
411
422
  "contrast": "bun scripts/contrast-page.tsx",
412
423
  "build:esm": "tsc -p tsconfig.esm.json && bun scripts/fix-esm-extensions.mjs",
@@ -423,6 +434,8 @@
423
434
  "@seed-design/dom-utils": "^2.1.0",
424
435
  "@seed-design/react-date-picker": "1.2.0",
425
436
  "@seed-design/react-file-upload": "1.0.1",
437
+ "@seed-design/react-middle-truncate": "1.0.1",
438
+ "@seed-design/react-quantity-picker": "1.0.0",
426
439
  "@seed-design/react-slider": "2.0.1",
427
440
  "@seed-design/react-time-picker": "1.0.0",
428
441
  "@zag-js/combobox": "^1.43.3",