@timeax/form-palette 0.0.1

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 (109) hide show
  1. package/.scaffold-cache.json +537 -0
  2. package/package.json +42 -0
  3. package/src/.scaffold-cache.json +544 -0
  4. package/src/adapters/axios.ts +117 -0
  5. package/src/adapters/index.ts +91 -0
  6. package/src/adapters/inertia.ts +187 -0
  7. package/src/core/adapter-registry.ts +87 -0
  8. package/src/core/bound/bind-host.ts +14 -0
  9. package/src/core/bound/observe-bound-field.ts +172 -0
  10. package/src/core/bound/wait-for-bound-field.ts +57 -0
  11. package/src/core/context.ts +23 -0
  12. package/src/core/core-provider.tsx +818 -0
  13. package/src/core/core-root.tsx +72 -0
  14. package/src/core/core-shell.tsx +44 -0
  15. package/src/core/errors/error-strip.tsx +71 -0
  16. package/src/core/errors/index.ts +2 -0
  17. package/src/core/errors/map-error-bag.ts +51 -0
  18. package/src/core/errors/map-zod.ts +39 -0
  19. package/src/core/hooks/use-button.ts +220 -0
  20. package/src/core/hooks/use-core-context.ts +20 -0
  21. package/src/core/hooks/use-core-utility.ts +0 -0
  22. package/src/core/hooks/use-core.ts +13 -0
  23. package/src/core/hooks/use-field.ts +497 -0
  24. package/src/core/hooks/use-optional-field.ts +28 -0
  25. package/src/core/index.ts +0 -0
  26. package/src/core/registry/binder-registry.ts +82 -0
  27. package/src/core/registry/field-registry.ts +187 -0
  28. package/src/core/test.tsx +17 -0
  29. package/src/global.d.ts +14 -0
  30. package/src/index.ts +68 -0
  31. package/src/input/index.ts +4 -0
  32. package/src/input/input-field.tsx +854 -0
  33. package/src/input/input-layout-graph.ts +230 -0
  34. package/src/input/input-props.ts +190 -0
  35. package/src/lib/get-global-countries.ts +87 -0
  36. package/src/lib/utils.ts +6 -0
  37. package/src/presets/index.ts +0 -0
  38. package/src/presets/shadcn-preset.ts +0 -0
  39. package/src/presets/shadcn-variants/checkbox.tsx +849 -0
  40. package/src/presets/shadcn-variants/chips.tsx +756 -0
  41. package/src/presets/shadcn-variants/color.tsx +284 -0
  42. package/src/presets/shadcn-variants/custom.tsx +227 -0
  43. package/src/presets/shadcn-variants/date.tsx +796 -0
  44. package/src/presets/shadcn-variants/file.tsx +764 -0
  45. package/src/presets/shadcn-variants/keyvalue.tsx +556 -0
  46. package/src/presets/shadcn-variants/multiselect.tsx +1132 -0
  47. package/src/presets/shadcn-variants/number.tsx +176 -0
  48. package/src/presets/shadcn-variants/password.tsx +737 -0
  49. package/src/presets/shadcn-variants/phone.tsx +628 -0
  50. package/src/presets/shadcn-variants/radio.tsx +578 -0
  51. package/src/presets/shadcn-variants/select.tsx +956 -0
  52. package/src/presets/shadcn-variants/slider.tsx +622 -0
  53. package/src/presets/shadcn-variants/text.tsx +343 -0
  54. package/src/presets/shadcn-variants/textarea.tsx +66 -0
  55. package/src/presets/shadcn-variants/toggle.tsx +218 -0
  56. package/src/presets/shadcn-variants/treeselect.tsx +784 -0
  57. package/src/presets/ui/badge.tsx +46 -0
  58. package/src/presets/ui/button.tsx +60 -0
  59. package/src/presets/ui/calendar.tsx +214 -0
  60. package/src/presets/ui/checkbox.tsx +115 -0
  61. package/src/presets/ui/custom.tsx +0 -0
  62. package/src/presets/ui/dialog.tsx +141 -0
  63. package/src/presets/ui/field.tsx +246 -0
  64. package/src/presets/ui/input-mask.tsx +739 -0
  65. package/src/presets/ui/input-otp.tsx +77 -0
  66. package/src/presets/ui/input.tsx +1011 -0
  67. package/src/presets/ui/label.tsx +22 -0
  68. package/src/presets/ui/number.tsx +1370 -0
  69. package/src/presets/ui/popover.tsx +46 -0
  70. package/src/presets/ui/radio-group.tsx +43 -0
  71. package/src/presets/ui/scroll-area.tsx +56 -0
  72. package/src/presets/ui/select.tsx +190 -0
  73. package/src/presets/ui/separator.tsx +28 -0
  74. package/src/presets/ui/slider.tsx +61 -0
  75. package/src/presets/ui/switch.tsx +32 -0
  76. package/src/presets/ui/textarea.tsx +634 -0
  77. package/src/presets/ui/time-dropdowns.tsx +350 -0
  78. package/src/schema/adapter.ts +217 -0
  79. package/src/schema/core.ts +429 -0
  80. package/src/schema/field-map.ts +0 -0
  81. package/src/schema/field.ts +224 -0
  82. package/src/schema/index.ts +0 -0
  83. package/src/schema/input-field.ts +260 -0
  84. package/src/schema/presets.ts +0 -0
  85. package/src/schema/variant.ts +216 -0
  86. package/src/variants/core/checkbox.tsx +54 -0
  87. package/src/variants/core/chips.tsx +22 -0
  88. package/src/variants/core/color.tsx +16 -0
  89. package/src/variants/core/custom.tsx +18 -0
  90. package/src/variants/core/date.tsx +25 -0
  91. package/src/variants/core/file.tsx +9 -0
  92. package/src/variants/core/keyvalue.tsx +12 -0
  93. package/src/variants/core/multiselect.tsx +28 -0
  94. package/src/variants/core/number.tsx +115 -0
  95. package/src/variants/core/password.tsx +35 -0
  96. package/src/variants/core/phone.tsx +16 -0
  97. package/src/variants/core/radio.tsx +38 -0
  98. package/src/variants/core/select.tsx +15 -0
  99. package/src/variants/core/slider.tsx +55 -0
  100. package/src/variants/core/text.tsx +114 -0
  101. package/src/variants/core/textarea.tsx +22 -0
  102. package/src/variants/core/toggle.tsx +50 -0
  103. package/src/variants/core/treeselect.tsx +11 -0
  104. package/src/variants/helpers/selection-summary.tsx +236 -0
  105. package/src/variants/index.ts +75 -0
  106. package/src/variants/registry.ts +38 -0
  107. package/src/variants/select-shared.ts +0 -0
  108. package/src/variants/shared.ts +126 -0
  109. package/tsconfig.json +14 -0
@@ -0,0 +1,230 @@
1
+ // src/input/input-layout-graph.ts
2
+
3
+ import * as React from "react";
4
+
5
+ import type {
6
+ FieldLayoutConfig,
7
+ FieldOrdering,
8
+ FieldRootId,
9
+ FieldSlotId,
10
+ RelativeRootsMap,
11
+ SlotPlacement,
12
+ } from "@/schema/input-field";
13
+
14
+ /**
15
+ * Helper slots are all non-root slots:
16
+ * - sublabel
17
+ * - description
18
+ * - helpText
19
+ * - errorText
20
+ */
21
+ export type HelperSlotId = Exclude<FieldSlotId, FieldRootId>;
22
+
23
+ export interface HelperSlot {
24
+ id: HelperSlotId;
25
+ root: FieldRootId;
26
+ placement: SlotPlacement;
27
+ content: React.ReactNode;
28
+ }
29
+
30
+ /**
31
+ * Accessor for a (root, placement) group.
32
+ *
33
+ * - `slots()` gives you the concrete HelperSlot[] (possibly empty).
34
+ * - `render(fn)` calls `fn(slots)` only if there are slots,
35
+ * otherwise returns null (so React renders nothing).
36
+ */
37
+ export interface SlotAccessor {
38
+ root: FieldRootId;
39
+ placement: SlotPlacement;
40
+
41
+ /**
42
+ * Concrete list of slots for this root + placement.
43
+ * May be an empty array.
44
+ */
45
+ slots(): HelperSlot[];
46
+
47
+ /**
48
+ * Render this group.
49
+ *
50
+ * If no slots are present, returns null so nothing is rendered.
51
+ *
52
+ * Example:
53
+ * graph
54
+ * .getSlotsFor("input", "below")
55
+ * .render((slots) =>
56
+ * slots.map((slot) =>
57
+ * renderHelperSlot("input", slot, classes)
58
+ * )
59
+ * );
60
+ */
61
+ render(
62
+ renderFn: (slots: HelperSlot[]) => React.ReactNode
63
+ ): React.ReactNode;
64
+ }
65
+
66
+ /**
67
+ * Layout graph for helpers.
68
+ */
69
+ export interface LayoutGraph {
70
+ helperSlots: HelperSlot[];
71
+
72
+ /**
73
+ * Get a slot accessor for a given root + placement.
74
+ */
75
+ getSlotsFor(
76
+ root: FieldRootId,
77
+ placement: SlotPlacement
78
+ ): SlotAccessor;
79
+ }
80
+
81
+ /**
82
+ * Default root attachment for helpers when layout.relativeRoots
83
+ * does not specify anything.
84
+ */
85
+ const defaultRelativeRoots: RelativeRootsMap = {
86
+ sublabel: "label",
87
+ description: "input",
88
+ helpText: "input",
89
+ errorText: "input",
90
+ };
91
+
92
+ /**
93
+ * Default relative ordering per root when layout.ordering
94
+ * is not provided.
95
+ *
96
+ * Only governs *priority* when multiple helpers share the same
97
+ * root + placement. It does not decide the placement itself.
98
+ */
99
+ const defaultOrdering: FieldOrdering = {
100
+ label: ["sublabel"],
101
+ input: ["errorText", "description", "helpText"],
102
+ };
103
+
104
+ function defaultPlacementFor(id: HelperSlotId): SlotPlacement {
105
+ if (id === "sublabel") {
106
+ // Typical: small label text to the right of the main label
107
+ return "right";
108
+ }
109
+ // For description/help/error, "below" the root is the usual default
110
+ return "below";
111
+ }
112
+
113
+ interface BuildLayoutGraphArgs {
114
+ layout: FieldLayoutConfig;
115
+ /**
116
+ * Raw contents for each helper slot.
117
+ * Undefined/null means "no slot".
118
+ */
119
+ sublabel?: React.ReactNode;
120
+ description?: React.ReactNode;
121
+ helpText?: React.ReactNode;
122
+ errorText?: React.ReactNode;
123
+ tags?: React.ReactNode;
124
+ }
125
+
126
+ /**
127
+ * Build a layout graph for helper slots given:
128
+ * - the effective layout (after variant defaults + overrides)
129
+ * - the actual content for each slot
130
+ */
131
+ export function buildLayoutGraph(
132
+ args: BuildLayoutGraphArgs
133
+ ): LayoutGraph {
134
+ const { layout, sublabel, description, helpText, errorText, tags } = args;
135
+
136
+ const relativeRoots: RelativeRootsMap = {
137
+ ...defaultRelativeRoots,
138
+ ...(layout.relativeRoots ?? {}),
139
+ };
140
+
141
+ const ordering: FieldOrdering = {
142
+ ...defaultOrdering,
143
+ ...(layout.ordering ?? {}),
144
+ };
145
+
146
+ const helperSlots: HelperSlot[] = [];
147
+
148
+ const pushSlot = (
149
+ id: HelperSlotId,
150
+ content: React.ReactNode | undefined,
151
+ placement: SlotPlacement | undefined
152
+ ) => {
153
+ if (content === undefined || content === null) return;
154
+
155
+ const root: FieldRootId =
156
+ relativeRoots[id] ??
157
+ (id === "sublabel" ? "label" : "input");
158
+
159
+ const effectivePlacement: SlotPlacement =
160
+ placement ?? defaultPlacementFor(id);
161
+
162
+ if (effectivePlacement === "hidden") return;
163
+
164
+ helperSlots.push({
165
+ id,
166
+ root,
167
+ placement: effectivePlacement,
168
+ content,
169
+ });
170
+ };
171
+
172
+ pushSlot("sublabel", sublabel, layout.sublabelPlacement);
173
+ pushSlot("description", description, layout.descriptionPlacement);
174
+ pushSlot("helpText", helpText, layout.helpTextPlacement);
175
+ pushSlot("errorText", errorText, layout.errorTextPlacement);
176
+ pushSlot("tags", tags, layout.tagPlacement)
177
+
178
+ function makeAccessor(
179
+ root: FieldRootId,
180
+ placement: SlotPlacement
181
+ ): SlotAccessor {
182
+ // cache per accessor so multiple .slots()/.render() calls
183
+ // don't keep re-filtering
184
+ let cache: HelperSlot[] | null = null;
185
+
186
+ const compute = (): HelperSlot[] => {
187
+ if (cache) return cache;
188
+
189
+ const base = helperSlots.filter(
190
+ (s) => s.root === root && s.placement === placement
191
+ );
192
+
193
+ const order = ordering[root] ?? [];
194
+ if (!order.length) {
195
+ cache = base;
196
+ return cache;
197
+ }
198
+
199
+ cache = [...base].sort((a, b) => {
200
+ const ai = order.indexOf(a.id);
201
+ const bi = order.indexOf(b.id);
202
+
203
+ const aRank = ai === -1 ? Number.POSITIVE_INFINITY : ai;
204
+ const bRank = bi === -1 ? Number.POSITIVE_INFINITY : bi;
205
+
206
+ return aRank - bRank;
207
+ });
208
+
209
+ return cache;
210
+ };
211
+
212
+ return {
213
+ root,
214
+ placement,
215
+ slots: () => compute(),
216
+ render(renderFn) {
217
+ const slots = compute();
218
+ if (!slots.length) return null; // nothing rendered
219
+ return renderFn(slots);
220
+ },
221
+ };
222
+ }
223
+
224
+ return {
225
+ helperSlots,
226
+ getSlotsFor(root, placement) {
227
+ return makeAccessor(root, placement);
228
+ },
229
+ };
230
+ }
@@ -0,0 +1,190 @@
1
+ // src/input/input-props.ts
2
+ // noinspection DuplicatedCode
3
+
4
+ import * as React from "react";
5
+
6
+ import type { CoreContext, Dict } from "@/schema/core";
7
+ import type { Field } from "@/schema/field";
8
+ import type { FieldSize, FieldDensity, ChangeDetail } from "@/variants/shared";
9
+ import type {
10
+ VariantKey,
11
+ VariantValueFor,
12
+ VariantPropsFor,
13
+ } from "@/schema/variant";
14
+ import type {
15
+ LabelPlacement,
16
+ SublabelPlacement,
17
+ DescriptionPlacement,
18
+ HelpTextPlacement,
19
+ ErrorTextPlacement,
20
+ ValidateResult,
21
+ SlotPlacement,
22
+ } from "@/schema/input-field";
23
+
24
+ /**
25
+ * Core, variant-agnostic props for InputField.
26
+ *
27
+ * @template TValue Logical value type for this field. Will be refined by
28
+ * variant typing (VariantValueFor<K>).
29
+ */
30
+ export interface InputFieldBaseProps<TValue = unknown> {
31
+ // ─────────────────────────────────────────────
32
+ // Identity / wiring into the core runtime
33
+ // ─────────────────────────────────────────────
34
+
35
+ name?: string;
36
+ bind?: string;
37
+ groupId?: string;
38
+ shared?: string;
39
+ ignore?: boolean;
40
+ alias?: string
41
+ main?: boolean;
42
+ tags?: FieldTag[];
43
+ contain?: boolean
44
+
45
+ // ─────────────────────────────────────────────
46
+ // Chrome / description
47
+ // ─────────────────────────────────────────────
48
+
49
+ label?: React.ReactNode;
50
+ sublabel?: React.ReactNode;
51
+ description?: React.ReactNode;
52
+ helpText?: React.ReactNode;
53
+
54
+ /**
55
+ * Optional explicit error text to display.
56
+ *
57
+ * This is *visual* error copy. The actual validation state still
58
+ * lives in field.error / schema / onValidate.
59
+ */
60
+ errorText?: React.ReactNode;
61
+
62
+ /**
63
+ * Placement hints for label / sublabel / description / helpText / errorText.
64
+ *
65
+ * These are purely layout hints; actual behaviour is implemented
66
+ * by the preset / host component.
67
+ */
68
+ labelPlacement?: LabelPlacement;
69
+ sublabelPlacement?: SublabelPlacement;
70
+ descriptionPlacement?: DescriptionPlacement;
71
+ helpTextPlacement?: HelpTextPlacement;
72
+ errorTextPlacement?: ErrorTextPlacement;
73
+ tagPlacement?: SlotPlacement
74
+
75
+ // ─────────────────────────────────────────────
76
+ // State flags
77
+ // ─────────────────────────────────────────────
78
+
79
+ required?: boolean;
80
+ disabled?: boolean;
81
+ readOnly?: boolean;
82
+
83
+ size?: FieldSize;
84
+ density?: FieldDensity;
85
+
86
+ // ─────────────────────────────────────────────
87
+ // Layout hooks
88
+ // ─────────────────────────────────────────────
89
+
90
+ inline?: boolean;
91
+ fullWidth?: boolean;
92
+
93
+ // ─────────────────────────────────────────────
94
+ // Validation hooks
95
+ // ─────────────────────────────────────────────
96
+
97
+ onValidate?(
98
+ value: TValue | undefined,
99
+ field: Field,
100
+ form: CoreContext<Dict>
101
+ ): ValidateResult;
102
+
103
+ /**
104
+ * Per-field change hook at the InputField level.
105
+ *
106
+ * - `value` is what the variant is trying to set.
107
+ * - `detail` comes from the variant (`ChangeDetail`).
108
+ * - If you return `undefined`, the original value is used.
109
+ * - If you return *anything else*, that is what will be stored
110
+ * in the core (and emitted to the form).
111
+ */
112
+ onChange?(e: {
113
+ value: TValue | undefined,
114
+ preventDefault(): void;
115
+ event?: React.SyntheticEvent;
116
+ readonly isDefaultPrevented?: boolean;
117
+ readonly detail: ChangeDetail
118
+ }): void;
119
+ }
120
+
121
+
122
+ export type Events<TRaw, TValue, TMeta> = {
123
+ onValidate?(
124
+ value: TValue | undefined,
125
+ field: Field,
126
+ form: CoreContext<Dict>
127
+ ): ValidateResult;
128
+
129
+ /**
130
+ * Per-field change hook at the InputField level.
131
+ *
132
+ * - `value` is what the variant is trying to set.
133
+ * - `detail` comes from the variant (`ChangeDetail`).
134
+ * - If you return `undefined`, the original value is used.
135
+ * - If you return *anything else*, that is what will be stored
136
+ * in the core (and emitted to the form).
137
+ */
138
+ onChange?(e: {
139
+ value: TValue | undefined,
140
+ preventDefault(): void;
141
+ event?: React.SyntheticEvent;
142
+ readonly isDefaultPrevented?: boolean;
143
+ readonly detail: ChangeDetail<TMeta, TRaw>
144
+ }): void;
145
+ }
146
+
147
+ /**
148
+ * Public props for <InputField />.
149
+ *
150
+ * - `variant` selects the variant module.
151
+ * - All variant-specific props are merged directly into the field props
152
+ * via `VariantPropsFor<K>`.
153
+ *
154
+ * NOTE: this is a type alias (not an interface) so we can safely intersect
155
+ * unions coming from VariantPropsFor<K> / VariantValueFor<K>.
156
+ */
157
+ export type InputFieldProps<K extends VariantKey = VariantKey, H = unknown> =
158
+ InputFieldBaseProps<VariantValueFor<K, H>> &
159
+ VariantPropsFor<K, H> &
160
+ Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> & {
161
+ variant: K;
162
+ classes?: Partial<InputFieldClassNames>;
163
+ };
164
+
165
+ export interface InputFieldClassNames {
166
+ root?: string;
167
+ labelRow?: string;
168
+ inlineRow?: string;
169
+ label?: string;
170
+ sublabel?: string;
171
+ description?: string;
172
+ helpText?: string;
173
+ error?: string;
174
+ group?: string;
175
+ content?: string;
176
+ variant?: string;
177
+ inlineInputColumn?: string
178
+ inlineLabelColumn?: string;
179
+ required?: string;
180
+ tag?: string
181
+ }
182
+
183
+
184
+ export interface FieldTag {
185
+ label: React.ReactNode;
186
+ icon?: React.ReactNode;
187
+ className?: string;
188
+ color?: string; // text color
189
+ bgColor?: string; // background color
190
+ }
@@ -0,0 +1,87 @@
1
+ import { PhoneCountry } from "@/presets/shadcn-variants/phone";
2
+
3
+ // e.g. src/lib/get-global-countries.ts
4
+ let cachedCountries: PhoneCountry[] | null = null;
5
+
6
+ const DEFAULT_COUNTRIES: PhoneCountry[] = [
7
+ {
8
+ code: "NG",
9
+ label: "Nigeria",
10
+ dial: "234",
11
+ mask: "999 999 9999",
12
+ flag: "πŸ‡³πŸ‡¬",
13
+ },
14
+ {
15
+ code: "US",
16
+ label: "United States",
17
+ dial: "1",
18
+ mask: "(999) 999-9999",
19
+ flag: "πŸ‡ΊπŸ‡Έ",
20
+ },
21
+ {
22
+ code: "GB",
23
+ label: "United Kingdom",
24
+ dial: "44",
25
+ mask: "9999 999 999",
26
+ flag: "πŸ‡¬πŸ‡§",
27
+ },
28
+ ];
29
+
30
+ cachedCountries = DEFAULT_COUNTRIES;
31
+ let validatedOnce = false;
32
+
33
+ function isPhoneCountry(value: unknown): value is PhoneCountry {
34
+ if (!value || typeof value !== "object") return false;
35
+
36
+ const v = value as Record<string, unknown>;
37
+
38
+ return (
39
+ typeof v.code === "string" &&
40
+ typeof v.label === "string" &&
41
+ typeof v.dial === "string" &&
42
+ typeof v.mask === "string"
43
+ // flag is optional & can be anything React can render, so we don't
44
+ // validate it beyond existence.
45
+ );
46
+ }
47
+
48
+ export function getGlobalCountryList(): PhoneCountry[] {
49
+ // If we've already validated & cached, just return it.
50
+ if (cachedCountries) return cachedCountries;
51
+
52
+ if (typeof window === "undefined") {
53
+ cachedCountries = [];
54
+ return cachedCountries;
55
+ }
56
+
57
+ const raw = window["form-palette"]?.countries;
58
+
59
+ if (!Array.isArray(raw)) {
60
+ if (!validatedOnce && process.env.NODE_ENV !== "production") {
61
+ console.warn(
62
+ "['form-palette'] window.'form-palette'.countries is not an array:",
63
+ raw,
64
+ );
65
+ }
66
+ validatedOnce = true;
67
+ cachedCountries = [];
68
+ return cachedCountries;
69
+ }
70
+
71
+ const result: PhoneCountry[] = [];
72
+
73
+ for (const item of raw) {
74
+ if (isPhoneCountry(item)) {
75
+ result.push(item);
76
+ } else if (process.env.NODE_ENV !== "production") {
77
+ console.warn(
78
+ "['form-palette'] Ignoring invalid PhoneCountry entry:",
79
+ item,
80
+ );
81
+ }
82
+ }
83
+
84
+ validatedOnce = true;
85
+ cachedCountries = result;
86
+ return result;
87
+ }
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
File without changes
File without changes