@ponti-studios/ui 1.1.0 → 1.2.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/package.json +1 -1
- package/src/components/feedback/update-guard.tsx +5 -1
- package/src/components/filters/active-filters-bar.stories.tsx +34 -36
- package/src/components/filters/filter-select.stories.tsx +26 -26
- package/src/components/filters/filter-select.tsx +1 -6
- package/src/components/filters/filters.stories.tsx +21 -21
- package/src/components/filters/filters.tsx +1 -7
- package/src/components/filters/search-filter-bar.stories.tsx +8 -10
- package/src/components/filters/search-filter-bar.tsx +6 -8
- package/src/components/navigation/app-navigation.stories.tsx +5 -1
- package/src/components/navigation/app-navigation.tsx +19 -4
- package/src/hooks/use-derived-filter-state.ts +1 -3
- package/src/hooks/use-filter-state.ts +1 -2
- package/src/native/card.tsx +3 -1
- package/src/native/index.ts +21 -11
- package/src/native/list-row.tsx +67 -0
- package/src/styles/tokens/generated/native.generated.ts +34 -6
- package/src/styles/tokens/index.ts +2 -0
- package/src/styles/tokens/normalize-native.ts +5 -3
- package/src/styles/tokens/source/native.tokens.json +42 -7
- package/src/styles/tokens/style-dictionary.native.mjs +4 -1
package/package.json
CHANGED
|
@@ -196,7 +196,11 @@ function UpdateGuardClient({
|
|
|
196
196
|
updateServiceWorker,
|
|
197
197
|
} = useRegisterSW({ serviceWorkerPath });
|
|
198
198
|
|
|
199
|
-
const isOnline = useSyncExternalStore(
|
|
199
|
+
const isOnline = useSyncExternalStore(
|
|
200
|
+
subscribeOnline,
|
|
201
|
+
getOnlineSnapshot,
|
|
202
|
+
getOnlineServerSnapshot,
|
|
203
|
+
);
|
|
200
204
|
|
|
201
205
|
const closePrompt = () => {
|
|
202
206
|
setOfflineReady(false);
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from
|
|
2
|
-
import { expect, userEvent, within } from
|
|
3
|
-
import { fn } from
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect, userEvent, within } from "storybook/test";
|
|
3
|
+
import { fn } from "storybook/test";
|
|
4
4
|
|
|
5
|
-
import { ActiveFiltersBar } from
|
|
5
|
+
import { ActiveFiltersBar } from "./active-filters-bar";
|
|
6
6
|
|
|
7
7
|
const meta = {
|
|
8
|
-
title:
|
|
8
|
+
title: "Filters/ActiveFiltersBar",
|
|
9
9
|
component: ActiveFiltersBar,
|
|
10
|
-
tags: [
|
|
10
|
+
tags: ["autodocs"],
|
|
11
11
|
decorators: [
|
|
12
12
|
(Story) => (
|
|
13
13
|
<div className="w-[600px]">
|
|
@@ -23,36 +23,36 @@ type Story = StoryObj<typeof meta>;
|
|
|
23
23
|
export const Default: Story = {
|
|
24
24
|
args: {
|
|
25
25
|
filters: [
|
|
26
|
-
{ id:
|
|
27
|
-
{ id:
|
|
26
|
+
{ id: "1", label: "Status: Active", onRemove: fn() },
|
|
27
|
+
{ id: "2", label: "Created after Jan 1", onRemove: fn() },
|
|
28
28
|
],
|
|
29
29
|
},
|
|
30
30
|
play: async ({ canvasElement }) => {
|
|
31
31
|
const canvas = within(canvasElement);
|
|
32
|
-
const list = canvas.getByRole(
|
|
32
|
+
const list = canvas.getByRole("list");
|
|
33
33
|
await expect(list).toBeInTheDocument();
|
|
34
|
-
const items = within(list).getAllByRole(
|
|
34
|
+
const items = within(list).getAllByRole("listitem");
|
|
35
35
|
expect(items.length).toBe(2);
|
|
36
|
-
await expect(canvas.getByText(
|
|
37
|
-
await expect(canvas.getByText(
|
|
38
|
-
await expect(canvas.getByRole(
|
|
36
|
+
await expect(canvas.getByText("Status: Active")).toBeInTheDocument();
|
|
37
|
+
await expect(canvas.getByText("Created after Jan 1")).toBeInTheDocument();
|
|
38
|
+
await expect(canvas.getByRole("button", { name: "Clear all" })).toBeInTheDocument();
|
|
39
39
|
},
|
|
40
40
|
};
|
|
41
41
|
|
|
42
42
|
export const WithClearAll: Story = {
|
|
43
43
|
args: {
|
|
44
44
|
filters: [
|
|
45
|
-
{ id:
|
|
46
|
-
{ id:
|
|
45
|
+
{ id: "1", label: "Status: Active", onRemove: fn() },
|
|
46
|
+
{ id: "2", label: "Role: Engineer", onRemove: fn() },
|
|
47
47
|
],
|
|
48
48
|
onClearAll: fn(),
|
|
49
49
|
},
|
|
50
50
|
play: async ({ canvasElement, args }) => {
|
|
51
51
|
const canvas = within(canvasElement);
|
|
52
|
-
const list = canvas.getByRole(
|
|
53
|
-
const items = within(list).getAllByRole(
|
|
52
|
+
const list = canvas.getByRole("list");
|
|
53
|
+
const items = within(list).getAllByRole("listitem");
|
|
54
54
|
expect(items.length).toBe(2);
|
|
55
|
-
const clearButton = canvas.getByRole(
|
|
55
|
+
const clearButton = canvas.getByRole("button", { name: "Clear all" });
|
|
56
56
|
await expect(clearButton).toBeInTheDocument();
|
|
57
57
|
await userEvent.click(clearButton);
|
|
58
58
|
await expect(args.onClearAll).toHaveBeenCalledTimes(1);
|
|
@@ -61,36 +61,34 @@ export const WithClearAll: Story = {
|
|
|
61
61
|
|
|
62
62
|
export const WithSortOnly: Story = {
|
|
63
63
|
args: {
|
|
64
|
-
filters: [
|
|
65
|
-
{ id: '1', label: 'Date', onRemove: fn(), variant: 'sort', direction: 'desc' },
|
|
66
|
-
],
|
|
64
|
+
filters: [{ id: "1", label: "Date", onRemove: fn(), variant: "sort", direction: "desc" }],
|
|
67
65
|
},
|
|
68
66
|
play: async ({ canvasElement }) => {
|
|
69
67
|
const canvas = within(canvasElement);
|
|
70
|
-
const list = canvas.getByRole(
|
|
71
|
-
const items = within(list).getAllByRole(
|
|
68
|
+
const list = canvas.getByRole("list");
|
|
69
|
+
const items = within(list).getAllByRole("listitem");
|
|
72
70
|
expect(items.length).toBe(1);
|
|
73
|
-
await expect(canvas.getByText(
|
|
74
|
-
await expect(canvas.getByRole(
|
|
71
|
+
await expect(canvas.getByText("Date")).toBeInTheDocument();
|
|
72
|
+
await expect(canvas.getByRole("button", { name: "Clear all" })).toBeInTheDocument();
|
|
75
73
|
},
|
|
76
74
|
};
|
|
77
75
|
|
|
78
76
|
export const WithSortAndFilter: Story = {
|
|
79
77
|
args: {
|
|
80
78
|
filters: [
|
|
81
|
-
{ id:
|
|
82
|
-
{ id:
|
|
79
|
+
{ id: "filter-1", label: "Status: Active", onRemove: fn(), variant: "filter" },
|
|
80
|
+
{ id: "sort-1", label: "Date", onRemove: fn(), variant: "sort", direction: "desc" },
|
|
83
81
|
],
|
|
84
82
|
onClearAll: fn(),
|
|
85
83
|
},
|
|
86
84
|
play: async ({ canvasElement }) => {
|
|
87
85
|
const canvas = within(canvasElement);
|
|
88
|
-
const list = canvas.getByRole(
|
|
89
|
-
const items = within(list).getAllByRole(
|
|
86
|
+
const list = canvas.getByRole("list");
|
|
87
|
+
const items = within(list).getAllByRole("listitem");
|
|
90
88
|
expect(items.length).toBe(2);
|
|
91
|
-
await expect(canvas.getByText(
|
|
92
|
-
await expect(canvas.getByText(
|
|
93
|
-
await expect(canvas.getByRole(
|
|
89
|
+
await expect(canvas.getByText("Status: Active")).toBeInTheDocument();
|
|
90
|
+
await expect(canvas.getByText("Date")).toBeInTheDocument();
|
|
91
|
+
await expect(canvas.getByRole("button", { name: "Clear all" })).toBeInTheDocument();
|
|
94
92
|
},
|
|
95
93
|
};
|
|
96
94
|
|
|
@@ -98,17 +96,17 @@ export const Empty: Story = {
|
|
|
98
96
|
args: { filters: [] },
|
|
99
97
|
play: async ({ canvasElement }) => {
|
|
100
98
|
const canvas = within(canvasElement);
|
|
101
|
-
expect(canvas.queryByRole(
|
|
99
|
+
expect(canvas.queryByRole("list")).toBeNull();
|
|
102
100
|
},
|
|
103
101
|
};
|
|
104
102
|
|
|
105
103
|
export const WithClassName: Story = {
|
|
106
104
|
args: {
|
|
107
|
-
filters: [{ id:
|
|
108
|
-
className:
|
|
105
|
+
filters: [{ id: "1", label: "Status: Active", onRemove: fn() }],
|
|
106
|
+
className: "mt-4",
|
|
109
107
|
},
|
|
110
108
|
play: async ({ canvasElement }) => {
|
|
111
|
-
const container = canvasElement.querySelector(
|
|
109
|
+
const container = canvasElement.querySelector(".mt-4");
|
|
112
110
|
await expect(container).not.toBeNull();
|
|
113
111
|
},
|
|
114
112
|
};
|
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from
|
|
2
|
-
import { expect, within } from
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect, within } from "storybook/test";
|
|
3
3
|
|
|
4
|
-
import { FilterSelect } from
|
|
4
|
+
import { FilterSelect } from "./filter-select";
|
|
5
5
|
|
|
6
6
|
const meta = {
|
|
7
|
-
title:
|
|
7
|
+
title: "Filters/FilterSelect",
|
|
8
8
|
component: FilterSelect,
|
|
9
|
-
tags: [
|
|
9
|
+
tags: ["autodocs"],
|
|
10
10
|
args: {
|
|
11
11
|
options: [
|
|
12
|
-
{ value:
|
|
13
|
-
{ value:
|
|
14
|
-
{ value:
|
|
15
|
-
{ value:
|
|
16
|
-
{ value:
|
|
12
|
+
{ value: "active", label: "Active" },
|
|
13
|
+
{ value: "interview", label: "Interview" },
|
|
14
|
+
{ value: "offer", label: "Offer" },
|
|
15
|
+
{ value: "rejected", label: "Rejected" },
|
|
16
|
+
{ value: "archived", label: "Archived" },
|
|
17
17
|
],
|
|
18
|
-
placeholder:
|
|
19
|
-
label:
|
|
18
|
+
placeholder: "All statuses",
|
|
19
|
+
label: "Status",
|
|
20
20
|
onChange: () => {},
|
|
21
21
|
},
|
|
22
22
|
} satisfies Meta<typeof FilterSelect>;
|
|
@@ -25,47 +25,47 @@ export default meta;
|
|
|
25
25
|
type Story = StoryObj<typeof meta>;
|
|
26
26
|
|
|
27
27
|
export const Default: Story = {
|
|
28
|
-
args: { value:
|
|
28
|
+
args: { value: "" },
|
|
29
29
|
play: async ({ canvasElement }) => {
|
|
30
30
|
const canvas = within(canvasElement);
|
|
31
|
-
const trigger = canvas.getByRole(
|
|
31
|
+
const trigger = canvas.getByRole("combobox", { name: "Status" });
|
|
32
32
|
await expect(trigger).toBeInTheDocument();
|
|
33
|
-
await expect(trigger).toHaveTextContent(
|
|
33
|
+
await expect(trigger).toHaveTextContent("All statuses");
|
|
34
34
|
},
|
|
35
35
|
};
|
|
36
36
|
|
|
37
37
|
export const WithValue: Story = {
|
|
38
|
-
args: { value:
|
|
38
|
+
args: { value: "active" },
|
|
39
39
|
play: async ({ canvasElement }) => {
|
|
40
40
|
const canvas = within(canvasElement);
|
|
41
|
-
const trigger = canvas.getByRole(
|
|
42
|
-
await expect(trigger).toHaveTextContent(
|
|
41
|
+
const trigger = canvas.getByRole("combobox", { name: "Status" });
|
|
42
|
+
await expect(trigger).toHaveTextContent("Active");
|
|
43
43
|
},
|
|
44
44
|
};
|
|
45
45
|
|
|
46
46
|
export const Loading: Story = {
|
|
47
|
-
args: { value:
|
|
47
|
+
args: { value: "", isLoading: true, options: [] },
|
|
48
48
|
play: async ({ canvasElement }) => {
|
|
49
49
|
const canvas = within(canvasElement);
|
|
50
|
-
await expect(canvas.getByRole(
|
|
51
|
-
await expect(canvas.getByText(
|
|
50
|
+
await expect(canvas.getByRole("combobox", { name: "Status" })).toBeInTheDocument();
|
|
51
|
+
await expect(canvas.getByText("Status")).toBeInTheDocument();
|
|
52
52
|
},
|
|
53
53
|
};
|
|
54
54
|
|
|
55
55
|
export const Empty: Story = {
|
|
56
|
-
args: { value:
|
|
56
|
+
args: { value: "", options: [], emptyLabel: "No statuses found" },
|
|
57
57
|
play: async ({ canvasElement }) => {
|
|
58
58
|
const canvas = within(canvasElement);
|
|
59
|
-
await expect(canvas.getByRole(
|
|
60
|
-
await expect(canvas.getByText(
|
|
59
|
+
await expect(canvas.getByRole("combobox", { name: "Status" })).toBeInTheDocument();
|
|
60
|
+
await expect(canvas.getByText("Status")).toBeInTheDocument();
|
|
61
61
|
},
|
|
62
62
|
};
|
|
63
63
|
|
|
64
64
|
export const Disabled: Story = {
|
|
65
|
-
args: { value:
|
|
65
|
+
args: { value: "active", disabled: true },
|
|
66
66
|
play: async ({ canvasElement }) => {
|
|
67
67
|
const canvas = within(canvasElement);
|
|
68
|
-
const trigger = canvas.getByRole(
|
|
68
|
+
const trigger = canvas.getByRole("combobox", { name: "Status" });
|
|
69
69
|
await expect(trigger).toBeDisabled();
|
|
70
70
|
},
|
|
71
71
|
};
|
|
@@ -47,12 +47,7 @@ export function FilterSelect({
|
|
|
47
47
|
<div className={cn("space-y-2 min-w-36 bg-background", className)}>
|
|
48
48
|
<Label htmlFor={selectId}>{label}</Label>
|
|
49
49
|
<Select value={value} onValueChange={onChange} items={items}>
|
|
50
|
-
<SelectTrigger
|
|
51
|
-
id={selectId}
|
|
52
|
-
disabled={disabled}
|
|
53
|
-
aria-label={label}
|
|
54
|
-
className="w-full"
|
|
55
|
-
>
|
|
50
|
+
<SelectTrigger id={selectId} disabled={disabled} aria-label={label} className="w-full">
|
|
56
51
|
<SelectValue placeholder={placeholder} />
|
|
57
52
|
</SelectTrigger>
|
|
58
53
|
<SelectContent className="bg-background max-h-[250px] min-w-36 overflow-y-auto">
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from
|
|
2
|
-
import { expect, within } from
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect, within } from "storybook/test";
|
|
3
3
|
|
|
4
|
-
import { Input } from
|
|
5
|
-
import { Filters } from
|
|
4
|
+
import { Input } from "../forms/input";
|
|
5
|
+
import { Filters } from "./filters";
|
|
6
6
|
|
|
7
7
|
const meta = {
|
|
8
|
-
title:
|
|
8
|
+
title: "Filters/Compound",
|
|
9
9
|
component: Filters,
|
|
10
|
-
tags: [
|
|
10
|
+
tags: ["autodocs"],
|
|
11
11
|
args: {
|
|
12
12
|
children: null,
|
|
13
13
|
},
|
|
@@ -17,9 +17,9 @@ export default meta;
|
|
|
17
17
|
type Story = StoryObj<typeof meta>;
|
|
18
18
|
|
|
19
19
|
const statusOptions = [
|
|
20
|
-
{ value:
|
|
21
|
-
{ value:
|
|
22
|
-
{ value:
|
|
20
|
+
{ value: "active", label: "Active" },
|
|
21
|
+
{ value: "interview", label: "Interview" },
|
|
22
|
+
{ value: "offer", label: "Offer" },
|
|
23
23
|
];
|
|
24
24
|
|
|
25
25
|
export const Default: Story = {
|
|
@@ -38,15 +38,15 @@ export const Default: Story = {
|
|
|
38
38
|
/>
|
|
39
39
|
<Filters.Select
|
|
40
40
|
value=""
|
|
41
|
-
options={[{ value:
|
|
41
|
+
options={[{ value: "engineer", label: "Engineer" }]}
|
|
42
42
|
onChange={() => {}}
|
|
43
43
|
placeholder="All roles"
|
|
44
44
|
label="Role"
|
|
45
45
|
/>
|
|
46
46
|
<Filters.Active
|
|
47
47
|
filters={[
|
|
48
|
-
{ id:
|
|
49
|
-
{ id:
|
|
48
|
+
{ id: "status", label: "Status: Active", onRemove: () => {} },
|
|
49
|
+
{ id: "sort", label: "Date", onRemove: () => {}, variant: "sort", direction: "desc" },
|
|
50
50
|
]}
|
|
51
51
|
onClearAll={() => {}}
|
|
52
52
|
/>
|
|
@@ -56,17 +56,17 @@ export const Default: Story = {
|
|
|
56
56
|
play: async ({ canvasElement }) => {
|
|
57
57
|
const canvas = within(canvasElement);
|
|
58
58
|
|
|
59
|
-
await expect(canvas.getByText(
|
|
60
|
-
await expect(canvas.getByRole(
|
|
59
|
+
await expect(canvas.getByText("Search")).toBeInTheDocument();
|
|
60
|
+
await expect(canvas.getByRole("textbox", { name: "Search applications" })).toBeInTheDocument();
|
|
61
61
|
|
|
62
|
-
await expect(canvas.getByText(
|
|
63
|
-
await expect(canvas.getByRole(
|
|
62
|
+
await expect(canvas.getByText("Status")).toBeInTheDocument();
|
|
63
|
+
await expect(canvas.getByRole("combobox", { name: "Status" })).toBeInTheDocument();
|
|
64
64
|
|
|
65
|
-
await expect(canvas.getByText(
|
|
66
|
-
await expect(canvas.getByRole(
|
|
65
|
+
await expect(canvas.getByText("Role")).toBeInTheDocument();
|
|
66
|
+
await expect(canvas.getByRole("combobox", { name: "Role" })).toBeInTheDocument();
|
|
67
67
|
|
|
68
|
-
await expect(canvas.getByText(
|
|
69
|
-
await expect(canvas.getByText(
|
|
70
|
-
await expect(canvas.getByRole(
|
|
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
71
|
},
|
|
72
72
|
};
|
|
@@ -5,13 +5,7 @@ import { ActiveFiltersBar, type ActiveFiltersBarProps } from "./active-filters-b
|
|
|
5
5
|
import { FilterSelect, type FilterSelectProps } from "./filter-select";
|
|
6
6
|
import { Label } from "../primitives/label";
|
|
7
7
|
|
|
8
|
-
export function FiltersSearch({
|
|
9
|
-
label,
|
|
10
|
-
children,
|
|
11
|
-
}: {
|
|
12
|
-
label: string;
|
|
13
|
-
children: ReactNode;
|
|
14
|
-
}) {
|
|
8
|
+
export function FiltersSearch({ label, children }: { label: string; children: ReactNode }) {
|
|
15
9
|
const id = `filters-search-${label.toLowerCase().replace(/\s+/g, "-")}`;
|
|
16
10
|
return (
|
|
17
11
|
<div className="flex-1 space-y-2">
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
2
|
|
|
3
|
-
import { Input } from
|
|
4
|
-
import { FilterSelect } from
|
|
5
|
-
import { SearchFilterBar } from
|
|
3
|
+
import { Input } from "../forms/input";
|
|
4
|
+
import { FilterSelect } from "./filter-select";
|
|
5
|
+
import { SearchFilterBar } from "./search-filter-bar";
|
|
6
6
|
|
|
7
7
|
const meta = {
|
|
8
|
-
title:
|
|
8
|
+
title: "Filters/SearchFilterBar",
|
|
9
9
|
component: SearchFilterBar,
|
|
10
|
-
tags: [
|
|
10
|
+
tags: ["autodocs"],
|
|
11
11
|
} satisfies Meta<typeof SearchFilterBar>;
|
|
12
12
|
|
|
13
13
|
export default meta;
|
|
@@ -20,16 +20,14 @@ export const Legacy: Story = {
|
|
|
20
20
|
render: () => (
|
|
21
21
|
<div className="w-[720px]">
|
|
22
22
|
<SearchFilterBar
|
|
23
|
-
activeFilters={[
|
|
24
|
-
{ id: 'status', label: 'Status: Active', onRemove: () => {} },
|
|
25
|
-
]}
|
|
23
|
+
activeFilters={[{ id: "status", label: "Status: Active", onRemove: () => {} }]}
|
|
26
24
|
onClearAll={() => {}}
|
|
27
25
|
search={<Input placeholder="Search..." aria-label="Search" />}
|
|
28
26
|
filters={[
|
|
29
27
|
<FilterSelect
|
|
30
28
|
key="status"
|
|
31
29
|
value="active"
|
|
32
|
-
options={[{ value:
|
|
30
|
+
options={[{ value: "active", label: "Active" }]}
|
|
33
31
|
onChange={() => {}}
|
|
34
32
|
placeholder="All"
|
|
35
33
|
label="Status"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { type ReactNode } from
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
2
|
|
|
3
|
-
import { cn } from
|
|
4
|
-
import { ActiveFiltersBar, type ActiveFilter } from
|
|
3
|
+
import { cn } from "../../lib/utils";
|
|
4
|
+
import { ActiveFiltersBar, type ActiveFilter } from "./active-filters-bar";
|
|
5
5
|
|
|
6
6
|
export interface SearchFilterBarProps {
|
|
7
7
|
activeFilters: ActiveFilter[];
|
|
@@ -22,10 +22,10 @@ export function SearchFilterBar({
|
|
|
22
22
|
results,
|
|
23
23
|
children,
|
|
24
24
|
className,
|
|
25
|
-
filterItemClassName =
|
|
25
|
+
filterItemClassName = "sm:w-48",
|
|
26
26
|
}: SearchFilterBarProps) {
|
|
27
27
|
return (
|
|
28
|
-
<div className={cn(
|
|
28
|
+
<div className={cn("flex flex-col gap-4", className)}>
|
|
29
29
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-center">
|
|
30
30
|
{search ? <div className="flex-1">{search}</div> : null}
|
|
31
31
|
|
|
@@ -35,9 +35,7 @@ export function SearchFilterBar({
|
|
|
35
35
|
</div>
|
|
36
36
|
))}
|
|
37
37
|
|
|
38
|
-
{results ?
|
|
39
|
-
<div className="self-start lg:ml-auto lg:self-auto">{results}</div>
|
|
40
|
-
) : null}
|
|
38
|
+
{results ? <div className="self-start lg:ml-auto lg:self-auto">{results}</div> : null}
|
|
41
39
|
|
|
42
40
|
{children}
|
|
43
41
|
</div>
|
|
@@ -121,7 +121,11 @@ export const WithCustomRouterLink: Story = {
|
|
|
121
121
|
|
|
122
122
|
<Navigation.Action>
|
|
123
123
|
<Button size="md" asChild>
|
|
124
|
-
<a
|
|
124
|
+
<a
|
|
125
|
+
href="/get-started"
|
|
126
|
+
data-router-link="stand-in"
|
|
127
|
+
onClick={(e) => e.preventDefault()}
|
|
128
|
+
>
|
|
125
129
|
Get started
|
|
126
130
|
</a>
|
|
127
131
|
</Button>
|
|
@@ -26,17 +26,26 @@ export interface NavigationRootProps {
|
|
|
26
26
|
* shape) so `Navigation.Item`'s active state and `asChild` composition
|
|
27
27
|
* behave identically in both places.
|
|
28
28
|
*/
|
|
29
|
-
function NavigationRoot({
|
|
29
|
+
function NavigationRoot({
|
|
30
|
+
children,
|
|
31
|
+
ariaLabel = "Primary navigation",
|
|
32
|
+
className,
|
|
33
|
+
}: NavigationRootProps) {
|
|
30
34
|
const [mobileOpen, setMobileOpen] = React.useState(false);
|
|
31
35
|
|
|
32
36
|
const items = React.Children.toArray(children);
|
|
33
|
-
const brand = items.find(
|
|
37
|
+
const brand = items.find(
|
|
38
|
+
(child) => React.isValidElement(child) && child.type === NavigationBrand,
|
|
39
|
+
);
|
|
34
40
|
const rest = items.filter((child) => child !== brand);
|
|
35
41
|
|
|
36
42
|
return (
|
|
37
43
|
<header className="bg-background/95 supports-backdrop-filter:bg-background/60 sticky top-0 z-50 flex w-full justify-center border-b px-4 backdrop-blur">
|
|
38
44
|
<nav
|
|
39
|
-
className={cn(
|
|
45
|
+
className={cn(
|
|
46
|
+
"flex min-h-14 w-full max-w-7xl items-center justify-between gap-6",
|
|
47
|
+
className,
|
|
48
|
+
)}
|
|
40
49
|
aria-label={ariaLabel}
|
|
41
50
|
>
|
|
42
51
|
<div className="flex min-w-0 items-center">{brand}</div>
|
|
@@ -108,7 +117,13 @@ export type NavigationItemProps = NavigationItemOwnProps &
|
|
|
108
117
|
Omit<ComponentPropsWithoutRef<"a">, keyof NavigationItemOwnProps>;
|
|
109
118
|
|
|
110
119
|
/** A single nav link, wrapped in `<li>`. Auto-closes the mobile Sheet on click when rendered there. */
|
|
111
|
-
function NavigationItem({
|
|
120
|
+
function NavigationItem({
|
|
121
|
+
active = false,
|
|
122
|
+
asChild = false,
|
|
123
|
+
className,
|
|
124
|
+
children,
|
|
125
|
+
...props
|
|
126
|
+
}: NavigationItemProps) {
|
|
112
127
|
const isMobile = React.useContext(MobileMenuContext);
|
|
113
128
|
const itemClassName = cn(
|
|
114
129
|
"inline-flex h-9 max-h-9 shrink-0 items-center px-3 py-2 text-sm font-medium hover:bg-muted hover:text-muted-foreground data-[active]:font-semibold data-[active]:text-foreground",
|
|
@@ -53,9 +53,7 @@ export function useDerivedFilterState<T extends Record<string, FilterConfig>>({
|
|
|
53
53
|
.filter(([, v]) => v !== "")
|
|
54
54
|
.map(([key, v]) => ({
|
|
55
55
|
id: key,
|
|
56
|
-
label: fields[key]?.label
|
|
57
|
-
? fields[key].label!(v)
|
|
58
|
-
: `${key}: ${v}`,
|
|
56
|
+
label: fields[key]?.label ? fields[key].label!(v) : `${key}: ${v}`,
|
|
59
57
|
onRemove: () => setValue(key, ""),
|
|
60
58
|
}));
|
|
61
59
|
}, [values, fields, setValue]);
|
|
@@ -18,8 +18,7 @@ export function useFilterState<T extends Record<string, unknown>>(
|
|
|
18
18
|
const setFilters = useCallback(
|
|
19
19
|
(newFilters: T | ((prev: T) => T)) => {
|
|
20
20
|
setFiltersState((prev) => {
|
|
21
|
-
const updatedFilters =
|
|
22
|
-
typeof newFilters === "function" ? newFilters(prev) : newFilters;
|
|
21
|
+
const updatedFilters = typeof newFilters === "function" ? newFilters(prev) : newFilters;
|
|
23
22
|
|
|
24
23
|
if (debounceMs && debounceMs > 0) {
|
|
25
24
|
if (debounceTimerRef.current) {
|
package/src/native/card.tsx
CHANGED
|
@@ -4,7 +4,9 @@ import { Text, View, type TextProps, type ViewProps } from "react-native";
|
|
|
4
4
|
import { cn } from "../lib/utils";
|
|
5
5
|
|
|
6
6
|
function Card({ className, ...props }: ViewProps) {
|
|
7
|
-
return
|
|
7
|
+
return (
|
|
8
|
+
<View className={cn("bg-card gap-4 rounded border border-border p-4", className)} {...props} />
|
|
9
|
+
);
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
function CardHeader({ className, ...props }: ViewProps) {
|
package/src/native/index.ts
CHANGED
|
@@ -6,23 +6,33 @@
|
|
|
6
6
|
* code, so web consumers never pull React Native into their graph and native
|
|
7
7
|
* consumers never pull in react-dom.
|
|
8
8
|
*/
|
|
9
|
+
export {
|
|
10
|
+
Card,
|
|
11
|
+
CardAction,
|
|
12
|
+
CardContent,
|
|
13
|
+
CardDescription,
|
|
14
|
+
CardFooter,
|
|
15
|
+
CardHeader,
|
|
16
|
+
CardTitle,
|
|
17
|
+
} from "./card";
|
|
18
|
+
export { IconButton } from "./icon-button";
|
|
19
|
+
export { ListRow } from "./list-row";
|
|
9
20
|
export { createMakeStyles, type NamedStyles } from "./make-styles";
|
|
10
21
|
export { nativeShadows, type NativeShadowLayer } from "./shadows";
|
|
11
|
-
export { useColorMode, useThemeColors } from "./use-theme-colors";
|
|
12
|
-
export { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./card";
|
|
13
22
|
export { TextField } from "./text-field";
|
|
14
|
-
export {
|
|
23
|
+
export { useColorMode, useThemeColors } from "./use-theme-colors";
|
|
15
24
|
|
|
16
25
|
export {
|
|
17
|
-
typography as nativeTypography,
|
|
18
|
-
spacing as nativeSpacing,
|
|
19
|
-
screenMargin as nativeScreenMargin,
|
|
20
|
-
componentSize as nativeComponentSize,
|
|
21
26
|
borderRadius as nativeBorderRadius,
|
|
22
|
-
|
|
27
|
+
componentSize as nativeComponentSize,
|
|
23
28
|
fontFamily as nativeFontFamily,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
screenMargin as nativeScreenMargin,
|
|
30
|
+
spacing as nativeSpacing,
|
|
31
|
+
streamItem as nativeStreamItem,
|
|
32
|
+
typography as nativeTypography,
|
|
27
33
|
type BorderRadiusToken,
|
|
34
|
+
type ComponentSizeToken,
|
|
35
|
+
type SpacingToken,
|
|
36
|
+
type TypographyVariant,
|
|
28
37
|
} from "../styles/tokens/normalize-native";
|
|
38
|
+
export { motion as nativeMotion } from "../styles/tokens";
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { Pressable, type StyleProp, Text, type TextStyle, View } from "react-native";
|
|
3
|
+
import { useCSSVariable } from "uniwind";
|
|
4
|
+
|
|
5
|
+
interface ListRowProps {
|
|
6
|
+
accessibilityLabel: string;
|
|
7
|
+
actionTestID?: string;
|
|
8
|
+
eyebrow?: string;
|
|
9
|
+
leading?: ReactNode;
|
|
10
|
+
onLongPress?: () => void;
|
|
11
|
+
onPress: () => void;
|
|
12
|
+
subtitle?: string | null;
|
|
13
|
+
testID?: string;
|
|
14
|
+
title: string;
|
|
15
|
+
titleStyle?: StyleProp<TextStyle>;
|
|
16
|
+
trailing?: ReactNode;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function ListRow({
|
|
20
|
+
accessibilityLabel,
|
|
21
|
+
actionTestID,
|
|
22
|
+
eyebrow,
|
|
23
|
+
leading,
|
|
24
|
+
onLongPress,
|
|
25
|
+
onPress,
|
|
26
|
+
subtitle,
|
|
27
|
+
testID,
|
|
28
|
+
title,
|
|
29
|
+
titleStyle,
|
|
30
|
+
trailing,
|
|
31
|
+
}: ListRowProps) {
|
|
32
|
+
const [muted] = useCSSVariable(["--color-muted"]) as string[];
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<Pressable
|
|
36
|
+
accessibilityLabel={accessibilityLabel}
|
|
37
|
+
accessibilityRole="button"
|
|
38
|
+
onLongPress={onLongPress}
|
|
39
|
+
onPress={onPress}
|
|
40
|
+
className="flex-row items-center gap-3 min-h-14 border-b border-border px-4 py-2"
|
|
41
|
+
style={({ pressed }) => pressed && { backgroundColor: muted }}
|
|
42
|
+
testID={testID ?? actionTestID}
|
|
43
|
+
>
|
|
44
|
+
{leading ? <View className="items-center justify-center w-6">{leading}</View> : null}
|
|
45
|
+
<View className="flex-1 gap-0.5 min-w-0">
|
|
46
|
+
{eyebrow ? (
|
|
47
|
+
<Text className="text-caption2 text-muted-foreground tracking-[0.2]">{eyebrow}</Text>
|
|
48
|
+
) : null}
|
|
49
|
+
<Text numberOfLines={2} className="text-body text-foreground" style={titleStyle}>
|
|
50
|
+
{title}
|
|
51
|
+
</Text>
|
|
52
|
+
{subtitle ? (
|
|
53
|
+
<Text
|
|
54
|
+
ellipsizeMode="tail"
|
|
55
|
+
numberOfLines={1}
|
|
56
|
+
className="text-caption1 text-muted-foreground"
|
|
57
|
+
>
|
|
58
|
+
{subtitle}
|
|
59
|
+
</Text>
|
|
60
|
+
) : null}
|
|
61
|
+
</View>
|
|
62
|
+
{trailing ? <View className="items-center justify-center">{trailing}</View> : null}
|
|
63
|
+
</Pressable>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export { ListRow };
|
|
@@ -223,16 +223,44 @@ export default {
|
|
|
223
223
|
icon: "0.375rem",
|
|
224
224
|
},
|
|
225
225
|
"stream-item": {
|
|
226
|
-
"border-radius": "
|
|
226
|
+
"border-radius": "0rem",
|
|
227
227
|
"border-width": "1px",
|
|
228
|
-
gap: "0.
|
|
229
|
-
"icon-size": "1.
|
|
230
|
-
"min-height": "
|
|
231
|
-
"padding-horizontal": "
|
|
232
|
-
"padding-vertical": "0.
|
|
228
|
+
gap: "0.75rem",
|
|
229
|
+
"icon-size": "1.25rem",
|
|
230
|
+
"min-height": "3.5rem",
|
|
231
|
+
"padding-horizontal": "1rem",
|
|
232
|
+
"padding-vertical": "0.5rem",
|
|
233
233
|
},
|
|
234
234
|
"font-family": {
|
|
235
235
|
primary: "System",
|
|
236
236
|
mono: "Menlo",
|
|
237
237
|
},
|
|
238
|
+
motion: {
|
|
239
|
+
duration: {
|
|
240
|
+
instant: "0ms",
|
|
241
|
+
quick: "150ms",
|
|
242
|
+
standard: "300ms",
|
|
243
|
+
deliberate: "500ms",
|
|
244
|
+
},
|
|
245
|
+
easing: {
|
|
246
|
+
enter: [0, 0, 0.2, 1],
|
|
247
|
+
exit: [0.4, 0, 1, 1],
|
|
248
|
+
inOut: [0.4, 0, 0.2, 1],
|
|
249
|
+
},
|
|
250
|
+
distance: {
|
|
251
|
+
rowEnter: "0.5rem",
|
|
252
|
+
sceneReveal: "2rem",
|
|
253
|
+
},
|
|
254
|
+
opacity: {
|
|
255
|
+
hidden: 0,
|
|
256
|
+
visible: 1,
|
|
257
|
+
disabled: 0.5,
|
|
258
|
+
},
|
|
259
|
+
interruption: {
|
|
260
|
+
policy: "cancel-and-settle",
|
|
261
|
+
},
|
|
262
|
+
reducedMotion: {
|
|
263
|
+
policy: "replace-transform-with-opacity",
|
|
264
|
+
},
|
|
265
|
+
},
|
|
238
266
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import darkTokens from "./generated/dark.colors.generated";
|
|
2
2
|
import foundations from "./generated/foundations.generated";
|
|
3
3
|
import lightTokens from "./generated/light.colors.generated";
|
|
4
|
+
import nativeTokens from "./generated/native.generated";
|
|
4
5
|
|
|
5
6
|
const toPixels = (value: string) => {
|
|
6
7
|
if (value.endsWith("rem")) return Number.parseFloat(value) * 16;
|
|
@@ -62,6 +63,7 @@ export const transitionDurations = Object.fromEntries(
|
|
|
62
63
|
Object.entries(foundations.duration).map(([key, value]) => [key, toMilliseconds(value)]),
|
|
63
64
|
) as Record<keyof typeof foundations.duration, number>;
|
|
64
65
|
export const animations = foundations.animate;
|
|
66
|
+
export const motion = nativeTokens.motion;
|
|
65
67
|
|
|
66
68
|
export type RadiusToken = keyof typeof radii;
|
|
67
69
|
export type TextSizeToken = keyof typeof textSizes;
|
|
@@ -17,7 +17,9 @@ const makeFlat = (raw: Record<string, unknown>) => ({
|
|
|
17
17
|
fontWeight: raw.fontWeight as number,
|
|
18
18
|
lineHeight: unwrap(raw.lineHeight as DimensionValue),
|
|
19
19
|
letterSpacing: unwrap(raw.letterSpacing as DimensionValue),
|
|
20
|
-
...(raw.textTransform
|
|
20
|
+
...(raw.textTransform
|
|
21
|
+
? { textTransform: raw.textTransform as "uppercase" | "lowercase" | "capitalize" | "none" }
|
|
22
|
+
: {}),
|
|
21
23
|
});
|
|
22
24
|
|
|
23
25
|
const generated = nativeGenerated as unknown as {
|
|
@@ -62,13 +64,13 @@ export const screenMargin = toPixels(generated["screen-margin"]);
|
|
|
62
64
|
|
|
63
65
|
export const componentSize = Object.fromEntries(
|
|
64
66
|
Object.entries(generated["component-size"]).map(([key, value]) => [key, toPixels(value)]),
|
|
65
|
-
) as Record<keyof typeof generated["component-size"], number>;
|
|
67
|
+
) as Record<keyof (typeof generated)["component-size"], number>;
|
|
66
68
|
|
|
67
69
|
export type ComponentSizeToken = keyof typeof componentSize;
|
|
68
70
|
|
|
69
71
|
export const borderRadius = Object.fromEntries(
|
|
70
72
|
Object.entries(generated["border-radius"]).map(([key, value]) => [key, toPixels(value)]),
|
|
71
|
-
) as Record<keyof typeof generated["border-radius"], number>;
|
|
73
|
+
) as Record<keyof (typeof generated)["border-radius"], number>;
|
|
72
74
|
|
|
73
75
|
export type BorderRadiusToken = keyof typeof borderRadius;
|
|
74
76
|
|
|
@@ -29,15 +29,15 @@
|
|
|
29
29
|
"icon": { "$value": "{radius.md}" }
|
|
30
30
|
},
|
|
31
31
|
"stream-item": {
|
|
32
|
-
"$description": "Shared
|
|
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":
|
|
34
|
+
"border-radius": { "$value": { "value": 0, "unit": "rem" } },
|
|
35
35
|
"border-width": { "$value": { "value": 1, "unit": "px" } },
|
|
36
|
-
"gap": { "$value": "{spacing-scale.
|
|
37
|
-
"icon-size": { "$value": "{component-size.
|
|
38
|
-
"min-height": { "$value": {
|
|
39
|
-
"padding-horizontal": { "$value": "{spacing-scale.
|
|
40
|
-
"padding-vertical": { "$value": "{spacing-scale.
|
|
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: [
|
|
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",
|