@sableui/react 0.15.0 → 0.17.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 (145) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/bin/sable-lint.mjs +5 -1
  3. package/dist/{_contract-CkqXDVww.d.ts → _contract-CDa9-NdB.d.ts} +47 -1
  4. package/dist/blocks/approval-block.js +30 -48
  5. package/dist/blocks/approval-block.meta.js +7 -8
  6. package/dist/blocks/index.d.ts +1 -1
  7. package/dist/blocks/wallet-block.js +2 -2
  8. package/dist/components/app-shell/app-shell-frame.js +16 -0
  9. package/dist/components/app-shell/app-shell-main.js +12 -0
  10. package/dist/components/app-shell/app-shell.js +28 -4
  11. package/dist/components/app-shell/app-shell.meta.js +88 -4
  12. package/dist/components/area-chart/area-chart.meta.js +6 -1
  13. package/dist/components/attachment/attachment.meta.js +2 -1
  14. package/dist/components/attachment/attachment.variants.js +1 -1
  15. package/dist/components/badge/badge.meta.js +8 -1
  16. package/dist/components/badge/badge.variants.js +7 -7
  17. package/dist/components/banner/banner.meta.js +6 -1
  18. package/dist/components/banner/banner.variants.js +5 -5
  19. package/dist/components/bar-chart/bar-chart.meta.js +6 -1
  20. package/dist/components/caret-menu/caret-menu.js +5 -6
  21. package/dist/components/chat-composer/chat-composer.meta.js +2 -2
  22. package/dist/components/chat-composer/chat-composer.variants.js +1 -1
  23. package/dist/components/color-field/color-field.js +6 -8
  24. package/dist/components/combobox/combobox.js +27 -5
  25. package/dist/components/combobox/combobox.meta.js +10 -2
  26. package/dist/components/command-menu/command-menu.js +14 -3
  27. package/dist/components/command-menu/command-menu.meta.js +9 -1
  28. package/dist/components/date-picker/date-picker.meta.js +2 -2
  29. package/dist/components/feedback/feedback.js +1 -1
  30. package/dist/components/feedback/feedback.meta.js +2 -1
  31. package/dist/components/gradient-art/gradient-art.js +8 -5
  32. package/dist/components/gradient-art/gradient-art.meta.js +35 -2
  33. package/dist/components/input/input.meta.js +11 -3
  34. package/dist/components/input-group/input-group.js +1 -1
  35. package/dist/components/input-group/input-group.meta.js +1 -1
  36. package/dist/components/line-chart/line-chart.meta.js +6 -1
  37. package/dist/components/menu/menu.js +1 -1
  38. package/dist/components/meter/meter.js +53 -7
  39. package/dist/components/meter/meter.meta.js +51 -4
  40. package/dist/components/meter/meter.variants.js +43 -7
  41. package/dist/components/note/note.meta.js +5 -1
  42. package/dist/components/note/note.variants.js +4 -4
  43. package/dist/components/number-field/number-field.meta.js +2 -2
  44. package/dist/components/otp-field/otp-field.meta.js +2 -2
  45. package/dist/components/panel-group/panel-group.js +5 -8
  46. package/dist/components/pie-chart/pie-chart.meta.js +6 -1
  47. package/dist/components/radar-chart/radar-chart.meta.js +6 -1
  48. package/dist/components/radial-chart/radial-chart.meta.js +6 -1
  49. package/dist/components/search-input/search-input.meta.js +11 -3
  50. package/dist/components/section-rail/index.js +7 -0
  51. package/dist/components/section-rail/section-rail.js +153 -0
  52. package/dist/components/section-rail/section-rail.meta.js +189 -0
  53. package/dist/components/segmented-bar/segmented-bar.meta.js +8 -1
  54. package/dist/components/segmented-bar/segmented-bar.variants.js +7 -7
  55. package/dist/components/select/select.js +1 -1
  56. package/dist/components/select/select.meta.js +2 -2
  57. package/dist/components/sheet/sheet.js +5 -4
  58. package/dist/components/sidebar/sidebar.js +16 -1
  59. package/dist/components/sidebar/sidebar.meta.js +7 -2
  60. package/dist/components/sparkline/sparkline.meta.js +8 -1
  61. package/dist/components/status-dot/status-dot.meta.js +8 -1
  62. package/dist/components/status-dot/status-dot.variants.js +7 -7
  63. package/dist/components/stepper/stepper.meta.js +2 -1
  64. package/dist/components/stepper/stepper.variants.js +1 -1
  65. package/dist/components/table/table.meta.js +4 -4
  66. package/dist/components/textarea/textarea.js +5 -0
  67. package/dist/components/textarea/textarea.meta.js +3 -3
  68. package/dist/file-hashes.json +99 -92
  69. package/dist/foundations/accessibility.js +5 -0
  70. package/dist/index.d.ts +227 -25
  71. package/dist/index.js +12 -1
  72. package/dist/lib/chart/types.js +8 -8
  73. package/dist/lib/chart/use-arrival.js +5 -4
  74. package/dist/lib/field-shell.js +1 -1
  75. package/dist/lib/highlight-glide.js +6 -6
  76. package/dist/lib/scrub-gesture.js +6 -6
  77. package/dist/lib/taxonomy.js +1 -0
  78. package/dist/lib/use-active-section.js +59 -0
  79. package/dist/lib/use-caret-trigger.js +11 -18
  80. package/dist/lib/use-match-highlight.js +69 -0
  81. package/dist/lib/use-smooth-wheel.js +10 -8
  82. package/dist/presets/index.d.ts +13 -2
  83. package/dist/presets/index.js +17 -1
  84. package/dist/styles.css +100 -0
  85. package/dist/{theme-provider-CdSicAad.d.ts → theme-provider-BydfM5Z0.d.ts} +1 -1
  86. package/dist/themes/_contract.js +14 -0
  87. package/dist/themes/atelier/atelier.css +18 -0
  88. package/dist/themes/atelier/index.d.ts +1 -1
  89. package/dist/themes/contrast.js +48 -1
  90. package/dist/themes/ember/ember.css +18 -0
  91. package/dist/themes/ember/index.d.ts +1 -1
  92. package/dist/themes/index.d.ts +2 -2
  93. package/dist/themes/meridian/index.d.ts +1 -1
  94. package/dist/themes/meridian/meridian.css +18 -0
  95. package/dist/themes/poppy/index.d.ts +1 -1
  96. package/dist/themes/poppy/poppy.css +18 -0
  97. package/dist/themes/prism/index.d.ts +1 -1
  98. package/dist/themes/prism/prism.css +18 -0
  99. package/dist/themes/sable/index.d.ts +1 -1
  100. package/dist/themes/sable/sable.css +18 -0
  101. package/dist/themes/verdant/index.d.ts +1 -1
  102. package/dist/themes/verdant/verdant.css +18 -0
  103. package/llms/blocks/approval-block.md +40 -55
  104. package/llms/blocks/wallet-block.md +5 -4
  105. package/llms/components/app-shell.md +72 -3
  106. package/llms/components/area-chart.md +1 -1
  107. package/llms/components/attachment.md +1 -1
  108. package/llms/components/badge.md +1 -1
  109. package/llms/components/banner.md +1 -1
  110. package/llms/components/bar-chart.md +1 -1
  111. package/llms/components/chat-composer.md +1 -1
  112. package/llms/components/combobox.md +2 -1
  113. package/llms/components/command-menu.md +3 -2
  114. package/llms/components/date-picker.md +1 -1
  115. package/llms/components/feedback.md +1 -1
  116. package/llms/components/gradient-art.md +28 -1
  117. package/llms/components/input-group.md +1 -1
  118. package/llms/components/input.md +4 -2
  119. package/llms/components/line-chart.md +1 -1
  120. package/llms/components/meter.md +33 -4
  121. package/llms/components/note.md +1 -1
  122. package/llms/components/number-field.md +1 -1
  123. package/llms/components/otp-field.md +1 -1
  124. package/llms/components/pie-chart.md +1 -1
  125. package/llms/components/radar-chart.md +1 -1
  126. package/llms/components/radial-chart.md +1 -1
  127. package/llms/components/search-input.md +20 -2
  128. package/llms/components/section-rail.md +101 -0
  129. package/llms/components/segmented-bar.md +1 -1
  130. package/llms/components/select.md +1 -1
  131. package/llms/components/sidebar.md +3 -2
  132. package/llms/components/sparkline.md +1 -1
  133. package/llms/components/status-dot.md +1 -1
  134. package/llms/components/stepper.md +1 -1
  135. package/llms/components/table.md +2 -2
  136. package/llms/components/textarea.md +2 -2
  137. package/llms/foundations/accessibility.md +1 -0
  138. package/llms/reference/token-intent.md +22 -2
  139. package/llms/reference/utilities.md +5 -2
  140. package/llms-full.txt +367 -105
  141. package/llms.txt +6 -5
  142. package/package.json +1 -1
  143. package/registry.json +609 -67
  144. package/scripts/lint-rules.mjs +6 -3
  145. package/targets/ios.json +9 -0
@@ -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
@@ -90,6 +90,7 @@ var COMPONENT_TAXONOMY = {
90
90
  BottomNav: ["Navigation", "page"],
91
91
  NavigationMenu: ["Navigation", "page"],
92
92
  Pagination: ["Navigation", "either"],
93
+ SectionRail: ["Navigation", "page"],
93
94
  ShowMore: ["Navigation", "page"],
94
95
  SideNav: ["Navigation", "page"],
95
96
  Sidebar: ["Navigation", "page"],
@@ -0,0 +1,59 @@
1
+ "use client";
2
+
3
+ // src/lib/use-active-section.ts
4
+ import { useEffect, useState } from "react";
5
+ var END_SLACK = 2;
6
+ function useActiveSection(ids, { offset = 0.35, root } = {}) {
7
+ const [active, setActive] = useState(ids[0] ?? null);
8
+ const key = ids.join("\n");
9
+ useEffect(() => {
10
+ const list = key ? key.split("\n") : [];
11
+ if (list.length === 0) {
12
+ setActive(null);
13
+ return;
14
+ }
15
+ const scroller = root?.current ?? null;
16
+ let frame = 0;
17
+ const measure = () => {
18
+ frame = 0;
19
+ const top = scroller ? scroller.getBoundingClientRect().top : 0;
20
+ const height = scroller ? scroller.clientHeight : window.innerHeight;
21
+ const line = top + height * offset;
22
+ const bottom = top + height;
23
+ const scrolled = scroller ? scroller.scrollTop : window.scrollY;
24
+ const range = scroller ? scroller.scrollHeight - scroller.clientHeight : document.documentElement.scrollHeight - window.innerHeight;
25
+ const atEnd = range > END_SLACK && scrolled >= range - END_SLACK;
26
+ let current = list[0];
27
+ for (const id of list) {
28
+ const section = document.getElementById(id);
29
+ if (!section) continue;
30
+ const sectionTop = section.getBoundingClientRect().top;
31
+ if (sectionTop <= line || atEnd && sectionTop < bottom) current = id;
32
+ }
33
+ setActive(current);
34
+ };
35
+ const schedule = () => {
36
+ if (!frame) frame = requestAnimationFrame(measure);
37
+ };
38
+ measure();
39
+ const target = scroller ?? window;
40
+ target.addEventListener("scroll", schedule, { passive: true });
41
+ window.addEventListener("resize", schedule);
42
+ const resize = new ResizeObserver(schedule);
43
+ resize.observe(scroller ?? document.documentElement);
44
+ for (const id of list) {
45
+ const section = document.getElementById(id);
46
+ if (section) resize.observe(section);
47
+ }
48
+ return () => {
49
+ cancelAnimationFrame(frame);
50
+ target.removeEventListener("scroll", schedule);
51
+ window.removeEventListener("resize", schedule);
52
+ resize.disconnect();
53
+ };
54
+ }, [key, offset, root]);
55
+ return active;
56
+ }
57
+ export {
58
+ useActiveSection
59
+ };
@@ -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]);
@@ -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,
package/dist/styles.css CHANGED
@@ -86,6 +86,16 @@
86
86
  --color-border-strong: var(--border-strong);
87
87
  --color-border-floating: var(--border-floating);
88
88
  --color-input: var(--input);
89
+ --color-field: var(--field);
90
+ --color-field-border: var(--field-border);
91
+ --color-shell-ground: var(--shell-ground);
92
+ --color-destructive-mark: var(--destructive-mark);
93
+ --color-success-mark: var(--success-mark);
94
+ --color-tone-warning-mark: var(--tone-warning-mark);
95
+ --color-tone-info-mark: var(--tone-info-mark);
96
+ --color-tone-1-mark: var(--tone-1-mark);
97
+ --color-tone-2-mark: var(--tone-2-mark);
98
+ --color-tone-3-mark: var(--tone-3-mark);
89
99
  --color-control-border: var(--control-border);
90
100
  --color-ring: var(--ring);
91
101
 
@@ -378,6 +388,33 @@
378
388
  and reads as a faint lit edge in dark mode, where shadows barely show. */
379
389
  --border-floating: var(--theme-border-floating, color-mix(in oklch, transparent 92%, var(--foreground)));
380
390
 
391
+ /* The text field: its fill and its outline. Outlined on the card by default;
392
+ a theme fills its fields instead with `--theme-field: var(--muted)` and
393
+ `--theme-field-border: transparent`. Every field reads the pair through
394
+ lib/field-shell, so one choice restyles them all. */
395
+ --field: var(--theme-field, var(--card));
396
+ --field-border: var(--theme-field-border, var(--input));
397
+
398
+ /* The ground of an inset app frame (`AppShell frame="inset"`): the rail and
399
+ the gap around the raised main panel. One step off the rail toward its ink,
400
+ because most themes paint the rail the page's colour and the panel would
401
+ otherwise sit on its own colour. */
402
+ --shell-ground: var(--theme-shell-ground, color-mix(in oklch, var(--sidebar) 97%, var(--sidebar-foreground)));
403
+
404
+ /* Status and tone colours at graphic strength: a meter's fill, a segmented
405
+ bar, a status dot, a chart series, a soft tint. The label fill is dark
406
+ enough for white ink; a mark carries no ink, so it is the same hue lifted
407
+ and saturated (MARK_TOKENS in themes/_contract.ts). Every bundled theme
408
+ tunes its own through the hooks; this derivation is the fallback for a
409
+ preset that does not. */
410
+ --destructive-mark: var(--theme-destructive-mark, oklch(from var(--destructive) max(l, 0.62) calc(c * 1.25) h));
411
+ --success-mark: var(--theme-success-mark, oklch(from var(--success) max(l, 0.68) calc(c * 1.25) h));
412
+ --tone-warning-mark: var(--theme-tone-warning-mark, oklch(from var(--tone-warning) max(l, 0.71) calc(c * 1.25) h));
413
+ --tone-info-mark: var(--theme-tone-info-mark, oklch(from var(--tone-info) max(l, 0.63) calc(c * 1.25) h));
414
+ --tone-1-mark: var(--theme-tone-1-mark, oklch(from var(--tone-1) max(l, 0.62) calc(c * 1.25) h));
415
+ --tone-2-mark: var(--theme-tone-2-mark, oklch(from var(--tone-2) max(l, 0.6) calc(c * 1.25) h));
416
+ --tone-3-mark: var(--theme-tone-3-mark, oklch(from var(--tone-3) max(l, 0.66) calc(c * 1.25) h));
417
+
381
418
  /* The Switch knob when OFF: light in BOTH modes, like every physical toggle.
382
419
  It used to be `--background`, which in a dark theme is a near-black knob on
383
420
  a dark track (well under 3:1). */
@@ -848,6 +885,24 @@
848
885
  }
849
886
  }
850
887
 
888
+ /* ==========================================================================
889
+ Search matches — the paint behind `useMatchHighlight` (lib/use-match-highlight),
890
+ which Combobox and CommandMenu run on their rows and a consumer can run on any
891
+ results list. The CSS Custom Highlight API marks text Ranges, so nothing is
892
+ wrapped in a <mark> and React's text nodes stay as rendered.
893
+
894
+ On `highlight` because that token exists for exactly this ("search hits" in
895
+ its intent). A tint of it, not the solid pair: a solid fill behind a few
896
+ letters is what a text selection looks like, and a list where every row
897
+ seems half-selected reads as a bug. The tint leaves the row's own ink, so
898
+ contrast stays the row's. Where the API is missing no ranges are registered
899
+ and this rule matches nothing.
900
+ ========================================================================== */
901
+
902
+ ::highlight(sable-match) {
903
+ background-color: color-mix(in oklab, var(--highlight) 36%, transparent);
904
+ }
905
+
851
906
  /* ==========================================================================
852
907
  UI tier — the WORKSPACE register's chrome typography. A separate ramp from the
853
908
  six page tiers above, on purpose.
@@ -1995,6 +2050,51 @@
1995
2050
  background-position: 0 0;
1996
2051
  }
1997
2052
 
2053
+ /* texture-dither — a faint field of tiny squares that dissolves, the pixel /
2054
+ halftone texture products lay over the top of a content area or behind an
2055
+ empty-state heading. Pure CSS and static: the dots are the element's own
2056
+ background, cut out by a mask of a small pixel tile intersected with a fade.
2057
+
2058
+ The tile is 12×12 cells of one square each on a 2-unit pitch, about 40%
2059
+ filled at random (no runs of three, no 2×2 clumps) and a third of the dots at
2060
+ half strength, so it reads as dither rather than as a grid of dots. It is a
2061
+ MASK, so the SVG carries no colour of its own: the dots paint in
2062
+ `--border-strong`, the structural-divider ink, which every theme tunes for
2063
+ both modes — faint on a light page, a soft grey on a dark one.
2064
+
2065
+ Decoration only: put it on its own absolutely-positioned, aria-hidden element
2066
+ (it sets pointer-events: none but no position, so it never takes layout),
2067
+ first in a `relative isolate` parent and at -z-10, so the content after it
2068
+ paints on top without a z-index of its own:
2069
+
2070
+ <div class="relative isolate">
2071
+ <div aria-hidden class="texture-dither absolute inset-x-0 top-0 -z-10 h-40" />
2072
+ …content…
2073
+ </div>
2074
+
2075
+ Three hooks, all optional, read with fallbacks (the checkerboard pattern):
2076
+ --dither-fade the fade's direction, any linear-gradient side or angle
2077
+ (default `to bottom`: dense at the top, gone at the bottom)
2078
+ --dither-size the tile (default 48px → 2px squares on a 4px pitch)
2079
+ --dither-color the dot ink (default var(--border-strong))
2080
+
2081
+ Not `bg-dither`, for the reason `checkerboard` is not `bg-checkerboard`: a
2082
+ `bg-` name joins tailwind-merge's background group and would evict, or be
2083
+ evicted by, a real colour class. */
2084
+ @utility texture-dither {
2085
+ pointer-events: none;
2086
+ background-color: var(--dither-color, var(--border-strong));
2087
+ --dither-tile: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' shape-rendering='crispEdges'%3E%3Cpath d='M4 0h1v1H4zM8 0h1v1H8zM16 0h1v1H16zM2 2h1v1H2zM6 2h1v1H6zM14 2h1v1H14zM6 4h1v1H6zM10 4h1v1H10zM22 4h1v1H22zM2 6h1v1H2zM8 6h1v1H8zM14 6h1v1H14zM20 6h1v1H20zM2 8h1v1H2zM12 8h1v1H12zM18 8h1v1H18zM20 8h1v1H20zM12 10h1v1H12zM8 12h1v1H8zM2 14h1v1H2zM6 14h1v1H6zM16 14h1v1H16zM18 14h1v1H18zM0 16h1v1H0zM4 16h1v1H4zM6 16h1v1H6zM22 16h1v1H22zM10 18h1v1H10zM12 18h1v1H12zM16 18h1v1H16zM4 20h1v1H4zM20 20h1v1H20zM22 20h1v1H22zM0 22h1v1H0zM8 22h1v1H8zM12 22h1v1H12zM14 22h1v1H14zM20 22h1v1H20z'/%3E%3Cpath fill-opacity='.5' d='M0 0h1v1H0zM22 0h1v1H22zM20 2h1v1H20zM8 8h1v1H8zM14 8h1v1H14zM0 10h1v1H0zM4 10h1v1H4zM16 10h1v1H16zM22 10h1v1H22zM2 12h1v1H2zM4 12h1v1H4zM10 12h1v1H10zM18 12h1v1H18zM10 14h1v1H10zM0 18h1v1H0zM18 18h1v1H18zM10 20h1v1H10zM16 20h1v1H16zM18 22h1v1H18z'/%3E%3C/svg%3E");
2088
+ -webkit-mask-image: var(--dither-tile), linear-gradient(var(--dither-fade, to bottom), rgb(0 0 0), rgb(0 0 0 / 0.45) 35%, rgb(0 0 0 / 0));
2089
+ mask-image: var(--dither-tile), linear-gradient(var(--dither-fade, to bottom), rgb(0 0 0), rgb(0 0 0 / 0.45) 35%, rgb(0 0 0 / 0));
2090
+ -webkit-mask-size: var(--dither-size, 48px) var(--dither-size, 48px), 100% 100%;
2091
+ mask-size: var(--dither-size, 48px) var(--dither-size, 48px), 100% 100%;
2092
+ -webkit-mask-repeat: repeat, no-repeat;
2093
+ mask-repeat: repeat, no-repeat;
2094
+ -webkit-mask-composite: source-in;
2095
+ mask-composite: intersect;
2096
+ }
2097
+
1998
2098
  /* The default transition for interactive chrome — hover/focus/selection colour
1999
2099
  shifts plus the press/slide transforms. The single transition declarer for
2000
2100
  interactive controls: it lists scale + translate too, so `motion-press` and
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { T as ThemeManifest } from './_contract-CkqXDVww.js';
3
+ import { T as ThemeManifest } from './_contract-CDa9-NdB.js';
4
4
 
5
5
  declare const SABLE_THEME_ATTRIBUTE: "data-sable-theme";
6
6
  declare const DEFAULT_THEME_ID: string;
@@ -312,6 +312,17 @@ var PALETTE_SLOT_TOKENS = [
312
312
  "palette-solid-ink"
313
313
  ];
314
314
  var DERIVED_EDGE_TOKENS = ["border-floating"];
315
+ var FIELD_TOKENS = ["field", "field-border"];
316
+ var APP_SHELL_TOKENS = ["shell-ground"];
317
+ var MARK_TOKENS = [
318
+ "destructive-mark",
319
+ "success-mark",
320
+ "tone-warning-mark",
321
+ "tone-info-mark",
322
+ "tone-1-mark",
323
+ "tone-2-mark",
324
+ "tone-3-mark"
325
+ ];
315
326
  var TYPE_UI_TOKENS = [
316
327
  "type-ui-md-size",
317
328
  "type-ui-md-weight",
@@ -323,10 +334,13 @@ var TYPE_UI_TOKENS = [
323
334
  "type-ui-micro-weight"
324
335
  ];
325
336
  export {
337
+ APP_SHELL_TOKENS,
326
338
  ART_TOKENS,
327
339
  DENSITY_TOKENS,
328
340
  DERIVED_EDGE_TOKENS,
341
+ FIELD_TOKENS,
329
342
  LAYERING_TOKENS,
343
+ MARK_TOKENS,
330
344
  MOTION_TOKENS,
331
345
  PALETTE_SLOT_TOKENS,
332
346
  PALETTE_TOKENS,
@@ -128,6 +128,15 @@
128
128
  --tone-1-foreground: #ffffff;
129
129
  --tone-info-foreground: #ffffff;
130
130
  --tone-warning-foreground: #ffffff;
131
+ /* Graphic strength (MARK_TOKENS): meters, bars, dots, chart series and soft tints.
132
+ The same hues lifted and saturated; held to 2:1 on the page, not a text ratio. */
133
+ --theme-destructive-mark: oklch(62.0% 0.234 22.0);
134
+ --theme-success-mark: oklch(70.0% 0.130 173.0);
135
+ --theme-tone-warning-mark: oklch(71.0% 0.156 52.0);
136
+ --theme-tone-info-mark: oklch(63.0% 0.130 250.0);
137
+ --theme-tone-1-mark: oklch(70.0% 0.117 173.0);
138
+ --theme-tone-2-mark: oklch(62.0% 0.143 300.0);
139
+ --theme-tone-3-mark: oklch(60.0% 0.000 0.0);
131
140
  --highlight-foreground: #000000;
132
141
  --overlay: oklch(7% 0 0 / 0.55);
133
142
  --overlay-foreground: #ffffff;
@@ -211,6 +220,15 @@
211
220
  --tone-1-foreground: oklch(13% 0 0);
212
221
  --tone-info-foreground: oklch(13% 0 0);
213
222
  --tone-warning-foreground: oklch(13% 0 0);
223
+ /* Graphic strength (MARK_TOKENS): meters, bars, dots, chart series and soft tints.
224
+ The same hues lifted and saturated; held to 2:1 on the page, not a text ratio. */
225
+ --theme-destructive-mark: oklch(65.0% 0.218 22.0);
226
+ --theme-success-mark: oklch(74.0% 0.137 173.0);
227
+ --theme-tone-warning-mark: oklch(80.0% 0.129 52.0);
228
+ --theme-tone-info-mark: oklch(76.0% 0.114 250.0);
229
+ --theme-tone-1-mark: oklch(76.0% 0.114 173.0);
230
+ --theme-tone-2-mark: oklch(76.0% 0.137 300.0);
231
+ --theme-tone-3-mark: oklch(80.0% 0.000 0.0);
214
232
  --highlight-foreground: #ffffff;
215
233
  --overlay: oklch(0% 0 0 / 0.7);
216
234
  --overlay-foreground: #ffffff;
@@ -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
  /**
4
4
  * Mono Editorial — a Sable theme. Concrete token values live in
@@ -127,7 +127,16 @@ var NON_TEXT_PAIRS = {
127
127
  // The sidebar family repeats the set against its own surface, which is exactly
128
128
  // why it exists as a family (see token-docs: "tuned against `sidebar`").
129
129
  "sidebar-border": { surfaces: ["sidebar"], tier: "hairline" },
130
- "sidebar-ring": { surfaces: ["sidebar"], tier: "focus" }
130
+ "sidebar-ring": { surfaces: ["sidebar"], tier: "focus" },
131
+ // Status and tone colours at graphic strength, on the surfaces a meter, dot
132
+ // or chart sits on. Bundled themes declare these as `--theme-*-mark` hooks.
133
+ "destructive-mark": { surfaces: ["background", "card"], tier: "graphic" },
134
+ "success-mark": { surfaces: ["background", "card"], tier: "graphic" },
135
+ "tone-warning-mark": { surfaces: ["background", "card"], tier: "graphic" },
136
+ "tone-info-mark": { surfaces: ["background", "card"], tier: "graphic" },
137
+ "tone-1-mark": { surfaces: ["background", "card"], tier: "graphic" },
138
+ "tone-2-mark": { surfaces: ["background", "card"], tier: "graphic" },
139
+ "tone-3-mark": { surfaces: ["background", "card"], tier: "graphic" }
131
140
  };
132
141
  function evaluateNonTextContrast(mark, surface, tier = "focus") {
133
142
  const ratio = contrastRatio(mark, surface);
@@ -140,6 +149,10 @@ function evaluateNonTextContrast(mark, surface, tier = "focus") {
140
149
  if (tier === "control") {
141
150
  return { ratio, lc, wcagRatioPasses, passes: wcagRatioPasses, basis: wcagRatioPasses ? "wcag" : "none" };
142
151
  }
152
+ if (tier === "graphic") {
153
+ const passes = ratio >= NON_TEXT_HARD_FLOOR;
154
+ return { ratio, lc, wcagRatioPasses, passes, basis: wcagRatioPasses ? "wcag" : passes ? "visibility" : "none" };
155
+ }
143
156
  if (wcagRatioPasses) return { ratio, lc, wcagRatioPasses, passes: true, basis: "wcag" };
144
157
  if (Math.abs(lc) >= NON_TEXT_PERCEPTUAL_FLOOR && ratio >= NON_TEXT_HARD_FLOOR) {
145
158
  return { ratio, lc, wcagRatioPasses, passes: true, basis: "perceptual" };
@@ -198,6 +211,38 @@ function inkPolarity(color, inks) {
198
211
  };
199
212
  return inks.reduce((best, ink) => cost(ink) < cost(best) ? ink : best);
200
213
  }
214
+ var MARK_FLOOR = NON_TEXT_HARD_FLOOR;
215
+ var MARK_NEUTRAL_CHROMA = 0.03;
216
+ function markLightness(h) {
217
+ const hue = (h % 360 + 360) % 360;
218
+ if (hue < 38 || hue >= 350) return 0.62;
219
+ if (hue < 70) return 0.71;
220
+ if (hue < 110) return 0.78;
221
+ if (hue < 190) return 0.7;
222
+ if (hue < 268) return 0.63;
223
+ if (hue < 300) return 0.6;
224
+ return 0.62;
225
+ }
226
+ function markFor(fill, mode, surfaces = []) {
227
+ const neutral = fill.c < MARK_NEUTRAL_CHROMA;
228
+ const hue = (fill.h % 360 + 360) % 360;
229
+ let l = fill.l;
230
+ if (mode === "light") l = neutral ? Math.max(l, 0.6) : Math.max(l, markLightness(hue));
231
+ else if (!neutral && (hue < 38 || hue >= 350)) l = Math.max(l, 0.65);
232
+ const lift = mode === "light" ? 1.3 : 1.15;
233
+ const chromaAt = (lightness) => {
234
+ if (neutral) return fill.c;
235
+ const max = maxChromaAt(lightness, fill.h);
236
+ return Math.max(Math.min(fill.c, max), Math.min(max * FILL_GAMUT_SAFETY, fill.c * lift));
237
+ };
238
+ let mark = { ...fill, l, c: chromaAt(l) };
239
+ const reads = (m) => surfaces.every((s) => contrastRatio(linearOf(m), linearOf(s)) >= MARK_FLOOR);
240
+ for (let i = 0; i < 60 && !reads(mark); i++) {
241
+ l = Math.min(0.98, Math.max(0.05, l + (mode === "light" ? -0.01 : 0.01)));
242
+ mark = { ...mark, l, c: neutral ? fill.c : Math.min(mark.c, maxChromaAt(l, fill.h)) };
243
+ }
244
+ return mark;
245
+ }
201
246
  function linearRgbToOklch([r, g, b]) {
202
247
  const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
203
248
  const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
@@ -257,6 +302,7 @@ export {
257
302
  HAIRLINE_VISIBILITY_FLOOR,
258
303
  LABELLED_FILLS,
259
304
  LABEL_CONTRAST_RULE,
305
+ MARK_FLOOR,
260
306
  NON_TEXT_FLOOR,
261
307
  NON_TEXT_HARD_FLOOR,
262
308
  NON_TEXT_PAIRS,
@@ -280,6 +326,7 @@ export {
280
326
  linearRgbToOklch,
281
327
  linearToSrgb,
282
328
  luminance,
329
+ markFor,
283
330
  maxChromaAt,
284
331
  oklchToLinearRgb,
285
332
  parseColor,
@@ -184,6 +184,15 @@
184
184
  --tone-1-foreground: oklch(100% 0 0);
185
185
  --tone-info-foreground: oklch(100% 0 0);
186
186
  --tone-warning-foreground: oklch(100% 0 0);
187
+ /* Graphic strength (MARK_TOKENS): meters, bars, dots, chart series and soft tints.
188
+ The same hues lifted and saturated; held to 2:1 on the page, not a text ratio. */
189
+ --theme-destructive-mark: oklch(62.0% 0.221 25.0);
190
+ --theme-success-mark: oklch(70.0% 0.130 165.0);
191
+ --theme-tone-warning-mark: oklch(71.0% 0.156 52.0);
192
+ --theme-tone-info-mark: oklch(63.0% 0.169 250.0);
193
+ --theme-tone-1-mark: oklch(62.0% 0.195 309.0);
194
+ --theme-tone-2-mark: oklch(60.0% 0.182 285.0);
195
+ --theme-tone-3-mark: oklch(70.0% 0.091 130.0);
187
196
  --highlight-foreground: oklch(100% 0 0);
188
197
  --overlay: oklch(14.1% 0.018 70.5);
189
198
  --overlay-foreground: oklch(100% 0 0);
@@ -272,6 +281,15 @@
272
281
  --tone-1-foreground: oklch(14.1% 0.018 70.5);
273
282
  --tone-info-foreground: oklch(14.1% 0.018 70.5);
274
283
  --tone-warning-foreground: oklch(14.1% 0.018 70.5);
284
+ /* Graphic strength (MARK_TOKENS): meters, bars, dots, chart series and soft tints.
285
+ The same hues lifted and saturated; held to 2:1 on the page, not a text ratio. */
286
+ --theme-destructive-mark: oklch(65.0% 0.184 25.0);
287
+ --theme-success-mark: oklch(74.0% 0.137 165.0);
288
+ --theme-tone-warning-mark: oklch(78.0% 0.149 60.0);
289
+ --theme-tone-info-mark: oklch(75.5% 0.126 250.0);
290
+ --theme-tone-1-mark: oklch(68.0% 0.161 309.0);
291
+ --theme-tone-2-mark: oklch(70.0% 0.137 285.0);
292
+ --theme-tone-3-mark: oklch(70.0% 0.103 130.0);
275
293
  --highlight-foreground: oklch(100% 0 0);
276
294
  --overlay: oklch(14.1% 0.018 70.5);
277
295
  --overlay-foreground: oklch(100% 0 0);
@@ -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
  /**
4
4
  * Ember — a Sable theme. Concrete token values live in