@crisp-ui-kit/crisp 0.9.0 → 0.10.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.
Files changed (42) hide show
  1. package/dist/appshell.css +2 -0
  2. package/dist/breadcrumb.css +2 -0
  3. package/dist/cjs/components/breadcrumb/breadcrumb.js +1 -1
  4. package/dist/cjs/components/breadcrumb/breadcrumb.recipe.js +3 -1
  5. package/dist/cjs/components/slider/index.js +18 -0
  6. package/dist/cjs/components/slider/slider.js +22 -0
  7. package/dist/cjs/components/slider/slider.recipe.js +70 -0
  8. package/dist/cjs/core/index.js +15 -0
  9. package/dist/cjs/core/rtl.js +17 -0
  10. package/dist/cjs/index.js +1 -0
  11. package/dist/cjs/product/appshell/appshell.js +1 -1
  12. package/dist/cjs/product/appshell/appshell.recipe.js +2 -0
  13. package/dist/cjs/product/recordstable/recordstable.js +2 -2
  14. package/dist/cjs/product/recordstable/recordstable.recipe.js +2 -0
  15. package/dist/cjs/product/settingsshell/settingsshell.js +1 -1
  16. package/dist/cjs/product/settingsshell/settingsshell.recipe.js +3 -1
  17. package/dist/esm/components/breadcrumb/breadcrumb.js +1 -1
  18. package/dist/esm/components/breadcrumb/breadcrumb.recipe.js +4 -2
  19. package/dist/esm/components/slider/index.js +2 -0
  20. package/dist/esm/components/slider/slider.js +19 -0
  21. package/dist/esm/components/slider/slider.recipe.js +67 -0
  22. package/dist/esm/core/index.js +1 -0
  23. package/dist/esm/core/rtl.js +14 -0
  24. package/dist/esm/index.js +1 -0
  25. package/dist/esm/product/appshell/appshell.js +1 -1
  26. package/dist/esm/product/appshell/appshell.recipe.js +3 -1
  27. package/dist/esm/product/recordstable/recordstable.js +2 -2
  28. package/dist/esm/product/recordstable/recordstable.recipe.js +3 -1
  29. package/dist/esm/product/settingsshell/settingsshell.js +1 -1
  30. package/dist/esm/product/settingsshell/settingsshell.recipe.js +4 -2
  31. package/dist/recordstable.css +2 -0
  32. package/dist/settingsshell.css +2 -0
  33. package/dist/slider.css +50 -0
  34. package/dist/slider.d.ts +7 -0
  35. package/dist/styles.css +59 -0
  36. package/dist/types/components/slider/index.d.ts +2 -0
  37. package/dist/types/components/slider/slider.d.ts +8 -0
  38. package/dist/types/components/slider/slider.recipe.d.ts +5 -0
  39. package/dist/types/core/index.d.ts +1 -0
  40. package/dist/types/core/rtl.d.ts +14 -0
  41. package/dist/types/index.d.ts +1 -0
  42. package/package.json +7 -1
package/dist/appshell.css CHANGED
@@ -10,6 +10,8 @@
10
10
  & .appshell-expand { position: absolute; top: var(--space-2); inset-inline-start: var(--space-2); z-index: 6; display: inline-flex; align-items: center; justify-content: center; width: var(--size-row); height: var(--size-row); padding: 0; border: none; border-radius: var(--radius-md); box-shadow: inset 0 0 0 1px var(--border-subtle); background: var(--bg-surface); color: var(--fg-muted); font-family: inherit; cursor: pointer; transition: background-color .12s; }
11
11
  & .appshell-expand:hover { background: var(--bg-raised); color: var(--fg-primary); }
12
12
 
13
+ [dir="rtl"] & [data-rtl-flip] { transform: scaleX(-1); }
14
+
13
15
  }
14
16
 
15
17
  @media (prefers-reduced-motion: reduce) {
@@ -6,4 +6,6 @@
6
6
  & button.breadcrumb-item { border: none; background: none; font: inherit; cursor: pointer; }
7
7
  & .breadcrumb-item[aria-current] { color: var(--fg-primary); font-weight: var(--weight-medium); }
8
8
  & .breadcrumb-sep { flex-shrink: 0; color: color-mix(in srgb, var(--fg-primary) 28%, transparent); }
9
+ [dir="rtl"] & [data-rtl-flip] { transform: scaleX(-1); }
10
+
9
11
  }
@@ -43,7 +43,7 @@ const breadcrumb_recipe_1 = require("./breadcrumb.recipe");
43
43
  * marked `aria-current="page"`; earlier items render as links or buttons.
44
44
  */
45
45
  function Breadcrumb({ items, separator, "aria-label": ariaLabel = "Breadcrumb", className, ...rest }) {
46
- const sep = separator ?? ((0, jsx_runtime_1.jsx)(lucide_react_1.ChevronRight, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }));
46
+ const sep = separator ?? ((0, jsx_runtime_1.jsx)(lucide_react_1.ChevronRight, { "data-rtl-flip": "", size: 14, strokeWidth: 1.75, "aria-hidden": "true" }));
47
47
  return ((0, jsx_runtime_1.jsx)("nav", { "aria-label": ariaLabel, className: (0, breadcrumb_recipe_1.breadcrumbRecipe)({ className }), ...rest, children: items.map((item, i) => {
48
48
  const last = i === items.length - 1;
49
49
  const content = ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [item.icon, item.label] }));
@@ -12,7 +12,9 @@ const config = (0, core_1.defineRecipe)({
12
12
  & a.breadcrumb-item:hover, & button.breadcrumb-item:hover { color: ${tokens_1.contract.fgPrimary}; background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 5%, transparent); }
13
13
  & button.breadcrumb-item { border: none; background: none; font: inherit; cursor: pointer; }
14
14
  & .breadcrumb-item[aria-current] { color: ${tokens_1.contract.fgPrimary}; font-weight: ${tokens_1.contract.weight.medium}; }
15
- & .breadcrumb-sep { flex-shrink: 0; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 28%, transparent); }`,
15
+ & .breadcrumb-sep { flex-shrink: 0; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 28%, transparent); }
16
+ ${core_1.rtlFlipCss}
17
+ `,
16
18
  variants: {},
17
19
  defaultVariants: {},
18
20
  compoundVariants: [],
@@ -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("./slider"), exports);
18
+ __exportStar(require("./slider.recipe"), exports);
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ "use client";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.Slider = Slider;
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ const react_slider_1 = require("@seed-design/react-slider");
7
+ const slider_recipe_1 = require("./slider.recipe");
8
+ /** Reads the thumb count from the engine rather than re-deriving it from props. */
9
+ function Thumbs() {
10
+ const { values } = (0, react_slider_1.useSliderContext)();
11
+ return ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: values.map((_, i) => ((0, jsx_runtime_1.jsx)(react_slider_1.SliderThumb
12
+ // biome-ignore lint/suspicious/noArrayIndexKey: thumbs are positional — the index IS the identity the engine addresses them by
13
+ , { thumbIndex: i, className: "slider-thumb" }, i))) }));
14
+ }
15
+ /**
16
+ * A range input, driven by seed-design's headless engine so the keyboard model,
17
+ * the pointer maths and multi-thumb constraints are theirs by construction.
18
+ * Pass one value for a simple slider, two for a range.
19
+ */
20
+ function Slider({ className, ...props }) {
21
+ return ((0, jsx_runtime_1.jsx)(react_slider_1.SliderRoot, { ...props, className: (0, slider_recipe_1.sliderRecipe)({ className }), children: (0, jsx_runtime_1.jsxs)("span", { className: "slider-control", children: [(0, jsx_runtime_1.jsx)("span", { className: "slider-track", children: (0, jsx_runtime_1.jsx)(react_slider_1.SliderRange, { className: "slider-range" }) }), (0, jsx_runtime_1.jsx)(Thumbs, {})] }) }));
22
+ }
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sliderRecipe = void 0;
4
+ const core_1 = require("../../core");
5
+ const tokens_1 = require("../../tokens");
6
+ // seed-design's slider, shape for shape, in crisp's colours.
7
+ //
8
+ // Attio ships no slider anywhere — Records, Reports, Settings and the record
9
+ // pages contain no `input[type=range]` or `[role=slider]` — so there is nothing
10
+ // of Attio's to measure. The GEOMETRY is therefore seed's own, read from
11
+ // `@seed-design/css/recipes/slider.css`: a 26px control row, a 4px track on
12
+ // `radius.full`, a 20px thumb whose visible disc is an `::after` that scales to
13
+ // 1.2 while dragging, and a 2px focus ring offset 2px.
14
+ //
15
+ // Only the COLOUR roles are re-pointed at crisp's tokens:
16
+ // seed gray-400 track -> bgNeutralWeak (crisp's weak neutral fill)
17
+ // seed fg-neutral range -> bgBrandSolid (crisp fills with brand, as Progress does)
18
+ // seed bg-neutral-inverted -> #fff + elevation.field (crisp's control disc, as Switch does)
19
+ //
20
+ // Position comes from the engine's own `--range-start`/`--range-end` and
21
+ // `--thumb-position`/`--thumb-offset`, switched on the `data-dir` it stamps —
22
+ // the same mechanism seed uses, rather than CSS direction inheritance.
23
+ const config = (0, core_1.defineRecipe)({
24
+ name: "slider",
25
+ base: `
26
+ display: flex; flex-direction: column; align-items: stretch;
27
+ width: 100%; min-width: 0; gap: ${tokens_1.contract.space["0.5"]};
28
+ font-family: ${tokens_1.contract.font.sans}; user-select: none; touch-action: none;
29
+
30
+ & .slider-control { position: relative; height: 26px; display: flex; align-items: center; }
31
+
32
+ & .slider-track {
33
+ position: relative; flex-grow: 1; height: ${tokens_1.contract.space["1"]};
34
+ border-radius: ${tokens_1.contract.radius.full}; background: ${tokens_1.contract.bgNeutralWeak};
35
+ overflow: hidden;
36
+ }
37
+
38
+ & .slider-range { position: absolute; height: 100%; background: ${tokens_1.contract.bgBrandSolid}; }
39
+ & .slider-range[data-dir="ltr"] { left: var(--range-start); right: var(--range-end); }
40
+ & .slider-range[data-dir="rtl"] { right: var(--range-start); left: var(--range-end); }
41
+
42
+ & .slider-thumb {
43
+ position: absolute; top: 50%;
44
+ width: ${tokens_1.contract.space["5"]}; height: ${tokens_1.contract.space["5"]};
45
+ transform: translate(-50%, -50%);
46
+ border: none; background: none; padding: 0;
47
+ }
48
+ & .slider-thumb[data-dir="ltr"] { left: calc(var(--thumb-position) * 1% + var(--thumb-offset)); }
49
+ & .slider-thumb[data-dir="rtl"] { right: calc(var(--thumb-position) * 1% + var(--thumb-offset)); }
50
+ & .slider-thumb::after {
51
+ content: ""; position: absolute; inset: 0;
52
+ border-radius: ${tokens_1.contract.radius.full}; background: #fff; box-shadow: ${tokens_1.elevation.field};
53
+ outline: 2px solid transparent; outline-offset: 2px;
54
+ transition: transform .2s ease, outline-color .2s ease;
55
+ cursor: grab;
56
+ }
57
+ & .slider-thumb[data-thumb-dragging]::after { transform: scale(1.2); cursor: grabbing; }
58
+ & .slider-thumb:focus { outline: none; }
59
+ & .slider-thumb:focus-visible::after { outline-color: ${tokens_1.contract.strokeFocusRing}; }
60
+
61
+ &[data-disabled] { cursor: not-allowed; }
62
+ &[data-disabled] .slider-range { background: ${tokens_1.contract.fgDisabled}; }
63
+ &[data-disabled] .slider-thumb::after { cursor: not-allowed; }
64
+ `,
65
+ variants: {},
66
+ defaultVariants: {},
67
+ compoundVariants: [],
68
+ });
69
+ exports.sliderRecipe = Object.assign((0, core_1.resolveRecipe)(config), { config });
70
+ exports.default = config;
@@ -1,4 +1,18 @@
1
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
+ };
2
16
  Object.defineProperty(exports, "__esModule", { value: true });
3
17
  exports.resolveRecipe = exports.Primitive = exports.defineRecipe = exports.cx = void 0;
4
18
  var cx_1 = require("./cx");
@@ -9,3 +23,4 @@ var primitive_1 = require("./primitive");
9
23
  Object.defineProperty(exports, "Primitive", { enumerable: true, get: function () { return primitive_1.Primitive; } });
10
24
  var resolve_1 = require("./resolve");
11
25
  Object.defineProperty(exports, "resolveRecipe", { enumerable: true, get: function () { return resolve_1.resolveRecipe; } });
26
+ __exportStar(require("./rtl"), exports);
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.rtlFlipCss = void 0;
4
+ /**
5
+ * Mirroring for direction-bearing icons.
6
+ *
7
+ * A chevron or arrow that means "back", "next", "toward the start" or "open"
8
+ * points the wrong way under RTL, and unlike spacing or alignment it cannot be
9
+ * fixed with a logical property: these are `lucide-react` nodes, static SVG
10
+ * paths with no direction of their own.
11
+ *
12
+ * Mark the icons that carry a direction with `data-rtl-flip` and include this
13
+ * snippet in the recipe. Icons whose meaning is NOT directional — a downward
14
+ * disclosure chevron, a search glass, a check — must be left unmarked: flipping
15
+ * them is a regression, not a fix.
16
+ */
17
+ exports.rtlFlipCss = `[dir="rtl"] & [data-rtl-flip] { transform: scaleX(-1); }`;
package/dist/cjs/index.js CHANGED
@@ -53,6 +53,7 @@ __exportStar(require("./components/resizablepanel"), exports);
53
53
  __exportStar(require("./components/segmented"), exports);
54
54
  __exportStar(require("./components/select"), exports);
55
55
  __exportStar(require("./components/skeleton"), exports);
56
+ __exportStar(require("./components/slider"), exports);
56
57
  __exportStar(require("./components/spinner"), exports);
57
58
  __exportStar(require("./components/stagebar"), exports);
58
59
  __exportStar(require("./components/statcard"), exports);
@@ -99,6 +99,6 @@ function AppShell({ sidebar, children, width: widthProp, defaultWidth = 275, onW
99
99
  return ((0, jsx_runtime_1.jsxs)("div", { className: (0, appshell_recipe_1.appshellRecipe)({ className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("div", { className: "appshell-side", "data-collapsed": collapsed ? "" : undefined, style: {
100
100
  width: collapsed ? 0 : width,
101
101
  transition: dragging ? "none" : "width .14s ease",
102
- }, children: [(0, jsx_runtime_1.jsx)("div", { className: "appshell-side-inner", style: { width }, children: sidebar(toggleCollapse) }), !collapsed && ((0, jsx_runtime_1.jsx)("div", { className: "appshell-resize", "data-dragging": dragging ? "" : undefined, "aria-hidden": "true", onPointerDown: startResize }))] }), (0, jsx_runtime_1.jsxs)("div", { className: "appshell-main", children: [collapsed && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "appshell-expand", "aria-label": expandLabel, onClick: () => setCollapsed(false), children: (0, jsx_runtime_1.jsx)(lucide_react_1.PanelLeft, { size: 15, strokeWidth: 1.75 }) })), children] })] }));
102
+ }, children: [(0, jsx_runtime_1.jsx)("div", { className: "appshell-side-inner", style: { width }, children: sidebar(toggleCollapse) }), !collapsed && ((0, jsx_runtime_1.jsx)("div", { className: "appshell-resize", "data-dragging": dragging ? "" : undefined, "aria-hidden": "true", onPointerDown: startResize }))] }), (0, jsx_runtime_1.jsxs)("div", { className: "appshell-main", children: [collapsed && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "appshell-expand", "aria-label": expandLabel, onClick: () => setCollapsed(false), children: (0, jsx_runtime_1.jsx)(lucide_react_1.PanelLeft, { "data-rtl-flip": "", size: 15, strokeWidth: 1.75 }) })), children] })] }));
103
103
  }
104
104
  AppShell.displayName = "AppShell";
@@ -18,6 +18,8 @@ const config = (0, core_1.defineRecipe)({
18
18
  & .appshell-main { position: relative; flex: 1; min-width: 0; display: flex; flex-direction: column; overflow: hidden; }
19
19
  & .appshell-expand { position: absolute; top: ${tokens_1.contract.space["2"]}; inset-inline-start: ${tokens_1.contract.space["2"]}; z-index: 6; display: inline-flex; align-items: center; justify-content: center; width: ${tokens_1.contract.size.row}; height: ${tokens_1.contract.size.row}; padding: 0; border: none; border-radius: ${tokens_1.contract.radius.md}; box-shadow: inset 0 0 0 1px ${tokens_1.contract.borderSubtle}; background: ${tokens_1.contract.bgSurface}; color: ${tokens_1.contract.fgMuted}; font-family: inherit; cursor: pointer; transition: background-color .12s; }
20
20
  & .appshell-expand:hover { background: ${tokens_1.contract.bgRaised}; color: ${tokens_1.contract.fgPrimary}; }
21
+
22
+ ${core_1.rtlFlipCss}
21
23
  `,
22
24
  variants: {},
23
25
  defaultVariants: {},
@@ -984,7 +984,7 @@ function RecordsTable({ labels, columns: columnsProp = exports.defaultColumns, r
984
984
  }
985
985
  : undefined, onDragEnd: () => setDragCol(null), onClick: (e) => openColumnMenu(col.key, e.currentTarget), children: [Icon ? (0, jsx_runtime_1.jsx)(Icon, { size: 14, strokeWidth: 1.75 }) : null, (0, jsx_runtime_1.jsx)("span", { className: "recordstable-th-label", children: col.label }), (0, jsx_runtime_1.jsx)("span", { className: "recordstable-sort", children: (0, jsx_runtime_1.jsx)("svg", { width: "12", height: "12", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "1.5", "aria-hidden": "true", children: (0, jsx_runtime_1.jsx)("path", { d: "m4 6 4 4 4-4" }) }) })] })), openMenu === `col:${col.key}` &&
986
986
  rootRef.current &&
987
- (0, react_dom_1.createPortal)((0, jsx_runtime_1.jsxs)("div", { ref: menuRef, className: "recordstable-menu recordstable-menu--fixed recordstable-menu--hdr", role: "menu", "aria-label": `${col.label} column`, style: menuPos ?? undefined, onKeyDown: onMenuKeyDown, children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => sortBy(col.key, "asc"), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowUp, { size: 15, strokeWidth: 1.75 }), t.sortAscending] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => sortBy(col.key, "desc"), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowDown, { size: 15, strokeWidth: 1.75 }), t.sortDescending] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "recordstable-menu-item", role: "menuitem", tabIndex: -1, disabled: i <= 1, onClick: () => moveColumn(col.key, -1), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowLeft, { size: 15, strokeWidth: 1.75 }), t.moveLeft] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "recordstable-menu-item", role: "menuitem", tabIndex: -1, disabled: i === 0 || i >= cols.length - 1, onClick: () => moveColumn(col.key, 1), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowRight, { size: 15, strokeWidth: 1.75 }), t.moveRight] }), (0, jsx_runtime_1.jsx)("div", { className: "recordstable-menu-sep" }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => {
987
+ (0, react_dom_1.createPortal)((0, jsx_runtime_1.jsxs)("div", { ref: menuRef, className: "recordstable-menu recordstable-menu--fixed recordstable-menu--hdr", role: "menu", "aria-label": `${col.label} column`, style: menuPos ?? undefined, onKeyDown: onMenuKeyDown, children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => sortBy(col.key, "asc"), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowUp, { size: 15, strokeWidth: 1.75 }), t.sortAscending] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => sortBy(col.key, "desc"), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowDown, { size: 15, strokeWidth: 1.75 }), t.sortDescending] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "recordstable-menu-item", role: "menuitem", tabIndex: -1, disabled: i <= 1, onClick: () => moveColumn(col.key, -1), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowLeft, { "data-rtl-flip": "", size: 15, strokeWidth: 1.75 }), t.moveLeft] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "recordstable-menu-item", role: "menuitem", tabIndex: -1, disabled: i === 0 || i >= cols.length - 1, onClick: () => moveColumn(col.key, 1), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowRight, { "data-rtl-flip": "", size: 15, strokeWidth: 1.75 }), t.moveRight] }), (0, jsx_runtime_1.jsx)("div", { className: "recordstable-menu-sep" }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => {
988
988
  setRenamingCol(col.key);
989
989
  setOpenMenu(null);
990
990
  }, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Pencil, { size: 15, strokeWidth: 1.75 }), t.editColumnLabel] }), cols.length > 1 && ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => hideColumn(col.key), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.EyeOff, { size: 15, strokeWidth: 1.75 }), t.hideFromView] }))] }), rootRef.current), (0, jsx_runtime_1.jsx)("span", { className: "recordstable-resize", "aria-hidden": "true", onPointerDown: (e) => {
@@ -1025,7 +1025,7 @@ function RecordsTable({ labels, columns: columnsProp = exports.defaultColumns, r
1025
1025
  : undefined, avatarTone: avatarTone })), i === 0 && onOpenRecord && !isEditing && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "recordstable-open", "aria-label": `Open ${cellText(col, row)}`, onClick: (e) => {
1026
1026
  e.stopPropagation();
1027
1027
  onOpenRecord(row);
1028
- }, children: (0, jsx_runtime_1.jsx)(lucide_react_1.ArrowUpRight, { size: 15, strokeWidth: 1.75 }) }))] }, col.key));
1028
+ }, children: (0, jsx_runtime_1.jsx)(lucide_react_1.ArrowUpRight, { "data-rtl-flip": "", size: 15, strokeWidth: 1.75 }) }))] }, col.key));
1029
1029
  }), (0, jsx_runtime_1.jsx)("td", { className: "recordstable-td recordstable-addcol" })] }, row.id))) }), (0, jsx_runtime_1.jsx)("tfoot", { children: (0, jsx_runtime_1.jsxs)("tr", { children: [(0, jsx_runtime_1.jsx)("td", { className: "recordstable-sum recordstable-col-check" }), cols.map((col, i) => {
1030
1030
  const a = agg[col.key] ?? "none";
1031
1031
  const res = summarize(col, a, visibleRows);
@@ -145,6 +145,8 @@ ${(0, tabletoolbar_chrome_1.toolbarChromeBody)("recordstable")}
145
145
  & .recordstable-bulk-sep { width: 1px; height: 20px; margin: 0 ${tokens_1.contract.space["0.5"]}; background: ${tokens_1.contract.borderSubtle}; }
146
146
  & .recordstable-bulk-close { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; padding: 0; border: none; border-radius: ${tokens_1.contract.radius.md}; background: transparent; color: ${tokens_1.contract.fgMuted}; cursor: pointer; }
147
147
  & .recordstable-bulk-close:hover { background: ${tokens_1.contract.bgRaised}; color: ${tokens_1.contract.fgPrimary}; }
148
+
149
+ ${core_1.rtlFlipCss}
148
150
  `,
149
151
  variants: {},
150
152
  defaultVariants: {},
@@ -54,6 +54,6 @@ function SettingsShell({ title = "Settings", onBack, groups, activeItem, onSelec
54
54
  }))
55
55
  .filter((g) => g.items.length > 0)
56
56
  : groups;
57
- return ((0, jsx_runtime_1.jsxs)("div", { className: (0, settingsshell_recipe_1.settingsshellRecipe)({ className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("nav", { className: "settingsshell-nav", "aria-label": navLabel, children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "settingsshell-back", onClick: onBack, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ChevronLeft, { size: 16, strokeWidth: 2 }), title] }), searchable && ((0, jsx_runtime_1.jsxs)("div", { className: "settingsshell-search", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Search, { size: 15, strokeWidth: 1.75 }), (0, jsx_runtime_1.jsx)("input", { type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, value: query, onChange: (e) => setQuery(e.target.value) })] })), (0, jsx_runtime_1.jsx)("div", { className: "settingsshell-groups", children: filtered.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "settingsshell-empty", children: emptyLabel })) : (filtered.map((g, gi) => ((0, jsx_runtime_1.jsxs)("div", { className: "settingsshell-group", children: [g.label != null && ((0, jsx_runtime_1.jsx)("div", { className: "settingsshell-group-label", children: g.label })), g.items.map((it) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "settingsshell-item", "aria-current": it.id === activeItem ? "page" : undefined, "data-active": it.id === activeItem ? "" : undefined, onClick: () => onSelect(it.id), children: [it.icon, (0, jsx_runtime_1.jsx)("span", { className: "settingsshell-item-label", children: it.label })] }, it.id)))] }, gi)))) })] }), (0, jsx_runtime_1.jsx)("div", { className: "settingsshell-content", children: children })] }));
57
+ return ((0, jsx_runtime_1.jsxs)("div", { className: (0, settingsshell_recipe_1.settingsshellRecipe)({ className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("nav", { className: "settingsshell-nav", "aria-label": navLabel, children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "settingsshell-back", onClick: onBack, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ChevronLeft, { "data-rtl-flip": "", size: 16, strokeWidth: 2 }), title] }), searchable && ((0, jsx_runtime_1.jsxs)("div", { className: "settingsshell-search", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Search, { size: 15, strokeWidth: 1.75 }), (0, jsx_runtime_1.jsx)("input", { type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, value: query, onChange: (e) => setQuery(e.target.value) })] })), (0, jsx_runtime_1.jsx)("div", { className: "settingsshell-groups", children: filtered.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "settingsshell-empty", children: emptyLabel })) : (filtered.map((g, gi) => ((0, jsx_runtime_1.jsxs)("div", { className: "settingsshell-group", children: [g.label != null && ((0, jsx_runtime_1.jsx)("div", { className: "settingsshell-group-label", children: g.label })), g.items.map((it) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "settingsshell-item", "aria-current": it.id === activeItem ? "page" : undefined, "data-active": it.id === activeItem ? "" : undefined, onClick: () => onSelect(it.id), children: [it.icon, (0, jsx_runtime_1.jsx)("span", { className: "settingsshell-item-label", children: it.label })] }, it.id)))] }, gi)))) })] }), (0, jsx_runtime_1.jsx)("div", { className: "settingsshell-content", children: children })] }));
58
58
  }
59
59
  SettingsShell.displayName = "SettingsShell";
@@ -37,7 +37,9 @@ const config = (0, core_1.defineRecipe)({
37
37
  & .settingsshell-item[data-active] svg { color: ${tokens_1.contract.fgPrimary}; }
38
38
  & .settingsshell-empty { padding: ${tokens_1.contract.space["3"]} ${tokens_1.contract.space["2"]}; font-size: ${tokens_1.contract.text.md}; color: ${tokens_1.contract.fgMuted}; }
39
39
 
40
- & .settingsshell-content { flex: 1; min-width: 0; min-height: 0; overflow-y: auto; padding: ${tokens_1.contract.space["6"]} ${tokens_1.contract.space["8"]}; }`,
40
+ & .settingsshell-content { flex: 1; min-width: 0; min-height: 0; overflow-y: auto; padding: ${tokens_1.contract.space["6"]} ${tokens_1.contract.space["8"]}; }
41
+ ${core_1.rtlFlipCss}
42
+ `,
41
43
  variants: {},
42
44
  defaultVariants: {},
43
45
  compoundVariants: [],
@@ -7,7 +7,7 @@ import { breadcrumbRecipe } from "./breadcrumb.recipe.js";
7
7
  * marked `aria-current="page"`; earlier items render as links or buttons.
8
8
  */
9
9
  export function Breadcrumb({ items, separator, "aria-label": ariaLabel = "Breadcrumb", className, ...rest }) {
10
- const sep = separator ?? (_jsx(ChevronRight, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }));
10
+ const sep = separator ?? (_jsx(ChevronRight, { "data-rtl-flip": "", size: 14, strokeWidth: 1.75, "aria-hidden": "true" }));
11
11
  return (_jsx("nav", { "aria-label": ariaLabel, className: breadcrumbRecipe({ className }), ...rest, children: items.map((item, i) => {
12
12
  const last = i === items.length - 1;
13
13
  const content = (_jsxs(_Fragment, { children: [item.icon, item.label] }));
@@ -1,4 +1,4 @@
1
- import { defineRecipe, resolveRecipe } from "../../core/index.js";
1
+ import { defineRecipe, resolveRecipe, rtlFlipCss } from "../../core/index.js";
2
2
  import { contract as c } from "../../tokens/index.js";
3
3
  // A breadcrumb trail — Attio's record-page nav ("Companies / Google").
4
4
  const config = defineRecipe({
@@ -9,7 +9,9 @@ const config = defineRecipe({
9
9
  & a.breadcrumb-item:hover, & button.breadcrumb-item:hover { color: ${c.fgPrimary}; background: color-mix(in srgb, ${c.fgPrimary} 5%, transparent); }
10
10
  & button.breadcrumb-item { border: none; background: none; font: inherit; cursor: pointer; }
11
11
  & .breadcrumb-item[aria-current] { color: ${c.fgPrimary}; font-weight: ${c.weight.medium}; }
12
- & .breadcrumb-sep { flex-shrink: 0; color: color-mix(in srgb, ${c.fgPrimary} 28%, transparent); }`,
12
+ & .breadcrumb-sep { flex-shrink: 0; color: color-mix(in srgb, ${c.fgPrimary} 28%, transparent); }
13
+ ${rtlFlipCss}
14
+ `,
13
15
  variants: {},
14
16
  defaultVariants: {},
15
17
  compoundVariants: [],
@@ -0,0 +1,2 @@
1
+ export * from "./slider.js";
2
+ export * from "./slider.recipe.js";
@@ -0,0 +1,19 @@
1
+ "use client";
2
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { SliderRange, SliderRoot, SliderThumb, useSliderContext, } from "@seed-design/react-slider";
4
+ import { sliderRecipe } from "./slider.recipe.js";
5
+ /** Reads the thumb count from the engine rather than re-deriving it from props. */
6
+ function Thumbs() {
7
+ const { values } = useSliderContext();
8
+ return (_jsx(_Fragment, { children: values.map((_, i) => (_jsx(SliderThumb
9
+ // biome-ignore lint/suspicious/noArrayIndexKey: thumbs are positional — the index IS the identity the engine addresses them by
10
+ , { thumbIndex: i, className: "slider-thumb" }, i))) }));
11
+ }
12
+ /**
13
+ * A range input, driven by seed-design's headless engine so the keyboard model,
14
+ * the pointer maths and multi-thumb constraints are theirs by construction.
15
+ * Pass one value for a simple slider, two for a range.
16
+ */
17
+ export function Slider({ className, ...props }) {
18
+ return (_jsx(SliderRoot, { ...props, className: sliderRecipe({ className }), children: _jsxs("span", { className: "slider-control", children: [_jsx("span", { className: "slider-track", children: _jsx(SliderRange, { className: "slider-range" }) }), _jsx(Thumbs, {})] }) }));
19
+ }
@@ -0,0 +1,67 @@
1
+ import { defineRecipe, resolveRecipe } from "../../core/index.js";
2
+ import { contract as c, elevation as e } from "../../tokens/index.js";
3
+ // seed-design's slider, shape for shape, in crisp's colours.
4
+ //
5
+ // Attio ships no slider anywhere — Records, Reports, Settings and the record
6
+ // pages contain no `input[type=range]` or `[role=slider]` — so there is nothing
7
+ // of Attio's to measure. The GEOMETRY is therefore seed's own, read from
8
+ // `@seed-design/css/recipes/slider.css`: a 26px control row, a 4px track on
9
+ // `radius.full`, a 20px thumb whose visible disc is an `::after` that scales to
10
+ // 1.2 while dragging, and a 2px focus ring offset 2px.
11
+ //
12
+ // Only the COLOUR roles are re-pointed at crisp's tokens:
13
+ // seed gray-400 track -> bgNeutralWeak (crisp's weak neutral fill)
14
+ // seed fg-neutral range -> bgBrandSolid (crisp fills with brand, as Progress does)
15
+ // seed bg-neutral-inverted -> #fff + elevation.field (crisp's control disc, as Switch does)
16
+ //
17
+ // Position comes from the engine's own `--range-start`/`--range-end` and
18
+ // `--thumb-position`/`--thumb-offset`, switched on the `data-dir` it stamps —
19
+ // the same mechanism seed uses, rather than CSS direction inheritance.
20
+ const config = defineRecipe({
21
+ name: "slider",
22
+ base: `
23
+ display: flex; flex-direction: column; align-items: stretch;
24
+ width: 100%; min-width: 0; gap: ${c.space["0.5"]};
25
+ font-family: ${c.font.sans}; user-select: none; touch-action: none;
26
+
27
+ & .slider-control { position: relative; height: 26px; display: flex; align-items: center; }
28
+
29
+ & .slider-track {
30
+ position: relative; flex-grow: 1; height: ${c.space["1"]};
31
+ border-radius: ${c.radius.full}; background: ${c.bgNeutralWeak};
32
+ overflow: hidden;
33
+ }
34
+
35
+ & .slider-range { position: absolute; height: 100%; background: ${c.bgBrandSolid}; }
36
+ & .slider-range[data-dir="ltr"] { left: var(--range-start); right: var(--range-end); }
37
+ & .slider-range[data-dir="rtl"] { right: var(--range-start); left: var(--range-end); }
38
+
39
+ & .slider-thumb {
40
+ position: absolute; top: 50%;
41
+ width: ${c.space["5"]}; height: ${c.space["5"]};
42
+ transform: translate(-50%, -50%);
43
+ border: none; background: none; padding: 0;
44
+ }
45
+ & .slider-thumb[data-dir="ltr"] { left: calc(var(--thumb-position) * 1% + var(--thumb-offset)); }
46
+ & .slider-thumb[data-dir="rtl"] { right: calc(var(--thumb-position) * 1% + var(--thumb-offset)); }
47
+ & .slider-thumb::after {
48
+ content: ""; position: absolute; inset: 0;
49
+ border-radius: ${c.radius.full}; background: #fff; box-shadow: ${e.field};
50
+ outline: 2px solid transparent; outline-offset: 2px;
51
+ transition: transform .2s ease, outline-color .2s ease;
52
+ cursor: grab;
53
+ }
54
+ & .slider-thumb[data-thumb-dragging]::after { transform: scale(1.2); cursor: grabbing; }
55
+ & .slider-thumb:focus { outline: none; }
56
+ & .slider-thumb:focus-visible::after { outline-color: ${c.strokeFocusRing}; }
57
+
58
+ &[data-disabled] { cursor: not-allowed; }
59
+ &[data-disabled] .slider-range { background: ${c.fgDisabled}; }
60
+ &[data-disabled] .slider-thumb::after { cursor: not-allowed; }
61
+ `,
62
+ variants: {},
63
+ defaultVariants: {},
64
+ compoundVariants: [],
65
+ });
66
+ export const sliderRecipe = Object.assign(resolveRecipe(config), { config });
67
+ export default config;
@@ -2,3 +2,4 @@ export { cx } from "./cx.js";
2
2
  export { defineRecipe } from "./define.js";
3
3
  export { Primitive } from "./primitive.js";
4
4
  export { resolveRecipe } from "./resolve.js";
5
+ export * from "./rtl.js";
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Mirroring for direction-bearing icons.
3
+ *
4
+ * A chevron or arrow that means "back", "next", "toward the start" or "open"
5
+ * points the wrong way under RTL, and unlike spacing or alignment it cannot be
6
+ * fixed with a logical property: these are `lucide-react` nodes, static SVG
7
+ * paths with no direction of their own.
8
+ *
9
+ * Mark the icons that carry a direction with `data-rtl-flip` and include this
10
+ * snippet in the recipe. Icons whose meaning is NOT directional — a downward
11
+ * disclosure chevron, a search glass, a check — must be left unmarked: flipping
12
+ * them is a regression, not a fix.
13
+ */
14
+ export const rtlFlipCss = `[dir="rtl"] & [data-rtl-flip] { transform: scaleX(-1); }`;
package/dist/esm/index.js CHANGED
@@ -37,6 +37,7 @@ export * from "./components/resizablepanel/index.js";
37
37
  export * from "./components/segmented/index.js";
38
38
  export * from "./components/select/index.js";
39
39
  export * from "./components/skeleton/index.js";
40
+ export * from "./components/slider/index.js";
40
41
  export * from "./components/spinner/index.js";
41
42
  export * from "./components/stagebar/index.js";
42
43
  export * from "./components/statcard/index.js";
@@ -63,6 +63,6 @@ export function AppShell({ sidebar, children, width: widthProp, defaultWidth = 2
63
63
  return (_jsxs("div", { className: appshellRecipe({ className }), ...rest, children: [_jsxs("div", { className: "appshell-side", "data-collapsed": collapsed ? "" : undefined, style: {
64
64
  width: collapsed ? 0 : width,
65
65
  transition: dragging ? "none" : "width .14s ease",
66
- }, children: [_jsx("div", { className: "appshell-side-inner", style: { width }, children: sidebar(toggleCollapse) }), !collapsed && (_jsx("div", { className: "appshell-resize", "data-dragging": dragging ? "" : undefined, "aria-hidden": "true", onPointerDown: startResize }))] }), _jsxs("div", { className: "appshell-main", children: [collapsed && (_jsx("button", { type: "button", className: "appshell-expand", "aria-label": expandLabel, onClick: () => setCollapsed(false), children: _jsx(PanelLeft, { size: 15, strokeWidth: 1.75 }) })), children] })] }));
66
+ }, children: [_jsx("div", { className: "appshell-side-inner", style: { width }, children: sidebar(toggleCollapse) }), !collapsed && (_jsx("div", { className: "appshell-resize", "data-dragging": dragging ? "" : undefined, "aria-hidden": "true", onPointerDown: startResize }))] }), _jsxs("div", { className: "appshell-main", children: [collapsed && (_jsx("button", { type: "button", className: "appshell-expand", "aria-label": expandLabel, onClick: () => setCollapsed(false), children: _jsx(PanelLeft, { "data-rtl-flip": "", size: 15, strokeWidth: 1.75 }) })), children] })] }));
67
67
  }
68
68
  AppShell.displayName = "AppShell";
@@ -1,4 +1,4 @@
1
- import { defineRecipe, resolveRecipe } from "../../core/index.js";
1
+ import { defineRecipe, resolveRecipe, rtlFlipCss } from "../../core/index.js";
2
2
  import { contract as c } from "../../tokens/index.js";
3
3
  // App-shell layout: a collapsible + resizable sidebar pane next to a main pane,
4
4
  // mirroring how Attio's window works. The sidebar pane clips (never squishes)
@@ -15,6 +15,8 @@ const config = defineRecipe({
15
15
  & .appshell-main { position: relative; flex: 1; min-width: 0; display: flex; flex-direction: column; overflow: hidden; }
16
16
  & .appshell-expand { position: absolute; top: ${c.space["2"]}; inset-inline-start: ${c.space["2"]}; z-index: 6; display: inline-flex; align-items: center; justify-content: center; width: ${c.size.row}; height: ${c.size.row}; padding: 0; border: none; border-radius: ${c.radius.md}; box-shadow: inset 0 0 0 1px ${c.borderSubtle}; background: ${c.bgSurface}; color: ${c.fgMuted}; font-family: inherit; cursor: pointer; transition: background-color .12s; }
17
17
  & .appshell-expand:hover { background: ${c.bgRaised}; color: ${c.fgPrimary}; }
18
+
19
+ ${rtlFlipCss}
18
20
  `,
19
21
  variants: {},
20
22
  defaultVariants: {},
@@ -946,7 +946,7 @@ export function RecordsTable({ labels, columns: columnsProp = defaultColumns, ro
946
946
  }
947
947
  : undefined, onDragEnd: () => setDragCol(null), onClick: (e) => openColumnMenu(col.key, e.currentTarget), children: [Icon ? _jsx(Icon, { size: 14, strokeWidth: 1.75 }) : null, _jsx("span", { className: "recordstable-th-label", children: col.label }), _jsx("span", { className: "recordstable-sort", children: _jsx("svg", { width: "12", height: "12", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "1.5", "aria-hidden": "true", children: _jsx("path", { d: "m4 6 4 4 4-4" }) }) })] })), openMenu === `col:${col.key}` &&
948
948
  rootRef.current &&
949
- createPortal(_jsxs("div", { ref: menuRef, className: "recordstable-menu recordstable-menu--fixed recordstable-menu--hdr", role: "menu", "aria-label": `${col.label} column`, style: menuPos ?? undefined, onKeyDown: onMenuKeyDown, children: [_jsxs("button", { type: "button", className: "recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => sortBy(col.key, "asc"), children: [_jsx(ArrowUp, { size: 15, strokeWidth: 1.75 }), t.sortAscending] }), _jsxs("button", { type: "button", className: "recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => sortBy(col.key, "desc"), children: [_jsx(ArrowDown, { size: 15, strokeWidth: 1.75 }), t.sortDescending] }), _jsxs("button", { type: "button", className: "recordstable-menu-item", role: "menuitem", tabIndex: -1, disabled: i <= 1, onClick: () => moveColumn(col.key, -1), children: [_jsx(ArrowLeft, { size: 15, strokeWidth: 1.75 }), t.moveLeft] }), _jsxs("button", { type: "button", className: "recordstable-menu-item", role: "menuitem", tabIndex: -1, disabled: i === 0 || i >= cols.length - 1, onClick: () => moveColumn(col.key, 1), children: [_jsx(ArrowRight, { size: 15, strokeWidth: 1.75 }), t.moveRight] }), _jsx("div", { className: "recordstable-menu-sep" }), _jsxs("button", { type: "button", className: "recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => {
949
+ createPortal(_jsxs("div", { ref: menuRef, className: "recordstable-menu recordstable-menu--fixed recordstable-menu--hdr", role: "menu", "aria-label": `${col.label} column`, style: menuPos ?? undefined, onKeyDown: onMenuKeyDown, children: [_jsxs("button", { type: "button", className: "recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => sortBy(col.key, "asc"), children: [_jsx(ArrowUp, { size: 15, strokeWidth: 1.75 }), t.sortAscending] }), _jsxs("button", { type: "button", className: "recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => sortBy(col.key, "desc"), children: [_jsx(ArrowDown, { size: 15, strokeWidth: 1.75 }), t.sortDescending] }), _jsxs("button", { type: "button", className: "recordstable-menu-item", role: "menuitem", tabIndex: -1, disabled: i <= 1, onClick: () => moveColumn(col.key, -1), children: [_jsx(ArrowLeft, { "data-rtl-flip": "", size: 15, strokeWidth: 1.75 }), t.moveLeft] }), _jsxs("button", { type: "button", className: "recordstable-menu-item", role: "menuitem", tabIndex: -1, disabled: i === 0 || i >= cols.length - 1, onClick: () => moveColumn(col.key, 1), children: [_jsx(ArrowRight, { "data-rtl-flip": "", size: 15, strokeWidth: 1.75 }), t.moveRight] }), _jsx("div", { className: "recordstable-menu-sep" }), _jsxs("button", { type: "button", className: "recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => {
950
950
  setRenamingCol(col.key);
951
951
  setOpenMenu(null);
952
952
  }, children: [_jsx(Pencil, { size: 15, strokeWidth: 1.75 }), t.editColumnLabel] }), cols.length > 1 && (_jsxs("button", { type: "button", className: "recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => hideColumn(col.key), children: [_jsx(EyeOff, { size: 15, strokeWidth: 1.75 }), t.hideFromView] }))] }), rootRef.current), _jsx("span", { className: "recordstable-resize", "aria-hidden": "true", onPointerDown: (e) => {
@@ -987,7 +987,7 @@ export function RecordsTable({ labels, columns: columnsProp = defaultColumns, ro
987
987
  : undefined, avatarTone: avatarTone })), i === 0 && onOpenRecord && !isEditing && (_jsx("button", { type: "button", className: "recordstable-open", "aria-label": `Open ${cellText(col, row)}`, onClick: (e) => {
988
988
  e.stopPropagation();
989
989
  onOpenRecord(row);
990
- }, children: _jsx(ArrowUpRight, { size: 15, strokeWidth: 1.75 }) }))] }, col.key));
990
+ }, children: _jsx(ArrowUpRight, { "data-rtl-flip": "", size: 15, strokeWidth: 1.75 }) }))] }, col.key));
991
991
  }), _jsx("td", { className: "recordstable-td recordstable-addcol" })] }, row.id))) }), _jsx("tfoot", { children: _jsxs("tr", { children: [_jsx("td", { className: "recordstable-sum recordstable-col-check" }), cols.map((col, i) => {
992
992
  const a = agg[col.key] ?? "none";
993
993
  const res = summarize(col, a, visibleRows);
@@ -1,5 +1,5 @@
1
1
  import { toolbarChromeBody, toolbarChromeHead, } from "../../components/tabletoolbar/tabletoolbar.chrome.js";
2
- import { defineRecipe, resolveRecipe } from "../../core/index.js";
2
+ import { defineRecipe, resolveRecipe, rtlFlipCss } from "../../core/index.js";
3
3
  import { contract as c } from "../../tokens/index.js";
4
4
  import { recordTagCss } from "./recordstable.tag.js";
5
5
  // Attio's record grid, measured: 36px rows, 1px borderSubtle (#eeeff1) grid
@@ -142,6 +142,8 @@ ${toolbarChromeBody("recordstable")}
142
142
  & .recordstable-bulk-sep { width: 1px; height: 20px; margin: 0 ${c.space["0.5"]}; background: ${c.borderSubtle}; }
143
143
  & .recordstable-bulk-close { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; padding: 0; border: none; border-radius: ${c.radius.md}; background: transparent; color: ${c.fgMuted}; cursor: pointer; }
144
144
  & .recordstable-bulk-close:hover { background: ${c.bgRaised}; color: ${c.fgPrimary}; }
145
+
146
+ ${rtlFlipCss}
145
147
  `,
146
148
  variants: {},
147
149
  defaultVariants: {},
@@ -18,6 +18,6 @@ export function SettingsShell({ title = "Settings", onBack, groups, activeItem,
18
18
  }))
19
19
  .filter((g) => g.items.length > 0)
20
20
  : groups;
21
- return (_jsxs("div", { className: settingsshellRecipe({ className }), ...rest, children: [_jsxs("nav", { className: "settingsshell-nav", "aria-label": navLabel, children: [_jsxs("button", { type: "button", className: "settingsshell-back", onClick: onBack, children: [_jsx(ChevronLeft, { size: 16, strokeWidth: 2 }), title] }), searchable && (_jsxs("div", { className: "settingsshell-search", children: [_jsx(Search, { size: 15, strokeWidth: 1.75 }), _jsx("input", { type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, value: query, onChange: (e) => setQuery(e.target.value) })] })), _jsx("div", { className: "settingsshell-groups", children: filtered.length === 0 ? (_jsx("div", { className: "settingsshell-empty", children: emptyLabel })) : (filtered.map((g, gi) => (_jsxs("div", { className: "settingsshell-group", children: [g.label != null && (_jsx("div", { className: "settingsshell-group-label", children: g.label })), g.items.map((it) => (_jsxs("button", { type: "button", className: "settingsshell-item", "aria-current": it.id === activeItem ? "page" : undefined, "data-active": it.id === activeItem ? "" : undefined, onClick: () => onSelect(it.id), children: [it.icon, _jsx("span", { className: "settingsshell-item-label", children: it.label })] }, it.id)))] }, gi)))) })] }), _jsx("div", { className: "settingsshell-content", children: children })] }));
21
+ return (_jsxs("div", { className: settingsshellRecipe({ className }), ...rest, children: [_jsxs("nav", { className: "settingsshell-nav", "aria-label": navLabel, children: [_jsxs("button", { type: "button", className: "settingsshell-back", onClick: onBack, children: [_jsx(ChevronLeft, { "data-rtl-flip": "", size: 16, strokeWidth: 2 }), title] }), searchable && (_jsxs("div", { className: "settingsshell-search", children: [_jsx(Search, { size: 15, strokeWidth: 1.75 }), _jsx("input", { type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, value: query, onChange: (e) => setQuery(e.target.value) })] })), _jsx("div", { className: "settingsshell-groups", children: filtered.length === 0 ? (_jsx("div", { className: "settingsshell-empty", children: emptyLabel })) : (filtered.map((g, gi) => (_jsxs("div", { className: "settingsshell-group", children: [g.label != null && (_jsx("div", { className: "settingsshell-group-label", children: g.label })), g.items.map((it) => (_jsxs("button", { type: "button", className: "settingsshell-item", "aria-current": it.id === activeItem ? "page" : undefined, "data-active": it.id === activeItem ? "" : undefined, onClick: () => onSelect(it.id), children: [it.icon, _jsx("span", { className: "settingsshell-item-label", children: it.label })] }, it.id)))] }, gi)))) })] }), _jsx("div", { className: "settingsshell-content", children: children })] }));
22
22
  }
23
23
  SettingsShell.displayName = "SettingsShell";
@@ -1,4 +1,4 @@
1
- import { defineRecipe, resolveRecipe } from "../../core/index.js";
1
+ import { defineRecipe, resolveRecipe, rtlFlipCss } from "../../core/index.js";
2
2
  import { contract as c } from "../../tokens/index.js";
3
3
  // The Settings layout — measured from Attio's Settings: a 256px left nav with a
4
4
  // back header, a search field, and grouped nav items (active row raised), beside
@@ -34,7 +34,9 @@ const config = defineRecipe({
34
34
  & .settingsshell-item[data-active] svg { color: ${c.fgPrimary}; }
35
35
  & .settingsshell-empty { padding: ${c.space["3"]} ${c.space["2"]}; font-size: ${c.text.md}; color: ${c.fgMuted}; }
36
36
 
37
- & .settingsshell-content { flex: 1; min-width: 0; min-height: 0; overflow-y: auto; padding: ${c.space["6"]} ${c.space["8"]}; }`,
37
+ & .settingsshell-content { flex: 1; min-width: 0; min-height: 0; overflow-y: auto; padding: ${c.space["6"]} ${c.space["8"]}; }
38
+ ${rtlFlipCss}
39
+ `,
38
40
  variants: {},
39
41
  defaultVariants: {},
40
42
  compoundVariants: [],
@@ -198,6 +198,8 @@
198
198
  & .recordstable-bulk-close { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; padding: 0; border: none; border-radius: var(--radius-md); background: transparent; color: var(--fg-muted); cursor: pointer; }
199
199
  & .recordstable-bulk-close:hover { background: var(--bg-raised); color: var(--fg-primary); }
200
200
 
201
+ [dir="rtl"] & [data-rtl-flip] { transform: scaleX(-1); }
202
+
201
203
  }
202
204
 
203
205
  @media (prefers-reduced-motion: reduce) {
@@ -29,6 +29,8 @@
29
29
  & .settingsshell-empty { padding: var(--space-3) var(--space-2); font-size: var(--text-md); color: var(--fg-muted); }
30
30
 
31
31
  & .settingsshell-content { flex: 1; min-width: 0; min-height: 0; overflow-y: auto; padding: var(--space-6) var(--space-8); }
32
+ [dir="rtl"] & [data-rtl-flip] { transform: scaleX(-1); }
33
+
32
34
  }
33
35
 
34
36
  @media (prefers-reduced-motion: reduce) {
@@ -0,0 +1,50 @@
1
+ .slider {
2
+
3
+ display: flex; flex-direction: column; align-items: stretch;
4
+ width: 100%; min-width: 0; gap: var(--space-0-5);
5
+ font-family: var(--font-sans); user-select: none; touch-action: none;
6
+
7
+ & .slider-control { position: relative; height: 26px; display: flex; align-items: center; }
8
+
9
+ & .slider-track {
10
+ position: relative; flex-grow: 1; height: var(--space-1);
11
+ border-radius: var(--radius-full); background: var(--bg-neutral-weak);
12
+ overflow: hidden;
13
+ }
14
+
15
+ & .slider-range { position: absolute; height: 100%; background: var(--bg-brand-solid); }
16
+ & .slider-range[data-dir="ltr"] { left: var(--range-start); right: var(--range-end); }
17
+ & .slider-range[data-dir="rtl"] { right: var(--range-start); left: var(--range-end); }
18
+
19
+ & .slider-thumb {
20
+ position: absolute; top: 50%;
21
+ width: var(--space-5); height: var(--space-5);
22
+ transform: translate(-50%, -50%);
23
+ border: none; background: none; padding: 0;
24
+ }
25
+ & .slider-thumb[data-dir="ltr"] { left: calc(var(--thumb-position) * 1% + var(--thumb-offset)); }
26
+ & .slider-thumb[data-dir="rtl"] { right: calc(var(--thumb-position) * 1% + var(--thumb-offset)); }
27
+ & .slider-thumb::after {
28
+ content: ""; position: absolute; inset: 0;
29
+ border-radius: var(--radius-full); background: #fff; box-shadow: inset 0 0 0 1px var(--ring-neutral), 0 0 2px rgba(28,40,64,0.18), 0 1px 3px rgba(0,0,0,0.04);
30
+ outline: 2px solid transparent; outline-offset: 2px;
31
+ transition: transform .2s ease, outline-color .2s ease;
32
+ cursor: grab;
33
+ }
34
+ & .slider-thumb[data-thumb-dragging]::after { transform: scale(1.2); cursor: grabbing; }
35
+ & .slider-thumb:focus { outline: none; }
36
+ & .slider-thumb:focus-visible::after { outline-color: var(--stroke-focus-ring); }
37
+
38
+ &[data-disabled] { cursor: not-allowed; }
39
+ &[data-disabled] .slider-range { background: var(--fg-disabled); }
40
+ &[data-disabled] .slider-thumb::after { cursor: not-allowed; }
41
+
42
+ }
43
+
44
+ @media (prefers-reduced-motion: reduce) {
45
+ .slider, .slider * {
46
+ animation-duration: 0.01ms !important;
47
+ animation-iteration-count: 1 !important;
48
+ transition-duration: 0.01ms !important;
49
+ }
50
+ }
@@ -0,0 +1,7 @@
1
+ import type { RecipeFn } from "../core/types";
2
+
3
+ export interface SliderVariants {
4
+
5
+ }
6
+
7
+ export declare const sliderRecipe: RecipeFn<{ }>;
package/dist/styles.css CHANGED
@@ -272,6 +272,8 @@
272
272
  & button.breadcrumb-item { border: none; background: none; font: inherit; cursor: pointer; }
273
273
  & .breadcrumb-item[aria-current] { color: var(--fg-primary); font-weight: var(--weight-medium); }
274
274
  & .breadcrumb-sep { flex-shrink: 0; color: color-mix(in srgb, var(--fg-primary) 28%, transparent); }
275
+ [dir="rtl"] & [data-rtl-flip] { transform: scaleX(-1); }
276
+
275
277
  }
276
278
 
277
279
  .bulkbar {
@@ -1363,6 +1365,57 @@
1363
1365
  }
1364
1366
  }
1365
1367
 
1368
+ .slider {
1369
+
1370
+ display: flex; flex-direction: column; align-items: stretch;
1371
+ width: 100%; min-width: 0; gap: var(--space-0-5);
1372
+ font-family: var(--font-sans); user-select: none; touch-action: none;
1373
+
1374
+ & .slider-control { position: relative; height: 26px; display: flex; align-items: center; }
1375
+
1376
+ & .slider-track {
1377
+ position: relative; flex-grow: 1; height: var(--space-1);
1378
+ border-radius: var(--radius-full); background: var(--bg-neutral-weak);
1379
+ overflow: hidden;
1380
+ }
1381
+
1382
+ & .slider-range { position: absolute; height: 100%; background: var(--bg-brand-solid); }
1383
+ & .slider-range[data-dir="ltr"] { left: var(--range-start); right: var(--range-end); }
1384
+ & .slider-range[data-dir="rtl"] { right: var(--range-start); left: var(--range-end); }
1385
+
1386
+ & .slider-thumb {
1387
+ position: absolute; top: 50%;
1388
+ width: var(--space-5); height: var(--space-5);
1389
+ transform: translate(-50%, -50%);
1390
+ border: none; background: none; padding: 0;
1391
+ }
1392
+ & .slider-thumb[data-dir="ltr"] { left: calc(var(--thumb-position) * 1% + var(--thumb-offset)); }
1393
+ & .slider-thumb[data-dir="rtl"] { right: calc(var(--thumb-position) * 1% + var(--thumb-offset)); }
1394
+ & .slider-thumb::after {
1395
+ content: ""; position: absolute; inset: 0;
1396
+ border-radius: var(--radius-full); background: #fff; box-shadow: inset 0 0 0 1px var(--ring-neutral), 0 0 2px rgba(28,40,64,0.18), 0 1px 3px rgba(0,0,0,0.04);
1397
+ outline: 2px solid transparent; outline-offset: 2px;
1398
+ transition: transform .2s ease, outline-color .2s ease;
1399
+ cursor: grab;
1400
+ }
1401
+ & .slider-thumb[data-thumb-dragging]::after { transform: scale(1.2); cursor: grabbing; }
1402
+ & .slider-thumb:focus { outline: none; }
1403
+ & .slider-thumb:focus-visible::after { outline-color: var(--stroke-focus-ring); }
1404
+
1405
+ &[data-disabled] { cursor: not-allowed; }
1406
+ &[data-disabled] .slider-range { background: var(--fg-disabled); }
1407
+ &[data-disabled] .slider-thumb::after { cursor: not-allowed; }
1408
+
1409
+ }
1410
+
1411
+ @media (prefers-reduced-motion: reduce) {
1412
+ .slider, .slider * {
1413
+ animation-duration: 0.01ms !important;
1414
+ animation-iteration-count: 1 !important;
1415
+ transition-duration: 0.01ms !important;
1416
+ }
1417
+ }
1418
+
1366
1419
  @keyframes crisp-spin { to { transform: rotate(360deg); } }
1367
1420
  .spinner {
1368
1421
  display: inline-block; box-sizing: border-box; width: 16px; height: 16px; border-radius: var(--radius-full); border: 2px solid color-mix(in srgb, var(--fg-primary) 15%, transparent); border-top-color: var(--fg-muted); animation: crisp-spin 0.6s linear infinite;
@@ -1732,6 +1785,8 @@
1732
1785
  & .appshell-expand { position: absolute; top: var(--space-2); inset-inline-start: var(--space-2); z-index: 6; display: inline-flex; align-items: center; justify-content: center; width: var(--size-row); height: var(--size-row); padding: 0; border: none; border-radius: var(--radius-md); box-shadow: inset 0 0 0 1px var(--border-subtle); background: var(--bg-surface); color: var(--fg-muted); font-family: inherit; cursor: pointer; transition: background-color .12s; }
1733
1786
  & .appshell-expand:hover { background: var(--bg-raised); color: var(--fg-primary); }
1734
1787
 
1788
+ [dir="rtl"] & [data-rtl-flip] { transform: scaleX(-1); }
1789
+
1735
1790
  }
1736
1791
 
1737
1792
  @media (prefers-reduced-motion: reduce) {
@@ -2449,6 +2504,8 @@
2449
2504
  & .recordstable-bulk-close { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; padding: 0; border: none; border-radius: var(--radius-md); background: transparent; color: var(--fg-muted); cursor: pointer; }
2450
2505
  & .recordstable-bulk-close:hover { background: var(--bg-raised); color: var(--fg-primary); }
2451
2506
 
2507
+ [dir="rtl"] & [data-rtl-flip] { transform: scaleX(-1); }
2508
+
2452
2509
  }
2453
2510
 
2454
2511
  @media (prefers-reduced-motion: reduce) {
@@ -2537,6 +2594,8 @@
2537
2594
  & .settingsshell-empty { padding: var(--space-3) var(--space-2); font-size: var(--text-md); color: var(--fg-muted); }
2538
2595
 
2539
2596
  & .settingsshell-content { flex: 1; min-width: 0; min-height: 0; overflow-y: auto; padding: var(--space-6) var(--space-8); }
2597
+ [dir="rtl"] & [data-rtl-flip] { transform: scaleX(-1); }
2598
+
2540
2599
  }
2541
2600
 
2542
2601
  @media (prefers-reduced-motion: reduce) {
@@ -0,0 +1,2 @@
1
+ export * from "./slider";
2
+ export * from "./slider.recipe";
@@ -0,0 +1,8 @@
1
+ import { type SliderRootProps } from "@seed-design/react-slider";
2
+ export type SliderProps = SliderRootProps;
3
+ /**
4
+ * A range input, driven by seed-design's headless engine so the keyboard model,
5
+ * the pointer maths and multi-thumb constraints are theirs by construction.
6
+ * Pass one value for a simple slider, two for a range.
7
+ */
8
+ export declare function Slider({ className, ...props }: SliderProps): import("react").JSX.Element;
@@ -0,0 +1,5 @@
1
+ declare const config: import("../..").RecipeConfig<{}>;
2
+ export declare const sliderRecipe: import("../..").RecipeFn<{}> & {
3
+ config: import("../..").RecipeConfig<{}>;
4
+ };
5
+ export default config;
@@ -2,4 +2,5 @@ export { cx } from "./cx";
2
2
  export { defineRecipe } from "./define";
3
3
  export { Primitive } from "./primitive";
4
4
  export { resolveRecipe } from "./resolve";
5
+ export * from "./rtl";
5
6
  export type * from "./types";
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Mirroring for direction-bearing icons.
3
+ *
4
+ * A chevron or arrow that means "back", "next", "toward the start" or "open"
5
+ * points the wrong way under RTL, and unlike spacing or alignment it cannot be
6
+ * fixed with a logical property: these are `lucide-react` nodes, static SVG
7
+ * paths with no direction of their own.
8
+ *
9
+ * Mark the icons that carry a direction with `data-rtl-flip` and include this
10
+ * snippet in the recipe. Icons whose meaning is NOT directional — a downward
11
+ * disclosure chevron, a search glass, a check — must be left unmarked: flipping
12
+ * them is a regression, not a fix.
13
+ */
14
+ export declare const rtlFlipCss = "[dir=\"rtl\"] & [data-rtl-flip] { transform: scaleX(-1); }";
@@ -33,6 +33,7 @@ export * from "./components/resizablepanel";
33
33
  export * from "./components/segmented";
34
34
  export * from "./components/select";
35
35
  export * from "./components/skeleton";
36
+ export * from "./components/slider";
36
37
  export * from "./components/spinner";
37
38
  export * from "./components/stagebar";
38
39
  export * from "./components/statcard";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crisp-ui-kit/crisp",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "A lean React design system, measured pixel-for-pixel from Attio.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -207,6 +207,11 @@
207
207
  "import": "./dist/esm/components/skeleton/index.js",
208
208
  "require": "./dist/cjs/components/skeleton/index.js"
209
209
  },
210
+ "./slider": {
211
+ "types": "./dist/types/components/slider/index.d.ts",
212
+ "import": "./dist/esm/components/slider/index.js",
213
+ "require": "./dist/cjs/components/slider/index.js"
214
+ },
210
215
  "./spinner": {
211
216
  "types": "./dist/types/components/spinner/index.d.ts",
212
217
  "import": "./dist/esm/components/spinner/index.js",
@@ -418,6 +423,7 @@
418
423
  "@seed-design/dom-utils": "^2.1.0",
419
424
  "@seed-design/react-date-picker": "1.2.0",
420
425
  "@seed-design/react-file-upload": "1.0.1",
426
+ "@seed-design/react-slider": "2.0.1",
421
427
  "@seed-design/react-time-picker": "1.0.0",
422
428
  "@zag-js/combobox": "^1.43.3",
423
429
  "@zag-js/dialog": "^1.43.3",