ngx-dynamic-entity 1.10.0 → 1.11.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/fesm2022/ngx-dynamic-entity.mjs +1364 -303
- package/fesm2022/ngx-dynamic-entity.mjs.map +1 -1
- package/package.json +2 -2
- package/styles.css +723 -195
- package/types/ngx-dynamic-entity.d.ts +262 -15
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
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
|
17
|
-
* .ngx-form / .ngx-record-editor so importing this cannot affect the rest of your app.
|
|
18
|
-
*
|
|
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: #
|
|
25
|
-
--ngx-color-muted: #
|
|
26
|
-
--ngx-color-border: #
|
|
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: #
|
|
29
|
-
--ngx-color-accent: #
|
|
30
|
-
--ngx-color-
|
|
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:
|
|
37
|
-
--ngx-
|
|
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:
|
|
40
|
-
--ngx-control-padding:
|
|
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
|
-
/*
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
/*
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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:
|
|
71
|
-
outline-offset:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
155
|
-
|
|
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
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
border
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
284
|
-
height:
|
|
285
|
-
border-radius:
|
|
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:
|
|
296
|
-
height:
|
|
690
|
+
width: 18px;
|
|
691
|
+
height: 18px;
|
|
297
692
|
border-radius: 50%;
|
|
298
|
-
background:
|
|
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:
|
|
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:
|
|
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
|
|
739
|
+
padding: 0 8px;
|
|
740
|
+
margin-left: -4px;
|
|
333
741
|
font-size: var(--ngx-label-size);
|
|
334
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
362
|
-
margin-bottom:
|
|
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:
|
|
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
|
-
|
|
412
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
433
|
-
|
|
434
|
-
border-color:
|
|
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
|
-
|
|
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
|
-
|
|
440
|
-
|
|
889
|
+
flex-direction: column;
|
|
890
|
+
gap: 2px;
|
|
891
|
+
margin: 0;
|
|
892
|
+
padding: 0;
|
|
893
|
+
list-style: none;
|
|
441
894
|
}
|
|
442
895
|
|
|
443
|
-
.ngx-
|
|
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
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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-
|
|
457
|
-
background: var(--ngx-color-
|
|
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-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
918
|
+
.ngx-form__error-summary-field {
|
|
919
|
+
font-weight: 700;
|
|
920
|
+
text-decoration: underline;
|
|
921
|
+
text-underline-offset: 2px;
|
|
466
922
|
}
|
|
467
923
|
|
|
468
|
-
/*
|
|
469
|
-
|
|
470
|
-
|
|
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-
|
|
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
|
-
|
|
484
|
-
|
|
485
|
-
border: 1px solid var(--ngx-color-border);
|
|
486
|
-
|
|
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
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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-
|
|
495
|
-
.ngx-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
font-
|
|
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-
|
|
502
|
-
|
|
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-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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-
|
|
518
|
-
|
|
519
|
-
|
|
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-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
1003
|
+
.ngx-form__submit:disabled,
|
|
1004
|
+
.ngx-form__reset:disabled {
|
|
1005
|
+
opacity: 0.55;
|
|
1006
|
+
cursor: not-allowed;
|
|
529
1007
|
}
|
|
530
1008
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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-
|
|
542
|
-
|
|
543
|
-
|
|
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
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
-
|
|
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
|
-
/*
|
|
570
|
-
|
|
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
|
-
.
|
|
577
|
-
|
|
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 {
|
|
597
|
-
|
|
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
|
-
|
|
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
|
|
610
|
-
border-radius:
|
|
611
|
-
background: var(--ngx-surface
|
|
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
|
|
616
|
-
border-color: var(--ngx-accent
|
|
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:
|
|
624
|
-
border: 1px solid var(--ngx-border
|
|
625
|
-
border-radius:
|
|
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
|
}
|