@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
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { HTMLTextareaAttributes } from 'svelte/elements';
|
|
3
|
+
|
|
4
|
+
/** Attributes and handlers not listed here go to the `<textarea>`. */
|
|
5
|
+
export type TextAreaProps = Omit<HTMLTextareaAttributes, 'value' | 'children' | 'rows'> & {
|
|
6
|
+
/** The caps field label above the well. Required: a placeholder is never the only label. */
|
|
7
|
+
label: string;
|
|
8
|
+
/** The text. Bindable. */
|
|
9
|
+
value?: string;
|
|
10
|
+
/** Guidance under the well in `--muted`, linked by `aria-describedby`. */
|
|
11
|
+
hint?: string;
|
|
12
|
+
/**
|
|
13
|
+
* The error to show under the well, with an icon, linked by
|
|
14
|
+
* `aria-describedby`; it sets `aria-invalid`. Say how to fix the
|
|
15
|
+
* problem. The app validates; the field shows what it is given.
|
|
16
|
+
*/
|
|
17
|
+
error?: string;
|
|
18
|
+
/** The height of the empty well, in lines. */
|
|
19
|
+
rows?: number;
|
|
20
|
+
/**
|
|
21
|
+
* The well grows with its text up to this many lines, then scrolls
|
|
22
|
+
* inside. Never less than `rows`.
|
|
23
|
+
*/
|
|
24
|
+
maxRows?: number;
|
|
25
|
+
/** The `<textarea>`, for example to focus the first error on submit. */
|
|
26
|
+
ref?: HTMLTextAreaElement | null;
|
|
27
|
+
};
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<script lang="ts">
|
|
31
|
+
import { untrack } from 'svelte';
|
|
32
|
+
import { Label } from 'bits-ui';
|
|
33
|
+
import { fadeIn } from './internal/fade-in.js';
|
|
34
|
+
|
|
35
|
+
const uid = $props.id();
|
|
36
|
+
|
|
37
|
+
let {
|
|
38
|
+
label,
|
|
39
|
+
value = $bindable(''),
|
|
40
|
+
hint,
|
|
41
|
+
error,
|
|
42
|
+
rows = 3,
|
|
43
|
+
maxRows = 10,
|
|
44
|
+
id = uid,
|
|
45
|
+
disabled,
|
|
46
|
+
readonly,
|
|
47
|
+
ref = $bindable(null),
|
|
48
|
+
class: className,
|
|
49
|
+
'aria-describedby': describedBy,
|
|
50
|
+
'aria-invalid': ariaInvalid,
|
|
51
|
+
...rest
|
|
52
|
+
}: TextAreaProps = $props();
|
|
53
|
+
|
|
54
|
+
const hintId = $derived(`${id}-hint`);
|
|
55
|
+
const errorId = $derived(`${id}-error`);
|
|
56
|
+
// The error first, so a screen reader reads it before the hint.
|
|
57
|
+
const describedByIds = $derived(
|
|
58
|
+
[error ? errorId : undefined, hint ? hintId : undefined, describedBy].filter(Boolean).join(' ') ||
|
|
59
|
+
undefined
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
// ---- Short motion: the error (BRIEF, --dur-message) --------------------
|
|
63
|
+
|
|
64
|
+
// As TextField: an error that appears after the first render fades in
|
|
65
|
+
// from 0 over --dur-message, and the well's edge fades to --danger with
|
|
66
|
+
// it (the CSS below). An error that clears goes at once, and so does the
|
|
67
|
+
// red edge. A field that renders or mounts with an error shows it at once.
|
|
68
|
+
let root: HTMLDivElement | null = $state(null);
|
|
69
|
+
let errorShown = untrack(() => !!error);
|
|
70
|
+
$effect(() => {
|
|
71
|
+
const shown = !!error;
|
|
72
|
+
untrack(() => {
|
|
73
|
+
const appeared = shown && !errorShown;
|
|
74
|
+
errorShown = shown;
|
|
75
|
+
const message = appeared ? root?.querySelector(':scope > .error') : null;
|
|
76
|
+
if (message) fadeIn(message, '--dur-message');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// ---- Growth ------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
// Where the browser has field-sizing (Chromium 123+), CSS alone sizes the
|
|
83
|
+
// well between its rows and maxRows heights. Elsewhere the height comes
|
|
84
|
+
// from scrollHeight whenever the text, the line counts or the width
|
|
85
|
+
// change, written through element.style, never a style attribute.
|
|
86
|
+
const sizesItself = typeof CSS !== 'undefined' && CSS.supports('field-sizing', 'content');
|
|
87
|
+
|
|
88
|
+
function fit(field: HTMLTextAreaElement) {
|
|
89
|
+
// The well collapses to its content to measure it. Holding the root's
|
|
90
|
+
// height meanwhile keeps the page from getting shorter, which would
|
|
91
|
+
// move its scroll position. Without a scrollbar while it measures, the
|
|
92
|
+
// text wraps at the width it has once it fits.
|
|
93
|
+
if (root) root.style.minHeight = `${root.offsetHeight}px`;
|
|
94
|
+
field.style.overflowY = 'hidden';
|
|
95
|
+
field.style.height = 'auto';
|
|
96
|
+
const borders = field.offsetHeight - field.clientHeight;
|
|
97
|
+
field.style.height = `${field.scrollHeight + borders}px`;
|
|
98
|
+
field.style.overflowY = '';
|
|
99
|
+
if (root) root.style.minHeight = '';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
$effect(() => {
|
|
103
|
+
const field = ref;
|
|
104
|
+
if (sizesItself || !field) return;
|
|
105
|
+
// Read so the effect runs again when they change. A new `style` from
|
|
106
|
+
// the app replaces the element's inline height, so it refits too.
|
|
107
|
+
void [value, rows, maxRows, rest.style];
|
|
108
|
+
fit(field);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
$effect(() => {
|
|
112
|
+
const field = ref;
|
|
113
|
+
if (sizesItself || !field) return;
|
|
114
|
+
let width = field.clientWidth;
|
|
115
|
+
const observer = new ResizeObserver(() => {
|
|
116
|
+
if (field.clientWidth === width) return;
|
|
117
|
+
width = field.clientWidth;
|
|
118
|
+
fit(field);
|
|
119
|
+
});
|
|
120
|
+
observer.observe(field);
|
|
121
|
+
return () => observer.disconnect();
|
|
122
|
+
});
|
|
123
|
+
</script>
|
|
124
|
+
|
|
125
|
+
<!--
|
|
126
|
+
Multi-line text in the black well, with a plain caps label above it (Bits
|
|
127
|
+
UI Label), an optional hint and an inline error below. The well starts
|
|
128
|
+
`rows` lines high, grows with its text up to `maxRows` lines, then scrolls
|
|
129
|
+
inside. Extra attributes (`name`, `placeholder`, `required`, `maxlength`,
|
|
130
|
+
`autocomplete`, `enterkeyhint`), handlers and `class` go to the
|
|
131
|
+
<textarea>. The textarea is never re-created and its value is written
|
|
132
|
+
only when it differs, so the text, the caret and focus survive any
|
|
133
|
+
re-render around it.
|
|
134
|
+
-->
|
|
135
|
+
<div
|
|
136
|
+
bind:this={root}
|
|
137
|
+
class="hp-text-area"
|
|
138
|
+
data-invalid={error ? '' : undefined}
|
|
139
|
+
data-disabled={disabled ? '' : undefined}
|
|
140
|
+
data-readonly={readonly ? '' : undefined}
|
|
141
|
+
style:--text-area-rows={Math.max(1, rows)}
|
|
142
|
+
style:--text-area-max-rows={Math.max(1, maxRows)}
|
|
143
|
+
>
|
|
144
|
+
<Label.Root for={id}>
|
|
145
|
+
{#snippet child({ props })}
|
|
146
|
+
<label {...props} class="hp-label">{label}</label>
|
|
147
|
+
{/snippet}
|
|
148
|
+
</Label.Root>
|
|
149
|
+
<textarea
|
|
150
|
+
{...rest}
|
|
151
|
+
bind:this={ref}
|
|
152
|
+
bind:value
|
|
153
|
+
{id}
|
|
154
|
+
rows={Math.max(1, rows)}
|
|
155
|
+
{disabled}
|
|
156
|
+
{readonly}
|
|
157
|
+
class={['hp-well', className]}
|
|
158
|
+
aria-describedby={describedByIds}
|
|
159
|
+
aria-invalid={error ? true : ariaInvalid}
|
|
160
|
+
></textarea>
|
|
161
|
+
{#if hint}
|
|
162
|
+
<p class="hint hp-caption" id={hintId}>{hint}</p>
|
|
163
|
+
{/if}
|
|
164
|
+
{#if error}
|
|
165
|
+
<p class="error hp-caption" id={errorId}>
|
|
166
|
+
<span class="icon" aria-hidden="true">
|
|
167
|
+
<svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor">
|
|
168
|
+
<path d="M1.5 1.5h13v13h-13z" />
|
|
169
|
+
<path d="M8 4v4M8 10v2" stroke-width="2" />
|
|
170
|
+
</svg>
|
|
171
|
+
</span>
|
|
172
|
+
<span>{error}</span>
|
|
173
|
+
</p>
|
|
174
|
+
{/if}
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<style>
|
|
178
|
+
.hp-text-area {
|
|
179
|
+
display: grid;
|
|
180
|
+
gap: var(--space-1);
|
|
181
|
+
min-width: 0;
|
|
182
|
+
}
|
|
183
|
+
label {
|
|
184
|
+
color: var(--muted);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* The well, in lines of 24px. Its heights are the line count times the
|
|
188
|
+
line, plus 12px of padding at the top and bottom and the two 1px
|
|
189
|
+
borders: 98px empty and 266px at most by default. The root sets the
|
|
190
|
+
line counts from `rows` and `maxRows`; the fallbacks are the defaults,
|
|
191
|
+
for a page whose policy blocks the style attribute of server-rendered
|
|
192
|
+
markup. The text is 16px at every width, from .hp-well. */
|
|
193
|
+
textarea {
|
|
194
|
+
display: block;
|
|
195
|
+
width: 100%;
|
|
196
|
+
min-width: 0;
|
|
197
|
+
height: auto;
|
|
198
|
+
min-height: calc(var(--text-area-rows, 3) * 24px + 2 * var(--space-3) + 2px);
|
|
199
|
+
max-height: calc(var(--text-area-max-rows, 10) * 24px + 2 * var(--space-3) + 2px);
|
|
200
|
+
padding: var(--space-3);
|
|
201
|
+
line-height: 24px;
|
|
202
|
+
field-sizing: content;
|
|
203
|
+
resize: none;
|
|
204
|
+
overflow-y: auto;
|
|
205
|
+
/* Once it scrolls, the browser keeps the caret's line clear of the
|
|
206
|
+
padding, so the line being typed never sits on the edge. */
|
|
207
|
+
scroll-padding-block: var(--space-3);
|
|
208
|
+
/* As the other scrolling parts: a thin bar on fine pointers; touch
|
|
209
|
+
shows its own while it scrolls. */
|
|
210
|
+
scrollbar-width: thin;
|
|
211
|
+
}
|
|
212
|
+
textarea::placeholder {
|
|
213
|
+
color: var(--muted);
|
|
214
|
+
opacity: 1;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/* Read-only: muted text as when disabled, and no hover fill, because
|
|
218
|
+
nothing here can change. It keeps the 3:1 boundary: the field still
|
|
219
|
+
takes focus and its text can be selected and copied, so it stays a
|
|
220
|
+
control (WCAG 1.4.11 exempts only inactive ones). */
|
|
221
|
+
textarea[readonly] {
|
|
222
|
+
color: var(--muted);
|
|
223
|
+
}
|
|
224
|
+
@media (hover: hover) and (pointer: fine) {
|
|
225
|
+
textarea[readonly]:hover {
|
|
226
|
+
background-color: var(--canvas);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/* Invalid: as TextField. The boundary turns --danger, never alone: the
|
|
231
|
+
icon and the message say what is wrong. Keyed on the root's
|
|
232
|
+
data-invalid, set by `error`. The edge fades to --danger over
|
|
233
|
+
--dur-message with the message and goes back at once when the error
|
|
234
|
+
clears. A disabled well keeps --border. */
|
|
235
|
+
[data-invalid] textarea {
|
|
236
|
+
border-color: var(--danger);
|
|
237
|
+
transition:
|
|
238
|
+
background-color var(--dur-hover) var(--settle),
|
|
239
|
+
border-color var(--dur-message) var(--settle);
|
|
240
|
+
}
|
|
241
|
+
[data-invalid] textarea:disabled {
|
|
242
|
+
border-color: var(--border);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.hint {
|
|
246
|
+
margin: 0;
|
|
247
|
+
color: var(--muted);
|
|
248
|
+
}
|
|
249
|
+
.error {
|
|
250
|
+
display: flex;
|
|
251
|
+
gap: var(--space-2);
|
|
252
|
+
margin: 0;
|
|
253
|
+
color: var(--danger);
|
|
254
|
+
}
|
|
255
|
+
/* The first message starts 8px under the well, clear of the focus
|
|
256
|
+
contour (2px, drawn 4px outside the well). */
|
|
257
|
+
textarea + .hint,
|
|
258
|
+
textarea + .error {
|
|
259
|
+
margin-top: var(--space-1);
|
|
260
|
+
}
|
|
261
|
+
/* One line box high, so the icon centres on the first line of the
|
|
262
|
+
message however it wraps. */
|
|
263
|
+
.icon {
|
|
264
|
+
display: flex;
|
|
265
|
+
flex: none;
|
|
266
|
+
align-items: center;
|
|
267
|
+
height: 1lh;
|
|
268
|
+
}
|
|
269
|
+
.icon svg {
|
|
270
|
+
display: block;
|
|
271
|
+
}
|
|
272
|
+
</style>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { HTMLTextareaAttributes } from 'svelte/elements';
|
|
2
|
+
/** Attributes and handlers not listed here go to the `<textarea>`. */
|
|
3
|
+
export type TextAreaProps = Omit<HTMLTextareaAttributes, 'value' | 'children' | 'rows'> & {
|
|
4
|
+
/** The caps field label above the well. Required: a placeholder is never the only label. */
|
|
5
|
+
label: string;
|
|
6
|
+
/** The text. Bindable. */
|
|
7
|
+
value?: string;
|
|
8
|
+
/** Guidance under the well in `--muted`, linked by `aria-describedby`. */
|
|
9
|
+
hint?: string;
|
|
10
|
+
/**
|
|
11
|
+
* The error to show under the well, with an icon, linked by
|
|
12
|
+
* `aria-describedby`; it sets `aria-invalid`. Say how to fix the
|
|
13
|
+
* problem. The app validates; the field shows what it is given.
|
|
14
|
+
*/
|
|
15
|
+
error?: string;
|
|
16
|
+
/** The height of the empty well, in lines. */
|
|
17
|
+
rows?: number;
|
|
18
|
+
/**
|
|
19
|
+
* The well grows with its text up to this many lines, then scrolls
|
|
20
|
+
* inside. Never less than `rows`.
|
|
21
|
+
*/
|
|
22
|
+
maxRows?: number;
|
|
23
|
+
/** The `<textarea>`, for example to focus the first error on submit. */
|
|
24
|
+
ref?: HTMLTextAreaElement | null;
|
|
25
|
+
};
|
|
26
|
+
declare const TextArea: import("svelte").Component<TextAreaProps, {}, "value" | "ref">;
|
|
27
|
+
type TextArea = ReturnType<typeof TextArea>;
|
|
28
|
+
export default TextArea;
|