signal-layers 0.0.6 → 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/src/TextField.jsx CHANGED
@@ -1,328 +1,150 @@
1
- import { useState } from "react";
2
-
3
1
  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
- * ──────────────────────────────────────────────────────────────────────────── */
2
+ const { layer, data, state, classes, signals } = createSignalUtils(contract);
3
+ const { inputSignal, layerSignal, dataSignal, stateSignal } = signals;
88
4
 
89
5
  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"),
6
+ container = {
7
+ base: layer("base", "container"),
8
+ layout: layer("layout", "container"),
9
+ };
10
+
11
+ label = {
12
+ base: layer("base", "label"),
13
+ color: layer("color", "label"),
14
+ layout: layer("layout", "label"),
15
+ font: layer("font", "label"),
16
+ size: layer("size", "label"),
17
+ };
18
+
19
+ input = {
20
+ base: layer("base", "input"),
21
+ size: layer("size", "input"),
22
+ border: layer("border", "input"),
23
+ color: layer("color", "input"),
24
+ shape: layer("shape", "input"),
25
+ hover: layer("hover", "input"),
26
+ focus: layer("focus", "input"),
27
+ text: layer("text", "input"),
28
+ };
29
+
30
+ hint = {
31
+ base: layer("base", "hint"),
32
+ color: layer("color", "hint"),
33
+ };
34
+
35
+ error = {
36
+ base: layer("base", "error"),
37
+ color: layer("color", "error"),
38
+ };
39
+
40
+ container.base("flex flex-col gap-1 relative");
41
+ container.layout("block");
42
+
43
+ label.base("absolute pointer-events-none transition-all duration-500 bg-transparent");
44
+ label.layout("left-2 top-2 px-1");
45
+ label.color("text-gray-600");
46
+ label.font("font-light");
47
+ label.size("text-md");
48
+
49
+ input.base("outline-none transition-all duration-200");
50
+ input.border("border-0 border-b");
51
+ input.size("px-3 py-2");
52
+ input.color("bg-transparent");
53
+ input.shape("rounded-none");
54
+ input.hover("hover:border-gray-400");
55
+ input.focus("focus:border-gray-900");
56
+ input.text("text-md");
57
+
58
+ hint.base("text-xs");
59
+ hint.color("text-gray-500");
60
+
61
+ error.base("text-xs");
62
+ error.color("text-red-600");
63
+
64
+ inputSignal.outline &&
65
+ (input.border("border-2 border-black/80"),
174
66
  input.color("bg-transparent"),
175
67
  input.shape("rounded-sm"),
176
68
  input.hover("hover:border-black/50"),
177
69
  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"),
70
+ label.layout("left-0 top-2 px-3"));
71
+ inputSignal.fill &&
72
+ (input.border("border-0 border-b"),
182
73
  input.color("bg-gray-400/30"),
183
74
  input.hover("hover:bg-gray-400/50"),
184
75
  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"),
76
+ label.color("text-gray-800"));
77
+ inputSignal.underline &&
78
+ (input.border("border-0 border-b-2"),
189
79
  input.color("bg-transparent"),
190
80
  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
- * ──────────────────────────────────────────────────────────────────────────── */
81
+ input.focus("focus:border-gray-800 focus:border-b-4"));
82
+
83
+ inputSignal.xs && input.size("px-2 py-1 text-xs");
84
+ inputSignal.sm && input.size("px-2.5 py-1.5 text-xs");
85
+ inputSignal.md && input.size("px-3 py-2 text-sm");
86
+ inputSignal.lg && input.size("px-4 py-2.5 text-base");
87
+ inputSignal.xl && input.size("px-5 py-3 text-lg");
88
+
89
+ inputSignal.primary &&
90
+ (input.color("border-blue-500 focus:border-blue-600"),
91
+ input.focus("focus:ring-blue-500"));
92
+ inputSignal.neutral &&
93
+ (input.color("border-gray-300 focus:border-gray-700"),
94
+ input.focus("focus:ring-gray-700"));
95
+ inputSignal.danger &&
96
+ (input.color("border-red-500 focus:border-red-600"),
97
+ input.focus("focus:ring-red-500"),
98
+ label.color("text-red-600"));
99
+
100
+ inputSignal.square && input.shape("rounded-none");
101
+
102
+ inputSignal.inline && container.layout("inline-flex");
103
+ inputSignal.block && container.layout("block");
104
+ inputSignal.full && input.base("w-full");
105
+
106
+ inputSignal.disabled && input.focus("opacity-50 pointer-events-none");
107
+ inputSignal.readonly && input.focus("bg-gray-100 cursor-default");
108
+ inputSignal.invalid && input.color("border-red-500");
109
+
110
+ inputSignal.value && data("value");
111
+ inputSignal.defaultValue && data("defaultValue");
112
+ inputSignal.placeHolder && data("placeholder");
113
+ inputSignal.label && data("label");
114
+ inputSignal.name && data("name");
115
+ inputSignal.onChange && data("onChange");
116
+ inputSignal.onFocus && data("onFocus");
117
+ inputSignal.onBlur && data("onBlur");
118
+ inputSignal.ariaLabel && data("ariaLabel");
119
+ inputSignal.disabled && data("disabled");
120
+ inputSignal.readOnly && data("readOnly");
121
+ inputSignal.hintMsg && data("hintMsg");
122
+ inputSignal.errorMsg && data("errorMsg");
123
+ inputSignal.type && data("type");
124
+ inputSignal.required && data("required");
125
+ inputSignal.pattern && data("pattern");
126
+ data("cachedPattern");
127
+ state("value", 1, dataSignal.defaultValue ?? "");
128
+ state("focused", 2, false);
129
+ state("touched", 3, false);
130
+ state("error", 4, false);
131
+
132
+ inputSignal.type === "number" &&
133
+ (inputSignal.min && data("min"),
134
+ inputSignal.max && data("max"),
135
+ inputSignal.step && data("step"));
136
+
137
+ (stateSignal.focused?.get || stateSignal.value.get) &&
138
+ (label.layout("left-2 -top-4 px-1"), label.size("text-xs"));
139
+
140
+ dataSignal.pattern &&
141
+ (dataSignal.cachedPattern = new RegExp(dataSignal.pattern));
315
142
 
316
143
  return (
317
144
  <div className={classes(layerSignal.container)}>
318
-
319
145
  {dataSignal.label && (
320
- <label
321
- className={classes(layerSignal.label)}
322
- >
323
- {dataSignal.label}
324
- </label>
325
- )}
146
+ <label className={classes(layerSignal.label)}>{dataSignal.label}</label>
147
+ )}
326
148
 
327
149
  <input
328
150
  type={dataSignal.type ?? "text"}
@@ -341,41 +163,46 @@ export function TextField(contract = {}) {
341
163
  const v = e.target.value;
342
164
  stateSignal.value?.set && stateSignal.value.set(v);
343
165
  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
- )
166
+ stateSignal.error?.set &&
167
+ stateSignal.error.set(
168
+ (dataSignal.required && !v) ||
169
+ (dataSignal.pattern &&
170
+ v &&
171
+ !dataSignal.cachedPattern.test(v)) ||
172
+ (dataSignal.type === "number" &&
173
+ dataSignal.min &&
174
+ Number(v) < Number(dataSignal.min)) ||
175
+ (dataSignal.type === "number" &&
176
+ dataSignal.max &&
177
+ Number(v) > Number(dataSignal.max))
178
+ );
179
+ }}
180
+ onFocus={(e) => {
181
+ stateSignal.focused?.set && stateSignal.focused.set(true);
182
+ dataSignal.onFocus?.(e.target.value);
350
183
  }}
351
- onFocus={(e) => {
352
- stateSignal.focused?.set && stateSignal.focused.set(true);
353
- dataSignal.onFocus?.(e.target.value);
354
- }}
355
184
  onBlur={(e) => {
356
- stateSignal.focused?.set && stateSignal.focused.set(false);
357
- stateSignal.touched?.set && stateSignal.touched.set(true);
358
- dataSignal.onBlur?.(e.target.value);
185
+ stateSignal.focused?.set && stateSignal.focused.set(false);
186
+ stateSignal.touched?.set && stateSignal.touched.set(true);
187
+ dataSignal.onBlur?.(e.target.value);
359
188
  }}
360
189
  className={classes(layerSignal.input)}
361
190
  />
362
-
363
- {stateSignal.error?.get && (
191
+
192
+ {stateSignal.error?.get && (
364
193
  <div className={classes(layerSignal.error)}>
365
- {
366
- dataSignal.errorMsg ||
367
- (dataSignal.required && !(stateSignal.value?.get) ? "This field is required" : "Invalid format")
368
- }
194
+ {dataSignal.errorMsg ||
195
+ (dataSignal.required && !stateSignal.value?.get
196
+ ? "This field is required"
197
+ : "Invalid format")}
369
198
  </div>
370
- )}
199
+ )}
371
200
 
372
- {dataSignal.hintMsg && stateSignal.focused?.get && !stateSignal.error?.get && (
373
- <div className={classes(layerSignal.hint)}>
374
- {dataSignal.hintMsg}
375
- </div>
201
+ {dataSignal.hintMsg &&
202
+ stateSignal.focused?.get &&
203
+ !stateSignal.error?.get && (
204
+ <div className={classes(layerSignal.hint)}>{dataSignal.hintMsg}</div>
376
205
  )}
377
-
378
-
379
206
  </div>
380
207
  );
381
208
  }
package/src/index.js ADDED
@@ -0,0 +1,11 @@
1
+ export { Button } from "./Button";
2
+ export { Card } from "./Card";
3
+ export { CheckBox } from "./CheckBox";
4
+ export { Dropdown } from "./Dropdown";
5
+ export { FabMenu } from "./FabMenu";
6
+ export { ProgressBar } from "./ProgressBar";
7
+ export { Slider } from "./Slider";
8
+ export { Spinner } from "./Spinner";
9
+ export { Switch } from "./Switch";
10
+ export { TextField } from "./TextField";
11
+ export { createSignalUtils } from "./utils/signal-utils.js";
@@ -0,0 +1,40 @@
1
+ import { useState } from "react";
2
+
3
+ export const createSignalUtils = (contract) => {
4
+ const [inputSignal, layerSignal, dataSignal, stateSignal] = [
5
+ { ...contract },
6
+ {},
7
+ {},
8
+ {},
9
+ ];
10
+
11
+ const layer =
12
+ (name, scope) =>
13
+ (className) => (
14
+ (layerSignal[scope] ||= {}),
15
+ (layerSignal[scope][name] ||= []),
16
+ (layerSignal[scope][name][0] = className)
17
+ );
18
+
19
+ const data = (name, key = name) =>
20
+ inputSignal[key] && (dataSignal[name] = inputSignal[key]);
21
+
22
+ const state = (name, priority = 0, initial = false) => (
23
+ ((stateSignal._hooks ||= {})[name] ||= (() => {
24
+ const [get, set] = useState(initial);
25
+ return { get, set };
26
+ })()),
27
+ priority &&
28
+ (!stateSignal._priority || priority > stateSignal._priority) &&
29
+ ((stateSignal[name] = stateSignal._hooks[name]),
30
+ (stateSignal._priority = priority))
31
+ );
32
+
33
+ const classes = (layers = {}) =>
34
+ Object.values(layers)
35
+ .map((l) => l[0])
36
+ .filter(Boolean)
37
+ .join(" ");
38
+
39
+ return { layer, data, state, classes, signals: { inputSignal, layerSignal, dataSignal, stateSignal } };
40
+ };