@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,32 +1,51 @@
1
- import { defineRecipe, resolveRecipe } from "../../core/index.js";
1
+ import { defineSpec, resolveRecipe, specToRecipe } from "../../core/index.js";
2
2
  import { contract as c } from "../../tokens/index.js";
3
- const config = defineRecipe({
3
+ const spec = defineSpec({
4
4
  name: "avatar",
5
- base: `display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; font-family: ${c.font.sans}; font-weight: ${c.weight.semibold}; overflow: hidden; user-select: none; & img { width: 100%; height: 100%; object-fit: cover; }`,
5
+ base: {
6
+ display: "inline-flex",
7
+ alignItems: "center",
8
+ justifyContent: "center",
9
+ flexShrink: 0,
10
+ fontFamily: c.font.sans,
11
+ fontWeight: c.weight.semibold,
12
+ overflow: "hidden",
13
+ userSelect: "none",
14
+ },
15
+ parts: [
16
+ {
17
+ slot: "image",
18
+ selector: "& img",
19
+ decls: { width: "100%", height: "100%", objectFit: "cover" },
20
+ },
21
+ ],
6
22
  variants: {
7
23
  size: {
8
- sm: `height: 16px; width: 16px; font-size: 9px;`,
9
- md: `height: 20px; width: 20px; font-size: 10px;`,
10
- lg: `height: 24px; width: 24px; font-size: ${c.text.sm};`,
11
- xl: `height: 32px; width: 32px; font-size: ${c.text.lg};`,
24
+ sm: { height: "16px", width: "16px", fontSize: "9px" },
25
+ md: { height: "20px", width: "20px", fontSize: "10px" },
26
+ lg: { height: "24px", width: "24px", fontSize: c.text.sm },
27
+ xl: { height: "32px", width: "32px", fontSize: c.text.lg },
12
28
  },
13
29
  shape: {
14
- circle: `border-radius: ${c.radius.full};`,
15
- rounded: `border-radius: 30%;`,
30
+ circle: { borderRadius: c.radius.full },
31
+ rounded: { borderRadius: "30%" },
16
32
  },
17
33
  tone: {
18
- neutral: `background: ${c.bgRaised}; color: ${c.fgMuted}; box-shadow: inset 0 0 0 1px ${c.borderDefault};`,
19
- brand: `background: ${c.bgBrandSolid}; color: #fff;`,
20
- orange: `background: ${c.bgOrange}; color: #fff;`,
21
- cyan: `background: ${c.bgCyan}; color: #fff;`,
22
- purple: `background: ${c.bgPurple}; color: #fff;`,
23
- green: `background: ${c.bgSuccess}; color: #fff;`,
34
+ neutral: {
35
+ background: c.bgRaised,
36
+ color: c.fgMuted,
37
+ boxShadow: `inset 0 0 0 1px ${c.borderDefault}`,
38
+ },
39
+ brand: { background: c.bgBrandSolid, color: "#fff" },
40
+ orange: { background: c.bgOrange, color: "#fff" },
41
+ cyan: { background: c.bgCyan, color: "#fff" },
42
+ purple: { background: c.bgPurple, color: "#fff" },
43
+ green: { background: c.bgSuccess, color: "#fff" },
24
44
  },
25
45
  },
26
46
  defaultVariants: { size: "md", shape: "circle", tone: "neutral" },
27
- compoundVariants: [],
28
- });
29
- export const avatarRecipe = Object.assign(resolveRecipe(config), {
30
- config,
31
47
  });
48
+ const config = specToRecipe(spec);
49
+ export const avatarRecipe = Object.assign(resolveRecipe(config), { config });
50
+ export { spec };
32
51
  export default config;
@@ -1,57 +1,116 @@
1
- import { defineRecipe, resolveRecipe } from "../../core/index.js";
1
+ import { defineSpec, resolveRecipe, specToRecipe } from "../../core/index.js";
2
2
  import { contract as c } from "../../tokens/index.js";
3
- const config = defineRecipe({
3
+ const ring = (v, pct) => `inset 0 0 0 1px color-mix(in srgb, ${v} ${pct}%, transparent)`;
4
+ const soft = (v, pct) => `color-mix(in srgb, ${v} ${pct}%, transparent)`;
5
+ const spec = defineSpec({
4
6
  name: "badge",
5
- base: `display: inline-flex; align-items: center; justify-content: center; gap: ${c.space["1"]}; font-family: ${c.font.sans}; font-weight: ${c.weight.medium}; letter-spacing: -0.1px; white-space: nowrap;
6
- & .crisp-badge-dot { width: 6px; height: 6px; flex-shrink: 0; border-radius: ${c.radius.full}; background: currentColor; }`,
7
+ base: {
8
+ display: "inline-flex",
9
+ alignItems: "center",
10
+ justifyContent: "center",
11
+ gap: c.space["1"],
12
+ fontFamily: c.font.sans,
13
+ fontWeight: c.weight.medium,
14
+ letterSpacing: "-0.1px",
15
+ whiteSpace: "nowrap",
16
+ },
17
+ parts: [
18
+ {
19
+ slot: "dot",
20
+ selector: "& .crisp-badge-dot",
21
+ decls: {
22
+ width: "6px",
23
+ height: "6px",
24
+ flexShrink: 0,
25
+ borderRadius: c.radius.full,
26
+ background: "currentColor",
27
+ },
28
+ },
29
+ ],
7
30
  variants: {
8
31
  tone: {
9
- neutral: `background: ${c.bgRaised}; color: ${c.fgMuted}; box-shadow: inset 0 0 0 1px ${c.borderDefault};`,
10
- brand: `background: color-mix(in srgb, ${c.bgBrandSolid} 14%, transparent); color: ${c.bgBrandSolid}; box-shadow: inset 0 0 0 1px color-mix(in srgb, ${c.bgBrandSolid} 30%, transparent);`,
11
- success: `background: color-mix(in srgb, ${c.bgSuccess} 14%, transparent); color: ${c.bgSuccess}; box-shadow: inset 0 0 0 1px color-mix(in srgb, ${c.bgSuccess} 30%, transparent);`,
12
- warning: `background: color-mix(in srgb, ${c.bgWarning} 16%, transparent); color: ${c.bgWarning}; box-shadow: inset 0 0 0 1px color-mix(in srgb, ${c.bgWarning} 32%, transparent);`,
13
- danger: `background: color-mix(in srgb, ${c.bgDanger} 14%, transparent); color: ${c.bgDanger}; box-shadow: inset 0 0 0 1px color-mix(in srgb, ${c.bgDanger} 30%, transparent);`,
32
+ neutral: {
33
+ background: c.bgRaised,
34
+ color: c.fgMuted,
35
+ boxShadow: `inset 0 0 0 1px ${c.borderDefault}`,
36
+ },
37
+ brand: {
38
+ background: soft(c.bgBrandSolid, 14),
39
+ color: c.bgBrandSolid,
40
+ boxShadow: ring(c.bgBrandSolid, 30),
41
+ },
42
+ success: {
43
+ background: soft(c.bgSuccess, 14),
44
+ color: c.bgSuccess,
45
+ boxShadow: ring(c.bgSuccess, 30),
46
+ },
47
+ warning: {
48
+ background: soft(c.bgWarning, 16),
49
+ color: c.bgWarning,
50
+ boxShadow: ring(c.bgWarning, 32),
51
+ },
52
+ danger: {
53
+ background: soft(c.bgDanger, 14),
54
+ color: c.bgDanger,
55
+ boxShadow: ring(c.bgDanger, 30),
56
+ },
14
57
  },
15
58
  size: {
16
- small: `height: 16px; padding-inline: ${c.space["1.5"]}; font-size: ${c.text.xs}; border-radius: ${c.radius.sm};`,
17
- medium: `height: 20px; padding-inline: ${c.space["2"]}; font-size: ${c.text.sm}; border-radius: ${c.radius.sm};`,
18
- large: `height: ${c.size.chip}; padding-inline: 9px; font-size: ${c.text.lg}; border-radius: ${c.radius.chip};`,
19
- },
20
- variant: {
21
- soft: ``,
22
- solid: ``,
59
+ small: {
60
+ height: "16px",
61
+ paddingInline: c.space["1.5"],
62
+ fontSize: c.text.xs,
63
+ borderRadius: c.radius.sm,
64
+ },
65
+ medium: {
66
+ height: "20px",
67
+ paddingInline: c.space["2"],
68
+ fontSize: c.text.sm,
69
+ borderRadius: c.radius.sm,
70
+ },
71
+ large: {
72
+ height: c.size.chip,
73
+ paddingInline: "9px",
74
+ fontSize: c.text.lg,
75
+ borderRadius: c.radius.chip,
76
+ },
23
77
  },
78
+ variant: { soft: {}, solid: {} },
24
79
  },
25
80
  defaultVariants: { tone: "neutral", size: "medium", variant: "soft" },
26
81
  compoundVariants: [
27
82
  {
28
83
  variant: "solid",
29
84
  tone: "brand",
30
- css: `background: ${c.bgBrandSolid}; color: ${c.fgOnBrand}; box-shadow: none;`,
85
+ decls: {
86
+ background: c.bgBrandSolid,
87
+ color: c.fgOnBrand,
88
+ boxShadow: "none",
89
+ },
31
90
  },
32
91
  {
33
92
  variant: "solid",
34
93
  tone: "success",
35
- css: `background: ${c.bgSuccess}; color: ${c.fgOnTone}; box-shadow: none;`,
94
+ decls: { background: c.bgSuccess, color: c.fgOnTone, boxShadow: "none" },
36
95
  },
37
96
  {
38
97
  variant: "solid",
39
98
  tone: "warning",
40
- css: `background: ${c.bgWarning}; color: ${c.fgOnTone}; box-shadow: none;`,
99
+ decls: { background: c.bgWarning, color: c.fgOnTone, boxShadow: "none" },
41
100
  },
42
101
  {
43
102
  variant: "solid",
44
103
  tone: "danger",
45
- css: `background: ${c.bgDanger}; color: ${c.fgOnTone}; box-shadow: none;`,
104
+ decls: { background: c.bgDanger, color: c.fgOnTone, boxShadow: "none" },
46
105
  },
47
106
  {
48
107
  variant: "solid",
49
108
  tone: "neutral",
50
- css: `background: ${c.fgMuted}; color: ${c.bgSurface}; box-shadow: none;`,
109
+ decls: { background: c.fgMuted, color: c.bgSurface, boxShadow: "none" },
51
110
  },
52
111
  ],
53
112
  });
54
- export const badgeRecipe = Object.assign(resolveRecipe(config), {
55
- config,
56
- });
113
+ const config = specToRecipe(spec);
114
+ export const badgeRecipe = Object.assign(resolveRecipe(config), { config });
115
+ export { spec };
57
116
  export default config;
@@ -1,23 +1,96 @@
1
- import { defineRecipe, resolveRecipe } from "../../core/index.js";
1
+ import { defineSpec, resolveRecipe, specToRecipe } from "../../core/index.js";
2
2
  import { contract as c, elevation as e } from "../../tokens/index.js";
3
- const config = defineRecipe({
3
+ const spec = defineSpec({
4
4
  name: "button",
5
- base: `display: inline-flex; align-items: center; justify-content: center; gap: ${c.space["1.5"]}; font-family: ${c.font.sans}; font-weight: ${c.weight.medium}; letter-spacing: -0.14px; line-height: 20px; white-space: nowrap; border: none; cursor: pointer; transition: background-color .2s, box-shadow .2s, color .2s; &:disabled { opacity: 0.5; cursor: not-allowed; }`,
5
+ base: {
6
+ display: "inline-flex",
7
+ alignItems: "center",
8
+ justifyContent: "center",
9
+ gap: c.space["1.5"],
10
+ fontFamily: c.font.sans,
11
+ fontWeight: c.weight.medium,
12
+ letterSpacing: "-0.14px",
13
+ lineHeight: "20px",
14
+ whiteSpace: "nowrap",
15
+ border: "none",
16
+ cursor: "pointer",
17
+ transition: "background-color .2s, box-shadow .2s, color .2s",
18
+ },
19
+ parts: [
20
+ { selector: "&:disabled", decls: { opacity: 0.5, cursor: "not-allowed" } },
21
+ ],
6
22
  variants: {
7
23
  intent: {
8
- primary: `background: ${c.bgBrandSolid}; color: ${c.fgOnBrand}; box-shadow: inset 0 0 0 1px rgba(255,255,255,0.07), 0 2px 4px -2px rgba(38,109,240,0.12), 0 3px 6px -2px rgba(38,109,240,0.08); &:hover { background: ${c.bgBrandSolidPressed}; } &:active { background: ${c.bgBrandSolidPressed}; }`,
9
- neutral: `background: ${c.bgSurface}; color: ${c.fgPrimary}; box-shadow: ${e.field}; &:hover { background: ${c.bgRaised}; }`,
10
- danger: `background: ${c.bgDangerSolid}; color: ${c.fgOnBrand}; box-shadow: inset 0 0 0 1px rgba(255,255,255,0.07), 0 2px 4px -2px rgba(186,37,37,0.20), 0 3px 6px -2px rgba(186,37,37,0.12); &:hover { filter: brightness(0.96); } &:active { filter: brightness(0.92); }`,
11
- ghost: `background: transparent; color: ${c.fgMuted}; box-shadow: none; &:hover { background: ${c.bgRaised}; color: ${c.fgPrimary}; }`,
24
+ primary: {
25
+ decls: {
26
+ background: c.bgBrandSolid,
27
+ color: c.fgOnBrand,
28
+ boxShadow: "inset 0 0 0 1px rgba(255,255,255,0.07), 0 2px 4px -2px rgba(38,109,240,0.12), 0 3px 6px -2px rgba(38,109,240,0.08)",
29
+ },
30
+ parts: [
31
+ { selector: "&:hover", decls: { background: c.bgBrandSolidPressed } },
32
+ {
33
+ selector: "&:active",
34
+ decls: { background: c.bgBrandSolidPressed },
35
+ },
36
+ ],
37
+ },
38
+ neutral: {
39
+ decls: {
40
+ background: c.bgSurface,
41
+ color: c.fgPrimary,
42
+ boxShadow: e.field,
43
+ },
44
+ parts: [{ selector: "&:hover", decls: { background: c.bgRaised } }],
45
+ },
46
+ danger: {
47
+ decls: {
48
+ background: c.bgDangerSolid,
49
+ color: c.fgOnBrand,
50
+ boxShadow: "inset 0 0 0 1px rgba(255,255,255,0.07), 0 2px 4px -2px rgba(186,37,37,0.20), 0 3px 6px -2px rgba(186,37,37,0.12)",
51
+ },
52
+ parts: [
53
+ { selector: "&:hover", decls: { filter: "brightness(0.96)" } },
54
+ { selector: "&:active", decls: { filter: "brightness(0.92)" } },
55
+ ],
56
+ },
57
+ ghost: {
58
+ decls: {
59
+ background: "transparent",
60
+ color: c.fgMuted,
61
+ boxShadow: "none",
62
+ },
63
+ parts: [
64
+ {
65
+ selector: "&:hover",
66
+ decls: { background: c.bgRaised, color: c.fgPrimary },
67
+ },
68
+ ],
69
+ },
12
70
  },
13
71
  size: {
14
- sm: `height: 24px; padding-inline: ${c.space["1.5"]}; font-size: ${c.text.md}; border-radius: ${c.radius.md};`,
15
- md: `height: 28px; padding-inline: ${c.space["2"]}; font-size: ${c.text.lg}; border-radius: ${c.radius.md};`,
16
- lg: `height: 40px; padding-inline: 14px; font-size: ${c.text.lg}; border-radius: 10px;`,
72
+ sm: {
73
+ height: "24px",
74
+ paddingInline: c.space["1.5"],
75
+ fontSize: c.text.md,
76
+ borderRadius: c.radius.md,
77
+ },
78
+ md: {
79
+ height: "28px",
80
+ paddingInline: c.space["2"],
81
+ fontSize: c.text.lg,
82
+ borderRadius: c.radius.md,
83
+ },
84
+ lg: {
85
+ height: "40px",
86
+ paddingInline: "14px",
87
+ fontSize: c.text.lg,
88
+ borderRadius: "10px",
89
+ },
17
90
  },
18
91
  shape: {
19
- text: ``,
20
- icon: `padding-inline: 0; aspect-ratio: 1 / 1;`,
92
+ text: {},
93
+ icon: { paddingInline: 0, aspectRatio: "1 / 1" },
21
94
  },
22
95
  // Opt-in hit-area expansion (crisp/#62): none of sm/md/lg reaches the
23
96
  // 44x44 touch-target minimum (Apple HIG / Material), and enlarging the
@@ -27,8 +100,27 @@ const config = defineRecipe({
27
100
  // Opt-in rather than always-on: an always-on overlay would swallow
28
101
  // clicks meant for a neighbour in a toolbar packed tighter than 44px.
29
102
  touchTarget: {
30
- true: `position: relative; &::after { content: ""; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 100%; height: 100%; min-width: 44px; min-height: 44px; }`,
31
- false: ``,
103
+ true: {
104
+ decls: { position: "relative" },
105
+ parts: [
106
+ {
107
+ slot: "hitArea",
108
+ selector: "&::after",
109
+ decls: {
110
+ content: '""',
111
+ position: "absolute",
112
+ top: "50%",
113
+ left: "50%",
114
+ transform: "translate(-50%, -50%)",
115
+ width: "100%",
116
+ height: "100%",
117
+ minWidth: "44px",
118
+ minHeight: "44px",
119
+ },
120
+ },
121
+ ],
122
+ },
123
+ false: {},
32
124
  },
33
125
  },
34
126
  defaultVariants: {
@@ -37,9 +129,8 @@ const config = defineRecipe({
37
129
  shape: "text",
38
130
  touchTarget: false,
39
131
  },
40
- compoundVariants: [],
41
- });
42
- export const buttonRecipe = Object.assign(resolveRecipe(config), {
43
- config,
44
132
  });
133
+ const config = specToRecipe(spec);
134
+ export const buttonRecipe = Object.assign(resolveRecipe(config), { config });
135
+ export { spec };
45
136
  export default config;
@@ -1,14 +1,26 @@
1
- import { defineRecipe, resolveRecipe } from "../../core/index.js";
1
+ import { defineSpec, resolveRecipe, specToRecipe } from "../../core/index.js";
2
2
  import { contract as c } from "../../tokens/index.js";
3
3
  // A hairline separator — horizontal (default) or vertical.
4
- const config = defineRecipe({
4
+ const spec = defineSpec({
5
5
  name: "divider",
6
- base: `flex-shrink: 0; border: none; margin: 0; background: ${c.borderSubtle};
7
- &[data-orientation="horizontal"] { width: 100%; height: 1px; }
8
- &[data-orientation="vertical"] { width: 1px; align-self: stretch; min-height: 1em; }`,
9
- variants: {},
10
- defaultVariants: {},
11
- compoundVariants: [],
6
+ base: {
7
+ flexShrink: 0,
8
+ border: "none",
9
+ margin: 0,
10
+ background: c.borderSubtle,
11
+ },
12
+ parts: [
13
+ {
14
+ selector: '&[data-orientation="horizontal"]',
15
+ decls: { width: "100%", height: "1px" },
16
+ },
17
+ {
18
+ selector: '&[data-orientation="vertical"]',
19
+ decls: { width: "1px", alignSelf: "stretch", minHeight: "1em" },
20
+ },
21
+ ],
12
22
  });
23
+ const config = specToRecipe(spec);
13
24
  export const dividerRecipe = resolveRecipe(config);
25
+ export { spec };
14
26
  export default config;
@@ -1,17 +1,57 @@
1
- import { defineRecipe, resolveRecipe } from "../../core/index.js";
1
+ import { defineSpec, resolveRecipe, specToRecipe } from "../../core/index.js";
2
2
  import { contract as c } from "../../tokens/index.js";
3
3
  // A progress indicator — a continuous brand-filled track, or a segmented row of
4
4
  // pills (a "Getting started 3/6" bar).
5
- const config = defineRecipe({
5
+ const spec = defineSpec({
6
6
  name: "progress",
7
- base: `display: block; box-sizing: border-box; height: 6px; border-radius: ${c.radius.full}; background: color-mix(in srgb, ${c.fgPrimary} 8%, transparent); overflow: hidden;
8
- & .crisp-progress-fill { display: block; height: 100%; border-radius: ${c.radius.full}; background: ${c.bgBrandSolid}; transition: width 0.25s ease; }
9
- &[data-segmented] { display: flex; gap: ${c.space["1"]}; background: transparent; overflow: visible; }
10
- &[data-segmented] .crisp-progress-seg { flex: 1; height: 100%; border-radius: ${c.radius.full}; background: color-mix(in srgb, ${c.fgPrimary} 8%, transparent); transition: background 0.2s; }
11
- &[data-segmented] .crisp-progress-seg[data-filled] { background: ${c.bgBrandSolid}; }`,
12
- variants: {},
13
- defaultVariants: {},
14
- compoundVariants: [],
7
+ base: {
8
+ display: "block",
9
+ boxSizing: "border-box",
10
+ height: "6px",
11
+ borderRadius: c.radius.full,
12
+ background: `color-mix(in srgb, ${c.fgPrimary} 8%, transparent)`,
13
+ overflow: "hidden",
14
+ },
15
+ parts: [
16
+ {
17
+ slot: "fill",
18
+ selector: "& .crisp-progress-fill",
19
+ decls: {
20
+ display: "block",
21
+ height: "100%",
22
+ borderRadius: c.radius.full,
23
+ background: c.bgBrandSolid,
24
+ transition: "width 0.25s ease",
25
+ },
26
+ },
27
+ {
28
+ selector: "&[data-segmented]",
29
+ decls: {
30
+ display: "flex",
31
+ gap: c.space["1"],
32
+ background: "transparent",
33
+ overflow: "visible",
34
+ },
35
+ },
36
+ {
37
+ slot: "seg",
38
+ selector: "&[data-segmented] .crisp-progress-seg",
39
+ decls: {
40
+ flex: 1,
41
+ height: "100%",
42
+ borderRadius: c.radius.full,
43
+ background: `color-mix(in srgb, ${c.fgPrimary} 8%, transparent)`,
44
+ transition: "background 0.2s",
45
+ },
46
+ },
47
+ {
48
+ slot: "seg",
49
+ selector: "&[data-segmented] .crisp-progress-seg[data-filled]",
50
+ decls: { background: c.bgBrandSolid },
51
+ },
52
+ ],
15
53
  });
54
+ const config = specToRecipe(spec);
16
55
  export const progressRecipe = resolveRecipe(config);
56
+ export { spec };
17
57
  export default config;
@@ -1,13 +1,21 @@
1
- import { defineRecipe, resolveRecipe } from "../../core/index.js";
1
+ import { defineSpec, resolveRecipe, specToRecipe } from "../../core/index.js";
2
2
  import { contract as c } from "../../tokens/index.js";
3
3
  // A spinning ring loading indicator.
4
- const config = defineRecipe({
4
+ const spec = defineSpec({
5
5
  name: "spinner",
6
- globalCss: `@keyframes crisp-spin { to { transform: rotate(360deg); } }`,
7
- base: `display: inline-block; box-sizing: border-box; width: 16px; height: 16px; border-radius: ${c.radius.full}; border: 2px solid color-mix(in srgb, ${c.fgPrimary} 15%, transparent); border-top-color: ${c.fgMuted}; animation: crisp-spin 0.6s linear infinite;`,
8
- variants: {},
9
- defaultVariants: {},
10
- compoundVariants: [],
6
+ globalCss: "@keyframes crisp-spin { to { transform: rotate(360deg); } }",
7
+ base: {
8
+ display: "inline-block",
9
+ boxSizing: "border-box",
10
+ width: "16px",
11
+ height: "16px",
12
+ borderRadius: c.radius.full,
13
+ border: `2px solid color-mix(in srgb, ${c.fgPrimary} 15%, transparent)`,
14
+ borderTopColor: c.fgMuted,
15
+ animation: "crisp-spin 0.6s linear infinite",
16
+ },
11
17
  });
18
+ const config = specToRecipe(spec);
12
19
  export const spinnerRecipe = resolveRecipe(config);
20
+ export { spec };
13
21
  export default config;
@@ -1,25 +1,55 @@
1
- import { defineRecipe, resolveRecipe } from "../../core/index.js";
1
+ import { defineSpec, resolveRecipe, specToRecipe } from "../../core/index.js";
2
2
  import { contract as c } from "../../tokens/index.js";
3
3
  // A settings toggle, measured from the live app: a 24×16 pill track that
4
4
  // flips from a muted grey to brand blue (#266df0), with a 12px white thumb
5
5
  // (2px inset) that slides 8px across. background + transform both animate .2s.
6
- const config = defineRecipe({
6
+ const spec = defineSpec({
7
7
  name: "switch",
8
- base: `
9
- display: inline-flex; align-items: center; flex-shrink: 0; width: 24px; height: 16px; padding: 0; margin: 0;
10
- border: none; border-radius: ${c.radius.full}; background: color-mix(in srgb, ${c.fgPrimary} 9%, transparent);
11
- cursor: pointer; transition: background-color .2s ease; -webkit-tap-highlight-color: transparent;
12
- & .crisp-switch-thumb { width: 12px; height: 12px; margin: 0 2px; border-radius: ${c.radius.full}; background: #fff; transition: transform .2s ease; }
13
- &[data-checked] { background: ${c.bgBrandSolid}; }
14
- &[data-checked] .crisp-switch-thumb { transform: translateX(8px); }
15
- &:focus-visible { outline: 2px solid ${c.bgBrandSolid}; outline-offset: 2px; }
16
- &:disabled { opacity: 0.5; cursor: not-allowed; }
17
- `,
18
- variants: {},
19
- defaultVariants: {},
20
- compoundVariants: [],
21
- });
22
- export const switchRecipe = Object.assign(resolveRecipe(config), {
23
- config,
8
+ base: {
9
+ display: "inline-flex",
10
+ alignItems: "center",
11
+ flexShrink: 0,
12
+ width: "24px",
13
+ height: "16px",
14
+ padding: 0,
15
+ margin: 0,
16
+ border: "none",
17
+ borderRadius: c.radius.full,
18
+ background: `color-mix(in srgb, ${c.fgPrimary} 9%, transparent)`,
19
+ cursor: "pointer",
20
+ transition: "background-color .2s ease",
21
+ WebkitTapHighlightColor: "transparent",
22
+ },
23
+ parts: [
24
+ {
25
+ slot: "thumb",
26
+ selector: "& .crisp-switch-thumb",
27
+ decls: {
28
+ width: "12px",
29
+ height: "12px",
30
+ margin: "0 2px",
31
+ borderRadius: c.radius.full,
32
+ background: "#fff",
33
+ transition: "transform .2s ease",
34
+ },
35
+ },
36
+ { selector: "&[data-checked]", decls: { background: c.bgBrandSolid } },
37
+ {
38
+ slot: "thumb",
39
+ selector: "&[data-checked] .crisp-switch-thumb",
40
+ decls: { transform: "translateX(8px)" },
41
+ },
42
+ {
43
+ selector: "&:focus-visible",
44
+ decls: {
45
+ outline: `2px solid ${c.bgBrandSolid}`,
46
+ outlineOffset: "2px",
47
+ },
48
+ },
49
+ { selector: "&:disabled", decls: { opacity: 0.5, cursor: "not-allowed" } },
50
+ ],
24
51
  });
52
+ const config = specToRecipe(spec);
53
+ export const switchRecipe = Object.assign(resolveRecipe(config), { config });
54
+ export { spec };
25
55
  export default config;
@@ -7,6 +7,7 @@ export { placeFloating, } from "./place-floating.js";
7
7
  export { Primitive } from "./primitive.js";
8
8
  export { resolveRecipe } from "./resolve.js";
9
9
  export * from "./rtl.js";
10
+ export { defineSpec, specToRecipe } from "./spec.js";
10
11
  export { useFocusAfterRemoval } from "./use-focus-after-removal.js";
11
12
  export { useFormReset } from "./use-form-reset.js";
12
13
  export { useLabelAssociation } from "./use-label-association.js";
@@ -0,0 +1,66 @@
1
+ const kebab = (p) => p.startsWith("--") ? p : p.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
2
+ /** `{ flexShrink: 0 }` -> `flex-shrink: 0;`, in insertion order. */
3
+ export function declsToCss(decls) {
4
+ return Object.entries(decls)
5
+ .map(([prop, value]) => `${kebab(prop)}: ${value};`)
6
+ .join(" ");
7
+ }
8
+ /**
9
+ * `const` type parameter, not a plain `ComponentSpec` return: widening the
10
+ * variant keys here loses the literal union that `VariantProps<typeof recipe>`
11
+ * hands a component, and `BadgeProps["tone"]` silently degrades from
12
+ * `"neutral" | "brand" | …` to `string`. That is a public API regression that
13
+ * typechecks everywhere except at the call sites it was meant to protect.
14
+ */
15
+ export function defineSpec(spec) {
16
+ return spec;
17
+ }
18
+ /**
19
+ * Compile a spec into the RecipeConfig the existing pipeline already takes, so
20
+ * nothing downstream — the CSS generator, the d.ts generator, `resolveRecipe`,
21
+ * the spec guards — has to know a spec exists. That is what keeps the
22
+ * conversion provable: `dist/` must not move, and CI's
23
+ * `git diff --exit-code dist/` is the gate.
24
+ *
25
+ * The formatting here is not cosmetic freedom. It reproduces exactly what the
26
+ * hand-written template strings emit — root declarations space-joined, each
27
+ * nested rule on its own line indented four spaces — because any other choice
28
+ * rewrites every committed stylesheet and buries the conversion's real diff.
29
+ */
30
+ export function specToRecipe(spec) {
31
+ const root = declsToCss(spec.base);
32
+ const nested = (spec.parts ?? []).map((p) => `${p.selector} { ${declsToCss(p.decls)} }`);
33
+ const base = nested.length ? `${root}\n ${nested.join("\n ")}` : root;
34
+ // A variant value compiles the same way the root does: declarations first,
35
+ // then any nested rules it contributes, so a variant that restyles a part
36
+ // (Badge's solid dot) stays in the variant rather than leaking into base.
37
+ const compileValue = (v) => {
38
+ const decls = "decls" in v ? v.decls : v;
39
+ const nestedParts = "decls" in v ? (v.parts ?? []) : [];
40
+ const head = declsToCss(decls);
41
+ if (!nestedParts.length)
42
+ return head;
43
+ const tail = nestedParts
44
+ .map((p) => `${p.selector} { ${declsToCss(p.decls)} }`)
45
+ .join("\n ");
46
+ return head ? `${head}\n ${tail}` : tail;
47
+ };
48
+ const variants = {};
49
+ for (const [name, values] of Object.entries(spec.variants ?? {})) {
50
+ variants[name] = {};
51
+ for (const [value, v] of Object.entries(values)) {
52
+ variants[name][value] = compileValue(v);
53
+ }
54
+ }
55
+ return {
56
+ name: spec.name,
57
+ base,
58
+ variants: variants,
59
+ defaultVariants: (spec.defaultVariants ?? {}),
60
+ compoundVariants: (spec.compoundVariants ?? []).map(({ decls, ...sel }) => ({
61
+ ...sel,
62
+ css: declsToCss(decls),
63
+ })),
64
+ ...(spec.globalCss ? { globalCss: spec.globalCss } : {}),
65
+ };
66
+ }