@stridge/noctis 1.0.0-beta.0 → 1.0.0-beta.2

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.
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # @stridge/noctis
2
2
 
3
+ ![license](https://img.shields.io/badge/license-MIT-blue) ![coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)
4
+
3
5
  The Stridge design system, and the single package consumers install. It pairs Base UI
4
6
  behavior with precompiled, **framework-neutral component CSS** on the semantic token layer, and
5
7
  ships the authoring substrate every primitive builds on — the polymorphic `Primitive` base, the
@@ -41,6 +41,7 @@ function CodeBlockRoot({ label, language, icon, copyText, copyLabel, copiedLabel
41
41
  const context = useMemo(() => ({
42
42
  state: { activeValue: void 0 },
43
43
  actions: {
44
+ /* v8 ignore next -- unreachable: Root has no tab strip to switch */
44
45
  setActiveValue: () => {},
45
46
  registerTarget,
46
47
  getText
@@ -144,6 +145,7 @@ function CodeBlockTabs({ children, "aria-label": ariaLabel, copyLabel, copiedLab
144
145
  const items = Children.toArray(children).filter((child) => isValidElement(child));
145
146
  const [value, setValue] = useState("0");
146
147
  const panelRef = useRef(null);
148
+ /* v8 ignore next -- unreachable: Tabs parts read the shared panelRef instead of registering */
147
149
  const registerTarget = useCallback((_ref) => {}, []);
148
150
  const getText = useCallback(() => panelRef.current?.querySelector("code, pre")?.textContent ?? "", []);
149
151
  const setActiveValue = useCallback((next) => setValue(next), []);
@@ -167,6 +169,7 @@ function CodeBlockTabs({ children, "aria-label": ariaLabel, copyLabel, copiedLab
167
169
  copiedLabel
168
170
  ]);
169
171
  if (items.length === 0) return null;
172
+ /* v8 ignore next 2 -- unreachable: value tracks a live tab index, so the left side is always defined */
170
173
  const activeChild = items[Number(value)] ?? items[0];
171
174
  return /* @__PURE__ */ jsx(CodeBlockProvider, {
172
175
  value: context,
@@ -186,13 +189,15 @@ function CodeBlockTabs({ children, "aria-label": ariaLabel, copyLabel, copiedLab
186
189
  children: items.map((child, index) => {
187
190
  const language = childLanguage(child.props);
188
191
  const label = childLabel(child.props);
192
+ /* v8 ignore next 2 -- unreachable: Children.toArray always assigns a key */
193
+ const key = child.key ?? index;
189
194
  return /* @__PURE__ */ jsx(CodeBlockTab, {
190
195
  value: String(index),
191
196
  language,
192
197
  label,
193
198
  icon: childIcon(child.props),
194
199
  children: resolveTabLabel(label, language, index)
195
- }, child.key ?? index);
200
+ }, key);
196
201
  })
197
202
  }), /* @__PURE__ */ jsx(CodeBlockCopyButton, {})]
198
203
  }), /* @__PURE__ */ jsx(CodeBlockPanel, {
@@ -51,7 +51,8 @@ function CollapsibleRoot({ className, ...props }) {
51
51
  * @see {@link Collapsible.Trigger.Props}
52
52
  */
53
53
  function CollapsibleTrigger({ indicator = true, className, children, ...props }) {
54
- const labelId = useCollapsibleId();
54
+ /* v8 ignore next */
55
+ const labelIdAttr = useCollapsibleId() ?? void 0;
55
56
  const side = resolveIndicator(indicator);
56
57
  const { "data-disabled": _ghostDisabled, ...ghostButton } = Button.props({
57
58
  variant: "ghost",
@@ -76,7 +77,7 @@ function CollapsibleTrigger({ indicator = true, className, children, ...props })
76
77
  side === "chevron-start" ? chevron : null,
77
78
  /* @__PURE__ */ jsx("span", {
78
79
  "data-collapsible-label": "",
79
- id: labelId ?? void 0,
80
+ id: labelIdAttr,
80
81
  children
81
82
  }),
82
83
  side === "chevron-end" ? chevron : null
@@ -18,6 +18,7 @@ function ColorPickerArea({ className }) {
18
18
  const draggingRef = useRef(false);
19
19
  const updateFromPoint = (clientX, clientY) => {
20
20
  const node = areaRef.current;
21
+ /* v8 ignore next -- defensive: updateFromPoint only runs from pointer handlers on this node, so the ref is always set */
21
22
  if (!node) return;
22
23
  const rect = node.getBoundingClientRect();
23
24
  const ratioX = clamp01((clientX - rect.left) / rect.width);
@@ -35,6 +35,7 @@ function ColorPickerRoot({ value, defaultValue, format, defaultFormat = "oklch",
35
35
  const syncingValue = useRef(false);
36
36
  const syncingFormat = useRef(false);
37
37
  if (storeRef.current === null) {
38
+ /* v8 ignore next -- defensive: DEFAULT_COLOR always parses, so the `?? BLACK` fallback is unreachable */
38
39
  const seed = parseColor(value ?? defaultValue ?? "oklch(0.504 0.151 275.2)") ?? BLACK;
39
40
  const color = {
40
41
  ...seed,
@@ -59,6 +60,8 @@ function ColorPickerRoot({ value, defaultValue, format, defaultFormat = "oklch",
59
60
  const next = formatColor(state.color, state.format, alpha);
60
61
  if (next !== lastValue.current) {
61
62
  lastValue.current = next;
63
+ /* v8 ignore next -- the value-sync effect pre-seeds lastValue to this exact string, so when
64
+ syncingValue is true the outer guard is already false; the skip branch is unreachable */
62
65
  if (!syncingValue.current) onValueChange?.(next);
63
66
  }
64
67
  }), [
@@ -38,6 +38,7 @@ function parseColor(input) {
38
38
  const parsed = parse(input);
39
39
  if (!parsed) return null;
40
40
  const rgb = toRgb(parsed);
41
+ /* v8 ignore next -- defensive: culori's rgb converter only returns undefined for an undefined input, and `parsed` is a valid color here */
41
42
  if (!rgb) return null;
42
43
  return {
43
44
  r: to255(rgb.r),
@@ -63,12 +64,14 @@ function rgbToHsv(rgb) {
63
64
  b: rgb.b / 255,
64
65
  alpha: rgb.a
65
66
  });
67
+ /* v8 ignore next -- defensive: the hsv converter never returns undefined for a constructed rgb object */
66
68
  if (!hsv) return {
67
69
  h: 0,
68
70
  s: 0,
69
71
  v: 0,
70
72
  a: rgb.a
71
73
  };
74
+ /* v8 ignore next -- s/v are always present (0 on the gray/black axis); only h drops out, so the s/v `?? 0` fallbacks are unreachable */
72
75
  return {
73
76
  h: wrapHue(hsv.h ?? 0),
74
77
  s: round((hsv.s ?? 0) * 100),
@@ -85,6 +88,7 @@ function hsvToRgb(hsv) {
85
88
  v: hsv.v / 100,
86
89
  alpha: hsv.a
87
90
  });
91
+ /* v8 ignore next -- defensive: the rgb converter never returns undefined for a constructed hsv object */
88
92
  if (!rgb) return {
89
93
  ...BLACK,
90
94
  a: hsv.a
@@ -181,19 +185,23 @@ function formatRgbString(rgb, withAlpha) {
181
185
  return `rgb(${r} ${g} ${b} / ${trim(rgb.a.toFixed(3))})`;
182
186
  }
183
187
  function formatHslString(hsl, withAlpha) {
188
+ /* v8 ignore next -- defensive: the hsl converter never returns undefined for the constructed rgb object formatColor passes */
184
189
  if (!hsl) return "hsl(0 0% 0%)";
185
190
  const h = round(wrapHue(hsl.h ?? 0));
186
191
  const s = round(hsl.s * 100);
187
192
  const l = round(hsl.l * 100);
193
+ /* v8 ignore next -- alpha is always set: formatColor builds the source rgb with `alpha: rgb.a`, so the `?? 1` fallback is unreachable */
188
194
  const a = hsl.alpha ?? 1;
189
195
  if (!withAlpha || a >= 1) return `hsl(${h} ${s}% ${l}%)`;
190
196
  return `hsl(${h} ${s}% ${l}% / ${trim(a.toFixed(3))})`;
191
197
  }
192
198
  function formatOklchString(oklch, withAlpha) {
199
+ /* v8 ignore next -- defensive: the oklch converter never returns undefined for the constructed rgb object formatColor passes */
193
200
  if (!oklch) return "oklch(0% 0 0)";
194
201
  const l = `${trim((oklch.l * 100).toFixed(2))}%`;
195
202
  const c = trim(oklch.c.toFixed(3));
196
203
  const h = trim(wrapHue(oklch.h ?? 0).toFixed(2));
204
+ /* v8 ignore next -- alpha is always set: formatColor builds the source rgb with `alpha: rgb.a`, so the `?? 1` fallback is unreachable */
197
205
  const a = oklch.alpha ?? 1;
198
206
  if (!withAlpha || a >= 1) return `oklch(${l} ${c} ${h})`;
199
207
  return `oklch(${l} ${c} ${h} / ${trim(a.toFixed(3))})`;
@@ -218,6 +226,7 @@ function wrapHue(n) {
218
226
  return (n % 360 + 360) % 360;
219
227
  }
220
228
  function trim(s) {
229
+ /* v8 ignore next -- every caller passes a `toFixed(n)` string, which always contains a "."; the no-dot `: s` branch is unreachable */
221
230
  return s.includes(".") ? s.replace(/0+$/, "").replace(/\.$/, "") : s;
222
231
  }
223
232
  //#endregion
@@ -19,6 +19,8 @@ function ColorPickerEyeDropper({ className }) {
19
19
  }, []);
20
20
  if (!supported) return null;
21
21
  const pick = async () => {
22
+ /* v8 ignore next -- defensive: the button is rendered `disabled` when `disabled` is set, so
23
+ React never dispatches this click; the guard can't be reached through the rendered button */
22
24
  if (disabled) return;
23
25
  const Ctor = window.EyeDropper;
24
26
  if (!Ctor) return;
@@ -26,6 +26,8 @@ function ColorPickerInput({ className }) {
26
26
  setDraft(next);
27
27
  if (!accepts(next, format, alpha)) return;
28
28
  const rgb = parseColor(format === "hex" ? withHash(next) : next);
29
+ /* v8 ignore next -- defensive: `accepts` already proved `next` parses, and culori's rgb
30
+ converter never fails on a parsed color, so parseColor can't return null here */
29
31
  if (!rgb) return;
30
32
  const constrained = {
31
33
  ...rgb,
@@ -68,7 +68,9 @@ function ChannelSlider({ slot, thumbSlot, ariaLabel, value, max, disabled, track
68
68
  max,
69
69
  step: 1,
70
70
  disabled,
71
- onValueChange: (next) => onValueChange(typeof next === "number" ? next : next[0] ?? 0),
71
+ onValueChange: (next) => {
72
+ onValueChange(typeof next === "number" ? next : next[0] ?? 0);
73
+ },
72
74
  className: clsx$1(className),
73
75
  children: /* @__PURE__ */ jsxs(Slider.Control, {
74
76
  "data-color-picker-channel-control": true,
@@ -62,6 +62,7 @@ function SelectValue({ className, children, placeholder, ...props }) {
62
62
  const { multiple } = useSelectContext("Value");
63
63
  const formatter = useNoctisStringFormatter();
64
64
  const render = children === void 0 && multiple ? (value) => {
65
+ /* v8 ignore next -- non-array value never reaches a multiple Select.Value */
65
66
  const count = Array.isArray(value) ? value.length : 0;
66
67
  return count > 0 ? formatter.format("select.selectedCount", { count }) : placeholder ?? null;
67
68
  } : children;
@@ -119,6 +119,7 @@ function renderLayer(entries, index, manager) {
119
119
  return /* @__PURE__ */ jsx(Sheet.Root, {
120
120
  open: entry.open,
121
121
  onOpenChange: (open) => {
122
+ /* v8 ignore next -- open===true never fires: layers are opened via the controlled `open` prop (manager.push), never by Base UI requesting open on a triggerless stack root */
122
123
  if (!open) manager.close(entry.key);
123
124
  },
124
125
  onOpenChangeComplete: (open) => {
@@ -16,6 +16,7 @@ declare function NoctisProvider({
16
16
  messages,
17
17
  theme,
18
18
  themeOverrides,
19
+ themePresets,
19
20
  nonce,
20
21
  children
21
22
  }: NoctisProvider.Props): ReactElement;
@@ -29,7 +30,12 @@ declare namespace NoctisProvider {
29
30
  direction?: TextDirection; /** String dictionary for the localized-string hooks. Defaults to the built-in `en` stub. */
30
31
  messages?: LocalizedStrings<string, string>; /** Server-resolved theme seed for the engine; omit it for the default theme. */
31
32
  theme?: ThemeProviderProps["initialInput"]; /** Generation-time engine overrides — per-primitive replacements that participate in derivation. */
32
- themeOverrides?: ThemeProviderProps["overrides"]; /** CSP nonce stamped on the inline styles Base UI injects, so they stay valid under a strict CSP. */
33
+ themeOverrides?: ThemeProviderProps["overrides"];
34
+ /**
35
+ * Named theme seeds advertised to the tree via `useTheme().presets` — what a theme picker offers.
36
+ * Defaults to the presets Noctis ships; pass your own palette to showcase additional themes.
37
+ */
38
+ themePresets?: ThemeProviderProps["presets"]; /** CSP nonce stamped on the inline styles Base UI injects, so they stay valid under a strict CSP. */
33
39
  nonce?: string;
34
40
  children: ReactNode;
35
41
  };
@@ -14,7 +14,7 @@ import { ThemeProvider } from "@stridge/noctis-design-tokens/react";
14
14
  * it for the default theme. RTL-aware primitives resolve the active direction from Base UI's
15
15
  * innermost `DirectionProvider`.
16
16
  */
17
- function NoctisProvider({ locale, direction, messages, theme, themeOverrides, nonce, children }) {
17
+ function NoctisProvider({ locale, direction, messages, theme, themeOverrides, themePresets, nonce, children }) {
18
18
  const resolvedDirection = direction ?? (isRTL(locale) ? "rtl" : "ltr");
19
19
  const value = useMemo(() => ({
20
20
  locale,
@@ -30,6 +30,7 @@ function NoctisProvider({ locale, direction, messages, theme, themeOverrides, no
30
30
  children: /* @__PURE__ */ jsx(ThemeProvider, {
31
31
  initialInput: theme,
32
32
  overrides: themeOverrides,
33
+ presets: themePresets,
33
34
  children: /* @__PURE__ */ jsx(LocaleContext.Provider, {
34
35
  value,
35
36
  children: /* @__PURE__ */ jsx(DirectionProvider, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stridge/noctis",
3
- "version": "1.0.0-beta.0",
3
+ "version": "1.0.0-beta.2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -69,9 +69,9 @@
69
69
  "culori": "4.0.2",
70
70
  "lucide-react": "1.17.0",
71
71
  "simple-icons": "16.23.0",
72
- "@stridge/noctis-design-tokens": "1.0.0-beta.0",
73
- "@stridge/noctis-intl": "1.0.0-beta.0",
74
- "@stridge/noctis-theme-engine": "1.0.0-beta.0"
72
+ "@stridge/noctis-design-tokens": "1.0.0-beta.2",
73
+ "@stridge/noctis-intl": "1.0.0-beta.2",
74
+ "@stridge/noctis-theme-engine": "1.0.0-beta.2"
75
75
  },
76
76
  "peerDependencies": {
77
77
  "react": "19.2.7",
@@ -106,6 +106,7 @@
106
106
  "tsdown": "0.21.10",
107
107
  "typescript": "6.0.3",
108
108
  "vitest": "4.1.8",
109
+ "@vitest/coverage-v8": "4.1.8",
109
110
  "@stridge/noctis-typescript": "0.0.0",
110
111
  "@stridge/noctis-test-utils": "0.0.0"
111
112
  },
@@ -115,7 +116,7 @@
115
116
  "check:publint": "publint",
116
117
  "check:size": "size-limit",
117
118
  "check:publish": "pnpm run check:publint && pnpm run check:size",
118
- "test": "vitest --run",
119
+ "test": "vitest --run --coverage --maxWorkers=2",
119
120
  "primitives:gen": "node scripts/generate-primitives.mjs"
120
121
  }
121
122
  }