signal-layers 0.0.5

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/src/Switch.jsx ADDED
@@ -0,0 +1,196 @@
1
+ import { useState } from "react";
2
+
3
+ export function Switch(contract = {}) {
4
+
5
+ const [inputSignal, layerSignal, dataSignal, stateSignal] = [{ ...contract }, {}, {}, {}];
6
+ const switchId = `switch-${Math.random().toString(36).substring(2, 9)}`;
7
+
8
+ const layer = (name, scope = "checkbox") => (className) =>
9
+ (layerSignal[scope] ||= {},
10
+ layerSignal[scope][name] ||= [],
11
+ (layerSignal[scope][name][0] = className));
12
+
13
+ const data = (name, key = name) =>
14
+ inputSignal[key] !== undefined && (dataSignal[name] = inputSignal[key]);
15
+
16
+ const state = (name, priority = 0, initial = false) => (
17
+ (stateSignal._hooks ||= {})[name] ||= (() => {
18
+ const [get, set] = useState(initial);
19
+ return { get, set };
20
+ })(),
21
+ priority &&
22
+ (!stateSignal._priority || priority > stateSignal._priority) &&
23
+ (stateSignal[name] = stateSignal._hooks[name],
24
+ stateSignal._priority = priority)
25
+ );
26
+
27
+ const classes = (layers = {}) =>
28
+ Object.values(layers).map(l => l[0]).filter(Boolean).join(" ");
29
+
30
+ let container;
31
+ let button;
32
+ let thumb;
33
+ let label;
34
+
35
+ container = {
36
+ base: layer("base", "container"),
37
+ size: layer("size", "container"),
38
+ color: layer("color", "container"),
39
+ layout: layer("layout", "container")
40
+ }
41
+
42
+ button = {
43
+ base: layer("base", "button"),
44
+ size: layer("size", "button"),
45
+ color: layer("color", "button"),
46
+ shape: layer("shape", "button"),
47
+ layout: layer("layout", "button"),
48
+ checked: layer("checked", "button"),
49
+ interactive: layer("interactive", "button")
50
+ }
51
+
52
+ thumb = {
53
+ base: layer("base", "thumb"),
54
+ color: layer("color", "thumb"),
55
+ shape: layer("shape", "thumb"),
56
+ size: layer("size", "thumb"),
57
+ layout: layer("layout", "thumb"),
58
+ position: layer("position", "thumb"),
59
+ interactive: layer("interactive", "thumb")
60
+ }
61
+
62
+ label = {
63
+ base: layer("base", "label"),
64
+ color: layer("color", "label"),
65
+ layout: layer("layout", "label"),
66
+ size: layer("size", "label")
67
+ }
68
+
69
+ container.base("relative");
70
+ container.size("h-4 w-8");
71
+ container.color("bg-transparent");
72
+ container.layout("inline-flex items-center gap-2");
73
+
74
+ button.base("relative transition-colors duration-500 cursor-pointer");
75
+ button.size("h-4 w-8");
76
+ button.color("bg-gray-200");
77
+ button.shape("rounded-full");
78
+ button.layout("inline-flex items-center");
79
+ button.checked("checked:bg-blue-600");
80
+
81
+ thumb.base("absolute transition-transform duration-500");
82
+ thumb.color("bg-white");
83
+ thumb.shape("rounded-full");
84
+ thumb.size("h-4 w-4");
85
+ thumb.layout("inline-block");
86
+ thumb.position("translate-x-0");
87
+
88
+ label.base("absolute cursor-pointer whitespace-nowrap");
89
+ label.color("text-gray-700");
90
+ label.size("text-sm font-light");
91
+ label.layout("left-12");
92
+
93
+ inputSignal.xs && (
94
+ container.size("h-3 w-6"),
95
+ button.size("h-3 w-6"),
96
+ thumb.size("h-3 w-3")
97
+ )
98
+ inputSignal.sm && (
99
+ container.size("h-4 w-8"),
100
+ button.size("h-4 w-8"),
101
+ thumb.size("h-4 w-4")
102
+ )
103
+ inputSignal.md && (
104
+ container.size("h-5 w-10"),
105
+ button.size("h-5 w-10"),
106
+ thumb.size("h-5 w-5")
107
+ )
108
+ inputSignal.lg && (
109
+ container.size("h-6 w-12"),
110
+ button.size("h-6 w-12"),
111
+ thumb.size("h-6 w-6")
112
+ )
113
+
114
+ inputSignal.square && (button.shape("rounded-md"), thumb.shape("rounded-md"));
115
+ inputSignal.pill && (button.shape("rounded-full"), thumb.shape("rounded-full"));
116
+
117
+ inputSignal.labelLeft && label.layout("right-12 left-auto");
118
+ inputSignal.labelRight && label.layout("left-12");
119
+ inputSignal.labelTop && label.layout("bottom-8 left-0 right-0");
120
+ inputSignal.labelBottom && label.layout("top-8 left-0 right-0");
121
+ inputSignal.labelHidden && label.base("absolute opacity-0 pointer-events-none");
122
+
123
+ inputSignal.disabled && (button.interactive("cursor-not-allowed opacity-40"), thumb.interactive("opacity-40"));
124
+
125
+ inputSignal.id && data("id");
126
+ inputSignal.label && data("label");
127
+ inputSignal.name && data("name");
128
+ inputSignal.value && data("value");
129
+ inputSignal.checked && data("checked");
130
+ inputSignal.defaultChecked && data("defaultChecked");
131
+ inputSignal.disabled && data("disabled");
132
+ inputSignal.required && data("required");
133
+ inputSignal.readOnly && data("readOnly");
134
+ inputSignal.ariaLabel && data("aria-label");
135
+ inputSignal.ariaLabelledBy && data("aria-labelledby");
136
+ inputSignal.ariaDescribedBy && data("aria-describedby");
137
+ inputSignal.ariaInvalid && data("aria-invalid");
138
+ inputSignal.ariaChecked && data("aria-checked");
139
+
140
+
141
+ state("checked", 1, false);
142
+ stateSignal.checked?.get && (
143
+ thumb.position("translate-x-4"),
144
+ inputSignal.xs && thumb.position("translate-x-3"),
145
+ inputSignal.sm && thumb.position("translate-x-4"),
146
+ inputSignal.md && thumb.position("translate-x-5"),
147
+ inputSignal.lg && thumb.position("translate-x-6"),
148
+ button.color("bg-blue-600")
149
+ );
150
+
151
+ return (
152
+ <div className={classes(layerSignal.container)}>
153
+
154
+ <button
155
+ type="button"
156
+ role="switch"
157
+ label={dataSignal.label}
158
+ id={dataSignal.id || switchId}
159
+ name={dataSignal.name}
160
+ value={dataSignal.value}
161
+ checked={dataSignal.checked ?? stateSignal.checked?.get}
162
+ defaultChecked={dataSignal.defaultChecked}
163
+ disabled={dataSignal.disabled}
164
+ required={dataSignal.required}
165
+ readOnly={dataSignal.readOnly}
166
+ aria-label={dataSignal.ariaLabel}
167
+ aria-labelledby={dataSignal.ariaLabelledBy}
168
+ aria-describedby={dataSignal.ariaDescribedBy}
169
+ aria-invalid={dataSignal.ariaInvalid}
170
+ aria-checked={dataSignal.ariaChecked ?? (dataSignal.checked ?? stateSignal.checked?.get)}
171
+ onClick={() => stateSignal.checked?.set(!stateSignal.checked?.get)}
172
+ className={classes(layerSignal.button)}
173
+ >
174
+
175
+ <span
176
+ className={classes(layerSignal.thumb)}
177
+ />
178
+
179
+ </button>
180
+
181
+ {dataSignal.label &&
182
+
183
+ <label
184
+ htmlFor={dataSignal.id || switchId}
185
+ className={classes(layerSignal.label)}
186
+ >
187
+
188
+ {dataSignal.label}
189
+
190
+ </label>
191
+ }
192
+
193
+ </div>
194
+ )
195
+
196
+ }
@@ -0,0 +1,381 @@
1
+ import { useState } from "react";
2
+
3
+ export function TextField(contract = {}) {
4
+ /* ────────────────────────────────────────────────────────────────────────────
5
+ * CONTRACT
6
+ * ────────────────────────────────────────────────────────────────────────────
7
+ *
8
+ * TextField - Text input with floating label, validation, and helper messages
9
+ *
10
+ * Foundation: Native HTML input with floating label and validation states
11
+ *
12
+ * Signals:
13
+ * Size: xs, sm, md, lg, xl
14
+ * Color: primary, neutral, danger
15
+ * Shape: square
16
+ * Layout: inline, block, full
17
+ * State: disabled, readonly, invalid
18
+ * Variant: outline, fill, underline
19
+ *
20
+ * Data:
21
+ * value - Controlled value
22
+ * defaultValue - Uncontrolled initial value
23
+ * placeholder - Placeholder text
24
+ * label - Field label
25
+ * name - Input name
26
+ * onChange - Change handler
27
+ * onFocus - Focus handler
28
+ * onBlur - Blur handler
29
+ * ariaLabel - Accessibility label
30
+ * disabled - Disable input
31
+ * readOnly - Make input read-only
32
+ * hintMsg - Helper hint message
33
+ * errorMsg - Custom error message
34
+ * type - Input type (text, number, etc.)
35
+ * required - Required field validation
36
+ * pattern - Regex pattern validation
37
+ * min - Minimum value (for number type)
38
+ * max - Maximum value (for number type)
39
+ * step - Step increment (for number type)
40
+ *
41
+ * Defaults: md, neutral, block
42
+ *
43
+ * Usage:
44
+ * <TextField label="Email" placeholder="you@example.com" />
45
+ * <TextField sm primary outline />
46
+ * <TextField danger invalid errorMsg="Invalid email" />
47
+ * <TextField type="number" min={0} max={100} />
48
+ *
49
+ * ────────────────────────────────────────────────────────────────────────────
50
+ */
51
+
52
+ const [inputSignal, layerSignal, dataSignal, stateSignal] = [
53
+ { ...contract },
54
+ {},
55
+ {},
56
+ {}
57
+ ];
58
+
59
+ /* ────────────────────────────────────────────────────────────────────────────
60
+ * CONTRACT TOOLS
61
+ * ──────────────────────────────────────────────────────────────────────────── */
62
+
63
+ const layer = (name, scope = "field") => (className) =>
64
+ (layerSignal[scope] ||= {},
65
+ layerSignal[scope][name] ||= [],
66
+ (layerSignal[scope][name][0] = className));
67
+
68
+ const data = (name, key = name) =>
69
+ inputSignal[key] !== undefined && (dataSignal[name] = inputSignal[key]);
70
+
71
+ const state = (name, priority = 0, initial = false) => (
72
+ (stateSignal._hooks ||= {})[name] ||= (() => {
73
+ const [get, set] = useState(initial);
74
+ return { get, set };
75
+ })(),
76
+ priority &&
77
+ (!stateSignal._priority || priority > stateSignal._priority) &&
78
+ (stateSignal[name] = stateSignal._hooks[name],
79
+ stateSignal._priority = priority)
80
+ );
81
+
82
+ const classes = (layers = {}) =>
83
+ Object.values(layers).map(l => l[0]).filter(Boolean).join(" ");
84
+
85
+ /* ────────────────────────────────────────────────────────────────────────────
86
+ * BASE LAYERS
87
+ * ──────────────────────────────────────────────────────────────────────────── */
88
+
89
+ let container, label, input, hint, error;
90
+
91
+ (() => (
92
+ container = {
93
+ base: layer("base", "container"),
94
+ layout: layer("layout", "container")
95
+ }
96
+ ))();
97
+
98
+ (() => (
99
+ label = {
100
+ base: layer("base", "label"),
101
+ color: layer("color", "label"),
102
+ layout: layer("layout", "label"),
103
+ font: layer("font", "label"),
104
+ size: layer("size", "label")
105
+ }
106
+ ))();
107
+
108
+ (() => (
109
+ input = {
110
+ base: layer("base", "input"),
111
+ size: layer("size", "input"),
112
+ border: layer("border", "input"),
113
+ color: layer("color", "input"),
114
+ shape: layer("shape", "input"),
115
+ hover: layer("hover", "input"),
116
+ focus: layer("focus", "input"),
117
+ text: layer("text", "input")
118
+ }
119
+ ))();
120
+
121
+ (() => (
122
+ hint = {
123
+ base: layer("base", "hint"),
124
+ color: layer("color", "hint")
125
+ }
126
+ ))();
127
+
128
+ (() => (
129
+ error = {
130
+ base: layer("base", "error"),
131
+ color: layer("color", "error")
132
+ }
133
+ ))();
134
+
135
+ /* ────────────────────────────────────────────────────────────────────────────
136
+ * DEFAULTS
137
+ * ──────────────────────────────────────────────────────────────────────────── */
138
+
139
+ (() => (
140
+ container.base("flex flex-col gap-1 relative"),
141
+ container.layout("block"),
142
+
143
+ label.base("absolute pointer-events-none transition-all duration-500 bg-transparent"),
144
+ label.layout("left-2 top-2 px-1"),
145
+ label.color("text-gray-600"),
146
+ label.font("font-light"),
147
+ label.size("text-md"),
148
+
149
+ input.base(
150
+ "outline-none transition-all duration-200"
151
+ ),
152
+ input.border("border-0 border-b"),
153
+ input.size("px-3 py-2"),
154
+ input.color("bg-transparent"),
155
+ input.shape("rounded-none"),
156
+ input.hover("hover:border-gray-400"),
157
+ input.focus("focus:border-gray-900"),
158
+ input.text("text-md"),
159
+
160
+ hint.base("text-xs"),
161
+ hint.color("text-gray-500"),
162
+
163
+ error.base("text-xs"),
164
+ error.color("text-red-600")
165
+ ))();
166
+
167
+ /* ────────────────────────────────────────────────────────────────────────────
168
+ * VARIANT SIGNALS
169
+ * ──────────────────────────────────────────────────────────────────────────── */
170
+
171
+ (() => (
172
+ inputSignal.outline && (
173
+ input.border("border-2 border-black/80"),
174
+ input.color("bg-transparent"),
175
+ input.shape("rounded-sm"),
176
+ input.hover("hover:border-black/50"),
177
+ input.focus("focus:border-black focus:ring-1 focus:ring-black"),
178
+ label.layout("left-0 top-2 px-3")
179
+ ),
180
+ inputSignal.fill && (
181
+ input.border("border-0 border-b"),
182
+ input.color("bg-gray-400/30"),
183
+ input.hover("hover:bg-gray-400/50"),
184
+ input.focus("focus:bg-gray-400/80 focus:border-black focus:border-b-2"),
185
+ label.color("text-gray-800")
186
+ ),
187
+ inputSignal.underline && (
188
+ input.border("border-0 border-b-2"),
189
+ input.color("bg-transparent"),
190
+ input.hover("hover:border-gray-400"),
191
+ input.focus("focus:border-gray-800 focus:border-b-4")
192
+ )
193
+ ))();
194
+
195
+ /* ────────────────────────────────────────────────────────────────────────────
196
+ * SIZE SIGNALS
197
+ * ──────────────────────────────────────────────────────────────────────────── */
198
+
199
+ (() => (
200
+ inputSignal.xs && input.size("px-2 py-1 text-xs"),
201
+ inputSignal.sm && input.size("px-2.5 py-1.5 text-xs"),
202
+ inputSignal.md && input.size("px-3 py-2 text-sm"),
203
+ inputSignal.lg && input.size("px-4 py-2.5 text-base"),
204
+ inputSignal.xl && input.size("px-5 py-3 text-lg")
205
+ ))();
206
+
207
+ /* ────────────────────────────────────────────────────────────────────────────
208
+ * COLOR SIGNALS
209
+ * ──────────────────────────────────────────────────────────────────────────── */
210
+
211
+ (() => (
212
+ inputSignal.primary && (
213
+ input.color("border-blue-500 focus:border-blue-600"),
214
+ input.focus("focus:ring-blue-500")
215
+ ),
216
+ inputSignal.neutral && (
217
+ input.color("border-gray-300 focus:border-gray-700"),
218
+ input.focus("focus:ring-gray-700")
219
+ ),
220
+ inputSignal.danger && (
221
+ input.color("border-red-500 focus:border-red-600"),
222
+ input.focus("focus:ring-red-500"),
223
+ label.color("text-red-600")
224
+ )
225
+ ))();
226
+
227
+ /* ────────────────────────────────────────────────────────────────────────────
228
+ * SHAPE SIGNALS
229
+ * ──────────────────────────────────────────────────────────────────────────── */
230
+
231
+ (() => (
232
+ inputSignal.square &&
233
+ input.shape("rounded-none")
234
+ ))();
235
+
236
+ /* ────────────────────────────────────────────────────────────────────────────
237
+ * LAYOUT SIGNALS
238
+ * ──────────────────────────────────────────────────────────────────────────── */
239
+
240
+ (() => (
241
+ inputSignal.inline && container.layout("inline-flex"),
242
+ inputSignal.block && container.layout("block"),
243
+ inputSignal.full && input.base("w-full")
244
+ ))();
245
+
246
+ /* ────────────────────────────────────────────────────────────────────────────
247
+ * INTERACTION & STATE SIGNALS
248
+ * ──────────────────────────────────────────────────────────────────────────── */
249
+
250
+ (() => (
251
+ inputSignal.disabled &&
252
+ input.focus("opacity-50 pointer-events-none"),
253
+ inputSignal.readonly &&
254
+ input.focus("bg-gray-100 cursor-default"),
255
+ inputSignal.invalid &&
256
+ input.color("border-red-500")
257
+ ))();
258
+
259
+ /* ────────────────────────────────────────────────────────────────────────────
260
+ * DATA & STATE
261
+ * ──────────────────────────────────────────────────────────────────────────── */
262
+
263
+ (() => (
264
+ inputSignal.value && data("value"),
265
+ inputSignal.defaultValue && data("defaultValue"),
266
+ inputSignal.placeHolder && data("placeholder"),
267
+ inputSignal.label && data("label"),
268
+ inputSignal.name && data("name"),
269
+ inputSignal.onChange && data("onChange"),
270
+ inputSignal.onFocus && data("onFocus"),
271
+ inputSignal.onBlur && data("onBlur"),
272
+ inputSignal.ariaLabel && data("ariaLabel"),
273
+ inputSignal.disabled && data("disabled"),
274
+ inputSignal.readOnly && data("readOnly"),
275
+ inputSignal.hintMsg && data("hintMsg"),
276
+ inputSignal.errorMsg && data("errorMsg"),
277
+ inputSignal.type && data("type"),
278
+ inputSignal.required && data("required"),
279
+ inputSignal.pattern && data("pattern"),
280
+ data("cachedPattern"),
281
+ state("value", 1, dataSignal.defaultValue ?? ""),
282
+ state("focused", 2, false),
283
+ state("touched", 3, false),
284
+ state("error", 4, false)
285
+ ))();
286
+
287
+ (() => (
288
+ inputSignal.type === "number" && (
289
+ inputSignal.min && data("min"),
290
+ inputSignal.max && data("max"),
291
+ inputSignal.step && data("step")
292
+ )
293
+ ))();
294
+
295
+ /* ────────────────────────────────────────────────────────────────────────────
296
+ * DYNAMIC CLASSES
297
+ * ──────────────────────────────────────────────────────────────────────────── */
298
+
299
+ (() => (
300
+ (stateSignal.focused?.get || stateSignal.value.get) && (
301
+ label.layout("left-2 -top-4 px-1"),
302
+ label.size("text-xs")
303
+ )
304
+ ))();
305
+
306
+ (() => (
307
+ dataSignal.pattern && (
308
+ dataSignal.cachedPattern = new RegExp(dataSignal.pattern)
309
+ )
310
+ ))();
311
+
312
+ /* ────────────────────────────────────────────────────────────────────────────
313
+ * RENDER
314
+ * ──────────────────────────────────────────────────────────────────────────── */
315
+
316
+ return (
317
+ <div className={classes(layerSignal.container)}>
318
+
319
+ {dataSignal.label && (
320
+ <label
321
+ className={classes(layerSignal.label)}
322
+ >
323
+ {dataSignal.label}
324
+ </label>
325
+ )}
326
+
327
+ <input
328
+ type={dataSignal.type ?? "text"}
329
+ name={dataSignal.name}
330
+ aria-label={dataSignal.ariaLabel}
331
+ placeholder=" "
332
+ pattern={dataSignal.pattern}
333
+ disabled={dataSignal.disabled}
334
+ readOnly={dataSignal.readOnly}
335
+ value={stateSignal.value?.get}
336
+ required={dataSignal.required}
337
+ min={dataSignal.min}
338
+ max={dataSignal.max}
339
+ step={dataSignal.step}
340
+ onChange={(e) => {
341
+ const v = e.target.value;
342
+ stateSignal.value?.set && stateSignal.value.set(v);
343
+ dataSignal.onChange?.(v);
344
+ stateSignal.error?.set && stateSignal.error.set(
345
+ (dataSignal.required && !(v)) ||
346
+ (dataSignal.pattern && v && !dataSignal.cachedPattern.test(v)) ||
347
+ (dataSignal.type === "number" && dataSignal.min && Number(v) < Number(dataSignal.min)) ||
348
+ (dataSignal.type === "number" && dataSignal.max && Number(v) > Number(dataSignal.max))
349
+ )
350
+ }}
351
+ onFocus={(e) => {
352
+ stateSignal.focused?.set && stateSignal.focused.set(true);
353
+ dataSignal.onFocus?.(e.target.value);
354
+ }}
355
+ onBlur={(e) => {
356
+ stateSignal.focused?.set && stateSignal.focused.set(false);
357
+ stateSignal.touched?.set && stateSignal.touched.set(true);
358
+ dataSignal.onBlur?.(e.target.value);
359
+ }}
360
+ className={classes(layerSignal.input)}
361
+ />
362
+
363
+ {stateSignal.error?.get && (
364
+ <div className={classes(layerSignal.error)}>
365
+ {
366
+ dataSignal.errorMsg ||
367
+ (dataSignal.required && !(stateSignal.value?.get) ? "This field is required" : "Invalid format")
368
+ }
369
+ </div>
370
+ )}
371
+
372
+ {dataSignal.hintMsg && stateSignal.focused?.get && !stateSignal.error?.get && (
373
+ <div className={classes(layerSignal.hint)}>
374
+ {dataSignal.hintMsg}
375
+ </div>
376
+ )}
377
+
378
+
379
+ </div>
380
+ );
381
+ }