@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 +2 -0
- package/dist/components/code-block/code-block.js +6 -1
- package/dist/components/collapsible/collapsible.js +3 -2
- package/dist/components/color-picker/area.js +1 -0
- package/dist/components/color-picker/color-picker.js +3 -0
- package/dist/components/color-picker/color.js +9 -0
- package/dist/components/color-picker/eyedropper.js +2 -0
- package/dist/components/color-picker/inputs.js +2 -0
- package/dist/components/color-picker/sliders.js +3 -1
- package/dist/components/select/select.js +1 -0
- package/dist/components/sheet/sheet-stack.js +1 -0
- package/dist/core/noctis-provider.d.ts +7 -1
- package/dist/core/noctis-provider.js +2 -1
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# @stridge/noctis
|
|
2
2
|
|
|
3
|
+
 
|
|
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
|
-
},
|
|
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
|
-
|
|
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:
|
|
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) =>
|
|
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"];
|
|
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.
|
|
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.
|
|
73
|
-
"@stridge/noctis-intl": "1.0.0-beta.
|
|
74
|
-
"@stridge/noctis-theme-engine": "1.0.0-beta.
|
|
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
|
}
|