@neasg/design-system 0.4.9 → 0.4.10

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
@@ -2,6 +2,8 @@
2
2
 
3
3
  Shared NEA UI primitives, theme tokens, and Storybook documentation.
4
4
 
5
+ Package: [@neasg/design-system on npm](https://www.npmjs.com/package/@neasg/design-system)
6
+
5
7
  ## Local Development
6
8
 
7
9
  Use Node 22 for local development:
@@ -174,16 +176,14 @@ Release flow:
174
176
 
175
177
  ## Storybook Deployment
176
178
 
177
- `vercel.json` is configured to deploy the built Storybook static site:
178
-
179
- - Install command: `npm ci`
180
- - Build command: `npm run build-storybook`
181
- - Output directory: `storybook-static`
179
+ Storybook is published to Chromatic from GitHub Actions. CI builds Storybook
180
+ with `npm run build-storybook`, then uploads `storybook-static` with
181
+ `npm run chromatic`.
182
182
 
183
- One-time Vercel setup:
183
+ One-time setup:
184
184
 
185
- 1. Import this GitHub repo into Vercel.
186
- 2. Let Vercel use the repo root as the project root.
187
- 3. Keep the build settings from `vercel.json`.
185
+ 1. Create or connect the Chromatic project for this repo.
186
+ 2. Add a GitHub Actions secret named `CHROMATIC_PROJECT_TOKEN`.
188
187
 
189
- After that, every push to the tracked branch deploys Storybook automatically in Vercel.
188
+ After that, pull requests and pushes to `main` publish a Chromatic build. The
189
+ GitHub Actions log includes the published Storybook URL.
package/dist/checkbox.js CHANGED
@@ -8,10 +8,16 @@ const sizeClasses = {
8
8
  md: "size-4",
9
9
  lg: "size-5",
10
10
  };
11
+ const startAlignedOffsetClasses = {
12
+ sm: "mt-[3px]",
13
+ md: "mt-[3px]",
14
+ lg: "mt-0",
15
+ };
11
16
  const Checkbox = React.forwardRef(({ className, indeterminate = false, size = "md", checked, disabled, label, description, labelAlign = "center", id, ...props }, ref) => {
12
17
  const innerRef = React.useRef(null);
13
18
  const generatedId = React.useId();
14
19
  const inputId = id !== null && id !== void 0 ? id : `checkbox-${generatedId}`;
20
+ const alignToStart = labelAlign === "start" || Boolean(description);
15
21
  React.useImperativeHandle(ref, () => innerRef.current);
16
22
  React.useEffect(() => {
17
23
  if (innerRef.current) {
@@ -20,10 +26,10 @@ const Checkbox = React.forwardRef(({ className, indeterminate = false, size = "m
20
26
  }, [indeterminate]);
21
27
  const box = (_jsxs("span", { className: cn("relative inline-block shrink-0 rounded-sm border bg-background align-middle text-primary-foreground transition-colors", sizeClasses[size], (checked || indeterminate) && !disabled
22
28
  ? "border-primary bg-primary"
23
- : "border-input", disabled && "cursor-not-allowed opacity-50", !label && className), children: [_jsx("input", { ref: innerRef, type: "checkbox", id: inputId, checked: checked, disabled: disabled, className: "absolute inset-0 h-full w-full cursor-pointer appearance-none opacity-0 disabled:cursor-not-allowed", ...props }), indeterminate ? (_jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "3", strokeLinecap: "round", className: "pointer-events-none absolute inset-0 m-auto h-3 w-3", "aria-hidden": "true", children: _jsx("line", { x1: "5", y1: "12", x2: "19", y2: "12" }) })) : checked ? (_jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "3", strokeLinecap: "round", strokeLinejoin: "round", className: "pointer-events-none absolute inset-0 m-auto h-3 w-3", "aria-hidden": "true", children: _jsx("polyline", { points: "20 6 9 17 4 12" }) })) : null] }));
29
+ : "border-input", disabled && "cursor-not-allowed opacity-50", label && alignToStart && startAlignedOffsetClasses[size], !label && className), children: [_jsx("input", { ref: innerRef, type: "checkbox", id: inputId, checked: checked, disabled: disabled, className: "absolute inset-0 h-full w-full cursor-pointer appearance-none opacity-0 disabled:cursor-not-allowed", ...props }), indeterminate ? (_jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "3", strokeLinecap: "round", className: "pointer-events-none absolute inset-0 m-auto h-3 w-3", "aria-hidden": "true", children: _jsx("line", { x1: "5", y1: "12", x2: "19", y2: "12" }) })) : checked ? (_jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "3", strokeLinecap: "round", strokeLinejoin: "round", className: "pointer-events-none absolute inset-0 m-auto h-3 w-3", "aria-hidden": "true", children: _jsx("polyline", { points: "20 6 9 17 4 12" }) })) : null] }));
24
30
  if (!label)
25
31
  return box;
26
- return (_jsxs("label", { htmlFor: inputId, className: cn("inline-flex cursor-pointer items-center gap-2 text-sm", labelAlign === "start" && "items-start", disabled && "cursor-not-allowed opacity-50", className), children: [box, _jsxs("div", { className: "select-none", children: [_jsx(Typography, { as: "span", variant: "bodySm", children: label }), description ? (_jsx(Typography, { as: "p", variant: "caption", className: "mt-1", children: description })) : null] })] }));
32
+ return (_jsxs("label", { htmlFor: inputId, className: cn("inline-flex cursor-pointer gap-2 text-sm", alignToStart ? "items-start" : "items-center", disabled && "cursor-not-allowed opacity-50", className), children: [box, _jsxs("div", { className: "flex min-w-0 flex-col gap-0.5 select-none", children: [_jsx(Typography, { as: "span", variant: "bodySm", children: label }), description ? (_jsx(Typography, { as: "p", variant: "caption", color: "muted", children: description })) : null] })] }));
27
33
  });
28
34
  Checkbox.displayName = "Checkbox";
29
35
  export { Checkbox };
package/dist/field.d.ts CHANGED
@@ -4,6 +4,10 @@ interface FieldLabelProps extends React.ComponentProps<"label"> {
4
4
  required?: boolean;
5
5
  }
6
6
  declare function FieldLabel({ className, required, children, ...props }: FieldLabelProps): import("react/jsx-runtime").JSX.Element;
7
+ interface FieldLegendProps extends React.ComponentProps<"legend"> {
8
+ required?: boolean;
9
+ }
10
+ declare function FieldLegend({ className, required, children, ...props }: FieldLegendProps): import("react/jsx-runtime").JSX.Element;
7
11
  declare function FieldDescription({ className, ...props }: Omit<React.ComponentProps<"p">, "color">): import("react/jsx-runtime").JSX.Element;
8
12
  declare function FieldError({ className, ...props }: Omit<React.ComponentProps<"p">, "color">): import("react/jsx-runtime").JSX.Element | null;
9
- export { Field, FieldDescription, FieldError, FieldLabel };
13
+ export { Field, FieldDescription, FieldError, FieldLabel, FieldLegend };
package/dist/field.js CHANGED
@@ -7,6 +7,9 @@ function Field({ className, ...props }) {
7
7
  function FieldLabel({ className, required, children, ...props }) {
8
8
  return (_jsx(Typography, { asChild: true, variant: "label", className: cn("group-data-[disabled=true]/field:cursor-not-allowed group-data-[disabled=true]/field:opacity-50 group-data-[invalid=true]/field:text-destructive", className), children: _jsxs("label", { "data-slot": "field-label", ...props, children: [children, required ? (_jsx("span", { "aria-hidden": "true", className: "ml-0.5 text-destructive", children: "*" })) : null] }) }));
9
9
  }
10
+ function FieldLegend({ className, required, children, ...props }) {
11
+ return (_jsx(Typography, { asChild: true, variant: "label", className: cn("group-data-[disabled=true]/field:cursor-not-allowed group-data-[disabled=true]/field:opacity-50 group-data-[invalid=true]/field:text-destructive", className), children: _jsxs("legend", { "data-slot": "field-legend", ...props, children: [children, required ? (_jsx("span", { "aria-hidden": "true", className: "ml-0.5 text-destructive", children: "*" })) : null] }) }));
12
+ }
10
13
  function FieldDescription({ className, ...props }) {
11
14
  return (_jsx(Typography, { as: "p", variant: "bodySm", color: "muted", "data-slot": "field-description", className: className, ...props }));
12
15
  }
@@ -16,4 +19,4 @@ function FieldError({ className, ...props }) {
16
19
  }
17
20
  return (_jsx(Typography, { as: "p", variant: "bodySm", role: "alert", "data-slot": "field-error", className: cn("text-destructive", className), ...props }));
18
21
  }
19
- export { Field, FieldDescription, FieldError, FieldLabel };
22
+ export { Field, FieldDescription, FieldError, FieldLabel, FieldLegend };
@@ -14,8 +14,14 @@ export interface RadioGroupProps extends Omit<React.FieldsetHTMLAttributes<HTMLF
14
14
  label?: React.ReactNode;
15
15
  hideLabel?: boolean;
16
16
  description?: React.ReactNode;
17
+ error?: React.ReactNode;
18
+ invalid?: boolean;
19
+ required?: boolean;
17
20
  orientation?: "horizontal" | "vertical";
18
21
  groupClassName?: string;
19
22
  optionClassName?: string;
23
+ labelClassName?: string;
24
+ descriptionClassName?: string;
25
+ errorClassName?: string;
20
26
  }
21
27
  export declare const RadioGroup: React.ForwardRefExoticComponent<RadioGroupProps & React.RefAttributes<HTMLFieldSetElement>>;
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
+ import { FieldDescription, FieldError, FieldLegend } from "./field";
4
5
  import { cn } from "./lib/utils";
5
6
  import { Typography } from "./typography";
6
7
  function useControllableValue({ value, defaultValue, onValueChange, }) {
@@ -15,27 +16,35 @@ function useControllableValue({ value, defaultValue, onValueChange, }) {
15
16
  }, [isControlled, onValueChange]);
16
17
  return [currentValue, setValue];
17
18
  }
18
- export const RadioGroup = React.forwardRef(({ className, optionClassName, options, value, defaultValue, onValueChange, name, label, hideLabel = false, description, orientation = "horizontal", groupClassName, disabled, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, ...props }, ref) => {
19
+ export const RadioGroup = React.forwardRef(({ className, optionClassName, options, value, defaultValue, onValueChange, name, label, hideLabel = false, description, error, invalid = false, required = false, orientation = "horizontal", groupClassName, disabled, labelClassName, descriptionClassName, errorClassName, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, ...props }, ref) => {
19
20
  const generatedId = React.useId();
20
21
  const groupName = name !== null && name !== void 0 ? name : `radio-group-${generatedId.replace(/:/g, "")}`;
21
22
  const labelId = label ? `${groupName}-label` : undefined;
22
- const descriptionId = description ? `${groupName}-description` : undefined;
23
+ const errorId = error ? `${groupName}-error` : undefined;
24
+ const showDescription = Boolean(description) && !error;
25
+ const descriptionId = showDescription ? `${groupName}-description` : undefined;
26
+ const isInvalid = invalid ||
27
+ ariaInvalid === true ||
28
+ ariaInvalid === "true" ||
29
+ Boolean(error);
30
+ const describedBy = [ariaDescribedBy, descriptionId, errorId].filter(Boolean).join(" ") ||
31
+ undefined;
23
32
  const [currentValue, setCurrentValue] = useControllableValue({
24
33
  value,
25
34
  defaultValue,
26
35
  onValueChange,
27
36
  });
28
- return (_jsxs("fieldset", { ref: ref, disabled: disabled, className: cn("min-w-0 border-0 p-0", className), ...props, children: [label ? (_jsx("legend", { id: labelId, className: cn("mb-2 text-sm font-medium text-foreground", hideLabel && "sr-only"), children: label })) : null, description ? (_jsx(Typography, { as: "p", id: descriptionId, variant: "caption", color: "muted", className: cn("mb-2", hideLabel && "sr-only"), children: description })) : null, _jsx("div", { role: "radiogroup", "aria-orientation": orientation, "aria-labelledby": labelId !== null && labelId !== void 0 ? labelId : ariaLabelledBy, "aria-describedby": descriptionId !== null && descriptionId !== void 0 ? descriptionId : ariaDescribedBy, className: cn(orientation === "horizontal"
37
+ return (_jsxs("fieldset", { ref: ref, disabled: disabled, "aria-invalid": isInvalid ? "true" : undefined, className: cn("group/field flex w-full min-w-0 flex-col gap-2 border-0 p-0", className), "data-disabled": disabled ? "true" : undefined, "data-invalid": isInvalid ? "true" : undefined, ...props, children: [label ? (_jsx(FieldLegend, { id: labelId, required: required, className: cn("mb-2", hideLabel && "sr-only", labelClassName), children: label })) : null, _jsx("div", { role: "radiogroup", "aria-orientation": orientation, "aria-labelledby": labelId !== null && labelId !== void 0 ? labelId : ariaLabelledBy, "aria-describedby": describedBy, "aria-invalid": isInvalid ? "true" : undefined, className: cn(orientation === "horizontal"
29
38
  ? "flex flex-wrap items-center gap-4"
30
39
  : "grid gap-2", groupClassName), children: options.map((option, optionIndex) => {
31
40
  const optionId = `${groupName}-option-${optionIndex}`;
32
41
  const checked = currentValue === option.value;
33
42
  const optionDisabled = disabled || option.disabled;
34
- return (_jsxs("label", { htmlFor: optionId, className: cn("inline-flex min-w-0 cursor-pointer items-start gap-2 text-sm text-foreground", optionDisabled && "cursor-not-allowed opacity-50", optionClassName), children: [_jsxs("span", { className: cn("relative mt-0.5 inline-flex size-3.5 shrink-0 items-center justify-center rounded-full border bg-background transition-colors focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2", checked ? "border-primary" : "border-input"), children: [_jsx("span", { className: cn("size-2 rounded-full bg-primary transition-transform", checked ? "scale-100" : "scale-0") }), _jsx("input", { id: optionId, type: "radio", name: groupName, value: option.value, checked: checked, disabled: optionDisabled, onChange: (event) => {
43
+ return (_jsxs("label", { htmlFor: optionId, className: cn("inline-flex min-w-0 cursor-pointer items-start gap-2 text-sm text-foreground", optionDisabled && "cursor-not-allowed opacity-50", optionClassName), children: [_jsxs("span", { className: cn("relative mt-[3px] inline-flex size-3.5 shrink-0 items-center justify-center rounded-full border bg-background transition-colors focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2", checked ? "border-primary" : "border-input"), children: [_jsx("span", { className: cn("size-2 rounded-full bg-primary transition-transform", checked ? "scale-100" : "scale-0") }), _jsx("input", { id: optionId, type: "radio", name: groupName, value: option.value, checked: checked, required: required, disabled: optionDisabled, onChange: (event) => {
35
44
  if (event.target.checked) {
36
45
  setCurrentValue(option.value);
37
46
  }
38
- }, className: "absolute inset-0 cursor-pointer opacity-0 disabled:cursor-not-allowed" })] }), _jsxs("span", { className: "min-w-0", children: [_jsx(Typography, { as: "span", variant: "bodySm", children: option.label }), option.description ? (_jsx(Typography, { as: "p", variant: "caption", color: "muted", children: option.description })) : null] })] }, option.value));
39
- }) })] }));
47
+ }, className: "absolute inset-0 cursor-pointer opacity-0 disabled:cursor-not-allowed" })] }), _jsxs("span", { className: "flex min-w-0 flex-col gap-0.5", children: [_jsx(Typography, { as: "span", variant: "bodySm", children: option.label }), option.description ? (_jsx(Typography, { as: "p", variant: "caption", color: "muted", children: option.description })) : null] })] }, option.value));
48
+ }) }), showDescription ? (_jsx(FieldDescription, { id: descriptionId, className: descriptionClassName, children: description })) : null, _jsx(FieldError, { id: errorId, className: errorClassName, children: error })] }));
40
49
  });
41
50
  RadioGroup.displayName = "RadioGroup";
package/dist/styles.css CHANGED
@@ -351,6 +351,14 @@
351
351
  --table-border: var(--component-border);
352
352
  }
353
353
 
354
+ sgds-masthead {
355
+ color-scheme: light;
356
+ }
357
+
358
+ .dark sgds-masthead {
359
+ color-scheme: dark;
360
+ }
361
+
354
362
  * {
355
363
  @apply border-border;
356
364
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neasg/design-system",
3
- "version": "0.4.9",
3
+ "version": "0.4.10",
4
4
  "description": "NEA shared design system primitives, theme tokens, and Storybook docs.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -22,7 +22,8 @@
22
22
  "storybook": "storybook dev -p 6007 -c .storybook",
23
23
  "build-storybook": "storybook build -c .storybook",
24
24
  "test": "vitest run",
25
- "prepublishOnly": "npm test && npm run build"
25
+ "prepublishOnly": "npm test && npm run build",
26
+ "chromatic": "chromatic --storybook-build-dir storybook-static --exit-once-uploaded"
26
27
  },
27
28
  "publishConfig": {
28
29
  "access": "public",
@@ -85,6 +86,7 @@
85
86
  "@types/react-dom": "^18.2.18",
86
87
  "@vitejs/plugin-react": "^6.0.1",
87
88
  "autoprefixer": "^10.4.16",
89
+ "chromatic": "^16.10.1",
88
90
  "jsdom": "^29.0.0",
89
91
  "postcss": "^8.4.31",
90
92
  "react": "^18.2.0",