@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.
Files changed (124) hide show
  1. package/dist/chunk-HDGMSYQS.js +26461 -0
  2. package/dist/chunk-HDGMSYQS.js.map +1 -0
  3. package/dist/chunk-PR4QN5HX.js +39 -0
  4. package/dist/chunk-PR4QN5HX.js.map +1 -0
  5. package/dist/form.d.ts +175 -0
  6. package/dist/form.js +5207 -0
  7. package/dist/form.js.map +1 -0
  8. package/dist/index.d.ts +1462 -0
  9. package/dist/index.js +81862 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/input-CZvh825j.d.ts +24 -0
  12. package/dist/qr-code-styling-3Y6LZH6V.js +1123 -0
  13. package/dist/qr-code-styling-3Y6LZH6V.js.map +1 -0
  14. package/package.json +79 -0
  15. package/src/components/form/checkbox-group-field.tsx +101 -0
  16. package/src/components/form/date-field.tsx +79 -0
  17. package/src/components/form/date-range-field.tsx +106 -0
  18. package/src/components/form/form-context.ts +10 -0
  19. package/src/components/form/form.tsx +54 -0
  20. package/src/components/form/number-field.tsx +69 -0
  21. package/src/components/form/select-field.tsx +76 -0
  22. package/src/components/form/submit-button.tsx +28 -0
  23. package/src/components/form/text-field.tsx +107 -0
  24. package/src/components/layout/dashboard-header.tsx +54 -0
  25. package/src/components/layout/dashboard-panel.tsx +34 -0
  26. package/src/components/theme-provider.tsx +403 -0
  27. package/src/components/ui/accordion.tsx +69 -0
  28. package/src/components/ui/alert-dialog.tsx +169 -0
  29. package/src/components/ui/alert.tsx +80 -0
  30. package/src/components/ui/animated-theme-toggler.tsx +265 -0
  31. package/src/components/ui/app-store-buttons.tsx +182 -0
  32. package/src/components/ui/aspect-ratio.tsx +23 -0
  33. package/src/components/ui/autocomplete.tsx +296 -0
  34. package/src/components/ui/avatar-group.tsx +95 -0
  35. package/src/components/ui/avatar.tsx +285 -0
  36. package/src/components/ui/badge-group.tsx +160 -0
  37. package/src/components/ui/badge.tsx +172 -0
  38. package/src/components/ui/breadcrumb.tsx +112 -0
  39. package/src/components/ui/button.tsx +77 -0
  40. package/src/components/ui/calendar.tsx +137 -0
  41. package/src/components/ui/card.tsx +244 -0
  42. package/src/components/ui/carousel.tsx +258 -0
  43. package/src/components/ui/chart.tsx +379 -0
  44. package/src/components/ui/checkbox-group.tsx +16 -0
  45. package/src/components/ui/checkbox.tsx +82 -0
  46. package/src/components/ui/collapsible.tsx +45 -0
  47. package/src/components/ui/combobox.tsx +411 -0
  48. package/src/components/ui/command.tsx +264 -0
  49. package/src/components/ui/context-menu.tsx +271 -0
  50. package/src/components/ui/credit-card.tsx +214 -0
  51. package/src/components/ui/dialog.tsx +196 -0
  52. package/src/components/ui/drawer.tsx +135 -0
  53. package/src/components/ui/empty.tsx +127 -0
  54. package/src/components/ui/featured-icon.tsx +149 -0
  55. package/src/components/ui/field.tsx +88 -0
  56. package/src/components/ui/fieldset.tsx +29 -0
  57. package/src/components/ui/form.tsx +17 -0
  58. package/src/components/ui/frame.tsx +82 -0
  59. package/src/components/ui/generic-empty.tsx +142 -0
  60. package/src/components/ui/group.tsx +97 -0
  61. package/src/components/ui/horizontal-scroll-fader.tsx +228 -0
  62. package/src/components/ui/input-group.tsx +102 -0
  63. package/src/components/ui/input-otp.tsx +96 -0
  64. package/src/components/ui/input.tsx +66 -0
  65. package/src/components/ui/item.tsx +198 -0
  66. package/src/components/ui/kbd.tsx +30 -0
  67. package/src/components/ui/label.tsx +28 -0
  68. package/src/components/ui/menu.tsx +312 -0
  69. package/src/components/ui/menubar.tsx +93 -0
  70. package/src/components/ui/meter.tsx +67 -0
  71. package/src/components/ui/multi-select.tsx +308 -0
  72. package/src/components/ui/navigation-menu.tsx +143 -0
  73. package/src/components/ui/number-field.tsx +160 -0
  74. package/src/components/ui/pagination-controls.tsx +74 -0
  75. package/src/components/ui/pagination.tsx +149 -0
  76. package/src/components/ui/popover.tsx +119 -0
  77. package/src/components/ui/preview-card.tsx +55 -0
  78. package/src/components/ui/progress.tsx +289 -0
  79. package/src/components/ui/qr-code.tsx +150 -0
  80. package/src/components/ui/radio-group.tsx +103 -0
  81. package/src/components/ui/resizable.tsx +56 -0
  82. package/src/components/ui/scroll-area.tsx +90 -0
  83. package/src/components/ui/scroller.tsx +38 -0
  84. package/src/components/ui/section-header.tsx +118 -0
  85. package/src/components/ui/select.tsx +181 -0
  86. package/src/components/ui/separator.tsx +23 -0
  87. package/src/components/ui/sheet.tsx +224 -0
  88. package/src/components/ui/sidebar.tsx +744 -0
  89. package/src/components/ui/skeleton.tsx +16 -0
  90. package/src/components/ui/slider.tsx +108 -0
  91. package/src/components/ui/smooth-scroll.tsx +143 -0
  92. package/src/components/ui/social-button.tsx +247 -0
  93. package/src/components/ui/spinner-on-demand.tsx +32 -0
  94. package/src/components/ui/spinner.tsx +18 -0
  95. package/src/components/ui/stat.tsx +187 -0
  96. package/src/components/ui/stepper.tsx +167 -0
  97. package/src/components/ui/switch.tsx +56 -0
  98. package/src/components/ui/table.tsx +126 -0
  99. package/src/components/ui/tabs.tsx +90 -0
  100. package/src/components/ui/tag.tsx +229 -0
  101. package/src/components/ui/target-countdown.tsx +46 -0
  102. package/src/components/ui/text-editor.tsx +313 -0
  103. package/src/components/ui/textarea.tsx +51 -0
  104. package/src/components/ui/timeline.tsx +116 -0
  105. package/src/components/ui/toast.tsx +268 -0
  106. package/src/components/ui/toggle-group.tsx +101 -0
  107. package/src/components/ui/toggle.tsx +45 -0
  108. package/src/components/ui/toolbar.tsx +89 -0
  109. package/src/components/ui/tooltip.tsx +102 -0
  110. package/src/components/ui/vertical-scroll-fader.tsx +250 -0
  111. package/src/components/ui/video-player.tsx +275 -0
  112. package/src/components/upload/avatar-upload-base.tsx +131 -0
  113. package/src/components/upload/image-upload-base.tsx +112 -0
  114. package/src/form.ts +17 -0
  115. package/src/index.ts +125 -0
  116. package/src/lib/hooks/use-callback-ref.ts +15 -0
  117. package/src/lib/hooks/use-first-render.ts +11 -0
  118. package/src/lib/hooks/use-hover.ts +53 -0
  119. package/src/lib/hooks/use-is-tab-active.ts +17 -0
  120. package/src/lib/hooks/use-media-query.ts +164 -0
  121. package/src/lib/utils/css.ts +6 -0
  122. package/src/styles.css +300 -0
  123. package/src/types/helpers.ts +24 -0
  124. 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
+ }