@ponti-studios/ui 1.1.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 (48) 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 +7 -3
  9. package/src/components/filters/active-filters-bar.stories.tsx +36 -36
  10. package/src/components/filters/active-filters-bar.tsx +21 -23
  11. package/src/components/filters/filter-select.stories.tsx +26 -26
  12. package/src/components/filters/filter-select.tsx +5 -8
  13. package/src/components/filters/index.ts +0 -4
  14. package/src/components/forms/date-month-select.tsx +2 -2
  15. package/src/components/forms/date-picker.tsx +2 -2
  16. package/src/components/forms/drop-zone.stories.tsx +112 -28
  17. package/src/components/forms/drop-zone.tsx +265 -112
  18. package/src/components/forms/input.tsx +5 -1
  19. package/src/components/forms/radio-group.tsx +3 -2
  20. package/src/components/forms/select.tsx +14 -4
  21. package/src/components/forms/slider.stories.tsx +2 -2
  22. package/src/components/forms/slider.tsx +12 -9
  23. package/src/components/forms/switch.tsx +5 -2
  24. package/src/components/forms/textarea.tsx +5 -1
  25. package/src/components/layout/index.ts +0 -2
  26. package/src/components/layout/section-intro.tsx +1 -1
  27. package/src/components/layout/surface-panel.tsx +10 -2
  28. package/src/components/navigation/app-navigation.stories.tsx +5 -1
  29. package/src/components/navigation/app-navigation.tsx +19 -4
  30. package/src/components/navigation/tabs.tsx +4 -1
  31. package/src/components/overlays/command.tsx +39 -14
  32. package/src/components/overlays/popover.stories.tsx +16 -4
  33. package/src/components/primitives/badge.tsx +1 -1
  34. package/src/components/primitives/button.tsx +6 -1
  35. package/src/hooks/use-derived-filter-state.ts +1 -3
  36. package/src/hooks/use-filter-state.ts +1 -2
  37. package/src/native/card.tsx +3 -1
  38. package/src/native/index.ts +21 -11
  39. package/src/native/list-row.tsx +67 -0
  40. package/src/styles/tokens/generated/native.generated.ts +34 -6
  41. package/src/styles/tokens/index.ts +2 -0
  42. package/src/styles/tokens/normalize-native.ts +5 -3
  43. package/src/styles/tokens/source/native.tokens.json +42 -7
  44. package/src/styles/tokens/style-dictionary.native.mjs +4 -1
  45. package/src/components/filters/filters.stories.tsx +0 -72
  46. package/src/components/filters/filters.tsx +0 -78
  47. package/src/components/filters/search-filter-bar.stories.tsx +0 -41
  48. package/src/components/filters/search-filter-bar.tsx +0 -48
@@ -29,15 +29,15 @@
29
29
  "icon": { "$value": "{radius.md}" }
30
30
  },
31
31
  "stream-item": {
32
- "$description": "Shared look for stream rows (time, inbox). Mirrors what the app used to hand-roll in theme.ts.",
32
+ "$description": "Shared flat list-row look (Inbox, Time, Tasks) — Vercel-style dense list: full-bleed bottom divider, no radius.",
33
33
  "$type": "dimension",
34
- "border-radius": { "$value": "{radius.md}" },
34
+ "border-radius": { "$value": { "value": 0, "unit": "rem" } },
35
35
  "border-width": { "$value": { "value": 1, "unit": "px" } },
36
- "gap": { "$value": "{spacing-scale.2}" },
37
- "icon-size": { "$value": "{component-size.md}" },
38
- "min-height": { "$value": { "value": 50, "unit": "px" } },
39
- "padding-horizontal": { "$value": "{spacing-scale.2}" },
40
- "padding-vertical": { "$value": "{spacing-scale.1}" }
36
+ "gap": { "$value": "{spacing-scale.3}" },
37
+ "icon-size": { "$value": "{component-size.icon}" },
38
+ "min-height": { "$value": "{spacing-scale.14}" },
39
+ "padding-horizontal": { "$value": "{spacing-scale.4}" },
40
+ "padding-vertical": { "$value": "{spacing-scale.2}" }
41
41
  },
42
42
  "font-family": {
43
43
  "$type": "string",
@@ -165,5 +165,40 @@
165
165
  "letterSpacing": { "value": 0, "unit": "px" }
166
166
  }
167
167
  }
168
+ },
169
+ "motion": {
170
+ "$description": "Portable motion semantics. Platform adapters own runtime animation objects and reduced-motion behavior.",
171
+ "duration": {
172
+ "$type": "duration",
173
+ "instant": { "$value": "0ms" },
174
+ "quick": { "$value": "{duration.150}" },
175
+ "standard": { "$value": "{duration.300}" },
176
+ "deliberate": { "$value": "{duration.500}" }
177
+ },
178
+ "easing": {
179
+ "$type": "cubicBezier",
180
+ "enter": { "$value": "{ease.out}" },
181
+ "exit": { "$value": "{ease.in}" },
182
+ "inOut": { "$value": "{ease.in-out}" }
183
+ },
184
+ "distance": {
185
+ "$type": "dimension",
186
+ "rowEnter": { "$value": "{spacing-scale.2}" },
187
+ "sceneReveal": { "$value": "{spacing-scale.8}" }
188
+ },
189
+ "opacity": {
190
+ "$type": "number",
191
+ "hidden": { "$value": 0 },
192
+ "visible": { "$value": 1 },
193
+ "disabled": { "$value": 0.5 }
194
+ },
195
+ "interruption": {
196
+ "$type": "string",
197
+ "policy": { "$value": "cancel-and-settle" }
198
+ },
199
+ "reducedMotion": {
200
+ "$type": "string",
201
+ "policy": { "$value": "replace-transform-with-opacity" }
202
+ }
168
203
  }
169
204
  }
@@ -5,7 +5,10 @@
5
5
  * foundations.generated.ts.
6
6
  */
7
7
  export default {
8
- source: ["src/styles/tokens/source/foundations.tokens.json", "src/styles/tokens/source/native.tokens.json"],
8
+ source: [
9
+ "src/styles/tokens/source/foundations.tokens.json",
10
+ "src/styles/tokens/source/native.tokens.json",
11
+ ],
9
12
  platforms: {
10
13
  typescript: {
11
14
  transformGroup: "js",
@@ -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,78 +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({
9
- label,
10
- children,
11
- }: {
12
- label: string;
13
- children: ReactNode;
14
- }) {
15
- const id = `filters-search-${label.toLowerCase().replace(/\s+/g, "-")}`;
16
- return (
17
- <div className="flex-1 space-y-2">
18
- <Label htmlFor={id}>{label}</Label>
19
- {children}
20
- </div>
21
- );
22
- }
23
- FiltersSearch.slot = "search";
24
-
25
- export function FiltersSelect(props: FilterSelectProps) {
26
- return <FilterSelect {...props} label={props.label ?? props.placeholder} />;
27
- }
28
- FiltersSelect.slot = "select";
29
-
30
- export function FiltersActive(props: ActiveFiltersBarProps) {
31
- return <ActiveFiltersBar {...props} />;
32
- }
33
- FiltersActive.slot = "active";
34
-
35
- export interface FiltersProps {
36
- children: ReactNode;
37
- className?: string;
38
- }
39
-
40
- function FiltersRoot({ children, className }: FiltersProps) {
41
- const search: ReactNode[] = [];
42
- const selects: ReactNode[] = [];
43
- const active: ReactNode[] = [];
44
-
45
- Children.forEach(children, (child) => {
46
- if (!isValidElement(child)) return;
47
- const slot = (child.type as any)?.slot;
48
- if (slot === "search") search.push(child);
49
- else if (slot === "select") selects.push(child);
50
- else if (slot === "active") active.push(child);
51
- });
52
-
53
- return (
54
- <div className={cn("flex flex-col gap-4", className)}>
55
- <div className="flex flex-col gap-3 lg:flex-row lg:items-start">
56
- {search}
57
- {selects.map((s, i) => (
58
- <div key={i} className="sm:w-48">
59
- {s}
60
- </div>
61
- ))}
62
- </div>
63
- {active}
64
- </div>
65
- );
66
- }
67
-
68
- type FiltersComponent = typeof FiltersRoot & {
69
- Search: typeof FiltersSearch;
70
- Select: typeof FiltersSelect;
71
- Active: typeof FiltersActive;
72
- };
73
-
74
- export const Filters: FiltersComponent = Object.assign(FiltersRoot, {
75
- Search: FiltersSearch,
76
- Select: FiltersSelect,
77
- Active: FiltersActive,
78
- });
@@ -1,41 +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={[
24
- { id: 'status', label: 'Status: Active', onRemove: () => {} },
25
- ]}
26
- onClearAll={() => {}}
27
- search={<Input placeholder="Search..." aria-label="Search" />}
28
- filters={[
29
- <FilterSelect
30
- key="status"
31
- value="active"
32
- options={[{ value: 'active', label: 'Active' }]}
33
- onChange={() => {}}
34
- placeholder="All"
35
- label="Status"
36
- />,
37
- ]}
38
- />
39
- </div>
40
- ),
41
- };
@@ -1,48 +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 ? (
39
- <div className="self-start lg:ml-auto lg:self-auto">{results}</div>
40
- ) : null}
41
-
42
- {children}
43
- </div>
44
-
45
- <ActiveFiltersBar filters={activeFilters} onClearAll={onClearAll} />
46
- </div>
47
- );
48
- }