@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.
- package/dist/cjs/components/quantitypicker/index.js +18 -0
- package/dist/cjs/components/quantitypicker/quantitypicker.js +62 -0
- package/dist/cjs/components/quantitypicker/quantitypicker.recipe.js +114 -0
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/product/richtext/richtext.js +10 -4
- package/dist/esm/components/quantitypicker/index.js +2 -0
- package/dist/esm/components/quantitypicker/quantitypicker.js +59 -0
- package/dist/esm/components/quantitypicker/quantitypicker.recipe.js +111 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/product/richtext/richtext.js +10 -4
- package/dist/quantitypicker.css +78 -0
- package/dist/quantitypicker.d.ts +8 -0
- package/dist/styles.css +79 -0
- package/dist/types/components/quantitypicker/index.d.ts +2 -0
- package/dist/types/components/quantitypicker/quantitypicker.d.ts +43 -0
- package/dist/types/components/quantitypicker/quantitypicker.recipe.d.ts +38 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/product/richtext/richtext.d.ts +3 -1
- package/package.json +8 -1
|
@@ -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(
|
|
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
|
-
|
|
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,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(
|
|
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
|
-
|
|
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,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;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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.
|
|
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",
|