@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 { Button as BitsButton } from 'bits-ui';
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
- export type ButtonProps = BitsButton.RootProps & {
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
- An <a> when `href` is set, a <button> otherwise (Bits UI Button.Root). A
37
- disabled link renders without href, with aria-disabled="true" and
38
- tabindex -1; a disabled <button> is not focusable.
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
- <BitsButton.Root
41
- bind:ref
42
- class={['hp-button', 'hp-label', className]}
43
- data-variant={variant}
44
- data-opens={opens || undefined}
45
- data-icon={icon || undefined}
46
- {...rest}
47
- >
48
- {@render children?.()}
49
- </BitsButton.Root>
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
- /* Bits UI renders the element, so the root class reaches it from the
53
- outside: the rules are :global, keyed on .hp-button and its data-*. */
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 { Button as BitsButton } from 'bits-ui';
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
2
3
  export type ButtonVariant = 'primary' | 'secondary' | 'tertiary';
3
- export type ButtonProps = BitsButton.RootProps & {
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
- <DropdownMenu.Portal>
165
- {#if wide.current}
166
- <DropdownMenu.Content forceMount bind:ref {...placement} {onInteractOutside} {onCloseAutoFocus} {...rest}>
167
- {#snippet child({ wrapperProps, props, open: shown })}
168
- {#if shown}
169
- <div {...wrapperProps}>
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="popover"
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
- </div>
182
- {/if}
183
- {/snippet}
184
- </DropdownMenu.Content>
185
- {:else}
186
- <DropdownMenu.ContentStatic
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
- /** The caps field label above the well. Required: a placeholder is never the only label. */
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="hp-label">{label}</label>
140
+ <label {...props} class={['hp-label', labelHidden && 'hp-visually-hidden']}>{label}</label>
103
141
  {/snippet}
104
142
  </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
- />
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
- /** The caps field label above the well. Required: a placeholder is never the only label. */
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.2.1",
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",