@adea-ai/ui 0.85.0 → 0.88.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/README.md +17 -1
- package/dist/NOTICE +34 -0
- package/dist/components/composites/action-button/action-button.d.ts.map +1 -1
- package/dist/components/composites/action-button/action-button.js +2 -2
- package/dist/components/composites/action-button/action-button.js.map +1 -1
- package/dist/components/composites/list-row/list-row.d.ts +6 -3
- package/dist/components/composites/list-row/list-row.d.ts.map +1 -1
- package/dist/components/composites/list-row/list-row.js +69 -7
- package/dist/components/composites/list-row/list-row.js.map +1 -1
- package/dist/components/composites/settings/settings.d.ts +9 -11
- package/dist/components/composites/settings/settings.d.ts.map +1 -1
- package/dist/components/composites/settings/settings.js +48 -33
- package/dist/components/composites/settings/settings.js.map +1 -1
- package/dist/components/conversation/message-composer.js +1 -1
- package/dist/components/conversation/scroll-follow.d.ts.map +1 -1
- package/dist/components/conversation/scroll-follow.js +95 -15
- package/dist/components/conversation/scroll-follow.js.map +1 -1
- package/dist/components/layout/app-shell/index.d.ts +1 -0
- package/dist/components/layout/app-shell/index.d.ts.map +1 -1
- package/dist/components/layout/app-shell/index.js +2 -1
- package/dist/components/layout/app-shell/skip-link.d.ts +11 -0
- package/dist/components/layout/app-shell/skip-link.d.ts.map +1 -0
- package/dist/components/layout/app-shell/skip-link.js +44 -0
- package/dist/components/layout/app-shell/skip-link.js.map +1 -0
- package/dist/components/layout/floating-preview/floating-preview.d.ts +12 -0
- package/dist/components/layout/floating-preview/floating-preview.d.ts.map +1 -0
- package/dist/components/layout/floating-preview/floating-preview.js +283 -0
- package/dist/components/layout/floating-preview/floating-preview.js.map +1 -0
- package/dist/components/layout/floating-preview/index.d.ts +3 -0
- package/dist/components/layout/floating-preview/index.d.ts.map +1 -0
- package/dist/components/layout/floating-preview/index.js +3 -0
- package/dist/components/layout/floating-preview/model.d.ts +74 -0
- package/dist/components/layout/floating-preview/model.d.ts.map +1 -0
- package/dist/components/layout/floating-preview/model.js +146 -0
- package/dist/components/layout/floating-preview/model.js.map +1 -0
- package/dist/components/layout/sidebar-nav/index.d.ts +1 -1
- package/dist/components/layout/sidebar-nav/index.d.ts.map +1 -1
- package/dist/components/layout/sidebar-nav/sidebar-nav.d.ts +18 -2
- package/dist/components/layout/sidebar-nav/sidebar-nav.d.ts.map +1 -1
- package/dist/components/layout/sidebar-nav/sidebar-nav.js +42 -11
- package/dist/components/layout/sidebar-nav/sidebar-nav.js.map +1 -1
- package/dist/components/theme/index.d.ts +1 -0
- package/dist/components/theme/index.d.ts.map +1 -1
- package/dist/components/theme/index.js +2 -1
- package/dist/components/theme/theme-mode-toggle.d.ts +10 -0
- package/dist/components/theme/theme-mode-toggle.d.ts.map +1 -0
- package/dist/components/theme/theme-mode-toggle.js +82 -0
- package/dist/components/theme/theme-mode-toggle.js.map +1 -0
- package/dist/components/ui/checkbox/checkbox.d.ts +2 -0
- package/dist/components/ui/checkbox/checkbox.d.ts.map +1 -1
- package/dist/components/ui/checkbox/checkbox.js +24 -3
- package/dist/components/ui/checkbox/checkbox.js.map +1 -1
- package/dist/components/ui/combobox/combobox.d.ts.map +1 -1
- package/dist/components/ui/combobox/combobox.js +41 -4
- package/dist/components/ui/combobox/combobox.js.map +1 -1
- package/dist/components/ui/combobox/index.d.ts +1 -0
- package/dist/components/ui/combobox/index.d.ts.map +1 -1
- package/dist/components/ui/combobox/index.js +2 -1
- package/dist/components/ui/combobox/value-combobox.d.ts +19 -0
- package/dist/components/ui/combobox/value-combobox.d.ts.map +1 -0
- package/dist/components/ui/combobox/value-combobox.js +42 -0
- package/dist/components/ui/combobox/value-combobox.js.map +1 -0
- package/dist/components/ui/empty/empty-state.d.ts +15 -0
- package/dist/components/ui/empty/empty-state.d.ts.map +1 -0
- package/dist/components/ui/empty/empty-state.js +96 -0
- package/dist/components/ui/empty/empty-state.js.map +1 -0
- package/dist/components/ui/empty/index.d.ts +1 -0
- package/dist/components/ui/empty/index.d.ts.map +1 -1
- package/dist/components/ui/empty/index.js +2 -1
- package/dist/components/ui/entity-icon/entity-icon.d.ts +4 -1
- package/dist/components/ui/entity-icon/entity-icon.d.ts.map +1 -1
- package/dist/components/ui/entity-icon/entity-icon.js +37 -13
- package/dist/components/ui/entity-icon/entity-icon.js.map +1 -1
- package/dist/components/ui/field/field.d.ts +36 -13
- package/dist/components/ui/field/field.d.ts.map +1 -1
- package/dist/components/ui/field/field.js +146 -24
- package/dist/components/ui/field/field.js.map +1 -1
- package/dist/components/ui/field/index.d.ts +3 -1
- package/dist/components/ui/field/index.d.ts.map +1 -1
- package/dist/components/ui/field/index.js +3 -2
- package/dist/components/ui/field/number-field.d.ts +19 -0
- package/dist/components/ui/field/number-field.d.ts.map +1 -0
- package/dist/components/ui/field/number-field.js +76 -0
- package/dist/components/ui/field/number-field.js.map +1 -0
- package/dist/components/ui/input/index.d.ts +1 -0
- package/dist/components/ui/input/index.d.ts.map +1 -1
- package/dist/components/ui/input/index.js +2 -1
- package/dist/components/ui/input/input-control.d.ts +11 -0
- package/dist/components/ui/input/input-control.d.ts.map +1 -0
- package/dist/components/ui/input/input-control.js +24 -0
- package/dist/components/ui/input/input-control.js.map +1 -0
- package/dist/components/ui/input/input.d.ts.map +1 -1
- package/dist/components/ui/input/input.js +56 -28
- package/dist/components/ui/input/input.js.map +1 -1
- package/dist/components/ui/input-group/index.d.ts +1 -0
- package/dist/components/ui/input-group/index.d.ts.map +1 -1
- package/dist/components/ui/input-group/index.js +2 -1
- package/dist/components/ui/input-group/input-group.d.ts.map +1 -1
- package/dist/components/ui/input-group/input-group.js +42 -5
- package/dist/components/ui/input-group/input-group.js.map +1 -1
- package/dist/components/ui/input-group/secret-input-group.d.ts +11 -0
- package/dist/components/ui/input-group/secret-input-group.d.ts.map +1 -0
- package/dist/components/ui/input-group/secret-input-group.js +67 -0
- package/dist/components/ui/input-group/secret-input-group.js.map +1 -0
- package/dist/components/ui/native-select/native-select.d.ts.map +1 -1
- package/dist/components/ui/native-select/native-select.js +39 -4
- package/dist/components/ui/native-select/native-select.js.map +1 -1
- package/dist/components/ui/progress/progress.d.ts.map +1 -1
- package/dist/components/ui/progress/progress.js +30 -18
- package/dist/components/ui/progress/progress.js.map +1 -1
- package/dist/components/ui/radio-group/radio-group.d.ts.map +1 -1
- package/dist/components/ui/radio-group/radio-group.js +41 -4
- package/dist/components/ui/radio-group/radio-group.js.map +1 -1
- package/dist/components/ui/switch/switch.d.ts +2 -0
- package/dist/components/ui/switch/switch.d.ts.map +1 -1
- package/dist/components/ui/switch/switch.js +24 -3
- package/dist/components/ui/switch/switch.js.map +1 -1
- package/dist/components/ui/tabs/tabs.js +1 -1
- package/dist/components/ui/tabs/tabs.js.map +1 -1
- package/dist/components/ui/text-link/index.d.ts +2 -0
- package/dist/components/ui/text-link/index.d.ts.map +1 -0
- package/dist/components/ui/text-link/index.js +2 -0
- package/dist/components/ui/text-link/text-link.d.ts +9 -0
- package/dist/components/ui/text-link/text-link.d.ts.map +1 -0
- package/dist/components/ui/text-link/text-link.js +30 -0
- package/dist/components/ui/text-link/text-link.js.map +1 -0
- package/dist/components/ui/textarea/textarea.d.ts.map +1 -1
- package/dist/components/ui/textarea/textarea.js +41 -4
- package/dist/components/ui/textarea/textarea.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -7
- package/dist/lib/clipboard.d.ts +3 -0
- package/dist/lib/clipboard.d.ts.map +1 -0
- package/dist/lib/clipboard.js +35 -0
- package/dist/lib/clipboard.js.map +1 -0
- package/dist/lib/download.d.ts +3 -0
- package/dist/lib/download.d.ts.map +1 -0
- package/dist/lib/download.js +31 -0
- package/dist/lib/download.js.map +1 -0
- package/dist/lib/form-field.d.ts +37 -0
- package/dist/lib/form-field.d.ts.map +1 -0
- package/dist/lib/form-field.js +47 -0
- package/dist/lib/form-field.js.map +1 -0
- package/dist/r/app-shell.json +5 -0
- package/dist/r/combobox.json +5 -0
- package/dist/r/empty.json +10 -1
- package/dist/r/field.json +7 -0
- package/dist/r/floating-preview.json +30 -0
- package/dist/r/input-group.json +6 -0
- package/dist/r/input.json +5 -0
- package/dist/r/lib.json +15 -0
- package/dist/r/list-row.json +1 -0
- package/dist/r/registry.json +112 -1
- package/dist/r/src/components/composites/action-button/action-button.tsx +5 -1
- package/dist/r/src/components/composites/list-row/list-row.tsx +81 -8
- package/dist/r/src/components/composites/settings/settings.tsx +67 -37
- package/dist/r/src/components/conversation/scroll-follow.ts +109 -15
- package/dist/r/src/components/layout/app-shell/index.ts +1 -0
- package/dist/r/src/components/layout/app-shell/skip-link.tsx +35 -0
- package/dist/r/src/components/layout/floating-preview/floating-preview.tsx +260 -0
- package/dist/r/src/components/layout/floating-preview/index.ts +2 -0
- package/dist/r/src/components/layout/floating-preview/model.ts +285 -0
- package/dist/r/src/components/layout/sidebar-nav/index.ts +2 -0
- package/dist/r/src/components/layout/sidebar-nav/sidebar-nav.tsx +71 -16
- package/dist/r/src/components/theme/index.ts +1 -0
- package/dist/r/src/components/theme/theme-mode-toggle.tsx +56 -0
- package/dist/r/src/components/ui/checkbox/checkbox.tsx +20 -3
- package/dist/r/src/components/ui/combobox/combobox.tsx +24 -1
- package/dist/r/src/components/ui/combobox/index.ts +1 -0
- package/dist/r/src/components/ui/combobox/value-combobox.tsx +38 -0
- package/dist/r/src/components/ui/empty/empty-state.tsx +71 -0
- package/dist/r/src/components/ui/empty/index.ts +1 -0
- package/dist/r/src/components/ui/entity-icon/entity-icon.tsx +33 -2
- package/dist/r/src/components/ui/field/field.tsx +102 -5
- package/dist/r/src/components/ui/field/index.ts +5 -0
- package/dist/r/src/components/ui/field/number-field.tsx +71 -0
- package/dist/r/src/components/ui/input/index.ts +1 -0
- package/dist/r/src/components/ui/input/input-control.tsx +38 -0
- package/dist/r/src/components/ui/input/input.tsx +32 -20
- package/dist/r/src/components/ui/input-group/index.ts +1 -0
- package/dist/r/src/components/ui/input-group/input-group.tsx +25 -2
- package/dist/r/src/components/ui/input-group/secret-input-group.tsx +58 -0
- package/dist/r/src/components/ui/native-select/native-select.tsx +27 -1
- package/dist/r/src/components/ui/progress/progress.tsx +12 -8
- package/dist/r/src/components/ui/radio-group/radio-group.tsx +30 -2
- package/dist/r/src/components/ui/switch/switch.tsx +20 -3
- package/dist/r/src/components/ui/tabs/tabs.tsx +1 -1
- package/dist/r/src/components/ui/text-link/index.ts +2 -0
- package/dist/r/src/components/ui/text-link/text-link.tsx +28 -0
- package/dist/r/src/components/ui/textarea/textarea.tsx +24 -1
- package/dist/r/src/lib/clipboard.ts +41 -0
- package/dist/r/src/lib/download.ts +29 -0
- package/dist/r/src/lib/form-field.ts +93 -0
- package/dist/r/text-link.json +22 -0
- package/dist/r/theming.json +6 -0
- package/package.json +1 -1
- package/registry.json +112 -1
- package/src/components/composites/action-button/action-button.tsx +5 -1
- package/src/components/composites/list-row/list-row.tsx +81 -8
- package/src/components/composites/settings/settings.tsx +67 -37
- package/src/components/conversation/scroll-follow.ts +109 -15
- package/src/components/layout/app-shell/index.ts +1 -0
- package/src/components/layout/app-shell/skip-link.tsx +35 -0
- package/src/components/layout/floating-preview/floating-preview.tsx +260 -0
- package/src/components/layout/floating-preview/index.ts +2 -0
- package/src/components/layout/floating-preview/model.ts +285 -0
- package/src/components/layout/sidebar-nav/index.ts +2 -0
- package/src/components/layout/sidebar-nav/sidebar-nav.tsx +71 -16
- package/src/components/theme/index.ts +1 -0
- package/src/components/theme/theme-mode-toggle.tsx +56 -0
- package/src/components/ui/checkbox/checkbox.tsx +20 -3
- package/src/components/ui/combobox/combobox.tsx +24 -1
- package/src/components/ui/combobox/index.ts +1 -0
- package/src/components/ui/combobox/value-combobox.tsx +38 -0
- package/src/components/ui/empty/empty-state.tsx +71 -0
- package/src/components/ui/empty/index.ts +1 -0
- package/src/components/ui/entity-icon/entity-icon.tsx +33 -2
- package/src/components/ui/field/field.tsx +102 -5
- package/src/components/ui/field/index.ts +5 -0
- package/src/components/ui/field/number-field.tsx +71 -0
- package/src/components/ui/input/index.ts +1 -0
- package/src/components/ui/input/input-control.tsx +38 -0
- package/src/components/ui/input/input.tsx +32 -20
- package/src/components/ui/input-group/index.ts +1 -0
- package/src/components/ui/input-group/input-group.tsx +25 -2
- package/src/components/ui/input-group/secret-input-group.tsx +58 -0
- package/src/components/ui/native-select/native-select.tsx +27 -1
- package/src/components/ui/progress/progress.tsx +12 -8
- package/src/components/ui/radio-group/radio-group.tsx +30 -2
- package/src/components/ui/switch/switch.tsx +20 -3
- package/src/components/ui/tabs/tabs.tsx +1 -1
- package/src/components/ui/text-link/index.ts +2 -0
- package/src/components/ui/text-link/text-link.tsx +28 -0
- package/src/components/ui/textarea/textarea.tsx +24 -1
- package/src/index.ts +3 -0
- package/src/lib/clipboard.ts +41 -0
- package/src/lib/download.ts +29 -0
- package/src/lib/form-field.ts +93 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createEffect, createSignal, type JSX } from 'solid-js'
|
|
2
|
+
import { FormField } from './field'
|
|
3
|
+
import { Input } from '../input/input'
|
|
4
|
+
|
|
5
|
+
export type NumberFieldProps = {
|
|
6
|
+
label: string
|
|
7
|
+
hint?: string
|
|
8
|
+
value: number
|
|
9
|
+
min: number
|
|
10
|
+
max: number
|
|
11
|
+
integer?: boolean
|
|
12
|
+
onChange: (value: number) => void
|
|
13
|
+
class?: string
|
|
14
|
+
onBlur?: JSX.EventHandler<HTMLInputElement, FocusEvent>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A controlled numeric field that keeps an editable draft while validating
|
|
19
|
+
* range and integer constraints. Invalid drafts are announced and restored
|
|
20
|
+
* to the latest controlled value when focus leaves the input.
|
|
21
|
+
*/
|
|
22
|
+
export function NumberField(props: NumberFieldProps) {
|
|
23
|
+
const [draft, setDraft] = createSignal(String(props.value))
|
|
24
|
+
const [error, setError] = createSignal('')
|
|
25
|
+
const integer = () => props.integer ?? true
|
|
26
|
+
|
|
27
|
+
createEffect(() => {
|
|
28
|
+
setDraft(String(props.value))
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const validate = (raw: string) => {
|
|
32
|
+
if (!raw) return `${props.label} is required.`
|
|
33
|
+
const next = Number(raw)
|
|
34
|
+
if (!Number.isFinite(next)) return `${props.label} must be a valid number.`
|
|
35
|
+
if (integer() && !Number.isInteger(next)) {
|
|
36
|
+
return `${props.label} must be a whole number.`
|
|
37
|
+
}
|
|
38
|
+
if (next < props.min || next > props.max) {
|
|
39
|
+
return `${props.label} must be between ${props.min} and ${props.max}.`
|
|
40
|
+
}
|
|
41
|
+
return ''
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<FormField label={props.label} hint={props.hint} error={error()} class={props.class}>
|
|
46
|
+
<Input
|
|
47
|
+
type="number"
|
|
48
|
+
value={draft()}
|
|
49
|
+
min={props.min}
|
|
50
|
+
max={props.max}
|
|
51
|
+
step={integer() ? 1 : 'any'}
|
|
52
|
+
required
|
|
53
|
+
onInput={(event) => {
|
|
54
|
+
const raw = event.currentTarget.value
|
|
55
|
+
setDraft(raw)
|
|
56
|
+
const nextError = validate(raw)
|
|
57
|
+
setError(nextError)
|
|
58
|
+
if (!nextError) props.onChange(Number(raw))
|
|
59
|
+
}}
|
|
60
|
+
onBlur={(event) => {
|
|
61
|
+
const nextError = validate(draft())
|
|
62
|
+
if (nextError) {
|
|
63
|
+
setDraft(String(props.value))
|
|
64
|
+
setError('')
|
|
65
|
+
}
|
|
66
|
+
props.onBlur?.(event)
|
|
67
|
+
}}
|
|
68
|
+
/>
|
|
69
|
+
</FormField>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ComponentProps } from 'solid-js'
|
|
2
|
+
import { splitProps } from 'solid-js'
|
|
3
|
+
import { cn } from '../../../lib/utils'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* InputControl.
|
|
7
|
+
*
|
|
8
|
+
* A styled native input with no optional suggestion-list behavior. Use this
|
|
9
|
+
* when a consumer needs only the control; `Input` adds native datalist
|
|
10
|
+
* suggestions for forms that need them.
|
|
11
|
+
*/
|
|
12
|
+
export type InputControlProps = ComponentProps<'input'>
|
|
13
|
+
|
|
14
|
+
export function InputControl(props: InputControlProps) {
|
|
15
|
+
const [local, rest] = splitProps(props, ['class', 'type'])
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<input
|
|
19
|
+
data-slot="input"
|
|
20
|
+
type={local.type ?? 'text'}
|
|
21
|
+
class={cn(
|
|
22
|
+
'h-control-md w-full min-w-0 rounded-md border border-input bg-transparent px-control-md py-1 text-sm',
|
|
23
|
+
'transition-[color,box-shadow,border-color] ease-out outline-none',
|
|
24
|
+
'placeholder:text-muted-foreground',
|
|
25
|
+
'selection:bg-primary selection:text-primary-foreground',
|
|
26
|
+
'file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground',
|
|
27
|
+
/* A pointer click on a text field is a typing affordance, so the ring
|
|
28
|
+
appears where the user agent already matches :focus-visible. */
|
|
29
|
+
'focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-primary-subtle',
|
|
30
|
+
'aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive-subtle',
|
|
31
|
+
'disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50',
|
|
32
|
+
'read-only:bg-surface-hover',
|
|
33
|
+
local.class
|
|
34
|
+
)}
|
|
35
|
+
{...rest}
|
|
36
|
+
/>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ComponentProps } from 'solid-js'
|
|
2
2
|
import { createUniqueId, For, Show, splitProps } from 'solid-js'
|
|
3
|
-
import {
|
|
3
|
+
import { useFormFieldControl } from '../../../lib/form-field'
|
|
4
|
+
import { InputControl } from './input-control'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Input.
|
|
@@ -20,31 +21,42 @@ export type InputProps = ComponentProps<'input'> & {
|
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
export function Input(props: InputProps) {
|
|
23
|
-
const [local, rest] = splitProps(props, [
|
|
24
|
+
const [local, rest] = splitProps(props, [
|
|
25
|
+
'class',
|
|
26
|
+
'type',
|
|
27
|
+
'id',
|
|
28
|
+
'list',
|
|
29
|
+
'suggestions',
|
|
30
|
+
'aria-label',
|
|
31
|
+
'aria-labelledby',
|
|
32
|
+
'aria-describedby',
|
|
33
|
+
'aria-invalid',
|
|
34
|
+
'aria-errormessage',
|
|
35
|
+
])
|
|
36
|
+
const field = useFormFieldControl('control', {
|
|
37
|
+
id: local.id,
|
|
38
|
+
'aria-label': local['aria-label'],
|
|
39
|
+
'aria-labelledby': local['aria-labelledby'],
|
|
40
|
+
'aria-describedby': local['aria-describedby'],
|
|
41
|
+
'aria-invalid': local['aria-invalid'],
|
|
42
|
+
'aria-errormessage': local['aria-errormessage'],
|
|
43
|
+
})
|
|
24
44
|
const suggestionsId = createUniqueId()
|
|
25
45
|
const listId = () => local.list ?? `input-suggestions-${suggestionsId}`
|
|
26
46
|
|
|
27
47
|
return (
|
|
28
48
|
<>
|
|
29
|
-
<
|
|
30
|
-
data-slot="input"
|
|
31
|
-
type={local.type ?? 'text'}
|
|
32
|
-
list={local.suggestions ? listId() : local.list}
|
|
33
|
-
class={cn(
|
|
34
|
-
'h-control-md w-full min-w-0 rounded-md border border-input bg-transparent px-control-md py-1 text-sm',
|
|
35
|
-
'transition-[color,box-shadow,border-color] ease-out outline-none',
|
|
36
|
-
'placeholder:text-muted-foreground',
|
|
37
|
-
'selection:bg-primary selection:text-primary-foreground',
|
|
38
|
-
'file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground',
|
|
39
|
-
/* A pointer click on a text field is a typing affordance, so the ring
|
|
40
|
-
appears where the user agent already matches :focus-visible. */
|
|
41
|
-
'focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-primary-subtle',
|
|
42
|
-
'aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive-subtle',
|
|
43
|
-
'disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50',
|
|
44
|
-
'read-only:bg-surface-hover',
|
|
45
|
-
local.class
|
|
46
|
-
)}
|
|
49
|
+
<InputControl
|
|
47
50
|
{...rest}
|
|
51
|
+
class={local.class}
|
|
52
|
+
type={local.type}
|
|
53
|
+
id={field?.id ?? local.id}
|
|
54
|
+
list={local.suggestions ? listId() : local.list}
|
|
55
|
+
aria-label={local['aria-label']}
|
|
56
|
+
aria-labelledby={field?.['aria-labelledby'] ?? local['aria-labelledby']}
|
|
57
|
+
aria-describedby={field?.['aria-describedby'] ?? local['aria-describedby']}
|
|
58
|
+
aria-invalid={field?.['aria-invalid'] ?? local['aria-invalid']}
|
|
59
|
+
aria-errormessage={field?.['aria-errormessage'] ?? local['aria-errormessage']}
|
|
48
60
|
/>
|
|
49
61
|
<Show when={local.suggestions}>
|
|
50
62
|
{(suggestions) => (
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ComponentProps, JSX } from 'solid-js'
|
|
2
2
|
import { splitProps } from 'solid-js'
|
|
3
|
+
import { useFormFieldControl } from '../../../lib/form-field'
|
|
3
4
|
import { cn } from '../../../lib/utils'
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -26,7 +27,7 @@ export function InputGroup(props: ComponentProps<'div'>) {
|
|
|
26
27
|
'transition-[color,box-shadow,border-color] ease-out',
|
|
27
28
|
'focus-within:border-ring focus-within:ring-3 focus-within:ring-primary-subtle',
|
|
28
29
|
'has-[[aria-invalid=true]]:border-destructive has-[[aria-invalid=true]]:ring-3 has-[[aria-invalid=true]]:ring-destructive-subtle',
|
|
29
|
-
'has-[input:disabled]:
|
|
30
|
+
'has-[input:disabled]:cursor-not-allowed has-[input:disabled]:opacity-50',
|
|
30
31
|
local.class
|
|
31
32
|
)}
|
|
32
33
|
{...rest}
|
|
@@ -35,11 +36,33 @@ export function InputGroup(props: ComponentProps<'div'>) {
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
export function InputGroupInput(props: ComponentProps<'input'>) {
|
|
38
|
-
const [local, rest] = splitProps(props, [
|
|
39
|
+
const [local, rest] = splitProps(props, [
|
|
40
|
+
'class',
|
|
41
|
+
'id',
|
|
42
|
+
'aria-label',
|
|
43
|
+
'aria-labelledby',
|
|
44
|
+
'aria-describedby',
|
|
45
|
+
'aria-invalid',
|
|
46
|
+
'aria-errormessage',
|
|
47
|
+
])
|
|
48
|
+
const field = useFormFieldControl('control', {
|
|
49
|
+
id: local.id,
|
|
50
|
+
'aria-label': local['aria-label'],
|
|
51
|
+
'aria-labelledby': local['aria-labelledby'],
|
|
52
|
+
'aria-describedby': local['aria-describedby'],
|
|
53
|
+
'aria-invalid': local['aria-invalid'],
|
|
54
|
+
'aria-errormessage': local['aria-errormessage'],
|
|
55
|
+
})
|
|
39
56
|
|
|
40
57
|
return (
|
|
41
58
|
<input
|
|
42
59
|
data-slot="input-group-input"
|
|
60
|
+
id={field?.id ?? local.id}
|
|
61
|
+
aria-label={local['aria-label']}
|
|
62
|
+
aria-labelledby={field?.['aria-labelledby'] ?? local['aria-labelledby']}
|
|
63
|
+
aria-describedby={field?.['aria-describedby'] ?? local['aria-describedby']}
|
|
64
|
+
aria-invalid={field?.['aria-invalid'] ?? local['aria-invalid']}
|
|
65
|
+
aria-errormessage={field?.['aria-errormessage'] ?? local['aria-errormessage']}
|
|
43
66
|
class={cn(
|
|
44
67
|
'h-full min-w-0 flex-1 bg-transparent text-sm outline-none',
|
|
45
68
|
'placeholder:text-muted-foreground',
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { splitProps, useContext, type ComponentProps, type JSX } from 'solid-js'
|
|
2
|
+
import { ActionButton } from '../../composites/action-button'
|
|
3
|
+
import { FormFieldContext } from '../../../lib/form-field'
|
|
4
|
+
import { InputGroup, InputGroupInput } from './input-group'
|
|
5
|
+
|
|
6
|
+
export type SecretInputGroupProps = {
|
|
7
|
+
value: string
|
|
8
|
+
disabled: boolean
|
|
9
|
+
onChange: JSX.EventHandler<HTMLInputElement, Event>
|
|
10
|
+
onClear?: () => void
|
|
11
|
+
class?: string
|
|
12
|
+
} & Pick<ComponentProps<'input'>, 'id' | 'aria-label' | 'aria-describedby' | 'aria-invalid'>
|
|
13
|
+
|
|
14
|
+
/** A password input with an optional keyboard-accessible clear action. */
|
|
15
|
+
export function SecretInputGroup(props: SecretInputGroupProps) {
|
|
16
|
+
const field = useContext(FormFieldContext)
|
|
17
|
+
const [local, rest] = splitProps(props, [
|
|
18
|
+
'value',
|
|
19
|
+
'disabled',
|
|
20
|
+
'onChange',
|
|
21
|
+
'onClear',
|
|
22
|
+
'aria-label',
|
|
23
|
+
'class',
|
|
24
|
+
])
|
|
25
|
+
|
|
26
|
+
const input = (
|
|
27
|
+
<InputGroupInput
|
|
28
|
+
{...rest}
|
|
29
|
+
aria-label={local['aria-label'] ?? (field ? undefined : 'New API key')}
|
|
30
|
+
type="password"
|
|
31
|
+
autocomplete="new-password"
|
|
32
|
+
value={local.value}
|
|
33
|
+
disabled={local.disabled}
|
|
34
|
+
onInput={local.onChange}
|
|
35
|
+
/>
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
if (!local.onClear) return <InputGroup class={local.class}>{input}</InputGroup>
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<InputGroup class={local.class}>
|
|
42
|
+
{input}
|
|
43
|
+
<ActionButton
|
|
44
|
+
data-slot="input-group-button"
|
|
45
|
+
variant="ghost"
|
|
46
|
+
size="xs"
|
|
47
|
+
class="-me-1 h-6 min-w-0 shrink-0 px-2"
|
|
48
|
+
disabled={local.disabled}
|
|
49
|
+
tooltip={local.disabled ? 'This API key is managed by policy' : 'Clear API key'}
|
|
50
|
+
tooltipSide="top"
|
|
51
|
+
aria-label="Clear API key"
|
|
52
|
+
onClick={() => local.onClear?.()}
|
|
53
|
+
>
|
|
54
|
+
Clear
|
|
55
|
+
</ActionButton>
|
|
56
|
+
</InputGroup>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ChevronDownIcon } from 'lucide-solid'
|
|
2
2
|
import type { ComponentProps } from 'solid-js'
|
|
3
3
|
import { createEffect, For, onMount, Show, splitProps } from 'solid-js'
|
|
4
|
+
import { useFormFieldControl } from '../../../lib/form-field'
|
|
4
5
|
import { cn } from '../../../lib/utils'
|
|
5
6
|
|
|
6
7
|
export type NativeSelectProps = ComponentProps<'select'> & {
|
|
@@ -59,7 +60,26 @@ export type NativeSelectOption = Readonly<{
|
|
|
59
60
|
}>
|
|
60
61
|
|
|
61
62
|
export function NativeSelect(props: NativeSelectProps) {
|
|
62
|
-
const [local, rest] = splitProps(props, [
|
|
63
|
+
const [local, rest] = splitProps(props, [
|
|
64
|
+
'class',
|
|
65
|
+
'children',
|
|
66
|
+
'defaultValue',
|
|
67
|
+
'options',
|
|
68
|
+
'id',
|
|
69
|
+
'aria-label',
|
|
70
|
+
'aria-labelledby',
|
|
71
|
+
'aria-describedby',
|
|
72
|
+
'aria-invalid',
|
|
73
|
+
'aria-errormessage',
|
|
74
|
+
])
|
|
75
|
+
const field = useFormFieldControl('control', {
|
|
76
|
+
id: local.id,
|
|
77
|
+
'aria-label': local['aria-label'],
|
|
78
|
+
'aria-labelledby': local['aria-labelledby'],
|
|
79
|
+
'aria-describedby': local['aria-describedby'],
|
|
80
|
+
'aria-invalid': local['aria-invalid'],
|
|
81
|
+
'aria-errormessage': local['aria-errormessage'],
|
|
82
|
+
})
|
|
63
83
|
let select: HTMLSelectElement | undefined
|
|
64
84
|
|
|
65
85
|
// Set selection after native options exist; setting select.value before its
|
|
@@ -81,6 +101,12 @@ export function NativeSelect(props: NativeSelectProps) {
|
|
|
81
101
|
select = element
|
|
82
102
|
}}
|
|
83
103
|
data-slot="native-select"
|
|
104
|
+
id={field?.id ?? local.id}
|
|
105
|
+
aria-label={local['aria-label']}
|
|
106
|
+
aria-labelledby={field?.['aria-labelledby'] ?? local['aria-labelledby']}
|
|
107
|
+
aria-describedby={field?.['aria-describedby'] ?? local['aria-describedby']}
|
|
108
|
+
aria-invalid={field?.['aria-invalid'] ?? local['aria-invalid']}
|
|
109
|
+
aria-errormessage={field?.['aria-errormessage'] ?? local['aria-errormessage']}
|
|
84
110
|
class={cn(
|
|
85
111
|
'h-control-md min-w-0 appearance-none rounded-md border border-input bg-transparent px-control-md pr-9 text-sm',
|
|
86
112
|
'transition-[color,box-shadow,border-color] ease-out outline-none',
|
|
@@ -40,14 +40,18 @@ export function Progress(props: ProgressProps) {
|
|
|
40
40
|
|
|
41
41
|
return (
|
|
42
42
|
<KobalteProgress class={cn('flex w-full flex-col gap-1.5', local.class)} {...rest}>
|
|
43
|
-
<
|
|
44
|
-
<
|
|
45
|
-
{local.label}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
43
|
+
<Show when={local.label || !local.hideValue}>
|
|
44
|
+
<div class="flex items-center justify-between gap-3">
|
|
45
|
+
<Show when={local.label}>
|
|
46
|
+
<KobalteProgress.Label class="text-muted-foreground text-sm">
|
|
47
|
+
{local.label}
|
|
48
|
+
</KobalteProgress.Label>
|
|
49
|
+
</Show>
|
|
50
|
+
<Show when={!local.hideValue}>
|
|
51
|
+
<KobalteProgress.ValueLabel class="text-muted-foreground text-sm tabular-nums" />
|
|
52
|
+
</Show>
|
|
53
|
+
</div>
|
|
54
|
+
</Show>
|
|
51
55
|
<KobalteProgress.Track
|
|
52
56
|
class={cn('bg-input relative h-1.5 w-full overflow-hidden rounded-full', local.trackClass)}
|
|
53
57
|
>
|
|
@@ -2,6 +2,7 @@ import { RadioGroup as KobalteRadioGroup } from '@kobalte/core/radio-group'
|
|
|
2
2
|
import { Circle } from 'lucide-solid'
|
|
3
3
|
import type { ComponentProps } from 'solid-js'
|
|
4
4
|
import { splitProps } from 'solid-js'
|
|
5
|
+
import { useFormFieldControl } from '../../../lib/form-field'
|
|
5
6
|
import { cn } from '../../../lib/utils'
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -17,8 +18,35 @@ import { cn } from '../../../lib/utils'
|
|
|
17
18
|
* tokens so either choice sits correctly next to the other.
|
|
18
19
|
*/
|
|
19
20
|
export function RadioGroup(props: ComponentProps<typeof KobalteRadioGroup>) {
|
|
20
|
-
const [local, rest] = splitProps(props, [
|
|
21
|
-
|
|
21
|
+
const [local, rest] = splitProps(props, [
|
|
22
|
+
'class',
|
|
23
|
+
'id',
|
|
24
|
+
'aria-label',
|
|
25
|
+
'aria-labelledby',
|
|
26
|
+
'aria-describedby',
|
|
27
|
+
'aria-invalid',
|
|
28
|
+
'aria-errormessage',
|
|
29
|
+
])
|
|
30
|
+
const field = useFormFieldControl('group', {
|
|
31
|
+
id: local.id,
|
|
32
|
+
'aria-label': local['aria-label'],
|
|
33
|
+
'aria-labelledby': local['aria-labelledby'],
|
|
34
|
+
'aria-describedby': local['aria-describedby'],
|
|
35
|
+
'aria-invalid': local['aria-invalid'],
|
|
36
|
+
'aria-errormessage': local['aria-errormessage'],
|
|
37
|
+
})
|
|
38
|
+
return (
|
|
39
|
+
<KobalteRadioGroup
|
|
40
|
+
id={field?.id ?? local.id}
|
|
41
|
+
aria-label={local['aria-label']}
|
|
42
|
+
aria-labelledby={field?.['aria-labelledby'] ?? local['aria-labelledby']}
|
|
43
|
+
aria-describedby={field?.['aria-describedby'] ?? local['aria-describedby']}
|
|
44
|
+
aria-invalid={field?.['aria-invalid'] ?? local['aria-invalid']}
|
|
45
|
+
aria-errormessage={field?.['aria-errormessage'] ?? local['aria-errormessage']}
|
|
46
|
+
class={cn('grid gap-2', local.class)}
|
|
47
|
+
{...rest}
|
|
48
|
+
/>
|
|
49
|
+
)
|
|
22
50
|
}
|
|
23
51
|
|
|
24
52
|
export type RadioGroupItemProps = ComponentProps<typeof KobalteRadioGroup.Item> & {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Switch as KobalteSwitch } from '@kobalte/core/switch'
|
|
2
2
|
import type { ComponentProps, JSX } from 'solid-js'
|
|
3
3
|
import { splitProps } from 'solid-js'
|
|
4
|
+
import { useFormFieldControl } from '../../../lib/form-field'
|
|
4
5
|
import { cn } from '../../../lib/utils'
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -17,6 +18,8 @@ import { cn } from '../../../lib/utils'
|
|
|
17
18
|
* user sees.
|
|
18
19
|
*/
|
|
19
20
|
export type SwitchProps = Omit<ComponentProps<typeof KobalteSwitch>, 'children'> & {
|
|
21
|
+
/** ID for the hidden native switch input; useful with an external label. */
|
|
22
|
+
inputId?: string
|
|
20
23
|
controlClass?: string
|
|
21
24
|
thumbClass?: string
|
|
22
25
|
labelClass?: string
|
|
@@ -44,22 +47,36 @@ export function Switch(props: SwitchProps) {
|
|
|
44
47
|
'aria-labelledby',
|
|
45
48
|
'aria-describedby',
|
|
46
49
|
'aria-invalid',
|
|
50
|
+
'aria-errormessage',
|
|
47
51
|
'aria-busy',
|
|
52
|
+
'id',
|
|
53
|
+
'inputId',
|
|
48
54
|
'title',
|
|
49
55
|
])
|
|
56
|
+
const field = useFormFieldControl('control', {
|
|
57
|
+
id: local.inputId ?? local.id,
|
|
58
|
+
'aria-label': local['aria-label'],
|
|
59
|
+
'aria-labelledby': local['aria-labelledby'],
|
|
60
|
+
'aria-describedby': local['aria-describedby'],
|
|
61
|
+
'aria-invalid': local['aria-invalid'],
|
|
62
|
+
'aria-errormessage': local['aria-errormessage'],
|
|
63
|
+
})
|
|
50
64
|
|
|
51
65
|
return (
|
|
52
66
|
<KobalteSwitch
|
|
67
|
+
id={field ? undefined : local.id}
|
|
53
68
|
title={local.title}
|
|
54
69
|
class={cn('group/switch flex items-center gap-2.5', local.class)}
|
|
55
70
|
{...rest}
|
|
56
71
|
>
|
|
57
72
|
{/* The name belongs on the input; see Checkbox for why. */}
|
|
58
73
|
<KobalteSwitch.Input
|
|
74
|
+
id={field?.id ?? local.inputId}
|
|
59
75
|
aria-label={local['aria-label']}
|
|
60
|
-
aria-labelledby={local['aria-labelledby']}
|
|
61
|
-
aria-describedby={local['aria-describedby']}
|
|
62
|
-
aria-invalid={local['aria-invalid']}
|
|
76
|
+
aria-labelledby={field?.['aria-labelledby'] ?? local['aria-labelledby']}
|
|
77
|
+
aria-describedby={field?.['aria-describedby'] ?? local['aria-describedby']}
|
|
78
|
+
aria-invalid={field?.['aria-invalid'] ?? local['aria-invalid']}
|
|
79
|
+
aria-errormessage={field?.['aria-errormessage'] ?? local['aria-errormessage']}
|
|
63
80
|
aria-busy={local['aria-busy']}
|
|
64
81
|
title={local.title}
|
|
65
82
|
/>
|
|
@@ -59,7 +59,7 @@ export function TabsTrigger(props: TabsTriggerProps) {
|
|
|
59
59
|
'transition-colors ease-out outline-none select-none',
|
|
60
60
|
'focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-primary-subtle',
|
|
61
61
|
'disabled:pointer-events-none disabled:opacity-50',
|
|
62
|
-
'text-
|
|
62
|
+
'text-foreground',
|
|
63
63
|
'[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg]:size-4',
|
|
64
64
|
local.appearance === 'segmented'
|
|
65
65
|
? 'h-control-sm rounded-md px-control-sm data-[selected]:bg-card data-[selected]:shadow-xs'
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ComponentProps } from 'solid-js'
|
|
2
|
+
import { splitProps } from 'solid-js'
|
|
3
|
+
import { cn } from '../../../lib/utils'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* TextLink.
|
|
7
|
+
*
|
|
8
|
+
* An inline, underlined link for prose and content navigation. Use ActionButton
|
|
9
|
+
* or ListRow when the link is a compact action or a row-sized target.
|
|
10
|
+
*/
|
|
11
|
+
export function TextLink(props: ComponentProps<'a'>) {
|
|
12
|
+
const [local, rest] = splitProps(props, ['class', 'tabIndex'])
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<a
|
|
16
|
+
data-slot="textlink"
|
|
17
|
+
class={cn(
|
|
18
|
+
'rounded-sm text-foreground underline decoration-primary underline-offset-4',
|
|
19
|
+
'transition-colors ease-out hover:decoration-foreground outline-none',
|
|
20
|
+
'focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-primary-subtle',
|
|
21
|
+
'motion-reduce:transition-none',
|
|
22
|
+
local.class
|
|
23
|
+
)}
|
|
24
|
+
tabIndex={local.tabIndex ?? 0}
|
|
25
|
+
{...rest}
|
|
26
|
+
/>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ComponentProps } from 'solid-js'
|
|
2
2
|
import { splitProps } from 'solid-js'
|
|
3
|
+
import { useFormFieldControl } from '../../../lib/form-field'
|
|
3
4
|
import { cn } from '../../../lib/utils'
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -14,11 +15,33 @@ import { cn } from '../../../lib/utils'
|
|
|
14
15
|
* remains the fallback so an unsupported engine still gets a usable height.
|
|
15
16
|
*/
|
|
16
17
|
export function Textarea(props: ComponentProps<'textarea'>) {
|
|
17
|
-
const [local, rest] = splitProps(props, [
|
|
18
|
+
const [local, rest] = splitProps(props, [
|
|
19
|
+
'class',
|
|
20
|
+
'id',
|
|
21
|
+
'aria-label',
|
|
22
|
+
'aria-labelledby',
|
|
23
|
+
'aria-describedby',
|
|
24
|
+
'aria-invalid',
|
|
25
|
+
'aria-errormessage',
|
|
26
|
+
])
|
|
27
|
+
const field = useFormFieldControl('control', {
|
|
28
|
+
id: local.id,
|
|
29
|
+
'aria-label': local['aria-label'],
|
|
30
|
+
'aria-labelledby': local['aria-labelledby'],
|
|
31
|
+
'aria-describedby': local['aria-describedby'],
|
|
32
|
+
'aria-invalid': local['aria-invalid'],
|
|
33
|
+
'aria-errormessage': local['aria-errormessage'],
|
|
34
|
+
})
|
|
18
35
|
|
|
19
36
|
return (
|
|
20
37
|
<textarea
|
|
21
38
|
data-slot="textarea"
|
|
39
|
+
id={field?.id ?? local.id}
|
|
40
|
+
aria-label={local['aria-label']}
|
|
41
|
+
aria-labelledby={field?.['aria-labelledby'] ?? local['aria-labelledby']}
|
|
42
|
+
aria-describedby={field?.['aria-describedby'] ?? local['aria-describedby']}
|
|
43
|
+
aria-invalid={field?.['aria-invalid'] ?? local['aria-invalid']}
|
|
44
|
+
aria-errormessage={field?.['aria-errormessage'] ?? local['aria-errormessage']}
|
|
22
45
|
class={cn(
|
|
23
46
|
'flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-transparent px-control-md py-2 text-sm',
|
|
24
47
|
'transition-[color,box-shadow,border-color] ease-out outline-none',
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const CLIPBOARD_UNAVAILABLE = 'Clipboard is unavailable in this WebView'
|
|
2
|
+
|
|
3
|
+
/** Writes text with the async Clipboard API, falling back for older WebViews. */
|
|
4
|
+
export async function writeClipboardText(value: string): Promise<void> {
|
|
5
|
+
if (typeof navigator !== 'undefined' && typeof navigator.clipboard?.writeText === 'function') {
|
|
6
|
+
await navigator.clipboard.writeText(value)
|
|
7
|
+
return
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (typeof document === 'undefined' || !document.body) {
|
|
11
|
+
throw new Error(CLIPBOARD_UNAVAILABLE)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const previousFocus = document.activeElement
|
|
15
|
+
const textarea = document.createElement('textarea')
|
|
16
|
+
textarea.value = value
|
|
17
|
+
textarea.setAttribute('readonly', '')
|
|
18
|
+
textarea.setAttribute('aria-hidden', 'true')
|
|
19
|
+
textarea.tabIndex = -1
|
|
20
|
+
textarea.style.position = 'fixed'
|
|
21
|
+
textarea.style.opacity = '0'
|
|
22
|
+
document.body.appendChild(textarea)
|
|
23
|
+
|
|
24
|
+
let copied = false
|
|
25
|
+
try {
|
|
26
|
+
textarea.focus({ preventScroll: true })
|
|
27
|
+
textarea.select()
|
|
28
|
+
copied = typeof document.execCommand === 'function' && document.execCommand('copy')
|
|
29
|
+
} finally {
|
|
30
|
+
textarea.remove()
|
|
31
|
+
if (previousFocus instanceof HTMLElement && previousFocus.isConnected) {
|
|
32
|
+
try {
|
|
33
|
+
previousFocus.focus({ preventScroll: true })
|
|
34
|
+
} catch {
|
|
35
|
+
// A host may revoke focus during the copy; preserve the clipboard outcome.
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!copied) throw new Error(CLIPBOARD_UNAVAILABLE)
|
|
41
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Download a Blob through a temporary, non-focusable anchor. */
|
|
2
|
+
export function downloadBlob(blob: Blob, filename: string): void {
|
|
3
|
+
const objectUrl = URL.createObjectURL(blob)
|
|
4
|
+
const previousFocus = document.activeElement as HTMLElement | null
|
|
5
|
+
let anchor: HTMLAnchorElement | undefined
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
anchor = document.createElement('a')
|
|
9
|
+
anchor.href = objectUrl
|
|
10
|
+
anchor.download = filename
|
|
11
|
+
anchor.hidden = true
|
|
12
|
+
anchor.tabIndex = -1
|
|
13
|
+
anchor.setAttribute('aria-hidden', 'true')
|
|
14
|
+
document.body.append(anchor)
|
|
15
|
+
anchor.click()
|
|
16
|
+
} finally {
|
|
17
|
+
try {
|
|
18
|
+
anchor?.remove()
|
|
19
|
+
} finally {
|
|
20
|
+
try {
|
|
21
|
+
if (previousFocus?.isConnected && document.activeElement !== previousFocus) {
|
|
22
|
+
previousFocus.focus({ preventScroll: true })
|
|
23
|
+
}
|
|
24
|
+
} finally {
|
|
25
|
+
setTimeout(() => URL.revokeObjectURL(objectUrl), 0)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|