@sableui/react 0.14.0 → 0.16.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 (163) hide show
  1. package/CHANGELOG.md +131 -0
  2. package/DESIGN.md +3 -1
  3. package/bin/sable-lint.mjs +5 -1
  4. package/dist/{_contract-CkqXDVww.d.ts → _contract-CDa9-NdB.d.ts} +47 -1
  5. package/dist/blocks/index.d.ts +12 -1
  6. package/dist/blocks/index.js +4 -0
  7. package/dist/blocks/message-reactions-block.js +87 -0
  8. package/dist/blocks/message-reactions-block.meta.js +23 -0
  9. package/dist/blocks/wallet-block.js +2 -2
  10. package/dist/components/app-shell/app-shell-frame.js +16 -0
  11. package/dist/components/app-shell/app-shell-main.js +12 -0
  12. package/dist/components/app-shell/app-shell.js +28 -4
  13. package/dist/components/app-shell/app-shell.meta.js +88 -4
  14. package/dist/components/area-chart/area-chart.meta.js +6 -1
  15. package/dist/components/attachment/attachment.meta.js +2 -1
  16. package/dist/components/attachment/attachment.variants.js +1 -1
  17. package/dist/components/badge/badge.meta.js +8 -1
  18. package/dist/components/badge/badge.variants.js +7 -7
  19. package/dist/components/banner/banner.meta.js +6 -1
  20. package/dist/components/banner/banner.variants.js +5 -5
  21. package/dist/components/bar-chart/bar-chart.meta.js +6 -1
  22. package/dist/components/bubble/bubble.js +105 -5
  23. package/dist/components/bubble/bubble.meta.js +42 -6
  24. package/dist/components/bubble/bubble.variants.js +1 -1
  25. package/dist/components/bubble/index.js +2 -0
  26. package/dist/components/caret-menu/caret-menu.js +5 -6
  27. package/dist/components/chat-composer/chat-composer.meta.js +2 -2
  28. package/dist/components/chat-composer/chat-composer.variants.js +1 -1
  29. package/dist/components/color-field/color-field.js +6 -8
  30. package/dist/components/combobox/combobox.js +27 -5
  31. package/dist/components/combobox/combobox.meta.js +10 -2
  32. package/dist/components/command-menu/command-menu.js +14 -3
  33. package/dist/components/command-menu/command-menu.meta.js +9 -1
  34. package/dist/components/date-picker/date-picker.meta.js +2 -2
  35. package/dist/components/feedback/feedback.js +9 -2
  36. package/dist/components/feedback/feedback.meta.js +8 -1
  37. package/dist/components/gradient-art/gradient-art.js +8 -5
  38. package/dist/components/gradient-art/gradient-art.meta.js +35 -2
  39. package/dist/components/input/input.meta.js +11 -3
  40. package/dist/components/input-group/input-group.js +1 -1
  41. package/dist/components/input-group/input-group.meta.js +1 -1
  42. package/dist/components/like-button/index.js +7 -0
  43. package/dist/components/like-button/like-button.js +89 -0
  44. package/dist/components/like-button/like-button.meta.js +132 -0
  45. package/dist/components/line-chart/line-chart.meta.js +6 -1
  46. package/dist/components/menu/menu.js +1 -1
  47. package/dist/components/meter/meter.js +53 -7
  48. package/dist/components/meter/meter.meta.js +51 -4
  49. package/dist/components/meter/meter.variants.js +43 -7
  50. package/dist/components/note/note.meta.js +5 -1
  51. package/dist/components/note/note.variants.js +4 -4
  52. package/dist/components/number-field/number-field.meta.js +2 -2
  53. package/dist/components/otp-field/otp-field.meta.js +2 -2
  54. package/dist/components/panel-group/panel-group.js +5 -8
  55. package/dist/components/pie-chart/pie-chart.meta.js +6 -1
  56. package/dist/components/radar-chart/radar-chart.meta.js +6 -1
  57. package/dist/components/radial-chart/radial-chart.meta.js +6 -1
  58. package/dist/components/reaction-picker/index.js +13 -0
  59. package/dist/components/reaction-picker/reaction-picker.js +104 -0
  60. package/dist/components/reaction-picker/reaction-picker.meta.js +156 -0
  61. package/dist/components/search-input/search-input.meta.js +11 -3
  62. package/dist/components/segmented-bar/segmented-bar.meta.js +8 -1
  63. package/dist/components/segmented-bar/segmented-bar.variants.js +7 -7
  64. package/dist/components/select/select.js +1 -1
  65. package/dist/components/select/select.meta.js +2 -2
  66. package/dist/components/sheet/sheet.js +5 -4
  67. package/dist/components/sidebar/sidebar.js +16 -1
  68. package/dist/components/sidebar/sidebar.meta.js +7 -2
  69. package/dist/components/sparkline/sparkline.meta.js +8 -1
  70. package/dist/components/status-dot/status-dot.meta.js +8 -1
  71. package/dist/components/status-dot/status-dot.variants.js +7 -7
  72. package/dist/components/stepper/stepper.meta.js +2 -1
  73. package/dist/components/stepper/stepper.variants.js +1 -1
  74. package/dist/components/table/table.meta.js +4 -4
  75. package/dist/components/textarea/textarea.js +5 -0
  76. package/dist/components/textarea/textarea.meta.js +3 -3
  77. package/dist/components/toast/toast.js +25 -13
  78. package/dist/components/toast/toast.meta.js +7 -1
  79. package/dist/file-hashes.json +112 -100
  80. package/dist/foundations/accessibility.js +5 -0
  81. package/dist/foundations/motion.js +21 -1
  82. package/dist/icon-sets/reicon.js +11 -1
  83. package/dist/index.d.ts +425 -84
  84. package/dist/index.js +11 -1
  85. package/dist/lib/chart/types.js +8 -8
  86. package/dist/lib/chart/use-arrival.js +5 -4
  87. package/dist/lib/field-shell.js +1 -1
  88. package/dist/lib/highlight-glide.js +6 -6
  89. package/dist/lib/scrub-gesture.js +6 -6
  90. package/dist/lib/taxonomy.js +2 -0
  91. package/dist/lib/use-caret-trigger.js +11 -18
  92. package/dist/lib/use-match-highlight.js +69 -0
  93. package/dist/lib/use-smooth-wheel.js +10 -8
  94. package/dist/motion/Burst.js +79 -0
  95. package/dist/motion/index.js +2 -0
  96. package/dist/presets/index.d.ts +13 -2
  97. package/dist/presets/index.js +17 -1
  98. package/dist/styles.css +241 -0
  99. package/dist/{theme-provider-CdSicAad.d.ts → theme-provider-BydfM5Z0.d.ts} +1 -1
  100. package/dist/themes/_contract.js +14 -0
  101. package/dist/themes/atelier/atelier.css +18 -0
  102. package/dist/themes/atelier/index.d.ts +1 -1
  103. package/dist/themes/contrast.js +48 -1
  104. package/dist/themes/ember/ember.css +18 -0
  105. package/dist/themes/ember/index.d.ts +1 -1
  106. package/dist/themes/index.d.ts +2 -2
  107. package/dist/themes/meridian/index.d.ts +1 -1
  108. package/dist/themes/meridian/meridian.css +18 -0
  109. package/dist/themes/poppy/index.d.ts +1 -1
  110. package/dist/themes/poppy/poppy.css +18 -0
  111. package/dist/themes/prism/index.d.ts +1 -1
  112. package/dist/themes/prism/prism.css +18 -0
  113. package/dist/themes/sable/index.d.ts +1 -1
  114. package/dist/themes/sable/sable.css +18 -0
  115. package/dist/themes/verdant/index.d.ts +1 -1
  116. package/dist/themes/verdant/verdant.css +18 -0
  117. package/llms/blocks/message-reactions-block.md +116 -0
  118. package/llms/blocks/wallet-block.md +5 -4
  119. package/llms/components/app-shell.md +72 -3
  120. package/llms/components/area-chart.md +1 -1
  121. package/llms/components/attachment.md +1 -1
  122. package/llms/components/badge.md +1 -1
  123. package/llms/components/banner.md +1 -1
  124. package/llms/components/bar-chart.md +1 -1
  125. package/llms/components/bubble.md +17 -7
  126. package/llms/components/chat-composer.md +1 -1
  127. package/llms/components/combobox.md +2 -1
  128. package/llms/components/command-menu.md +3 -2
  129. package/llms/components/date-picker.md +1 -1
  130. package/llms/components/feedback.md +2 -1
  131. package/llms/components/gradient-art.md +28 -1
  132. package/llms/components/input-group.md +1 -1
  133. package/llms/components/input.md +4 -2
  134. package/llms/components/like-button.md +91 -0
  135. package/llms/components/line-chart.md +1 -1
  136. package/llms/components/meter.md +33 -4
  137. package/llms/components/note.md +1 -1
  138. package/llms/components/number-field.md +1 -1
  139. package/llms/components/otp-field.md +1 -1
  140. package/llms/components/pie-chart.md +1 -1
  141. package/llms/components/radar-chart.md +1 -1
  142. package/llms/components/radial-chart.md +1 -1
  143. package/llms/components/reaction-picker.md +115 -0
  144. package/llms/components/search-input.md +20 -2
  145. package/llms/components/segmented-bar.md +1 -1
  146. package/llms/components/select.md +1 -1
  147. package/llms/components/sidebar.md +3 -2
  148. package/llms/components/sparkline.md +1 -1
  149. package/llms/components/status-dot.md +1 -1
  150. package/llms/components/stepper.md +1 -1
  151. package/llms/components/table.md +2 -2
  152. package/llms/components/textarea.md +2 -2
  153. package/llms/components/toast.md +2 -1
  154. package/llms/foundations/accessibility.md +1 -0
  155. package/llms/foundations/motion.md +18 -2
  156. package/llms/reference/token-intent.md +22 -2
  157. package/llms/reference/utilities.md +9 -3
  158. package/llms-full.txt +596 -61
  159. package/llms.txt +10 -7
  160. package/package.json +1 -1
  161. package/registry.json +968 -68
  162. package/scripts/lint-rules.mjs +6 -3
  163. package/targets/ios.json +18 -0
package/dist/index.js CHANGED
@@ -18,6 +18,9 @@ import {
18
18
  DENSITY_TOKENS,
19
19
  RHYTHM_TOKENS,
20
20
  WORKSPACE_SURFACE_TOKENS,
21
+ FIELD_TOKENS,
22
+ APP_SHELL_TOKENS,
23
+ MARK_TOKENS,
21
24
  ART_TOKENS,
22
25
  PALETTE_TOKENS,
23
26
  TYPE_UI_TOKENS
@@ -44,6 +47,7 @@ export * from "./motion/index.js";
44
47
  export * from "./components/button/index.js";
45
48
  export * from "./components/button-group/index.js";
46
49
  export * from "./components/confirm-button/index.js";
50
+ export * from "./components/like-button/index.js";
47
51
  export * from "./components/card/index.js";
48
52
  export * from "./components/caret-menu/index.js";
49
53
  export * from "./components/badge/index.js";
@@ -163,11 +167,13 @@ export * from "./components/chat-composer/index.js";
163
167
  export * from "./components/settings-menu/index.js";
164
168
  export * from "./components/message/index.js";
165
169
  export * from "./components/bubble/index.js";
170
+ export * from "./components/reaction-picker/index.js";
166
171
  export * from "./components/attachment/index.js";
167
172
  export * from "./components/marker/index.js";
168
173
  import { Density, DensityProvider, useDensity, useAnchoredDensity, useTypeTier } from "./lib/density.js";
169
174
  import { PresentationProvider, usePresentation } from "./lib/presentation.js";
170
175
  import { useScrollActivity } from "./lib/use-scroll-activity.js";
176
+ import { useMatchHighlight } from "./lib/use-match-highlight.js";
171
177
  import { useCopy } from "./lib/use-copy.js";
172
178
  import { useSmoothWheel } from "./lib/use-smooth-wheel.js";
173
179
  import {
@@ -175,8 +181,9 @@ import {
175
181
  } from "./lib/use-caret-trigger.js";
176
182
  import { STATUS_TONES, STATUS_TONE_ICON } from "./lib/status-tone.js";
177
183
  import { PALETTE_COLORS, PALETTE_FAMILIES, PALETTE_SCALES, PALETTE_STEPS, PALETTE_TONES, isPaletteColor } from "./lib/palette.js";
178
- var SABLE_UI_VERSION = "0.14.0";
184
+ var SABLE_UI_VERSION = "0.16.0";
179
185
  export {
186
+ APP_SHELL_TOKENS,
180
187
  ART_TOKENS,
181
188
  CHART_SERIES_COUNT,
182
189
  COMPONENT_GROUPS,
@@ -185,6 +192,8 @@ export {
185
192
  DENSITY_TOKENS,
186
193
  Density,
187
194
  DensityProvider,
195
+ FIELD_TOKENS,
196
+ MARK_TOKENS,
188
197
  MOTION_TOKENS,
189
198
  PALETTE_COLORS,
190
199
  PALETTE_FAMILIES,
@@ -229,6 +238,7 @@ export {
229
238
  useCaretTrigger,
230
239
  useCopy,
231
240
  useDensity,
241
+ useMatchHighlight,
232
242
  usePresentation,
233
243
  useSableTheme,
234
244
  useScrollActivity,
@@ -4,18 +4,18 @@ function chartColor(index) {
4
4
  return `var(--chart-${index % CHART_SERIES_COUNT + 1})`;
5
5
  }
6
6
  var TONE_VAR = {
7
- "1": "var(--tone-1)",
8
- "2": "var(--tone-2)",
9
- "3": "var(--tone-3)",
10
- info: "var(--tone-info)",
11
- warning: "var(--tone-warning)",
7
+ "1": "var(--tone-1-mark)",
8
+ "2": "var(--tone-2-mark)",
9
+ "3": "var(--tone-3-mark)",
10
+ info: "var(--tone-info-mark)",
11
+ warning: "var(--tone-warning-mark)",
12
12
  primary: "var(--primary)",
13
13
  neutral: "var(--muted-foreground)",
14
- success: "var(--success)",
15
- destructive: "var(--destructive)"
14
+ success: "var(--success-mark)",
15
+ destructive: "var(--destructive-mark)"
16
16
  };
17
17
  function toneColor(tone) {
18
- return `oklch(from ${TONE_VAR[tone]} clamp(0.5, calc(l + 0.1), 0.72) max(calc(c * 1.1), 0.1) h)`;
18
+ return `oklch(from ${TONE_VAR[tone]} clamp(0.5, l, 0.78) max(c, 0.1) h)`;
19
19
  }
20
20
  function resolveSeries(series) {
21
21
  return series.map((s, i) => ({
@@ -25,13 +25,14 @@ function useArrival() {
25
25
  cancelAnimationFrame(frame);
26
26
  frame = requestAnimationFrame(() => onScreen() && setArrived(true));
27
27
  };
28
- window.addEventListener("scroll", check, { passive: true, capture: true });
29
- window.addEventListener("resize", check, { passive: true });
28
+ const listeners = new AbortController();
29
+ const { signal } = listeners;
30
+ window.addEventListener("scroll", check, { passive: true, capture: true, signal });
31
+ window.addEventListener("resize", check, { passive: true, signal });
30
32
  return () => {
31
33
  observer.disconnect();
32
34
  cancelAnimationFrame(frame);
33
- window.removeEventListener("scroll", check, { capture: true });
34
- window.removeEventListener("resize", check);
35
+ listeners.abort();
35
36
  };
36
37
  }, [el, arrived]);
37
38
  return [ref, arrived];
@@ -1,5 +1,5 @@
1
1
  // src/lib/field-shell.ts
2
- var FIELD_SHELL = "rounded-sm border border-input bg-card motion-interactive placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50";
2
+ var FIELD_SHELL = "rounded-sm border border-field-border bg-field motion-interactive placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50";
3
3
  var MOBILE_FIELD_TIER = "max-sm:text-body-lg";
4
4
  var FIELD_INVALID = "aria-[invalid=true]:border-destructive aria-[invalid=true]:ring-destructive/30";
5
5
  var FIELD_SHELL_XS = "rounded-[var(--panel-field-radius,var(--radius-xs))] border-transparent bg-panel-field hover:bg-panel-field-hover";
@@ -95,16 +95,16 @@ function HighlightGlide({
95
95
  mutations.observe(host, { attributes: true, attributeFilter: [follow], subtree: true, childList: true });
96
96
  const resize = new ResizeObserver(place);
97
97
  resize.observe(host);
98
- host.addEventListener("scroll", place, true);
99
- host.addEventListener("transitionend", place);
100
- host.addEventListener("animationend", place);
98
+ const listeners = new AbortController();
99
+ const { signal } = listeners;
100
+ host.addEventListener("scroll", place, { capture: true, signal });
101
+ host.addEventListener("transitionend", place, { signal });
102
+ host.addEventListener("animationend", place, { signal });
101
103
  return () => {
102
104
  cancelAnimationFrame(frame);
103
105
  mutations.disconnect();
104
106
  resize.disconnect();
105
- host.removeEventListener("scroll", place, true);
106
- host.removeEventListener("transitionend", place);
107
- host.removeEventListener("animationend", place);
107
+ listeners.abort();
108
108
  host.removeAttribute("data-glide");
109
109
  };
110
110
  }, [follow, timing]);
@@ -15,6 +15,8 @@ function beginScrub(e, {
15
15
  const startValue = value;
16
16
  const target = e.currentTarget;
17
17
  let moved = false;
18
+ const gesture = new AbortController();
19
+ const { signal } = gesture;
18
20
  function onMove(ev) {
19
21
  const dx = ev.clientX - startX;
20
22
  if (!moved) {
@@ -37,13 +39,11 @@ function beginScrub(e, {
37
39
  { capture: true, once: true }
38
40
  );
39
41
  }
40
- window.removeEventListener("pointermove", onMove);
41
- window.removeEventListener("pointerup", onUp);
42
- window.removeEventListener("pointercancel", onUp);
42
+ gesture.abort();
43
43
  }
44
- window.addEventListener("pointermove", onMove);
45
- window.addEventListener("pointerup", onUp);
46
- window.addEventListener("pointercancel", onUp);
44
+ window.addEventListener("pointermove", onMove, { signal });
45
+ window.addEventListener("pointerup", onUp, { signal });
46
+ window.addEventListener("pointercancel", onUp, { signal });
47
47
  }
48
48
  export {
49
49
  beginScrub
@@ -26,8 +26,10 @@ var COMPONENT_TAXONOMY = {
26
26
  Button: ["Actions", "either"],
27
27
  ButtonGroup: ["Actions", "either"],
28
28
  ConfirmButton: ["Actions", "either"],
29
+ LikeButton: ["Actions", "either"],
29
30
  Menubar: ["Actions", "either"],
30
31
  OverflowMenu: ["Actions", "either"],
32
+ ReactionPicker: ["Actions", "either"],
31
33
  Toolbar: ["Actions", "either"],
32
34
  /* ── Forms ────────────────────────────────────────────────────────────────
33
35
  Typing or entering a value, and the structure a form is built from.
@@ -36,24 +36,17 @@ function useCaretTrigger({ triggers }) {
36
36
  const onInput = (event) => {
37
37
  if (event.target === field) evaluate();
38
38
  };
39
- doc.addEventListener("input", onInput);
40
- field.addEventListener("focus", evaluate);
41
- field.addEventListener("click", evaluate);
42
- field.addEventListener("keyup", evaluate);
43
- field.addEventListener("compositionstart", onCompositionStart);
44
- field.addEventListener("compositionend", onCompositionEnd);
45
- field.addEventListener("blur", onBlur);
46
- doc.addEventListener("selectionchange", onSelection);
47
- return () => {
48
- doc.removeEventListener("input", onInput);
49
- field.removeEventListener("focus", evaluate);
50
- field.removeEventListener("click", evaluate);
51
- field.removeEventListener("keyup", evaluate);
52
- field.removeEventListener("compositionstart", onCompositionStart);
53
- field.removeEventListener("compositionend", onCompositionEnd);
54
- field.removeEventListener("blur", onBlur);
55
- doc.removeEventListener("selectionchange", onSelection);
56
- };
39
+ const listeners = new AbortController();
40
+ const { signal } = listeners;
41
+ doc.addEventListener("input", onInput, { signal });
42
+ field.addEventListener("focus", evaluate, { signal });
43
+ field.addEventListener("click", evaluate, { signal });
44
+ field.addEventListener("keyup", evaluate, { signal });
45
+ field.addEventListener("compositionstart", onCompositionStart, { signal });
46
+ field.addEventListener("compositionend", onCompositionEnd, { signal });
47
+ field.addEventListener("blur", onBlur, { signal });
48
+ doc.addEventListener("selectionchange", onSelection, { signal });
49
+ return () => listeners.abort();
57
50
  }, [field, evaluate]);
58
51
  const dismiss = useCallback(() => {
59
52
  setMatch((current) => {
@@ -0,0 +1,69 @@
1
+ "use client";
2
+
3
+ // src/lib/use-match-highlight.ts
4
+ import { useEffect, useState } from "react";
5
+ function useMatchHighlight(query, options = {}) {
6
+ const { selector, enabled = true } = options;
7
+ const [element, setElement] = useState(null);
8
+ useEffect(() => {
9
+ const highlight = enabled && element ? sharedHighlight() : null;
10
+ if (!highlight || !element) return;
11
+ let ranges = [];
12
+ const paint = () => {
13
+ for (const range of ranges) highlight.delete(range);
14
+ ranges = matchRanges(element, query, selector);
15
+ for (const range of ranges) highlight.add(range);
16
+ };
17
+ paint();
18
+ let frame = 0;
19
+ const observer = new MutationObserver(() => {
20
+ if (frame) return;
21
+ frame = requestAnimationFrame(() => {
22
+ frame = 0;
23
+ paint();
24
+ });
25
+ });
26
+ observer.observe(element, { subtree: true, childList: true, characterData: true });
27
+ return () => {
28
+ observer.disconnect();
29
+ cancelAnimationFrame(frame);
30
+ for (const range of ranges) highlight.delete(range);
31
+ };
32
+ }, [element, query, selector, enabled]);
33
+ return setElement;
34
+ }
35
+ var MATCH_HIGHLIGHT = "sable-match";
36
+ var shared = null;
37
+ function sharedHighlight() {
38
+ if (typeof CSS === "undefined" || !("highlights" in CSS) || typeof Highlight === "undefined") {
39
+ return null;
40
+ }
41
+ shared ??= new Highlight();
42
+ if (CSS.highlights.get(MATCH_HIGHLIGHT) !== shared) CSS.highlights.set(MATCH_HIGHLIGHT, shared);
43
+ return shared;
44
+ }
45
+ function matchRanges(root, query, selector) {
46
+ const needle = query.trim().toLocaleLowerCase();
47
+ if (!needle) return [];
48
+ const scopes = selector ? Array.from(root.querySelectorAll(selector)) : [root];
49
+ const ranges = [];
50
+ for (const scope of scopes) {
51
+ const walker = document.createTreeWalker(scope, NodeFilter.SHOW_TEXT);
52
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) {
53
+ const haystack = node.data.toLocaleLowerCase();
54
+ if (haystack.length !== node.data.length) continue;
55
+ for (let at = haystack.indexOf(needle); at !== -1; at = haystack.indexOf(needle, at + needle.length)) {
56
+ const range = document.createRange();
57
+ range.setStart(node, at);
58
+ range.setEnd(node, at + needle.length);
59
+ ranges.push(range);
60
+ }
61
+ }
62
+ }
63
+ return ranges;
64
+ }
65
+ export {
66
+ MATCH_HIGHLIGHT,
67
+ matchRanges,
68
+ useMatchHighlight
69
+ };
@@ -112,21 +112,23 @@ function useSmoothWheel({
112
112
  const activeRef = useRef(active);
113
113
  activeRef.current = active;
114
114
  const attached = useRef(null);
115
+ const listeners = useRef(null);
115
116
  const sync = useCallback(() => {
116
117
  const want = activeRef.current ? nodeRef.current : null;
117
118
  if (attached.current === want) return;
118
- const was = attached.current;
119
- if (was) {
120
- was.removeEventListener("wheel", onWheel);
121
- was.removeEventListener("scroll", onScroll);
122
- was.removeEventListener("pointerdown", stop);
119
+ if (attached.current) {
120
+ listeners.current?.abort();
121
+ listeners.current = null;
123
122
  stop();
124
123
  }
125
124
  attached.current = want;
126
125
  if (want) {
127
- want.addEventListener("wheel", onWheel, { passive: false });
128
- want.addEventListener("scroll", onScroll, { passive: true });
129
- want.addEventListener("pointerdown", stop, { passive: true });
126
+ const controller = new AbortController();
127
+ listeners.current = controller;
128
+ const { signal } = controller;
129
+ want.addEventListener("wheel", onWheel, { passive: false, signal });
130
+ want.addEventListener("scroll", onScroll, { passive: true, signal });
131
+ want.addEventListener("pointerdown", stop, { passive: true, signal });
130
132
  }
131
133
  }, [onWheel, onScroll, stop]);
132
134
  useEffect(sync, [active, sync]);
@@ -0,0 +1,79 @@
1
+ "use client";
2
+
3
+ // src/motion/Burst.tsx
4
+ import { useRef, useState } from "react";
5
+ import { cn } from "../lib/cn.js";
6
+ import { motionAttribute, useMotionLevel } from "./MotionProvider.js";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ var DEFAULT_PALETTE = ["pink", "purple", "blue", "orange"];
9
+ function Burst({
10
+ trigger,
11
+ palette = DEFAULT_PALETTE,
12
+ count = 7,
13
+ particle,
14
+ playOnMount = false,
15
+ className,
16
+ motion
17
+ }) {
18
+ const { expressive } = useMotionLevel(motion);
19
+ const [last, setLast] = useState(trigger);
20
+ const [shot, setShot] = useState(playOnMount ? 1 : 0);
21
+ if (!Object.is(trigger, last)) {
22
+ setLast(trigger);
23
+ setShot((n) => n + 1);
24
+ }
25
+ const [done, setDone] = useState(0);
26
+ const landed = useRef({ shot: 0, pieces: 0 });
27
+ if (shot === 0 || shot === done || !expressive || palette.length === 0) return null;
28
+ const places = Math.max(1, Math.round(count));
29
+ const onAnimationEnd = (event) => {
30
+ if (!event.target.classList.contains("sable-burst-dot")) return;
31
+ if (landed.current.shot !== shot) landed.current = { shot, pieces: 0 };
32
+ landed.current.pieces += 1;
33
+ if (landed.current.pieces >= places * 2) setDone(shot);
34
+ };
35
+ const step = 360 / places;
36
+ const scale = particle === void 0 ? 1 : 2.5;
37
+ return /* @__PURE__ */ jsxs(
38
+ "span",
39
+ {
40
+ "aria-hidden": true,
41
+ "data-motion": motionAttribute(motion),
42
+ className: cn("sable-burst [--burst-size:2.25em]", className),
43
+ onAnimationEnd,
44
+ children: [
45
+ particle === void 0 ? /* @__PURE__ */ jsx("span", { className: "sable-burst-ring", "data-palette": palette[0] }) : null,
46
+ Array.from({ length: places }, (_, i) => {
47
+ const angle = i * step - 90;
48
+ return [
49
+ { angle, reach: 0.95, size: `calc(var(--burst-size) * ${0.14 * scale})`, family: palette[i % palette.length] },
50
+ {
51
+ angle: angle + step / 3,
52
+ reach: 0.75,
53
+ size: `calc(var(--burst-size) * ${0.1 * scale})`,
54
+ family: palette[(i + 1) % palette.length]
55
+ }
56
+ ].map((dot, j) => /* @__PURE__ */ jsx(
57
+ "span",
58
+ {
59
+ className: "sable-burst-dot",
60
+ "data-palette": particle === void 0 ? dot.family : void 0,
61
+ "data-particle": particle === void 0 ? void 0 : "",
62
+ style: {
63
+ "--burst-dot-angle": `${dot.angle}deg`,
64
+ "--burst-dot-reach": dot.reach,
65
+ "--burst-dot-size": dot.size
66
+ },
67
+ children: particle
68
+ },
69
+ `${i}-${j}`
70
+ ));
71
+ })
72
+ ]
73
+ },
74
+ shot
75
+ );
76
+ }
77
+ export {
78
+ Burst
79
+ };
@@ -13,12 +13,14 @@ import { useInView } from "./useInView.js";
13
13
  import { Reveal } from "./Reveal.js";
14
14
  import { TextReveal } from "./TextReveal.js";
15
15
  import { Presence } from "./Presence.js";
16
+ import { Burst } from "./Burst.js";
16
17
  import {
17
18
  startViewTransition,
18
19
  supportsViewTransitions,
19
20
  ViewTransitionName
20
21
  } from "./viewTransition.js";
21
22
  export {
23
+ Burst,
22
24
  MotionProvider,
23
25
  MotionScope,
24
26
  Presence,
@@ -1,4 +1,4 @@
1
- import { T as ThemeManifest } from '../_contract-CkqXDVww.js';
1
+ import { T as ThemeManifest } from '../_contract-CDa9-NdB.js';
2
2
 
3
3
  /** An OKLCH colour, lightness 0–1. Extra fields (alpha) ride along untouched. */
4
4
  interface OklchColor {
@@ -31,6 +31,17 @@ declare function fitFillToInk<T extends OklchColor>(fill: T, ink: OklchColor): T
31
31
  * once, on the colour as picked, then `fitFillToInk` each mode's fill to it.
32
32
  */
33
33
  declare function inkPolarity<T extends OklchColor>(color: OklchColor, inks: readonly T[]): T;
34
+ /** A mark's floor against the surfaces it sits on: visibility, not text contrast. */
35
+ declare const MARK_FLOOR = 2;
36
+ /**
37
+ * The mark for a fill: its hue at graphic strength. In light mode the fill is
38
+ * lifted to its hue's mark lightness and its chroma raised 1.3× (never past the
39
+ * gamut, never below its own); in dark mode, where fills are already light, a
40
+ * red is lifted a little and every hue gains 1.15×. A grey tone stays grey.
41
+ * Then it steps back toward the ink until it clears MARK_FLOOR on every surface
42
+ * given, so a pale amber never vanishes into a cream page.
43
+ */
44
+ declare function markFor<T extends OklchColor>(fill: T, mode: "light" | "dark", surfaces?: readonly OklchColor[]): T;
34
45
  /** Fills a theme or preset authors from its brand. Their ink follows the colour as picked. */
35
46
  declare const BRAND_FILLS: readonly ["primary", "sidebar-primary", "highlight"];
36
47
  /**
@@ -143,4 +154,4 @@ declare function presetLabelContrast(input: SablePreset): {
143
154
  unmeasured: string[];
144
155
  };
145
156
 
146
- export { BRAND_FILLS, LABELLED_FILLS, LABEL_CONTRAST_RULE, type LabelFinding, type OklchColor, PAGE_WIDTH_TOKENS, PRESET_TOKEN_NAMES, type PresetException, STATUS_FILLS, type SablePreset, checkLabelledFills, fitFillToInk, inkFor, inkPolarity, labelContrast, parsePreset, presetCss, presetLabelContrast, presetManifest, validatePreset };
157
+ export { BRAND_FILLS, LABELLED_FILLS, LABEL_CONTRAST_RULE, type LabelFinding, MARK_FLOOR, type OklchColor, PAGE_WIDTH_TOKENS, PRESET_TOKEN_NAMES, type PresetException, STATUS_FILLS, type SablePreset, checkLabelledFills, fitFillToInk, inkFor, inkPolarity, labelContrast, markFor, parsePreset, presetCss, presetLabelContrast, presetManifest, validatePreset };
@@ -11,6 +11,9 @@ import {
11
11
  LAYERING_TOKENS,
12
12
  WORKSPACE_SURFACE_TOKENS,
13
13
  DERIVED_EDGE_TOKENS,
14
+ FIELD_TOKENS,
15
+ APP_SHELL_TOKENS,
16
+ MARK_TOKENS,
14
17
  TYPE_UI_TOKENS,
15
18
  ART_TOKENS,
16
19
  PALETTE_TOKENS
@@ -25,7 +28,9 @@ import {
25
28
  fitFillToInk,
26
29
  inkFor,
27
30
  inkPolarity,
28
- labelContrast
31
+ labelContrast,
32
+ MARK_FLOOR,
33
+ markFor
29
34
  } from "../themes/contrast.js";
30
35
  var PAGE_WIDTH_TOKENS = [
31
36
  "page-inset",
@@ -39,11 +44,20 @@ var PRESET_TOKEN_NAMES = [.../* @__PURE__ */ new Set([
39
44
  ...SEMANTIC_TOKENS,
40
45
  ...WORKSPACE_SURFACE_TOKENS.map((name) => `theme-${name}`),
41
46
  ...DERIVED_EDGE_TOKENS.map((name) => `theme-${name}`),
47
+ // The field's fill and outline: optional — a preset without them keeps outlined fields.
48
+ ...FIELD_TOKENS.map((name) => `theme-${name}`),
49
+ // The inset frame's ground: optional — a preset without it keeps the derived step off the rail.
50
+ ...APP_SHELL_TOKENS.map((name) => `theme-${name}`),
51
+ // Graphic-strength status colours: optional — a preset without them derives them from its fills.
52
+ ...MARK_TOKENS.map((name) => `theme-${name}`),
42
53
  // Art direction: optional hooks — a preset without them paints the default art.
43
54
  ...ART_TOKENS.map((name) => `theme-${name}`),
44
55
  ...PALETTE_TOKENS.map((name) => `theme-${name}`),
45
56
  ...WORKSPACE_SURFACE_TOKENS,
46
57
  ...DERIVED_EDGE_TOKENS,
58
+ ...FIELD_TOKENS,
59
+ ...APP_SHELL_TOKENS,
60
+ ...MARK_TOKENS,
47
61
  ...RADIUS_TOKENS,
48
62
  ...MOTION_TOKENS,
49
63
  ...TYPE_SCALE_TOKENS,
@@ -226,6 +240,7 @@ export {
226
240
  BRAND_FILLS,
227
241
  LABELLED_FILLS2 as LABELLED_FILLS,
228
242
  LABEL_CONTRAST_RULE2 as LABEL_CONTRAST_RULE,
243
+ MARK_FLOOR,
229
244
  PAGE_WIDTH_TOKENS,
230
245
  PRESET_TOKEN_NAMES,
231
246
  STATUS_FILLS,
@@ -234,6 +249,7 @@ export {
234
249
  inkFor,
235
250
  inkPolarity,
236
251
  labelContrast,
252
+ markFor,
237
253
  parsePreset,
238
254
  presetCss,
239
255
  presetLabelContrast,