@mystravil/hardpoint 0.2.1 → 0.3.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/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
Hardpoint follows semantic versioning. Before 1.0.0, a breaking change raises the minor version.
|
|
4
4
|
|
|
5
|
+
## 0.3.0 (2026-11-12)
|
|
6
|
+
|
|
7
|
+
- **Added**:
|
|
8
|
+
- `TextField` with `type="search"` shows its own clear button at the end of the well while the field holds text, and hides the browser's cancel button, which Chrome on Android drew in bright blue. The button is 44 px on coarse pointers. A press empties the field, fires `input` (so `bind:value` and `oninput` see it) and leaves focus in the field; Tab reaches it, and Enter or Space clears. `clearLabel` names it (default `Clear`).
|
|
9
|
+
- `TextField` `labelHidden`: the label stays the field's accessible name but is hidden from sight, for a search in a toolbar. The well keeps its look and 16 px text.
|
|
10
|
+
- **Changed**:
|
|
11
|
+
- `Menu` mounts Bits UI's portal and content on the first open instead of with the trigger, so a closed menu costs about a fifth of what it did (66 µs against 322 µs in Chromium, with its Button trigger). The trigger keeps `aria-haspopup`, `aria-expanded` and, while open, `aria-controls`; the first open plays the entrance as before.
|
|
12
|
+
- `Button` renders a plain `<button>`, or an `<a>` with `href`, without Bits UI's dynamic element, and mounts in less than half the time (10 µs against 23 µs). A disabled link still has no `href`, `role="link"`, `aria-disabled="true"` and `tabindex="-1"`. The element no longer carries Bits UI's `data-button-root`, `tabindex="0"` or `aria-disabled="false"`. `ButtonProps` is Hardpoint's own type with the same shape.
|
|
13
|
+
|
|
5
14
|
## 0.2.1 (2026-11-12)
|
|
6
15
|
|
|
7
16
|
- **Changed**: the source is public at [MYSTRAVIL/hardpoint](https://github.com/MYSTRAVIL/hardpoint), and `package.json` links it. Releases publish from GitHub Actions through npm trusted publishing, so from this version on each release carries a provenance attestation that ties it to its commit and workflow run. The components did not change.
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
<script lang="ts" module>
|
|
2
|
-
import {
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
|
|
3
4
|
|
|
4
5
|
export type ButtonVariant = 'primary' | 'secondary' | 'tertiary';
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
type ButtonOwnProps = {
|
|
7
8
|
/**
|
|
8
9
|
* `primary`: solid titanium block, for the view's one group of solid
|
|
9
10
|
* blocks. `secondary` (default): outlined. `tertiary`: caps label only.
|
|
@@ -17,7 +18,27 @@
|
|
|
17
18
|
* `aria-label`.
|
|
18
19
|
*/
|
|
19
20
|
icon?: boolean;
|
|
21
|
+
/** The `<button>`, or the `<a>` when `href` is set. */
|
|
22
|
+
ref?: HTMLElement | null;
|
|
23
|
+
children?: Snippet;
|
|
20
24
|
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* An `<a>` when `href` is set, a `<button>` otherwise. Attributes and
|
|
28
|
+
* handlers not listed here go to the element.
|
|
29
|
+
*/
|
|
30
|
+
export type ButtonProps =
|
|
31
|
+
| (ButtonOwnProps &
|
|
32
|
+
Omit<HTMLButtonAttributes, 'children' | 'type' | 'href'> & {
|
|
33
|
+
type?: HTMLButtonAttributes['type'];
|
|
34
|
+
href?: never;
|
|
35
|
+
})
|
|
36
|
+
| (ButtonOwnProps &
|
|
37
|
+
Omit<HTMLAnchorAttributes, 'children' | 'type' | 'href'> & {
|
|
38
|
+
href: HTMLAnchorAttributes['href'];
|
|
39
|
+
type?: never;
|
|
40
|
+
disabled?: HTMLButtonAttributes['disabled'];
|
|
41
|
+
});
|
|
21
42
|
</script>
|
|
22
43
|
|
|
23
44
|
<script lang="ts">
|
|
@@ -27,30 +48,56 @@
|
|
|
27
48
|
icon = false,
|
|
28
49
|
class: className,
|
|
29
50
|
ref = $bindable(null),
|
|
51
|
+
href,
|
|
52
|
+
disabled,
|
|
30
53
|
children,
|
|
31
54
|
...rest
|
|
32
55
|
}: ButtonProps = $props();
|
|
56
|
+
|
|
57
|
+
// `rest` holds one element's attributes; each branch reads it as its own.
|
|
58
|
+
const asButton = (attributes: typeof rest) => attributes as HTMLButtonAttributes;
|
|
59
|
+
const asLink = (attributes: typeof rest) => attributes as HTMLAnchorAttributes;
|
|
33
60
|
</script>
|
|
34
61
|
|
|
35
62
|
<!--
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
tabindex -1; a disabled
|
|
63
|
+
A plain <button> without `href`, an <a> with it, with no component layer
|
|
64
|
+
between: a transcript may draw hundreds. A disabled link renders without
|
|
65
|
+
href, with role="link", aria-disabled="true" and tabindex -1; a disabled
|
|
66
|
+
<button> is not focusable.
|
|
39
67
|
-->
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
68
|
+
{#if !href}
|
|
69
|
+
<button
|
|
70
|
+
bind:this={ref}
|
|
71
|
+
class={['hp-button', 'hp-label', className]}
|
|
72
|
+
data-variant={variant}
|
|
73
|
+
data-opens={opens || undefined}
|
|
74
|
+
data-icon={icon || undefined}
|
|
75
|
+
{disabled}
|
|
76
|
+
{...asButton(rest)}
|
|
77
|
+
>
|
|
78
|
+
{@render children?.()}
|
|
79
|
+
</button>
|
|
80
|
+
{:else}
|
|
81
|
+
<a
|
|
82
|
+
bind:this={ref}
|
|
83
|
+
class={['hp-button', 'hp-label', className]}
|
|
84
|
+
data-variant={variant}
|
|
85
|
+
data-opens={opens || undefined}
|
|
86
|
+
data-icon={icon || undefined}
|
|
87
|
+
href={disabled ? undefined : href}
|
|
88
|
+
role={disabled ? 'link' : undefined}
|
|
89
|
+
aria-disabled={disabled ? 'true' : undefined}
|
|
90
|
+
tabindex={disabled ? -1 : undefined}
|
|
91
|
+
{...asLink(rest)}
|
|
92
|
+
>
|
|
93
|
+
{@render children?.()}
|
|
94
|
+
</a>
|
|
95
|
+
{/if}
|
|
50
96
|
|
|
51
97
|
<style>
|
|
52
|
-
/*
|
|
53
|
-
|
|
98
|
+
/* Global rules keyed on .hp-button and its data-*, at one class of
|
|
99
|
+
specificity as when Bits UI rendered the element, so an app's rules
|
|
100
|
+
that override them still apply. */
|
|
54
101
|
:global(.hp-button) {
|
|
55
102
|
display: inline-flex;
|
|
56
103
|
align-items: center;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
|
|
2
3
|
export type ButtonVariant = 'primary' | 'secondary' | 'tertiary';
|
|
3
|
-
|
|
4
|
+
type ButtonOwnProps = {
|
|
4
5
|
/**
|
|
5
6
|
* `primary`: solid titanium block, for the view's one group of solid
|
|
6
7
|
* blocks. `secondary` (default): outlined. `tertiary`: caps label only.
|
|
@@ -14,7 +15,22 @@ export type ButtonProps = BitsButton.RootProps & {
|
|
|
14
15
|
* `aria-label`.
|
|
15
16
|
*/
|
|
16
17
|
icon?: boolean;
|
|
18
|
+
/** The `<button>`, or the `<a>` when `href` is set. */
|
|
19
|
+
ref?: HTMLElement | null;
|
|
20
|
+
children?: Snippet;
|
|
17
21
|
};
|
|
22
|
+
/**
|
|
23
|
+
* An `<a>` when `href` is set, a `<button>` otherwise. Attributes and
|
|
24
|
+
* handlers not listed here go to the element.
|
|
25
|
+
*/
|
|
26
|
+
export type ButtonProps = (ButtonOwnProps & Omit<HTMLButtonAttributes, 'children' | 'type' | 'href'> & {
|
|
27
|
+
type?: HTMLButtonAttributes['type'];
|
|
28
|
+
href?: never;
|
|
29
|
+
}) | (ButtonOwnProps & Omit<HTMLAnchorAttributes, 'children' | 'type' | 'href'> & {
|
|
30
|
+
href: HTMLAnchorAttributes['href'];
|
|
31
|
+
type?: never;
|
|
32
|
+
disabled?: HTMLButtonAttributes['disabled'];
|
|
33
|
+
});
|
|
18
34
|
declare const Button: import("svelte").Component<ButtonProps, {}, "ref">;
|
|
19
35
|
type Button = ReturnType<typeof Button>;
|
|
20
36
|
export default Button;
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
</script>
|
|
38
38
|
|
|
39
39
|
<script lang="ts">
|
|
40
|
+
import { tick } from 'svelte';
|
|
40
41
|
import { DropdownMenu } from 'bits-ui';
|
|
41
42
|
import { MediaQuery } from 'svelte/reactivity';
|
|
42
43
|
import { pop } from '../motion/index.js';
|
|
@@ -99,6 +100,20 @@
|
|
|
99
100
|
let sheetNode = $state<HTMLElement | null>(null);
|
|
100
101
|
let triggerNode = $state<HTMLElement | null>(null);
|
|
101
102
|
|
|
103
|
+
// Bits UI's content layers (the portal, positioning, focus and dismiss
|
|
104
|
+
// handling) cost about 0.3 ms a menu, and an app may draw several closed
|
|
105
|
+
// menus per view, so they mount on the first open, not with the
|
|
106
|
+
// trigger. Until then only the trigger and its ARIA exist. They mount
|
|
107
|
+
// closed, and Bits UI sees the menu open one update later, so the first
|
|
108
|
+
// entrance plays as every later one does. They stay mounted after that.
|
|
109
|
+
let layers = $state(false);
|
|
110
|
+
let ready = $state(false);
|
|
111
|
+
$effect(() => {
|
|
112
|
+
if (!open || layers) return;
|
|
113
|
+
layers = true;
|
|
114
|
+
tick().then(() => (ready = true));
|
|
115
|
+
});
|
|
116
|
+
|
|
102
117
|
function close() {
|
|
103
118
|
open = false;
|
|
104
119
|
}
|
|
@@ -150,7 +165,7 @@
|
|
|
150
165
|
it and return focus to the trigger. From 600px a press outside closes it
|
|
151
166
|
and leaves focus where the press put it; Tab closes it and moves on.
|
|
152
167
|
-->
|
|
153
|
-
<DropdownMenu.Root bind:open>
|
|
168
|
+
<DropdownMenu.Root bind:open={() => open && ready, (value) => (open = value)}>
|
|
154
169
|
<DropdownMenu.Trigger
|
|
155
170
|
bind:ref={triggerNode}
|
|
156
171
|
onkeydowncapture={() => (keyboard = true)}
|
|
@@ -161,53 +176,55 @@
|
|
|
161
176
|
{/snippet}
|
|
162
177
|
</DropdownMenu.Trigger>
|
|
163
178
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
{#
|
|
169
|
-
|
|
179
|
+
{#if layers}
|
|
180
|
+
<DropdownMenu.Portal>
|
|
181
|
+
{#if wide.current}
|
|
182
|
+
<DropdownMenu.Content forceMount bind:ref {...placement} {onInteractOutside} {onCloseAutoFocus} {...rest}>
|
|
183
|
+
{#snippet child({ wrapperProps, props, open: shown })}
|
|
184
|
+
{#if shown}
|
|
185
|
+
<div {...wrapperProps}>
|
|
186
|
+
<div
|
|
187
|
+
{...props}
|
|
188
|
+
class={['hp-menu', className]}
|
|
189
|
+
data-form="popover"
|
|
190
|
+
data-keyboard={keyboard || undefined}
|
|
191
|
+
aria-label={label}
|
|
192
|
+
transition:pop
|
|
193
|
+
{@attach trackInput}
|
|
194
|
+
>
|
|
195
|
+
{@render children()}
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
{/if}
|
|
199
|
+
{/snippet}
|
|
200
|
+
</DropdownMenu.Content>
|
|
201
|
+
{:else}
|
|
202
|
+
<DropdownMenu.ContentStatic
|
|
203
|
+
forceMount
|
|
204
|
+
bind:ref
|
|
205
|
+
{...rest}
|
|
206
|
+
preventScroll
|
|
207
|
+
onInteractOutside={insideSheet}
|
|
208
|
+
onCloseAutoFocus={returnFocus}
|
|
209
|
+
>
|
|
210
|
+
{#snippet child({ props, open: shown })}
|
|
211
|
+
<Sheet open={shown} layer="popover" title={label} {closeLabel} onclose={close} bind:ref={sheetNode}>
|
|
170
212
|
<div
|
|
171
213
|
{...props}
|
|
172
214
|
class={['hp-menu', className]}
|
|
173
|
-
data-form="
|
|
215
|
+
data-form="sheet"
|
|
174
216
|
data-keyboard={keyboard || undefined}
|
|
175
217
|
aria-label={label}
|
|
176
|
-
transition:pop
|
|
177
218
|
{@attach trackInput}
|
|
178
219
|
>
|
|
179
220
|
{@render children()}
|
|
180
221
|
</div>
|
|
181
|
-
</
|
|
182
|
-
{/
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
forceMount
|
|
188
|
-
bind:ref
|
|
189
|
-
{...rest}
|
|
190
|
-
preventScroll
|
|
191
|
-
onInteractOutside={insideSheet}
|
|
192
|
-
onCloseAutoFocus={returnFocus}
|
|
193
|
-
>
|
|
194
|
-
{#snippet child({ props, open: shown })}
|
|
195
|
-
<Sheet open={shown} layer="popover" title={label} {closeLabel} onclose={close} bind:ref={sheetNode}>
|
|
196
|
-
<div
|
|
197
|
-
{...props}
|
|
198
|
-
class={['hp-menu', className]}
|
|
199
|
-
data-form="sheet"
|
|
200
|
-
data-keyboard={keyboard || undefined}
|
|
201
|
-
aria-label={label}
|
|
202
|
-
{@attach trackInput}
|
|
203
|
-
>
|
|
204
|
-
{@render children()}
|
|
205
|
-
</div>
|
|
206
|
-
</Sheet>
|
|
207
|
-
{/snippet}
|
|
208
|
-
</DropdownMenu.ContentStatic>
|
|
209
|
-
{/if}
|
|
210
|
-
</DropdownMenu.Portal>
|
|
222
|
+
</Sheet>
|
|
223
|
+
{/snippet}
|
|
224
|
+
</DropdownMenu.ContentStatic>
|
|
225
|
+
{/if}
|
|
226
|
+
</DropdownMenu.Portal>
|
|
227
|
+
{/if}
|
|
211
228
|
</DropdownMenu.Root>
|
|
212
229
|
|
|
213
230
|
<style>
|
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
|
|
10
10
|
/** Attributes and handlers not listed here go to the `<input>`. */
|
|
11
11
|
export type TextFieldProps = Omit<HTMLInputAttributes, 'type' | 'value' | 'children'> & {
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* The caps field label above the well. Required, also with
|
|
14
|
+
* `labelHidden`: a placeholder is never the only label.
|
|
15
|
+
*/
|
|
13
16
|
label: string;
|
|
14
17
|
/** The text. Bindable. */
|
|
15
18
|
value?: string;
|
|
@@ -28,6 +31,18 @@
|
|
|
28
31
|
* The hint and the error stay under the well.
|
|
29
32
|
*/
|
|
30
33
|
labelBeside?: boolean | TextFieldBreakpoint;
|
|
34
|
+
/**
|
|
35
|
+
* Hides the label from sight; it stays the field's accessible name.
|
|
36
|
+
* For a field whose purpose its place makes plain, such as a search in
|
|
37
|
+
* a toolbar. The well keeps its look and its 16 px text. It overrides
|
|
38
|
+
* `labelBeside`.
|
|
39
|
+
*/
|
|
40
|
+
labelHidden?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* The accessible name of the clear button that `type="search"` shows
|
|
43
|
+
* inside the well while it holds text.
|
|
44
|
+
*/
|
|
45
|
+
clearLabel?: string;
|
|
31
46
|
/** The `<input>`, for example to focus the first error on submit. */
|
|
32
47
|
ref?: HTMLInputElement | null;
|
|
33
48
|
};
|
|
@@ -47,8 +62,11 @@
|
|
|
47
62
|
hint,
|
|
48
63
|
error,
|
|
49
64
|
labelBeside = false,
|
|
65
|
+
labelHidden = false,
|
|
66
|
+
clearLabel = 'Clear',
|
|
50
67
|
id = uid,
|
|
51
68
|
disabled,
|
|
69
|
+
readonly,
|
|
52
70
|
ref = $bindable(null),
|
|
53
71
|
class: className,
|
|
54
72
|
'aria-describedby': describedBy,
|
|
@@ -82,37 +100,87 @@
|
|
|
82
100
|
if (message) fadeIn(message, '--dur-message');
|
|
83
101
|
});
|
|
84
102
|
});
|
|
103
|
+
|
|
104
|
+
// ---- Search: the clear button --------------------------------------
|
|
105
|
+
|
|
106
|
+
// The browser's own cancel button is hidden (CSS below) and this one
|
|
107
|
+
// takes its place, so every engine draws the same control. It writes
|
|
108
|
+
// the empty value and fires a bubbling `input`, as typing would, so
|
|
109
|
+
// `bind:value` and the app's `oninput` both hear it, then focus returns
|
|
110
|
+
// to the field. A mouse or a touch press never takes focus from the
|
|
111
|
+
// field, so an on-screen keyboard stays up; keys reach the button by Tab.
|
|
112
|
+
const clearable = $derived(type === 'search');
|
|
113
|
+
const showClear = $derived(clearable && !!value && !disabled && !readonly);
|
|
114
|
+
|
|
115
|
+
function clear() {
|
|
116
|
+
if (!ref) return;
|
|
117
|
+
ref.value = '';
|
|
118
|
+
ref.dispatchEvent(new Event('input', { bubbles: true }));
|
|
119
|
+
ref.focus();
|
|
120
|
+
}
|
|
85
121
|
</script>
|
|
86
122
|
|
|
87
123
|
<!--
|
|
88
124
|
A single-line text input in the black well, with a plain caps label above
|
|
89
125
|
it (Bits UI Label), an optional hint and an inline error below. Extra
|
|
90
126
|
attributes (`name`, `inputmode`, `autocomplete`, `enterkeyhint`,
|
|
91
|
-
`required`), handlers and `class` go to the <input>.
|
|
127
|
+
`required`), handlers and `class` go to the <input>. With
|
|
128
|
+
`type="search"` the well holds a clear button at its end while it has
|
|
129
|
+
text.
|
|
92
130
|
-->
|
|
93
131
|
<div
|
|
94
132
|
bind:this={root}
|
|
95
133
|
class="hp-text-field"
|
|
96
134
|
data-invalid={error ? '' : undefined}
|
|
97
135
|
data-disabled={disabled ? '' : undefined}
|
|
98
|
-
data-label-beside={labelBeside === true ? 'always' : labelBeside || undefined}
|
|
136
|
+
data-label-beside={labelHidden ? undefined : labelBeside === true ? 'always' : labelBeside || undefined}
|
|
99
137
|
>
|
|
100
138
|
<Label.Root for={id}>
|
|
101
139
|
{#snippet child({ props })}
|
|
102
|
-
<label {...props} class=
|
|
140
|
+
<label {...props} class={['hp-label', labelHidden && 'hp-visually-hidden']}>{label}</label>
|
|
103
141
|
{/snippet}
|
|
104
142
|
</Label.Root>
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
143
|
+
{#snippet well()}
|
|
144
|
+
<input
|
|
145
|
+
{...rest}
|
|
146
|
+
bind:this={ref}
|
|
147
|
+
bind:value
|
|
148
|
+
{id}
|
|
149
|
+
{type}
|
|
150
|
+
{disabled}
|
|
151
|
+
{readonly}
|
|
152
|
+
class={['hp-well', className]}
|
|
153
|
+
aria-describedby={describedByIds}
|
|
154
|
+
aria-invalid={error ? true : ariaInvalid}
|
|
155
|
+
/>
|
|
156
|
+
{/snippet}
|
|
157
|
+
{#if clearable}
|
|
158
|
+
<div class="search">
|
|
159
|
+
{@render well()}
|
|
160
|
+
{#if showClear}
|
|
161
|
+
<button
|
|
162
|
+
type="button"
|
|
163
|
+
class="clear hp-focus-inset"
|
|
164
|
+
aria-label={clearLabel}
|
|
165
|
+
onmousedown={(e) => e.preventDefault()}
|
|
166
|
+
onclick={clear}
|
|
167
|
+
>
|
|
168
|
+
<svg
|
|
169
|
+
viewBox="0 0 16 16"
|
|
170
|
+
fill="none"
|
|
171
|
+
stroke="currentColor"
|
|
172
|
+
stroke-width="1.5"
|
|
173
|
+
stroke-linecap="square"
|
|
174
|
+
aria-hidden="true"
|
|
175
|
+
>
|
|
176
|
+
<path d="M4 4l8 8M12 4l-8 8" />
|
|
177
|
+
</svg>
|
|
178
|
+
</button>
|
|
179
|
+
{/if}
|
|
180
|
+
</div>
|
|
181
|
+
{:else}
|
|
182
|
+
{@render well()}
|
|
183
|
+
{/if}
|
|
116
184
|
{#if hint}
|
|
117
185
|
<p class="hint hp-caption" id={hintId}>{hint}</p>
|
|
118
186
|
{/if}
|
|
@@ -148,6 +216,72 @@
|
|
|
148
216
|
opacity: 1;
|
|
149
217
|
}
|
|
150
218
|
|
|
219
|
+
/* ---- Search: the clear button replaces the browser's own --------- */
|
|
220
|
+
|
|
221
|
+
/* WebKit and Blink draw a cancel button in a search input whatever its
|
|
222
|
+
appearance; Chrome on Android draws it bright blue. */
|
|
223
|
+
input::-webkit-search-cancel-button,
|
|
224
|
+
input::-webkit-search-decoration {
|
|
225
|
+
appearance: none;
|
|
226
|
+
display: none;
|
|
227
|
+
}
|
|
228
|
+
.search {
|
|
229
|
+
position: relative;
|
|
230
|
+
min-width: 0;
|
|
231
|
+
}
|
|
232
|
+
/* The text stops short of the button's square, whether it shows or not,
|
|
233
|
+
so the text never moves when it appears. */
|
|
234
|
+
.search > input {
|
|
235
|
+
padding-right: var(--control-height);
|
|
236
|
+
}
|
|
237
|
+
/* A --control-height square over the end of the well: 36px on fine
|
|
238
|
+
pointers, 44px on coarse. Its transparent border keeps a press fill
|
|
239
|
+
inside the well's edge. The cross is smaller than a close button's,
|
|
240
|
+
in --muted like the placeholder, so it reads as part of the well. */
|
|
241
|
+
.clear {
|
|
242
|
+
position: absolute;
|
|
243
|
+
top: 0;
|
|
244
|
+
right: 0;
|
|
245
|
+
display: grid;
|
|
246
|
+
place-items: center;
|
|
247
|
+
box-sizing: border-box;
|
|
248
|
+
width: var(--control-height);
|
|
249
|
+
height: var(--control-height);
|
|
250
|
+
padding: 0;
|
|
251
|
+
border: 1px solid transparent;
|
|
252
|
+
background: transparent;
|
|
253
|
+
background-clip: padding-box;
|
|
254
|
+
color: var(--muted);
|
|
255
|
+
cursor: pointer;
|
|
256
|
+
-webkit-tap-highlight-color: transparent;
|
|
257
|
+
}
|
|
258
|
+
.clear svg {
|
|
259
|
+
display: block;
|
|
260
|
+
width: var(--space-4);
|
|
261
|
+
height: var(--space-4);
|
|
262
|
+
}
|
|
263
|
+
.clear:active {
|
|
264
|
+
background-color: var(--surface-raised);
|
|
265
|
+
color: var(--text);
|
|
266
|
+
}
|
|
267
|
+
@media (hover: hover) and (pointer: fine) {
|
|
268
|
+
.clear {
|
|
269
|
+
transition:
|
|
270
|
+
background-color var(--dur-hover) var(--settle),
|
|
271
|
+
color var(--dur-hover) var(--settle);
|
|
272
|
+
}
|
|
273
|
+
.clear:hover {
|
|
274
|
+
color: var(--text);
|
|
275
|
+
}
|
|
276
|
+
.clear:active {
|
|
277
|
+
transition-duration: var(--dur-press);
|
|
278
|
+
}
|
|
279
|
+
/* The button is part of the well, so the well keeps its hover fill. */
|
|
280
|
+
.search:has(> .clear:hover) > input {
|
|
281
|
+
background-color: var(--surface-hover);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
151
285
|
/* Invalid: the boundary turns --danger (5.9:1 on black), never alone:
|
|
152
286
|
the icon and the message say what is wrong. Keyed on the root's
|
|
153
287
|
data-invalid, set by `error`, so an aria-invalid the app passes
|
|
@@ -178,7 +312,9 @@
|
|
|
178
312
|
/* The first message starts 8px under the well, clear of the focus
|
|
179
313
|
contour (2px, drawn 4px outside the well). */
|
|
180
314
|
input + .hint,
|
|
181
|
-
input + .error
|
|
315
|
+
input + .error,
|
|
316
|
+
.search + .hint,
|
|
317
|
+
.search + .error {
|
|
182
318
|
margin-top: var(--space-1);
|
|
183
319
|
}
|
|
184
320
|
/* One line box high, so the icon centres on the first line of the
|
|
@@ -205,6 +341,7 @@
|
|
|
205
341
|
white-space: nowrap;
|
|
206
342
|
}
|
|
207
343
|
[data-label-beside='always'] input,
|
|
344
|
+
[data-label-beside='always'] .search,
|
|
208
345
|
[data-label-beside='always'] p {
|
|
209
346
|
grid-column: 2;
|
|
210
347
|
}
|
|
@@ -219,6 +356,7 @@
|
|
|
219
356
|
white-space: nowrap;
|
|
220
357
|
}
|
|
221
358
|
[data-label-beside='medium'] input,
|
|
359
|
+
[data-label-beside='medium'] .search,
|
|
222
360
|
[data-label-beside='medium'] p {
|
|
223
361
|
grid-column: 2;
|
|
224
362
|
}
|
|
@@ -234,6 +372,7 @@
|
|
|
234
372
|
white-space: nowrap;
|
|
235
373
|
}
|
|
236
374
|
[data-label-beside='expanded'] input,
|
|
375
|
+
[data-label-beside='expanded'] .search,
|
|
237
376
|
[data-label-beside='expanded'] p {
|
|
238
377
|
grid-column: 2;
|
|
239
378
|
}
|
|
@@ -249,6 +388,7 @@
|
|
|
249
388
|
white-space: nowrap;
|
|
250
389
|
}
|
|
251
390
|
[data-label-beside='large'] input,
|
|
391
|
+
[data-label-beside='large'] .search,
|
|
252
392
|
[data-label-beside='large'] p {
|
|
253
393
|
grid-column: 2;
|
|
254
394
|
}
|
|
@@ -5,7 +5,10 @@ export type TextFieldType = 'text' | 'email' | 'password' | 'search' | 'tel' | '
|
|
|
5
5
|
export type TextFieldBreakpoint = 'medium' | 'expanded' | 'large';
|
|
6
6
|
/** Attributes and handlers not listed here go to the `<input>`. */
|
|
7
7
|
export type TextFieldProps = Omit<HTMLInputAttributes, 'type' | 'value' | 'children'> & {
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* The caps field label above the well. Required, also with
|
|
10
|
+
* `labelHidden`: a placeholder is never the only label.
|
|
11
|
+
*/
|
|
9
12
|
label: string;
|
|
10
13
|
/** The text. Bindable. */
|
|
11
14
|
value?: string;
|
|
@@ -24,6 +27,18 @@ export type TextFieldProps = Omit<HTMLInputAttributes, 'type' | 'value' | 'child
|
|
|
24
27
|
* The hint and the error stay under the well.
|
|
25
28
|
*/
|
|
26
29
|
labelBeside?: boolean | TextFieldBreakpoint;
|
|
30
|
+
/**
|
|
31
|
+
* Hides the label from sight; it stays the field's accessible name.
|
|
32
|
+
* For a field whose purpose its place makes plain, such as a search in
|
|
33
|
+
* a toolbar. The well keeps its look and its 16 px text. It overrides
|
|
34
|
+
* `labelBeside`.
|
|
35
|
+
*/
|
|
36
|
+
labelHidden?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* The accessible name of the clear button that `type="search"` shows
|
|
39
|
+
* inside the well while it holds text.
|
|
40
|
+
*/
|
|
41
|
+
clearLabel?: string;
|
|
27
42
|
/** The `<input>`, for example to focus the first error on submit. */
|
|
28
43
|
ref?: HTMLInputElement | null;
|
|
29
44
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mystravil/hardpoint",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Hardpoint: a Svelte 5 design system on Bits UI. AMOLED black, titanium ink, Geist, square solid blocks, banded mono tables, opt-in expressive motion, dark and light themes.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "MYSTRAVIL",
|