@crisp-ui-kit/crisp 0.32.1 → 0.33.1

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 (124) hide show
  1. package/README.md +33 -1
  2. package/dist/appshell.css +1 -4
  3. package/dist/appshell.layered.css +1 -4
  4. package/dist/authshell.css +4 -19
  5. package/dist/authshell.layered.css +4 -19
  6. package/dist/avatar.css +2 -1
  7. package/dist/avatar.layered.css +2 -1
  8. package/dist/board.css +1 -4
  9. package/dist/board.layered.css +1 -4
  10. package/dist/breadcrumb.css +0 -1
  11. package/dist/breadcrumb.layered.css +0 -1
  12. package/dist/button.css +14 -6
  13. package/dist/button.layered.css +14 -6
  14. package/dist/card.css +2 -1
  15. package/dist/card.layered.css +2 -1
  16. package/dist/checkbox.css +6 -9
  17. package/dist/checkbox.layered.css +6 -9
  18. package/dist/cjs/components/avatar/avatar.recipe.js +38 -19
  19. package/dist/cjs/components/badge/badge.recipe.js +83 -24
  20. package/dist/cjs/components/button/button.recipe.js +109 -18
  21. package/dist/cjs/components/divider/divider.recipe.js +20 -8
  22. package/dist/cjs/components/progress/progress.recipe.js +50 -10
  23. package/dist/cjs/components/spinner/spinner.recipe.js +15 -7
  24. package/dist/cjs/components/switch/switch.recipe.js +48 -18
  25. package/dist/cjs/core/index.js +4 -1
  26. package/dist/cjs/core/spec.js +71 -0
  27. package/dist/columncalc.css +2 -6
  28. package/dist/columncalc.layered.css +2 -6
  29. package/dist/command.css +9 -11
  30. package/dist/command.layered.css +9 -11
  31. package/dist/datatable.css +9 -19
  32. package/dist/datatable.layered.css +9 -19
  33. package/dist/datepicker.css +15 -80
  34. package/dist/datepicker.layered.css +15 -80
  35. package/dist/datepickerfield.css +3 -18
  36. package/dist/datepickerfield.layered.css +3 -18
  37. package/dist/donutchart.css +1 -2
  38. package/dist/donutchart.layered.css +1 -2
  39. package/dist/entitycard.css +1 -7
  40. package/dist/entitycard.layered.css +1 -7
  41. package/dist/esm/components/avatar/avatar.recipe.js +38 -19
  42. package/dist/esm/components/badge/badge.recipe.js +83 -24
  43. package/dist/esm/components/button/button.recipe.js +109 -18
  44. package/dist/esm/components/divider/divider.recipe.js +20 -8
  45. package/dist/esm/components/progress/progress.recipe.js +50 -10
  46. package/dist/esm/components/spinner/spinner.recipe.js +15 -7
  47. package/dist/esm/components/switch/switch.recipe.js +48 -18
  48. package/dist/esm/core/index.js +1 -0
  49. package/dist/esm/core/spec.js +66 -0
  50. package/dist/fileupload.css +10 -60
  51. package/dist/fileupload.layered.css +10 -60
  52. package/dist/inbox.css +1 -7
  53. package/dist/inbox.layered.css +1 -7
  54. package/dist/input.css +9 -20
  55. package/dist/input.layered.css +9 -20
  56. package/dist/listpage.css +2 -12
  57. package/dist/listpage.layered.css +2 -12
  58. package/dist/menu.css +3 -19
  59. package/dist/menu.layered.css +3 -19
  60. package/dist/multiselect.css +4 -25
  61. package/dist/multiselect.layered.css +4 -25
  62. package/dist/navitem.css +5 -13
  63. package/dist/navitem.layered.css +5 -13
  64. package/dist/onboarding.css +1 -8
  65. package/dist/onboarding.layered.css +1 -8
  66. package/dist/quantitypicker.css +15 -50
  67. package/dist/quantitypicker.layered.css +15 -50
  68. package/dist/radiogroup.css +1 -6
  69. package/dist/radiogroup.layered.css +1 -6
  70. package/dist/recordpage.css +2 -10
  71. package/dist/recordpage.layered.css +2 -10
  72. package/dist/recordpanel.css +1 -13
  73. package/dist/recordpanel.layered.css +1 -13
  74. package/dist/recordstable.css +2 -11
  75. package/dist/recordstable.layered.css +2 -11
  76. package/dist/resizablepanel.css +4 -15
  77. package/dist/resizablepanel.layered.css +4 -15
  78. package/dist/richtext.css +1 -5
  79. package/dist/richtext.layered.css +1 -5
  80. package/dist/select.css +7 -30
  81. package/dist/select.layered.css +7 -30
  82. package/dist/settingsshell.css +2 -7
  83. package/dist/settingsshell.layered.css +2 -7
  84. package/dist/sidebar.css +23 -28
  85. package/dist/sidebar.layered.css +23 -28
  86. package/dist/slider.css +1 -8
  87. package/dist/slider.layered.css +1 -8
  88. package/dist/statcard.css +1 -2
  89. package/dist/statcard.layered.css +1 -2
  90. package/dist/styles.css +194 -652
  91. package/dist/styles.layered.css +194 -652
  92. package/dist/switch.css +1 -5
  93. package/dist/switch.layered.css +1 -5
  94. package/dist/table.css +2 -1
  95. package/dist/table.layered.css +2 -1
  96. package/dist/tabletoolbar.css +1 -3
  97. package/dist/tabletoolbar.layered.css +1 -3
  98. package/dist/tabs.css +1 -5
  99. package/dist/tabs.layered.css +1 -5
  100. package/dist/tasklist.css +1 -8
  101. package/dist/tasklist.layered.css +1 -8
  102. package/dist/textarea.css +2 -13
  103. package/dist/textarea.layered.css +2 -13
  104. package/dist/tooltip.css +2 -1
  105. package/dist/tooltip.layered.css +2 -1
  106. package/dist/trialbanner.css +2 -11
  107. package/dist/trialbanner.layered.css +2 -11
  108. package/dist/types/components/avatar/avatar.d.ts +30 -30
  109. package/dist/types/components/avatar/avatar.recipe.d.ts +136 -49
  110. package/dist/types/components/badge/badge.d.ts +26 -26
  111. package/dist/types/components/badge/badge.recipe.d.ts +169 -45
  112. package/dist/types/components/button/button.d.ts +30 -30
  113. package/dist/types/components/button/button.recipe.d.ts +193 -45
  114. package/dist/types/components/divider/divider.recipe.d.ts +26 -2
  115. package/dist/types/components/progress/progress.recipe.d.ts +49 -2
  116. package/dist/types/components/spinner/spinner.recipe.d.ts +17 -2
  117. package/dist/types/components/switch/switch.recipe.d.ts +57 -3
  118. package/dist/types/core/index.d.ts +1 -0
  119. package/dist/types/core/spec.d.ts +83 -0
  120. package/dist/viewtoggle.css +3 -9
  121. package/dist/viewtoggle.layered.css +3 -9
  122. package/dist/wheelpicker.css +14 -59
  123. package/dist/wheelpicker.layered.css +14 -59
  124. package/package.json +4 -1
@@ -1,3 +1,50 @@
1
- declare const config: import("../..").RecipeConfig<{}>;
2
- export declare const progressRecipe: import("../..").RecipeFn<{}>;
1
+ declare const spec: {
2
+ readonly name: "progress";
3
+ readonly base: {
4
+ readonly display: "block";
5
+ readonly boxSizing: "border-box";
6
+ readonly height: "6px";
7
+ readonly borderRadius: `var(--${string})`;
8
+ readonly background: `color-mix(in srgb, var(--${string}) 8%, transparent)`;
9
+ readonly overflow: "hidden";
10
+ };
11
+ readonly parts: [{
12
+ readonly slot: "fill";
13
+ readonly selector: "& .crisp-progress-fill";
14
+ readonly decls: {
15
+ readonly display: "block";
16
+ readonly height: "100%";
17
+ readonly borderRadius: `var(--${string})`;
18
+ readonly background: `var(--${string})`;
19
+ readonly transition: "width 0.25s ease";
20
+ };
21
+ }, {
22
+ readonly selector: "&[data-segmented]";
23
+ readonly decls: {
24
+ readonly display: "flex";
25
+ readonly gap: `var(--${string})`;
26
+ readonly background: "transparent";
27
+ readonly overflow: "visible";
28
+ };
29
+ }, {
30
+ readonly slot: "seg";
31
+ readonly selector: "&[data-segmented] .crisp-progress-seg";
32
+ readonly decls: {
33
+ readonly flex: 1;
34
+ readonly height: "100%";
35
+ readonly borderRadius: `var(--${string})`;
36
+ readonly background: `color-mix(in srgb, var(--${string}) 8%, transparent)`;
37
+ readonly transition: "background 0.2s";
38
+ };
39
+ }, {
40
+ readonly slot: "seg";
41
+ readonly selector: "&[data-segmented] .crisp-progress-seg[data-filled]";
42
+ readonly decls: {
43
+ background: `var(--${string})`;
44
+ };
45
+ }];
46
+ };
47
+ declare const config: import("../..").RecipeConfig<Record<string, Record<string, string>>>;
48
+ export declare const progressRecipe: import("../..").RecipeFn<Record<string, Record<string, string>>>;
49
+ export { spec };
3
50
  export default config;
@@ -1,3 +1,18 @@
1
- declare const config: import("../..").RecipeConfig<{}>;
2
- export declare const spinnerRecipe: import("../..").RecipeFn<{}>;
1
+ declare const spec: {
2
+ readonly name: "spinner";
3
+ readonly globalCss: "@keyframes crisp-spin { to { transform: rotate(360deg); } }";
4
+ readonly base: {
5
+ readonly display: "inline-block";
6
+ readonly boxSizing: "border-box";
7
+ readonly width: "16px";
8
+ readonly height: "16px";
9
+ readonly borderRadius: `var(--${string})`;
10
+ readonly border: `2px solid color-mix(in srgb, var(--${string}) 15%, transparent)`;
11
+ readonly borderTopColor: `var(--${string})`;
12
+ readonly animation: "crisp-spin 0.6s linear infinite";
13
+ };
14
+ };
15
+ declare const config: import("../..").RecipeConfig<Record<string, Record<string, string>>>;
16
+ export declare const spinnerRecipe: import("../..").RecipeFn<Record<string, Record<string, string>>>;
17
+ export { spec };
3
18
  export default config;
@@ -1,5 +1,59 @@
1
- declare const config: import("../..").RecipeConfig<{}>;
2
- export declare const switchRecipe: import("../..").RecipeFn<{}> & {
3
- config: import("../..").RecipeConfig<{}>;
1
+ declare const spec: {
2
+ readonly name: "switch";
3
+ readonly base: {
4
+ readonly display: "inline-flex";
5
+ readonly alignItems: "center";
6
+ readonly flexShrink: 0;
7
+ readonly width: "24px";
8
+ readonly height: "16px";
9
+ readonly padding: 0;
10
+ readonly margin: 0;
11
+ readonly border: "none";
12
+ readonly borderRadius: `var(--${string})`;
13
+ readonly background: `color-mix(in srgb, var(--${string}) 9%, transparent)`;
14
+ readonly cursor: "pointer";
15
+ readonly transition: "background-color .2s ease";
16
+ readonly WebkitTapHighlightColor: "transparent";
17
+ };
18
+ readonly parts: [{
19
+ readonly slot: "thumb";
20
+ readonly selector: "& .crisp-switch-thumb";
21
+ readonly decls: {
22
+ readonly width: "12px";
23
+ readonly height: "12px";
24
+ readonly margin: "0 2px";
25
+ readonly borderRadius: `var(--${string})`;
26
+ readonly background: "#fff";
27
+ readonly transition: "transform .2s ease";
28
+ };
29
+ }, {
30
+ readonly selector: "&[data-checked]";
31
+ readonly decls: {
32
+ background: `var(--${string})`;
33
+ };
34
+ }, {
35
+ readonly slot: "thumb";
36
+ readonly selector: "&[data-checked] .crisp-switch-thumb";
37
+ readonly decls: {
38
+ readonly transform: "translateX(8px)";
39
+ };
40
+ }, {
41
+ readonly selector: "&:focus-visible";
42
+ readonly decls: {
43
+ readonly outline: `2px solid var(--${string})`;
44
+ readonly outlineOffset: "2px";
45
+ };
46
+ }, {
47
+ readonly selector: "&:disabled";
48
+ readonly decls: {
49
+ readonly opacity: 0.5;
50
+ readonly cursor: "not-allowed";
51
+ };
52
+ }];
4
53
  };
54
+ declare const config: import("../..").RecipeConfig<Record<string, Record<string, string>>>;
55
+ export declare const switchRecipe: import("../..").RecipeFn<Record<string, Record<string, string>>> & {
56
+ config: import("../..").RecipeConfig<Record<string, Record<string, string>>>;
57
+ };
58
+ export { spec };
5
59
  export default config;
@@ -7,6 +7,7 @@ export { type FloatingAlign, type PlaceFloatingOptions, placeFloating, } from ".
7
7
  export { Primitive } from "./primitive";
8
8
  export { resolveRecipe } from "./resolve";
9
9
  export * from "./rtl";
10
+ export { defineSpec, specToRecipe } from "./spec";
10
11
  export type * from "./types";
11
12
  export { useFocusAfterRemoval } from "./use-focus-after-removal";
12
13
  export { useFormReset } from "./use-form-reset";
@@ -0,0 +1,83 @@
1
+ import type { RecipeConfig } from "./types";
2
+ /**
3
+ * A component spec: the layer above a recipe, where style intent is DATA
4
+ * rather than a CSS string.
5
+ *
6
+ * ADR 0002 §2. A string cannot be asked a question — `build/emit-lynx.mjs`
7
+ * mangled every at-rule and the Spinner never span (#174) because the only
8
+ * thing it could do to CSS text was rewrite it and hope. A spec is a list of
9
+ * properties, so a target that cannot express one can REFUSE it and fail the
10
+ * build, which is the difference between a broken build and a silently wrong
11
+ * one.
12
+ *
13
+ * Two deliberate limits:
14
+ *
15
+ * - `parts` is an ORDERED array, not a map of slots. The emitted CSS depends on
16
+ * rule order (`&[data-segmented] .seg` must follow `&[data-segmented]`), and
17
+ * a map of named slots cannot express an interleaving of child-slot rules and
18
+ * root state rules. The `slot` field carries the name a non-CSS target needs
19
+ * for a declared part; `selector` carries what the web needs.
20
+ * - Values stay `string | number`. Resolving a token to its `var(--…)` is the
21
+ * contract's job, already typed, and re-encoding token references here would
22
+ * be a second source of truth for values that lesson 31 says will drift.
23
+ */
24
+ export type SpecValue = string | number;
25
+ /** Declarations in camelCase, the way a native style object spells them. */
26
+ export type SpecDecls = Record<string, SpecValue>;
27
+ export type SpecPart = {
28
+ /** Name of the declared part, for targets with no CSS selectors. */
29
+ slot?: string;
30
+ /** The nested selector, relative to the component root (`&` is the root). */
31
+ selector: string;
32
+ decls: SpecDecls;
33
+ };
34
+ /** One variant value's declarations, plus any nested rules it adds. */
35
+ export type SpecVariantValue = SpecDecls | {
36
+ decls: SpecDecls;
37
+ parts?: SpecPart[];
38
+ };
39
+ export type ComponentSpec = {
40
+ name: string;
41
+ /** Declarations on the component root. */
42
+ base: SpecDecls;
43
+ /** Nested rules, in emission order. */
44
+ parts?: SpecPart[];
45
+ /** variant -> value -> declarations. */
46
+ variants?: Record<string, Record<string, SpecVariantValue>>;
47
+ defaultVariants?: Record<string, string | boolean>;
48
+ /** Selections that apply together, in emission order. */
49
+ compoundVariants?: Array<Record<string, unknown> & {
50
+ decls: SpecDecls;
51
+ }>;
52
+ /** Raw CSS that must sit outside the component rule (`@keyframes`). */
53
+ globalCss?: string;
54
+ };
55
+ /** `{ flexShrink: 0 }` -> `flex-shrink: 0;`, in insertion order. */
56
+ export declare function declsToCss(decls: SpecDecls): string;
57
+ /**
58
+ * `const` type parameter, not a plain `ComponentSpec` return: widening the
59
+ * variant keys here loses the literal union that `VariantProps<typeof recipe>`
60
+ * hands a component, and `BadgeProps["tone"]` silently degrades from
61
+ * `"neutral" | "brand" | …` to `string`. That is a public API regression that
62
+ * typechecks everywhere except at the call sites it was meant to protect.
63
+ */
64
+ export declare function defineSpec<const S extends ComponentSpec>(spec: S): S;
65
+ /** The recipe variant record a spec implies: same keys, values compiled to CSS. */
66
+ export type SpecVariants<S extends ComponentSpec> = S["variants"] extends Record<string, Record<string, SpecVariantValue>> ? {
67
+ [K in keyof S["variants"]]: {
68
+ [P in keyof S["variants"][K]]: string;
69
+ };
70
+ } : Record<string, Record<string, string>>;
71
+ /**
72
+ * Compile a spec into the RecipeConfig the existing pipeline already takes, so
73
+ * nothing downstream — the CSS generator, the d.ts generator, `resolveRecipe`,
74
+ * the spec guards — has to know a spec exists. That is what keeps the
75
+ * conversion provable: `dist/` must not move, and CI's
76
+ * `git diff --exit-code dist/` is the gate.
77
+ *
78
+ * The formatting here is not cosmetic freedom. It reproduces exactly what the
79
+ * hand-written template strings emit — root declarations space-joined, each
80
+ * nested rule on its own line indented four spaces — because any other choice
81
+ * rewrites every committed stylesheet and buries the conversion's real diff.
82
+ */
83
+ export declare function specToRecipe<const S extends ComponentSpec>(spec: S): RecipeConfig<SpecVariants<S>>;
@@ -1,14 +1,8 @@
1
1
  .crisp-viewtoggle {
2
- display: inline-flex; align-items: center; gap: 2px; box-sizing: border-box; padding: 2px;
3
- border-radius: var(--crisp-radius-lg); background: color-mix(in srgb, var(--crisp-fg-primary) 5%, transparent);
4
- font-family: var(--crisp-font-sans);
5
-
6
- & .crisp-viewtoggle-tile { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px;
7
- padding: 0; margin: 0; border: none; border-radius: var(--crisp-radius-md); background: none; color: var(--crisp-fg-muted);
8
- font-family: inherit; cursor: pointer; transition: background-color .12s, color .12s, box-shadow .12s; }
2
+ display: inline-flex; align-items: center; gap: 2px; box-sizing: border-box; padding: 2px; border-radius: var(--crisp-radius-lg); background: color-mix(in srgb, var(--crisp-fg-primary) 5%, transparent); font-family: var(--crisp-font-sans);
3
+ & .crisp-viewtoggle-tile { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; padding: 0; margin: 0; border: none; border-radius: var(--crisp-radius-md); background: none; color: var(--crisp-fg-muted); font-family: inherit; cursor: pointer; transition: background-color .12s, color .12s, box-shadow .12s; }
9
4
  & .crisp-viewtoggle-tile:hover { color: var(--crisp-fg-primary); }
10
- & .crisp-viewtoggle-tile[data-active] { background: var(--crisp-bg-tile-active); color: var(--crisp-fg-primary);
11
- box-shadow: 0 1px 2px -1px var(--crisp-shadow-tile-active); }
5
+ & .crisp-viewtoggle-tile[data-active] { background: var(--crisp-bg-tile-active); color: var(--crisp-fg-primary); box-shadow: 0 1px 2px -1px var(--crisp-shadow-tile-active); }
12
6
  & .crisp-viewtoggle-tile:focus-visible { outline: 2px solid var(--crisp-bg-brand-solid); outline-offset: 1px; }
13
7
  }
14
8
 
@@ -1,15 +1,9 @@
1
1
  @layer crisp-components {
2
2
  .crisp-viewtoggle {
3
- display: inline-flex; align-items: center; gap: 2px; box-sizing: border-box; padding: 2px;
4
- border-radius: var(--crisp-radius-lg); background: color-mix(in srgb, var(--crisp-fg-primary) 5%, transparent);
5
- font-family: var(--crisp-font-sans);
6
-
7
- & .crisp-viewtoggle-tile { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px;
8
- padding: 0; margin: 0; border: none; border-radius: var(--crisp-radius-md); background: none; color: var(--crisp-fg-muted);
9
- font-family: inherit; cursor: pointer; transition: background-color .12s, color .12s, box-shadow .12s; }
3
+ display: inline-flex; align-items: center; gap: 2px; box-sizing: border-box; padding: 2px; border-radius: var(--crisp-radius-lg); background: color-mix(in srgb, var(--crisp-fg-primary) 5%, transparent); font-family: var(--crisp-font-sans);
4
+ & .crisp-viewtoggle-tile { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; padding: 0; margin: 0; border: none; border-radius: var(--crisp-radius-md); background: none; color: var(--crisp-fg-muted); font-family: inherit; cursor: pointer; transition: background-color .12s, color .12s, box-shadow .12s; }
10
5
  & .crisp-viewtoggle-tile:hover { color: var(--crisp-fg-primary); }
11
- & .crisp-viewtoggle-tile[data-active] { background: var(--crisp-bg-tile-active); color: var(--crisp-fg-primary);
12
- box-shadow: 0 1px 2px -1px var(--crisp-shadow-tile-active); }
6
+ & .crisp-viewtoggle-tile[data-active] { background: var(--crisp-bg-tile-active); color: var(--crisp-fg-primary); box-shadow: 0 1px 2px -1px var(--crisp-shadow-tile-active); }
13
7
  & .crisp-viewtoggle-tile:focus-visible { outline: 2px solid var(--crisp-bg-brand-solid); outline-offset: 1px; }
14
8
  }
15
9
 
@@ -1,68 +1,23 @@
1
1
  .crisp-wheelpicker {
2
-
3
- position: relative; box-sizing: border-box; width: 100%; overflow: hidden;
4
- background: var(--crisp-bg-surface); font-family: var(--crisp-font-sans);
5
-
6
- /* the band the selected row sits in; behind the items, never hit-tested */
7
- & .crisp-wheelpicker-indicator {
8
- position: absolute; z-index: 0; inset-inline: var(--crisp-space-4); top: 50%;
9
- transform: translateY(-50%); border-radius: var(--crisp-radius-md);
10
- background: var(--crisp-bg-neutral-weak); pointer-events: none;
11
- }
12
-
13
- /* fades the rows furthest from the centre, so the column reads as a wheel
14
- rather than a list. 40% of the viewport, capped, top and bottom. */
15
- & .crisp-wheelpicker-fog {
16
- position: relative; z-index: 1; height: 100%; overflow: hidden;
17
- mask-image: linear-gradient(
18
- to bottom,
19
- transparent 0,
20
- #000 var(--crisp-wheelpicker-fog),
21
- #000 calc(100% - var(--crisp-wheelpicker-fog)),
22
- transparent 100%
23
- );
24
- }
2
+ position: relative; box-sizing: border-box; width: 100%; overflow: hidden; background: var(--crisp-bg-surface); font-family: var(--crisp-font-sans); /* the band the selected row sits in; behind the items, never hit-tested */
3
+ & .crisp-wheelpicker-indicator { position: absolute; z-index: 0; inset-inline: var(--crisp-space-4); top: 50%; transform: translateY(-50%); border-radius: var(--crisp-radius-md); background: var(--crisp-bg-neutral-weak); pointer-events: none; }
4
+ /* fades the rows furthest from the centre, so the column reads as a wheel rather than a list. 40% of the viewport, capped, top and bottom. */
5
+ & .crisp-wheelpicker-fog { position: relative; z-index: 1; height: 100%; overflow: hidden; mask-image: linear-gradient( to bottom, transparent 0, #000 var(--crisp-wheelpicker-fog), #000 calc(100% - var(--crisp-wheelpicker-fog)), transparent 100% ); }
25
6
  & .crisp-wheelpicker-columns { display: flex; width: 100%; height: 100%; justify-content: center; }
26
-
27
- & .crisp-wheelpicker-column {
28
- box-sizing: border-box; flex: 0 0 auto; width: max-content; height: 100%;
29
- overflow-x: hidden; overflow-y: auto;
30
- overscroll-behavior-y: contain; overscroll-behavior-x: none;
31
- scroll-snap-type: y mandatory; touch-action: pan-y;
32
- scrollbar-width: none; -ms-overflow-style: none; outline: none;
33
- padding-block: var(--crisp-wheelpicker-offset);
34
- }
7
+ & .crisp-wheelpicker-column { box-sizing: border-box; flex: 0 0 auto; width: max-content; height: 100%; overflow-x: hidden; overflow-y: auto; overscroll-behavior-y: contain; overscroll-behavior-x: none; scroll-snap-type: y mandatory; touch-action: pan-y; scrollbar-width: none; -ms-overflow-style: none; outline: none; padding-block: var(--crisp-wheelpicker-offset); }
35
8
  & .crisp-wheelpicker-column::-webkit-scrollbar { display: none; }
36
-
37
- & .crisp-wheelpicker-item {
38
- display: flex; align-items: center; justify-content: center;
39
- scroll-snap-align: center; user-select: none; white-space: nowrap;
40
- padding-inline: var(--crisp-space-4); color: var(--crisp-fg-disabled);
41
- background: none; border: none; font: inherit; cursor: pointer;
42
- }
9
+ & .crisp-wheelpicker-item { display: flex; align-items: center; justify-content: center; scroll-snap-align: center; user-select: none; white-space: nowrap; padding-inline: var(--crisp-space-4); color: var(--crisp-fg-disabled); background: none; border: none; font: inherit; cursor: pointer; }
43
10
  & .crisp-wheelpicker-item[data-selected] { color: var(--crisp-fg-primary); }
44
-
45
- /* the ring is drawn on the SELECTED row, and only for keyboard focus:
46
- a pointer user has just tapped the row they meant and does not need it */
47
- & .crisp-wheelpicker-column:focus-visible .crisp-wheelpicker-item[data-selected] {
48
- outline: 2px solid var(--crisp-stroke-focus-ring); outline-offset: -2px;
49
- border-radius: var(--crisp-radius-md);
50
- }
51
-
11
+ /* the ring is drawn on the SELECTED row, and only for keyboard focus: a pointer user has just tapped the row they meant and does not need it */
12
+ & .crisp-wheelpicker-column:focus-visible .crisp-wheelpicker-item[data-selected] { outline: 2px solid var(--crisp-stroke-focus-ring); outline-offset: -2px; border-radius: var(--crisp-radius-md); }
52
13
  }
53
14
  .crisp-wheelpicker--size_small {
54
-
55
- height: 180px; --crisp-wheelpicker-offset: 72px;
56
- --crisp-wheelpicker-fog: min(40%, 108px);
57
- & .crisp-wheelpicker-indicator { height: 36px; }
58
- & .crisp-wheelpicker-item { height: 36px; min-height: 36px; font-size: 20px; line-height: 27px; }
59
-
15
+ height: 180px; --crisp-wheelpicker-offset: 72px; --crisp-wheelpicker-fog: min(40%, 108px);
16
+ & .crisp-wheelpicker-indicator { height: 36px; }
17
+ & .crisp-wheelpicker-item { height: 36px; min-height: 36px; font-size: 20px; line-height: 27px; }
60
18
  }
61
19
  .crisp-wheelpicker--size_medium {
62
-
63
- height: 220px; --crisp-wheelpicker-offset: 88px;
64
- --crisp-wheelpicker-fog: min(40%, 132px);
65
- & .crisp-wheelpicker-indicator { height: 44px; }
66
- & .crisp-wheelpicker-item { height: 44px; min-height: 44px; font-size: 26px; line-height: 35px; }
67
-
20
+ height: 220px; --crisp-wheelpicker-offset: 88px; --crisp-wheelpicker-fog: min(40%, 132px);
21
+ & .crisp-wheelpicker-indicator { height: 44px; }
22
+ & .crisp-wheelpicker-item { height: 44px; min-height: 44px; font-size: 26px; line-height: 35px; }
68
23
  }
@@ -1,70 +1,25 @@
1
1
  @layer crisp-components {
2
2
  .crisp-wheelpicker {
3
-
4
- position: relative; box-sizing: border-box; width: 100%; overflow: hidden;
5
- background: var(--crisp-bg-surface); font-family: var(--crisp-font-sans);
6
-
7
- /* the band the selected row sits in; behind the items, never hit-tested */
8
- & .crisp-wheelpicker-indicator {
9
- position: absolute; z-index: 0; inset-inline: var(--crisp-space-4); top: 50%;
10
- transform: translateY(-50%); border-radius: var(--crisp-radius-md);
11
- background: var(--crisp-bg-neutral-weak); pointer-events: none;
12
- }
13
-
14
- /* fades the rows furthest from the centre, so the column reads as a wheel
15
- rather than a list. 40% of the viewport, capped, top and bottom. */
16
- & .crisp-wheelpicker-fog {
17
- position: relative; z-index: 1; height: 100%; overflow: hidden;
18
- mask-image: linear-gradient(
19
- to bottom,
20
- transparent 0,
21
- #000 var(--crisp-wheelpicker-fog),
22
- #000 calc(100% - var(--crisp-wheelpicker-fog)),
23
- transparent 100%
24
- );
25
- }
3
+ position: relative; box-sizing: border-box; width: 100%; overflow: hidden; background: var(--crisp-bg-surface); font-family: var(--crisp-font-sans); /* the band the selected row sits in; behind the items, never hit-tested */
4
+ & .crisp-wheelpicker-indicator { position: absolute; z-index: 0; inset-inline: var(--crisp-space-4); top: 50%; transform: translateY(-50%); border-radius: var(--crisp-radius-md); background: var(--crisp-bg-neutral-weak); pointer-events: none; }
5
+ /* fades the rows furthest from the centre, so the column reads as a wheel rather than a list. 40% of the viewport, capped, top and bottom. */
6
+ & .crisp-wheelpicker-fog { position: relative; z-index: 1; height: 100%; overflow: hidden; mask-image: linear-gradient( to bottom, transparent 0, #000 var(--crisp-wheelpicker-fog), #000 calc(100% - var(--crisp-wheelpicker-fog)), transparent 100% ); }
26
7
  & .crisp-wheelpicker-columns { display: flex; width: 100%; height: 100%; justify-content: center; }
27
-
28
- & .crisp-wheelpicker-column {
29
- box-sizing: border-box; flex: 0 0 auto; width: max-content; height: 100%;
30
- overflow-x: hidden; overflow-y: auto;
31
- overscroll-behavior-y: contain; overscroll-behavior-x: none;
32
- scroll-snap-type: y mandatory; touch-action: pan-y;
33
- scrollbar-width: none; -ms-overflow-style: none; outline: none;
34
- padding-block: var(--crisp-wheelpicker-offset);
35
- }
8
+ & .crisp-wheelpicker-column { box-sizing: border-box; flex: 0 0 auto; width: max-content; height: 100%; overflow-x: hidden; overflow-y: auto; overscroll-behavior-y: contain; overscroll-behavior-x: none; scroll-snap-type: y mandatory; touch-action: pan-y; scrollbar-width: none; -ms-overflow-style: none; outline: none; padding-block: var(--crisp-wheelpicker-offset); }
36
9
  & .crisp-wheelpicker-column::-webkit-scrollbar { display: none; }
37
-
38
- & .crisp-wheelpicker-item {
39
- display: flex; align-items: center; justify-content: center;
40
- scroll-snap-align: center; user-select: none; white-space: nowrap;
41
- padding-inline: var(--crisp-space-4); color: var(--crisp-fg-disabled);
42
- background: none; border: none; font: inherit; cursor: pointer;
43
- }
10
+ & .crisp-wheelpicker-item { display: flex; align-items: center; justify-content: center; scroll-snap-align: center; user-select: none; white-space: nowrap; padding-inline: var(--crisp-space-4); color: var(--crisp-fg-disabled); background: none; border: none; font: inherit; cursor: pointer; }
44
11
  & .crisp-wheelpicker-item[data-selected] { color: var(--crisp-fg-primary); }
45
-
46
- /* the ring is drawn on the SELECTED row, and only for keyboard focus:
47
- a pointer user has just tapped the row they meant and does not need it */
48
- & .crisp-wheelpicker-column:focus-visible .crisp-wheelpicker-item[data-selected] {
49
- outline: 2px solid var(--crisp-stroke-focus-ring); outline-offset: -2px;
50
- border-radius: var(--crisp-radius-md);
51
- }
52
-
12
+ /* the ring is drawn on the SELECTED row, and only for keyboard focus: a pointer user has just tapped the row they meant and does not need it */
13
+ & .crisp-wheelpicker-column:focus-visible .crisp-wheelpicker-item[data-selected] { outline: 2px solid var(--crisp-stroke-focus-ring); outline-offset: -2px; border-radius: var(--crisp-radius-md); }
53
14
  }
54
15
  .crisp-wheelpicker--size_small {
55
-
56
- height: 180px; --crisp-wheelpicker-offset: 72px;
57
- --crisp-wheelpicker-fog: min(40%, 108px);
58
- & .crisp-wheelpicker-indicator { height: 36px; }
59
- & .crisp-wheelpicker-item { height: 36px; min-height: 36px; font-size: 20px; line-height: 27px; }
60
-
16
+ height: 180px; --crisp-wheelpicker-offset: 72px; --crisp-wheelpicker-fog: min(40%, 108px);
17
+ & .crisp-wheelpicker-indicator { height: 36px; }
18
+ & .crisp-wheelpicker-item { height: 36px; min-height: 36px; font-size: 20px; line-height: 27px; }
61
19
  }
62
20
  .crisp-wheelpicker--size_medium {
63
-
64
- height: 220px; --crisp-wheelpicker-offset: 88px;
65
- --crisp-wheelpicker-fog: min(40%, 132px);
66
- & .crisp-wheelpicker-indicator { height: 44px; }
67
- & .crisp-wheelpicker-item { height: 44px; min-height: 44px; font-size: 26px; line-height: 35px; }
68
-
21
+ height: 220px; --crisp-wheelpicker-offset: 88px; --crisp-wheelpicker-fog: min(40%, 132px);
22
+ & .crisp-wheelpicker-indicator { height: 44px; }
23
+ & .crisp-wheelpicker-item { height: 44px; min-height: 44px; font-size: 26px; line-height: 35px; }
69
24
  }
70
25
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crisp-ui-kit/crisp",
3
- "version": "0.32.1",
3
+ "version": "0.33.1",
4
4
  "description": "A lean React design system, measured pixel-for-pixel from Attio.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -422,6 +422,8 @@
422
422
  "lynx": "bun build/lynx-lint.mjs",
423
423
  "lynx:emit": "bun build/emit-lynx.mjs",
424
424
  "ssr": "bun scripts/ssr-check.tsx",
425
+ "visual": "bun scripts/visual-page.tsx && playwright test",
426
+ "visual:baseline": "bun run build && bun scripts/visual-page.tsx && docker run --rm -v \"$PWD\":/w -w /w mcr.microsoft.com/playwright:v1.63.0-noble npx playwright test --update-snapshots",
425
427
  "contrast": "bun scripts/contrast-page.tsx",
426
428
  "build:esm": "tsc -p tsconfig.esm.json && bun scripts/fix-esm-extensions.mjs",
427
429
  "build:cjs": "tsc -p tsconfig.cjs.json && bun scripts/finish-cjs.mjs",
@@ -450,6 +452,7 @@
450
452
  "devDependencies": {
451
453
  "@biomejs/biome": "^2.5.10",
452
454
  "@happy-dom/global-registrator": "^20.11.6",
455
+ "@playwright/test": "^1.63.0",
453
456
  "@testing-library/jest-dom": "^7.0.1",
454
457
  "@testing-library/react": "^16.3.2",
455
458
  "@testing-library/user-event": "^14.6.6",