@ponti-studios/ui 1.2.0 → 1.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.
Files changed (35) hide show
  1. package/package.json +1 -1
  2. package/src/components/data-display/accordion.tsx +10 -1
  3. package/src/components/feedback/empty-state.tsx +1 -1
  4. package/src/components/feedback/file-upload-status.tsx +14 -6
  5. package/src/components/feedback/progress-bar.tsx +3 -2
  6. package/src/components/feedback/spinner.tsx +3 -4
  7. package/src/components/feedback/update-guard.stories.tsx +1 -1
  8. package/src/components/feedback/update-guard.tsx +2 -2
  9. package/src/components/filters/active-filters-bar.stories.tsx +2 -0
  10. package/src/components/filters/active-filters-bar.tsx +21 -23
  11. package/src/components/filters/filter-select.tsx +4 -2
  12. package/src/components/filters/index.ts +0 -4
  13. package/src/components/forms/date-month-select.tsx +2 -2
  14. package/src/components/forms/date-picker.tsx +2 -2
  15. package/src/components/forms/drop-zone.stories.tsx +112 -28
  16. package/src/components/forms/drop-zone.tsx +265 -112
  17. package/src/components/forms/input.tsx +5 -1
  18. package/src/components/forms/radio-group.tsx +3 -2
  19. package/src/components/forms/select.tsx +14 -4
  20. package/src/components/forms/slider.stories.tsx +2 -2
  21. package/src/components/forms/slider.tsx +12 -9
  22. package/src/components/forms/switch.tsx +5 -2
  23. package/src/components/forms/textarea.tsx +5 -1
  24. package/src/components/layout/index.ts +0 -2
  25. package/src/components/layout/section-intro.tsx +1 -1
  26. package/src/components/layout/surface-panel.tsx +10 -2
  27. package/src/components/navigation/tabs.tsx +4 -1
  28. package/src/components/overlays/command.tsx +39 -14
  29. package/src/components/overlays/popover.stories.tsx +16 -4
  30. package/src/components/primitives/badge.tsx +1 -1
  31. package/src/components/primitives/button.tsx +6 -1
  32. package/src/components/filters/filters.stories.tsx +0 -72
  33. package/src/components/filters/filters.tsx +0 -72
  34. package/src/components/filters/search-filter-bar.stories.tsx +0 -39
  35. package/src/components/filters/search-filter-bar.tsx +0 -46
@@ -2,6 +2,7 @@ import { Combobox as CommandPrimitive } from "@base-ui/react/combobox";
2
2
  import { SearchIcon } from "lucide-react";
3
3
  import * as React from "react";
4
4
 
5
+ import { cn } from "../../lib/utils";
5
6
  import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "./dialog";
6
7
 
7
8
  function Command({
@@ -14,7 +15,10 @@ function Command({
14
15
  return (
15
16
  <div
16
17
  data-slot="command"
17
- className={`bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md ${className ?? ""}`}
18
+ className={cn(
19
+ "bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
20
+ className,
21
+ )}
18
22
  >
19
23
  <CommandPrimitive.Root {...props}>{children}</CommandPrimitive.Root>
20
24
  </div>
@@ -63,7 +67,11 @@ function CommandInput({
63
67
  <CommandPrimitive.Input
64
68
  data-slot="command-input"
65
69
  aria-label={ariaLabel}
66
- className={`placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50 ${className ?? ""}`}
70
+ className={cn(
71
+ "placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden",
72
+ "disabled:cursor-not-allowed disabled:opacity-50",
73
+ className,
74
+ )}
67
75
  onChange={(event) => {
68
76
  onChange?.(event);
69
77
  onValueChange?.(event.currentTarget.value);
@@ -74,14 +82,18 @@ function CommandInput({
74
82
  );
75
83
  }
76
84
 
77
- function CommandList({ children, ...props }: React.ComponentProps<typeof CommandPrimitive.List>) {
85
+ function CommandList({
86
+ children,
87
+ className,
88
+ ...props
89
+ }: React.ComponentProps<typeof CommandPrimitive.List>) {
78
90
  if (typeof children === "function") {
79
91
  return (
80
92
  <CommandPrimitive.List
81
93
  data-slot="command-list"
82
94
  aria-label="Command results"
83
95
  tabIndex={0}
84
- className="max-h-75 scroll-py-1 overflow-x-hidden overflow-y-auto"
96
+ className={cn("max-h-75 scroll-py-1 overflow-x-hidden overflow-y-auto", className)}
85
97
  {...props}
86
98
  >
87
99
  {children}
@@ -103,7 +115,7 @@ function CommandList({ children, ...props }: React.ComponentProps<typeof Command
103
115
  data-slot="command-list"
104
116
  aria-label="Command results"
105
117
  tabIndex={0}
106
- className="max-h-75 scroll-py-1 overflow-x-hidden overflow-y-auto"
118
+ className={cn("max-h-75 scroll-py-1 overflow-x-hidden overflow-y-auto", className)}
107
119
  {...props}
108
120
  >
109
121
  {listChildren}
@@ -113,11 +125,14 @@ function CommandList({ children, ...props }: React.ComponentProps<typeof Command
113
125
  );
114
126
  }
115
127
 
116
- function CommandEmpty({ ...props }: React.ComponentProps<typeof CommandPrimitive.Empty>) {
128
+ function CommandEmpty({
129
+ className,
130
+ ...props
131
+ }: React.ComponentProps<typeof CommandPrimitive.Empty>) {
117
132
  return (
118
133
  <CommandPrimitive.Empty
119
134
  data-slot="command-empty"
120
- className="py-6 text-center text-sm"
135
+ className={cn("py-6 text-center text-sm", className)}
121
136
  {...props}
122
137
  />
123
138
  );
@@ -132,7 +147,7 @@ function CommandGroup({
132
147
  return (
133
148
  <CommandPrimitive.Group
134
149
  data-slot="command-group"
135
- className={`text-foreground overflow-hidden p-1 ${className ?? ""}`}
150
+ className={cn("text-foreground overflow-hidden p-1", className)}
136
151
  {...props}
137
152
  >
138
153
  {heading ? (
@@ -153,7 +168,11 @@ function CommandItem({ className, onSelect, onClick, ...props }: CommandItemProp
153
168
  return (
154
169
  <CommandPrimitive.Item
155
170
  data-slot="command-item"
156
- className={`data-highlighted:bg-muted data-highlighted:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 ${className ?? ""}`}
171
+ className={cn(
172
+ "data-highlighted:bg-muted data-highlighted:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-hidden select-none",
173
+ "data-disabled:pointer-events-none data-disabled:opacity-50",
174
+ className,
175
+ )}
157
176
  onClick={(event) => {
158
177
  onClick?.(event);
159
178
  onSelect?.();
@@ -168,24 +187,30 @@ function CommandSeparator({ className, ...props }: React.ComponentProps<"div">)
168
187
  <div
169
188
  data-slot="command-separator"
170
189
  aria-hidden="true"
171
- className={`bg-border -mx-1 h-px ${className ?? ""}`}
190
+ className={cn("bg-border -mx-1 h-px", className)}
172
191
  {...props}
173
192
  />
174
193
  );
175
194
  }
176
195
 
177
- function CommandShortcut(props: React.ComponentProps<"span">) {
196
+ function CommandShortcut({ className, ...props }: React.ComponentProps<"span">) {
178
197
  return (
179
198
  <span
180
199
  data-slot="command-shortcut"
181
- className="text-muted-foreground ml-auto text-xs tracking-widest"
200
+ className={cn("text-muted-foreground ml-auto text-xs tracking-widest", className)}
182
201
  {...props}
183
202
  />
184
203
  );
185
204
  }
186
205
 
187
- function CommandListLoading(props: React.ComponentProps<"div">) {
188
- return <div data-slot="command-list-loading" className="py-6 text-center text-sm" {...props} />;
206
+ function CommandListLoading({ className, ...props }: React.ComponentProps<"div">) {
207
+ return (
208
+ <div
209
+ data-slot="command-list-loading"
210
+ className={cn("py-6 text-center text-sm", className)}
211
+ {...props}
212
+ />
213
+ );
189
214
  }
190
215
 
191
216
  export {
@@ -25,12 +25,24 @@ export const Default: Story = {
25
25
  </div>
26
26
  <div className="grid gap-4 pt-4">
27
27
  <div className="grid grid-cols-3 items-center gap-4">
28
- <label className="text-sm">Width</label>
29
- <input className="col-span-2 h-8 rounded border px-2 text-sm" defaultValue="100%" />
28
+ <label htmlFor="popover-width" className="text-sm">
29
+ Width
30
+ </label>
31
+ <input
32
+ id="popover-width"
33
+ className="col-span-2 h-8 rounded border px-2 text-sm"
34
+ defaultValue="100%"
35
+ />
30
36
  </div>
31
37
  <div className="grid grid-cols-3 items-center gap-4">
32
- <label className="text-sm">Height</label>
33
- <input className="col-span-2 h-8 rounded border px-2 text-sm" defaultValue="25px" />
38
+ <label htmlFor="popover-height" className="text-sm">
39
+ Height
40
+ </label>
41
+ <input
42
+ id="popover-height"
43
+ className="col-span-2 h-8 rounded border px-2 text-sm"
44
+ defaultValue="25px"
45
+ />
34
46
  </div>
35
47
  </div>
36
48
  </PopoverContent>
@@ -12,7 +12,7 @@ const badgeVariants = cva(
12
12
  secondary: "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/80",
13
13
  destructive: "bg-destructive text-destructive-foreground [a&]:hover:bg-destructive/90",
14
14
  outline: "border-border text-foreground [a&]:hover:bg-muted",
15
- ghost: "[a&]:[a&]: [a&]: [a&]:",
15
+ ghost: "bg-transparent text-foreground [a&]:hover:bg-muted",
16
16
  link: "text-foreground underline-offset-4 [a&]:hover:underline",
17
17
  /** Quiet metadata marker for real reference IDs (case/appointment/feed
18
18
  * numbers) — never for decorative sequence markers. */
@@ -6,7 +6,12 @@ import { cn } from "../../lib/utils";
6
6
  import { Spinner } from "../feedback/spinner";
7
7
 
8
8
  const buttonVariants = cva(
9
- "inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md border border-transparent font-medium transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9
+ [
10
+ "inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md border border-transparent font-medium transition-colors outline-none",
11
+ "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
12
+ "disabled:pointer-events-none disabled:opacity-50",
13
+ "[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
14
+ ],
10
15
  {
11
16
  variants: {
12
17
  variant: {
@@ -1,72 +0,0 @@
1
- import type { Meta, StoryObj } from "@storybook/react-vite";
2
- import { expect, within } from "storybook/test";
3
-
4
- import { Input } from "../forms/input";
5
- import { Filters } from "./filters";
6
-
7
- const meta = {
8
- title: "Filters/Compound",
9
- component: Filters,
10
- tags: ["autodocs"],
11
- args: {
12
- children: null,
13
- },
14
- } satisfies Meta<typeof Filters>;
15
-
16
- export default meta;
17
- type Story = StoryObj<typeof meta>;
18
-
19
- const statusOptions = [
20
- { value: "active", label: "Active" },
21
- { value: "interview", label: "Interview" },
22
- { value: "offer", label: "Offer" },
23
- ];
24
-
25
- export const Default: Story = {
26
- render: () => (
27
- <div className="w-[720px]">
28
- <Filters>
29
- <Filters.Search label="Search">
30
- <Input placeholder="Search applications..." aria-label="Search applications" />
31
- </Filters.Search>
32
- <Filters.Select
33
- value="active"
34
- options={statusOptions}
35
- onChange={() => {}}
36
- placeholder="All statuses"
37
- label="Status"
38
- />
39
- <Filters.Select
40
- value=""
41
- options={[{ value: "engineer", label: "Engineer" }]}
42
- onChange={() => {}}
43
- placeholder="All roles"
44
- label="Role"
45
- />
46
- <Filters.Active
47
- filters={[
48
- { id: "status", label: "Status: Active", onRemove: () => {} },
49
- { id: "sort", label: "Date", onRemove: () => {}, variant: "sort", direction: "desc" },
50
- ]}
51
- onClearAll={() => {}}
52
- />
53
- </Filters>
54
- </div>
55
- ),
56
- play: async ({ canvasElement }) => {
57
- const canvas = within(canvasElement);
58
-
59
- await expect(canvas.getByText("Search")).toBeInTheDocument();
60
- await expect(canvas.getByRole("textbox", { name: "Search applications" })).toBeInTheDocument();
61
-
62
- await expect(canvas.getByText("Status")).toBeInTheDocument();
63
- await expect(canvas.getByRole("combobox", { name: "Status" })).toBeInTheDocument();
64
-
65
- await expect(canvas.getByText("Role")).toBeInTheDocument();
66
- await expect(canvas.getByRole("combobox", { name: "Role" })).toBeInTheDocument();
67
-
68
- await expect(canvas.getByText("Status: Active")).toBeInTheDocument();
69
- await expect(canvas.getByText("Date")).toBeInTheDocument();
70
- await expect(canvas.getByRole("button", { name: "Clear all" })).toBeInTheDocument();
71
- },
72
- };
@@ -1,72 +0,0 @@
1
- import { Children, isValidElement, type ReactNode } from "react";
2
-
3
- import { cn } from "../../lib/utils";
4
- import { ActiveFiltersBar, type ActiveFiltersBarProps } from "./active-filters-bar";
5
- import { FilterSelect, type FilterSelectProps } from "./filter-select";
6
- import { Label } from "../primitives/label";
7
-
8
- export function FiltersSearch({ label, children }: { label: string; children: ReactNode }) {
9
- const id = `filters-search-${label.toLowerCase().replace(/\s+/g, "-")}`;
10
- return (
11
- <div className="flex-1 space-y-2">
12
- <Label htmlFor={id}>{label}</Label>
13
- {children}
14
- </div>
15
- );
16
- }
17
- FiltersSearch.slot = "search";
18
-
19
- export function FiltersSelect(props: FilterSelectProps) {
20
- return <FilterSelect {...props} label={props.label ?? props.placeholder} />;
21
- }
22
- FiltersSelect.slot = "select";
23
-
24
- export function FiltersActive(props: ActiveFiltersBarProps) {
25
- return <ActiveFiltersBar {...props} />;
26
- }
27
- FiltersActive.slot = "active";
28
-
29
- export interface FiltersProps {
30
- children: ReactNode;
31
- className?: string;
32
- }
33
-
34
- function FiltersRoot({ children, className }: FiltersProps) {
35
- const search: ReactNode[] = [];
36
- const selects: ReactNode[] = [];
37
- const active: ReactNode[] = [];
38
-
39
- Children.forEach(children, (child) => {
40
- if (!isValidElement(child)) return;
41
- const slot = (child.type as any)?.slot;
42
- if (slot === "search") search.push(child);
43
- else if (slot === "select") selects.push(child);
44
- else if (slot === "active") active.push(child);
45
- });
46
-
47
- return (
48
- <div className={cn("flex flex-col gap-4", className)}>
49
- <div className="flex flex-col gap-3 lg:flex-row lg:items-start">
50
- {search}
51
- {selects.map((s, i) => (
52
- <div key={i} className="sm:w-48">
53
- {s}
54
- </div>
55
- ))}
56
- </div>
57
- {active}
58
- </div>
59
- );
60
- }
61
-
62
- type FiltersComponent = typeof FiltersRoot & {
63
- Search: typeof FiltersSearch;
64
- Select: typeof FiltersSelect;
65
- Active: typeof FiltersActive;
66
- };
67
-
68
- export const Filters: FiltersComponent = Object.assign(FiltersRoot, {
69
- Search: FiltersSearch,
70
- Select: FiltersSelect,
71
- Active: FiltersActive,
72
- });
@@ -1,39 +0,0 @@
1
- import type { Meta, StoryObj } from "@storybook/react-vite";
2
-
3
- import { Input } from "../forms/input";
4
- import { FilterSelect } from "./filter-select";
5
- import { SearchFilterBar } from "./search-filter-bar";
6
-
7
- const meta = {
8
- title: "Filters/SearchFilterBar",
9
- component: SearchFilterBar,
10
- tags: ["autodocs"],
11
- } satisfies Meta<typeof SearchFilterBar>;
12
-
13
- export default meta;
14
- type Story = StoryObj<typeof meta>;
15
-
16
- export const Legacy: Story = {
17
- args: {
18
- activeFilters: [],
19
- },
20
- render: () => (
21
- <div className="w-[720px]">
22
- <SearchFilterBar
23
- activeFilters={[{ id: "status", label: "Status: Active", onRemove: () => {} }]}
24
- onClearAll={() => {}}
25
- search={<Input placeholder="Search..." aria-label="Search" />}
26
- filters={[
27
- <FilterSelect
28
- key="status"
29
- value="active"
30
- options={[{ value: "active", label: "Active" }]}
31
- onChange={() => {}}
32
- placeholder="All"
33
- label="Status"
34
- />,
35
- ]}
36
- />
37
- </div>
38
- ),
39
- };
@@ -1,46 +0,0 @@
1
- import { type ReactNode } from "react";
2
-
3
- import { cn } from "../../lib/utils";
4
- import { ActiveFiltersBar, type ActiveFilter } from "./active-filters-bar";
5
-
6
- export interface SearchFilterBarProps {
7
- activeFilters: ActiveFilter[];
8
- onClearAll?: () => void;
9
- search?: ReactNode;
10
- filters?: ReactNode[];
11
- results?: ReactNode;
12
- children?: ReactNode;
13
- className?: string;
14
- filterItemClassName?: string;
15
- }
16
-
17
- export function SearchFilterBar({
18
- activeFilters,
19
- onClearAll,
20
- search,
21
- filters,
22
- results,
23
- children,
24
- className,
25
- filterItemClassName = "sm:w-48",
26
- }: SearchFilterBarProps) {
27
- return (
28
- <div className={cn("flex flex-col gap-4", className)}>
29
- <div className="flex flex-col gap-3 lg:flex-row lg:items-center">
30
- {search ? <div className="flex-1">{search}</div> : null}
31
-
32
- {filters?.map((filter, index) => (
33
- <div key={index} className={filterItemClassName}>
34
- {filter}
35
- </div>
36
- ))}
37
-
38
- {results ? <div className="self-start lg:ml-auto lg:self-auto">{results}</div> : null}
39
-
40
- {children}
41
- </div>
42
-
43
- <ActiveFiltersBar filters={activeFilters} onClearAll={onClearAll} />
44
- </div>
45
- );
46
- }