@mystravil/hardpoint 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -18
- package/LICENSE +25 -25
- package/README.md +67 -64
- package/dist/base.css +625 -346
- package/dist/components/AppShell.svelte +698 -698
- package/dist/components/Button.svelte +174 -174
- package/dist/components/Checkbox.svelte +235 -198
- package/dist/components/Checkbox.svelte.d.ts +9 -2
- package/dist/components/CheckboxGroup.svelte +207 -99
- package/dist/components/CheckboxGroup.svelte.d.ts +23 -4
- package/dist/components/Combobox.svelte +901 -824
- package/dist/components/Combobox.svelte.d.ts +10 -1
- package/dist/components/Dialog.svelte +359 -356
- package/dist/components/Disclosure.svelte +201 -201
- package/dist/components/EmptyState.svelte +88 -88
- package/dist/components/ErrorState.svelte +135 -135
- package/dist/components/ImageViewer.svelte +764 -0
- package/dist/components/ImageViewer.svelte.d.ts +55 -0
- package/dist/components/KeyValueList.svelte +114 -114
- package/dist/components/ListDetail.svelte +218 -0
- package/dist/components/ListDetail.svelte.d.ts +37 -0
- package/dist/components/LoadRegion.svelte +219 -219
- package/dist/components/MediaFrame.svelte +110 -110
- package/dist/components/MediaImage.svelte +256 -256
- package/dist/components/Menu.svelte +258 -258
- package/dist/components/MenuCheckboxItem.svelte +119 -119
- package/dist/components/MenuGroup.svelte +50 -50
- package/dist/components/MenuItem.svelte +94 -94
- package/dist/components/MenuRadioGroup.svelte +62 -62
- package/dist/components/MenuRadioItem.svelte +110 -110
- package/dist/components/MenuSeparator.svelte +28 -28
- package/dist/components/Meter.svelte +283 -0
- package/dist/components/Meter.svelte.d.ts +66 -0
- package/dist/components/NativeSelect.svelte +18 -2
- package/dist/components/NativeSelect.svelte.d.ts +6 -0
- package/dist/components/Notice.svelte +262 -0
- package/dist/components/Notice.svelte.d.ts +30 -0
- package/dist/components/Popover.svelte +222 -222
- package/dist/components/Progress.svelte +157 -157
- package/dist/components/RadioGroup.svelte +440 -0
- package/dist/components/RadioGroup.svelte.d.ts +50 -0
- package/dist/components/Select.svelte +471 -405
- package/dist/components/Select.svelte.d.ts +6 -0
- package/dist/components/Skeleton.svelte +149 -149
- package/dist/components/Spinner.svelte +118 -117
- package/dist/components/Stat.svelte +253 -253
- package/dist/components/Steps.svelte +189 -189
- package/dist/components/Switch.svelte +222 -222
- package/dist/components/Table.svelte +504 -504
- package/dist/components/Tabs.svelte +246 -246
- package/dist/components/Tag.svelte +40 -40
- package/dist/components/TextArea.svelte +272 -0
- package/dist/components/TextArea.svelte.d.ts +28 -0
- package/dist/components/TextField.svelte +256 -256
- package/dist/components/Timeline.svelte +156 -156
- package/dist/components/Toaster.svelte +411 -411
- package/dist/components/Tooltip.svelte +200 -200
- package/dist/components/TooltipGroup.svelte +46 -46
- package/dist/components/internal/Scrim.svelte +52 -52
- package/dist/components/internal/Sheet.svelte +263 -263
- package/dist/components/internal/groups.d.ts +14 -0
- package/dist/components/internal/groups.js +23 -0
- package/dist/fonts.css +71 -71
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/tokens.css +283 -279
- package/package.json +81 -76
|
@@ -1,256 +1,256 @@
|
|
|
1
|
-
<script lang="ts" module>
|
|
2
|
-
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
3
|
-
|
|
4
|
-
/** The text input types the well suits. For numbers use `text` with `inputmode="numeric"`. */
|
|
5
|
-
export type TextFieldType = 'text' | 'email' | 'password' | 'search' | 'tel' | 'url';
|
|
6
|
-
|
|
7
|
-
/** From which width the label sits beside the well: 600px, 900px or 1200px. */
|
|
8
|
-
export type TextFieldBreakpoint = 'medium' | 'expanded' | 'large';
|
|
9
|
-
|
|
10
|
-
/** Attributes and handlers not listed here go to the `<input>`. */
|
|
11
|
-
export type TextFieldProps = Omit<HTMLInputAttributes, 'type' | 'value' | 'children'> & {
|
|
12
|
-
/** The caps field label above the well. Required: a placeholder is never the only label. */
|
|
13
|
-
label: string;
|
|
14
|
-
/** The text. Bindable. */
|
|
15
|
-
value?: string;
|
|
16
|
-
type?: TextFieldType;
|
|
17
|
-
/** Guidance under the well in `--muted`, linked by `aria-describedby`. */
|
|
18
|
-
hint?: string;
|
|
19
|
-
/**
|
|
20
|
-
* The error to show under the well, with an icon, linked by
|
|
21
|
-
* `aria-describedby`; it sets `aria-invalid`. Say how to fix the
|
|
22
|
-
* problem. The app validates; the field shows what it is given.
|
|
23
|
-
*/
|
|
24
|
-
error?: string;
|
|
25
|
-
/**
|
|
26
|
-
* The label sits above the well by default. `true` puts it beside the
|
|
27
|
-
* well at every width; a breakpoint puts it beside from that width up.
|
|
28
|
-
* The hint and the error stay under the well.
|
|
29
|
-
*/
|
|
30
|
-
labelBeside?: boolean | TextFieldBreakpoint;
|
|
31
|
-
/** The `<input>`, for example to focus the first error on submit. */
|
|
32
|
-
ref?: HTMLInputElement | null;
|
|
33
|
-
};
|
|
34
|
-
</script>
|
|
35
|
-
|
|
36
|
-
<script lang="ts">
|
|
37
|
-
import { untrack } from 'svelte';
|
|
38
|
-
import { Label } from 'bits-ui';
|
|
39
|
-
import { fadeIn } from './internal/fade-in.js';
|
|
40
|
-
|
|
41
|
-
const uid = $props.id();
|
|
42
|
-
|
|
43
|
-
let {
|
|
44
|
-
label,
|
|
45
|
-
value = $bindable(''),
|
|
46
|
-
type = 'text',
|
|
47
|
-
hint,
|
|
48
|
-
error,
|
|
49
|
-
labelBeside = false,
|
|
50
|
-
id = uid,
|
|
51
|
-
disabled,
|
|
52
|
-
ref = $bindable(null),
|
|
53
|
-
class: className,
|
|
54
|
-
'aria-describedby': describedBy,
|
|
55
|
-
'aria-invalid': ariaInvalid,
|
|
56
|
-
...rest
|
|
57
|
-
}: TextFieldProps = $props();
|
|
58
|
-
|
|
59
|
-
const hintId = $derived(`${id}-hint`);
|
|
60
|
-
const errorId = $derived(`${id}-error`);
|
|
61
|
-
// The error first, so a screen reader reads it before the hint.
|
|
62
|
-
const describedByIds = $derived(
|
|
63
|
-
[error ? errorId : undefined, hint ? hintId : undefined, describedBy].filter(Boolean).join(' ') ||
|
|
64
|
-
undefined
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
// ---- Short motion: the error (BRIEF, --dur-message) --------------------
|
|
68
|
-
|
|
69
|
-
// An error that appears after the first render fades in from 0 over
|
|
70
|
-
// --dur-message, and the well's edge fades to --danger with it (the CSS
|
|
71
|
-
// below). An error that clears goes at once, and so does the red edge. A
|
|
72
|
-
// field that renders or mounts with an error, such as one in a dialog
|
|
73
|
-
// that opens, shows it at once. 0ms is a snap.
|
|
74
|
-
let root: HTMLDivElement | null = $state(null);
|
|
75
|
-
let errorShown = untrack(() => !!error);
|
|
76
|
-
$effect(() => {
|
|
77
|
-
const shown = !!error;
|
|
78
|
-
untrack(() => {
|
|
79
|
-
const appeared = shown && !errorShown;
|
|
80
|
-
errorShown = shown;
|
|
81
|
-
const message = appeared ? root?.querySelector(':scope > .error') : null;
|
|
82
|
-
if (message) fadeIn(message, '--dur-message');
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
</script>
|
|
86
|
-
|
|
87
|
-
<!--
|
|
88
|
-
A single-line text input in the black well, with a plain caps label above
|
|
89
|
-
it (Bits UI Label), an optional hint and an inline error below. Extra
|
|
90
|
-
attributes (`name`, `inputmode`, `autocomplete`, `enterkeyhint`,
|
|
91
|
-
`required`), handlers and `class` go to the <input>.
|
|
92
|
-
-->
|
|
93
|
-
<div
|
|
94
|
-
bind:this={root}
|
|
95
|
-
class="hp-text-field"
|
|
96
|
-
data-invalid={error ? '' : undefined}
|
|
97
|
-
data-disabled={disabled ? '' : undefined}
|
|
98
|
-
data-label-beside={labelBeside === true ? 'always' : labelBeside || undefined}
|
|
99
|
-
>
|
|
100
|
-
<Label.Root for={id}>
|
|
101
|
-
{#snippet child({ props })}
|
|
102
|
-
<label {...props} class="hp-label">{label}</label>
|
|
103
|
-
{/snippet}
|
|
104
|
-
</Label.Root>
|
|
105
|
-
<input
|
|
106
|
-
{...rest}
|
|
107
|
-
bind:this={ref}
|
|
108
|
-
bind:value
|
|
109
|
-
{id}
|
|
110
|
-
{type}
|
|
111
|
-
{disabled}
|
|
112
|
-
class={['hp-well', className]}
|
|
113
|
-
aria-describedby={describedByIds}
|
|
114
|
-
aria-invalid={error ? true : ariaInvalid}
|
|
115
|
-
/>
|
|
116
|
-
{#if hint}
|
|
117
|
-
<p class="hint hp-caption" id={hintId}>{hint}</p>
|
|
118
|
-
{/if}
|
|
119
|
-
{#if error}
|
|
120
|
-
<p class="error hp-caption" id={errorId}>
|
|
121
|
-
<span class="icon" aria-hidden="true">
|
|
122
|
-
<svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor">
|
|
123
|
-
<path d="M1.5 1.5h13v13h-13z" />
|
|
124
|
-
<path d="M8 4v4M8 10v2" stroke-width="2" />
|
|
125
|
-
</svg>
|
|
126
|
-
</span>
|
|
127
|
-
<span>{error}</span>
|
|
128
|
-
</p>
|
|
129
|
-
{/if}
|
|
130
|
-
</div>
|
|
131
|
-
|
|
132
|
-
<style>
|
|
133
|
-
.hp-text-field {
|
|
134
|
-
display: grid;
|
|
135
|
-
gap: var(--space-1);
|
|
136
|
-
min-width: 0;
|
|
137
|
-
}
|
|
138
|
-
label {
|
|
139
|
-
color: var(--muted);
|
|
140
|
-
}
|
|
141
|
-
input {
|
|
142
|
-
display: block;
|
|
143
|
-
width: 100%;
|
|
144
|
-
min-width: 0;
|
|
145
|
-
}
|
|
146
|
-
input::placeholder {
|
|
147
|
-
color: var(--muted);
|
|
148
|
-
opacity: 1;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/* Invalid: the boundary turns --danger (5.9:1 on black), never alone:
|
|
152
|
-
the icon and the message say what is wrong. Keyed on the root's
|
|
153
|
-
data-invalid, set by `error`, so an aria-invalid the app passes
|
|
154
|
-
without a message draws no red border. The edge fades to --danger
|
|
155
|
-
over --dur-message with the message; the transition belongs to this
|
|
156
|
-
rule, so the edge goes back at once when the error clears. The hover
|
|
157
|
-
fill keeps base.css's timing. */
|
|
158
|
-
[data-invalid] input {
|
|
159
|
-
border-color: var(--danger);
|
|
160
|
-
transition:
|
|
161
|
-
background-color var(--dur-hover) var(--settle),
|
|
162
|
-
border-color var(--dur-message) var(--settle);
|
|
163
|
-
}
|
|
164
|
-
[data-invalid] input:disabled {
|
|
165
|
-
border-color: var(--border);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
.hint {
|
|
169
|
-
margin: 0;
|
|
170
|
-
color: var(--muted);
|
|
171
|
-
}
|
|
172
|
-
.error {
|
|
173
|
-
display: flex;
|
|
174
|
-
gap: var(--space-2);
|
|
175
|
-
margin: 0;
|
|
176
|
-
color: var(--danger);
|
|
177
|
-
}
|
|
178
|
-
/* The first message starts 8px under the well, clear of the focus
|
|
179
|
-
contour (2px, drawn 4px outside the well). */
|
|
180
|
-
input + .hint,
|
|
181
|
-
input + .error {
|
|
182
|
-
margin-top: var(--space-1);
|
|
183
|
-
}
|
|
184
|
-
/* One line box high, so the icon centres on the first line of the
|
|
185
|
-
message however it wraps. */
|
|
186
|
-
.icon {
|
|
187
|
-
display: flex;
|
|
188
|
-
flex: none;
|
|
189
|
-
align-items: center;
|
|
190
|
-
height: 1lh;
|
|
191
|
-
}
|
|
192
|
-
.icon svg {
|
|
193
|
-
display: block;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/* Beside the well, the label keeps its one line in the first column; the
|
|
197
|
-
well, hint and error share the second. */
|
|
198
|
-
[data-label-beside='always'] {
|
|
199
|
-
grid-template-columns: auto minmax(0, 1fr);
|
|
200
|
-
column-gap: var(--space-2);
|
|
201
|
-
}
|
|
202
|
-
[data-label-beside='always'] label {
|
|
203
|
-
grid-row: 1;
|
|
204
|
-
align-self: center;
|
|
205
|
-
white-space: nowrap;
|
|
206
|
-
}
|
|
207
|
-
[data-label-beside='always'] input,
|
|
208
|
-
[data-label-beside='always'] p {
|
|
209
|
-
grid-column: 2;
|
|
210
|
-
}
|
|
211
|
-
@media (min-width: 600px) {
|
|
212
|
-
[data-label-beside='medium'] {
|
|
213
|
-
grid-template-columns: auto minmax(0, 1fr);
|
|
214
|
-
column-gap: var(--space-2);
|
|
215
|
-
}
|
|
216
|
-
[data-label-beside='medium'] label {
|
|
217
|
-
grid-row: 1;
|
|
218
|
-
align-self: center;
|
|
219
|
-
white-space: nowrap;
|
|
220
|
-
}
|
|
221
|
-
[data-label-beside='medium'] input,
|
|
222
|
-
[data-label-beside='medium'] p {
|
|
223
|
-
grid-column: 2;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
@media (min-width: 900px) {
|
|
227
|
-
[data-label-beside='expanded'] {
|
|
228
|
-
grid-template-columns: auto minmax(0, 1fr);
|
|
229
|
-
column-gap: var(--space-2);
|
|
230
|
-
}
|
|
231
|
-
[data-label-beside='expanded'] label {
|
|
232
|
-
grid-row: 1;
|
|
233
|
-
align-self: center;
|
|
234
|
-
white-space: nowrap;
|
|
235
|
-
}
|
|
236
|
-
[data-label-beside='expanded'] input,
|
|
237
|
-
[data-label-beside='expanded'] p {
|
|
238
|
-
grid-column: 2;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
@media (min-width: 1200px) {
|
|
242
|
-
[data-label-beside='large'] {
|
|
243
|
-
grid-template-columns: auto minmax(0, 1fr);
|
|
244
|
-
column-gap: var(--space-2);
|
|
245
|
-
}
|
|
246
|
-
[data-label-beside='large'] label {
|
|
247
|
-
grid-row: 1;
|
|
248
|
-
align-self: center;
|
|
249
|
-
white-space: nowrap;
|
|
250
|
-
}
|
|
251
|
-
[data-label-beside='large'] input,
|
|
252
|
-
[data-label-beside='large'] p {
|
|
253
|
-
grid-column: 2;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
</style>
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
3
|
+
|
|
4
|
+
/** The text input types the well suits. For numbers use `text` with `inputmode="numeric"`. */
|
|
5
|
+
export type TextFieldType = 'text' | 'email' | 'password' | 'search' | 'tel' | 'url';
|
|
6
|
+
|
|
7
|
+
/** From which width the label sits beside the well: 600px, 900px or 1200px. */
|
|
8
|
+
export type TextFieldBreakpoint = 'medium' | 'expanded' | 'large';
|
|
9
|
+
|
|
10
|
+
/** Attributes and handlers not listed here go to the `<input>`. */
|
|
11
|
+
export type TextFieldProps = Omit<HTMLInputAttributes, 'type' | 'value' | 'children'> & {
|
|
12
|
+
/** The caps field label above the well. Required: a placeholder is never the only label. */
|
|
13
|
+
label: string;
|
|
14
|
+
/** The text. Bindable. */
|
|
15
|
+
value?: string;
|
|
16
|
+
type?: TextFieldType;
|
|
17
|
+
/** Guidance under the well in `--muted`, linked by `aria-describedby`. */
|
|
18
|
+
hint?: string;
|
|
19
|
+
/**
|
|
20
|
+
* The error to show under the well, with an icon, linked by
|
|
21
|
+
* `aria-describedby`; it sets `aria-invalid`. Say how to fix the
|
|
22
|
+
* problem. The app validates; the field shows what it is given.
|
|
23
|
+
*/
|
|
24
|
+
error?: string;
|
|
25
|
+
/**
|
|
26
|
+
* The label sits above the well by default. `true` puts it beside the
|
|
27
|
+
* well at every width; a breakpoint puts it beside from that width up.
|
|
28
|
+
* The hint and the error stay under the well.
|
|
29
|
+
*/
|
|
30
|
+
labelBeside?: boolean | TextFieldBreakpoint;
|
|
31
|
+
/** The `<input>`, for example to focus the first error on submit. */
|
|
32
|
+
ref?: HTMLInputElement | null;
|
|
33
|
+
};
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<script lang="ts">
|
|
37
|
+
import { untrack } from 'svelte';
|
|
38
|
+
import { Label } from 'bits-ui';
|
|
39
|
+
import { fadeIn } from './internal/fade-in.js';
|
|
40
|
+
|
|
41
|
+
const uid = $props.id();
|
|
42
|
+
|
|
43
|
+
let {
|
|
44
|
+
label,
|
|
45
|
+
value = $bindable(''),
|
|
46
|
+
type = 'text',
|
|
47
|
+
hint,
|
|
48
|
+
error,
|
|
49
|
+
labelBeside = false,
|
|
50
|
+
id = uid,
|
|
51
|
+
disabled,
|
|
52
|
+
ref = $bindable(null),
|
|
53
|
+
class: className,
|
|
54
|
+
'aria-describedby': describedBy,
|
|
55
|
+
'aria-invalid': ariaInvalid,
|
|
56
|
+
...rest
|
|
57
|
+
}: TextFieldProps = $props();
|
|
58
|
+
|
|
59
|
+
const hintId = $derived(`${id}-hint`);
|
|
60
|
+
const errorId = $derived(`${id}-error`);
|
|
61
|
+
// The error first, so a screen reader reads it before the hint.
|
|
62
|
+
const describedByIds = $derived(
|
|
63
|
+
[error ? errorId : undefined, hint ? hintId : undefined, describedBy].filter(Boolean).join(' ') ||
|
|
64
|
+
undefined
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// ---- Short motion: the error (BRIEF, --dur-message) --------------------
|
|
68
|
+
|
|
69
|
+
// An error that appears after the first render fades in from 0 over
|
|
70
|
+
// --dur-message, and the well's edge fades to --danger with it (the CSS
|
|
71
|
+
// below). An error that clears goes at once, and so does the red edge. A
|
|
72
|
+
// field that renders or mounts with an error, such as one in a dialog
|
|
73
|
+
// that opens, shows it at once. 0ms is a snap.
|
|
74
|
+
let root: HTMLDivElement | null = $state(null);
|
|
75
|
+
let errorShown = untrack(() => !!error);
|
|
76
|
+
$effect(() => {
|
|
77
|
+
const shown = !!error;
|
|
78
|
+
untrack(() => {
|
|
79
|
+
const appeared = shown && !errorShown;
|
|
80
|
+
errorShown = shown;
|
|
81
|
+
const message = appeared ? root?.querySelector(':scope > .error') : null;
|
|
82
|
+
if (message) fadeIn(message, '--dur-message');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
</script>
|
|
86
|
+
|
|
87
|
+
<!--
|
|
88
|
+
A single-line text input in the black well, with a plain caps label above
|
|
89
|
+
it (Bits UI Label), an optional hint and an inline error below. Extra
|
|
90
|
+
attributes (`name`, `inputmode`, `autocomplete`, `enterkeyhint`,
|
|
91
|
+
`required`), handlers and `class` go to the <input>.
|
|
92
|
+
-->
|
|
93
|
+
<div
|
|
94
|
+
bind:this={root}
|
|
95
|
+
class="hp-text-field"
|
|
96
|
+
data-invalid={error ? '' : undefined}
|
|
97
|
+
data-disabled={disabled ? '' : undefined}
|
|
98
|
+
data-label-beside={labelBeside === true ? 'always' : labelBeside || undefined}
|
|
99
|
+
>
|
|
100
|
+
<Label.Root for={id}>
|
|
101
|
+
{#snippet child({ props })}
|
|
102
|
+
<label {...props} class="hp-label">{label}</label>
|
|
103
|
+
{/snippet}
|
|
104
|
+
</Label.Root>
|
|
105
|
+
<input
|
|
106
|
+
{...rest}
|
|
107
|
+
bind:this={ref}
|
|
108
|
+
bind:value
|
|
109
|
+
{id}
|
|
110
|
+
{type}
|
|
111
|
+
{disabled}
|
|
112
|
+
class={['hp-well', className]}
|
|
113
|
+
aria-describedby={describedByIds}
|
|
114
|
+
aria-invalid={error ? true : ariaInvalid}
|
|
115
|
+
/>
|
|
116
|
+
{#if hint}
|
|
117
|
+
<p class="hint hp-caption" id={hintId}>{hint}</p>
|
|
118
|
+
{/if}
|
|
119
|
+
{#if error}
|
|
120
|
+
<p class="error hp-caption" id={errorId}>
|
|
121
|
+
<span class="icon" aria-hidden="true">
|
|
122
|
+
<svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor">
|
|
123
|
+
<path d="M1.5 1.5h13v13h-13z" />
|
|
124
|
+
<path d="M8 4v4M8 10v2" stroke-width="2" />
|
|
125
|
+
</svg>
|
|
126
|
+
</span>
|
|
127
|
+
<span>{error}</span>
|
|
128
|
+
</p>
|
|
129
|
+
{/if}
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<style>
|
|
133
|
+
.hp-text-field {
|
|
134
|
+
display: grid;
|
|
135
|
+
gap: var(--space-1);
|
|
136
|
+
min-width: 0;
|
|
137
|
+
}
|
|
138
|
+
label {
|
|
139
|
+
color: var(--muted);
|
|
140
|
+
}
|
|
141
|
+
input {
|
|
142
|
+
display: block;
|
|
143
|
+
width: 100%;
|
|
144
|
+
min-width: 0;
|
|
145
|
+
}
|
|
146
|
+
input::placeholder {
|
|
147
|
+
color: var(--muted);
|
|
148
|
+
opacity: 1;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* Invalid: the boundary turns --danger (5.9:1 on black), never alone:
|
|
152
|
+
the icon and the message say what is wrong. Keyed on the root's
|
|
153
|
+
data-invalid, set by `error`, so an aria-invalid the app passes
|
|
154
|
+
without a message draws no red border. The edge fades to --danger
|
|
155
|
+
over --dur-message with the message; the transition belongs to this
|
|
156
|
+
rule, so the edge goes back at once when the error clears. The hover
|
|
157
|
+
fill keeps base.css's timing. */
|
|
158
|
+
[data-invalid] input {
|
|
159
|
+
border-color: var(--danger);
|
|
160
|
+
transition:
|
|
161
|
+
background-color var(--dur-hover) var(--settle),
|
|
162
|
+
border-color var(--dur-message) var(--settle);
|
|
163
|
+
}
|
|
164
|
+
[data-invalid] input:disabled {
|
|
165
|
+
border-color: var(--border);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.hint {
|
|
169
|
+
margin: 0;
|
|
170
|
+
color: var(--muted);
|
|
171
|
+
}
|
|
172
|
+
.error {
|
|
173
|
+
display: flex;
|
|
174
|
+
gap: var(--space-2);
|
|
175
|
+
margin: 0;
|
|
176
|
+
color: var(--danger);
|
|
177
|
+
}
|
|
178
|
+
/* The first message starts 8px under the well, clear of the focus
|
|
179
|
+
contour (2px, drawn 4px outside the well). */
|
|
180
|
+
input + .hint,
|
|
181
|
+
input + .error {
|
|
182
|
+
margin-top: var(--space-1);
|
|
183
|
+
}
|
|
184
|
+
/* One line box high, so the icon centres on the first line of the
|
|
185
|
+
message however it wraps. */
|
|
186
|
+
.icon {
|
|
187
|
+
display: flex;
|
|
188
|
+
flex: none;
|
|
189
|
+
align-items: center;
|
|
190
|
+
height: 1lh;
|
|
191
|
+
}
|
|
192
|
+
.icon svg {
|
|
193
|
+
display: block;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/* Beside the well, the label keeps its one line in the first column; the
|
|
197
|
+
well, hint and error share the second. */
|
|
198
|
+
[data-label-beside='always'] {
|
|
199
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
200
|
+
column-gap: var(--space-2);
|
|
201
|
+
}
|
|
202
|
+
[data-label-beside='always'] label {
|
|
203
|
+
grid-row: 1;
|
|
204
|
+
align-self: center;
|
|
205
|
+
white-space: nowrap;
|
|
206
|
+
}
|
|
207
|
+
[data-label-beside='always'] input,
|
|
208
|
+
[data-label-beside='always'] p {
|
|
209
|
+
grid-column: 2;
|
|
210
|
+
}
|
|
211
|
+
@media (min-width: 600px) {
|
|
212
|
+
[data-label-beside='medium'] {
|
|
213
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
214
|
+
column-gap: var(--space-2);
|
|
215
|
+
}
|
|
216
|
+
[data-label-beside='medium'] label {
|
|
217
|
+
grid-row: 1;
|
|
218
|
+
align-self: center;
|
|
219
|
+
white-space: nowrap;
|
|
220
|
+
}
|
|
221
|
+
[data-label-beside='medium'] input,
|
|
222
|
+
[data-label-beside='medium'] p {
|
|
223
|
+
grid-column: 2;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
@media (min-width: 900px) {
|
|
227
|
+
[data-label-beside='expanded'] {
|
|
228
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
229
|
+
column-gap: var(--space-2);
|
|
230
|
+
}
|
|
231
|
+
[data-label-beside='expanded'] label {
|
|
232
|
+
grid-row: 1;
|
|
233
|
+
align-self: center;
|
|
234
|
+
white-space: nowrap;
|
|
235
|
+
}
|
|
236
|
+
[data-label-beside='expanded'] input,
|
|
237
|
+
[data-label-beside='expanded'] p {
|
|
238
|
+
grid-column: 2;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
@media (min-width: 1200px) {
|
|
242
|
+
[data-label-beside='large'] {
|
|
243
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
244
|
+
column-gap: var(--space-2);
|
|
245
|
+
}
|
|
246
|
+
[data-label-beside='large'] label {
|
|
247
|
+
grid-row: 1;
|
|
248
|
+
align-self: center;
|
|
249
|
+
white-space: nowrap;
|
|
250
|
+
}
|
|
251
|
+
[data-label-beside='large'] input,
|
|
252
|
+
[data-label-beside='large'] p {
|
|
253
|
+
grid-column: 2;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
</style>
|