@crisp-ui-kit/crisp 0.10.1 → 0.12.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,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
@@ -49,6 +49,7 @@ __exportStar(require("./components/middletruncate"), exports);
49
49
  __exportStar(require("./components/multiselect"), exports);
50
50
  __exportStar(require("./components/popover"), exports);
51
51
  __exportStar(require("./components/progress"), exports);
52
+ __exportStar(require("./components/quantitypicker"), exports);
52
53
  __exportStar(require("./components/radiogroup"), exports);
53
54
  __exportStar(require("./components/resizablepanel"), exports);
54
55
  __exportStar(require("./components/segmented"), exports);
@@ -45,7 +45,7 @@ const exec = (cmd, value) => document.execCommand(cmd, false, value);
45
45
  * that appears over a text selection. Built on contentEditable, dependency-free.
46
46
  * Uncontrolled: seed with `defaultTitle`/`defaultHtml`, read via `onChange`.
47
47
  */
48
- function RichTextEditor({ defaultTitle = "", defaultHtml = "", titlePlaceholder = "Untitled", placeholder = "Start writing, or press a toolbar button…", onChange, className, titleLabel = "Title", bodyLabel = "Note body", }) {
48
+ function RichTextEditor({ defaultTitle = "", defaultHtml = "", titlePlaceholder = "Untitled", placeholder = "Start writing, or press a toolbar button…", onChange, className, titleLabel = "Title", bodyLabel = "Note body", linkPromptLabel = "Link URL", }) {
49
49
  const titleRef = React.useRef(null);
50
50
  const bodyRef = React.useRef(null);
51
51
  const [bar, setBar] = React.useState(null);
@@ -128,13 +128,19 @@ function RichTextEditor({ defaultTitle = "", defaultHtml = "", titlePlaceholder
128
128
  run("formatBlock", blockTag() === tag ? "p" : tag);
129
129
  };
130
130
  const addLink = () => {
131
- const url = window.prompt("Link URL");
131
+ const url = window.prompt(linkPromptLabel);
132
132
  if (url)
133
133
  run("createLink", url);
134
134
  };
135
135
  // mousedown on the toolbar must not clear the selection
136
136
  const keepSel = (e) => e.preventDefault();
137
- const tool = (key, label, icon, onClick) => ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "rte-tool", "aria-label": label, "aria-pressed": !!active[key], "data-active": active[key] ? "" : undefined, onMouseDown: keepSel, onClick: onClick, children: icon }));
137
+ /**
138
+ * `toggles` is false for a tool that performs an action rather than carrying
139
+ * a state. Link opens a prompt and has no "on" — giving it `aria-pressed`
140
+ * announces an un-pressed toggle that can never be pressed, which tells a
141
+ * screen-reader user there is a state here that does not exist.
142
+ */
143
+ const tool = (key, label, icon, onClick, toggles = true) => ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "rte-tool", "aria-label": label, "aria-pressed": toggles ? !!active[key] : undefined, "data-active": active[key] ? "" : undefined, onMouseDown: keepSel, onClick: onClick, children: icon }));
138
144
  const ic = { size: 15, strokeWidth: 2 };
139
145
  return ((0, jsx_runtime_1.jsxs)("div", { className: (0, richtext_recipe_1.richtextRecipe)({ className }), children: [(0, jsx_runtime_1.jsx)("div", { ref: titleRef, className: "rte-title", contentEditable: true, suppressContentEditableWarning: true, role: "textbox", tabIndex: 0, "aria-label": titleLabel, "data-placeholder": titlePlaceholder, onInput: () => {
140
146
  syncEmpty(titleRef.current);
@@ -142,6 +148,6 @@ function RichTextEditor({ defaultTitle = "", defaultHtml = "", titlePlaceholder
142
148
  } }), (0, jsx_runtime_1.jsx)("div", { ref: bodyRef, className: "rte-body", contentEditable: true, suppressContentEditableWarning: true, role: "textbox", tabIndex: 0, "aria-multiline": "true", "aria-label": bodyLabel, "data-placeholder": placeholder, onInput: () => {
143
149
  syncEmpty(bodyRef.current);
144
150
  emit();
145
- }, onMouseUp: updateBar, onKeyUp: updateBar }), bar && ((0, jsx_runtime_1.jsxs)("div", { className: "rte-toolbar", style: { left: bar.x, top: bar.y }, children: [tool("bold", "Bold", (0, jsx_runtime_1.jsx)(lucide_react_1.Bold, { ...ic }), () => run("bold")), tool("italic", "Italic", (0, jsx_runtime_1.jsx)(lucide_react_1.Italic, { ...ic }), () => run("italic")), tool("underline", "Underline", (0, jsx_runtime_1.jsx)(lucide_react_1.Underline, { ...ic }), () => run("underline")), tool("strikeThrough", "Strikethrough", (0, jsx_runtime_1.jsx)(lucide_react_1.Strikethrough, { ...ic }), () => run("strikeThrough")), tool("link", "Link", (0, jsx_runtime_1.jsx)(lucide_react_1.Link2, { ...ic }), addLink), (0, jsx_runtime_1.jsx)("span", { className: "rte-tool-sep" }), tool("h1", "Heading 1", (0, jsx_runtime_1.jsx)(lucide_react_1.Heading1, { ...ic }), () => toggleBlock("h1")), tool("h2", "Heading 2", (0, jsx_runtime_1.jsx)(lucide_react_1.Heading2, { ...ic }), () => toggleBlock("h2")), tool("ul", "Bullet list", (0, jsx_runtime_1.jsx)(lucide_react_1.List, { ...ic }), () => run("insertUnorderedList")), tool("ol", "Numbered list", (0, jsx_runtime_1.jsx)(lucide_react_1.ListOrdered, { ...ic }), () => run("insertOrderedList")), tool("blockquote", "Quote", (0, jsx_runtime_1.jsx)(lucide_react_1.Quote, { ...ic }), () => toggleBlock("blockquote"))] }))] }));
151
+ }, onMouseUp: updateBar, onKeyUp: updateBar }), bar && ((0, jsx_runtime_1.jsxs)("div", { className: "rte-toolbar", style: { left: bar.x, top: bar.y }, children: [tool("bold", "Bold", (0, jsx_runtime_1.jsx)(lucide_react_1.Bold, { ...ic }), () => run("bold")), tool("italic", "Italic", (0, jsx_runtime_1.jsx)(lucide_react_1.Italic, { ...ic }), () => run("italic")), tool("underline", "Underline", (0, jsx_runtime_1.jsx)(lucide_react_1.Underline, { ...ic }), () => run("underline")), tool("strikeThrough", "Strikethrough", (0, jsx_runtime_1.jsx)(lucide_react_1.Strikethrough, { ...ic }), () => run("strikeThrough")), tool("link", "Link", (0, jsx_runtime_1.jsx)(lucide_react_1.Link2, { ...ic }), addLink, false), (0, jsx_runtime_1.jsx)("span", { className: "rte-tool-sep" }), tool("h1", "Heading 1", (0, jsx_runtime_1.jsx)(lucide_react_1.Heading1, { ...ic }), () => toggleBlock("h1")), tool("h2", "Heading 2", (0, jsx_runtime_1.jsx)(lucide_react_1.Heading2, { ...ic }), () => toggleBlock("h2")), tool("ul", "Bullet list", (0, jsx_runtime_1.jsx)(lucide_react_1.List, { ...ic }), () => run("insertUnorderedList")), tool("ol", "Numbered list", (0, jsx_runtime_1.jsx)(lucide_react_1.ListOrdered, { ...ic }), () => run("insertOrderedList")), tool("blockquote", "Quote", (0, jsx_runtime_1.jsx)(lucide_react_1.Quote, { ...ic }), () => toggleBlock("blockquote"))] }))] }));
146
152
  }
147
153
  RichTextEditor.displayName = "RichTextEditor";
@@ -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
@@ -33,6 +33,7 @@ export * from "./components/middletruncate/index.js";
33
33
  export * from "./components/multiselect/index.js";
34
34
  export * from "./components/popover/index.js";
35
35
  export * from "./components/progress/index.js";
36
+ export * from "./components/quantitypicker/index.js";
36
37
  export * from "./components/radiogroup/index.js";
37
38
  export * from "./components/resizablepanel/index.js";
38
39
  export * from "./components/segmented/index.js";
@@ -9,7 +9,7 @@ const exec = (cmd, value) => document.execCommand(cmd, false, value);
9
9
  * that appears over a text selection. Built on contentEditable, dependency-free.
10
10
  * Uncontrolled: seed with `defaultTitle`/`defaultHtml`, read via `onChange`.
11
11
  */
12
- export function RichTextEditor({ defaultTitle = "", defaultHtml = "", titlePlaceholder = "Untitled", placeholder = "Start writing, or press a toolbar button…", onChange, className, titleLabel = "Title", bodyLabel = "Note body", }) {
12
+ export function RichTextEditor({ defaultTitle = "", defaultHtml = "", titlePlaceholder = "Untitled", placeholder = "Start writing, or press a toolbar button…", onChange, className, titleLabel = "Title", bodyLabel = "Note body", linkPromptLabel = "Link URL", }) {
13
13
  const titleRef = React.useRef(null);
14
14
  const bodyRef = React.useRef(null);
15
15
  const [bar, setBar] = React.useState(null);
@@ -92,13 +92,19 @@ export function RichTextEditor({ defaultTitle = "", defaultHtml = "", titlePlace
92
92
  run("formatBlock", blockTag() === tag ? "p" : tag);
93
93
  };
94
94
  const addLink = () => {
95
- const url = window.prompt("Link URL");
95
+ const url = window.prompt(linkPromptLabel);
96
96
  if (url)
97
97
  run("createLink", url);
98
98
  };
99
99
  // mousedown on the toolbar must not clear the selection
100
100
  const keepSel = (e) => e.preventDefault();
101
- const tool = (key, label, icon, onClick) => (_jsx("button", { type: "button", className: "rte-tool", "aria-label": label, "aria-pressed": !!active[key], "data-active": active[key] ? "" : undefined, onMouseDown: keepSel, onClick: onClick, children: icon }));
101
+ /**
102
+ * `toggles` is false for a tool that performs an action rather than carrying
103
+ * a state. Link opens a prompt and has no "on" — giving it `aria-pressed`
104
+ * announces an un-pressed toggle that can never be pressed, which tells a
105
+ * screen-reader user there is a state here that does not exist.
106
+ */
107
+ const tool = (key, label, icon, onClick, toggles = true) => (_jsx("button", { type: "button", className: "rte-tool", "aria-label": label, "aria-pressed": toggles ? !!active[key] : undefined, "data-active": active[key] ? "" : undefined, onMouseDown: keepSel, onClick: onClick, children: icon }));
102
108
  const ic = { size: 15, strokeWidth: 2 };
103
109
  return (_jsxs("div", { className: richtextRecipe({ className }), children: [_jsx("div", { ref: titleRef, className: "rte-title", contentEditable: true, suppressContentEditableWarning: true, role: "textbox", tabIndex: 0, "aria-label": titleLabel, "data-placeholder": titlePlaceholder, onInput: () => {
104
110
  syncEmpty(titleRef.current);
@@ -106,6 +112,6 @@ export function RichTextEditor({ defaultTitle = "", defaultHtml = "", titlePlace
106
112
  } }), _jsx("div", { ref: bodyRef, className: "rte-body", contentEditable: true, suppressContentEditableWarning: true, role: "textbox", tabIndex: 0, "aria-multiline": "true", "aria-label": bodyLabel, "data-placeholder": placeholder, onInput: () => {
107
113
  syncEmpty(bodyRef.current);
108
114
  emit();
109
- }, onMouseUp: updateBar, onKeyUp: updateBar }), bar && (_jsxs("div", { className: "rte-toolbar", style: { left: bar.x, top: bar.y }, children: [tool("bold", "Bold", _jsx(Bold, { ...ic }), () => run("bold")), tool("italic", "Italic", _jsx(Italic, { ...ic }), () => run("italic")), tool("underline", "Underline", _jsx(Underline, { ...ic }), () => run("underline")), tool("strikeThrough", "Strikethrough", _jsx(Strikethrough, { ...ic }), () => run("strikeThrough")), tool("link", "Link", _jsx(Link2, { ...ic }), addLink), _jsx("span", { className: "rte-tool-sep" }), tool("h1", "Heading 1", _jsx(Heading1, { ...ic }), () => toggleBlock("h1")), tool("h2", "Heading 2", _jsx(Heading2, { ...ic }), () => toggleBlock("h2")), tool("ul", "Bullet list", _jsx(List, { ...ic }), () => run("insertUnorderedList")), tool("ol", "Numbered list", _jsx(ListOrdered, { ...ic }), () => run("insertOrderedList")), tool("blockquote", "Quote", _jsx(Quote, { ...ic }), () => toggleBlock("blockquote"))] }))] }));
115
+ }, onMouseUp: updateBar, onKeyUp: updateBar }), bar && (_jsxs("div", { className: "rte-toolbar", style: { left: bar.x, top: bar.y }, children: [tool("bold", "Bold", _jsx(Bold, { ...ic }), () => run("bold")), tool("italic", "Italic", _jsx(Italic, { ...ic }), () => run("italic")), tool("underline", "Underline", _jsx(Underline, { ...ic }), () => run("underline")), tool("strikeThrough", "Strikethrough", _jsx(Strikethrough, { ...ic }), () => run("strikeThrough")), tool("link", "Link", _jsx(Link2, { ...ic }), addLink, false), _jsx("span", { className: "rte-tool-sep" }), tool("h1", "Heading 1", _jsx(Heading1, { ...ic }), () => toggleBlock("h1")), tool("h2", "Heading 2", _jsx(Heading2, { ...ic }), () => toggleBlock("h2")), tool("ul", "Bullet list", _jsx(List, { ...ic }), () => run("insertUnorderedList")), tool("ol", "Numbered list", _jsx(ListOrdered, { ...ic }), () => run("insertOrderedList")), tool("blockquote", "Quote", _jsx(Quote, { ...ic }), () => toggleBlock("blockquote"))] }))] }));
110
116
  }
111
117
  RichTextEditor.displayName = "RichTextEditor";
@@ -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,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;
@@ -29,6 +29,7 @@ export * from "./components/middletruncate";
29
29
  export * from "./components/multiselect";
30
30
  export * from "./components/popover";
31
31
  export * from "./components/progress";
32
+ export * from "./components/quantitypicker";
32
33
  export * from "./components/radiogroup";
33
34
  export * from "./components/resizablepanel";
34
35
  export * from "./components/segmented";
@@ -8,6 +8,8 @@ export type RichTextEditorProps = {
8
8
  titleLabel?: string;
9
9
  /** Accessible name for the body editor. */
10
10
  bodyLabel?: string;
11
+ /** Message shown when asking for a link URL. */
12
+ linkPromptLabel?: string;
11
13
  defaultTitle?: string;
12
14
  /** Initial body HTML. */
13
15
  defaultHtml?: string;
@@ -22,7 +24,7 @@ export type RichTextEditorProps = {
22
24
  * that appears over a text selection. Built on contentEditable, dependency-free.
23
25
  * Uncontrolled: seed with `defaultTitle`/`defaultHtml`, read via `onChange`.
24
26
  */
25
- export declare function RichTextEditor({ defaultTitle, defaultHtml, titlePlaceholder, placeholder, onChange, className, titleLabel, bodyLabel, }: RichTextEditorProps): React.JSX.Element;
27
+ export declare function RichTextEditor({ defaultTitle, defaultHtml, titlePlaceholder, placeholder, onChange, className, titleLabel, bodyLabel, linkPromptLabel, }: RichTextEditorProps): React.JSX.Element;
26
28
  export declare namespace RichTextEditor {
27
29
  var displayName: string;
28
30
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crisp-ui-kit/crisp",
3
- "version": "0.10.1",
3
+ "version": "0.12.0",
4
4
  "description": "A lean React design system, measured pixel-for-pixel from Attio.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -187,6 +187,11 @@
187
187
  "import": "./dist/esm/components/progress/index.js",
188
188
  "require": "./dist/cjs/components/progress/index.js"
189
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
+ },
190
195
  "./radiogroup": {
191
196
  "types": "./dist/types/components/radiogroup/index.d.ts",
192
197
  "import": "./dist/esm/components/radiogroup/index.js",
@@ -412,6 +417,7 @@
412
417
  "test": "bun test",
413
418
  "spec": "bun build/spec-check.mjs",
414
419
  "lynx": "bun build/lynx-lint.mjs",
420
+ "lynx:emit": "bun build/emit-lynx.mjs",
415
421
  "ssr": "bun scripts/ssr-check.tsx",
416
422
  "contrast": "bun scripts/contrast-page.tsx",
417
423
  "build:esm": "tsc -p tsconfig.esm.json && bun scripts/fix-esm-extensions.mjs",
@@ -429,6 +435,7 @@
429
435
  "@seed-design/react-date-picker": "1.2.0",
430
436
  "@seed-design/react-file-upload": "1.0.1",
431
437
  "@seed-design/react-middle-truncate": "1.0.1",
438
+ "@seed-design/react-quantity-picker": "1.0.0",
432
439
  "@seed-design/react-slider": "2.0.1",
433
440
  "@seed-design/react-time-picker": "1.0.0",
434
441
  "@zag-js/combobox": "^1.43.3",