@ponti-studios/ui 0.7.1 → 0.8.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 (41) hide show
  1. package/README.md +13 -0
  2. package/package.json +22 -20
  3. package/src/components/data-display/accordion.tsx +1 -1
  4. package/src/components/data-display/sort-controls.tsx +3 -3
  5. package/src/components/feedback/index.ts +0 -2
  6. package/src/components/forms/date-month-select.tsx +12 -3
  7. package/src/components/forms/date-picker.tsx +16 -12
  8. package/src/components/forms/drop-zone.tsx +1 -1
  9. package/src/components/forms/index.ts +4 -8
  10. package/src/components/forms/passkey-management.tsx +4 -10
  11. package/src/components/forms/radio-group.tsx +8 -4
  12. package/src/components/forms/select.tsx +1 -1
  13. package/src/components/forms/text-field.tsx +40 -5
  14. package/src/components/navigation/app-navigation.stories.tsx +0 -1
  15. package/src/components/navigation/app-navigation.tsx +67 -61
  16. package/src/components/navigation/index.ts +7 -4
  17. package/src/components/navigation/pagination-controls.stories.tsx +1 -1
  18. package/src/components/navigation/pagination-controls.tsx +10 -14
  19. package/src/components/overlays/dropdown-menu.stories.tsx +2 -2
  20. package/src/components/primitives/card.tsx +6 -10
  21. package/src/components/primitives/label.stories.tsx +0 -18
  22. package/src/components/primitives/label.tsx +12 -12
  23. package/src/constants/chart-colors.ts +16 -16
  24. package/src/styles/animations.css +0 -22
  25. package/src/styles/index.css +60 -6
  26. package/src/styles/tokens/color-systems.stories.tsx +2 -1
  27. package/src/styles/tokens/generated/light.colors.generated.ts +3 -3
  28. package/src/styles/tokens/generated/light.css +3 -3
  29. package/src/styles/tokens/source/light.tokens.json +3 -3
  30. package/src/styles/tokens/style-dictionary.light-storybook.json +16 -0
  31. package/src/components/feedback/file-upload-status-badge.stories.tsx +0 -28
  32. package/src/components/feedback/file-upload-status-badge.tsx +0 -33
  33. package/src/components/forms/field.stories.tsx +0 -62
  34. package/src/components/forms/field.tsx +0 -65
  35. package/src/components/forms/field.types.ts +0 -7
  36. package/src/components/forms/group-by-select.stories.tsx +0 -26
  37. package/src/components/forms/group-by-select.tsx +0 -47
  38. package/src/components/navigation/route-link.stories.tsx +0 -30
  39. package/src/components/navigation/route-link.tsx +0 -42
  40. package/src/hooks/use-derived-filter-state.stories.tsx +0 -136
  41. package/src/hooks/use-filter-state.stories.tsx +0 -102
@@ -6,7 +6,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
6
6
  return (
7
7
  <div
8
8
  data-slot="card"
9
- className={cn("bg-card text-card-foreground flex flex-col rounded-xl border", className)}
9
+ className={cn("bg-card text-card-foreground flex flex-col gap-4 rounded border", className)}
10
10
  {...props}
11
11
  />
12
12
  );
@@ -17,7 +17,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
17
17
  <div
18
18
  data-slot="card-header"
19
19
  className={cn(
20
- "flex items-center justify-between gap-2 border-b px-4 py-3 has-data-[slot=card-action]:grid has-data-[slot=card-action]:grid-cols-[1fr_auto]",
20
+ "flex flex-col items-start justify-between gap-1 p-4 has-data-[slot=card-action]:grid has-data-[slot=card-action]:grid-cols-[1fr_auto]",
21
21
  className,
22
22
  )}
23
23
  {...props}
@@ -26,14 +26,14 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
26
26
  }
27
27
 
28
28
  function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
29
- return <div data-slot="card-title" className={cn(className)} {...props} />;
29
+ return <div data-slot="card-title" className={cn("text-xl", className)} {...props} />;
30
30
  }
31
31
 
32
32
  function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
33
33
  return (
34
34
  <div
35
35
  data-slot="card-description"
36
- className={cn("text-muted-foreground text-sm", className)}
36
+ className={cn("text-muted-foreground text-xs", className)}
37
37
  {...props}
38
38
  />
39
39
  );
@@ -55,12 +55,8 @@ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
55
55
 
56
56
  function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
57
57
  return (
58
- <div
59
- data-slot="card-footer"
60
- className={cn("flex items-center border-t px-4 py-3", className)}
61
- {...props}
62
- />
58
+ <div data-slot="card-footer" className={cn("flex items-center p-4", className)} {...props} />
63
59
  );
64
60
  }
65
61
 
66
- export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent };
62
+ export { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle };
@@ -27,21 +27,3 @@ export const WithInput: Story = {
27
27
  </div>
28
28
  ),
29
29
  };
30
-
31
- export const Disabled: Story = {
32
- render: () => (
33
- <div className="flex w-full max-w-sm flex-col gap-2">
34
- <Label htmlFor="disabled-field">Subscription plan</Label>
35
- <div className="flex items-center gap-3">
36
- <Input id="disabled-field" className="peer" disabled placeholder="Enterprise" />
37
- <Label htmlFor="disabled-field" className="shrink-0">
38
- (locked)
39
- </Label>
40
- </div>
41
- <p className="text-muted-foreground text-xs">
42
- The <code className="font-mono">peer-disabled</code> variant mutes the label when its paired
43
- input is disabled.
44
- </p>
45
- </div>
46
- ),
47
- };
@@ -1,20 +1,20 @@
1
- "use client";
2
-
3
- import { cva, type VariantProps } from "class-variance-authority";
4
1
  import * as React from "react";
5
2
 
6
3
  import { cn } from "../../lib/utils";
7
4
 
8
- const labelVariants = cva(
9
- "text-sm font-medium leading-none text-foreground peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
5
+ const Label = React.forwardRef<HTMLLabelElement, React.LabelHTMLAttributes<HTMLLabelElement>>(
6
+ ({ className, ...props }, ref) => (
7
+ <label
8
+ htmlFor={props.htmlFor}
9
+ ref={ref}
10
+ className={cn(
11
+ "text-sm font-medium leading-none text-foreground peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
12
+ className,
13
+ )}
14
+ {...props}
15
+ />
16
+ ),
10
17
  );
11
-
12
- const Label = React.forwardRef<
13
- HTMLLabelElement,
14
- React.LabelHTMLAttributes<HTMLLabelElement> & VariantProps<typeof labelVariants>
15
- >(({ className, ...props }, ref) => (
16
- <label ref={ref} className={cn(labelVariants(), className)} {...props} />
17
- ));
18
18
  Label.displayName = "Label";
19
19
 
20
20
  export { Label };
@@ -4,15 +4,15 @@
4
4
  */
5
5
 
6
6
  export const CHART_COLORS = {
7
- chart1: "var(--color-chart-1)",
8
- chart2: "var(--color-chart-2)",
9
- chart3: "var(--color-chart-3)",
10
- chart4: "var(--color-chart-4)",
11
- chart5: "var(--color-chart-5)",
7
+ chart1: "var(--chart-1)",
8
+ chart2: "var(--chart-2)",
9
+ chart3: "var(--chart-3)",
10
+ chart4: "var(--chart-4)",
11
+ chart5: "var(--chart-5)",
12
12
 
13
- positive: "var(--color-chart-positive)",
14
- negative: "var(--color-chart-negative)",
15
- neutral: "var(--color-chart-neutral)",
13
+ positive: "var(--chart-positive)",
14
+ negative: "var(--chart-negative)",
15
+ neutral: "var(--chart-neutral)",
16
16
 
17
17
  background: "var(--color-background)",
18
18
  grid: "var(--color-border)",
@@ -28,12 +28,12 @@ export const CHART_COLORS = {
28
28
  } as const;
29
29
 
30
30
  export const CHART_CSS_VARS = {
31
- positive: "var(--color-chart-positive)",
32
- negative: "var(--color-chart-negative)",
33
- neutral: "var(--color-chart-neutral)",
34
- chart1: "var(--color-chart-1)",
35
- chart2: "var(--color-chart-2)",
36
- chart3: "var(--color-chart-3)",
37
- chart4: "var(--color-chart-4)",
38
- chart5: "var(--color-chart-5)",
31
+ positive: "var(--chart-positive)",
32
+ negative: "var(--chart-negative)",
33
+ neutral: "var(--chart-neutral)",
34
+ chart1: "var(--chart-1)",
35
+ chart2: "var(--chart-2)",
36
+ chart3: "var(--chart-3)",
37
+ chart4: "var(--chart-4)",
38
+ chart5: "var(--chart-5)",
39
39
  } as const;
@@ -51,28 +51,6 @@
51
51
  }
52
52
  }
53
53
 
54
- .animate-in {
55
- animation:
56
- fade-in 150ms ease-out,
57
- zoom-in 150ms ease-out;
58
- }
59
- .animate-out {
60
- animation:
61
- fade-out 120ms ease-in,
62
- zoom-out 120ms ease-in;
63
- }
64
- .fade-in-0 {
65
- animation-name: fade-in;
66
- }
67
- .fade-out-0 {
68
- animation-name: fade-out;
69
- }
70
- .zoom-in-95 {
71
- animation-name: zoom-in;
72
- }
73
- .zoom-out-95 {
74
- animation-name: zoom-out;
75
- }
76
54
  .data-open\:animate-in[data-open],
77
55
  .data-open\:animate-in[data-state="open"] {
78
56
  animation:
@@ -19,6 +19,30 @@
19
19
  @import "./tokens/generated/dark.css" (prefers-color-scheme: dark);
20
20
  @import "./animations.css";
21
21
 
22
+ @theme {
23
+ --animate-accordion-down: accordion-down 200ms ease-out;
24
+ --animate-accordion-up: accordion-up 200ms ease-out;
25
+ --animate-in: fade-in 150ms ease-out, zoom-in 150ms ease-out;
26
+ --animate-out: fade-out 120ms ease-in, zoom-out 120ms ease-in;
27
+ --animate-fade-in: fade-in 150ms ease-out;
28
+ --animate-fade-out: fade-out 120ms ease-in;
29
+ --animate-zoom-in: zoom-in 150ms ease-out;
30
+ --animate-zoom-out: zoom-out 120ms ease-in;
31
+ }
32
+
33
+ @utility fade-in-0 {
34
+ animation-name: fade-in;
35
+ }
36
+ @utility fade-out-0 {
37
+ animation-name: fade-out;
38
+ }
39
+ @utility zoom-in-95 {
40
+ animation-name: zoom-in;
41
+ }
42
+ @utility zoom-out-95 {
43
+ animation-name: zoom-out;
44
+ }
45
+
22
46
  /* shadcn New York semantic contract. Values are generated from the DTCG
23
47
  * source; this file only names the roles consumed by web components. */
24
48
  @theme inline {
@@ -101,22 +125,52 @@
101
125
  * Default fade size: 16 px (scoped to the element via inline var).
102
126
  * Disable with scroll-fade-none (sets --scroll-fade-mask: none). */
103
127
  @utility scroll-fade-x {
104
- mask-image: var(--scroll-fade-mask, linear-gradient(to right, transparent, black var(--scroll-fade-size, 16px), black calc(100% - var(--scroll-fade-size, 16px)), transparent));
128
+ mask-image: var(
129
+ --scroll-fade-mask,
130
+ linear-gradient(
131
+ to right,
132
+ transparent,
133
+ black var(--scroll-fade-size, 16px),
134
+ black calc(100% - var(--scroll-fade-size, 16px)),
135
+ transparent
136
+ )
137
+ );
105
138
  }
106
139
  @utility scroll-fade-y {
107
- mask-image: var(--scroll-fade-mask, linear-gradient(to bottom, transparent, black var(--scroll-fade-size, 16px), black calc(100% - var(--scroll-fade-size, 16px)), transparent));
140
+ mask-image: var(
141
+ --scroll-fade-mask,
142
+ linear-gradient(
143
+ to bottom,
144
+ transparent,
145
+ black var(--scroll-fade-size, 16px),
146
+ black calc(100% - var(--scroll-fade-size, 16px)),
147
+ transparent
148
+ )
149
+ );
108
150
  }
109
151
  @utility scroll-fade-l {
110
- mask-image: var(--scroll-fade-mask, linear-gradient(to right, transparent, black var(--scroll-fade-size, 16px)));
152
+ mask-image: var(
153
+ --scroll-fade-mask,
154
+ linear-gradient(to right, transparent, black var(--scroll-fade-size, 16px))
155
+ );
111
156
  }
112
157
  @utility scroll-fade-r {
113
- mask-image: var(--scroll-fade-mask, linear-gradient(to left, transparent, black var(--scroll-fade-size, 16px)));
158
+ mask-image: var(
159
+ --scroll-fade-mask,
160
+ linear-gradient(to left, transparent, black var(--scroll-fade-size, 16px))
161
+ );
114
162
  }
115
163
  @utility scroll-fade-t {
116
- mask-image: var(--scroll-fade-mask, linear-gradient(to bottom, transparent, black var(--scroll-fade-size, 16px)));
164
+ mask-image: var(
165
+ --scroll-fade-mask,
166
+ linear-gradient(to bottom, transparent, black var(--scroll-fade-size, 16px))
167
+ );
117
168
  }
118
169
  @utility scroll-fade-b {
119
- mask-image: var(--scroll-fade-mask, linear-gradient(to top, transparent, black var(--scroll-fade-size, 16px)));
170
+ mask-image: var(
171
+ --scroll-fade-mask,
172
+ linear-gradient(to top, transparent, black var(--scroll-fade-size, 16px))
173
+ );
120
174
  }
121
175
  @utility scroll-fade-none {
122
176
  --scroll-fade-mask: none;
@@ -1,6 +1,7 @@
1
1
  import type { Meta, StoryObj } from "@storybook/react-vite";
2
2
 
3
3
  import { Button } from "../../components/primitives/button";
4
+ import { colors } from "./css-vars";
4
5
  import { colorTokenNames } from "./index";
5
6
 
6
7
  const meta: Meta = { title: "Foundations/Colors", parameters: { layout: "fullscreen" } };
@@ -28,7 +29,7 @@ export const SystemAppearance: Story = {
28
29
  <div
29
30
  key={token}
30
31
  className="border-border grid min-h-20 content-end rounded-md border p-3"
31
- style={{ backgroundColor: `var(--color-${token})` }}
32
+ style={{ backgroundColor: colors[token] }}
32
33
  >
33
34
  <span className="bg-background text-foreground w-fit rounded px-1 font-mono text-xs">
34
35
  {token}
@@ -24,8 +24,8 @@ export default {
24
24
  "chart-3": "#12a594",
25
25
  "chart-4": "#f76b15",
26
26
  "chart-5": "#8b8d98",
27
- "chart-positive": "#30a46c",
28
- "chart-negative": "#e5484d",
29
- "chart-neutral": "#b9bbc6",
27
+ "chart-positive": "#1f7a50",
28
+ "chart-negative": "#be2e33",
29
+ "chart-neutral": "#626a73",
30
30
  "overlay-scrim": "#000000",
31
31
  };
@@ -24,8 +24,8 @@
24
24
  --chart-3: #12a594;
25
25
  --chart-4: #f76b15;
26
26
  --chart-5: #8b8d98;
27
- --chart-positive: #30a46c;
28
- --chart-negative: #e5484d;
29
- --chart-neutral: #b9bbc6;
27
+ --chart-positive: #1f7a50;
28
+ --chart-negative: #be2e33;
29
+ --chart-neutral: #626a73;
30
30
  --overlay-scrim: #000000;
31
31
  }
@@ -88,15 +88,15 @@
88
88
  },
89
89
  "chart-positive": {
90
90
  "$type": "color",
91
- "$value": "rgb(48, 164, 108)"
91
+ "$value": "rgb(31, 122, 80)"
92
92
  },
93
93
  "chart-negative": {
94
94
  "$type": "color",
95
- "$value": "rgb(229, 72, 77)"
95
+ "$value": "rgb(190, 46, 51)"
96
96
  },
97
97
  "chart-neutral": {
98
98
  "$type": "color",
99
- "$value": "rgb(185, 187, 198)"
99
+ "$value": "rgb(98, 106, 115)"
100
100
  },
101
101
  "overlay-scrim": {
102
102
  "$type": "color",
@@ -0,0 +1,16 @@
1
+ {
2
+ "source": ["src/styles/tokens/source/light.tokens.json"],
3
+ "platforms": {
4
+ "css": {
5
+ "transformGroup": "css",
6
+ "buildPath": ".storybook/generated/",
7
+ "files": [
8
+ {
9
+ "destination": "light-theme-class.css",
10
+ "format": "css/variables",
11
+ "options": { "selector": ":root.storybook-theme-light" }
12
+ }
13
+ ]
14
+ }
15
+ }
16
+ }
@@ -1,28 +0,0 @@
1
- import type { Meta, StoryObj } from "@storybook/react-vite";
2
-
3
- import { FileUploadStatusBadge } from "./file-upload-status-badge";
4
-
5
- const meta = {
6
- title: "Feedback/FileUploadStatusBadge",
7
- component: FileUploadStatusBadge,
8
- tags: ["autodocs"],
9
- } satisfies Meta<typeof FileUploadStatusBadge>;
10
-
11
- export default meta;
12
- type Story = StoryObj<typeof meta>;
13
-
14
- export const Default: Story = {
15
- args: { status: "uploading" },
16
- };
17
-
18
- export const AllStatuses: Story = {
19
- render: () => (
20
- <div className="flex flex-wrap gap-2">
21
- <FileUploadStatusBadge status="uploading" />
22
- <FileUploadStatusBadge status="processing" />
23
- <FileUploadStatusBadge status="queued" />
24
- <FileUploadStatusBadge status="done" />
25
- <FileUploadStatusBadge status="error" />
26
- </div>
27
- ),
28
- };
@@ -1,33 +0,0 @@
1
- import { memo } from "react";
2
-
3
- import { cn } from "../../lib/utils";
4
-
5
- export type FileUploadStatusValue = "uploading" | "processing" | "queued" | "done" | "error";
6
-
7
- const STATUS_CONFIG: Record<FileUploadStatusValue, { bg: string; text: string; label: string }> = {
8
- uploading: { bg: "bg-muted", text: "text-foreground", label: "Uploading" },
9
- processing: { bg: "bg-muted", text: "text-foreground", label: "Processing" },
10
- queued: { bg: "bg-secondary", text: "text-secondary-foreground", label: "Queued" },
11
- done: { bg: "bg-muted", text: "text-muted-foreground", label: "Complete" },
12
- error: { bg: "bg-destructive/10", text: "text-destructive-text", label: "Error" },
13
- };
14
-
15
- export const FileUploadStatusBadge = memo(function FileUploadStatusBadge({
16
- status,
17
- }: {
18
- status?: string | undefined;
19
- }) {
20
- if (!status) return null;
21
-
22
- const config = STATUS_CONFIG[status as FileUploadStatusValue] ?? {
23
- bg: "bg-muted",
24
- text: "text-foreground",
25
- label: status,
26
- };
27
-
28
- return (
29
- <span className={cn("px-2 py-1 text-xs font-medium", config.bg, config.text)}>
30
- {config.label}
31
- </span>
32
- );
33
- });
@@ -1,62 +0,0 @@
1
- import type { Meta, StoryObj } from "@storybook/react-vite";
2
- import { expect, userEvent, within } from "storybook/test";
3
-
4
- import { Field } from "./field";
5
- import { Input } from "./input";
6
-
7
- const meta = {
8
- title: "Forms/Field",
9
- component: Field,
10
- tags: ["autodocs"],
11
- args: {
12
- children: <Input placeholder="you@example.com" />,
13
- },
14
- } satisfies Meta<typeof Field>;
15
-
16
- export default meta;
17
- type Story = StoryObj<typeof meta>;
18
-
19
- export const Default: Story = {
20
- render: () => (
21
- <Field label="Email" helpText="We will never share your address.">
22
- <Input placeholder="you@example.com" />
23
- </Field>
24
- ),
25
- play: async ({ canvasElement }) => {
26
- const canvas = within(canvasElement);
27
- const input = canvas.getByLabelText("Email");
28
-
29
- await userEvent.type(input, "you@example.com");
30
-
31
- await expect(input).toHaveValue("you@example.com");
32
- await expect(canvas.getByText("We will never share your address.")).toBeInTheDocument();
33
- },
34
- };
35
-
36
- export const Error: Story = {
37
- render: () => (
38
- <Field label="Email" error="Email is required">
39
- <Input placeholder="you@example.com" />
40
- </Field>
41
- ),
42
- play: async ({ canvasElement }) => {
43
- const canvas = within(canvasElement);
44
-
45
- await expect(canvas.getByLabelText("Email")).toHaveAttribute("aria-invalid", "true");
46
- await expect(canvas.getByRole("alert")).toHaveTextContent("Email is required");
47
- },
48
- };
49
-
50
- export const Textarea: Story = {
51
- render: () => (
52
- <Field label="Notes" helpText="Markdown supported">
53
- <textarea rows={5} placeholder="Write something..." />
54
- </Field>
55
- ),
56
- play: async ({ canvasElement }) => {
57
- const canvas = within(canvasElement);
58
-
59
- await expect(canvas.getByLabelText("Notes")).toBeInTheDocument();
60
- await expect(canvas.getByText("Markdown supported")).toBeInTheDocument();
61
- },
62
- };
@@ -1,65 +0,0 @@
1
- import * as React from "react";
2
-
3
- import { cn } from "../../lib/utils";
4
- import type { FieldBaseProps } from "./field.types";
5
-
6
- interface FieldProps extends FieldBaseProps {
7
- children: React.ReactNode;
8
- }
9
-
10
- /**
11
- * Field — standalone label + control + helper/error slot.
12
- * Wires label ↔ control association automatically via a generated id.
13
- * Does NOT require react-hook-form — works with any controlled or uncontrolled input.
14
- *
15
- * @example
16
- * <Field label="Email" error={errors.email}>
17
- * <Input type="email" />
18
- * </Field>
19
- */
20
- function Field({ label, helpText, error, required, children, id: externalId }: FieldProps) {
21
- const generatedId = React.useId();
22
- const id = externalId ?? generatedId;
23
- const descId = `${id}-desc`;
24
- const errorId = `${id}-error`;
25
-
26
- return (
27
- <div className="flex flex-col gap-1.5">
28
- {label && (
29
- <label
30
- htmlFor={id}
31
- className={cn(
32
- "text-foreground text-sm font-medium",
33
- required && "after:text-destructive-text after:ml-0.5 after:content-['*']",
34
- )}
35
- >
36
- {label}
37
- </label>
38
- )}
39
-
40
- {/* Clone child to inject id and aria attributes */}
41
- {React.isValidElement(children)
42
- ? React.cloneElement(
43
- children as React.ReactElement<React.HTMLAttributes<HTMLElement>>,
44
- {
45
- id,
46
- "aria-describedby": error ? errorId : helpText ? descId : undefined,
47
- "aria-invalid": error ? true : undefined,
48
- } as React.HTMLAttributes<HTMLElement>,
49
- )
50
- : children}
51
-
52
- {error ? (
53
- <p id={errorId} role="alert" className="text-destructive-text text-sm">
54
- {error}
55
- </p>
56
- ) : helpText ? (
57
- <p id={descId} className="text-muted-foreground text-sm">
58
- {helpText}
59
- </p>
60
- ) : null}
61
- </div>
62
- );
63
- }
64
-
65
- export { Field, type FieldProps };
@@ -1,7 +0,0 @@
1
- export interface FieldBaseProps {
2
- error?: string | undefined;
3
- helpText?: string | undefined;
4
- id?: string | undefined;
5
- label?: string | undefined;
6
- required?: boolean | undefined;
7
- }
@@ -1,26 +0,0 @@
1
- import type { Meta, StoryObj } from "@storybook/react-vite";
2
- import { useState } from "react";
3
-
4
- import { GroupBySelect } from "./group-by-select";
5
-
6
- const meta = {
7
- title: "Forms/GroupBySelect",
8
- component: GroupBySelect,
9
- tags: ["autodocs"],
10
- } satisfies Meta<typeof GroupBySelect>;
11
-
12
- export default meta;
13
- type Story = StoryObj<typeof meta>;
14
-
15
- function GroupBySelectPreview() {
16
- const [groupBy, setGroupBy] = useState<"month" | "week" | "day">("month");
17
- return <GroupBySelect groupBy={groupBy} onGroupByChange={setGroupBy} />;
18
- }
19
-
20
- export const Default: Story = {
21
- args: {
22
- groupBy: "month",
23
- onGroupByChange: () => {},
24
- },
25
- render: () => <GroupBySelectPreview />,
26
- };
@@ -1,47 +0,0 @@
1
- import { useId } from "react";
2
-
3
- import { Label } from "../primitives/label";
4
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select";
5
-
6
- type GroupByOption = "month" | "week" | "day";
7
-
8
- export interface GroupBySelectProps {
9
- groupBy: GroupByOption;
10
- onGroupByChange: (value: GroupByOption) => void;
11
- label?: string;
12
- className?: string;
13
- }
14
-
15
- /** Month/week/day grouping select. */
16
- export function GroupBySelect({
17
- groupBy,
18
- onGroupByChange,
19
- label = "Group By",
20
- className,
21
- }: GroupBySelectProps) {
22
- const id = useId();
23
-
24
- return (
25
- <div className={`space-y-2 ${className || ""}`}>
26
- <Label htmlFor={id}>{label}</Label>
27
- <Select
28
- name="groupBy"
29
- value={groupBy}
30
- onValueChange={(value) => {
31
- if (value === "month" || value === "week" || value === "day") {
32
- onGroupByChange(value);
33
- }
34
- }}
35
- >
36
- <SelectTrigger id={id}>
37
- <SelectValue />
38
- </SelectTrigger>
39
- <SelectContent>
40
- <SelectItem value="month">Month</SelectItem>
41
- <SelectItem value="week">Week</SelectItem>
42
- <SelectItem value="day">Day</SelectItem>
43
- </SelectContent>
44
- </Select>
45
- </div>
46
- );
47
- }
@@ -1,30 +0,0 @@
1
- import type { Meta, StoryObj } from "@storybook/react-vite";
2
-
3
- import { RouteLink } from "./route-link";
4
-
5
- const meta = {
6
- title: "Navigation/RouteLink",
7
- component: RouteLink,
8
- tags: ["autodocs"],
9
- } satisfies Meta<typeof RouteLink>;
10
-
11
- export default meta;
12
- type Story = StoryObj<typeof meta>;
13
-
14
- export const Default: Story = {
15
- args: {
16
- children: "View accounts",
17
- href: "#",
18
- className: "text-accent underline",
19
- },
20
- };
21
-
22
- export const WithOnNavigate: Story = {
23
- name: "With onNavigate callback",
24
- args: {
25
- children: "Go to analytics",
26
- href: "#",
27
- className: "text-accent underline",
28
- onNavigate: () => console.log("route loading started"),
29
- },
30
- };