@cntyclub/ui-react 0.1.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/dist/chunk-HDGMSYQS.js +26461 -0
- package/dist/chunk-HDGMSYQS.js.map +1 -0
- package/dist/chunk-PR4QN5HX.js +39 -0
- package/dist/chunk-PR4QN5HX.js.map +1 -0
- package/dist/form.d.ts +175 -0
- package/dist/form.js +5207 -0
- package/dist/form.js.map +1 -0
- package/dist/index.d.ts +1462 -0
- package/dist/index.js +81862 -0
- package/dist/index.js.map +1 -0
- package/dist/input-CZvh825j.d.ts +24 -0
- package/dist/qr-code-styling-3Y6LZH6V.js +1123 -0
- package/dist/qr-code-styling-3Y6LZH6V.js.map +1 -0
- package/package.json +79 -0
- package/src/components/form/checkbox-group-field.tsx +101 -0
- package/src/components/form/date-field.tsx +79 -0
- package/src/components/form/date-range-field.tsx +106 -0
- package/src/components/form/form-context.ts +10 -0
- package/src/components/form/form.tsx +54 -0
- package/src/components/form/number-field.tsx +69 -0
- package/src/components/form/select-field.tsx +76 -0
- package/src/components/form/submit-button.tsx +28 -0
- package/src/components/form/text-field.tsx +107 -0
- package/src/components/layout/dashboard-header.tsx +54 -0
- package/src/components/layout/dashboard-panel.tsx +34 -0
- package/src/components/theme-provider.tsx +403 -0
- package/src/components/ui/accordion.tsx +69 -0
- package/src/components/ui/alert-dialog.tsx +169 -0
- package/src/components/ui/alert.tsx +80 -0
- package/src/components/ui/animated-theme-toggler.tsx +265 -0
- package/src/components/ui/app-store-buttons.tsx +182 -0
- package/src/components/ui/aspect-ratio.tsx +23 -0
- package/src/components/ui/autocomplete.tsx +296 -0
- package/src/components/ui/avatar-group.tsx +95 -0
- package/src/components/ui/avatar.tsx +285 -0
- package/src/components/ui/badge-group.tsx +160 -0
- package/src/components/ui/badge.tsx +172 -0
- package/src/components/ui/breadcrumb.tsx +112 -0
- package/src/components/ui/button.tsx +77 -0
- package/src/components/ui/calendar.tsx +137 -0
- package/src/components/ui/card.tsx +244 -0
- package/src/components/ui/carousel.tsx +258 -0
- package/src/components/ui/chart.tsx +379 -0
- package/src/components/ui/checkbox-group.tsx +16 -0
- package/src/components/ui/checkbox.tsx +82 -0
- package/src/components/ui/collapsible.tsx +45 -0
- package/src/components/ui/combobox.tsx +411 -0
- package/src/components/ui/command.tsx +264 -0
- package/src/components/ui/context-menu.tsx +271 -0
- package/src/components/ui/credit-card.tsx +214 -0
- package/src/components/ui/dialog.tsx +196 -0
- package/src/components/ui/drawer.tsx +135 -0
- package/src/components/ui/empty.tsx +127 -0
- package/src/components/ui/featured-icon.tsx +149 -0
- package/src/components/ui/field.tsx +88 -0
- package/src/components/ui/fieldset.tsx +29 -0
- package/src/components/ui/form.tsx +17 -0
- package/src/components/ui/frame.tsx +82 -0
- package/src/components/ui/generic-empty.tsx +142 -0
- package/src/components/ui/group.tsx +97 -0
- package/src/components/ui/horizontal-scroll-fader.tsx +228 -0
- package/src/components/ui/input-group.tsx +102 -0
- package/src/components/ui/input-otp.tsx +96 -0
- package/src/components/ui/input.tsx +66 -0
- package/src/components/ui/item.tsx +198 -0
- package/src/components/ui/kbd.tsx +30 -0
- package/src/components/ui/label.tsx +28 -0
- package/src/components/ui/menu.tsx +312 -0
- package/src/components/ui/menubar.tsx +93 -0
- package/src/components/ui/meter.tsx +67 -0
- package/src/components/ui/multi-select.tsx +308 -0
- package/src/components/ui/navigation-menu.tsx +143 -0
- package/src/components/ui/number-field.tsx +160 -0
- package/src/components/ui/pagination-controls.tsx +74 -0
- package/src/components/ui/pagination.tsx +149 -0
- package/src/components/ui/popover.tsx +119 -0
- package/src/components/ui/preview-card.tsx +55 -0
- package/src/components/ui/progress.tsx +289 -0
- package/src/components/ui/qr-code.tsx +150 -0
- package/src/components/ui/radio-group.tsx +103 -0
- package/src/components/ui/resizable.tsx +56 -0
- package/src/components/ui/scroll-area.tsx +90 -0
- package/src/components/ui/scroller.tsx +38 -0
- package/src/components/ui/section-header.tsx +118 -0
- package/src/components/ui/select.tsx +181 -0
- package/src/components/ui/separator.tsx +23 -0
- package/src/components/ui/sheet.tsx +224 -0
- package/src/components/ui/sidebar.tsx +744 -0
- package/src/components/ui/skeleton.tsx +16 -0
- package/src/components/ui/slider.tsx +108 -0
- package/src/components/ui/smooth-scroll.tsx +143 -0
- package/src/components/ui/social-button.tsx +247 -0
- package/src/components/ui/spinner-on-demand.tsx +32 -0
- package/src/components/ui/spinner.tsx +18 -0
- package/src/components/ui/stat.tsx +187 -0
- package/src/components/ui/stepper.tsx +167 -0
- package/src/components/ui/switch.tsx +56 -0
- package/src/components/ui/table.tsx +126 -0
- package/src/components/ui/tabs.tsx +90 -0
- package/src/components/ui/tag.tsx +229 -0
- package/src/components/ui/target-countdown.tsx +46 -0
- package/src/components/ui/text-editor.tsx +313 -0
- package/src/components/ui/textarea.tsx +51 -0
- package/src/components/ui/timeline.tsx +116 -0
- package/src/components/ui/toast.tsx +268 -0
- package/src/components/ui/toggle-group.tsx +101 -0
- package/src/components/ui/toggle.tsx +45 -0
- package/src/components/ui/toolbar.tsx +89 -0
- package/src/components/ui/tooltip.tsx +102 -0
- package/src/components/ui/vertical-scroll-fader.tsx +250 -0
- package/src/components/ui/video-player.tsx +275 -0
- package/src/components/upload/avatar-upload-base.tsx +131 -0
- package/src/components/upload/image-upload-base.tsx +112 -0
- package/src/form.ts +17 -0
- package/src/index.ts +125 -0
- package/src/lib/hooks/use-callback-ref.ts +15 -0
- package/src/lib/hooks/use-first-render.ts +11 -0
- package/src/lib/hooks/use-hover.ts +53 -0
- package/src/lib/hooks/use-is-tab-active.ts +17 -0
- package/src/lib/hooks/use-media-query.ts +164 -0
- package/src/lib/utils/css.ts +6 -0
- package/src/styles.css +300 -0
- package/src/types/helpers.ts +24 -0
- package/src/types/react.d.ts +7 -0
package/package.json
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cntyclub/ui-react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React component library for the Country Club UI Kit — Base UI primitives styled with the Country Club design system (Tailwind CSS v4)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": [
|
|
7
|
+
"**/*.css"
|
|
8
|
+
],
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"module": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./form": {
|
|
19
|
+
"types": "./dist/form.d.ts",
|
|
20
|
+
"import": "./dist/form.js",
|
|
21
|
+
"default": "./dist/form.js"
|
|
22
|
+
},
|
|
23
|
+
"./styles.css": "./src/styles.css",
|
|
24
|
+
"./package.json": "./package.json"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"src",
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
30
|
+
"comment-publishConfig": "In-repo (docs/storybook/vue) consume src/ via the exports above. When PUBLISHED to a registry, npm/pnpm swap in these fields so external installs get the compiled, self-contained dist/ (tsup bundles every dependency except react/react-dom) — no transpilePackages, no heavy transitive dep tree.",
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@base-ui/react": "1.1.0",
|
|
33
|
+
"@headlessui/react": "^2.2.9",
|
|
34
|
+
"@radix-ui/react-slot": "^1.2.4",
|
|
35
|
+
"@tanstack/react-form": "^1.28.5",
|
|
36
|
+
"@tiptap/core": "^3.26.1",
|
|
37
|
+
"@tiptap/extension-placeholder": "^3.26.1",
|
|
38
|
+
"@tiptap/extension-text-align": "^3.26.1",
|
|
39
|
+
"@tiptap/pm": "^3.26.1",
|
|
40
|
+
"@tiptap/react": "^3.26.1",
|
|
41
|
+
"@tiptap/starter-kit": "^3.26.1",
|
|
42
|
+
"class-variance-authority": "^0.7.1",
|
|
43
|
+
"clsx": "^2.1.1",
|
|
44
|
+
"date-fns": "^4.1.0",
|
|
45
|
+
"embla-carousel-react": "^8.6.0",
|
|
46
|
+
"input-otp": "^1.4.2",
|
|
47
|
+
"lucide-react": "^0.561.0",
|
|
48
|
+
"qr-code-styling": "^1.9.2",
|
|
49
|
+
"react-day-picker": "^9.14.0",
|
|
50
|
+
"react-dropzone": "^14.4.0",
|
|
51
|
+
"react-resizable-panels": "^3.0.6",
|
|
52
|
+
"recharts": "^3.7.0",
|
|
53
|
+
"spin-delay": "^2.0.1",
|
|
54
|
+
"tailwind-merge": "^3.4.0",
|
|
55
|
+
"tw-animate-css": "^1.4.0",
|
|
56
|
+
"use-resize-observer": "^9.1.0",
|
|
57
|
+
"vaul": "^1.1.2",
|
|
58
|
+
"zustand": "^5.0.11"
|
|
59
|
+
},
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"react": "^19.0.0",
|
|
62
|
+
"react-dom": "^19.0.0"
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@types/react": "^19.1.0",
|
|
66
|
+
"@types/react-dom": "^19.1.0",
|
|
67
|
+
"react": "^19.1.0",
|
|
68
|
+
"react-dom": "^19.1.0",
|
|
69
|
+
"tsup": "^8.4.0",
|
|
70
|
+
"typescript": "^5.8.3",
|
|
71
|
+
"@countryclub/typescript-config": "0.1.0"
|
|
72
|
+
},
|
|
73
|
+
"scripts": {
|
|
74
|
+
"build": "tsup",
|
|
75
|
+
"dev": "tsup --watch",
|
|
76
|
+
"typecheck": "tsc --noEmit",
|
|
77
|
+
"clean": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\""
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { StandardSchemaV1Issue } from "@tanstack/react-form";
|
|
4
|
+
import { Checkbox } from "../ui/checkbox";
|
|
5
|
+
import { CheckboxGroup } from "../ui/checkbox-group";
|
|
6
|
+
import {
|
|
7
|
+
Field,
|
|
8
|
+
FieldDescription,
|
|
9
|
+
FieldError,
|
|
10
|
+
FieldLabel,
|
|
11
|
+
} from "../ui/field";
|
|
12
|
+
|
|
13
|
+
export function CheckboxGroupField({
|
|
14
|
+
field,
|
|
15
|
+
label,
|
|
16
|
+
options,
|
|
17
|
+
emptyMessage,
|
|
18
|
+
description,
|
|
19
|
+
}: {
|
|
20
|
+
field: {
|
|
21
|
+
name: string;
|
|
22
|
+
state: {
|
|
23
|
+
value: string[];
|
|
24
|
+
meta: {
|
|
25
|
+
readonly errors: ReadonlyArray<
|
|
26
|
+
string | StandardSchemaV1Issue | undefined
|
|
27
|
+
>;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
handleChange: (nextValue: string[]) => void;
|
|
31
|
+
};
|
|
32
|
+
label: string;
|
|
33
|
+
options: Array<{
|
|
34
|
+
value: string;
|
|
35
|
+
label: string;
|
|
36
|
+
description?: string;
|
|
37
|
+
disabled?: boolean;
|
|
38
|
+
}>;
|
|
39
|
+
emptyMessage?: string;
|
|
40
|
+
description?: string;
|
|
41
|
+
}) {
|
|
42
|
+
const errors = field.state.meta.errors
|
|
43
|
+
.map((err) => (typeof err === "string" ? err : err?.message))
|
|
44
|
+
.filter(Boolean);
|
|
45
|
+
return (
|
|
46
|
+
<Field name={field.name}>
|
|
47
|
+
<FieldLabel>{label}</FieldLabel>
|
|
48
|
+
|
|
49
|
+
{options.length === 0 ? (
|
|
50
|
+
<div className="text-sm text-muted-foreground">
|
|
51
|
+
{emptyMessage ?? "No options"}
|
|
52
|
+
</div>
|
|
53
|
+
) : (
|
|
54
|
+
<CheckboxGroup
|
|
55
|
+
value={field.state.value}
|
|
56
|
+
onValueChange={(next) => field.handleChange(next)}
|
|
57
|
+
className="gap-2 w-full"
|
|
58
|
+
>
|
|
59
|
+
{options.map((opt) => {
|
|
60
|
+
const isChecked = field.state.value.includes(opt.value);
|
|
61
|
+
return (
|
|
62
|
+
<button
|
|
63
|
+
type="button"
|
|
64
|
+
key={opt.value}
|
|
65
|
+
disabled={opt.disabled}
|
|
66
|
+
className="flex w-full items-center gap-3 rounded-lg border px-3 py-2 transition-colors text-left bg-transparent disabled:cursor-not-allowed disabled:opacity-50 cursor-pointer hover:bg-accent/50"
|
|
67
|
+
onClick={() => {
|
|
68
|
+
if (opt.disabled) return;
|
|
69
|
+
const next = isChecked
|
|
70
|
+
? field.state.value.filter((v) => v !== opt.value)
|
|
71
|
+
: [...field.state.value, opt.value];
|
|
72
|
+
field.handleChange(next);
|
|
73
|
+
}}
|
|
74
|
+
>
|
|
75
|
+
<Checkbox
|
|
76
|
+
value={opt.value}
|
|
77
|
+
checked={isChecked}
|
|
78
|
+
disabled={opt.disabled}
|
|
79
|
+
readOnly
|
|
80
|
+
/>
|
|
81
|
+
<div className="min-w-0 flex-1">
|
|
82
|
+
<p className="text-sm font-medium text-foreground">
|
|
83
|
+
{opt.label}
|
|
84
|
+
</p>
|
|
85
|
+
{opt.description ? (
|
|
86
|
+
<p className="text-xs text-muted-foreground">
|
|
87
|
+
{opt.description}
|
|
88
|
+
</p>
|
|
89
|
+
) : null}
|
|
90
|
+
</div>
|
|
91
|
+
</button>
|
|
92
|
+
);
|
|
93
|
+
})}
|
|
94
|
+
</CheckboxGroup>
|
|
95
|
+
)}
|
|
96
|
+
|
|
97
|
+
<FieldError match={!!errors.length}>{errors.join(", ")}</FieldError>
|
|
98
|
+
{description && <FieldDescription>{description}</FieldDescription>}
|
|
99
|
+
</Field>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { StandardSchemaV1Issue } from "@tanstack/react-form";
|
|
4
|
+
import { format } from "date-fns";
|
|
5
|
+
import { CalendarIcon } from "lucide-react";
|
|
6
|
+
|
|
7
|
+
import { Button } from "../ui/button";
|
|
8
|
+
import { Calendar } from "../ui/calendar";
|
|
9
|
+
import { Field, FieldError, FieldLabel } from "../ui/field";
|
|
10
|
+
import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover";
|
|
11
|
+
import { cn } from "../../lib/utils/css";
|
|
12
|
+
|
|
13
|
+
export function DateField({
|
|
14
|
+
field,
|
|
15
|
+
label,
|
|
16
|
+
placeholder,
|
|
17
|
+
}: {
|
|
18
|
+
field: {
|
|
19
|
+
name: string;
|
|
20
|
+
state: {
|
|
21
|
+
value: Date | undefined;
|
|
22
|
+
meta: {
|
|
23
|
+
readonly errors: ReadonlyArray<
|
|
24
|
+
string | StandardSchemaV1Issue | undefined
|
|
25
|
+
>;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
handleChange: (nextValue: Date | undefined) => void;
|
|
29
|
+
handleBlur: () => void;
|
|
30
|
+
};
|
|
31
|
+
label: string;
|
|
32
|
+
placeholder?: string;
|
|
33
|
+
}) {
|
|
34
|
+
const value = field.state.value;
|
|
35
|
+
|
|
36
|
+
const errors = field.state.meta.errors
|
|
37
|
+
.map((err) => (typeof err === "string" ? err : err?.message))
|
|
38
|
+
.filter(Boolean);
|
|
39
|
+
|
|
40
|
+
const hasValue = Boolean(value);
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<Field name={field.name}>
|
|
44
|
+
<FieldLabel>{label}</FieldLabel>
|
|
45
|
+
<Popover onOpenChangeComplete={(open) => !open && field.handleBlur()}>
|
|
46
|
+
<PopoverTrigger
|
|
47
|
+
render={
|
|
48
|
+
<Button
|
|
49
|
+
className={cn("w-full justify-start", {
|
|
50
|
+
"text-muted-foreground": !hasValue,
|
|
51
|
+
})}
|
|
52
|
+
variant="outline"
|
|
53
|
+
/>
|
|
54
|
+
}
|
|
55
|
+
>
|
|
56
|
+
<CalendarIcon aria-hidden="true" />
|
|
57
|
+
{value ? (
|
|
58
|
+
format(value, "LLL dd, y")
|
|
59
|
+
) : (
|
|
60
|
+
<span>{placeholder ?? "Pick a date"}</span>
|
|
61
|
+
)}
|
|
62
|
+
</PopoverTrigger>
|
|
63
|
+
<PopoverPopup>
|
|
64
|
+
<Calendar
|
|
65
|
+
mode="single"
|
|
66
|
+
defaultMonth={value}
|
|
67
|
+
selected={value}
|
|
68
|
+
numberOfMonths={2}
|
|
69
|
+
onSelect={(next) => {
|
|
70
|
+
field.handleChange(next ?? undefined);
|
|
71
|
+
}}
|
|
72
|
+
autoFocus
|
|
73
|
+
/>
|
|
74
|
+
</PopoverPopup>
|
|
75
|
+
</Popover>
|
|
76
|
+
<FieldError match={!!errors.length}>{errors.join(", ")}</FieldError>
|
|
77
|
+
</Field>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { StandardSchemaV1Issue } from "@tanstack/react-form";
|
|
4
|
+
import { format } from "date-fns";
|
|
5
|
+
import { CalendarIcon } from "lucide-react";
|
|
6
|
+
import { useState } from "react";
|
|
7
|
+
import { Button } from "../ui/button";
|
|
8
|
+
import { Calendar } from "../ui/calendar";
|
|
9
|
+
import { Field, FieldError, FieldLabel } from "../ui/field";
|
|
10
|
+
import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover";
|
|
11
|
+
import { cn } from "../../lib/utils/css";
|
|
12
|
+
|
|
13
|
+
export function DateRangeField({
|
|
14
|
+
field,
|
|
15
|
+
label,
|
|
16
|
+
placeholder,
|
|
17
|
+
numberOfMonths = 2,
|
|
18
|
+
}: {
|
|
19
|
+
field: {
|
|
20
|
+
name: string;
|
|
21
|
+
state: {
|
|
22
|
+
value: {
|
|
23
|
+
from: Date | undefined;
|
|
24
|
+
to: Date | undefined;
|
|
25
|
+
};
|
|
26
|
+
meta: {
|
|
27
|
+
readonly errors: ReadonlyArray<
|
|
28
|
+
string | StandardSchemaV1Issue | undefined
|
|
29
|
+
>;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
handleChange: (nextValue: {
|
|
33
|
+
from: Date | undefined;
|
|
34
|
+
to: Date | undefined;
|
|
35
|
+
}) => void;
|
|
36
|
+
handleBlur: () => void;
|
|
37
|
+
};
|
|
38
|
+
label: string;
|
|
39
|
+
placeholder?: string;
|
|
40
|
+
numberOfMonths?: number;
|
|
41
|
+
}) {
|
|
42
|
+
const value = field.state.value;
|
|
43
|
+
|
|
44
|
+
const errors = field.state.meta.errors
|
|
45
|
+
.map((err) => (typeof err === "string" ? err : err?.message))
|
|
46
|
+
.filter(Boolean);
|
|
47
|
+
|
|
48
|
+
const hasValue = Boolean(value.from);
|
|
49
|
+
const [open, setOpen] = useState(false);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Field name={field.name}>
|
|
53
|
+
<FieldLabel>{label}</FieldLabel>
|
|
54
|
+
<Popover
|
|
55
|
+
open={open}
|
|
56
|
+
onOpenChange={setOpen}
|
|
57
|
+
onOpenChangeComplete={(open) => !open && field.handleBlur()}
|
|
58
|
+
>
|
|
59
|
+
<PopoverTrigger
|
|
60
|
+
render={
|
|
61
|
+
<Button
|
|
62
|
+
className={cn("w-full justify-start", {
|
|
63
|
+
"text-muted-foreground": !hasValue,
|
|
64
|
+
})}
|
|
65
|
+
variant="outline"
|
|
66
|
+
/>
|
|
67
|
+
}
|
|
68
|
+
>
|
|
69
|
+
<CalendarIcon aria-hidden="true" />
|
|
70
|
+
{value.from ? (
|
|
71
|
+
value.to ? (
|
|
72
|
+
<>
|
|
73
|
+
{format(value.from, "LLL dd, y")} -{" "}
|
|
74
|
+
{format(value.to, "LLL dd, y")}
|
|
75
|
+
</>
|
|
76
|
+
) : (
|
|
77
|
+
format(value.from, "LLL dd, y")
|
|
78
|
+
)
|
|
79
|
+
) : (
|
|
80
|
+
<span>{placeholder ?? "Pick a date range"}</span>
|
|
81
|
+
)}
|
|
82
|
+
</PopoverTrigger>
|
|
83
|
+
<PopoverPopup>
|
|
84
|
+
<Calendar
|
|
85
|
+
mode="range"
|
|
86
|
+
defaultMonth={value.from}
|
|
87
|
+
selected={value}
|
|
88
|
+
resetOnSelect
|
|
89
|
+
numberOfMonths={numberOfMonths}
|
|
90
|
+
onSelect={(range) => {
|
|
91
|
+
field.handleChange({
|
|
92
|
+
from: range?.from ?? undefined,
|
|
93
|
+
to: range?.to ?? undefined,
|
|
94
|
+
});
|
|
95
|
+
if (range?.from && range?.to) {
|
|
96
|
+
setOpen(false);
|
|
97
|
+
}
|
|
98
|
+
}}
|
|
99
|
+
autoFocus
|
|
100
|
+
/>
|
|
101
|
+
</PopoverPopup>
|
|
102
|
+
</Popover>
|
|
103
|
+
<FieldError match={!!errors.length}>{errors.join(", ")}</FieldError>
|
|
104
|
+
</Field>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createFormHookContexts } from "@tanstack/react-form";
|
|
4
|
+
|
|
5
|
+
export const {
|
|
6
|
+
fieldContext: appFieldContext,
|
|
7
|
+
formContext: appFormContext,
|
|
8
|
+
useFieldContext: useAppFieldContext,
|
|
9
|
+
useFormContext: useAppFormContext,
|
|
10
|
+
} = createFormHookContexts();
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createFormHook,
|
|
5
|
+
formOptions,
|
|
6
|
+
revalidateLogic,
|
|
7
|
+
} from "@tanstack/react-form";
|
|
8
|
+
import { CheckboxGroupField } from "./checkbox-group-field";
|
|
9
|
+
import { DateField } from "./date-field";
|
|
10
|
+
import { DateRangeField } from "./date-range-field";
|
|
11
|
+
import { appFieldContext, appFormContext } from "./form-context";
|
|
12
|
+
import { NumberField } from "./number-field";
|
|
13
|
+
import { SelectField } from "./select-field";
|
|
14
|
+
import { SubmitButton } from "./submit-button";
|
|
15
|
+
import { TextField } from "./text-field";
|
|
16
|
+
|
|
17
|
+
const { useAppForm, withForm: withAppForm } = createFormHook({
|
|
18
|
+
fieldContext: appFieldContext,
|
|
19
|
+
formContext: appFormContext,
|
|
20
|
+
fieldComponents: {
|
|
21
|
+
TextField,
|
|
22
|
+
CheckboxGroupField,
|
|
23
|
+
SelectField,
|
|
24
|
+
DateField,
|
|
25
|
+
DateRangeField,
|
|
26
|
+
NumberField,
|
|
27
|
+
},
|
|
28
|
+
formComponents: {
|
|
29
|
+
SubmitButton,
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const customFormOptions = formOptions({
|
|
34
|
+
validationLogic: revalidateLogic({
|
|
35
|
+
mode: "submit",
|
|
36
|
+
modeAfterSubmission: "blur",
|
|
37
|
+
}),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const useCustomAppForm: typeof useAppForm = (options) => {
|
|
41
|
+
return useAppForm({
|
|
42
|
+
...customFormOptions,
|
|
43
|
+
...options,
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const withCustomAppForm: typeof withAppForm = (options) => {
|
|
48
|
+
return withAppForm({
|
|
49
|
+
...customFormOptions,
|
|
50
|
+
...options,
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export { useCustomAppForm as useAppForm, withCustomAppForm as withAppForm };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { StandardSchemaV1Issue } from "@tanstack/react-form";
|
|
4
|
+
import {
|
|
5
|
+
Field,
|
|
6
|
+
FieldControl,
|
|
7
|
+
FieldError,
|
|
8
|
+
FieldLabel,
|
|
9
|
+
} from "../ui/field";
|
|
10
|
+
import {
|
|
11
|
+
NumberFieldDecrement,
|
|
12
|
+
NumberFieldGroup,
|
|
13
|
+
NumberFieldIncrement,
|
|
14
|
+
NumberFieldInput,
|
|
15
|
+
NumberField as NumberFieldPrimitive,
|
|
16
|
+
} from "../ui/number-field";
|
|
17
|
+
|
|
18
|
+
export function NumberField({
|
|
19
|
+
field,
|
|
20
|
+
label,
|
|
21
|
+
min,
|
|
22
|
+
className,
|
|
23
|
+
}: {
|
|
24
|
+
field: {
|
|
25
|
+
name: string;
|
|
26
|
+
state: {
|
|
27
|
+
value: number;
|
|
28
|
+
meta: {
|
|
29
|
+
readonly errors: ReadonlyArray<
|
|
30
|
+
string | StandardSchemaV1Issue | undefined
|
|
31
|
+
>;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
handleChange: (nextValue: number) => void;
|
|
35
|
+
handleBlur: () => void;
|
|
36
|
+
};
|
|
37
|
+
label: string;
|
|
38
|
+
min?: number;
|
|
39
|
+
className?: string;
|
|
40
|
+
}) {
|
|
41
|
+
const errors = field.state.meta.errors
|
|
42
|
+
.map((err) => (typeof err === "string" ? err : err?.message))
|
|
43
|
+
.filter(Boolean);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Field className={className} name={field.name}>
|
|
47
|
+
<FieldLabel>{label}</FieldLabel>
|
|
48
|
+
<FieldControl
|
|
49
|
+
render={
|
|
50
|
+
<NumberFieldPrimitive
|
|
51
|
+
value={field.state.value}
|
|
52
|
+
min={min}
|
|
53
|
+
onValueChange={(next) => {
|
|
54
|
+
if (typeof next === "number") field.handleChange(next);
|
|
55
|
+
}}
|
|
56
|
+
onBlur={() => field.handleBlur()}
|
|
57
|
+
>
|
|
58
|
+
<NumberFieldGroup>
|
|
59
|
+
<NumberFieldDecrement />
|
|
60
|
+
<NumberFieldInput />
|
|
61
|
+
<NumberFieldIncrement />
|
|
62
|
+
</NumberFieldGroup>
|
|
63
|
+
</NumberFieldPrimitive>
|
|
64
|
+
}
|
|
65
|
+
/>
|
|
66
|
+
<FieldError match={!!errors.length}>{errors.join(", ")}</FieldError>
|
|
67
|
+
</Field>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { StandardSchemaV1Issue } from "@tanstack/react-form";
|
|
4
|
+
import {
|
|
5
|
+
Field,
|
|
6
|
+
FieldControl,
|
|
7
|
+
FieldError,
|
|
8
|
+
FieldLabel,
|
|
9
|
+
} from "../ui/field";
|
|
10
|
+
import {
|
|
11
|
+
Select,
|
|
12
|
+
SelectItem,
|
|
13
|
+
SelectPopup,
|
|
14
|
+
SelectTrigger,
|
|
15
|
+
SelectValue,
|
|
16
|
+
} from "../ui/select";
|
|
17
|
+
|
|
18
|
+
export function SelectField({
|
|
19
|
+
field,
|
|
20
|
+
label,
|
|
21
|
+
placeholder,
|
|
22
|
+
options,
|
|
23
|
+
}: {
|
|
24
|
+
field: {
|
|
25
|
+
name: string;
|
|
26
|
+
state: {
|
|
27
|
+
value: string;
|
|
28
|
+
meta: {
|
|
29
|
+
readonly errors: ReadonlyArray<
|
|
30
|
+
string | StandardSchemaV1Issue | undefined
|
|
31
|
+
>;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
handleChange: (nextValue: string) => void;
|
|
35
|
+
handleBlur: () => void;
|
|
36
|
+
};
|
|
37
|
+
label: string;
|
|
38
|
+
placeholder?: string;
|
|
39
|
+
options: Array<{ value: string; label: string }>;
|
|
40
|
+
}) {
|
|
41
|
+
const errors = field.state.meta.errors
|
|
42
|
+
.map((err) => (typeof err === "string" ? err : err?.message))
|
|
43
|
+
.filter(Boolean);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Field name={field.name}>
|
|
47
|
+
<FieldLabel>{label}</FieldLabel>
|
|
48
|
+
<FieldControl
|
|
49
|
+
render={
|
|
50
|
+
<Select
|
|
51
|
+
items={options}
|
|
52
|
+
value={field.state.value ? field.state.value : null}
|
|
53
|
+
onValueChange={(next) => {
|
|
54
|
+
field.handleChange(next ?? "");
|
|
55
|
+
}}
|
|
56
|
+
onOpenChangeComplete={(open) => {
|
|
57
|
+
if (!open) field.handleBlur();
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
<SelectTrigger>
|
|
61
|
+
<SelectValue placeholder={placeholder ?? "Select"} />
|
|
62
|
+
</SelectTrigger>
|
|
63
|
+
<SelectPopup>
|
|
64
|
+
{options.map((opt) => (
|
|
65
|
+
<SelectItem key={opt.value} value={opt.value}>
|
|
66
|
+
{opt.label}
|
|
67
|
+
</SelectItem>
|
|
68
|
+
))}
|
|
69
|
+
</SelectPopup>
|
|
70
|
+
</Select>
|
|
71
|
+
}
|
|
72
|
+
/>
|
|
73
|
+
<FieldError match={!!errors.length}>{errors.join(", ")}</FieldError>
|
|
74
|
+
</Field>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button } from "../ui/button";
|
|
4
|
+
import { SpinnerOnDemand } from "../ui/spinner-on-demand";
|
|
5
|
+
import { useAppFormContext } from "./form-context";
|
|
6
|
+
|
|
7
|
+
interface SubmitButtonProps extends React.ComponentProps<typeof Button> {
|
|
8
|
+
icon?: React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function SubmitButton({
|
|
12
|
+
children,
|
|
13
|
+
disabled,
|
|
14
|
+
icon,
|
|
15
|
+
...props
|
|
16
|
+
}: SubmitButtonProps) {
|
|
17
|
+
const form = useAppFormContext();
|
|
18
|
+
return (
|
|
19
|
+
<form.Subscribe selector={(state) => state.isSubmitting}>
|
|
20
|
+
{(isSubmitting) => (
|
|
21
|
+
<Button type="submit" disabled={isSubmitting || disabled} {...props}>
|
|
22
|
+
<SpinnerOnDemand icon={icon} isLoading={isSubmitting} />
|
|
23
|
+
{children}
|
|
24
|
+
</Button>
|
|
25
|
+
)}
|
|
26
|
+
</form.Subscribe>
|
|
27
|
+
);
|
|
28
|
+
}
|