@ponti-studios/ui 0.7.0 → 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 (44) 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/feedback/update-guard.tsx +5 -1
  7. package/src/components/filters/filter-select.tsx +1 -1
  8. package/src/components/forms/date-month-select.tsx +12 -3
  9. package/src/components/forms/date-picker.tsx +16 -12
  10. package/src/components/forms/drop-zone.tsx +1 -1
  11. package/src/components/forms/index.ts +4 -8
  12. package/src/components/forms/passkey-management.tsx +4 -10
  13. package/src/components/forms/radio-group.tsx +8 -4
  14. package/src/components/forms/select.tsx +1 -1
  15. package/src/components/forms/text-field.tsx +40 -5
  16. package/src/components/navigation/app-navigation.stories.tsx +0 -1
  17. package/src/components/navigation/app-navigation.tsx +67 -61
  18. package/src/components/navigation/index.ts +7 -4
  19. package/src/components/navigation/pagination-controls.stories.tsx +1 -1
  20. package/src/components/navigation/pagination-controls.tsx +10 -14
  21. package/src/components/overlays/dropdown-menu.stories.tsx +2 -2
  22. package/src/components/primitives/card.tsx +6 -10
  23. package/src/components/primitives/label.stories.tsx +0 -18
  24. package/src/components/primitives/label.tsx +12 -12
  25. package/src/constants/chart-colors.ts +16 -16
  26. package/src/hooks/use-media-query.ts +5 -1
  27. package/src/styles/animations.css +0 -22
  28. package/src/styles/index.css +60 -6
  29. package/src/styles/tokens/color-systems.stories.tsx +2 -1
  30. package/src/styles/tokens/generated/light.colors.generated.ts +3 -3
  31. package/src/styles/tokens/generated/light.css +3 -3
  32. package/src/styles/tokens/source/light.tokens.json +3 -3
  33. package/src/styles/tokens/style-dictionary.light-storybook.json +16 -0
  34. package/src/components/feedback/file-upload-status-badge.stories.tsx +0 -28
  35. package/src/components/feedback/file-upload-status-badge.tsx +0 -33
  36. package/src/components/forms/field.stories.tsx +0 -62
  37. package/src/components/forms/field.tsx +0 -65
  38. package/src/components/forms/field.types.ts +0 -7
  39. package/src/components/forms/group-by-select.stories.tsx +0 -26
  40. package/src/components/forms/group-by-select.tsx +0 -47
  41. package/src/components/navigation/route-link.stories.tsx +0 -30
  42. package/src/components/navigation/route-link.tsx +0 -42
  43. package/src/hooks/use-derived-filter-state.stories.tsx +0 -136
  44. package/src/hooks/use-filter-state.stories.tsx +0 -102
@@ -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
- };
@@ -1,42 +0,0 @@
1
- import type { ComponentPropsWithoutRef, ElementType, MouseEvent, ReactNode } from "react";
2
- import { useCallback } from "react";
3
-
4
- type RouteLinkOwnProps<T extends ElementType> = {
5
- as?: T;
6
- children: ReactNode;
7
- /** Called before the click is forwarded — e.g. to flip a route-loading store. */
8
- onNavigate?: () => void;
9
- };
10
-
11
- export type RouteLinkProps<T extends ElementType = "a"> = RouteLinkOwnProps<T> &
12
- Omit<ComponentPropsWithoutRef<T>, keyof RouteLinkOwnProps<T>>;
13
-
14
- /**
15
- * Polymorphic link that fires `onNavigate` before `onClick`. Pass your
16
- * router's `Link` component as `as` for client-side navigation (e.g.
17
- * `as={Link} to="/accounts"`); the host app owns any route-loading state via
18
- * `onNavigate` instead of a hardcoded store.
19
- */
20
- export function RouteLink<T extends ElementType = "a">({
21
- as,
22
- children,
23
- onNavigate,
24
- onClick,
25
- ...props
26
- }: RouteLinkProps<T>) {
27
- const Component = (as ?? "a") as ElementType;
28
-
29
- const handleClick = useCallback(
30
- (e: MouseEvent) => {
31
- onNavigate?.();
32
- (onClick as ((e: MouseEvent) => void) | undefined)?.(e);
33
- },
34
- [onNavigate, onClick],
35
- );
36
-
37
- return (
38
- <Component onClick={handleClick} {...props}>
39
- {children}
40
- </Component>
41
- );
42
- }
@@ -1,136 +0,0 @@
1
- import type { Meta, StoryObj } from '@storybook/react-vite';
2
- import { expect, userEvent, within } from 'storybook/test';
3
- import { useRef } from 'react';
4
-
5
- import { useDerivedFilterState, type FilterConfig } from './use-derived-filter-state';
6
-
7
- function createMemorySync(initial: Record<string, string>) {
8
- const store = { ...initial };
9
- return {
10
- read: () => ({ ...store }),
11
- write: (values: Record<string, string>) => Object.assign(store, values),
12
- };
13
- }
14
-
15
- const fields = {
16
- status: { default: "", label: (v: string) => `Status: ${v}` } satisfies FilterConfig,
17
- search: { default: "" } satisfies FilterConfig,
18
- };
19
-
20
- function Harness() {
21
- const sync = useRef(createMemorySync({ status: "", search: "" }));
22
- const { values, setValue, activeFilters, clearAll, hasActive } =
23
- useDerivedFilterState({ fields, sync: sync.current });
24
-
25
- return (
26
- <div data-testid="harness">
27
- <span data-testid="values">{JSON.stringify(values)}</span>
28
- <span data-testid="active-filters">
29
- {activeFilters.map((f) => f.label).join(',')}
30
- </span>
31
- <span data-testid="has-active">{String(hasActive)}</span>
32
- <button
33
- data-testid="set-status"
34
- onClick={() => setValue('status', 'active')}
35
- >
36
- Set status
37
- </button>
38
- <button
39
- data-testid="set-search"
40
- onClick={() => setValue('search', 'hello')}
41
- >
42
- Set search
43
- </button>
44
- <button
45
- data-testid="clear-status"
46
- onClick={() => setValue('status', '')}
47
- >
48
- Clear status
49
- </button>
50
- <button data-testid="clear-all" onClick={() => clearAll()}>
51
- Clear all
52
- </button>
53
- </div>
54
- );
55
- }
56
-
57
- const meta: Meta<typeof Harness> = {
58
- title: 'Internal/useDerivedFilterState',
59
- component: Harness,
60
- tags: ['autodocs'],
61
- };
62
-
63
- export default meta;
64
- type Story = StoryObj<typeof meta>;
65
-
66
- export const InitialState: Story = {
67
- play: async ({ canvasElement }) => {
68
- const canvas = within(canvasElement);
69
- await expect(canvas.getByTestId('values')).toHaveTextContent(
70
- '{"status":"","search":""}',
71
- );
72
- await expect(canvas.getByTestId('has-active')).toHaveTextContent('false');
73
- await expect(canvas.getByTestId('active-filters')).toHaveTextContent('');
74
- },
75
- };
76
-
77
- export const SetValueCreatesActiveFilter: Story = {
78
- play: async ({ canvasElement }) => {
79
- const canvas = within(canvasElement);
80
- await userEvent.click(canvas.getByTestId('set-status'));
81
- await expect(canvas.getByTestId('values')).toHaveTextContent(
82
- '{"status":"active","search":""}',
83
- );
84
- await expect(canvas.getByTestId('active-filters')).toHaveTextContent(
85
- 'Status: active',
86
- );
87
- await expect(canvas.getByTestId('has-active')).toHaveTextContent('true');
88
- },
89
- };
90
-
91
- export const ClearValueRemovesActiveFilter: Story = {
92
- play: async ({ canvasElement }) => {
93
- const canvas = within(canvasElement);
94
- await userEvent.click(canvas.getByTestId('set-status'));
95
- await userEvent.click(canvas.getByTestId('clear-status'));
96
- await expect(canvas.getByTestId('values')).toHaveTextContent(
97
- '{"status":"","search":""}',
98
- );
99
- await expect(canvas.getByTestId('has-active')).toHaveTextContent('false');
100
- },
101
- };
102
-
103
- export const ClearAllResetsEverything: Story = {
104
- play: async ({ canvasElement }) => {
105
- const canvas = within(canvasElement);
106
- await userEvent.click(canvas.getByTestId('set-status'));
107
- await userEvent.click(canvas.getByTestId('set-search'));
108
- await userEvent.click(canvas.getByTestId('clear-all'));
109
- await expect(canvas.getByTestId('values')).toHaveTextContent(
110
- '{"status":"","search":""}',
111
- );
112
- await expect(canvas.getByTestId('has-active')).toHaveTextContent('false');
113
- await expect(canvas.getByTestId('active-filters')).toHaveTextContent('');
114
- },
115
- };
116
-
117
- export const ActiveFilterOnRemoveClearsValue: Story = {
118
- play: async ({ canvasElement }) => {
119
- const canvas = within(canvasElement);
120
- await userEvent.click(canvas.getByTestId('set-status'));
121
- await expect(canvas.getByTestId('active-filters')).toHaveTextContent(
122
- 'Status: active',
123
- );
124
- },
125
- };
126
-
127
- export const MultipleActiveFilters: Story = {
128
- play: async ({ canvasElement }) => {
129
- const canvas = within(canvasElement);
130
- await userEvent.click(canvas.getByTestId('set-status'));
131
- await userEvent.click(canvas.getByTestId('set-search'));
132
- await expect(canvas.getByTestId('active-filters')).toHaveTextContent(
133
- 'Status: active,search: hello',
134
- );
135
- },
136
- };
@@ -1,102 +0,0 @@
1
- import type { Meta, StoryObj } from '@storybook/react-vite';
2
- import { expect, userEvent, within } from 'storybook/test';
3
- import { useFilterState } from './use-filter-state';
4
-
5
- type Filters = Record<string, string>;
6
-
7
- function Harness({
8
- initialFilters,
9
- debounceMs,
10
- }: {
11
- initialFilters: Filters;
12
- debounceMs?: number;
13
- }) {
14
- const { filters, updateFilter, setFilters, clearFilters } = useFilterState<Filters>({
15
- initialFilters,
16
- debounceMs,
17
- });
18
-
19
- return (
20
- <div data-testid="harness">
21
- <span data-testid="filters">{JSON.stringify(filters)}</span>
22
- <button
23
- data-testid="update-status"
24
- onClick={() => updateFilter('status', 'active')}
25
- >
26
- Set status to active
27
- </button>
28
- <button
29
- data-testid="update-search"
30
- onClick={() => updateFilter('search', 'hello')}
31
- >
32
- Set search to hello
33
- </button>
34
- <button
35
- data-testid="set-filters"
36
- onClick={() => setFilters({ status: 'rejected', search: 'test' })}
37
- >
38
- Set all filters
39
- </button>
40
- <button data-testid="clear" onClick={() => clearFilters()}>
41
- Clear
42
- </button>
43
- </div>
44
- );
45
- }
46
-
47
- const meta: Meta<typeof Harness> = {
48
- title: 'Internal/useFilterState',
49
- component: Harness,
50
- tags: ['autodocs'],
51
- };
52
-
53
- export default meta;
54
- type Story = StoryObj<typeof meta>;
55
-
56
- export const InitialState: Story = {
57
- args: {
58
- initialFilters: { status: '', search: '' },
59
- },
60
- play: async ({ canvasElement }) => {
61
- const canvas = within(canvasElement);
62
- const output = canvas.getByTestId('filters');
63
- await expect(output).toHaveTextContent('{"status":"","search":""}');
64
- },
65
- };
66
-
67
- export const UpdateSingleFilter: Story = {
68
- args: {
69
- initialFilters: { status: '', search: '' },
70
- },
71
- play: async ({ canvasElement }) => {
72
- const canvas = within(canvasElement);
73
- await userEvent.click(canvas.getByTestId('update-status'));
74
- const output = canvas.getByTestId('filters');
75
- await expect(output).toHaveTextContent('{"status":"active","search":""}');
76
- },
77
- };
78
-
79
- export const SetAllFilters: Story = {
80
- args: {
81
- initialFilters: { status: '', search: '' },
82
- },
83
- play: async ({ canvasElement }) => {
84
- const canvas = within(canvasElement);
85
- await userEvent.click(canvas.getByTestId('set-filters'));
86
- const output = canvas.getByTestId('filters');
87
- await expect(output).toHaveTextContent('{"status":"rejected","search":"test"}');
88
- },
89
- };
90
-
91
- export const ClearResetsToInitial: Story = {
92
- args: {
93
- initialFilters: { status: 'pending', search: 'default' },
94
- },
95
- play: async ({ canvasElement }) => {
96
- const canvas = within(canvasElement);
97
- await userEvent.click(canvas.getByTestId('update-status'));
98
- await userEvent.click(canvas.getByTestId('clear'));
99
- const output = canvas.getByTestId('filters');
100
- await expect(output).toHaveTextContent('{"status":"pending","search":"default"}');
101
- },
102
- };