@neasg/design-system 0.2.0 → 0.3.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.
package/README.md CHANGED
@@ -125,15 +125,28 @@ One-time setup:
125
125
 
126
126
  Release flow:
127
127
 
128
- ```bash
129
- npm version patch # 0.2.0 0.2.1
130
- npm version minor # 0.2.0 → 0.3.0
131
- npm version major # 0.2.0 → 1.0.0
132
- npm version 0.5.0 # set an exact version
133
- git push origin main --follow-tags
134
- ```
135
-
136
- Pushing a `v*` tag triggers `.github/workflows/publish.yml`, which runs tests, builds the package, and publishes it to npm.
128
+ 1. Make sure all your changes are committed and pushed to `main`.
129
+ 2. Bump the version this updates `package.json` and creates a git tag automatically:
130
+ ```bash
131
+ npm version patch # 0.2.0 → 0.2.1
132
+ npm version minor # 0.2.0 0.3.0
133
+ npm version major # 0.2.0 → 1.0.0
134
+ npm version 0.5.0 # set an exact version
135
+ ```
136
+ 3. Push the commit and tag together:
137
+ ```bash
138
+ git push origin main --follow-tags
139
+ ```
140
+ 4. The `v*` tag triggers `.github/workflows/publish.yml`, which runs tests, builds, and publishes to npm.
141
+
142
+ > **Note:** If GPG signing fails on step 2, you can do it manually:
143
+ > ```bash
144
+ > npm version 0.2.0 --no-git-tag-version
145
+ > git add package.json package-lock.json
146
+ > git commit -m "0.2.0"
147
+ > git tag v0.2.0
148
+ > git push origin main --follow-tags
149
+ > ```
137
150
 
138
151
  ## Storybook Deployment
139
152
 
package/dist/badge.d.ts CHANGED
@@ -9,6 +9,8 @@ declare const badgeVariants: (props?: ({
9
9
  * surface the full label via `title`.
10
10
  */
11
11
  export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {
12
+ /** Show a visible border matching the badge colour. */
13
+ bordered?: boolean;
12
14
  }
13
- declare function Badge({ className, variant, children, ...props }: BadgeProps): import("react/jsx-runtime").JSX.Element;
15
+ declare function Badge({ className, variant, bordered, children, ...props }: BadgeProps): import("react/jsx-runtime").JSX.Element;
14
16
  export { Badge, badgeVariants };
package/dist/badge.js CHANGED
@@ -19,12 +19,23 @@ const badgeVariants = cva("inline-flex max-w-[200px] items-center justify-center
19
19
  variant: "secondary",
20
20
  },
21
21
  });
22
- function Badge({ className, variant, children, ...props }) {
22
+ function Badge({ className, variant, bordered = false, children, ...props }) {
23
23
  const textContent = typeof children === "string"
24
24
  ? children
25
25
  : React.Children.toArray(children)
26
26
  .filter((child) => typeof child === "string")
27
27
  .join("");
28
- return (_jsx("div", { className: cn(badgeVariants({ variant }), className), title: textContent || undefined, ...props, children: children }));
28
+ const resolvedVariant = variant !== null && variant !== void 0 ? variant : "secondary";
29
+ const borderedClasses = {
30
+ secondary: "border-secondary-foreground/30",
31
+ muted: "border-muted-foreground/30",
32
+ destructive: "border-[hsl(var(--status-destructive-foreground))]/30",
33
+ outline: "border-foreground/30",
34
+ success: "border-[hsl(var(--status-success-foreground))]/30",
35
+ warning: "border-[hsl(var(--status-warning-foreground))]/30",
36
+ info: "border-[hsl(var(--status-info-foreground))]/30",
37
+ accent: "border-[hsl(var(--status-accent-foreground))]/30",
38
+ };
39
+ return (_jsx("div", { className: cn(badgeVariants({ variant }), bordered && borderedClasses[resolvedVariant], className), title: textContent || undefined, ...props, children: children }));
29
40
  }
30
41
  export { Badge, badgeVariants };
package/dist/button.d.ts CHANGED
@@ -2,7 +2,7 @@ import * as React from "react";
2
2
  import { type VariantProps } from "class-variance-authority";
3
3
  declare const buttonVariants: (props?: ({
4
4
  variant?: "link" | "default" | "outline" | "secondary" | "destructive" | "ghost" | null | undefined;
5
- size?: "default" | "sm" | "icon" | "lg" | null | undefined;
5
+ size?: "default" | "sm" | "lg" | "icon" | null | undefined;
6
6
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
7
7
  export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
8
8
  /**
package/dist/button.js CHANGED
@@ -14,10 +14,10 @@ const buttonVariants = cva("inline-flex cursor-pointer items-center justify-cent
14
14
  link: "text-primary underline-offset-4 hover:underline",
15
15
  },
16
16
  size: {
17
- default: "h-9 px-4 py-2 [&_svg]:h-4 [&_svg]:w-4",
18
- sm: "h-8 rounded-md px-3 text-xs [&_svg]:h-4 [&_svg]:w-4",
19
- lg: "h-10 rounded-md px-8 [&_svg]:h-5 [&_svg]:w-5",
20
- icon: "h-9 w-9 [&_svg]:h-4 [&_svg]:w-4",
17
+ default: "h-10 px-4 py-2 [&_svg]:h-4 [&_svg]:w-4",
18
+ sm: "h-9 rounded-md px-3 text-xs [&_svg]:h-4 [&_svg]:w-4",
19
+ lg: "h-11 rounded-md px-8 [&_svg]:h-5 [&_svg]:w-5",
20
+ icon: "h-10 w-10 [&_svg]:h-4 [&_svg]:w-4",
21
21
  },
22
22
  },
23
23
  defaultVariants: {
@@ -0,0 +1,8 @@
1
+ import * as React from "react";
2
+ import { DayButton, DayPicker } from "react-day-picker";
3
+ import { Button } from "./button";
4
+ declare function CalendarDayButton({ className, day, modifiers, children, disabled, onClick, onFocus, onBlur, onKeyDown, onMouseEnter, onMouseLeave, tabIndex, type, "aria-label": ariaLabel, "aria-pressed": ariaPressed, "aria-selected": ariaSelected, }: React.ComponentProps<typeof DayButton>): import("react/jsx-runtime").JSX.Element;
5
+ declare function Calendar({ className, classNames, showOutsideDays, captionLayout, buttonVariant, formatters, components, ...props }: React.ComponentProps<typeof DayPicker> & {
6
+ buttonVariant?: React.ComponentProps<typeof Button>["variant"];
7
+ }): import("react/jsx-runtime").JSX.Element;
8
+ export { Calendar, CalendarDayButton };
@@ -0,0 +1,80 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { DayPicker, getDefaultClassNames } from "react-day-picker";
5
+ import { buttonVariants } from "./button";
6
+ import { cn } from "./lib/utils";
7
+ function CalendarDayButton({ className, day, modifiers, children, disabled, onClick, onFocus, onBlur, onKeyDown, onMouseEnter, onMouseLeave, tabIndex, type, "aria-label": ariaLabel, "aria-pressed": ariaPressed, "aria-selected": ariaSelected, }) {
8
+ const defaultClassNames = getDefaultClassNames();
9
+ const ref = React.useRef(null);
10
+ React.useEffect(() => {
11
+ var _a;
12
+ if (modifiers.focused)
13
+ (_a = ref.current) === null || _a === void 0 ? void 0 : _a.focus();
14
+ }, [modifiers.focused]);
15
+ return (_jsx("button", { ref: ref, type: type, disabled: disabled, onClick: onClick, onFocus: onFocus, onBlur: onBlur, onKeyDown: onKeyDown, onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, tabIndex: tabIndex, "aria-label": ariaLabel, "aria-pressed": ariaPressed, "aria-selected": ariaSelected, "data-day": day.date.toLocaleDateString(), "data-selected-single": modifiers.selected &&
16
+ !modifiers.range_start &&
17
+ !modifiers.range_end &&
18
+ !modifiers.range_middle, "data-range-start": modifiers.range_start, "data-range-end": modifiers.range_end, "data-range-middle": modifiers.range_middle, className: cn("inline-flex cursor-pointer items-center justify-center rounded-md text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", "data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 aspect-square w-full min-w-[--cell-size] font-normal leading-none data-[range-end=true]:rounded-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px]", defaultClassNames.day, className), children: children }));
19
+ }
20
+ function Calendar({ className, classNames, showOutsideDays = true, captionLayout = "label", buttonVariant = "ghost", formatters, components, ...props }) {
21
+ const defaultClassNames = getDefaultClassNames();
22
+ return (_jsx(DayPicker, { showOutsideDays: showOutsideDays, className: cn("bg-background group/calendar p-3 [--cell-size:2rem] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent", String.raw `rtl:**:[.rdp-button\_next>svg]:rotate-180`, String.raw `rtl:**:[.rdp-button\_previous>svg]:rotate-180`, className), captionLayout: captionLayout, formatters: {
23
+ formatMonthDropdown: (date) => date.toLocaleString("default", { month: "short" }),
24
+ ...formatters,
25
+ }, classNames: {
26
+ root: cn("w-fit", defaultClassNames.root),
27
+ months: cn("relative flex flex-col gap-4 md:flex-row", defaultClassNames.months),
28
+ month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
29
+ nav: cn("absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1", defaultClassNames.nav),
30
+ button_previous: cn(buttonVariants({ variant: buttonVariant }), "h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50", defaultClassNames.button_previous),
31
+ button_next: cn(buttonVariants({ variant: buttonVariant }), "h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50", defaultClassNames.button_next),
32
+ month_caption: cn("flex h-[--cell-size] w-full items-center justify-center px-[--cell-size]", defaultClassNames.month_caption),
33
+ dropdowns: cn("flex h-[--cell-size] w-full items-center justify-center gap-1.5 text-sm font-medium", defaultClassNames.dropdowns),
34
+ dropdown_root: cn("has-focus:border-ring border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative rounded-md border", defaultClassNames.dropdown_root),
35
+ dropdown: cn("bg-popover absolute inset-0 opacity-0", defaultClassNames.dropdown),
36
+ caption_label: cn("select-none font-medium", captionLayout === "label"
37
+ ? "text-sm"
38
+ : "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5", defaultClassNames.caption_label),
39
+ table: "w-full border-collapse",
40
+ weekdays: cn("flex", defaultClassNames.weekdays),
41
+ weekday: cn("text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal", defaultClassNames.weekday),
42
+ week: cn("mt-2 flex w-full", defaultClassNames.week),
43
+ week_number_header: cn("w-[--cell-size] select-none", defaultClassNames.week_number_header),
44
+ week_number: cn("text-muted-foreground select-none text-[0.8rem]", defaultClassNames.week_number),
45
+ day: cn("group/day relative aspect-square h-full w-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md", defaultClassNames.day),
46
+ range_start: cn("bg-accent rounded-l-md", defaultClassNames.range_start),
47
+ range_middle: cn("rounded-none", defaultClassNames.range_middle),
48
+ range_end: cn("bg-accent rounded-r-md", defaultClassNames.range_end),
49
+ today: cn("bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none", defaultClassNames.today),
50
+ outside: cn("text-muted-foreground aria-selected:text-muted-foreground", defaultClassNames.outside),
51
+ disabled: cn("text-muted-foreground opacity-50", defaultClassNames.disabled),
52
+ hidden: cn("invisible", defaultClassNames.hidden),
53
+ ...classNames,
54
+ }, components: {
55
+ Root: ({ className: rootClassName, rootRef, ...rootProps }) => (_jsx("div", { "data-slot": "calendar", ref: rootRef, className: cn(rootClassName), ...rootProps })),
56
+ Chevron: ({ className: chevronClassName, orientation }) => {
57
+ const svgProps = {
58
+ className: cn("size-4", chevronClassName),
59
+ viewBox: "0 0 24 24",
60
+ fill: "none",
61
+ stroke: "currentColor",
62
+ strokeWidth: 2,
63
+ strokeLinecap: "round",
64
+ strokeLinejoin: "round",
65
+ "aria-hidden": true,
66
+ };
67
+ if (orientation === "left") {
68
+ return _jsx("svg", { ...svgProps, children: _jsx("path", { d: "m15 18-6-6 6-6" }) });
69
+ }
70
+ if (orientation === "right") {
71
+ return _jsx("svg", { ...svgProps, children: _jsx("path", { d: "m9 18 6-6-6-6" }) });
72
+ }
73
+ return _jsx("svg", { ...svgProps, children: _jsx("path", { d: "m6 9 6 6 6-6" }) });
74
+ },
75
+ DayButton: CalendarDayButton,
76
+ WeekNumber: ({ children, ...weekNumberProps }) => (_jsx("td", { ...weekNumberProps, children: _jsx("div", { className: "flex size-[--cell-size] items-center justify-center text-center", children: children }) })),
77
+ ...components,
78
+ }, ...props }));
79
+ }
80
+ export { Calendar, CalendarDayButton };
package/dist/card.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ import * as React from "react";
2
+ export interface CardProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
3
+ title: React.ReactNode;
4
+ description?: React.ReactNode;
5
+ variant?: "default" | "elevated" | "gradient";
6
+ /** Large value displayed above the title — use for stat/metric cards. */
7
+ stat?: React.ReactNode;
8
+ /** Small supporting text below the stat or body — use for sub-metrics. */
9
+ subMetric?: React.ReactNode;
10
+ footer?: React.ReactNode;
11
+ active?: boolean;
12
+ }
13
+ declare const Card: React.ForwardRefExoticComponent<CardProps & React.RefAttributes<HTMLDivElement>>;
14
+ export { Card };
package/dist/card.js ADDED
@@ -0,0 +1,9 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { cn } from "./lib/utils";
5
+ import { Typography } from "./typography";
6
+ const Card = React.forwardRef(({ title, description, variant = "default", stat, subMetric, footer, active, children, className, onClick, ...props }, ref) => (_jsxs("div", { ref: ref, onClick: onClick, className: cn("rounded-xl border text-card-foreground", variant === "default" && "bg-card", variant === "elevated" && "bg-card shadow-sm", variant === "gradient" &&
7
+ "border-primary/20 bg-gradient-to-br from-primary to-primary/80 text-primary-foreground", active && "border-primary bg-primary/10 ring-1 ring-primary/20", onClick && "cursor-pointer transition-colors hover:border-primary", className), ...props, children: [_jsxs("div", { className: "flex flex-col gap-2 p-6", children: [_jsx(Typography, { as: "p", variant: "label", color: variant === "gradient" ? "inherit" : "muted", className: variant === "gradient" ? "opacity-80" : undefined, children: title }), description ? (_jsx(Typography, { as: "p", variant: "caption", color: variant === "gradient" ? "inherit" : undefined, className: variant === "gradient" ? "opacity-70" : undefined, children: description })) : null, stat ? (_jsx(Typography, { as: "p", variant: "title", color: variant === "gradient" ? "inherit" : undefined, children: stat })) : null, subMetric ? (_jsx(Typography, { as: "p", variant: "caption", color: variant === "gradient" ? "inherit" : undefined, className: variant === "gradient" ? "opacity-80" : undefined, children: subMetric })) : null] }), children ? _jsx("div", { className: "p-6 pt-0", children: children }) : null, footer ? (_jsx("div", { className: "flex items-center p-6 pt-0", children: footer })) : null] })));
8
+ Card.displayName = "Card";
9
+ export { Card };
@@ -8,6 +8,7 @@ import { SearchIcon } from "./animated-icons/search";
8
8
  import { DialogRoot, DialogSurface } from "./dialog-primitive";
9
9
  import { FilterToggle } from "./filter-toggle";
10
10
  import { cn } from "./lib/utils";
11
+ import { Typography } from "./typography";
11
12
  import { SearchHighlight } from "./use-search-highlight";
12
13
  function CommandSearchEmptyIcon({ className }) {
13
14
  return _jsx(SearchIcon, { className: className, size: 20 });
@@ -57,14 +58,14 @@ function CommandSearch({ value, onValueChange, sections, children, className, is
57
58
  }
58
59
  }, [isOpen]);
59
60
  return (_jsxs("div", { className: className, ...props, children: [_jsxs(Button, { type: "button", onClick: () => setIsOpen(true), variant: "ghost", className: cn(isCollapsed
60
- ? "h-9 w-full justify-center rounded-lg px-3 !font-normal text-muted-foreground hover:bg-white hover:text-foreground"
61
- : "h-9 w-full min-w-[14rem] justify-start gap-3 rounded-lg border border-gray-200 bg-white px-3 !font-normal text-muted-foreground shadow-sm hover:bg-white hover:text-foreground"), children: [_jsx(SearchIcon, { className: "shrink-0", size: 16 }), !isCollapsed ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "flex-1 text-left text-sm", children: triggerPlaceholder }), _jsx("kbd", { className: "hidden h-5 items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] text-muted-foreground sm:inline-flex", children: resolvedLabel })] })) : null] }), _jsx(DialogRoot, { open: isOpen, onOpenChange: setIsOpen, children: _jsx(DialogSurface, { className: cn("max-w-xl gap-0 overflow-hidden p-0 sm:max-w-xl", dialogClassName), hideCloseButton: true, children: _jsxs(Command, { shouldFilter: false, className: "rounded-none border-0", children: [_jsx(CommandInput, { ref: inputRef, value: value, onValueChange: onValueChange, placeholder: inputPlaceholder, className: "text-sm" }), allFilter || filters.length ? (_jsxs("div", { className: "flex items-center gap-1 border-b px-3 py-2", children: [allFilter ? (_jsx(FilterToggle, { selected: allFilter.selected, onClick: allFilter.onSelect, children: allFilter.label })) : null, filters.map((filter) => (_jsx(FilterToggle, { selected: filter.selected, onClick: filter.onSelect, children: filter.label }, filter.value)))] })) : null, _jsx(CommandList, { className: cn("max-h-[400px] p-1", resultsClassName), children: children !== undefined ? children : sections ? (visibleSections.length ? (visibleSections.map((section, index) => (_jsxs(React.Fragment, { children: [index > 0 ? _jsx(CommandSeparator, { alwaysRender: true }) : null, _jsx(CommandGroup, { heading: section.heading, children: section.items.map((item) => (_jsx(CommandItem, { value: getCommandSearchItemValue(item), disabled: item.disabled, onSelect: () => {
61
+ ? "h-10 w-full justify-center rounded-lg px-3 !font-normal text-muted-foreground hover:bg-white hover:text-foreground"
62
+ : "h-10 w-full min-w-[14rem] justify-start gap-3 rounded-lg border border-gray-200 bg-white px-3 !font-normal text-muted-foreground hover:bg-white hover:text-foreground"), children: [_jsx(SearchIcon, { className: "shrink-0", size: 16 }), !isCollapsed ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "flex-1 text-left text-sm", children: triggerPlaceholder }), _jsx("kbd", { className: "hidden h-5 items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] text-muted-foreground sm:inline-flex", children: resolvedLabel })] })) : null] }), _jsx(DialogRoot, { open: isOpen, onOpenChange: setIsOpen, children: _jsx(DialogSurface, { className: cn("max-w-xl gap-0 overflow-hidden p-0 sm:max-w-xl", dialogClassName), hideCloseButton: true, children: _jsxs(Command, { shouldFilter: false, className: "rounded-none border-0", children: [_jsx(CommandInput, { ref: inputRef, value: value, onValueChange: onValueChange, placeholder: inputPlaceholder, className: "text-sm" }), allFilter || filters.length ? (_jsxs("div", { className: "flex items-center gap-1 border-b px-3 py-2", children: [allFilter ? (_jsx(FilterToggle, { selected: allFilter.selected, onClick: allFilter.onSelect, children: allFilter.label })) : null, filters.map((filter) => (_jsx(FilterToggle, { selected: filter.selected, onClick: filter.onSelect, children: filter.label }, filter.value)))] })) : null, _jsx(CommandList, { className: cn("max-h-[400px] p-1", resultsClassName), children: children !== undefined ? children : sections ? (visibleSections.length ? (visibleSections.map((section, index) => (_jsxs(React.Fragment, { children: [index > 0 ? _jsx(CommandSeparator, { alwaysRender: true }) : null, _jsx(CommandGroup, { heading: section.heading, children: section.items.map((item) => (_jsx(CommandItem, { value: getCommandSearchItemValue(item), disabled: item.disabled, onSelect: () => {
62
63
  var _a;
63
64
  (_a = item.onSelect) === null || _a === void 0 ? void 0 : _a.call(item);
64
65
  if (item.closeOnSelect !== false) {
65
66
  setIsOpen(false);
66
67
  }
67
- }, children: _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "truncate text-sm font-medium", children: _jsx(SearchHighlight, { text: item.title, query: value }) }), item.subtitle ? (_jsx("p", { className: "truncate text-xs text-muted-foreground", children: _jsx(SearchHighlight, { text: item.subtitle, query: value }) })) : null] }) }, item.id))) })] }, section.key)))) : (_jsx(EmptyState, { icon: CommandSearchEmptyIcon, message: normalizedValue ? "No results found" : "Start typing to search", description: normalizedValue
68
+ }, children: _jsxs("div", { className: "min-w-0", children: [_jsx(Typography, { as: "p", variant: "label", className: "truncate", children: _jsx(SearchHighlight, { text: item.title, query: value }) }), item.subtitle ? (_jsx(Typography, { as: "p", variant: "caption", className: "truncate", children: _jsx(SearchHighlight, { text: item.subtitle, query: value }) })) : null] }) }, item.id))) })] }, section.key)))) : (_jsx(EmptyState, { icon: CommandSearchEmptyIcon, message: normalizedValue ? "No results found" : "Start typing to search", description: normalizedValue
68
69
  ? `No results found for "${value}".`
69
70
  : "Results will appear here as you type." }))) : (_jsx(EmptyState, { icon: CommandSearchEmptyIcon, message: normalizedValue ? "No results found" : "Start typing to search", description: normalizedValue
70
71
  ? `No results found for "${value}".`
@@ -0,0 +1,22 @@
1
+ import * as React from "react";
2
+ export interface DateInputProps {
3
+ label: React.ReactNode;
4
+ value: string;
5
+ onValueChange: (value: string) => void;
6
+ description?: React.ReactNode;
7
+ error?: React.ReactNode;
8
+ invalid?: boolean;
9
+ hideLabel?: boolean;
10
+ placeholder?: string;
11
+ disabled?: boolean;
12
+ minDate?: Date;
13
+ maxDate?: Date;
14
+ className?: string;
15
+ triggerClassName?: string;
16
+ labelClassName?: string;
17
+ descriptionClassName?: string;
18
+ errorClassName?: string;
19
+ id?: string;
20
+ }
21
+ declare const DateInput: React.ForwardRefExoticComponent<DateInputProps & React.RefAttributes<HTMLButtonElement>>;
22
+ export { DateInput };
@@ -0,0 +1,61 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { format, parse } from "date-fns";
5
+ import { Calendar } from "./calendar";
6
+ import { inputControlClassName } from "./input-control";
7
+ import { Field, FieldDescription, FieldError, FieldLabel } from "./field";
8
+ import { Popover, PopoverContent, PopoverTrigger } from "./popover";
9
+ import { cn } from "./lib/utils";
10
+ function parseDateString(dateStr) {
11
+ if (!(dateStr === null || dateStr === void 0 ? void 0 : dateStr.trim()))
12
+ return undefined;
13
+ try {
14
+ const nativeDate = new Date(dateStr);
15
+ if (!isNaN(nativeDate.getTime()))
16
+ return nativeDate;
17
+ const ddmmyyyyMatch = dateStr.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})/);
18
+ if (ddmmyyyyMatch) {
19
+ const [, day, month, year] = ddmmyyyyMatch;
20
+ const date = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
21
+ if (!isNaN(date.getTime()))
22
+ return date;
23
+ }
24
+ const date = parse(dateStr, "yyyy-MM-dd", new Date());
25
+ if (!isNaN(date.getTime()))
26
+ return date;
27
+ return undefined;
28
+ }
29
+ catch {
30
+ return undefined;
31
+ }
32
+ }
33
+ function formatISODate(date) {
34
+ return format(date, "yyyy-MM-dd");
35
+ }
36
+ const DateInput = React.forwardRef(({ label, value, onValueChange, description, error, invalid = false, hideLabel = false, placeholder = "Pick a date", disabled = false, minDate, maxDate, className, triggerClassName, labelClassName, descriptionClassName, errorClassName, id, }, ref) => {
37
+ const [open, setOpen] = React.useState(false);
38
+ const generatedId = React.useId();
39
+ const inputId = id !== null && id !== void 0 ? id : `date-input-${generatedId}`;
40
+ const errorId = error ? `${inputId}-error` : undefined;
41
+ const showDescription = Boolean(description) && !error;
42
+ const descriptionId = showDescription ? `${inputId}-description` : undefined;
43
+ const isInvalid = invalid || Boolean(error);
44
+ const selectedDate = React.useMemo(() => parseDateString(value), [value]);
45
+ const displayValue = selectedDate
46
+ ? format(selectedDate, "d MMM yyyy")
47
+ : undefined;
48
+ const handleSelect = (date) => {
49
+ onValueChange(date ? formatISODate(date) : "");
50
+ setOpen(false);
51
+ };
52
+ return (_jsxs(Field, { className: className, "data-disabled": disabled ? "true" : undefined, "data-invalid": isInvalid ? "true" : undefined, children: [_jsx(FieldLabel, { htmlFor: inputId, className: cn(hideLabel && "sr-only", labelClassName), children: label }), _jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs("div", { className: "relative w-full", children: [_jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground", style: { width: 14, height: 14 }, "aria-hidden": "true", children: [_jsx("rect", { width: "18", height: "18", x: "3", y: "4", rx: "2", ry: "2" }), _jsx("line", { x1: "16", x2: "16", y1: "2", y2: "6" }), _jsx("line", { x1: "8", x2: "8", y1: "2", y2: "6" }), _jsx("line", { x1: "3", x2: "21", y1: "10", y2: "10" })] }), _jsx("button", { ref: ref, type: "button", id: inputId, disabled: disabled, "aria-invalid": isInvalid ? "true" : undefined, "aria-describedby": [descriptionId, errorId].filter(Boolean).join(" ") || undefined, className: cn(inputControlClassName, "cursor-pointer pl-9 text-left", !selectedDate && "text-muted-foreground", isInvalid && "border-destructive focus-visible:ring-destructive", triggerClassName), children: displayValue !== null && displayValue !== void 0 ? displayValue : _jsx("span", { children: placeholder }) })] }) }), _jsx(PopoverContent, { className: "w-auto p-0", align: "start", children: _jsx(Calendar, { mode: "single", selected: selectedDate, onSelect: handleSelect, disabled: (date) => {
53
+ if (minDate && date < minDate)
54
+ return true;
55
+ if (maxDate && date > maxDate)
56
+ return true;
57
+ return false;
58
+ }, defaultMonth: selectedDate, autoFocus: true }) })] }), showDescription ? (_jsx(FieldDescription, { id: descriptionId, className: descriptionClassName, children: description })) : null, _jsx(FieldError, { id: errorId, className: errorClassName, children: error })] }));
59
+ });
60
+ DateInput.displayName = "DateInput";
61
+ export { DateInput };
@@ -101,7 +101,7 @@ function EditableTable({ value, onChange, columns, isEditing = false, className,
101
101
  const cellValue = getNestedValue(row, column.path);
102
102
  const isEditingCell = (editingCell === null || editingCell === void 0 ? void 0 : editingCell.rowIndex) === rowIndex &&
103
103
  (editingCell === null || editingCell === void 0 ? void 0 : editingCell.colPath) === column.path;
104
- return (_jsx(TableCell, { className: cn("whitespace-normal", isEditing && "cursor-pointer hover:bg-muted/50"), minWidth: getColumnMinWidth(column), onClick: () => handleCellClick(rowIndex, column.path), children: isEditingCell ? (_jsx(InputControl, { ref: inputRef, value: editValue, onChange: (event) => setEditValue(event.target.value), onBlur: commitEdit, onKeyDown: handleKeyDown, className: "h-9 w-full text-sm" })) : (_jsx("span", { className: cn("block whitespace-normal break-words", cellValue === null ||
104
+ return (_jsx(TableCell, { className: cn("whitespace-normal", isEditing && "cursor-pointer hover:bg-muted/50"), minWidth: getColumnMinWidth(column), onClick: () => handleCellClick(rowIndex, column.path), children: isEditingCell ? (_jsx(InputControl, { ref: inputRef, value: editValue, onChange: (event) => setEditValue(event.target.value), onBlur: commitEdit, onKeyDown: handleKeyDown, className: "h-10 w-full text-sm" })) : (_jsx("span", { className: cn("block whitespace-normal break-words", cellValue === null ||
105
105
  cellValue === undefined ||
106
106
  cellValue === ""
107
107
  ? "text-muted-foreground"
@@ -116,7 +116,7 @@ function EditableTable({ value, onChange, columns, isEditing = false, className,
116
116
  insertRow: (nextRow, index = rowIndex + 1) => handleInsertRow(nextRow, index),
117
117
  removeRow: () => handleDeleteRow(rowIndex),
118
118
  })
119
- : null, isEditing ? (_jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "h-8 w-8 text-destructive hover:bg-destructive/10 hover:text-destructive", onClick: () => handleDeleteRow(rowIndex), "aria-label": `Delete row ${rowIndex + 1}`, children: _jsx(TrashIcon, { size: 16 }) })) : null] }) })) : null] }, `row-${rowIndex}`)))) })] }) }), isEditing ? (_jsxs(Button, { type: "button", variant: "outline", onClick: handleAddRow, className: "w-full justify-center", children: [_jsx(PlusIcon, { size: 16 }), "Add Row"] })) : null] }));
119
+ : null, isEditing ? (_jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "text-destructive hover:bg-destructive/10 hover:text-destructive", onClick: () => handleDeleteRow(rowIndex), "aria-label": `Delete row ${rowIndex + 1}`, children: _jsx(TrashIcon, { size: 16 }) })) : null] }) })) : null] }, `row-${rowIndex}`)))) })] }) }), isEditing ? (_jsxs(Button, { type: "button", variant: "outline", onClick: handleAddRow, className: "w-full justify-center", children: [_jsx(PlusIcon, { size: 16 }), "Add Row"] })) : null] }));
120
120
  }
121
121
  EditableTable.displayName = "EditableTable";
122
122
  export { EditableTable };
@@ -0,0 +1,9 @@
1
+ import * as React from "react";
2
+ export interface GovtMastheadProps extends React.HTMLAttributes<HTMLElement> {
3
+ fluid?: boolean;
4
+ }
5
+ declare function GovtMasthead({ fluid, ...props }: GovtMastheadProps): import("react/jsx-runtime").JSX.Element;
6
+ declare namespace GovtMasthead {
7
+ var displayName: string;
8
+ }
9
+ export { GovtMasthead };
@@ -0,0 +1,8 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { SgdsMasthead } from "@govtechsg/sgds-web-component/react";
4
+ function GovtMasthead({ fluid = false, ...props }) {
5
+ return _jsx(SgdsMasthead, { fluid: fluid, ...props });
6
+ }
7
+ GovtMasthead.displayName = "GovtMasthead";
8
+ export { GovtMasthead };
package/dist/index.d.ts CHANGED
@@ -2,7 +2,12 @@ export { Avatar, AvatarProfile, AvatarRoot, AvatarImage, AvatarFallback, } from
2
2
  export type { AvatarProps, AvatarProfileProps, AvatarRootProps, } from "./avatar";
3
3
  export * from "./animated-icons";
4
4
  export { Badge, badgeVariants } from "./badge";
5
+ export { Card } from "./card";
6
+ export type { CardProps } from "./card";
5
7
  export type { BadgeProps } from "./badge";
8
+ export { Calendar, CalendarDayButton } from "./calendar";
9
+ export { DateInput } from "./date-input";
10
+ export type { DateInputProps } from "./date-input";
6
11
  export { CountBadge } from "./count-badge";
7
12
  export type { CountBadgeProps } from "./count-badge";
8
13
  export { EmptyState } from "./empty-state";
@@ -15,6 +20,8 @@ export { Input } from "./input";
15
20
  export type { InputProps } from "./input";
16
21
  export { Select } from "./select";
17
22
  export type { SelectOption, SelectProps } from "./select";
23
+ export { MultiSelect } from "./multi-select";
24
+ export type { MultiSelectOption, MultiSelectProps } from "./multi-select";
18
25
  export { DEFAULT_COUNTRY_CODES, PhoneInput, } from "./phone-input";
19
26
  export type { CountryCode, PhoneInputProps, } from "./phone-input";
20
27
  export { OtpInput } from "./otp-input";
@@ -33,6 +40,8 @@ export { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, Command
33
40
  export { CommandSearch, type CommandSearchFilter, type CommandSearchItem, type CommandSearchProps, type CommandSearchSection, } from "./command-search";
34
41
  export { SearchInput } from "./search-input";
35
42
  export type { SearchInputProps } from "./search-input";
43
+ export { GovtMasthead } from "./govt-masthead";
44
+ export type { GovtMastheadProps } from "./govt-masthead";
36
45
  export { SearchHighlight, useSearchHighlight, } from "./use-search-highlight";
37
46
  export type { SearchHighlightProps, SearchHighlightSegment, } from "./use-search-highlight";
38
47
  export { Textarea } from "./textarea";
@@ -54,5 +63,9 @@ export type { TableToolbarProps } from "./table-toolbar";
54
63
  export { Table, TableRoot, TableContainer, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption, TableRowSkeleton, TABLE_SKELETON_PRESETS, } from "./table";
55
64
  export type { TableColumn, TableProps, TableSortDirection, TableSortingProps, TableSkeletonConfig, TableRowSkeletonProps, } from "./table";
56
65
  export { Tabs, TabsRoot, type TabsItem, type TabsProps, TabsList, TabsTrigger, TabsContent, useTabsContext, } from "./tabs";
66
+ export { TabWithDropdown } from "./tab-with-dropdown";
67
+ export type { TabWithDropdownProps, TabDropdownSection } from "./tab-with-dropdown";
57
68
  export { Tooltip, TooltipRoot, TooltipTrigger, TooltipContent, TooltipProvider, type TooltipProps, } from "./tooltip";
69
+ export { Toaster, toast } from "./toaster";
70
+ export type { ToasterProps } from "./toaster";
58
71
  export { cn } from "./lib/utils";
package/dist/index.js CHANGED
@@ -1,12 +1,16 @@
1
1
  export { Avatar, AvatarProfile, AvatarRoot, AvatarImage, AvatarFallback, } from "./avatar";
2
2
  export * from "./animated-icons";
3
3
  export { Badge, badgeVariants } from "./badge";
4
+ export { Card } from "./card";
5
+ export { Calendar, CalendarDayButton } from "./calendar";
6
+ export { DateInput } from "./date-input";
4
7
  export { CountBadge } from "./count-badge";
5
8
  export { EmptyState } from "./empty-state";
6
9
  export { EditableTable } from "./editable-table";
7
10
  export { EmailInput } from "./email-input";
8
11
  export { Input } from "./input";
9
12
  export { Select } from "./select";
13
+ export { MultiSelect } from "./multi-select";
10
14
  export { DEFAULT_COUNTRY_CODES, PhoneInput, } from "./phone-input";
11
15
  export { OtpInput } from "./otp-input";
12
16
  export { LabelValue, isEmptyValue, } from "./label-value";
@@ -17,6 +21,7 @@ export { Dialog } from "./dialog";
17
21
  export { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandSeparator, CommandShortcut, } from "./command";
18
22
  export { CommandSearch, } from "./command-search";
19
23
  export { SearchInput } from "./search-input";
24
+ export { GovtMasthead } from "./govt-masthead";
20
25
  export { SearchHighlight, useSearchHighlight, } from "./use-search-highlight";
21
26
  export { Textarea } from "./textarea";
22
27
  export { DraggableTabs, } from "./draggable-tabs";
@@ -30,5 +35,7 @@ export { Pagination } from "./pagination";
30
35
  export { TableToolbar } from "./table-toolbar";
31
36
  export { Table, TableRoot, TableContainer, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption, TableRowSkeleton, TABLE_SKELETON_PRESETS, } from "./table";
32
37
  export { Tabs, TabsRoot, TabsList, TabsTrigger, TabsContent, useTabsContext, } from "./tabs";
38
+ export { TabWithDropdown } from "./tab-with-dropdown";
33
39
  export { Tooltip, TooltipRoot, TooltipTrigger, TooltipContent, TooltipProvider, } from "./tooltip";
40
+ export { Toaster, toast } from "./toaster";
34
41
  export { cn } from "./lib/utils";
@@ -1,4 +1,4 @@
1
1
  import * as React from "react";
2
- declare const inputControlClassName = "h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-[invalid=true]:border-destructive aria-[invalid=true]:ring-destructive/20 md:text-sm";
2
+ declare const inputControlClassName = "h-10 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-8 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-[invalid=true]:border-destructive aria-[invalid=true]:ring-destructive/20 md:text-sm";
3
3
  declare const InputControl: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, "ref"> & React.RefAttributes<HTMLInputElement>>;
4
4
  export { InputControl, inputControlClassName };
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { cn } from "./lib/utils";
4
- const inputControlClassName = "h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-[invalid=true]:border-destructive aria-[invalid=true]:ring-destructive/20 md:text-sm";
4
+ const inputControlClassName = "h-10 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-8 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-[invalid=true]:border-destructive aria-[invalid=true]:ring-destructive/20 md:text-sm";
5
5
  const InputControl = React.forwardRef(({ className, type = "text", ...props }, ref) => (_jsx("input", { ref: ref, type: type, "data-slot": "input", className: cn(inputControlClassName, className), ...props })));
6
6
  InputControl.displayName = "InputControl";
7
7
  export { InputControl, inputControlClassName };
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cn } from "./lib/utils";
3
+ import { Typography } from "./typography";
3
4
  export function isEmptyValue(value) {
4
5
  if (value === null || value === undefined || value === "")
5
6
  return true;
@@ -14,6 +15,6 @@ function LabelValue({ label, value, className, labelClassName, hideIfEmpty = fal
14
15
  if (hideIfEmpty && empty) {
15
16
  return null;
16
17
  }
17
- return (_jsxs("div", { className: cn("min-w-0 h-full space-y-1", className), children: [_jsx("p", { className: cn("text-sm font-semibold text-foreground", labelClassName), children: label }), _jsx("div", { className: cn("min-w-0 break-words text-sm text-foreground", empty && "italic text-muted-foreground/60"), children: empty ? "-" : value })] }));
18
+ return (_jsxs("div", { className: cn("min-w-0 h-full space-y-1", className), children: [_jsx(Typography, { as: "p", variant: "label", className: labelClassName, children: label }), _jsx("div", { className: cn("min-w-0 break-words text-sm text-foreground", empty && "italic text-muted-foreground/60"), children: empty ? "-" : value })] }));
18
19
  }
19
20
  export { LabelValue };
package/dist/layout.d.ts CHANGED
@@ -9,8 +9,10 @@ export declare function SidebarProvider({ children }: {
9
9
  }): import("react/jsx-runtime").JSX.Element;
10
10
  export declare function useSidebar(): SidebarContextType;
11
11
  export interface DashboardLayoutProps extends React.HTMLAttributes<HTMLDivElement> {
12
+ masthead?: React.ReactNode;
13
+ prototypeBanner?: React.ReactNode;
12
14
  }
13
- declare function DashboardLayout({ className, children, ...props }: DashboardLayoutProps): import("react/jsx-runtime").JSX.Element;
15
+ declare function DashboardLayout({ className, masthead, prototypeBanner, children, style, ...props }: DashboardLayoutProps): import("react/jsx-runtime").JSX.Element;
14
16
  export interface DashboardSidebarProps extends React.HTMLAttributes<HTMLElement> {
15
17
  logo?: React.ReactNode | ((context: {
16
18
  isCollapsed: boolean;
package/dist/layout.js CHANGED
@@ -9,7 +9,9 @@ import { CountBadge } from "./count-badge";
9
9
  import { Tooltip } from "./tooltip";
10
10
  import { cn } from "./lib/utils";
11
11
  const COLLAPSE_BREAKPOINT = 1280;
12
+ const TOP_OFFSET_CSS_VAR = "--layout-top-offset";
12
13
  const SidebarContext = React.createContext(undefined);
14
+ const useIsomorphicLayoutEffect = typeof window === "undefined" ? React.useEffect : React.useLayoutEffect;
13
15
  export function SidebarProvider({ children }) {
14
16
  const [isCollapsed, setIsCollapsed] = React.useState(false);
15
17
  const manualCollapsedRef = React.useRef(false);
@@ -42,11 +44,35 @@ export function useSidebar() {
42
44
  }
43
45
  return context;
44
46
  }
45
- function DashboardLayout({ className, children, ...props }) {
46
- return (_jsx("div", { className: cn("flex min-h-screen bg-[hsl(var(--layout-shell-background))]", className), ...props, children: children }));
47
+ function DashboardLayout({ className, masthead, prototypeBanner, children, style, ...props }) {
48
+ const chromeRef = React.useRef(null);
49
+ const [topOffset, setTopOffset] = React.useState(0);
50
+ const layoutStyle = React.useMemo(() => ({
51
+ ...style,
52
+ [TOP_OFFSET_CSS_VAR]: `${topOffset}px`,
53
+ }), [style, topOffset]);
54
+ useIsomorphicLayoutEffect(() => {
55
+ const node = chromeRef.current;
56
+ if (!node) {
57
+ setTopOffset(0);
58
+ return;
59
+ }
60
+ const updateOffset = () => {
61
+ setTopOffset(node.getBoundingClientRect().height);
62
+ };
63
+ updateOffset();
64
+ if (typeof ResizeObserver === "undefined") {
65
+ window.addEventListener("resize", updateOffset);
66
+ return () => window.removeEventListener("resize", updateOffset);
67
+ }
68
+ const observer = new ResizeObserver(() => updateOffset());
69
+ observer.observe(node);
70
+ return () => observer.disconnect();
71
+ }, [masthead, prototypeBanner]);
72
+ return (_jsxs("div", { className: cn("flex min-h-screen flex-col bg-[hsl(var(--layout-shell-background))]", className), style: layoutStyle, ...props, children: [masthead || prototypeBanner ? (_jsxs("div", { ref: chromeRef, className: "sticky top-0 z-40 shrink-0", children: [masthead, prototypeBanner] })) : null, _jsx("div", { className: "flex min-h-0 flex-1", children: children })] }));
47
73
  }
48
74
  function SidebarShell({ isCollapsed, className, children, ...props }) {
49
- return (_jsx("aside", { className: cn("fixed left-0 top-0 z-30 flex h-screen shrink-0 flex-col overflow-hidden bg-[hsl(var(--layout-shell-background))] pb-2 pl-[var(--layout-sidebar-padding-inline)] pt-[var(--layout-sidebar-padding-top)] transition-all duration-300 ease-in-out", isCollapsed
75
+ return (_jsx("aside", { className: cn("fixed left-0 top-[var(--layout-top-offset)] z-30 flex h-[calc(100dvh-var(--layout-top-offset))] shrink-0 flex-col overflow-hidden bg-[hsl(var(--layout-shell-background))] pb-2 pl-[var(--layout-sidebar-padding-inline)] pt-[var(--layout-sidebar-padding-top)] transition-all duration-300 ease-in-out", isCollapsed
50
76
  ? "w-[var(--layout-sidebar-collapsed-width)]"
51
77
  : "w-[var(--layout-sidebar-width)]", className), ...props, children: children }));
52
78
  }
@@ -72,7 +98,7 @@ function SidebarMenuItem({ className, children, }) {
72
98
  return _jsx("li", { className: className, children: children });
73
99
  }
74
100
  function SidebarMenuButton({ active = false, collapsed = false, disabled = false, className, asChild = false, children, ...props }) {
75
- return (_jsx(Button, { asChild: asChild, variant: "ghost", disabled: disabled, className: cn("h-auto w-full justify-start gap-3 rounded-lg py-2 text-sm font-medium shadow-none", collapsed ? "justify-center px-3" : "px-3", disabled
101
+ return (_jsx(Button, { asChild: asChild, variant: "ghost", disabled: disabled, className: cn("h-10 w-full justify-start gap-3 rounded-lg text-sm font-medium shadow-none", collapsed ? "justify-center px-3" : "px-3", disabled
76
102
  ? "text-muted-foreground/50 hover:bg-transparent hover:text-muted-foreground/50"
77
103
  : active
78
104
  ? "bg-white text-foreground hover:bg-white hover:text-foreground"
@@ -133,7 +159,7 @@ function DashboardContent({ className, surfaceClassName, children, ...props }) {
133
159
  toggle();
134
160
  }
135
161
  }, [isCollapsed, toggle]);
136
- return (_jsx("main", { onClick: handleContentClick, className: cn("flex min-w-0 flex-1 flex-col bg-[hsl(var(--layout-shell-background))] pb-[var(--layout-content-padding)] pl-[var(--layout-content-gap-start)] pr-[var(--layout-content-padding)] pt-[var(--layout-content-padding)] transition-all duration-300 md:pb-[var(--layout-content-padding-md)] md:pl-[var(--layout-content-gap-start-md)] md:pr-[var(--layout-content-padding-md)] md:pt-[var(--layout-content-padding-md)] lg:pb-[var(--layout-content-padding-lg)] lg:pl-[var(--layout-content-gap-start-lg)] lg:pr-[var(--layout-content-padding-lg)] lg:pt-[var(--layout-content-padding-lg)]", isCollapsed
162
+ return (_jsx("main", { onClick: handleContentClick, className: cn("flex min-h-[calc(100dvh-var(--layout-top-offset))] min-w-0 flex-1 flex-col bg-[hsl(var(--layout-shell-background))] pb-[var(--layout-content-padding)] pl-[var(--layout-content-gap-start)] pr-[var(--layout-content-padding)] pt-[var(--layout-content-padding)] transition-all duration-300 md:pb-[var(--layout-content-padding-md)] md:pl-[var(--layout-content-gap-start-md)] md:pr-[var(--layout-content-padding-md)] md:pt-[var(--layout-content-padding-md)] lg:pb-[var(--layout-content-padding-lg)] lg:pl-[var(--layout-content-gap-start-lg)] lg:pr-[var(--layout-content-padding-lg)] lg:pt-[var(--layout-content-padding-lg)]", isCollapsed
137
163
  ? "ml-[var(--layout-sidebar-collapsed-width)]"
138
164
  : "ml-[var(--layout-sidebar-width)]", className), ...props, children: _jsx("div", { className: cn("flex flex-1 flex-col gap-[var(--layout-surface-gap)] overflow-x-clip rounded-[var(--radius-lg)] bg-background p-[var(--layout-surface-padding)] shadow-sm md:p-[var(--layout-surface-padding-md)] lg:p-[var(--layout-surface-padding-lg)]", surfaceClassName), children: children }) }));
139
165
  }
@@ -0,0 +1,29 @@
1
+ import * as React from "react";
2
+ export interface MultiSelectOption {
3
+ label: string;
4
+ value: string;
5
+ description?: string;
6
+ disabled?: boolean;
7
+ }
8
+ export interface MultiSelectProps {
9
+ label: React.ReactNode;
10
+ options: MultiSelectOption[];
11
+ value: string[];
12
+ onValueChange: (value: string[]) => void;
13
+ placeholder?: string;
14
+ description?: React.ReactNode;
15
+ error?: React.ReactNode;
16
+ invalid?: boolean;
17
+ disabled?: boolean;
18
+ hideLabel?: boolean;
19
+ searchable?: boolean;
20
+ searchPlaceholder?: string;
21
+ className?: string;
22
+ triggerClassName?: string;
23
+ labelClassName?: string;
24
+ descriptionClassName?: string;
25
+ errorClassName?: string;
26
+ id?: string;
27
+ }
28
+ declare function MultiSelect({ label, options, value, onValueChange, placeholder, description, error, invalid, disabled, hideLabel, searchable, searchPlaceholder, className, triggerClassName, labelClassName, descriptionClassName, errorClassName, id, }: MultiSelectProps): import("react/jsx-runtime").JSX.Element;
29
+ export { MultiSelect };
@@ -0,0 +1,77 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { XIcon } from "./animated-icons/x";
5
+ import { Badge } from "./badge";
6
+ import { Button } from "./button";
7
+ import { Field, FieldDescription, FieldError, FieldLabel } from "./field";
8
+ import { InputControl } from "./input-control";
9
+ import { cn } from "./lib/utils";
10
+ import { Typography } from "./typography";
11
+ function MultiSelect({ label, options, value, onValueChange, placeholder = "Select options", description, error, invalid = false, disabled = false, hideLabel = false, searchable = false, searchPlaceholder = "Search...", className, triggerClassName, labelClassName, descriptionClassName, errorClassName, id, }) {
12
+ const generatedId = React.useId();
13
+ const selectId = id !== null && id !== void 0 ? id : `multi-select-${generatedId}`;
14
+ const errorId = error ? `${selectId}-error` : undefined;
15
+ const showDescription = Boolean(description) && !error;
16
+ const descriptionId = showDescription ? `${selectId}-description` : undefined;
17
+ const isInvalid = invalid || Boolean(error);
18
+ const [open, setOpen] = React.useState(false);
19
+ const [searchQuery, setSearchQuery] = React.useState("");
20
+ const containerRef = React.useRef(null);
21
+ const searchInputRef = React.useRef(null);
22
+ const normalizedQuery = searchQuery.trim().toLowerCase();
23
+ const filteredOptions = searchable && normalizedQuery
24
+ ? options.filter((opt) => {
25
+ var _a, _b;
26
+ return opt.label.toLowerCase().includes(normalizedQuery) ||
27
+ ((_b = (_a = opt.description) === null || _a === void 0 ? void 0 : _a.toLowerCase().includes(normalizedQuery)) !== null && _b !== void 0 ? _b : false);
28
+ })
29
+ : options;
30
+ const selectedLabels = value
31
+ .map((v) => options.find((opt) => opt.value === v))
32
+ .filter(Boolean);
33
+ const toggleOption = React.useCallback((optionValue) => {
34
+ const next = value.includes(optionValue)
35
+ ? value.filter((v) => v !== optionValue)
36
+ : [...value, optionValue];
37
+ onValueChange(next);
38
+ }, [value, onValueChange]);
39
+ const removeOption = React.useCallback((optionValue) => {
40
+ onValueChange(value.filter((v) => v !== optionValue));
41
+ }, [value, onValueChange]);
42
+ React.useEffect(() => {
43
+ if (!open) {
44
+ setSearchQuery("");
45
+ return;
46
+ }
47
+ if (searchable) {
48
+ const frame = window.requestAnimationFrame(() => {
49
+ var _a;
50
+ (_a = searchInputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
51
+ });
52
+ return () => window.cancelAnimationFrame(frame);
53
+ }
54
+ }, [open, searchable]);
55
+ React.useEffect(() => {
56
+ const handleClickOutside = (event) => {
57
+ if (containerRef.current &&
58
+ !containerRef.current.contains(event.target)) {
59
+ setOpen(false);
60
+ }
61
+ };
62
+ if (open) {
63
+ document.addEventListener("mousedown", handleClickOutside);
64
+ return () => document.removeEventListener("mousedown", handleClickOutside);
65
+ }
66
+ }, [open]);
67
+ return (_jsxs(Field, { className: className, "data-disabled": disabled ? "true" : undefined, "data-invalid": isInvalid ? "true" : undefined, children: [_jsx(FieldLabel, { htmlFor: selectId, className: cn(hideLabel && "sr-only", labelClassName), children: label }), _jsxs("div", { ref: containerRef, className: "relative", children: [_jsx("button", { type: "button", id: selectId, disabled: disabled, onClick: () => setOpen((prev) => !prev), "aria-expanded": open, "aria-haspopup": "listbox", "aria-invalid": isInvalid ? "true" : undefined, "aria-describedby": [descriptionId, errorId].filter(Boolean).join(" ") || undefined, className: cn("flex min-h-10 w-full flex-wrap items-center gap-1.5 rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background md:text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", isInvalid ? "border-destructive" : "border-input", triggerClassName), children: selectedLabels.length > 0 ? (selectedLabels.map((opt) => (_jsxs(Badge, { variant: "secondary", className: "gap-1 pr-1", children: [opt.label, _jsx(Button, { variant: "ghost", size: "icon", className: "h-4 w-4 p-0 hover:bg-transparent", onClick: (e) => {
68
+ e.stopPropagation();
69
+ removeOption(opt.value);
70
+ }, "aria-label": `Remove ${opt.label}`, children: _jsx(XIcon, { size: 10 }) })] }, opt.value)))) : (_jsx("span", { className: "text-muted-foreground", children: placeholder })) }), open ? (_jsxs("div", { className: "absolute z-50 mt-1 w-full rounded-md border bg-popover shadow-md", children: [searchable ? (_jsx("div", { className: "border-b p-2", children: _jsx(InputControl, { ref: searchInputRef, value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), placeholder: searchPlaceholder, className: "h-8 border-0 bg-transparent px-2 py-0 text-sm shadow-none focus-visible:ring-0", onKeyDown: (e) => e.stopPropagation() }) })) : null, _jsx("div", { role: "listbox", "aria-label": typeof label === "string" ? label : "Options", "aria-multiselectable": "true", className: "max-h-64 overflow-y-auto p-1", children: filteredOptions.length > 0 ? (filteredOptions.map((option) => {
71
+ const isSelected = value.includes(option.value);
72
+ return (_jsxs("button", { type: "button", role: "option", "aria-selected": isSelected, disabled: option.disabled, onClick: () => toggleOption(option.value), className: cn("flex h-10 w-full cursor-pointer items-center gap-2 rounded-sm px-2 text-left text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50", isSelected && "bg-accent"), children: [_jsx("div", { className: cn("mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-sm border", isSelected
73
+ ? "border-primary bg-primary text-primary-foreground"
74
+ : "border-input"), children: isSelected ? (_jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "3", strokeLinecap: "round", strokeLinejoin: "round", className: "h-3 w-3", children: _jsx("polyline", { points: "20 6 9 17 4 12" }) })) : null }), _jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx("span", { children: option.label }), option.description ? (_jsx(Typography, { as: "span", variant: "caption", children: option.description })) : null] })] }, option.value));
75
+ })) : (_jsx("div", { className: "px-3 py-6 text-center text-sm text-muted-foreground", children: "No options found." })) })] })) : null] }), showDescription ? (_jsx(FieldDescription, { id: descriptionId, className: descriptionClassName, children: description })) : null, _jsx(FieldError, { id: errorId, className: errorClassName, children: error })] }));
76
+ }
77
+ export { MultiSelect };
package/dist/otp-input.js CHANGED
@@ -7,7 +7,7 @@ import { validateOtp } from "./input-validation";
7
7
  import { cn } from "./lib/utils";
8
8
  function OtpSlot({ slot, invalid, className, }) {
9
9
  var _a, _b;
10
- return (_jsxs("div", { "data-slot": "otp-slot", className: cn("relative flex size-9 items-center justify-center rounded-md border bg-transparent text-sm font-medium shadow-sm transition-[border-color,box-shadow,color] outline-none group-data-[disabled=true]/field:cursor-not-allowed group-data-[disabled=true]/field:bg-muted/30 group-data-[disabled=true]/field:text-muted-foreground group-data-[disabled=true]/field:opacity-50", invalid ? "border-destructive" : "border-input", slot.isActive &&
10
+ return (_jsxs("div", { "data-slot": "otp-slot", className: cn("relative flex size-10 items-center justify-center rounded-md border bg-transparent text-sm font-medium transition-[border-color,box-shadow,color] outline-none group-data-[disabled=true]/field:cursor-not-allowed group-data-[disabled=true]/field:bg-muted/30 group-data-[disabled=true]/field:text-muted-foreground group-data-[disabled=true]/field:opacity-50", invalid ? "border-destructive" : "border-input", slot.isActive &&
11
11
  (invalid
12
12
  ? "border-destructive ring-[3px] ring-destructive/20"
13
13
  : "border-ring ring-[3px] ring-ring/50"), className), children: [_jsx("span", { className: cn("pointer-events-none", !slot.char && slot.placeholderChar ? "text-muted-foreground" : ""), children: (_b = (_a = slot.char) !== null && _a !== void 0 ? _a : slot.placeholderChar) !== null && _b !== void 0 ? _b : "" }), slot.hasFakeCaret ? (_jsx("span", { "aria-hidden": "true", className: "pointer-events-none absolute inset-0 flex items-center justify-center", children: _jsx("span", { className: "h-4 w-px animate-pulse bg-foreground" }) })) : null] }));
@@ -54,7 +54,7 @@ const OtpInput = React.forwardRef(({ label, value, onValueChange, description, e
54
54
  setInternalError(validate(value));
55
55
  onBlur === null || onBlur === void 0 ? void 0 : onBlur(event);
56
56
  }, [onBlur, validate, value]);
57
- return (_jsxs(Field, { className: className, "data-disabled": disabled ? "true" : undefined, "data-invalid": isInvalid ? "true" : undefined, children: [_jsx(FieldLabel, { htmlFor: inputId, className: cn(hideLabel && "sr-only", labelClassName), children: label }), _jsx(OTPInput, { ref: ref, id: inputId, value: value, onChange: handleChange, onBlur: handleBlur, disabled: disabled, maxLength: length, autoComplete: autoComplete, inputMode: inputMode, pattern: pattern, pushPasswordManagerStrategy: pushPasswordManagerStrategy, "aria-invalid": isInvalid ? "true" : undefined, "aria-describedby": describedBy, containerClassName: cn("flex w-full items-center gap-2", groupClassName), render: ({ slots }) => slots.map((slot, index) => (_jsx(OtpSlot, { slot: slot, invalid: isInvalid, className: slotClassName }, `${inputId}-${index}`))), ...props }), showDescription ? (_jsx(FieldDescription, { id: descriptionId, className: descriptionClassName, children: description })) : null, _jsx(FieldError, { id: errorId, className: errorClassName, children: displayError })] }));
57
+ return (_jsxs(Field, { className: className, "data-disabled": disabled ? "true" : undefined, "data-invalid": isInvalid ? "true" : undefined, children: [_jsx(FieldLabel, { htmlFor: inputId, className: cn(hideLabel && "sr-only", labelClassName), children: label }), _jsx(OTPInput, { ref: ref, id: inputId, value: value, onChange: handleChange, onBlur: handleBlur, disabled: disabled, maxLength: length, autoComplete: autoComplete, inputMode: inputMode, pattern: pattern, pushPasswordManagerStrategy: pushPasswordManagerStrategy, "aria-invalid": isInvalid ? "true" : undefined, "aria-describedby": describedBy, containerClassName: cn("flex w-full items-center justify-center gap-2", groupClassName), render: ({ slots }) => slots.map((slot, index) => (_jsx(OtpSlot, { slot: slot, invalid: isInvalid, className: slotClassName }, `${inputId}-${index}`))), ...props }), showDescription ? (_jsx(FieldDescription, { id: descriptionId, className: descriptionClassName, children: description })) : null, _jsx(FieldError, { id: errorId, className: errorClassName, children: displayError })] }));
58
58
  });
59
59
  OtpInput.displayName = "OtpInput";
60
60
  export { OtpInput };
@@ -6,6 +6,7 @@ import { ChevronRightIcon } from "./animated-icons/chevron-right";
6
6
  import { Button } from "./button";
7
7
  import { SelectContent, SelectItem, SelectRoot, SelectTrigger, SelectValue, } from "./select-primitive";
8
8
  import { cn } from "./lib/utils";
9
+ import { Typography } from "./typography";
9
10
  const PaginationNav = React.forwardRef(({ className, ...props }, ref) => (_jsx("nav", { ref: ref, role: "navigation", "aria-label": "pagination", className: cn("flex w-full justify-center", className), ...props })));
10
11
  PaginationNav.displayName = "PaginationNav";
11
12
  const PaginationContent = React.forwardRef(({ className, ...props }, ref) => (_jsx("ul", { ref: ref, className: cn("flex flex-row items-center gap-1", className), ...props })));
@@ -16,7 +17,7 @@ function Pagination({ currentPage, totalPages, totalItems, itemsPerPage = 5, onP
16
17
  const normalizedTotalPages = totalPages || 1;
17
18
  const startItem = totalItems === 0 ? 0 : (currentPage - 1) * itemsPerPage + 1;
18
19
  const endItem = Math.min(currentPage * itemsPerPage, totalItems);
19
- return (_jsxs("div", { className: cn("flex items-center justify-between gap-6", className), ...props, children: [_jsxs("div", { className: "text-sm text-muted-foreground", children: ["Showing ", startItem, "-", endItem, " out of ", totalItems] }), _jsxs("div", { className: "flex items-center gap-6", children: [onItemsPerPageChange ? (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-sm text-muted-foreground", children: "Rows per page" }), _jsxs(SelectRoot, { value: itemsPerPage.toString(), onValueChange: (value) => {
20
+ return (_jsxs("div", { className: cn("flex items-center justify-between gap-6", className), ...props, children: [_jsxs("div", { className: "text-sm text-muted-foreground", children: ["Showing ", startItem, "-", endItem, " out of ", totalItems] }), _jsxs("div", { className: "flex items-center gap-6", children: [onItemsPerPageChange ? (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Typography, { as: "span", variant: "caption", children: "Rows per page" }), _jsxs(SelectRoot, { value: itemsPerPage.toString(), onValueChange: (value) => {
20
21
  onItemsPerPageChange(Number(value));
21
22
  onPageChange(1);
22
23
  }, children: [_jsx(SelectTrigger, { className: "h-8 w-[70px] text-sm", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: itemsPerPageOptions.map((option) => (_jsx(SelectItem, { value: option.toString(), children: option }, option))) })] })] })) : null, _jsxs("div", { className: "text-sm text-muted-foreground", children: ["Page ", currentPage, " of ", normalizedTotalPages] }), _jsx(PaginationNav, { className: "mx-0 w-auto justify-start", children: _jsxs(PaginationContent, { children: [_jsx(PaginationItem, { children: _jsx(Button, { type: "button", variant: "outline", size: "icon", className: "h-8 w-8", onClick: () => onPageChange(1), disabled: currentPage === 1, "aria-label": "Go to first page", children: _jsx(ChevronsLeft, { className: "h-4 w-4" }) }) }), _jsx(PaginationItem, { children: _jsx(Button, { type: "button", variant: "outline", size: "icon", className: "h-8 w-8", onClick: () => onPageChange(Math.max(1, currentPage - 1)), disabled: currentPage === 1, "aria-label": "Go to previous page", children: _jsx(ChevronLeftIcon, { size: 16 }) }) }), _jsx(PaginationItem, { children: _jsx(Button, { type: "button", variant: "outline", size: "icon", className: "h-8 w-8", onClick: () => onPageChange(Math.min(normalizedTotalPages, currentPage + 1)), disabled: currentPage === normalizedTotalPages || totalPages === 0, "aria-label": "Go to next page", children: _jsx(ChevronRightIcon, { size: 16 }) }) }), _jsx(PaginationItem, { children: _jsx(Button, { type: "button", variant: "outline", size: "icon", className: "h-8 w-8", onClick: () => onPageChange(normalizedTotalPages), disabled: currentPage === normalizedTotalPages || totalPages === 0, "aria-label": "Go to last page", children: _jsx(ChevronsRight, { className: "h-4 w-4" }) }) })] }) })] })] }));
@@ -0,0 +1,7 @@
1
+ import * as React from "react";
2
+ import * as PopoverPrimitive from "@radix-ui/react-popover";
3
+ declare const Popover: React.FC<PopoverPrimitive.PopoverProps>;
4
+ declare const PopoverTrigger: React.ForwardRefExoticComponent<PopoverPrimitive.PopoverTriggerProps & React.RefAttributes<HTMLButtonElement>>;
5
+ declare const PopoverAnchor: React.ForwardRefExoticComponent<PopoverPrimitive.PopoverAnchorProps & React.RefAttributes<HTMLDivElement>>;
6
+ declare const PopoverContent: React.ForwardRefExoticComponent<Omit<PopoverPrimitive.PopoverContentProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
7
+ export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
@@ -0,0 +1,11 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import * as PopoverPrimitive from "@radix-ui/react-popover";
5
+ import { cn } from "./lib/utils";
6
+ const Popover = PopoverPrimitive.Root;
7
+ const PopoverTrigger = PopoverPrimitive.Trigger;
8
+ const PopoverAnchor = PopoverPrimitive.Anchor;
9
+ const PopoverContent = React.forwardRef(({ className, align = "center", sideOffset = 4, ...props }, ref) => (_jsx(PopoverPrimitive.Portal, { children: _jsx(PopoverPrimitive.Content, { ref: ref, align: align, sideOffset: sideOffset, className: cn("z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", className), ...props }) })));
10
+ PopoverContent.displayName = PopoverPrimitive.Content.displayName;
11
+ export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
@@ -12,6 +12,8 @@ interface SelectContentProps extends React.ComponentPropsWithoutRef<typeof Selec
12
12
  }
13
13
  declare const SelectContent: React.ForwardRefExoticComponent<SelectContentProps & React.RefAttributes<HTMLDivElement>>;
14
14
  declare const SelectLabel: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectLabelProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
15
- declare const SelectItem: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectItemProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
15
+ declare const SelectItem: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectItemProps & React.RefAttributes<HTMLDivElement>, "ref"> & {
16
+ description?: React.ReactNode;
17
+ } & React.RefAttributes<HTMLDivElement>>;
16
18
  declare const SelectSeparator: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectSeparatorProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
17
19
  export { SelectContent, SelectGroup, SelectItem, SelectLabel, SelectRoot, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, };
@@ -4,6 +4,7 @@ import * as React from "react";
4
4
  import * as SelectPrimitive from "@radix-ui/react-select";
5
5
  import { Check, ChevronDown, ChevronUp } from "lucide-react";
6
6
  import { cn } from "./lib/utils";
7
+ import { Typography } from "./typography";
7
8
  function SelectRoot(props) {
8
9
  return _jsx(SelectPrimitive.Root, { "data-slot": "select", ...props });
9
10
  }
@@ -13,7 +14,7 @@ function SelectGroup(props) {
13
14
  function SelectValue(props) {
14
15
  return _jsx(SelectPrimitive.Value, { "data-slot": "select-value", ...props });
15
16
  }
16
- const SelectTrigger = React.forwardRef(({ className, children, ...props }, ref) => (_jsxs(SelectPrimitive.Trigger, { ref: ref, "data-slot": "select-trigger", className: cn("flex h-9 w-full cursor-pointer items-center justify-between gap-2 whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-[invalid=true]:border-destructive aria-[invalid=true]:ring-destructive/20 data-[placeholder]:text-muted-foreground [&>span]:line-clamp-1 [&_svg]:pointer-events-none [&_svg]:shrink-0", className), ...props, children: [children, _jsx(SelectPrimitive.Icon, { asChild: true, children: _jsx(ChevronDown, { className: "h-4 w-4 shrink-0 opacity-50" }) })] })));
17
+ const SelectTrigger = React.forwardRef(({ className, children, ...props }, ref) => (_jsxs(SelectPrimitive.Trigger, { ref: ref, "data-slot": "select-trigger", className: cn("flex h-10 w-full cursor-pointer items-center justify-between gap-2 whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-base transition-[color,box-shadow] md:text-sm outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-[invalid=true]:border-destructive aria-[invalid=true]:ring-destructive/20 data-[placeholder]:text-muted-foreground [&>span]:line-clamp-1 [&_svg]:pointer-events-none [&_svg]:shrink-0", className), ...props, children: [children, _jsx(SelectPrimitive.Icon, { asChild: true, children: _jsx(ChevronDown, { className: "h-4 w-4 shrink-0 opacity-50" }) })] })));
17
18
  SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
18
19
  const SelectScrollUpButton = React.forwardRef(({ className, ...props }, ref) => (_jsx(SelectPrimitive.ScrollUpButton, { ref: ref, "data-slot": "select-scroll-up-button", className: cn("flex cursor-pointer items-center justify-center py-1", className), ...props, children: _jsx(ChevronUp, { className: "h-4 w-4" }) })));
19
20
  SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
@@ -26,7 +27,7 @@ const SelectContent = React.forwardRef(({ className, children, header, position
26
27
  SelectContent.displayName = SelectPrimitive.Content.displayName;
27
28
  const SelectLabel = React.forwardRef(({ className, ...props }, ref) => (_jsx(SelectPrimitive.Label, { ref: ref, "data-slot": "select-label", className: cn("px-2 py-1.5 text-xs text-muted-foreground", className), ...props })));
28
29
  SelectLabel.displayName = SelectPrimitive.Label.displayName;
29
- const SelectItem = React.forwardRef(({ className, children, ...props }, ref) => (_jsxs(SelectPrimitive.Item, { ref: ref, "data-slot": "select-item", className: cn("relative flex w-full cursor-pointer select-none items-center gap-2 rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", className), ...props, children: [_jsx("span", { "data-slot": "select-item-indicator", className: "absolute right-2 flex h-3.5 w-3.5 items-center justify-center", children: _jsx(SelectPrimitive.ItemIndicator, { children: _jsx(Check, { className: "h-4 w-4" }) }) }), _jsx(SelectPrimitive.ItemText, { className: "break-words", children: children })] })));
30
+ const SelectItem = React.forwardRef(({ className, children, description, ...props }, ref) => (_jsxs(SelectPrimitive.Item, { ref: ref, "data-slot": "select-item", className: cn("relative flex h-10 w-full cursor-pointer select-none items-center gap-2 rounded-sm pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", className), ...props, children: [_jsx("span", { "data-slot": "select-item-indicator", className: "absolute right-2 flex h-3.5 w-3.5 items-center justify-center", children: _jsx(SelectPrimitive.ItemIndicator, { children: _jsx(Check, { className: "h-4 w-4" }) }) }), _jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx(SelectPrimitive.ItemText, { className: "break-words font-normal", children: children }), description ? (_jsx(Typography, { as: "span", variant: "caption", children: description })) : null] })] })));
30
31
  SelectItem.displayName = SelectPrimitive.Item.displayName;
31
32
  const SelectSeparator = React.forwardRef(({ className, ...props }, ref) => (_jsx(SelectPrimitive.Separator, { ref: ref, "data-slot": "select-separator", className: cn("-mx-1 my-1 h-px bg-border", className), ...props })));
32
33
  SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
package/dist/select.d.ts CHANGED
@@ -3,6 +3,7 @@ import { SelectRoot } from "./select-primitive";
3
3
  export interface SelectOption {
4
4
  label: React.ReactNode;
5
5
  value: string;
6
+ description?: React.ReactNode;
6
7
  disabled?: boolean;
7
8
  group?: string;
8
9
  textValue?: string;
package/dist/select.js CHANGED
@@ -91,7 +91,7 @@ function Select({ label, options, placeholder = "Select an option", description,
91
91
  event.stopPropagation();
92
92
  } })) : undefined, viewportClassName: searchable ? "max-h-64" : undefined, children: hasVisibleOptions ? (optionGroups.map((group, groupIndex) => {
93
93
  var _a;
94
- return (_jsxs(React.Fragment, { children: [_jsxs(SelectGroup, { children: [group.group ? (_jsx(SelectLabel, { children: _jsx(SearchHighlight, { text: group.group, query: searchQuery }) })) : null, group.options.map((option) => (_jsx(SelectItem, { value: option.value, disabled: option.disabled, textValue: getOptionTextValue(option), children: renderHighlightedLabel(option.label, searchQuery) }, option.value)))] }), groupIndex < optionGroups.length - 1 ? _jsx(SelectSeparator, {}) : null] }, (_a = group.group) !== null && _a !== void 0 ? _a : `group-${groupIndex}`));
94
+ return (_jsxs(React.Fragment, { children: [_jsxs(SelectGroup, { children: [group.group ? (_jsx(SelectLabel, { children: _jsx(SearchHighlight, { text: group.group, query: searchQuery }) })) : null, group.options.map((option) => (_jsx(SelectItem, { value: option.value, disabled: option.disabled, textValue: getOptionTextValue(option), description: option.description, children: renderHighlightedLabel(option.label, searchQuery) }, option.value)))] }), groupIndex < optionGroups.length - 1 ? _jsx(SelectSeparator, {}) : null] }, (_a = group.group) !== null && _a !== void 0 ? _a : `group-${groupIndex}`));
95
95
  })) : (_jsx("div", { className: "px-3 py-6 text-sm text-muted-foreground", children: searchEmptyMessage })) })] }), showDescription ? (_jsx(FieldDescription, { id: descriptionId, className: descriptionClassName, children: description })) : null, _jsx(FieldError, { id: errorId, className: errorClassName, children: error })] }));
96
96
  }
97
97
  export { Select };
@@ -0,0 +1,18 @@
1
+ export interface TabDropdownSection {
2
+ id: string;
3
+ label: string;
4
+ }
5
+ export interface TabWithDropdownProps {
6
+ /** Tab value — must match a TabsContent value. */
7
+ value: string;
8
+ /** Tab label shown in the trigger. */
9
+ label: string;
10
+ /** Dropdown sections shown on hover. */
11
+ sections: TabDropdownSection[];
12
+ /** Called when a section is selected. Receives the section id. */
13
+ onSectionSelect?: (sectionId: string) => void;
14
+ /** Scroll offset in pixels when scrolling to a section. */
15
+ scrollOffset?: number;
16
+ }
17
+ declare function TabWithDropdown({ value, label, sections, onSectionSelect, scrollOffset, }: TabWithDropdownProps): import("react/jsx-runtime").JSX.Element;
18
+ export { TabWithDropdown };
@@ -0,0 +1,70 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import * as ReactDOM from "react-dom";
5
+ import { ChevronDownIcon } from "./animated-icons/chevron-down";
6
+ import { cn } from "./lib/utils";
7
+ import { TabsTrigger, useTabsContext } from "./tabs";
8
+ function TabWithDropdown({ value, label, sections, onSectionSelect, scrollOffset = 200, }) {
9
+ const [isOpen, setIsOpen] = React.useState(false);
10
+ const [dropdownPosition, setDropdownPosition] = React.useState({
11
+ top: 0,
12
+ left: 0,
13
+ });
14
+ const triggerRef = React.useRef(null);
15
+ const timeoutRef = React.useRef(null);
16
+ const tabsContext = useTabsContext();
17
+ const updatePosition = React.useCallback(() => {
18
+ if (triggerRef.current) {
19
+ const rect = triggerRef.current.getBoundingClientRect();
20
+ setDropdownPosition({
21
+ top: rect.bottom + 8,
22
+ left: rect.left,
23
+ });
24
+ }
25
+ }, []);
26
+ const handleMouseEnter = () => {
27
+ if (timeoutRef.current)
28
+ clearTimeout(timeoutRef.current);
29
+ updatePosition();
30
+ setIsOpen(true);
31
+ };
32
+ const handleMouseLeave = () => {
33
+ timeoutRef.current = setTimeout(() => setIsOpen(false), 150);
34
+ };
35
+ const handleDropdownMouseEnter = () => {
36
+ if (timeoutRef.current)
37
+ clearTimeout(timeoutRef.current);
38
+ };
39
+ const handleDropdownMouseLeave = () => {
40
+ timeoutRef.current = setTimeout(() => setIsOpen(false), 150);
41
+ };
42
+ const handleSectionClick = (sectionId) => {
43
+ if (onSectionSelect) {
44
+ onSectionSelect(sectionId);
45
+ }
46
+ else {
47
+ // Default: switch tab and scroll to the section element
48
+ const doScroll = () => {
49
+ const element = document.getElementById(sectionId);
50
+ if (!element)
51
+ return;
52
+ const top = element.getBoundingClientRect().top + window.scrollY - scrollOffset;
53
+ window.scrollTo({ top, behavior: "smooth" });
54
+ };
55
+ if (tabsContext && tabsContext.value !== value) {
56
+ tabsContext.setValue(value);
57
+ setTimeout(doScroll, 50);
58
+ }
59
+ else {
60
+ doScroll();
61
+ }
62
+ }
63
+ setIsOpen(false);
64
+ };
65
+ return (_jsxs(_Fragment, { children: [_jsx("div", { ref: triggerRef, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, children: _jsxs(TabsTrigger, { value: value, className: "flex items-center gap-1", children: [label, sections.length > 0 ? (_jsx(ChevronDownIcon, { className: cn("transition-transform duration-200", isOpen && "rotate-180"), size: 12 })) : null] }) }), isOpen &&
66
+ sections.length > 0 &&
67
+ typeof document !== "undefined" &&
68
+ ReactDOM.createPortal(_jsx("div", { className: "fixed z-[100] rounded-md border bg-popover p-1 shadow-lg animate-in fade-in-0 zoom-in-95", style: { top: dropdownPosition.top, left: dropdownPosition.left }, onMouseEnter: handleDropdownMouseEnter, onMouseLeave: handleDropdownMouseLeave, children: sections.map((section) => (_jsx("button", { type: "button", onClick: () => handleSectionClick(section.id), className: "block w-full rounded-sm px-3 py-2 text-left text-sm transition-colors hover:bg-accent hover:text-accent-foreground", children: section.label }, section.id))) }), document.body)] }));
69
+ }
70
+ export { TabWithDropdown };
package/dist/textarea.js CHANGED
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { Field, FieldDescription, FieldError, FieldLabel } from "./field";
4
4
  import { cn } from "./lib/utils";
5
- const TextareaControl = React.forwardRef(({ className, ...props }, ref) => (_jsx("textarea", { ref: ref, "data-slot": "textarea", className: cn("flex min-h-20 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-[invalid=true]:border-destructive aria-[invalid=true]:ring-destructive/20 md:text-sm", className), ...props })));
5
+ const TextareaControl = React.forwardRef(({ className, ...props }, ref) => (_jsx("textarea", { ref: ref, "data-slot": "textarea", className: cn("flex min-h-20 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-[invalid=true]:border-destructive aria-[invalid=true]:ring-destructive/20 md:text-sm", className), ...props })));
6
6
  TextareaControl.displayName = "TextareaControl";
7
7
  const Textarea = React.forwardRef(({ label, description, error, invalid = false, hideLabel = false, className, textareaClassName, labelClassName, descriptionClassName, errorClassName, id, disabled, "aria-invalid": ariaInvalid, "aria-describedby": ariaDescribedBy, ...props }, ref) => {
8
8
  const generatedId = React.useId();
@@ -0,0 +1,13 @@
1
+ import { toast } from "sonner";
2
+ export interface ToasterProps {
3
+ /** Position of the toast container. */
4
+ position?: "top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right";
5
+ /** Maximum number of toasts visible at once. */
6
+ visibleToasts?: number;
7
+ /** Whether to show the close button on each toast. */
8
+ closeButton?: boolean;
9
+ /** Duration in milliseconds before auto-dismiss. */
10
+ duration?: number;
11
+ }
12
+ declare function Toaster({ position, visibleToasts, closeButton, duration, }: ToasterProps): import("react/jsx-runtime").JSX.Element;
13
+ export { Toaster, toast };
@@ -0,0 +1,18 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { Toaster as SonnerToaster, toast } from "sonner";
4
+ function Toaster({ position = "bottom-right", visibleToasts = 3, closeButton = true, duration = 4000, }) {
5
+ return (_jsx(SonnerToaster, { position: position, visibleToasts: visibleToasts, closeButton: closeButton, duration: duration, toastOptions: {
6
+ classNames: {
7
+ toast: "group border border-border bg-background text-foreground shadow-md rounded-lg p-4 flex items-start gap-3",
8
+ title: "text-sm font-medium",
9
+ description: "text-sm text-muted-foreground",
10
+ closeButton: "text-muted-foreground hover:text-foreground",
11
+ success: "border-[hsl(var(--status-success-foreground))]/20",
12
+ error: "border-destructive/20",
13
+ warning: "border-[hsl(var(--status-warning-foreground))]/20",
14
+ info: "border-[hsl(var(--status-info-foreground))]/20",
15
+ },
16
+ } }));
17
+ }
18
+ export { Toaster, toast };
@@ -6,6 +6,8 @@ declare const typographyVariants: (props?: ({
6
6
  export interface TypographyProps extends React.HTMLAttributes<HTMLElement>, VariantProps<typeof typographyVariants> {
7
7
  as?: keyof React.JSX.IntrinsicElements;
8
8
  asChild?: boolean;
9
+ /** Override the built-in text colour. Use "inherit" to inherit from a parent container. */
10
+ color?: "inherit" | "foreground" | "muted";
9
11
  }
10
12
  declare const Typography: React.ForwardRefExoticComponent<TypographyProps & React.RefAttributes<HTMLElement>>;
11
13
  export { Typography, typographyVariants };
@@ -30,12 +30,19 @@ const defaultElementByVariant = {
30
30
  caption: "span",
31
31
  eyebrow: "span",
32
32
  };
33
- const Typography = React.forwardRef(({ as, asChild = false, className, variant, ...props }, ref) => {
33
+ const Typography = React.forwardRef(({ as, asChild = false, className, variant, color, ...props }, ref) => {
34
34
  const resolvedVariant = (variant !== null && variant !== void 0 ? variant : "body");
35
35
  const Comp = asChild
36
36
  ? Slot
37
37
  : as !== null && as !== void 0 ? as : defaultElementByVariant[resolvedVariant];
38
- return (_jsx(Comp, { ref: ref, className: cn(typographyVariants({ variant: resolvedVariant }), className), ...props }));
38
+ const colorClass = color === "inherit"
39
+ ? "text-inherit"
40
+ : color === "foreground"
41
+ ? "text-foreground"
42
+ : color === "muted"
43
+ ? "text-muted-foreground"
44
+ : undefined;
45
+ return (_jsx(Comp, { ref: ref, className: cn(typographyVariants({ variant: resolvedVariant }), colorClass, className), ...props }));
39
46
  });
40
47
  Typography.displayName = "Typography";
41
48
  export { Typography, typographyVariants };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neasg/design-system",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "NEA shared design system primitives, theme tokens, and Storybook docs.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -15,7 +15,7 @@
15
15
  "README.md"
16
16
  ],
17
17
  "scripts": {
18
- "build": "tsc -p tsconfig.build.json && node scripts/copy-css.mjs",
18
+ "build": "node scripts/clean-dist.mjs && tsc -p tsconfig.build.json && node scripts/copy-css.mjs",
19
19
  "storybook": "storybook dev -p 6007 -c .storybook",
20
20
  "build-storybook": "storybook build -c .storybook",
21
21
  "test": "vitest run",
@@ -33,8 +33,10 @@
33
33
  "@dnd-kit/core": "^6.3.1",
34
34
  "@dnd-kit/sortable": "^10.0.0",
35
35
  "@dnd-kit/utilities": "^3.2.2",
36
+ "@govtechsg/sgds-web-component": "^3.8.1",
36
37
  "@radix-ui/react-avatar": "^1.1.11",
37
38
  "@radix-ui/react-dialog": "^1.1.15",
39
+ "@radix-ui/react-popover": "^1.1.15",
38
40
  "@radix-ui/react-select": "^2.2.6",
39
41
  "@radix-ui/react-slot": "^1.2.4",
40
42
  "@radix-ui/react-tabs": "^1.1.13",
@@ -42,10 +44,13 @@
42
44
  "class-variance-authority": "^0.7.0",
43
45
  "clsx": "^2.1.1",
44
46
  "cmdk": "^1.1.1",
47
+ "date-fns": "^4.1.0",
45
48
  "input-otp": "^1.4.2",
46
49
  "libphonenumber-js": "^1.12.41",
47
50
  "lucide-react": "^0.555.0",
48
51
  "motion": "^12.33.0",
52
+ "react-day-picker": "^9.14.0",
53
+ "sonner": "^2.0.7",
49
54
  "tailwind-merge": "^2.3.0",
50
55
  "tailwindcss-animate": "^1.0.7"
51
56
  },