@thomasfosterau/components 0.0.0 → 0.1.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/LICENSE +21 -0
- package/README.md +2 -2
- package/dist/canvas/VectorCanvas.svelte +239 -0
- package/dist/canvas/VectorCanvas.svelte.d.ts +87 -0
- package/dist/canvas/history.d.ts +105 -0
- package/dist/canvas/history.js +147 -0
- package/dist/canvas/index.d.ts +15 -0
- package/dist/canvas/index.js +14 -0
- package/dist/canvas/types.d.ts +91 -0
- package/dist/canvas/types.js +13 -0
- package/dist/canvas/utils.d.ts +66 -0
- package/dist/canvas/utils.js +114 -0
- package/dist/charts/Chart.svelte +300 -0
- package/dist/charts/Chart.svelte.d.ts +50 -0
- package/dist/charts/index.d.ts +11 -0
- package/dist/charts/index.js +10 -0
- package/dist/charts/types.d.ts +28 -0
- package/dist/charts/types.js +6 -0
- package/dist/citations/BookCitation.svelte +53 -0
- package/dist/citations/BookCitation.svelte.d.ts +41 -0
- package/dist/citations/ChapterCitation.svelte +38 -0
- package/dist/citations/ChapterCitation.svelte.d.ts +50 -0
- package/dist/citations/Citation.svelte +47 -0
- package/dist/citations/Citation.svelte.d.ts +24 -0
- package/dist/citations/CreditList.svelte +69 -0
- package/dist/citations/CreditList.svelte.d.ts +47 -0
- package/dist/citations/DOILink.svelte +28 -0
- package/dist/citations/DOILink.svelte.d.ts +19 -0
- package/dist/citations/Footnote.svelte +39 -0
- package/dist/citations/Footnote.svelte.d.ts +17 -0
- package/dist/citations/FootnoteList.svelte +36 -0
- package/dist/citations/FootnoteList.svelte.d.ts +21 -0
- package/dist/citations/ISBNLink.svelte +29 -0
- package/dist/citations/ISBNLink.svelte.d.ts +19 -0
- package/dist/citations/ISSNLink.svelte +29 -0
- package/dist/citations/ISSNLink.svelte.d.ts +19 -0
- package/dist/citations/Publisher.svelte +34 -0
- package/dist/citations/Publisher.svelte.d.ts +14 -0
- package/dist/citations/ReferenceList.svelte +53 -0
- package/dist/citations/ReferenceList.svelte.d.ts +21 -0
- package/dist/citations/ReportCitation.svelte +41 -0
- package/dist/citations/ReportCitation.svelte.d.ts +36 -0
- package/dist/citations/ScholarlyArticleCitation.svelte +64 -0
- package/dist/citations/ScholarlyArticleCitation.svelte.d.ts +41 -0
- package/dist/citations/ThesisCitation.svelte +35 -0
- package/dist/citations/ThesisCitation.svelte.d.ts +29 -0
- package/dist/citations/bibliography.svelte.d.ts +55 -0
- package/dist/citations/bibliography.svelte.js +78 -0
- package/dist/citations/citation.d.ts +71 -0
- package/dist/citations/citation.js +121 -0
- package/dist/citations/csl.d.ts +102 -0
- package/dist/citations/csl.js +20 -0
- package/dist/citations/index.d.ts +31 -0
- package/dist/citations/index.js +36 -0
- package/dist/citations/list-format.d.ts +34 -0
- package/dist/citations/list-format.js +38 -0
- package/dist/context.svelte.d.ts +46 -0
- package/dist/context.svelte.js +51 -0
- package/dist/data/DataGrid.svelte +150 -0
- package/dist/data/DataGrid.svelte.d.ts +118 -0
- package/dist/data/DescriptionList.svelte +47 -0
- package/dist/data/DescriptionList.svelte.d.ts +26 -0
- package/dist/data/DescriptionRow.svelte +55 -0
- package/dist/data/DescriptionRow.svelte.d.ts +21 -0
- package/dist/data/DiffText.svelte +29 -0
- package/dist/data/DiffText.svelte.d.ts +19 -0
- package/dist/data/ItemList.svelte +80 -0
- package/dist/data/ItemList.svelte.d.ts +52 -0
- package/dist/data/LinkListItem.svelte +46 -0
- package/dist/data/LinkListItem.svelte.d.ts +21 -0
- package/dist/data/Pagination.svelte +136 -0
- package/dist/data/Pagination.svelte.d.ts +71 -0
- package/dist/data/PaginationFooter.svelte +41 -0
- package/dist/data/PaginationFooter.svelte.d.ts +59 -0
- package/dist/data/ResultCount.svelte +27 -0
- package/dist/data/ResultCount.svelte.d.ts +18 -0
- package/dist/data/Table.svelte +145 -0
- package/dist/data/Table.svelte.d.ts +75 -0
- package/dist/data/TreeList.svelte +41 -0
- package/dist/data/TreeList.svelte.d.ts +17 -0
- package/dist/data/TreeListItem.svelte +29 -0
- package/dist/data/TreeListItem.svelte.d.ts +10 -0
- package/dist/data/description-context.d.ts +33 -0
- package/dist/data/description-context.js +35 -0
- package/dist/data/diff.d.ts +14 -0
- package/dist/data/diff.js +10 -0
- package/dist/data/index.d.ts +35 -0
- package/dist/data/index.js +35 -0
- package/dist/data/list-item.d.ts +30 -0
- package/dist/data/list-item.js +19 -0
- package/dist/data/pagination.d.ts +81 -0
- package/dist/data/pagination.js +79 -0
- package/dist/data/table-types.d.ts +64 -0
- package/dist/data/table-types.js +35 -0
- package/dist/feedback/ConfirmDialog.svelte +79 -0
- package/dist/feedback/ConfirmDialog.svelte.d.ts +82 -0
- package/dist/feedback/Dialog.svelte +77 -0
- package/dist/feedback/Dialog.svelte.d.ts +37 -0
- package/dist/feedback/LoadingBoundary.svelte +61 -0
- package/dist/feedback/LoadingBoundary.svelte.d.ts +34 -0
- package/dist/feedback/ProgressBar.svelte +46 -0
- package/dist/feedback/ProgressBar.svelte.d.ts +28 -0
- package/dist/feedback/SegmentBar.svelte +83 -0
- package/dist/feedback/SegmentBar.svelte.d.ts +36 -0
- package/dist/feedback/StatTile.svelte +77 -0
- package/dist/feedback/StatTile.svelte.d.ts +49 -0
- package/dist/feedback/Stats.svelte +18 -0
- package/dist/feedback/Stats.svelte.d.ts +16 -0
- package/dist/feedback/Timeline.svelte +86 -0
- package/dist/feedback/Timeline.svelte.d.ts +52 -0
- package/dist/feedback/Toaster.svelte +78 -0
- package/dist/feedback/Toaster.svelte.d.ts +32 -0
- package/dist/feedback/index.d.ts +31 -0
- package/dist/feedback/index.js +27 -0
- package/dist/feedback/sizes.d.ts +27 -0
- package/dist/feedback/sizes.js +41 -0
- package/dist/feedback/toaster.d.ts +48 -0
- package/dist/feedback/toaster.js +29 -0
- package/dist/fields/Checkbox.svelte +33 -0
- package/dist/fields/Checkbox.svelte.d.ts +24 -0
- package/dist/fields/CheckboxGroup.svelte +41 -0
- package/dist/fields/CheckboxGroup.svelte.d.ts +40 -0
- package/dist/fields/Combobox.svelte +222 -0
- package/dist/fields/Combobox.svelte.d.ts +98 -0
- package/dist/fields/DateInput.svelte +31 -0
- package/dist/fields/DateInput.svelte.d.ts +29 -0
- package/dist/fields/FieldErrors.svelte +30 -0
- package/dist/fields/FieldErrors.svelte.d.ts +11 -0
- package/dist/fields/Fieldset.svelte +48 -0
- package/dist/fields/Fieldset.svelte.d.ts +26 -0
- package/dist/fields/FileInput.svelte +67 -0
- package/dist/fields/FileInput.svelte.d.ts +33 -0
- package/dist/fields/FormField.svelte +54 -0
- package/dist/fields/FormField.svelte.d.ts +29 -0
- package/dist/fields/ImageInput.svelte +87 -0
- package/dist/fields/ImageInput.svelte.d.ts +39 -0
- package/dist/fields/PushToggle.svelte +93 -0
- package/dist/fields/PushToggle.svelte.d.ts +55 -0
- package/dist/fields/RadioGroup.svelte +85 -0
- package/dist/fields/RadioGroup.svelte.d.ts +41 -0
- package/dist/fields/SearchInput.svelte +57 -0
- package/dist/fields/SearchInput.svelte.d.ts +33 -0
- package/dist/fields/Select.svelte +36 -0
- package/dist/fields/Select.svelte.d.ts +29 -0
- package/dist/fields/TextInput.svelte +26 -0
- package/dist/fields/TextInput.svelte.d.ts +30 -0
- package/dist/fields/Textarea.svelte +22 -0
- package/dist/fields/Textarea.svelte.d.ts +25 -0
- package/dist/fields/Toggle.svelte +72 -0
- package/dist/fields/Toggle.svelte.d.ts +32 -0
- package/dist/fields/VideoInput.svelte +35 -0
- package/dist/fields/VideoInput.svelte.d.ts +40 -0
- package/dist/fields/field-types.d.ts +51 -0
- package/dist/fields/field-types.js +60 -0
- package/dist/fields/index.d.ts +34 -0
- package/dist/fields/index.js +34 -0
- package/dist/formatting/DateTime.svelte +35 -0
- package/dist/formatting/DateTime.svelte.d.ts +26 -0
- package/dist/formatting/FormatList.svelte +31 -0
- package/dist/formatting/FormatList.svelte.d.ts +46 -0
- package/dist/formatting/Highlight.svelte +47 -0
- package/dist/formatting/Highlight.svelte.d.ts +15 -0
- package/dist/formatting/Number.svelte +35 -0
- package/dist/formatting/Number.svelte.d.ts +43 -0
- package/dist/formatting/RelativeTime.svelte +69 -0
- package/dist/formatting/RelativeTime.svelte.d.ts +26 -0
- package/dist/formatting/Year.svelte +39 -0
- package/dist/formatting/Year.svelte.d.ts +21 -0
- package/dist/formatting/index.d.ts +12 -0
- package/dist/formatting/index.js +14 -0
- package/dist/forms/EntityForm.svelte +177 -0
- package/dist/forms/EntityForm.svelte.d.ts +57 -0
- package/dist/forms/FormActions.svelte +29 -0
- package/dist/forms/FormActions.svelte.d.ts +37 -0
- package/dist/forms/FormErrors.svelte +21 -0
- package/dist/forms/FormErrors.svelte.d.ts +16 -0
- package/dist/forms/HiddenField.svelte +22 -0
- package/dist/forms/HiddenField.svelte.d.ts +11 -0
- package/dist/forms/SearchForm.svelte +43 -0
- package/dist/forms/SearchForm.svelte.d.ts +36 -0
- package/dist/forms/entity-form.d.ts +12 -0
- package/dist/forms/entity-form.js +12 -0
- package/dist/forms/index.d.ts +14 -0
- package/dist/forms/index.js +14 -0
- package/dist/hotkeys/HotkeyBadge.svelte +39 -0
- package/dist/hotkeys/HotkeyBadge.svelte.d.ts +25 -0
- package/dist/hotkeys/HotkeyHelpDialog.svelte +68 -0
- package/dist/hotkeys/HotkeyHelpDialog.svelte.d.ts +35 -0
- package/dist/hotkeys/HotkeyProvider.svelte +92 -0
- package/dist/hotkeys/HotkeyProvider.svelte.d.ts +4 -0
- package/dist/hotkeys/context.svelte.d.ts +47 -0
- package/dist/hotkeys/context.svelte.js +72 -0
- package/dist/hotkeys/index.d.ts +23 -0
- package/dist/hotkeys/index.js +22 -0
- package/dist/hotkeys/match.d.ts +65 -0
- package/dist/hotkeys/match.js +133 -0
- package/dist/hotkeys/registry.svelte.d.ts +55 -0
- package/dist/hotkeys/registry.svelte.js +110 -0
- package/dist/hotkeys/types.d.ts +72 -0
- package/dist/hotkeys/types.js +18 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +36 -0
- package/dist/infobox/Infobox.svelte +79 -0
- package/dist/infobox/Infobox.svelte.d.ts +26 -0
- package/dist/infobox/InfoboxHeader.svelte +27 -0
- package/dist/infobox/InfoboxHeader.svelte.d.ts +11 -0
- package/dist/infobox/InfoboxImage.svelte +38 -0
- package/dist/infobox/InfoboxImage.svelte.d.ts +21 -0
- package/dist/infobox/InfoboxRow.svelte +50 -0
- package/dist/infobox/InfoboxRow.svelte.d.ts +25 -0
- package/dist/infobox/InfoboxSection.svelte +24 -0
- package/dist/infobox/InfoboxSection.svelte.d.ts +12 -0
- package/dist/infobox/index.d.ts +21 -0
- package/dist/infobox/index.js +20 -0
- package/dist/infobox/types.d.ts +86 -0
- package/dist/infobox/types.js +27 -0
- package/dist/layout/Breadcrumbs.svelte +52 -0
- package/dist/layout/Breadcrumbs.svelte.d.ts +18 -0
- package/dist/layout/DetailLayout.svelte +50 -0
- package/dist/layout/DetailLayout.svelte.d.ts +19 -0
- package/dist/layout/FilterBar.svelte +40 -0
- package/dist/layout/FilterBar.svelte.d.ts +12 -0
- package/dist/layout/Footer.svelte +32 -0
- package/dist/layout/Footer.svelte.d.ts +12 -0
- package/dist/layout/Main.svelte +30 -0
- package/dist/layout/Main.svelte.d.ts +15 -0
- package/dist/layout/Navbar.svelte +68 -0
- package/dist/layout/Navbar.svelte.d.ts +31 -0
- package/dist/layout/PageHeader.svelte +77 -0
- package/dist/layout/PageHeader.svelte.d.ts +33 -0
- package/dist/layout/Section.svelte +72 -0
- package/dist/layout/Section.svelte.d.ts +40 -0
- package/dist/layout/Sidebar.svelte +54 -0
- package/dist/layout/Sidebar.svelte.d.ts +27 -0
- package/dist/layout/Tabs.svelte +85 -0
- package/dist/layout/Tabs.svelte.d.ts +42 -0
- package/dist/layout/index.d.ts +29 -0
- package/dist/layout/index.js +29 -0
- package/dist/layout/types.d.ts +52 -0
- package/dist/layout/types.js +27 -0
- package/dist/links/ContextLinks.svelte +28 -0
- package/dist/links/ContextLinks.svelte.d.ts +20 -0
- package/dist/links/EmailLink.svelte +26 -0
- package/dist/links/EmailLink.svelte.d.ts +20 -0
- package/dist/links/index.d.ts +7 -0
- package/dist/links/index.js +7 -0
- package/dist/media/Figure.svelte +26 -0
- package/dist/media/Figure.svelte.d.ts +12 -0
- package/dist/media/Image.svelte +76 -0
- package/dist/media/Image.svelte.d.ts +41 -0
- package/dist/media/Video.svelte +65 -0
- package/dist/media/Video.svelte.d.ts +29 -0
- package/dist/media/image-dimensions.d.ts +28 -0
- package/dist/media/image-dimensions.js +34 -0
- package/dist/media/index.d.ts +10 -0
- package/dist/media/index.js +10 -0
- package/dist/search/CommandPalette.svelte +257 -0
- package/dist/search/CommandPalette.svelte.d.ts +59 -0
- package/dist/search/index.d.ts +13 -0
- package/dist/search/index.js +13 -0
- package/dist/search/types.d.ts +37 -0
- package/dist/search/types.js +20 -0
- package/dist/seo/Head.svelte +190 -0
- package/dist/seo/Head.svelte.d.ts +86 -0
- package/dist/seo/JsonLd.svelte +30 -0
- package/dist/seo/JsonLd.svelte.d.ts +20 -0
- package/dist/seo/index.d.ts +18 -0
- package/dist/seo/index.js +17 -0
- package/dist/seo/json-ld.d.ts +33 -0
- package/dist/seo/json-ld.js +29 -0
- package/dist/seo/og-image.d.ts +71 -0
- package/dist/seo/og-image.js +81 -0
- package/dist/seo/types.d.ts +43 -0
- package/dist/seo/types.js +6 -0
- package/dist/theme.css +161 -0
- package/dist/tips/PopoverTip.svelte +80 -0
- package/dist/tips/PopoverTip.svelte.d.ts +33 -0
- package/dist/tips/Tip.svelte +91 -0
- package/dist/tips/Tip.svelte.d.ts +25 -0
- package/dist/tips/index.d.ts +15 -0
- package/dist/tips/index.js +14 -0
- package/dist/tips/tip-stack.d.ts +25 -0
- package/dist/tips/tip-stack.js +37 -0
- package/dist/tips/tip-store.svelte.d.ts +74 -0
- package/dist/tips/tip-store.svelte.js +158 -0
- package/dist/tips/types.d.ts +46 -0
- package/dist/tips/types.js +6 -0
- package/dist/ui/Alert.svelte +61 -0
- package/dist/ui/Alert.svelte.d.ts +37 -0
- package/dist/ui/Badge.svelte +39 -0
- package/dist/ui/Badge.svelte.d.ts +32 -0
- package/dist/ui/Button.svelte +43 -0
- package/dist/ui/Button.svelte.d.ts +29 -0
- package/dist/ui/Card.svelte +49 -0
- package/dist/ui/Card.svelte.d.ts +44 -0
- package/dist/ui/Chip.svelte +58 -0
- package/dist/ui/Chip.svelte.d.ts +21 -0
- package/dist/ui/ConfirmButton.svelte +75 -0
- package/dist/ui/ConfirmButton.svelte.d.ts +48 -0
- package/dist/ui/Disclosure.svelte +39 -0
- package/dist/ui/Disclosure.svelte.d.ts +22 -0
- package/dist/ui/EmptyState.svelte +37 -0
- package/dist/ui/EmptyState.svelte.d.ts +22 -0
- package/dist/ui/ExternalLink.svelte +38 -0
- package/dist/ui/ExternalLink.svelte.d.ts +34 -0
- package/dist/ui/Heading.svelte +58 -0
- package/dist/ui/Heading.svelte.d.ts +22 -0
- package/dist/ui/Link.svelte +34 -0
- package/dist/ui/Link.svelte.d.ts +20 -0
- package/dist/ui/LinkButton.svelte +51 -0
- package/dist/ui/LinkButton.svelte.d.ts +33 -0
- package/dist/ui/Menu.svelte +54 -0
- package/dist/ui/Menu.svelte.d.ts +34 -0
- package/dist/ui/MenuItem.svelte +75 -0
- package/dist/ui/MenuItem.svelte.d.ts +21 -0
- package/dist/ui/MenuSeparator.svelte +26 -0
- package/dist/ui/MenuSeparator.svelte.d.ts +8 -0
- package/dist/ui/Popover.svelte +63 -0
- package/dist/ui/Popover.svelte.d.ts +40 -0
- package/dist/ui/ShareButton.svelte +64 -0
- package/dist/ui/ShareButton.svelte.d.ts +39 -0
- package/dist/ui/Skeleton.svelte +43 -0
- package/dist/ui/Skeleton.svelte.d.ts +22 -0
- package/dist/ui/ThemeToggle.svelte +65 -0
- package/dist/ui/ThemeToggle.svelte.d.ts +24 -0
- package/dist/ui/Tooltip.svelte +54 -0
- package/dist/ui/Tooltip.svelte.d.ts +35 -0
- package/dist/ui/VisuallyHidden.svelte +23 -0
- package/dist/ui/VisuallyHidden.svelte.d.ts +10 -0
- package/dist/ui/colour-token.d.ts +41 -0
- package/dist/ui/colour-token.js +49 -0
- package/dist/ui/index.d.ts +44 -0
- package/dist/ui/index.js +44 -0
- package/dist/ui/menu-context.d.ts +22 -0
- package/dist/ui/menu-context.js +26 -0
- package/dist/ui/slug.d.ts +19 -0
- package/dist/ui/slug.js +25 -0
- package/dist/ui/tones.d.ts +68 -0
- package/dist/ui/tones.js +150 -0
- package/package.json +25 -25
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
import type { FieldControl } from './field-types.ts';
|
|
4
|
+
interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
|
|
5
|
+
/** The control's `id`. Minted when omitted. */
|
|
6
|
+
id?: string | undefined;
|
|
7
|
+
/** Visible label; becomes the control's accessible name. */
|
|
8
|
+
label: string;
|
|
9
|
+
/** Guidance rendered under the control. */
|
|
10
|
+
hint?: string | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* Validation messages; any at all marks the control invalid.
|
|
13
|
+
* @defaultValue `[]`
|
|
14
|
+
*/
|
|
15
|
+
errors?: readonly string[] | undefined;
|
|
16
|
+
/** Marks the control required and reads `requiredLabel` after the label. */
|
|
17
|
+
required?: boolean | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Read out after the label when `required`.
|
|
20
|
+
* @defaultValue `'(required)'`
|
|
21
|
+
*/
|
|
22
|
+
requiredLabel?: string | undefined;
|
|
23
|
+
/** The control; receives the `FieldControl` props to spread. */
|
|
24
|
+
children: Snippet<[FieldControl]>;
|
|
25
|
+
}
|
|
26
|
+
/** The label, hint and errors frame around one control, handing it the `FieldControl` wiring. */
|
|
27
|
+
declare const FormField: import("svelte").Component<Props, {}, "">;
|
|
28
|
+
type FormField = ReturnType<typeof FormField>;
|
|
29
|
+
export default FormField;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<!-- @component
|
|
2
|
+
A `FileInput` that also measures the chosen image and previews it.
|
|
3
|
+
-->
|
|
4
|
+
<!--
|
|
5
|
+
ImageInput — a `FileInput` that also measures the picture and previews it.
|
|
6
|
+
|
|
7
|
+
It exists for one thing a server cannot do cheaply: read a file's INTRINSIC
|
|
8
|
+
DIMENSIONS. `createImageBitmap` (via `media/image-dimensions.ts`) decodes it in the browser the moment it is
|
|
9
|
+
chosen, so the form has `width` / `height` to submit alongside the binary and
|
|
10
|
+
the page that later renders the image can reserve its box on first paint
|
|
11
|
+
instead of reflowing when it loads.
|
|
12
|
+
|
|
13
|
+
The preview is an object URL, so the file never leaves the browser until the
|
|
14
|
+
form is submitted. The URL is revoked as soon as it is replaced and again on
|
|
15
|
+
teardown — an un-revoked object URL pins the whole file in memory for the
|
|
16
|
+
life of the document.
|
|
17
|
+
|
|
18
|
+
A decode failure means the browser could not read the file as an image; the
|
|
19
|
+
server's own media-type gate will refuse it too, so the dimensions are left
|
|
20
|
+
unset rather than blocking the submission.
|
|
21
|
+
|
|
22
|
+
ARIA contract: `FileInput`'s. The preview `<img>` is named by `previewAlt`,
|
|
23
|
+
and the measured size is a `<figcaption>` rather than an `aria-label`,
|
|
24
|
+
because it is information about the file that a sighted reader gets too.
|
|
25
|
+
-->
|
|
26
|
+
<script lang="ts">import { imageDimensions } from "../media/image-dimensions.js";
|
|
27
|
+
import FileInput from "./FileInput.svelte";
|
|
28
|
+
let { file = $bindable(), width = $bindable(), height = $bindable(), previewAlt = "Preview of the selected image", accept = "image/jpeg,image/png,image/gif,image/webp,image/avif", name, class: className, ...rest } = $props();
|
|
29
|
+
let previewUrl = $state();
|
|
30
|
+
let measuring = $state(false);
|
|
31
|
+
// Revoke the PREVIOUS url whenever this one changes, and the last one when
|
|
32
|
+
// the component goes away. Reading `previewUrl` inside the effect is what
|
|
33
|
+
// makes the cleanup run on every change rather than only on teardown.
|
|
34
|
+
$effect(() => {
|
|
35
|
+
const url = previewUrl;
|
|
36
|
+
return () => {
|
|
37
|
+
if (url !== undefined) URL.revokeObjectURL(url);
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
const measure = async (chosen) => {
|
|
41
|
+
measuring = true;
|
|
42
|
+
try {
|
|
43
|
+
({width, height} = await imageDimensions(chosen));
|
|
44
|
+
} catch {
|
|
45
|
+
width = undefined;
|
|
46
|
+
height = undefined;
|
|
47
|
+
} finally {
|
|
48
|
+
measuring = false;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const select = (chosen) => {
|
|
52
|
+
if (chosen === undefined) {
|
|
53
|
+
previewUrl = undefined;
|
|
54
|
+
width = undefined;
|
|
55
|
+
height = undefined;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
previewUrl = URL.createObjectURL(chosen);
|
|
59
|
+
void measure(chosen);
|
|
60
|
+
};
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<div class={['flex flex-col gap-2', className]}>
|
|
64
|
+
<FileInput {...rest} {name} {accept} bind:file onSelect={select} />
|
|
65
|
+
|
|
66
|
+
{#if previewUrl !== undefined}
|
|
67
|
+
<figure>
|
|
68
|
+
<!-- The caller's own file, not a served image, so a plain `<img>`. -->
|
|
69
|
+
<img src={previewUrl} alt={previewAlt} class="rounded-box border-base-300 max-h-64 border" />
|
|
70
|
+
<figcaption class="fieldset-label mt-1">
|
|
71
|
+
{#if measuring}
|
|
72
|
+
|
|
73
|
+
{:else if width !== undefined && height !== undefined}
|
|
74
|
+
{width} × {height}
|
|
75
|
+
{/if}
|
|
76
|
+
</figcaption>
|
|
77
|
+
</figure>
|
|
78
|
+
{/if}
|
|
79
|
+
|
|
80
|
+
<!-- The measured dimensions ride the same multipart body as the binary. -->
|
|
81
|
+
{#if name !== undefined && width !== undefined}
|
|
82
|
+
<input type="hidden" name="width" value={width} />
|
|
83
|
+
{/if}
|
|
84
|
+
{#if name !== undefined && height !== undefined}
|
|
85
|
+
<input type="hidden" name="height" value={height} />
|
|
86
|
+
{/if}
|
|
87
|
+
</div>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
2
|
+
import type { FieldSize } from './field-types.ts';
|
|
3
|
+
interface Props extends Omit<HTMLInputAttributes, 'type' | 'value' | 'files' | 'size'> {
|
|
4
|
+
/** The chosen file. */
|
|
5
|
+
file?: File | undefined;
|
|
6
|
+
/** Measured intrinsic width, in pixels. */
|
|
7
|
+
width?: number | undefined;
|
|
8
|
+
/** Measured intrinsic height, in pixels. */
|
|
9
|
+
height?: number | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* Alternative text for the local preview.
|
|
12
|
+
* @defaultValue `'Preview of the selected image'`
|
|
13
|
+
*/
|
|
14
|
+
previewAlt?: string | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* The `accept` filter passed to the file input.
|
|
17
|
+
* @defaultValue `'image/jpeg,image/png,image/gif,image/webp,image/avif'`
|
|
18
|
+
*/
|
|
19
|
+
accept?: string | undefined;
|
|
20
|
+
/** Largest accepted file, in bytes; a larger file is rejected with `tooLargeMessage`. */
|
|
21
|
+
maxBytes?: number | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* Shown when the chosen file exceeds `maxBytes`.
|
|
24
|
+
* @defaultValue `'That file is too large.'` (from `FileInput`)
|
|
25
|
+
*/
|
|
26
|
+
tooLargeMessage?: string | undefined;
|
|
27
|
+
/** Called with the accepted file, or `undefined` when the selection is cleared. */
|
|
28
|
+
onSelect?: ((file: File | undefined) => void) | undefined;
|
|
29
|
+
/** daisyUI size step. */
|
|
30
|
+
size?: FieldSize | undefined;
|
|
31
|
+
/** `FieldControl`: the field is in error — sets `aria-invalid` and the error style. */
|
|
32
|
+
invalid?: boolean | undefined;
|
|
33
|
+
/** `FieldControl`: space-separated ids for `aria-describedby`. */
|
|
34
|
+
describedBy?: string | undefined;
|
|
35
|
+
}
|
|
36
|
+
/** A `FileInput` that also measures the chosen image and previews it. */
|
|
37
|
+
declare const ImageInput: import("svelte").Component<Props, {}, "height" | "width" | "file">;
|
|
38
|
+
type ImageInput = ReturnType<typeof ImageInput>;
|
|
39
|
+
export default ImageInput;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<!-- @component
|
|
2
|
+
A `Toggle` that subscribes this browser to Web Push, or unsubscribes it, through the caller’s `enable` / `disable`.
|
|
3
|
+
-->
|
|
4
|
+
<!--
|
|
5
|
+
PushToggle — the browser half of Web Push behind one `Toggle`: it reads
|
|
6
|
+
whether THIS browser is subscribed, and turns the subscription on or off
|
|
7
|
+
through the caller's `enable` / `disable`.
|
|
8
|
+
|
|
9
|
+
The switch reflects this browser's registration, not the account's
|
|
10
|
+
preference — two different questions, and a settings page asks both.
|
|
11
|
+
`enable` / `disable` are props rather than imports because the network calls
|
|
12
|
+
(subscribing with the VAPID key, registering the endpoint with the app's
|
|
13
|
+
API) belong to the app's own client; injecting them keeps this package free
|
|
14
|
+
of it.
|
|
15
|
+
|
|
16
|
+
Every failure of `enable` is a different thing to tell the reader — the
|
|
17
|
+
browser cannot, the site cannot, or they said no — so `enable` RESOLVES with
|
|
18
|
+
an outcome instead of throwing, and each reason renders its own message.
|
|
19
|
+
The switch writes back what actually happened (see `Toggle`), so a denied
|
|
20
|
+
permission never reads as "on".
|
|
21
|
+
|
|
22
|
+
The subscription state is read on mount, from the browser, rather than
|
|
23
|
+
trusted from a prop: the page may be cached, and the reader may have revoked
|
|
24
|
+
the permission in browser settings since it rendered. `supported` and
|
|
25
|
+
`readSubscribed` default to the real browser checks and exist so a caller
|
|
26
|
+
(or a test) can supply its own.
|
|
27
|
+
|
|
28
|
+
ARIA contract: `Toggle`'s — a native checkbox with `role="switch"`, named by
|
|
29
|
+
`label`. The notice under it is a polite live region, so a refusal is
|
|
30
|
+
announced when it appears.
|
|
31
|
+
-->
|
|
32
|
+
<script lang="ts">import Toggle from "./Toggle.svelte";
|
|
33
|
+
let { available, enable, disable, supported = typeof navigator !== "undefined" && "serviceWorker" in navigator && "PushManager" in globalThis, readSubscribed = async () => {
|
|
34
|
+
const registration = await navigator.serviceWorker.ready;
|
|
35
|
+
return await registration.pushManager.getSubscription() !== null;
|
|
36
|
+
}, label = "Send push notifications to this browser", description, unavailableMessage = "Push notifications aren't available on this site yet.", unsupportedMessage = "This browser can't receive push notifications.", deniedMessage = "This browser is blocking notifications. Allow them in your browser settings to turn this on.", class: className } = $props();
|
|
37
|
+
let subscribed = $state(false);
|
|
38
|
+
let notice = $state(undefined);
|
|
39
|
+
// Aborted on unmount, so a request in flight when the reader navigates away
|
|
40
|
+
// is cancelled rather than resolving into a destroyed component.
|
|
41
|
+
const controller = new AbortController();
|
|
42
|
+
$effect(() => () => controller.abort());
|
|
43
|
+
$effect(() => {
|
|
44
|
+
if (!available) return;
|
|
45
|
+
if (!supported) {
|
|
46
|
+
notice = "unsupported";
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
void readSubscribed(controller.signal).then((value) => {
|
|
50
|
+
subscribed = value;
|
|
51
|
+
}, () => {
|
|
52
|
+
// Unreadable is reported as off; the switch still works.
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
const apply = async (next) => {
|
|
56
|
+
notice = undefined;
|
|
57
|
+
if (!next) {
|
|
58
|
+
await disable(controller.signal);
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
const result = await enable(controller.signal);
|
|
62
|
+
if (result._tag === "enabled") return true;
|
|
63
|
+
notice = result._tag;
|
|
64
|
+
return false;
|
|
65
|
+
};
|
|
66
|
+
const messages = $derived({
|
|
67
|
+
unsupported: unsupportedMessage,
|
|
68
|
+
unavailable: unavailableMessage,
|
|
69
|
+
denied: deniedMessage
|
|
70
|
+
});
|
|
71
|
+
const noticeTone = {
|
|
72
|
+
unsupported: "text-base-content/70",
|
|
73
|
+
unavailable: "text-base-content/70",
|
|
74
|
+
denied: "text-warning"
|
|
75
|
+
};
|
|
76
|
+
</script>
|
|
77
|
+
|
|
78
|
+
{#if !available}
|
|
79
|
+
<p class={['text-base-content/70 text-sm', className]}>{unavailableMessage}</p>
|
|
80
|
+
{:else}
|
|
81
|
+
<div class={['flex flex-col gap-2', className]}>
|
|
82
|
+
<Toggle
|
|
83
|
+
{label}
|
|
84
|
+
{description}
|
|
85
|
+
checked={subscribed}
|
|
86
|
+
disabled={!supported}
|
|
87
|
+
onChange={apply}
|
|
88
|
+
/>
|
|
89
|
+
<p class={['text-sm', notice === undefined ? '' : noticeTone[notice]]} aria-live="polite">
|
|
90
|
+
{notice === undefined ? '' : messages[notice]}
|
|
91
|
+
</p>
|
|
92
|
+
</div>
|
|
93
|
+
{/if}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** Why `enable` did not subscribe this browser. */
|
|
2
|
+
export type PushDeclineReason = 'unsupported' | 'unavailable' | 'denied';
|
|
3
|
+
/**
|
|
4
|
+
* What `enable` resolves with: it worked, or why not. The `_tag` shape
|
|
5
|
+
* matches a tagged-union result an Effect client already returns.
|
|
6
|
+
*/
|
|
7
|
+
export interface PushEnableResult {
|
|
8
|
+
readonly _tag: 'enabled' | PushDeclineReason;
|
|
9
|
+
}
|
|
10
|
+
interface Props {
|
|
11
|
+
/** False when the site has no push configuration (no VAPID key pair): the switch is not offered. */
|
|
12
|
+
available: boolean;
|
|
13
|
+
/** Subscribe this browser and register it. Resolves with the outcome rather than throwing. */
|
|
14
|
+
enable: (signal: AbortSignal) => Promise<PushEnableResult>;
|
|
15
|
+
/** Unsubscribe this browser. Its resolved value is ignored. */
|
|
16
|
+
disable: (signal: AbortSignal) => Promise<unknown>;
|
|
17
|
+
/**
|
|
18
|
+
* Whether this browser can do Web Push at all.
|
|
19
|
+
* @defaultValue feature detection (`serviceWorker` and `PushManager`)
|
|
20
|
+
*/
|
|
21
|
+
supported?: boolean | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* Whether this browser is subscribed now.
|
|
24
|
+
* @defaultValue asks the service worker's `PushManager`
|
|
25
|
+
*/
|
|
26
|
+
readSubscribed?: ((signal: AbortSignal) => Promise<boolean>) | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Text beside the switch; its accessible name.
|
|
29
|
+
* @defaultValue `'Send push notifications to this browser'`
|
|
30
|
+
*/
|
|
31
|
+
label?: string | undefined;
|
|
32
|
+
/** A sentence under the switch. */
|
|
33
|
+
description?: string | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Shown instead of the switch when `available` is false.
|
|
36
|
+
* @defaultValue `"Push notifications aren't available on this site yet."`
|
|
37
|
+
*/
|
|
38
|
+
unavailableMessage?: string | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Shown instead of the switch when the browser cannot do Web Push.
|
|
41
|
+
* @defaultValue `"This browser can't receive push notifications."`
|
|
42
|
+
*/
|
|
43
|
+
unsupportedMessage?: string | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* Shown when the browser’s notification permission is denied.
|
|
46
|
+
* @defaultValue `'This browser is blocking notifications. Allow them in your browser settings to turn this on.'`
|
|
47
|
+
*/
|
|
48
|
+
deniedMessage?: string | undefined;
|
|
49
|
+
/** Extra classes, merged last. */
|
|
50
|
+
class?: string | undefined;
|
|
51
|
+
}
|
|
52
|
+
/** A `Toggle` that subscribes this browser to Web Push, or unsubscribes it, through the caller’s `enable` / `disable`. */
|
|
53
|
+
declare const PushToggle: import("svelte").Component<Props, {}, "">;
|
|
54
|
+
type PushToggle = ReturnType<typeof PushToggle>;
|
|
55
|
+
export default PushToggle;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<!-- @component
|
|
2
|
+
One choice from a short, known list, on bits-ui’s `RadioGroup`.
|
|
3
|
+
-->
|
|
4
|
+
<!--
|
|
5
|
+
RadioGroup — one choice from a short, known list, on bits-ui's `RadioGroup`.
|
|
6
|
+
|
|
7
|
+
bits-ui owns what is genuinely hard here: the roving tabindex (the group is
|
|
8
|
+
ONE tab stop, and the arrow keys move within it), wrap-around, and the
|
|
9
|
+
`aria-checked` bookkeeping. Hand-rolled radio groups get the tab stop wrong
|
|
10
|
+
almost every time.
|
|
11
|
+
|
|
12
|
+
ARIA contract: bits-ui renders `role="radiogroup"` around `role="radio"`
|
|
13
|
+
buttons, and this adds `aria-orientation` beside the `data-orientation`
|
|
14
|
+
bits-ui sets, so the arrow keys a reader is told to use match the ones that
|
|
15
|
+
work. A `role` element cannot be named by a `<legend>` — the legend names
|
|
16
|
+
the `<fieldset>`, which is a different node — so the group renders its
|
|
17
|
+
question as a heading-ish `<span>` and points `aria-labelledby` at it. That
|
|
18
|
+
is why this is the one field group NOT built on `Fieldset`, and the hint and
|
|
19
|
+
errors hang off the radiogroup element itself through `aria-describedby`.
|
|
20
|
+
|
|
21
|
+
The marker is drawn rather than delegated to daisyUI's `.radio`, because
|
|
22
|
+
that class styles an `<input type="radio">` through `:checked` and bits-ui's
|
|
23
|
+
radio is a `<button>`. It is two spans of semantic tokens; `checked` comes
|
|
24
|
+
from bits-ui's own snippet prop, so the visual and the accessibility tree
|
|
25
|
+
cannot disagree.
|
|
26
|
+
-->
|
|
27
|
+
<script lang="ts">import { RadioGroup } from "bits-ui";
|
|
28
|
+
import FieldErrors from "./FieldErrors.svelte";
|
|
29
|
+
import { toFieldOptions } from "./field-types.js";
|
|
30
|
+
let { legend, options, value = $bindable(""), orientation = "vertical", name, hint, errors = [], required = false, requiredLabel = "(required)", disabled = false, class: className, ...rest } = $props();
|
|
31
|
+
const uid = $props.id();
|
|
32
|
+
const legendId = `${uid}-legend`;
|
|
33
|
+
const hintId = `${uid}-hint`;
|
|
34
|
+
const errorId = `${uid}-error`;
|
|
35
|
+
const entries = $derived(toFieldOptions(options));
|
|
36
|
+
const invalid = $derived(errors.length > 0);
|
|
37
|
+
const describedBy = $derived([hint === undefined ? "" : hintId, invalid ? errorId : ""].filter((part) => part !== "").join(" "));
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<div {...rest} class={['fieldset', className]}>
|
|
41
|
+
<span id={legendId} class="fieldset-legend">
|
|
42
|
+
{legend}
|
|
43
|
+
{#if required}
|
|
44
|
+
<span class="text-error" aria-hidden="true">*</span>
|
|
45
|
+
<span class="sr-only">{requiredLabel}</span>
|
|
46
|
+
{/if}
|
|
47
|
+
</span>
|
|
48
|
+
<RadioGroup.Root
|
|
49
|
+
bind:value
|
|
50
|
+
{orientation}
|
|
51
|
+
{...name === undefined ? {} : { name }}
|
|
52
|
+
{required}
|
|
53
|
+
{disabled}
|
|
54
|
+
aria-labelledby={legendId}
|
|
55
|
+
aria-orientation={orientation}
|
|
56
|
+
aria-describedby={describedBy === '' ? undefined : describedBy}
|
|
57
|
+
aria-invalid={invalid ? 'true' : undefined}
|
|
58
|
+
class={['flex gap-3', orientation === 'horizontal' ? 'flex-row flex-wrap' : 'flex-col']}
|
|
59
|
+
>
|
|
60
|
+
{#each entries as option (option.value)}
|
|
61
|
+
<RadioGroup.Item
|
|
62
|
+
value={option.value}
|
|
63
|
+
disabled={option.disabled === true}
|
|
64
|
+
class="rounded-field focus-visible:outline-primary flex cursor-pointer items-center gap-3 text-left focus-visible:outline-2 focus-visible:outline-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
65
|
+
>
|
|
66
|
+
{#snippet children({ checked })}
|
|
67
|
+
<span
|
|
68
|
+
aria-hidden="true"
|
|
69
|
+
class={[
|
|
70
|
+
'grid size-4 shrink-0 place-items-center rounded-full border-2',
|
|
71
|
+
checked ? 'border-primary' : 'border-base-content/40',
|
|
72
|
+
]}
|
|
73
|
+
>
|
|
74
|
+
{#if checked}<span class="bg-primary size-2 rounded-full"></span>{/if}
|
|
75
|
+
</span>
|
|
76
|
+
<span>{option.label}</span>
|
|
77
|
+
{/snippet}
|
|
78
|
+
</RadioGroup.Item>
|
|
79
|
+
{/each}
|
|
80
|
+
</RadioGroup.Root>
|
|
81
|
+
{#if hint !== undefined}
|
|
82
|
+
<p class="fieldset-label" id={hintId}>{hint}</p>
|
|
83
|
+
{/if}
|
|
84
|
+
<FieldErrors {errors} id={errorId} />
|
|
85
|
+
</div>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { RadioGroup } from 'bits-ui';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
import { type FieldOptionInput } from './field-types.ts';
|
|
4
|
+
interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
|
|
5
|
+
/** The question the options answer; names the group. */
|
|
6
|
+
legend: string;
|
|
7
|
+
/** Full options, or bare strings when value and label are the same. */
|
|
8
|
+
options: readonly FieldOptionInput[];
|
|
9
|
+
/**
|
|
10
|
+
* The chosen value; empty string when nothing is chosen.
|
|
11
|
+
* @defaultValue `''`
|
|
12
|
+
*/
|
|
13
|
+
value?: string | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Lay the options out in a column or a row.
|
|
16
|
+
* @defaultValue `'vertical'`
|
|
17
|
+
*/
|
|
18
|
+
orientation?: 'vertical' | 'horizontal' | undefined;
|
|
19
|
+
/** Posted field name — bits-ui renders the hidden input when it is set. */
|
|
20
|
+
name?: string | undefined;
|
|
21
|
+
/** Guidance rendered under the legend. */
|
|
22
|
+
hint?: string | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Validation messages; any at all marks the group invalid.
|
|
25
|
+
* @defaultValue `[]`
|
|
26
|
+
*/
|
|
27
|
+
errors?: readonly string[] | undefined;
|
|
28
|
+
/** Marks the group required and reads `requiredLabel` after the legend. */
|
|
29
|
+
required?: boolean | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* Read out after the legend when `required`.
|
|
32
|
+
* @defaultValue `'(required)'`
|
|
33
|
+
*/
|
|
34
|
+
requiredLabel?: string | undefined;
|
|
35
|
+
/** Disables every option. */
|
|
36
|
+
disabled?: boolean | undefined;
|
|
37
|
+
}
|
|
38
|
+
/** One choice from a short, known list, on bits-ui’s `RadioGroup`. */
|
|
39
|
+
declare const RadioGroup: import("svelte").Component<Props, {}, "value">;
|
|
40
|
+
type RadioGroup = ReturnType<typeof RadioGroup>;
|
|
41
|
+
export default RadioGroup;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<!-- @component
|
|
2
|
+
A `type="search"` field with a leading icon, a clear button while there is text, and a spinner while `pending`. `name` defaults to `'q'` and `placeholder` to `'Search'`.
|
|
3
|
+
-->
|
|
4
|
+
<!--
|
|
5
|
+
SearchInput — a `type="search"` field in daisyUI's composite `input`
|
|
6
|
+
wrapper, with a leading icon, a clear button while there is text, and a
|
|
7
|
+
spinner while `pending`.
|
|
8
|
+
|
|
9
|
+
The wrapper is a `<div>`, not a `<label>`: the clear button lives inside it,
|
|
10
|
+
and a button inside a label is both mis-named (the label's text becomes part
|
|
11
|
+
of ITS name) and label-activated (clicking it also focuses the field). The
|
|
12
|
+
visible label is optional — `label` always supplies the accessible name.
|
|
13
|
+
|
|
14
|
+
ARIA contract: the leading magnifier is `aria-hidden`, because the field's
|
|
15
|
+
own name already says what it is; the clear button is a real button with a
|
|
16
|
+
visually-hidden name rather than a bare glyph; the spinner is `aria-hidden`,
|
|
17
|
+
because a busy state must not change the field's accessible name.
|
|
18
|
+
|
|
19
|
+
`onInput` fires on every keystroke (feed it to whatever debounce the app
|
|
20
|
+
uses) and `onSubmit` on Enter.
|
|
21
|
+
-->
|
|
22
|
+
<script lang="ts">import { Search, X } from "@lucide/svelte";
|
|
23
|
+
import { inputSize } from "./field-types.js";
|
|
24
|
+
let { label, value = $bindable(""), onInput, onSubmit, onClear, pending = false, clearLabel = "Clear search", size = "md", name = "q", placeholder = "Search", class: className, ...rest } = $props();
|
|
25
|
+
const clear = () => {
|
|
26
|
+
value = "";
|
|
27
|
+
onInput?.("");
|
|
28
|
+
onClear?.();
|
|
29
|
+
};
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<div class={['input w-full', inputSize[size], className]}>
|
|
33
|
+
<Search class="size-4 opacity-60" aria-hidden="true" />
|
|
34
|
+
<input
|
|
35
|
+
{...rest}
|
|
36
|
+
type="search"
|
|
37
|
+
{name}
|
|
38
|
+
{placeholder}
|
|
39
|
+
aria-label={label}
|
|
40
|
+
bind:value
|
|
41
|
+
oninput={() => onInput?.(value)}
|
|
42
|
+
onkeydown={(event) => {
|
|
43
|
+
if (event.key === 'Enter') {
|
|
44
|
+
event.preventDefault();
|
|
45
|
+
onSubmit?.(value);
|
|
46
|
+
}
|
|
47
|
+
}}
|
|
48
|
+
/>
|
|
49
|
+
{#if pending}
|
|
50
|
+
<span class="loading loading-spinner loading-xs" aria-hidden="true"></span>
|
|
51
|
+
{:else if value !== ''}
|
|
52
|
+
<button type="button" class="btn btn-ghost btn-xs btn-circle" onclick={clear}>
|
|
53
|
+
<X class="size-3.5" aria-hidden="true" />
|
|
54
|
+
<span class="sr-only">{clearLabel}</span>
|
|
55
|
+
</button>
|
|
56
|
+
{/if}
|
|
57
|
+
</div>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
2
|
+
import { type FieldSize } from './field-types.ts';
|
|
3
|
+
interface Props extends Omit<HTMLInputAttributes, 'type' | 'value' | 'size'> {
|
|
4
|
+
/** Accessible name of the field. */
|
|
5
|
+
label: string;
|
|
6
|
+
/**
|
|
7
|
+
* The query. Bindable.
|
|
8
|
+
* @defaultValue `''`
|
|
9
|
+
*/
|
|
10
|
+
value?: string | undefined;
|
|
11
|
+
/** Fires on every change with the current text. */
|
|
12
|
+
onInput?: ((query: string) => void) | undefined;
|
|
13
|
+
/** Fires on Enter with the current text. */
|
|
14
|
+
onSubmit?: ((query: string) => void) | undefined;
|
|
15
|
+
/** Fires after the clear button empties the field. */
|
|
16
|
+
onClear?: (() => void) | undefined;
|
|
17
|
+
/** A search is in flight. */
|
|
18
|
+
pending?: boolean | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Accessible name of the clear button.
|
|
21
|
+
* @defaultValue `'Clear search'`
|
|
22
|
+
*/
|
|
23
|
+
clearLabel?: string | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* daisyUI size step.
|
|
26
|
+
* @defaultValue `'md'`
|
|
27
|
+
*/
|
|
28
|
+
size?: FieldSize | undefined;
|
|
29
|
+
}
|
|
30
|
+
/** A `type="search"` field with a leading icon, a clear button while there is text, and a spinner while `pending`. `name` defaults to `'q'` and `placeholder` to `'Search'`. */
|
|
31
|
+
declare const SearchInput: import("svelte").Component<Props, {}, "value">;
|
|
32
|
+
type SearchInput = ReturnType<typeof SearchInput>;
|
|
33
|
+
export default SearchInput;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<!-- @component
|
|
2
|
+
A native `<select>` in daisyUI styling over a list of options, with an optional placeholder and `FieldControl` wiring.
|
|
3
|
+
-->
|
|
4
|
+
<!--
|
|
5
|
+
Select — a native `<select>` in daisyUI `select` styling over a list of
|
|
6
|
+
options, with a bindable `value`, an optional placeholder option (disabled,
|
|
7
|
+
selected while `value` is empty) and the `FieldControl` wiring.
|
|
8
|
+
|
|
9
|
+
Native, not bits-ui, and deliberately: a native select is keyboard-complete
|
|
10
|
+
and typeahead-complete without a line of script, and on a phone it opens the
|
|
11
|
+
platform picker. The rich alternative is `Combobox`, which exists for the
|
|
12
|
+
case a select genuinely cannot serve — a list too long to scroll, or one
|
|
13
|
+
that has to be fetched.
|
|
14
|
+
|
|
15
|
+
ARIA contract: the platform's. `describedBy` and `invalid` are the only
|
|
16
|
+
attributes added.
|
|
17
|
+
-->
|
|
18
|
+
<script lang="ts">import { selectSize, toFieldOptions } from "./field-types.js";
|
|
19
|
+
let { options, value = $bindable(""), placeholder, size = "md", invalid = false, describedBy = "", class: className, ...rest } = $props();
|
|
20
|
+
const entries = $derived(toFieldOptions(options));
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<select
|
|
24
|
+
{...rest}
|
|
25
|
+
bind:value
|
|
26
|
+
aria-invalid={invalid ? 'true' : undefined}
|
|
27
|
+
aria-describedby={describedBy === '' ? undefined : describedBy}
|
|
28
|
+
class={['select w-full', selectSize[size], invalid && 'select-error', className]}
|
|
29
|
+
>
|
|
30
|
+
{#if placeholder !== undefined}
|
|
31
|
+
<option value="" disabled>{placeholder}</option>
|
|
32
|
+
{/if}
|
|
33
|
+
{#each entries as option (option.value)}
|
|
34
|
+
<option value={option.value} disabled={option.disabled === true}>{option.label}</option>
|
|
35
|
+
{/each}
|
|
36
|
+
</select>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { HTMLSelectAttributes } from 'svelte/elements';
|
|
2
|
+
import { type FieldOptionInput, type FieldSize } from './field-types.ts';
|
|
3
|
+
interface Props extends Omit<HTMLSelectAttributes, 'value' | 'size'> {
|
|
4
|
+
/** Full options, or bare strings when value and label are the same. */
|
|
5
|
+
options: readonly FieldOptionInput[];
|
|
6
|
+
/**
|
|
7
|
+
* The selected option’s value; `''` selects the placeholder. Bindable.
|
|
8
|
+
* @defaultValue `''`
|
|
9
|
+
*/
|
|
10
|
+
value?: string | undefined;
|
|
11
|
+
/** Disabled first option, shown while nothing is selected. */
|
|
12
|
+
placeholder?: string | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* daisyUI size step.
|
|
15
|
+
* @defaultValue `'md'`
|
|
16
|
+
*/
|
|
17
|
+
size?: FieldSize | undefined;
|
|
18
|
+
/** `FieldControl`: the field is in error — sets `aria-invalid` and the error style. */
|
|
19
|
+
invalid?: boolean | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* `FieldControl`: space-separated ids for `aria-describedby`.
|
|
22
|
+
* @defaultValue `''`
|
|
23
|
+
*/
|
|
24
|
+
describedBy?: string | undefined;
|
|
25
|
+
}
|
|
26
|
+
/** A native `<select>` in daisyUI styling over a list of options, with an optional placeholder and `FieldControl` wiring. */
|
|
27
|
+
declare const Select: import("svelte").Component<Props, {}, "value">;
|
|
28
|
+
type Select = ReturnType<typeof Select>;
|
|
29
|
+
export default Select;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<!-- @component
|
|
2
|
+
A daisyUI `input` over a native `<input>`, with a bindable `value` and `FieldControl` wiring.
|
|
3
|
+
-->
|
|
4
|
+
<!--
|
|
5
|
+
TextInput — daisyUI `input` over a native `<input>`, with a bindable `value`
|
|
6
|
+
and the `FieldControl` wiring (`describedBy` → `aria-describedby`, `invalid`
|
|
7
|
+
→ `aria-invalid` + `input-error`).
|
|
8
|
+
|
|
9
|
+
ARIA contract: a native input, so the role, the keyboard and the validation
|
|
10
|
+
semantics are the platform's. The component adds only the two attributes
|
|
11
|
+
that tie it back to its `FormField`; everything else — `name`, `autocomplete`,
|
|
12
|
+
`maxlength`, `pattern`, `data-*` — passes straight through, because a
|
|
13
|
+
control that swallows those cannot be submitted, tested or hooked into.
|
|
14
|
+
-->
|
|
15
|
+
<script lang="ts">import { inputSize } from "./field-types.js";
|
|
16
|
+
let { value = $bindable(""), type = "text", size = "md", invalid = false, describedBy = "", class: className, ...rest } = $props();
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<input
|
|
20
|
+
{...rest}
|
|
21
|
+
{type}
|
|
22
|
+
bind:value
|
|
23
|
+
aria-invalid={invalid ? 'true' : undefined}
|
|
24
|
+
aria-describedby={describedBy === '' ? undefined : describedBy}
|
|
25
|
+
class={['input w-full', inputSize[size], invalid && 'input-error', className]}
|
|
26
|
+
/>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
2
|
+
import { type FieldSize } from './field-types.ts';
|
|
3
|
+
interface Props extends Omit<HTMLInputAttributes, 'type' | 'value' | 'size'> {
|
|
4
|
+
/**
|
|
5
|
+
* The text. Bindable.
|
|
6
|
+
* @defaultValue `''`
|
|
7
|
+
*/
|
|
8
|
+
value?: string | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* The text-ish `type`s. Dates have `DateInput`, files have `FileInput`.
|
|
11
|
+
* @defaultValue `'text'`
|
|
12
|
+
*/
|
|
13
|
+
type?: 'text' | 'email' | 'url' | 'password' | 'tel' | 'number' | 'search' | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* daisyUI size step.
|
|
16
|
+
* @defaultValue `'md'`
|
|
17
|
+
*/
|
|
18
|
+
size?: FieldSize | undefined;
|
|
19
|
+
/** `FieldControl`: the field is in error — sets `aria-invalid` and the error style. */
|
|
20
|
+
invalid?: boolean | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Ids for `aria-describedby`; empty means none.
|
|
23
|
+
* @defaultValue `''`
|
|
24
|
+
*/
|
|
25
|
+
describedBy?: string | undefined;
|
|
26
|
+
}
|
|
27
|
+
/** A daisyUI `input` over a native `<input>`, with a bindable `value` and `FieldControl` wiring. */
|
|
28
|
+
declare const TextInput: import("svelte").Component<Props, {}, "value">;
|
|
29
|
+
type TextInput = ReturnType<typeof TextInput>;
|
|
30
|
+
export default TextInput;
|