@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,236 @@
1
+ import * as React from "react";
2
+ import { X } from "lucide-react";
3
+ import { cn } from "@/lib/utils";
4
+ import {
5
+ Popover,
6
+ PopoverTrigger,
7
+ PopoverContent,
8
+ } from "@/presets/ui/popover";
9
+
10
+ type NormalizedMultiItem = {
11
+ key: string;
12
+ value: string | number;
13
+ labelNode: React.ReactNode;
14
+ labelText: string; // Used for width calculation
15
+ disabled?: boolean;
16
+ };
17
+
18
+ export interface SelectionSummaryProps {
19
+ selectedItems: NormalizedMultiItem[];
20
+ placeholder?: React.ReactNode;
21
+ onRemoveValue?: (value: NormalizedMultiItem) => void;
22
+ }
23
+
24
+ /**
25
+ * Helper: Measure text width using a canvas.
26
+ * Much faster than rendering hidden DOM elements.
27
+ */
28
+ function getTextWidth(text: string, font: string) {
29
+ if (typeof window === "undefined") return 0;
30
+ const canvas =
31
+ (window as any).__canvas ||
32
+ ((window as any).__canvas = document.createElement("canvas"));
33
+ const context = canvas.getContext("2d");
34
+ context.font = font;
35
+ const metrics = context.measureText(text);
36
+ return metrics.width;
37
+ }
38
+
39
+ export const SelectionSummary: React.FC<SelectionSummaryProps> = ({
40
+ selectedItems,
41
+ placeholder,
42
+ onRemoveValue,
43
+ }) => {
44
+ const containerRef = React.useRef<HTMLSpanElement | null>(null);
45
+ const [visibleCount, setVisibleCount] = React.useState(0);
46
+ const [moreOpen, setMoreOpen] = React.useState(false);
47
+
48
+ // Measure available width and calculate how many items fit
49
+ React.useLayoutEffect(() => {
50
+ const el = containerRef.current;
51
+ if (!el) return;
52
+
53
+ const computeVisibleItems = () => {
54
+ const containerWidth = el.clientWidth;
55
+
56
+ // 1. Get current font styles to ensure accurate measurement
57
+ const computedStyle = window.getComputedStyle(el);
58
+ const font = `${computedStyle.fontWeight} ${computedStyle.fontSize} ${computedStyle.fontFamily}`;
59
+
60
+ // 2. Calculate the "Buffer" (12 characters width)
61
+ // This is the space reserved for the "+ N more" trigger if truncation happens.
62
+ // We use 'M' or '0' as an average widest character approximation, or a standard string.
63
+ const bufferWidth = getTextWidth("000000000000", font);
64
+
65
+ // 3. Width of the separator (e.g., ", ")
66
+ const commaWidth = getTextWidth(", ", font);
67
+
68
+ let usedWidth = 0;
69
+ let count = 0;
70
+ const totalItems = selectedItems.length;
71
+
72
+ for (let i = 0; i < totalItems; i++) {
73
+ const item = selectedItems[i];
74
+ const itemWidth = getTextWidth(item.labelText, font);
75
+
76
+ // Is this the very last item in the entire list?
77
+ const isLastItem = i === totalItems - 1;
78
+
79
+ // If it's the last item, we don't need the buffer space.
80
+ // If it's NOT the last item, we must ensure we have space for this item AND the buffer.
81
+ // (Because if we can't fit the *next* item, we'll need the buffer to show the badge).
82
+ const spaceNeeded = isLastItem
83
+ ? itemWidth
84
+ : itemWidth + commaWidth + bufferWidth;
85
+
86
+ if (usedWidth + spaceNeeded <= containerWidth) {
87
+ usedWidth += itemWidth + commaWidth;
88
+ count++;
89
+ } else {
90
+ // No more space
91
+ break;
92
+ }
93
+ }
94
+
95
+ // Ensure we show at least 1 item if there are items,
96
+ // unless even the first item is wider than the container (then CSS truncation handles it).
97
+ setVisibleCount(Math.max(1, count));
98
+ };
99
+
100
+ computeVisibleItems();
101
+
102
+ const ro = new ResizeObserver(computeVisibleItems);
103
+ ro.observe(el);
104
+ return () => ro.disconnect();
105
+ }, [selectedItems, selectedItems.length]); // Re-run if items change
106
+
107
+ const totalCount = selectedItems.length;
108
+
109
+ if (!totalCount) {
110
+ return (
111
+ <span ref={containerRef} className="truncate text-muted-foreground w-full block">
112
+ {placeholder ?? "Select options…"}
113
+ </span>
114
+ );
115
+ }
116
+
117
+ const visibleItems = selectedItems.slice(0, visibleCount);
118
+ // If visible count covers everything, overflow is 0
119
+ const overflowCount = totalCount - visibleItems.length;
120
+
121
+ // Safety check: if our calculation says we can show X, but X < Total,
122
+ // strictly ensure we render the "More" chip.
123
+ // If calculation resulted in showing all items, overflow is 0.
124
+ const showMore = overflowCount > 0;
125
+
126
+ const handleRemove = (value: NormalizedMultiItem) => {
127
+ if (!onRemoveValue) return;
128
+ onRemoveValue(value);
129
+ };
130
+
131
+ return (
132
+ <span
133
+ ref={containerRef}
134
+ className="flex items-center w-full overflow-hidden whitespace-nowrap"
135
+ >
136
+ {/* Render Visible Items */}
137
+ {visibleItems.map((item, index) => (
138
+ <React.Fragment key={item.key}>
139
+ <span className="truncate flex-shrink-0">
140
+ {item.labelNode}
141
+ </span>
142
+ {/* Add comma if it's not the last visible item */}
143
+ {index < visibleItems.length - 1 && (
144
+ <span className="text-muted-foreground mr-1">,</span>
145
+ )}
146
+ </React.Fragment>
147
+ ))}
148
+
149
+ {/* Render Separator before "More" if needed */}
150
+ {showMore && (
151
+ <span className="text-muted-foreground mr-1">,</span>
152
+ )}
153
+
154
+ {/* Render "+N more" Chip */}
155
+ {showMore && (
156
+ <Popover open={moreOpen} onOpenChange={setMoreOpen}>
157
+ <PopoverTrigger asChild>
158
+ <button
159
+ type="button"
160
+ className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground underline underline-offset-2 flex-shrink-0"
161
+ onClick={(e) => e.stopPropagation()}
162
+ >
163
+ +{overflowCount} more
164
+ </button>
165
+ </PopoverTrigger>
166
+ <PopoverContent
167
+ align="start"
168
+ className="w-56 max-h-64 overflow-y-auto p-2 text-sm"
169
+ onClick={(e) => e.stopPropagation()}
170
+ >
171
+ <div className="flex items-center justify-between mb-1">
172
+ <span className="font-medium text-xs text-muted-foreground">
173
+ Selected ({totalCount})
174
+ </span>
175
+ <button
176
+ type="button"
177
+ className="p-1 rounded hover:bg-muted"
178
+ onClick={() => setMoreOpen(false)}
179
+ >
180
+ <X className="h-3 w-3" />
181
+ </button>
182
+ </div>
183
+
184
+ <div className="space-y-1">
185
+ {selectedItems.map((item) => (
186
+ <div
187
+ key={item.key}
188
+ className={cn(
189
+ "flex items-center justify-between gap-2 rounded px-2 py-1",
190
+ "bg-muted/40"
191
+ )}
192
+ >
193
+ <span className="truncate">{item.labelNode}</span>
194
+ {onRemoveValue && (
195
+ <button
196
+ type="button"
197
+ className="flex h-4 w-4 shrink-0 items-center justify-center rounded hover:bg-muted text-muted-foreground hover:text-foreground"
198
+ onClick={(e) => {
199
+ e.stopPropagation();
200
+ handleRemove(item);
201
+ }}
202
+ >
203
+ <X className="h-3 w-3" />
204
+ </button>
205
+ )}
206
+ </div>
207
+ ))}
208
+ </div>
209
+ </PopoverContent>
210
+ </Popover>
211
+ )}
212
+ </span>
213
+ );
214
+ };
215
+
216
+
217
+ // src/variants/select-utils.ts (or wherever you keep small helpers)
218
+
219
+ export type SelectPrimitive = string | number;
220
+
221
+ /**
222
+ * Remove a single value from a selection array.
223
+ *
224
+ * - Works even if the selection is undefined/null.
225
+ * - Compares using String() so "1" and 1 are treated consistently.
226
+ */
227
+ export function removeSelectValue<T extends SelectPrimitive>(
228
+ current: readonly T[] | undefined | null,
229
+ valueToRemove: T
230
+ ): T[] {
231
+ if (!current || current.length === 0) return [];
232
+
233
+ const target = String(valueToRemove);
234
+
235
+ return current.filter((v) => String(v) !== target);
236
+ }
@@ -0,0 +1,75 @@
1
+ // src/variants/index.ts
2
+
3
+ import {
4
+ registerVariant as _register,
5
+ getVariant as _get,
6
+ listVariants as _list,
7
+ } from "@/variants/registry";
8
+ import type {
9
+ VariantKey,
10
+ VariantModule,
11
+ VariantValueFor,
12
+ VariantPropsFor,
13
+ } from "@/schema/variant";
14
+ import { textVariant } from "@/variants/core/text";
15
+ import { numberVariant } from "./core/number";
16
+ import { PhoneVariantModule } from "./core/phone";
17
+ import { ColorVariantModule } from "./core/color";
18
+ import passwordVariant from "./core/password";
19
+ import { dateVariant } from "./core/date";
20
+ import { chipVariant } from "./core/chips";
21
+ import { textareaVariant } from "./core/textarea";
22
+ import ToggleVariantModule from "./core/toggle";
23
+ import { radioVariantModule } from "./core/radio";
24
+ import { checkboxModule } from "./core/checkbox";
25
+ import { selectModule } from "./core/select";
26
+ import multiSelectVariantModule from "./core/multiselect";
27
+ import sliderModule from './core/slider'
28
+ import { keyValueModule } from "./core/keyvalue";
29
+ import customVariant from "./core/custom";
30
+ import treeselectModule from "./core/treeselect";
31
+ import { fileManagerModule } from "./core/file";
32
+
33
+ export type { VariantKey, VariantModule, VariantValueFor, VariantPropsFor };
34
+ export {
35
+ _register as registerVariant,
36
+ _get as getVariant,
37
+ _list as listVariants,
38
+ };
39
+ export { textVariant };
40
+
41
+
42
+ const variants = [
43
+ textVariant,
44
+ numberVariant,
45
+ PhoneVariantModule,
46
+ ColorVariantModule,
47
+ passwordVariant,
48
+ dateVariant,
49
+ chipVariant,
50
+ textareaVariant,
51
+ ToggleVariantModule,
52
+ radioVariantModule,
53
+ checkboxModule,
54
+ selectModule,
55
+ multiSelectVariantModule,
56
+ sliderModule,
57
+ keyValueModule,
58
+ customVariant,
59
+ treeselectModule,
60
+ fileManagerModule
61
+ ]
62
+
63
+ /**
64
+ * Register all core/built-in variants.
65
+ *
66
+ * Hosts can call this once at bootstrap:
67
+ *
68
+ * import { registerCoreVariants } from "@timeax/form-palette/variants";
69
+ * registerCoreVariants();
70
+ */
71
+ export function registerCoreVariants(): void {
72
+ variants.forEach(item => _register(item as any))
73
+ }
74
+
75
+ registerCoreVariants();
@@ -0,0 +1,38 @@
1
+ // src/variants/registry.ts
2
+
3
+ import type { VariantKey, VariantModule } from "@/schema/variant";
4
+
5
+ /**
6
+ * Internal storage for registered variants.
7
+ */
8
+ const registry = new Map<VariantKey, VariantModule<any>>();
9
+
10
+ /**
11
+ * Register (or overwrite) a variant module.
12
+ *
13
+ * Typically called from presets, e.g.:
14
+ *
15
+ * registerVariant(textVariant);
16
+ * registerVariant(numberVariant);
17
+ */
18
+ export function registerVariant<K extends VariantKey>(
19
+ module: VariantModule<K>
20
+ ): void {
21
+ registry.set(module.variant, module as VariantModule<any>);
22
+ }
23
+
24
+ /**
25
+ * Look up a variant module by key.
26
+ */
27
+ export function getVariant<K extends VariantKey>(
28
+ key: K
29
+ ): VariantModule<K> | undefined {
30
+ return registry.get(key) as VariantModule<K> | undefined;
31
+ }
32
+
33
+ /**
34
+ * List all registered variant modules.
35
+ */
36
+ export function listVariants(): VariantModule<VariantKey>[] {
37
+ return Array.from(registry.values()) as VariantModule<VariantKey>[];
38
+ }
File without changes
@@ -0,0 +1,126 @@
1
+ // src/variants/shared.ts
2
+
3
+ import React from "react";
4
+
5
+ /**
6
+ * Size hint for field variants.
7
+ *
8
+ * Presets can interpret these however they like (font size, padding, etc.).
9
+ */
10
+ export type FieldSize = "sm" | "md" | "lg";
11
+
12
+ /**
13
+ * Density hint for field variants.
14
+ *
15
+ * - "compact" → tight vertical spacing
16
+ * - "comfortable" → default spacing
17
+ * - "loose" → extra breathing room
18
+ */
19
+ export type FieldDensity = "compact" | "comfortable" | "loose";
20
+
21
+ /**
22
+ * Logical source of a change event.
23
+ *
24
+ * Variants and utilities can tag changes to help the host reason
25
+ * about where a value came from.
26
+ */
27
+ export type ChangeSource =
28
+ | "variant"
29
+ | "paste"
30
+ | "programmatic"
31
+ | "util"
32
+ | (string & {}); // allow custom tags
33
+
34
+ /**
35
+ * Additional context passed along with value changes.
36
+ */
37
+ export interface ChangeDetail<TMeta = unknown, TRaw = unknown> {
38
+ /**
39
+ * Logical source for this change.
40
+ */
41
+ source: ChangeSource;
42
+
43
+ /**
44
+ * Optional raw input that produced this value.
45
+ *
46
+ * Example: original keyboard input or pasted string.
47
+ */
48
+ raw?: TRaw;
49
+
50
+ nativeEvent?: React.SyntheticEvent;
51
+ /**
52
+ * Variant-specific metadata (e.g. cursor position).
53
+ */
54
+ meta?: TMeta;
55
+ }
56
+
57
+ /**
58
+ * Base props shared by all variant components.
59
+ *
60
+ * Each variant module will extend this with its own props type.
61
+ */
62
+ export interface VariantBaseProps<TValue> {
63
+ /**
64
+ * Current logical value for this field.
65
+ */
66
+ value?: TValue | undefined;
67
+
68
+ /**
69
+ * Called whenever the variant wants to update the value.
70
+ *
71
+ * The detail payload describes where the change came from.
72
+ */
73
+ onValue?(value: TValue | undefined, detail?: ChangeDetail): void;
74
+
75
+ /**
76
+ * State flags.
77
+ */
78
+ disabled?: boolean;
79
+ defaultValue?: TValue;
80
+ readOnly?: boolean;
81
+ required?: boolean;
82
+
83
+ alias?: string;
84
+ main?: boolean;
85
+ /**
86
+ * Current error message for this field, if any.
87
+ */
88
+ error?: string;
89
+
90
+ /**
91
+ * Size & density hints.
92
+ *
93
+ * Variants are free to ignore these, but presets (e.g. Shadcn)
94
+ * will typically honour them.
95
+ */
96
+ size?: FieldSize;
97
+ density?: FieldDensity;
98
+ }
99
+
100
+ export interface Extras {
101
+ trailingIcons?: React.ReactNode[];
102
+ leadingIcons?: React.ReactNode[];
103
+ icon?: React.ReactNode;
104
+ iconGap?: number;
105
+ trailingIconSpacing?: number;
106
+ leadingIconSpacing?: number;
107
+ trailingControl?: React.ReactNode;
108
+ leadingControl?: React.ReactNode;
109
+ /**
110
+ * Optional className applied to the container that wraps the leading control.
111
+ * This does not affect the control node itself, only the wrapper div.
112
+ */
113
+ leadingControlClassName?: string;
114
+ /**
115
+ * Optional className applied to the container that wraps the trailing control.
116
+ * This does not affect the control node itself, only the wrapper div.
117
+ */
118
+ trailingControlClassName?: string;
119
+ px?: number;
120
+ py?: number
121
+ pb?: number;
122
+ pe?: number;
123
+ ps?: number;
124
+ }
125
+
126
+ export type ExtraFieldProps<Props> = Extras & Props;
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "declaration": true,
6
+ "declarationMap": true,
7
+ "sourceMap": true,
8
+ "rootDir": "src",
9
+ "jsx": "react-jsx"
10
+ },
11
+ "include": [
12
+ "src"
13
+ ]
14
+ }