ngx-dynamic-entity 1.10.0 → 1.12.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/styles.css CHANGED
@@ -6,69 +6,119 @@
6
6
  * default for a library, but it meant a consumer's first render was unstyled browser HTML
7
7
  * with no starting point.
8
8
  *
9
- * This file is that starting point. Import it and the forms are legible; override the custom
10
- * properties below and they match your product. It is deliberately plain — no shadows, no
11
- * brand colour, no opinion about type — because it exists to be built on, not shipped as-is.
9
+ * This file is that starting point, and it is meant to be good enough to ship. Import it and
10
+ * the forms are legible, dense and accessible; override the custom properties below and they
11
+ * match your product.
12
12
  *
13
13
  * // angular.json → styles, or a global stylesheet
14
14
  * @import 'ngx-dynamic-entity/styles.css';
15
15
  *
16
- * Everything is driven by the custom properties in the :root block, which are scoped under
17
- * .ngx-form / .ngx-record-editor so importing this cannot affect the rest of your app.
18
- * Colours use `color-mix` sparingly and always with a plain fallback declared first.
16
+ * Everything is driven by the custom properties in the token block, which is scoped under
17
+ * .ngx-form / .ngx-record-editor so importing this cannot affect the rest of your app. One
18
+ * consequence of that scoping is worth knowing: a token you want to change must be
19
+ * redeclared on the same selectors (or on a wrapper of your own), not on `:root`.
20
+ *
21
+ * Dark mode follows `prefers-color-scheme`. Pin it either way by redeclaring the palette.
19
22
  */
20
23
 
24
+ /* ─── Tokens ──────────────────────────────────────────────────────────────── */
25
+
21
26
  .ngx-form,
22
27
  .ngx-record-editor {
23
- /* Palette */
24
- --ngx-color-text: #16202a;
25
- --ngx-color-muted: #65788a;
26
- --ngx-color-border: #cfd8e0;
28
+ /* Palette — light */
29
+ --ngx-color-text: #17212b;
30
+ --ngx-color-muted: #5b6b7c;
31
+ --ngx-color-border: #d9e0e7;
32
+ --ngx-color-border-strong: #b6c2ce;
27
33
  --ngx-color-surface: #ffffff;
28
- --ngx-color-surface-alt: #f5f8fa;
29
- --ngx-color-accent: #0b5d8a;
30
- --ngx-color-error: #9e2a2b;
34
+ --ngx-color-surface-alt: #f5f7fa;
35
+ --ngx-color-accent: #2563a8;
36
+ --ngx-color-accent-text: #ffffff;
37
+ --ngx-color-accent-soft: #eaf1f9;
38
+ --ngx-color-error: #b3261e;
39
+ --ngx-color-error-soft: #fdeceb;
31
40
  --ngx-color-warning: #8a5a12;
41
+ --ngx-color-warning-soft: #fdf4e3;
32
42
  --ngx-color-success: #1c6b52;
43
+ --ngx-color-info: var(--ngx-color-accent);
44
+ --ngx-color-info-soft: var(--ngx-color-accent-soft);
33
45
  --ngx-color-disabled-bg: #eef1f4;
34
46
 
35
47
  /* Metrics */
36
- --ngx-radius: 4px;
37
- --ngx-gap: 16px;
48
+ --ngx-radius: 8px;
49
+ --ngx-radius-sm: 6px;
50
+ --ngx-radius-pill: 999px;
51
+ --ngx-gap: 18px;
52
+ --ngx-gap-sm: 10px;
38
53
  --ngx-field-gap: 6px;
39
- --ngx-control-height: 38px;
40
- --ngx-control-padding: 8px 10px;
54
+ --ngx-control-height: 40px;
55
+ --ngx-control-padding: 9px 12px;
56
+ --ngx-focus-width: 2px;
41
57
 
42
58
  /* Type */
43
59
  --ngx-font: inherit;
44
60
  --ngx-font-size: 14px;
45
61
  --ngx-label-size: 13px;
46
62
  --ngx-hint-size: 12px;
63
+ --ngx-label-weight: 600;
64
+ --ngx-line: 1.5;
65
+
66
+ /* Elevation — deliberately restrained; a form is not a card deck. */
67
+ --ngx-shadow-sm: 0 1px 2px rgba(15, 25, 35, 0.06);
68
+ --ngx-ring: 0 0 0 3px color-mix(in srgb, var(--ngx-color-accent) 22%, transparent);
47
69
 
48
70
  color: var(--ngx-color-text);
49
71
  font-family: var(--ngx-font);
50
72
  font-size: var(--ngx-font-size);
73
+ line-height: var(--ngx-line);
51
74
  }
52
75
 
53
- /* ─── Layout ──────────────────────────────────────────────────────────────── */
54
-
55
- .ngx-form__panel {
56
- display: grid;
57
- grid-template-columns: repeat(12, minmax(0, 1fr));
58
- gap: var(--ngx-gap);
59
- align-items: start;
76
+ /*
77
+ * Dark palette.
78
+ *
79
+ * Only the colour tokens are redeclared — metrics and type are scheme-independent, and
80
+ * duplicating them here is how the two halves drift apart.
81
+ */
82
+ @media (prefers-color-scheme: dark) {
83
+ .ngx-form,
84
+ .ngx-record-editor {
85
+ --ngx-color-text: #e6edf3;
86
+ --ngx-color-muted: #9bb0c4;
87
+ --ngx-color-border: #2c3a48;
88
+ --ngx-color-border-strong: #3f5063;
89
+ --ngx-color-surface: #131b24;
90
+ --ngx-color-surface-alt: #1a2430;
91
+ --ngx-color-accent: #6aa8e8;
92
+ --ngx-color-accent-text: #08121c;
93
+ --ngx-color-accent-soft: #17293c;
94
+ --ngx-color-error: #f0857d;
95
+ --ngx-color-error-soft: #33191a;
96
+ --ngx-color-warning: #e0b063;
97
+ --ngx-color-warning-soft: #2f2517;
98
+ --ngx-color-success: #6fc9a8;
99
+ --ngx-color-disabled-bg: #1c2631;
100
+ --ngx-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
101
+ }
60
102
  }
61
103
 
62
- /* colSpan is applied inline by the renderer; this is the fallback for fields without one. */
63
- .ngx-form__field-slot {
64
- grid-column: span 12;
65
- min-width: 0; /* lets long values shrink instead of forcing the grid wider */
66
- position: relative;
67
- }
104
+ /* ─── Layout ──────────────────────────────────────────────────────────────── */
105
+
106
+ /*
107
+ * The 12-column grid, the field slot and the lock button are NOT declared here.
108
+ *
109
+ * They are structural rather than decorative: `getFieldSpan` writes `grid-column` inline, so
110
+ * a consumer who never imports this file still needs the grid to exist, and it lives in
111
+ * `DynamicFormComponent`'s own `styles`. Declaring it in both places is how the two disagree
112
+ * — a component stylesheet is injected after a global one and carries an attribute selector,
113
+ * so the copy here would lose every tie and the rules that looked authoritative would be
114
+ * dead. What this file does instead is set the tokens the component's grid reads
115
+ * (`--ngx-gap`), which is the seam that was there all along.
116
+ */
68
117
 
69
118
  .ngx-form__tabpanel:focus-visible {
70
- outline: 2px solid var(--ngx-color-accent);
71
- outline-offset: 2px;
119
+ outline: var(--ngx-focus-width) solid var(--ngx-color-accent);
120
+ outline-offset: 3px;
121
+ border-radius: var(--ngx-radius-sm);
72
122
  }
73
123
 
74
124
  .ngx-form__module-panel {
@@ -77,12 +127,25 @@
77
127
 
78
128
  /* ─── Tabs ────────────────────────────────────────────────────────────────── */
79
129
 
130
+ /*
131
+ * The strip scrolls sideways rather than wrapping.
132
+ *
133
+ * A wrapped strip changes height as tabs move between rows, which shifts everything below it
134
+ * — including the panel the user is reading. Scrolling keeps the strip one row tall at every
135
+ * width, and the hidden scrollbar keeps it from looking like a scroll region on desktop,
136
+ * where it never needs to be one.
137
+ */
80
138
  .ngx-form__tabs {
81
139
  display: flex;
82
- flex-wrap: wrap;
83
- gap: 4px;
140
+ gap: 2px;
84
141
  border-bottom: 1px solid var(--ngx-color-border);
85
142
  margin-bottom: var(--ngx-gap);
143
+ overflow-x: auto;
144
+ scrollbar-width: none;
145
+ }
146
+
147
+ .ngx-form__tabs::-webkit-scrollbar {
148
+ height: 0;
86
149
  }
87
150
 
88
151
  .ngx-form__tab {
@@ -90,14 +153,24 @@
90
153
  background: none;
91
154
  border: 0;
92
155
  border-bottom: 2px solid transparent;
93
- padding: 8px 12px;
156
+ padding: 10px 14px;
94
157
  font: inherit;
158
+ font-weight: var(--ngx-label-weight);
95
159
  color: var(--ngx-color-muted);
96
160
  cursor: pointer;
161
+ white-space: nowrap;
162
+ display: inline-flex;
163
+ align-items: center;
164
+ gap: 8px;
165
+ border-radius: var(--ngx-radius-sm) var(--ngx-radius-sm) 0 0;
166
+ transition:
167
+ color 0.15s ease,
168
+ background 0.15s ease;
97
169
  }
98
170
 
99
171
  .ngx-form__tab:hover {
100
172
  color: var(--ngx-color-text);
173
+ background: var(--ngx-color-surface-alt);
101
174
  }
102
175
 
103
176
  .ngx-form__tab--active {
@@ -105,13 +178,43 @@
105
178
  border-bottom-color: var(--ngx-color-accent);
106
179
  }
107
180
 
181
+ /*
182
+ * The count of invalid fields on a tab.
183
+ *
184
+ * A tabbed form can fail validation entirely out of sight: the user presses Save, the button
185
+ * does nothing, and the field at fault is two tabs away with no indication of which. This is
186
+ * that indication, and the error summary above the form is the other half of it.
187
+ */
188
+ .ngx-form__tab-badge {
189
+ display: inline-flex;
190
+ align-items: center;
191
+ justify-content: center;
192
+ min-width: 18px;
193
+ height: 18px;
194
+ padding: 0 5px;
195
+ border-radius: var(--ngx-radius-pill);
196
+ background: var(--ngx-color-error);
197
+ color: #fff;
198
+ font-size: 11px;
199
+ font-weight: 700;
200
+ line-height: 1;
201
+ }
202
+
108
203
  .ngx-form__tabs--sub {
109
204
  border-bottom: 0;
205
+ margin-top: calc(var(--ngx-gap) * -0.6);
110
206
  }
111
207
 
112
208
  .ngx-form__tab--sub {
113
209
  font-size: var(--ngx-hint-size);
114
- padding: 4px 10px;
210
+ padding: 5px 12px;
211
+ border-bottom: 0;
212
+ border-radius: var(--ngx-radius-pill);
213
+ }
214
+
215
+ .ngx-form__tab--sub.ngx-form__tab--active {
216
+ background: var(--ngx-color-accent-soft);
217
+ color: var(--ngx-color-accent);
115
218
  }
116
219
 
117
220
  /* ─── Fields ──────────────────────────────────────────────────────────────── */
@@ -120,16 +223,20 @@
120
223
  display: flex;
121
224
  flex-direction: column;
122
225
  gap: var(--ngx-field-gap);
226
+ min-width: 0;
123
227
  }
124
228
 
125
229
  .ngx-field__label {
126
230
  font-size: var(--ngx-label-size);
127
- color: var(--ngx-color-muted);
231
+ font-weight: var(--ngx-label-weight);
232
+ color: var(--ngx-color-text);
233
+ line-height: 1.35;
128
234
  }
129
235
 
130
236
  .ngx-field__req {
131
237
  color: var(--ngx-color-error);
132
238
  margin-left: 2px;
239
+ font-weight: 700;
133
240
  }
134
241
 
135
242
  .ngx-field__input,
@@ -141,19 +248,69 @@
141
248
  color: inherit;
142
249
  background: var(--ngx-color-surface);
143
250
  border: 1px solid var(--ngx-color-border);
144
- border-radius: var(--ngx-radius);
251
+ border-radius: var(--ngx-radius-sm);
145
252
  box-sizing: border-box;
253
+ transition:
254
+ border-color 0.15s ease,
255
+ box-shadow 0.15s ease,
256
+ background 0.15s ease;
257
+ }
258
+
259
+ .ngx-field__input:hover:not(:disabled):not([disabled]) {
260
+ border-color: var(--ngx-color-border-strong);
261
+ }
262
+
263
+ /*
264
+ * A native select keeps its platform arrow, which sits at a different inset per browser and
265
+ * reads as a different control from every other field in the form. Drawing our own puts the
266
+ * affordance in one place everywhere; it is a background image, so no field component has to
267
+ * change its markup to get it.
268
+ */
269
+ select.ngx-field__input {
270
+ appearance: none;
271
+ -webkit-appearance: none;
272
+ padding-right: 34px;
273
+ background-image:
274
+ linear-gradient(45deg, transparent 50%, currentColor 50%), linear-gradient(135deg, currentColor 50%, transparent 50%);
275
+ background-position:
276
+ calc(100% - 17px) calc(50% + 1px),
277
+ calc(100% - 12px) calc(50% + 1px);
278
+ background-size:
279
+ 5px 5px,
280
+ 5px 5px;
281
+ background-repeat: no-repeat;
282
+ }
283
+
284
+ /* A multiple-select is a list box, not a dropdown — it has no arrow to draw. */
285
+ select.ngx-field__input[multiple] {
286
+ background-image: none;
287
+ padding-right: 12px;
146
288
  }
147
289
 
148
290
  textarea.ngx-field__input {
149
- min-height: calc(var(--ngx-control-height) * 2);
291
+ min-height: calc(var(--ngx-control-height) * 2.4);
150
292
  resize: vertical;
293
+ line-height: var(--ngx-line);
151
294
  }
152
295
 
153
296
  .ngx-field__input:focus-visible {
154
- outline: 2px solid var(--ngx-color-accent);
155
- outline-offset: 1px;
297
+ outline: none;
298
+ border-color: var(--ngx-color-accent);
299
+ box-shadow: var(--ngx-ring);
300
+ }
301
+
302
+ /*
303
+ * Kept alongside `:focus-visible` for the two controls where the browsers disagree.
304
+ *
305
+ * A `<select>` opened with the mouse is focused but not focus-visible in Chrome, so the field
306
+ * lost its ring at the moment the option list appeared — the one time the user most needs to
307
+ * see which field they are editing.
308
+ */
309
+ select.ngx-field__input:focus,
310
+ textarea.ngx-field__input:focus {
311
+ outline: none;
156
312
  border-color: var(--ngx-color-accent);
313
+ box-shadow: var(--ngx-ring);
157
314
  }
158
315
 
159
316
  .ngx-field__input:disabled,
@@ -161,26 +318,194 @@ textarea.ngx-field__input {
161
318
  background: var(--ngx-color-disabled-bg);
162
319
  color: var(--ngx-color-muted);
163
320
  cursor: not-allowed;
321
+ border-style: dashed;
164
322
  }
165
323
 
166
324
  .ngx-field--invalid .ngx-field__input {
167
325
  border-color: var(--ngx-color-error);
326
+ background: var(--ngx-color-error-soft);
327
+ }
328
+
329
+ .ngx-field--invalid .ngx-field__input:focus-visible,
330
+ .ngx-field--invalid select.ngx-field__input:focus,
331
+ .ngx-field--invalid textarea.ngx-field__input:focus {
332
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--ngx-color-error) 20%, transparent);
168
333
  }
169
334
 
170
335
  .ngx-field__error {
171
336
  font-size: var(--ngx-hint-size);
172
337
  color: var(--ngx-color-error);
338
+ font-weight: 500;
339
+ display: flex;
340
+ align-items: flex-start;
341
+ gap: 5px;
342
+ }
343
+
344
+ /*
345
+ * The error marker is decoration, so it is drawn rather than typed: a literal "⚠" in the
346
+ * markup is announced before every message, and the message already carries the meaning.
347
+ * Small enough to read as a marker rather than a list bullet.
348
+ */
349
+ .ngx-field__error::before {
350
+ content: '';
351
+ flex: none;
352
+ width: 8px;
353
+ height: 8px;
354
+ margin-top: 5px;
355
+ border-radius: 50%;
356
+ background: var(--ngx-color-error);
173
357
  }
174
358
 
175
359
  /* A referenced field whose source definition has changed since it was linked. */
176
360
  .ngx-field__drift {
177
361
  font-size: var(--ngx-hint-size);
178
362
  color: var(--ngx-color-warning);
363
+ background: var(--ngx-color-warning-soft);
364
+ border-radius: var(--ngx-radius-sm);
365
+ padding: 4px 8px;
366
+ display: block;
367
+ }
368
+
369
+ /*
370
+ * Author help text, as an "i" beside the label with the text on hover.
371
+ *
372
+ * A tooltip is a weak place to put anything essential — it is gone the moment the pointer
373
+ * moves, and a pointer is not something every visitor has. Three things keep this one honest:
374
+ * the text is always in the DOM and named by the control's `aria-describedby`, so a screen
375
+ * reader gets it whether or not anything is hovered; it appears on `:focus-within` too, so it
376
+ * is on screen for the whole time someone is actually filling the field in, by keyboard or by
377
+ * touch; and the bubble is itself hoverable, so it can be read rather than chased.
378
+ * That is WCAG 1.4.13's hoverable and persistent, met by construction.
379
+ */
380
+ .ngx-field__label-row {
381
+ display: flex;
382
+ align-items: center;
383
+ gap: 6px;
384
+ min-width: 0;
385
+ }
386
+
387
+ /*
388
+ * The bubble hangs off the field's own left edge, not off the icon.
389
+ *
390
+ * Centring it on the icon is the obvious thing and it is wrong at the edges: a 260px bubble
391
+ * centred on a field in the left column opens past the edge of the card and the first words
392
+ * are cut off — which is how the first version of this shipped in a screenshot. Anchoring to
393
+ * whichever of these boxes contains the icon puts the bubble inside the field's own column by
394
+ * construction, at any width, with no measuring.
395
+ *
396
+ * All three are listed because the icon sits in a different one per field type: beside the
397
+ * label for most, inside it for a checkbox, inside the legend for a radio group or a group.
398
+ */
399
+ .ngx-field__label-row,
400
+ .ngx-field__label,
401
+ .ngx-field__legend {
402
+ position: relative;
403
+ }
404
+
405
+ .ngx-field__hint-wrap {
406
+ display: inline-flex;
407
+ align-items: center;
408
+ /* Never let the icon be the thing that stretches a label row. */
409
+ flex: none;
410
+ }
411
+
412
+ .ngx-field__hint-icon {
413
+ display: inline-flex;
414
+ align-items: center;
415
+ justify-content: center;
416
+ width: 15px;
417
+ height: 15px;
418
+ border-radius: 50%;
419
+ border: 1px solid var(--ngx-color-border-strong);
420
+ color: var(--ngx-color-muted);
421
+ background: var(--ngx-color-surface);
422
+ font-size: 10px;
423
+ font-weight: 700;
424
+ font-style: italic;
425
+ font-family: Georgia, 'Times New Roman', serif;
426
+ line-height: 1;
427
+ cursor: help;
428
+ user-select: none;
429
+ transition:
430
+ color 0.15s ease,
431
+ border-color 0.15s ease;
432
+ }
433
+
434
+ .ngx-field__hint-wrap:hover .ngx-field__hint-icon,
435
+ .ngx-field:focus-within .ngx-field__hint-icon {
436
+ color: var(--ngx-color-accent);
437
+ border-color: var(--ngx-color-accent);
179
438
  }
180
439
 
440
+ /*
441
+ * Author help text, shown on hover and while the field has focus.
442
+ *
443
+ * A tooltip is a weak place to put anything essential — it is gone the moment the pointer
444
+ * moves, and a pointer is not something every visitor has. Three things keep this one honest:
445
+ * the text is always in the DOM and named by the control's `aria-describedby`, so a screen
446
+ * reader has it whether or not anything is hovered; `:focus-within` shows it for the whole
447
+ * time someone is actually filling the field in, which covers keyboard and touch; and the
448
+ * bubble is hoverable once open, so a long hint can be read rather than chased. That is
449
+ * WCAG 1.4.13's hoverable and persistent, met by construction.
450
+ */
181
451
  .ngx-field__hint {
452
+ position: absolute;
453
+ bottom: calc(100% + 8px);
454
+ left: 0;
455
+ z-index: 5;
456
+ width: max-content;
457
+ max-width: min(300px, 100%);
458
+ padding: 7px 10px;
459
+ border-radius: var(--ngx-radius-sm);
460
+ background: var(--ngx-color-text);
461
+ color: var(--ngx-color-surface);
182
462
  font-size: var(--ngx-hint-size);
183
- color: var(--ngx-color-muted);
463
+ font-weight: 400;
464
+ line-height: 1.4;
465
+ text-align: left;
466
+ white-space: normal;
467
+ box-shadow: var(--ngx-shadow-sm);
468
+ /*
469
+ * `visibility`, not `display`: the text has to stay in the accessibility tree for the
470
+ * `aria-describedby` that names it, and a removed node describes nothing. Hidden from the
471
+ * pointer too, or the invisible bubble would swallow clicks meant for the label beneath it.
472
+ */
473
+ opacity: 0;
474
+ visibility: hidden;
475
+ pointer-events: none;
476
+ transition:
477
+ opacity 0.12s ease,
478
+ visibility 0.12s ease;
479
+ }
480
+
481
+ .ngx-field__hint-wrap:hover .ngx-field__hint,
482
+ .ngx-field:focus-within .ngx-field__hint {
483
+ opacity: 1;
484
+ visibility: visible;
485
+ /* Hoverable once shown, so a long hint can be read instead of chased. */
486
+ pointer-events: auto;
487
+ }
488
+
489
+ /*
490
+ * A hint on the first row of a form would open above the top of the card and be clipped, so
491
+ * near the top edge it flips underneath. `:first-child` on the slot is the cheap proxy for
492
+ * "the top row" that needs no measurement.
493
+ */
494
+ .ngx-form__field-slot:first-child .ngx-field__hint {
495
+ bottom: auto;
496
+ top: calc(100% + 8px);
497
+ }
498
+
499
+ /* Nothing is being filled in, so nothing needs explaining while it is done. */
500
+ .ngx-form--readonly .ngx-field__hint-wrap {
501
+ display: none;
502
+ }
503
+
504
+ @media (prefers-reduced-motion: reduce) {
505
+ .ngx-field__hint,
506
+ .ngx-field__hint-icon {
507
+ transition: none;
508
+ }
184
509
  }
185
510
 
186
511
  /* Read-only and masked renderings are text, not controls. */
@@ -189,10 +514,13 @@ textarea.ngx-field__input {
189
514
  display: flex;
190
515
  align-items: center;
191
516
  padding: var(--ngx-control-padding);
192
- background: var(--ngx-color-surface-alt);
193
- border: 1px solid transparent;
194
- border-radius: var(--ngx-radius);
517
+ padding-left: 0;
518
+ background: none;
519
+ border: 0;
520
+ border-bottom: 1px solid var(--ngx-color-border);
521
+ border-radius: 0;
195
522
  word-break: break-word;
523
+ font-variant-numeric: tabular-nums;
196
524
  }
197
525
 
198
526
  .ngx-field__value--masked {
@@ -208,11 +536,12 @@ textarea.ngx-field__input {
208
536
  .ngx-field__file-wrap {
209
537
  display: flex;
210
538
  align-items: center;
211
- gap: 6px;
539
+ gap: 8px;
212
540
  }
213
541
 
214
542
  .ngx-field__currency-symbol {
215
543
  color: var(--ngx-color-muted);
544
+ font-weight: var(--ngx-label-weight);
216
545
  }
217
546
 
218
547
  .ngx-field__eye-btn,
@@ -223,12 +552,29 @@ textarea.ngx-field__input {
223
552
  .ngx-form__lock {
224
553
  appearance: none;
225
554
  font: inherit;
555
+ font-size: var(--ngx-hint-size);
556
+ font-weight: var(--ngx-label-weight);
226
557
  cursor: pointer;
227
558
  background: var(--ngx-color-surface);
228
559
  color: inherit;
229
560
  border: 1px solid var(--ngx-color-border);
230
- border-radius: var(--ngx-radius);
231
- padding: 6px 10px;
561
+ border-radius: var(--ngx-radius-sm);
562
+ padding: 7px 12px;
563
+ transition:
564
+ border-color 0.15s ease,
565
+ background 0.15s ease,
566
+ color 0.15s ease;
567
+ }
568
+
569
+ .ngx-field__eye-btn:hover,
570
+ .ngx-field__upload-btn:hover,
571
+ .ngx-field__remove-btn:hover,
572
+ .ngx-field__array-add-btn:hover,
573
+ .ngx-field__array-remove-btn:hover,
574
+ .ngx-form__lock:hover {
575
+ border-color: var(--ngx-color-accent);
576
+ color: var(--ngx-color-accent);
577
+ background: var(--ngx-color-accent-soft);
232
578
  }
233
579
 
234
580
  .ngx-field__eye-btn:focus-visible,
@@ -239,34 +585,82 @@ textarea.ngx-field__input {
239
585
  .ngx-form__lock:focus-visible,
240
586
  .ngx-form__tab:focus-visible,
241
587
  .ngx-form__submit:focus-visible,
242
- .ngx-form__reset:focus-visible {
243
- outline: 2px solid var(--ngx-color-accent);
588
+ .ngx-form__reset:focus-visible,
589
+ .ngx-form__error-summary-link:focus-visible {
590
+ outline: var(--ngx-focus-width) solid var(--ngx-color-accent);
244
591
  outline-offset: 2px;
245
592
  }
246
593
 
594
+ /*
595
+ * The lock's appearance only. Where it sits is the component's business — it is a flex item
596
+ * beside the field, and positioning it from here fought that placement rather than replacing
597
+ * it, leaving the button both absolutely positioned and pushed down by a margin.
598
+ */
247
599
  .ngx-form__lock {
248
- position: absolute;
249
- top: 0;
250
- right: 0;
251
600
  padding: 2px 6px;
252
601
  line-height: 1;
602
+ border-color: transparent;
603
+ background: none;
253
604
  }
254
605
 
255
606
  /* Radio */
256
607
  .ngx-field__radio-group {
257
608
  display: flex;
258
609
  flex-wrap: wrap;
259
- gap: 12px;
610
+ gap: 8px;
611
+ padding-top: 2px;
260
612
  }
261
613
 
614
+ /*
615
+ * Each option is a target in its own right, not a label beside a dot.
616
+ *
617
+ * The native radio is well under the 24px a touch target wants, and the label around it had
618
+ * no box of its own — so on a phone the only reliable way to pick an option was to hit the
619
+ * dot itself.
620
+ */
262
621
  .ngx-field__radio-option {
263
622
  display: inline-flex;
264
623
  align-items: center;
265
- gap: 6px;
624
+ gap: 8px;
625
+ min-height: 34px;
626
+ padding: 4px 12px 4px 10px;
627
+ border: 1px solid var(--ngx-color-border);
628
+ border-radius: var(--ngx-radius-pill);
629
+ background: var(--ngx-color-surface);
630
+ transition:
631
+ border-color 0.15s ease,
632
+ background 0.15s ease;
633
+ }
634
+
635
+ .ngx-field__radio-option:hover {
636
+ border-color: var(--ngx-color-border-strong);
637
+ background: var(--ngx-color-surface-alt);
638
+ }
639
+
640
+ .ngx-field__radio-option:has(input:checked) {
641
+ border-color: var(--ngx-color-accent);
642
+ background: var(--ngx-color-accent-soft);
643
+ }
644
+
645
+ .ngx-field__radio-option:has(input:focus-visible) {
646
+ box-shadow: var(--ngx-ring);
266
647
  }
267
648
 
268
649
  .ngx-field__radio-label {
269
650
  cursor: pointer;
651
+ font-size: var(--ngx-font-size);
652
+ }
653
+
654
+ .ngx-field__radio-option input[type='radio'] {
655
+ accent-color: var(--ngx-color-accent);
656
+ margin: 0;
657
+ }
658
+
659
+ /* Checkbox */
660
+ .ngx-field input[type='checkbox'] {
661
+ accent-color: var(--ngx-color-accent);
662
+ width: 17px;
663
+ height: 17px;
270
664
  }
271
665
 
272
666
  /* Boolean switch */
@@ -274,16 +668,17 @@ textarea.ngx-field__input {
274
668
  .ngx-field__toggle-label {
275
669
  display: inline-flex;
276
670
  align-items: center;
277
- gap: 8px;
671
+ gap: 10px;
278
672
  cursor: pointer;
673
+ min-height: 34px;
279
674
  }
280
675
 
281
676
  .ngx-field__toggle-track {
282
677
  position: relative;
283
- width: 38px;
284
- height: 20px;
285
- border-radius: 999px;
286
- background: var(--ngx-color-border);
678
+ width: 40px;
679
+ height: 22px;
680
+ border-radius: var(--ngx-radius-pill);
681
+ background: var(--ngx-color-border-strong);
287
682
  transition: background 0.15s ease;
288
683
  flex: none;
289
684
  }
@@ -292,10 +687,11 @@ textarea.ngx-field__input {
292
687
  position: absolute;
293
688
  top: 2px;
294
689
  left: 2px;
295
- width: 16px;
296
- height: 16px;
690
+ width: 18px;
691
+ height: 18px;
297
692
  border-radius: 50%;
298
- background: var(--ngx-color-surface);
693
+ background: #fff;
694
+ box-shadow: var(--ngx-shadow-sm);
299
695
  transition: transform 0.15s ease;
300
696
  }
301
697
 
@@ -308,30 +704,44 @@ textarea.ngx-field__input {
308
704
  }
309
705
 
310
706
  .ngx-field__toggle-input:focus-visible + .ngx-field__toggle-track {
311
- outline: 2px solid var(--ngx-color-accent);
707
+ outline: var(--ngx-focus-width) solid var(--ngx-color-accent);
312
708
  outline-offset: 2px;
313
709
  }
314
710
 
315
711
  @media (prefers-reduced-motion: reduce) {
712
+ .ngx-form__tab,
713
+ .ngx-field__input,
714
+ .ngx-field__radio-option,
316
715
  .ngx-field__toggle-track,
317
716
  .ngx-field__toggle-thumb {
318
717
  transition: none;
319
718
  }
320
719
  }
321
720
 
322
- /* Group and array containers */
721
+ /* ─── Group and array containers ──────────────────────────────────────────── */
722
+
723
+ /*
724
+ * A group or an array is a section of the form, so it reads as one: a tinted block with its
725
+ * name at the top. The previous hairline fieldset put a nested group at the same visual
726
+ * weight as the text input beside it, and a form with three of them had no structure the eye
727
+ * could find.
728
+ */
323
729
  .ngx-field__fieldset {
324
730
  border: 1px solid var(--ngx-color-border);
325
731
  border-radius: var(--ngx-radius);
326
- padding: 12px;
732
+ padding: 14px 16px 16px;
327
733
  margin: 0;
328
734
  min-width: 0;
735
+ background: var(--ngx-color-surface-alt);
329
736
  }
330
737
 
331
738
  .ngx-field__legend {
332
- padding: 0 6px;
739
+ padding: 0 8px;
740
+ margin-left: -4px;
333
741
  font-size: var(--ngx-label-size);
334
- color: var(--ngx-color-muted);
742
+ font-weight: 700;
743
+ color: var(--ngx-color-text);
744
+ letter-spacing: 0.01em;
335
745
  }
336
746
 
337
747
  .ngx-field__group-children,
@@ -344,13 +754,14 @@ textarea.ngx-field__input {
344
754
  .ngx-field__array-list {
345
755
  display: flex;
346
756
  flex-direction: column;
347
- gap: 10px;
757
+ gap: var(--ngx-gap-sm);
348
758
  }
349
759
 
350
760
  .ngx-field__array-item {
351
761
  border: 1px solid var(--ngx-color-border);
352
- border-radius: var(--ngx-radius);
353
- padding: 10px;
762
+ border-radius: var(--ngx-radius-sm);
763
+ padding: 14px;
764
+ background: var(--ngx-color-surface);
354
765
  }
355
766
 
356
767
  .ngx-field__array-header,
@@ -358,20 +769,25 @@ textarea.ngx-field__input {
358
769
  display: flex;
359
770
  align-items: center;
360
771
  justify-content: space-between;
361
- gap: 8px;
362
- margin-bottom: 8px;
772
+ gap: 10px;
773
+ margin-bottom: 10px;
363
774
  }
364
775
 
365
776
  .ngx-field__array-item-title {
366
777
  font-size: var(--ngx-label-size);
778
+ font-weight: var(--ngx-label-weight);
367
779
  color: var(--ngx-color-muted);
368
780
  }
369
781
 
370
782
  .ngx-field__array-empty,
371
- .ngx-field__image-placeholder,
372
- .ngx-record-editor__row-empty {
783
+ .ngx-field__image-placeholder {
373
784
  color: var(--ngx-color-muted);
374
785
  font-size: var(--ngx-hint-size);
786
+ padding: 14px;
787
+ border: 1px dashed var(--ngx-color-border);
788
+ border-radius: var(--ngx-radius-sm);
789
+ text-align: center;
790
+ margin: 0;
375
791
  }
376
792
 
377
793
  /* Image and file */
@@ -379,14 +795,14 @@ textarea.ngx-field__input {
379
795
  .ngx-field__image-actions {
380
796
  display: flex;
381
797
  align-items: center;
382
- gap: 10px;
798
+ gap: 12px;
383
799
  flex-wrap: wrap;
384
800
  }
385
801
 
386
802
  .ngx-field__image-preview {
387
803
  max-width: 140px;
388
804
  max-height: 140px;
389
- border-radius: var(--ngx-radius);
805
+ border-radius: var(--ngx-radius-sm);
390
806
  border: 1px solid var(--ngx-color-border);
391
807
  }
392
808
 
@@ -407,174 +823,277 @@ textarea.ngx-field__input {
407
823
  .ngx-form__error-banner,
408
824
  .ngx-form__warning-banner,
409
825
  .ngx-form__info-banner,
410
- .ngx-form__denied,
411
- .ngx-record-editor__banner {
412
- padding: 10px 12px;
413
- border-radius: var(--ngx-radius);
826
+ .ngx-form__denied {
827
+ padding: 11px 14px;
828
+ border-radius: var(--ngx-radius-sm);
414
829
  border: 1px solid var(--ngx-color-border);
830
+ border-left-width: 3px;
415
831
  background: var(--ngx-color-surface-alt);
416
- margin-bottom: 10px;
832
+ margin-bottom: var(--ngx-gap-sm);
833
+ font-size: var(--ngx-font-size);
417
834
  }
418
835
 
419
- .ngx-form__error-banner,
420
- .ngx-record-editor__banner--error {
836
+ .ngx-form__error-banner {
421
837
  color: var(--ngx-color-error);
422
- border-color: currentColor;
838
+ background: var(--ngx-color-error-soft);
839
+ border-color: color-mix(in srgb, var(--ngx-color-error) 35%, transparent);
840
+ border-left-color: var(--ngx-color-error);
423
841
  }
424
842
 
425
- .ngx-form__warning-banner,
426
- .ngx-record-editor__banner--warning {
843
+ .ngx-form__warning-banner {
427
844
  color: var(--ngx-color-warning);
428
- border-color: currentColor;
845
+ background: var(--ngx-color-warning-soft);
846
+ border-color: color-mix(in srgb, var(--ngx-color-warning) 35%, transparent);
847
+ border-left-color: var(--ngx-color-warning);
429
848
  }
430
849
 
431
- .ngx-form__info-banner,
432
- .ngx-record-editor__banner--info {
433
- color: var(--ngx-color-accent);
434
- border-color: currentColor;
850
+ .ngx-form__info-banner {
851
+ color: var(--ngx-color-info);
852
+ background: var(--ngx-color-info-soft);
853
+ border-color: color-mix(in srgb, var(--ngx-color-info) 35%, transparent);
854
+ border-left-color: var(--ngx-color-info);
435
855
  }
436
856
 
437
- .ngx-form__actions {
857
+ /*
858
+ * The summary shown when Save is pressed and validation blocks it.
859
+ *
860
+ * Marking every control touched is what the form used to do on its own, and on a tabbed form
861
+ * that is invisible: the errors appear on whichever tabs hold them, and the user is left
862
+ * looking at a button that did nothing. This names the count and links to each field.
863
+ */
864
+ .ngx-form__error-summary {
865
+ margin-bottom: var(--ngx-gap);
866
+ padding: 13px 16px;
867
+ border: 1px solid color-mix(in srgb, var(--ngx-color-error) 35%, transparent);
868
+ border-left: 3px solid var(--ngx-color-error);
869
+ border-radius: var(--ngx-radius-sm);
870
+ background: var(--ngx-color-error-soft);
871
+ color: var(--ngx-color-error);
872
+ }
873
+
874
+ .ngx-form__error-summary-title {
875
+ font-weight: 700;
876
+ font-size: var(--ngx-font-size);
877
+ margin: 0 0 8px;
878
+ }
879
+
880
+ /*
881
+ * A list of rows, not a row of pills.
882
+ *
883
+ * Each entry now carries the field's name *and* what is wrong with it, and a pill wide enough
884
+ * for "Minimum 4 characters required." is not a pill. Rows also read in the order the fields
885
+ * appear on the form, which a wrapping strip of chips does not.
886
+ */
887
+ .ngx-form__error-summary-list {
438
888
  display: flex;
439
- gap: 10px;
440
- margin-top: var(--ngx-gap);
889
+ flex-direction: column;
890
+ gap: 2px;
891
+ margin: 0;
892
+ padding: 0;
893
+ list-style: none;
441
894
  }
442
895
 
443
- .ngx-form__submit,
444
- .ngx-form__reset {
896
+ .ngx-form__error-summary-link {
445
897
  appearance: none;
446
898
  font: inherit;
899
+ font-size: var(--ngx-hint-size);
447
900
  cursor: pointer;
448
- min-height: var(--ngx-control-height);
449
- padding: 0 18px;
450
- border-radius: var(--ngx-radius);
451
- border: 1px solid var(--ngx-color-border);
452
- background: var(--ngx-color-surface);
453
- color: inherit;
901
+ display: flex;
902
+ flex-wrap: wrap;
903
+ align-items: baseline;
904
+ gap: 2px 8px;
905
+ width: 100%;
906
+ text-align: left;
907
+ border: 0;
908
+ border-radius: var(--ngx-radius-sm);
909
+ background: none;
910
+ color: var(--ngx-color-error);
911
+ padding: 5px 8px;
454
912
  }
455
913
 
456
- .ngx-form__submit {
457
- background: var(--ngx-color-accent);
458
- border-color: var(--ngx-color-accent);
459
- color: #fff;
914
+ .ngx-form__error-summary-link:hover {
915
+ background: var(--ngx-color-surface);
460
916
  }
461
917
 
462
- .ngx-form__submit:disabled,
463
- .ngx-form__reset:disabled {
464
- opacity: 0.55;
465
- cursor: not-allowed;
918
+ .ngx-form__error-summary-field {
919
+ font-weight: 700;
920
+ text-decoration: underline;
921
+ text-underline-offset: 2px;
466
922
  }
467
923
 
468
- /* ─── Record editor ───────────────────────────────────────────────────────── */
469
-
470
- .ngx-record-editor__header {
471
- display: flex;
472
- align-items: center;
473
- gap: 12px;
474
- margin-bottom: var(--ngx-gap);
924
+ /* The reason is the sentence the field already shows, so it carries no emphasis of its own. */
925
+ .ngx-form__error-summary-reason {
926
+ font-weight: 400;
475
927
  }
476
928
 
477
- .ngx-record-editor__avatar {
478
- width: 40px;
479
- height: 40px;
480
- border-radius: 50%;
929
+ .ngx-form__actions {
481
930
  display: flex;
931
+ gap: var(--ngx-gap-sm);
932
+ justify-content: flex-end;
482
933
  align-items: center;
483
- justify-content: center;
484
- background: var(--ngx-color-surface-alt);
485
- border: 1px solid var(--ngx-color-border);
486
- flex: none;
934
+ margin-top: var(--ngx-gap);
935
+ padding: 14px 0 2px;
936
+ border-top: 1px solid var(--ngx-color-border);
937
+ background: var(--ngx-color-surface);
487
938
  }
488
939
 
489
- .ngx-record-editor__title {
490
- margin: 0;
491
- font-size: 18px;
940
+ /*
941
+ * Opt in to an action bar that stays in view: add `ngx-form--sticky-actions` to the element
942
+ * wrapping the form, or to a container above it.
943
+ *
944
+ * A long form puts Save below the fold, so the user fills the last field and then goes
945
+ * looking for the button — and it moves on every tab switch, because each tab is a different
946
+ * height. That is worth fixing, and it is why this exists.
947
+ *
948
+ * It is opt-in rather than the default because `position: sticky` resolves against the
949
+ * nearest scrolling ancestor, which for a form embedded in the middle of a longer page is the
950
+ * page itself: the bar then detaches from its form and floats over whatever is below it. The
951
+ * builder's live preview is exactly that case. A host knows whether its form owns the scroll;
952
+ * this stylesheet cannot.
953
+ */
954
+ .ngx-form--sticky-actions .ngx-form__actions,
955
+ .ngx-form-sticky-actions .ngx-form__actions {
956
+ position: sticky;
957
+ bottom: 0;
958
+ z-index: 2;
959
+ }
960
+
961
+ /* A viewport this short is all action bar, so it stops being worth sticking. */
962
+ @media (max-height: 560px) {
963
+ .ngx-form--sticky-actions .ngx-form__actions,
964
+ .ngx-form-sticky-actions .ngx-form__actions {
965
+ position: static;
966
+ }
492
967
  }
493
968
 
494
- .ngx-record-editor__subtitle,
495
- .ngx-record-editor__section-hint,
496
- .ngx-record-editor__summary-label {
497
- color: var(--ngx-color-muted);
498
- font-size: var(--ngx-hint-size);
969
+ .ngx-form__submit,
970
+ .ngx-form__reset {
971
+ appearance: none;
972
+ font: inherit;
973
+ font-weight: var(--ngx-label-weight);
974
+ cursor: pointer;
975
+ min-height: var(--ngx-control-height);
976
+ padding: 0 20px;
977
+ border-radius: var(--ngx-radius-sm);
978
+ border: 1px solid var(--ngx-color-border);
979
+ background: var(--ngx-color-surface);
980
+ color: inherit;
981
+ transition:
982
+ background 0.15s ease,
983
+ border-color 0.15s ease,
984
+ color 0.15s ease;
499
985
  }
500
986
 
501
- .ngx-record-editor__summary-panel,
502
- .ngx-record-editor__drawer {
503
- border: 1px solid var(--ngx-color-border);
504
- border-radius: var(--ngx-radius);
505
- padding: 12px;
987
+ .ngx-form__reset:hover:not(:disabled) {
988
+ border-color: var(--ngx-color-border-strong);
506
989
  background: var(--ngx-color-surface-alt);
507
- margin-bottom: var(--ngx-gap);
508
990
  }
509
991
 
510
- .ngx-record-editor__summary-item {
511
- display: flex;
512
- flex-direction: column;
513
- gap: 2px;
514
- padding: 4px 0;
992
+ .ngx-form__submit {
993
+ background: var(--ngx-color-accent);
994
+ border-color: var(--ngx-color-accent);
995
+ color: var(--ngx-color-accent-text);
515
996
  }
516
997
 
517
- .ngx-record-editor__section-bar {
518
- display: flex;
519
- align-items: center;
520
- gap: 10px;
521
- flex-wrap: wrap;
522
- margin-bottom: 10px;
998
+ .ngx-form__submit:hover:not(:disabled) {
999
+ background: color-mix(in srgb, var(--ngx-color-accent) 86%, #000);
1000
+ border-color: color-mix(in srgb, var(--ngx-color-accent) 86%, #000);
523
1001
  }
524
1002
 
525
- .ngx-record-editor__rows {
526
- display: flex;
527
- flex-direction: column;
528
- gap: 6px;
1003
+ .ngx-form__submit:disabled,
1004
+ .ngx-form__reset:disabled {
1005
+ opacity: 0.55;
1006
+ cursor: not-allowed;
529
1007
  }
530
1008
 
531
- .ngx-record-editor__row {
532
- display: flex;
533
- align-items: center;
534
- justify-content: space-between;
535
- gap: 10px;
536
- padding: 8px 10px;
537
- border: 1px solid var(--ngx-color-border);
538
- border-radius: var(--ngx-radius);
1009
+ /*
1010
+ * "Won't go yet", as distinct from "can't be pressed".
1011
+ *
1012
+ * `data-blocked` is what the form sets while validation is what is stopping the save. The
1013
+ * button stays operable — pressing it is how the user finds out which field is at fault — so
1014
+ * it keeps its pointer cursor and its full hover response. A `not-allowed` cursor on a button
1015
+ * that does respond to a click would be a lie, and so would `aria-disabled`; see the note on
1016
+ * the button itself.
1017
+ *
1018
+ * Outlined rather than a faded fill. Fading the accent toward the surface is the obvious way
1019
+ * to say "not yet" and it is the wrong one: the label stays white, and a 45% accent behind
1020
+ * white text measured 2.6:1 — axe caught it on the first run. Dropping the fill hands the
1021
+ * label the same accent the border uses, against the surface, which is the contrast the rest
1022
+ * of the stylesheet already relies on.
1023
+ */
1024
+ .ngx-form__submit[data-blocked] {
1025
+ background: var(--ngx-color-surface);
1026
+ border-color: var(--ngx-color-accent);
1027
+ color: var(--ngx-color-accent);
539
1028
  }
540
1029
 
541
- .ngx-record-editor__row-text {
542
- min-width: 0;
543
- word-break: break-word;
1030
+ .ngx-form__submit[data-blocked]:hover:not(:disabled) {
1031
+ background: var(--ngx-color-accent);
1032
+ border-color: var(--ngx-color-accent);
1033
+ color: var(--ngx-color-accent-text);
544
1034
  }
545
1035
 
546
- .ngx-record-editor__banner-dismiss,
547
- .ngx-record-editor__row-btn,
548
- .ngx-record-editor__header-toggle {
549
- appearance: none;
550
- font: inherit;
551
- cursor: pointer;
552
- background: none;
553
- border: 1px solid var(--ngx-color-border);
554
- border-radius: var(--ngx-radius);
555
- padding: 4px 8px;
556
- color: inherit;
557
- }
1036
+ /* ─── Record editor ───────────────────────────────────────────────────────── */
1037
+
1038
+ /*
1039
+ * The record editor styles itself from these tokens, in its own component stylesheet.
1040
+ *
1041
+ * Nothing about its appearance is restated here, and that is deliberate: a component
1042
+ * stylesheet is injected after a global one and carries an attribute selector, so a copy in
1043
+ * this file would lose every tie and sit here looking authoritative while doing nothing. The
1044
+ * whole `.ngx-record-editor__*` section used to be exactly that.
1045
+ *
1046
+ * What is left is the part the component has no baseline for — focus rings, which depend on
1047
+ * `--ngx-focus-width` and are only worth drawing once a palette exists — plus the shared
1048
+ * button appearance above, which the row and dismiss buttons already inherit through tokens.
1049
+ */
558
1050
 
559
1051
  .ngx-record-editor__banner-dismiss:focus-visible,
560
1052
  .ngx-record-editor__row-btn:focus-visible,
561
- .ngx-record-editor__header-toggle:focus-visible {
562
- outline: 2px solid var(--ngx-color-accent);
1053
+ .ngx-record-editor__header-toggle:focus-visible,
1054
+ .ngx-record-editor__summary-item:focus-visible {
1055
+ outline: var(--ngx-focus-width) solid var(--ngx-color-accent);
563
1056
  outline-offset: 2px;
564
1057
  }
565
1058
 
1059
+ /*
1060
+ * A read-only record is a table of values, not a form.
1061
+ *
1062
+ * With every field at `span 12` the record view spent a full screen on six values — and no
1063
+ * control is being sized here, so the width a text input needs is not the width a value
1064
+ * needs. `auto-fit` packs them, and a long note still takes its own row when the content
1065
+ * demands one.
1066
+ */
1067
+ /*
1068
+ * Nothing is being asked of the reader, so nothing is required of them.
1069
+ *
1070
+ * The matching `.ngx-form--readonly` *grid* rule lives in the component's own styles, next to
1071
+ * the grid it overrides — see the Layout note above.
1072
+ */
1073
+ .ngx-form--readonly .ngx-field__req {
1074
+ display: none;
1075
+ }
1076
+
566
1077
  /* ─── Narrow screens ──────────────────────────────────────────────────────── */
567
1078
 
568
1079
  @media (max-width: 640px) {
569
- /* colSpan is set inline per field; below this width a 12-column grid is unreadable. */
570
- .ngx-form__panel,
1080
+ /*
1081
+ * The form panel's own collapse is in the component, with the rest of its grid. These two
1082
+ * are declared here and only here, so this is where they collapse.
1083
+ */
571
1084
  .ngx-field__group-children,
572
1085
  .ngx-field__array-item-fields {
573
1086
  grid-template-columns: minmax(0, 1fr);
574
1087
  }
575
1088
 
576
- .ngx-form__field-slot {
577
- grid-column: 1 / -1 !important;
1089
+ /* Reversed so the primary action sits on the right, where the thumb is. */
1090
+ .ngx-form__actions {
1091
+ flex-direction: row-reverse;
1092
+ }
1093
+
1094
+ .ngx-form__actions .ngx-form__submit,
1095
+ .ngx-form__actions .ngx-form__reset {
1096
+ flex: 1;
578
1097
  }
579
1098
  }
580
1099
 
@@ -593,8 +1112,12 @@ textarea.ngx-field__input {
593
1112
  max-width: 100%;
594
1113
  }
595
1114
 
596
- .ngx-field__markdown > :first-child { margin-top: 0; }
597
- .ngx-field__markdown > :last-child { margin-bottom: 0; }
1115
+ .ngx-field__markdown > :first-child {
1116
+ margin-top: 0;
1117
+ }
1118
+ .ngx-field__markdown > :last-child {
1119
+ margin-bottom: 0;
1120
+ }
598
1121
 
599
1122
  .ngx-field__markdown-tabs {
600
1123
  display: flex;
@@ -604,23 +1127,28 @@ textarea.ngx-field__input {
604
1127
 
605
1128
  .ngx-field__markdown-tab {
606
1129
  font: inherit;
607
- padding: 2px 10px;
1130
+ font-size: var(--ngx-hint-size);
1131
+ font-weight: var(--ngx-label-weight);
1132
+ padding: 4px 12px;
608
1133
  cursor: pointer;
609
- border: 1px solid var(--ngx-border, #d1d5db);
610
- border-radius: 4px;
611
- background: var(--ngx-surface, #fff);
1134
+ border: 1px solid var(--ngx-color-border);
1135
+ border-radius: var(--ngx-radius-pill);
1136
+ background: var(--ngx-color-surface);
1137
+ color: var(--ngx-color-muted);
612
1138
  }
613
1139
 
614
1140
  .ngx-field__markdown-tab[aria-pressed='true'] {
615
- background: var(--ngx-accent-soft, #eef2ff);
616
- border-color: var(--ngx-accent, #6366f1);
1141
+ background: var(--ngx-color-accent-soft);
1142
+ border-color: var(--ngx-color-accent);
1143
+ color: var(--ngx-color-accent);
617
1144
  }
618
1145
 
619
1146
  /* The preview replaces the textarea, so give it a comparable height — otherwise
620
1147
  switching tabs makes the whole form jump. */
621
1148
  .ngx-field__markdown--preview {
622
1149
  min-height: 8.5rem;
623
- padding: 8px 10px;
624
- border: 1px solid var(--ngx-border, #d1d5db);
625
- border-radius: 4px;
1150
+ padding: 9px 12px;
1151
+ border: 1px solid var(--ngx-color-border);
1152
+ border-radius: var(--ngx-radius-sm);
1153
+ background: var(--ngx-color-surface);
626
1154
  }