@thomasfosterau/components 0.0.0-stage → 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 +817 -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 +101 -3
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<!-- @component
|
|
2
|
+
A `mailto:` link whose subject, body, cc and bcc are correctly encoded.
|
|
3
|
+
-->
|
|
4
|
+
<!--
|
|
5
|
+
EmailLink — a `mailto:` anchor, its query string (`subject`/`body`/`cc`/
|
|
6
|
+
`bcc`) built with `URLSearchParams` so every value is correctly encoded.
|
|
7
|
+
|
|
8
|
+
ARIA contract: none beyond the native `<a>` — the visible text (the address
|
|
9
|
+
by default, or `children` when given) is the accessible name.
|
|
10
|
+
-->
|
|
11
|
+
<script lang="ts">let { email, subject, body, cc, bcc, children, class: className, ...rest } = $props();
|
|
12
|
+
const href = $derived.by(() => {
|
|
13
|
+
const params = new URLSearchParams();
|
|
14
|
+
if (subject) params.set("subject", subject);
|
|
15
|
+
if (body) params.set("body", body);
|
|
16
|
+
if (cc) params.set("cc", cc);
|
|
17
|
+
if (bcc) params.set("bcc", bcc);
|
|
18
|
+
const query = params.toString();
|
|
19
|
+
return `mailto:${email}${query ? `?${query}` : ""}`;
|
|
20
|
+
});
|
|
21
|
+
export {};
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<a {...rest} {href} class={['link link-primary link-hover', className]}>
|
|
25
|
+
{#if children}{@render children()}{:else}{email}{/if}
|
|
26
|
+
</a>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAnchorAttributes } from 'svelte/elements';
|
|
3
|
+
interface Props extends Omit<HTMLAnchorAttributes, 'children'> {
|
|
4
|
+
/** The address; also the link text unless `children` replaces it. */
|
|
5
|
+
email: string;
|
|
6
|
+
/** Pre-filled subject line. */
|
|
7
|
+
subject?: string | undefined;
|
|
8
|
+
/** Pre-filled message body. */
|
|
9
|
+
body?: string | undefined;
|
|
10
|
+
/** Address to copy. */
|
|
11
|
+
cc?: string | undefined;
|
|
12
|
+
/** Address to blind-copy. */
|
|
13
|
+
bcc?: string | undefined;
|
|
14
|
+
/** Replaces the link text; defaults to the address itself. */
|
|
15
|
+
children?: Snippet<[]> | undefined;
|
|
16
|
+
}
|
|
17
|
+
/** A `mailto:` link whose subject, body, cc and bcc are correctly encoded. */
|
|
18
|
+
declare const EmailLink: import("svelte").Component<Props, {}, "">;
|
|
19
|
+
type EmailLink = ReturnType<typeof EmailLink>;
|
|
20
|
+
export default EmailLink;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<!-- @component
|
|
2
|
+
A `<figure>` with an optional `<figcaption>`, for media that needs a caption.
|
|
3
|
+
-->
|
|
4
|
+
<!--
|
|
5
|
+
Figure — a `<figure>` with an optional `<figcaption>`, for media that needs
|
|
6
|
+
a caption associated with it (an image, a video, a diagram).
|
|
7
|
+
|
|
8
|
+
ARIA contract: none beyond the native grouping — `<figure>`/`<figcaption>`
|
|
9
|
+
already associate the caption with its content for assistive technology.
|
|
10
|
+
-->
|
|
11
|
+
<script lang="ts">let { caption, children, class: className, ...rest } = $props();
|
|
12
|
+
export {};
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<figure {...rest} class={className}>
|
|
16
|
+
{@render children()}
|
|
17
|
+
{#if caption}
|
|
18
|
+
<figcaption>
|
|
19
|
+
{#if typeof caption === 'string'}
|
|
20
|
+
{caption}
|
|
21
|
+
{:else}
|
|
22
|
+
{@render caption()}
|
|
23
|
+
{/if}
|
|
24
|
+
</figcaption>
|
|
25
|
+
{/if}
|
|
26
|
+
</figure>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
interface Props extends HTMLAttributes<HTMLElement> {
|
|
4
|
+
/** Rendered as `<figcaption>`; a plain string or a snippet for richer markup. */
|
|
5
|
+
caption?: string | Snippet<[]> | undefined;
|
|
6
|
+
/** The image, video or diagram the caption describes. */
|
|
7
|
+
children: Snippet<[]>;
|
|
8
|
+
}
|
|
9
|
+
/** A `<figure>` with an optional `<figcaption>`, for media that needs a caption. */
|
|
10
|
+
declare const Figure: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type Figure = ReturnType<typeof Figure>;
|
|
12
|
+
export default Figure;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<!-- @component
|
|
2
|
+
A responsive `<img>` from a URL or an uploaded image id, optionally wrapped in `<picture>`. `decoding` defaults to `'async'`.
|
|
3
|
+
-->
|
|
4
|
+
<!--
|
|
5
|
+
Image — a responsive `<img>`, optionally wrapped in `<picture>`.
|
|
6
|
+
|
|
7
|
+
Two ways to get a `srcset`: pass `src` directly for a plain image, or pass
|
|
8
|
+
`id` (+ optionally `base`/`widths`) for a width-bucketed rendition served
|
|
9
|
+
by an image-transform route (`${base}/${id}?w=${w}`). Requesting an arbitrary width
|
|
10
|
+
would defeat that bucketing (every distinct width is a distinct transform
|
|
11
|
+
and a distinct cache entry), so `widths` is a fixed list, not a free number.
|
|
12
|
+
|
|
13
|
+
`sources` adds art-direction / format-negotiation `<source>` elements ahead
|
|
14
|
+
of the `<img>` fallback, wrapping everything in `<picture>` — each source's
|
|
15
|
+
own `srcset` is the caller's (e.g. an AVIF/WebP rendition list), unrelated
|
|
16
|
+
to the `id`-bucketing above.
|
|
17
|
+
|
|
18
|
+
`width`/`height` reserve the box before the bytes arrive — the single most
|
|
19
|
+
visible layout-shift regression a media-heavy page can ship — so pass them
|
|
20
|
+
whenever they are known.
|
|
21
|
+
|
|
22
|
+
ARIA contract: `alt` is required and not defaulted — a caller with no
|
|
23
|
+
meaningful text must pass `""` (decorative) explicitly, which is a
|
|
24
|
+
different, deliberate claim from having forgotten it.
|
|
25
|
+
-->
|
|
26
|
+
<script lang="ts">const DEFAULT_WIDTHS = [
|
|
27
|
+
320,
|
|
28
|
+
640,
|
|
29
|
+
960,
|
|
30
|
+
1280,
|
|
31
|
+
1920
|
|
32
|
+
];
|
|
33
|
+
let { alt, src, id, base = "/images", widths = DEFAULT_WIDTHS, sources, width, height, sizes = "100vw", eager = false, decoding = "async", ...rest } = $props();
|
|
34
|
+
const bucketedHref = $derived(id === undefined ? undefined : `${base}/${id}`);
|
|
35
|
+
const bucketedSrcset = $derived(bucketedHref === undefined ? undefined : widths.map((w) => `${bucketedHref}?w=${w} ${w}w`).join(", "));
|
|
36
|
+
// The largest bucket as the plain `src`: the fallback a browser without
|
|
37
|
+
// `srcset` support (and every crawler) fetches.
|
|
38
|
+
const fallbackWidth = $derived(widths[widths.length - 1]);
|
|
39
|
+
const resolvedSrc = $derived(src ?? (bucketedHref === undefined ? undefined : `${bucketedHref}?w=${fallbackWidth}`));
|
|
40
|
+
const loading = $derived(eager ? "eager" : "lazy");
|
|
41
|
+
const fetchpriority = $derived(eager ? "high" : undefined);
|
|
42
|
+
export {};
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
{#if sources && sources.length > 0}
|
|
46
|
+
<picture>
|
|
47
|
+
{#each sources as source (source.srcset)}
|
|
48
|
+
<source srcset={source.srcset} type={source.type} media={source.media} />
|
|
49
|
+
{/each}
|
|
50
|
+
<img
|
|
51
|
+
{...rest}
|
|
52
|
+
src={resolvedSrc}
|
|
53
|
+
srcset={bucketedSrcset}
|
|
54
|
+
{sizes}
|
|
55
|
+
{alt}
|
|
56
|
+
{width}
|
|
57
|
+
{height}
|
|
58
|
+
{loading}
|
|
59
|
+
{fetchpriority}
|
|
60
|
+
{decoding}
|
|
61
|
+
/>
|
|
62
|
+
</picture>
|
|
63
|
+
{:else}
|
|
64
|
+
<img
|
|
65
|
+
{...rest}
|
|
66
|
+
src={resolvedSrc}
|
|
67
|
+
srcset={bucketedSrcset}
|
|
68
|
+
{sizes}
|
|
69
|
+
{alt}
|
|
70
|
+
{width}
|
|
71
|
+
{height}
|
|
72
|
+
{loading}
|
|
73
|
+
{fetchpriority}
|
|
74
|
+
{decoding}
|
|
75
|
+
/>
|
|
76
|
+
{/if}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { HTMLImgAttributes } from 'svelte/elements';
|
|
2
|
+
interface ImageSource {
|
|
3
|
+
srcset: string;
|
|
4
|
+
type?: string | undefined;
|
|
5
|
+
media?: string | undefined;
|
|
6
|
+
}
|
|
7
|
+
interface Props extends HTMLImgAttributes {
|
|
8
|
+
/** Alternative text; `''` only where the image is decoration. */
|
|
9
|
+
alt: string;
|
|
10
|
+
/** A plain image URL. Mutually exclusive with `id`; `src` wins if both are given. */
|
|
11
|
+
src?: string | undefined;
|
|
12
|
+
/** An uploaded image's id — combined with `base`/`widths` into a bucketed `srcset`. */
|
|
13
|
+
id?: string | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Route prefix the bucketed rendition lives under.
|
|
16
|
+
* @defaultValue `'/images'`
|
|
17
|
+
*/
|
|
18
|
+
base?: string | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* The width buckets the transform route serves. Must match its own list.
|
|
21
|
+
* @defaultValue `[320, 640, 960, 1280, 1920]`
|
|
22
|
+
*/
|
|
23
|
+
widths?: readonly number[] | undefined;
|
|
24
|
+
/** Extra `<source>` elements (art direction / format negotiation) ahead of the `<img>` fallback. Wraps everything in `<picture>`. */
|
|
25
|
+
sources?: readonly ImageSource[] | undefined;
|
|
26
|
+
/** Intrinsic width; reserves the box against layout shift. */
|
|
27
|
+
width?: number | string | undefined;
|
|
28
|
+
/** Intrinsic height; reserves the box against layout shift. */
|
|
29
|
+
height?: number | string | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* The `sizes` hint.
|
|
32
|
+
* @defaultValue `'100vw'`
|
|
33
|
+
*/
|
|
34
|
+
sizes?: string | undefined;
|
|
35
|
+
/** Eager-load this image. Use for the one image above the fold; everything else stays lazy. */
|
|
36
|
+
eager?: boolean | undefined;
|
|
37
|
+
}
|
|
38
|
+
/** A responsive `<img>` from a URL or an uploaded image id, optionally wrapped in `<picture>`. `decoding` defaults to `'async'`. */
|
|
39
|
+
declare const Image: import("svelte").Component<Props, {}, "">;
|
|
40
|
+
type Image = ReturnType<typeof Image>;
|
|
41
|
+
export default Image;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<!-- @component
|
|
2
|
+
An HLS video player (Cloudflare Stream shape) with a placeholder while the upload is still pending or processing.
|
|
3
|
+
-->
|
|
4
|
+
<!--
|
|
5
|
+
Video — the player for an HLS-served video (Cloudflare Stream shape:
|
|
6
|
+
manifest URL + poster + an upload lifecycle).
|
|
7
|
+
|
|
8
|
+
Deliberately a plain `<video>` with the HLS manifest as its source and the
|
|
9
|
+
generated thumbnail as its poster. That plays natively in Safari and on
|
|
10
|
+
iOS; everywhere else it needs an MSE shim (e.g. hls.js), which is an
|
|
11
|
+
app-side concern and stays out of this package — it is a real dependency
|
|
12
|
+
weight that must never land in the always-loaded closure of a package every
|
|
13
|
+
page imports. A caller that wants universal playback attaches the shim to
|
|
14
|
+
the element handed to `onReady`.
|
|
15
|
+
|
|
16
|
+
While the upload is still pending/processing there is no manifest at all,
|
|
17
|
+
so the component renders the poster (or a neutral placeholder) with the
|
|
18
|
+
state spelled out via `statusLabel` rather than an empty player that
|
|
19
|
+
silently fails.
|
|
20
|
+
|
|
21
|
+
ARIA contract: `title` is the player's accessible name (`aria-label`); the
|
|
22
|
+
non-playable placeholder's `statusLabel` is a `badge-neutral`, visible text,
|
|
23
|
+
not conveyed through colour alone.
|
|
24
|
+
-->
|
|
25
|
+
<script lang="ts">let { hlsUrl, posterUrl, title, width, height, uploadState = "ready", statusLabel, onReady, captionsSrc, class: className } = $props();
|
|
26
|
+
let element = $state();
|
|
27
|
+
const playable = $derived(uploadState === "ready" && hlsUrl !== undefined);
|
|
28
|
+
$effect(() => {
|
|
29
|
+
if (element && playable) onReady?.(element);
|
|
30
|
+
});
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<div class={['bg-base-200 rounded-box overflow-hidden', className]}>
|
|
34
|
+
{#if playable}
|
|
35
|
+
<video
|
|
36
|
+
bind:this={element}
|
|
37
|
+
controls
|
|
38
|
+
preload="none"
|
|
39
|
+
poster={posterUrl}
|
|
40
|
+
{width}
|
|
41
|
+
{height}
|
|
42
|
+
aria-label={title}
|
|
43
|
+
class="aspect-video h-auto w-full max-w-full"
|
|
44
|
+
>
|
|
45
|
+
<source src={hlsUrl} type="application/x-mpegURL" />
|
|
46
|
+
{#if captionsSrc}
|
|
47
|
+
<track kind="captions" src={captionsSrc} />
|
|
48
|
+
{:else}
|
|
49
|
+
<!-- A `<track>` is required for a caption-less video to be valid; an
|
|
50
|
+
empty default track is the honest statement that none exists yet,
|
|
51
|
+
rather than silently shipping a video with no captions option. -->
|
|
52
|
+
<track kind="captions" />
|
|
53
|
+
{/if}
|
|
54
|
+
</video>
|
|
55
|
+
{:else}
|
|
56
|
+
<div class="aspect-video relative flex w-full items-center justify-center">
|
|
57
|
+
{#if posterUrl}
|
|
58
|
+
<img src={posterUrl} alt="" class="h-full w-full object-cover opacity-60" />
|
|
59
|
+
{/if}
|
|
60
|
+
{#if statusLabel}
|
|
61
|
+
<span class="badge badge-neutral absolute">{statusLabel}</span>
|
|
62
|
+
{/if}
|
|
63
|
+
</div>
|
|
64
|
+
{/if}
|
|
65
|
+
</div>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
/** The HLS manifest URL; absent until the encode finishes. */
|
|
3
|
+
hlsUrl?: string | undefined;
|
|
4
|
+
/** The poster frame. */
|
|
5
|
+
posterUrl?: string | undefined;
|
|
6
|
+
/** Accessible name for the player. */
|
|
7
|
+
title: string;
|
|
8
|
+
/** Intrinsic dimensions, when known — reserves the box against layout shift. */
|
|
9
|
+
width?: number | undefined;
|
|
10
|
+
/** Intrinsic height, when known. */
|
|
11
|
+
height?: number | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* Where the upload is in its lifecycle.
|
|
14
|
+
* @defaultValue `'ready'`
|
|
15
|
+
*/
|
|
16
|
+
uploadState?: 'pending' | 'processing' | 'ready' | 'failed' | undefined;
|
|
17
|
+
/** Copy for the non-playable placeholder state, supplied by the app (this package holds no messages). */
|
|
18
|
+
statusLabel?: string | undefined;
|
|
19
|
+
/** Called with the mounted `<video>` element once it is playable, for a caller attaching an MSE shim. */
|
|
20
|
+
onReady?: ((element: HTMLVideoElement) => void) | undefined;
|
|
21
|
+
/** A captions track URL (WebVTT). When absent, an empty track is still rendered — see below. */
|
|
22
|
+
captionsSrc?: string | undefined;
|
|
23
|
+
/** Extra classes, merged last. */
|
|
24
|
+
class?: string | undefined;
|
|
25
|
+
}
|
|
26
|
+
/** An HLS video player (Cloudflare Stream shape) with a placeholder while the upload is still pending or processing. */
|
|
27
|
+
declare const Video: import("svelte").Component<Props, {}, "">;
|
|
28
|
+
type Video = ReturnType<typeof Video>;
|
|
29
|
+
export default Video;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading an image file's intrinsic pixel dimensions in the browser.
|
|
3
|
+
*
|
|
4
|
+
* A form that uploads an image wants its `width` / `height` alongside the
|
|
5
|
+
* binary, so the page that later renders it can reserve the box on first
|
|
6
|
+
* paint instead of reflowing when it loads — and the browser can decode the
|
|
7
|
+
* file for free the moment it is chosen, where a server would have to.
|
|
8
|
+
* `fields/ImageInput` measures with this; a caller with its own file control
|
|
9
|
+
* uses it directly.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
/** An image's intrinsic size, in pixels. */
|
|
14
|
+
export interface ImageDimensions {
|
|
15
|
+
readonly width: number;
|
|
16
|
+
readonly height: number;
|
|
17
|
+
}
|
|
18
|
+
/** Thrown by {@link imageDimensions} where there is no `createImageBitmap` (on a server). */
|
|
19
|
+
export declare class ImageDimensionsUnavailable extends Error {
|
|
20
|
+
readonly name = "ImageDimensionsUnavailable";
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Decode `file` with `createImageBitmap` and return its pixel size.
|
|
24
|
+
*
|
|
25
|
+
* Rejects with {@link ImageDimensionsUnavailable} outside a browser, and with
|
|
26
|
+
* the browser's own error when the file does not decode as an image.
|
|
27
|
+
*/
|
|
28
|
+
export declare function imageDimensions(file: Blob): Promise<ImageDimensions>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading an image file's intrinsic pixel dimensions in the browser.
|
|
3
|
+
*
|
|
4
|
+
* A form that uploads an image wants its `width` / `height` alongside the
|
|
5
|
+
* binary, so the page that later renders it can reserve the box on first
|
|
6
|
+
* paint instead of reflowing when it loads — and the browser can decode the
|
|
7
|
+
* file for free the moment it is chosen, where a server would have to.
|
|
8
|
+
* `fields/ImageInput` measures with this; a caller with its own file control
|
|
9
|
+
* uses it directly.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
/** Thrown by {@link imageDimensions} where there is no `createImageBitmap` (on a server). */
|
|
14
|
+
export class ImageDimensionsUnavailable extends Error {
|
|
15
|
+
name = 'ImageDimensionsUnavailable';
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Decode `file` with `createImageBitmap` and return its pixel size.
|
|
19
|
+
*
|
|
20
|
+
* Rejects with {@link ImageDimensionsUnavailable} outside a browser, and with
|
|
21
|
+
* the browser's own error when the file does not decode as an image.
|
|
22
|
+
*/
|
|
23
|
+
export async function imageDimensions(file) {
|
|
24
|
+
if (typeof globalThis.createImageBitmap !== 'function') {
|
|
25
|
+
throw new ImageDimensionsUnavailable('imageDimensions needs createImageBitmap, which only a browser has.');
|
|
26
|
+
}
|
|
27
|
+
const bitmap = await globalThis.createImageBitmap(file);
|
|
28
|
+
try {
|
|
29
|
+
return { width: bitmap.width, height: bitmap.height };
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
bitmap.close();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* media — images, video and figures. See the package README for the family
|
|
3
|
+
* reference.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
export { default as Image } from './Image.svelte';
|
|
8
|
+
export { default as Video } from './Video.svelte';
|
|
9
|
+
export { default as Figure } from './Figure.svelte';
|
|
10
|
+
export { imageDimensions, ImageDimensionsUnavailable, type ImageDimensions, } from './image-dimensions.ts';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* media — images, video and figures. See the package README for the family
|
|
3
|
+
* reference.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
export { default as Image } from './Image.svelte';
|
|
8
|
+
export { default as Video } from './Video.svelte';
|
|
9
|
+
export { default as Figure } from './Figure.svelte';
|
|
10
|
+
export { imageDimensions, ImageDimensionsUnavailable, } from "./image-dimensions.js";
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
<!-- @component
|
|
2
|
+
The ⌘K / Ctrl+K command palette: bits-ui’s `Command` inside this library’s `Dialog`.
|
|
3
|
+
-->
|
|
4
|
+
<!--
|
|
5
|
+
CommandPalette — the ⌘K / Ctrl+K palette: bits-ui's `Command` inside this
|
|
6
|
+
library's `Dialog`.
|
|
7
|
+
|
|
8
|
+
Two sources of rows, either of which may be absent:
|
|
9
|
+
|
|
10
|
+
- **The hotkey registry**, when one is mounted above. `paletteActions` is
|
|
11
|
+
already role-filtered and in registration order, and each row shows its
|
|
12
|
+
trigger as a `HotkeyBadge`, so the palette teaches the keyboard while it
|
|
13
|
+
is used with the mouse. The palette also binds its open state to the
|
|
14
|
+
registry's `paletteOpen`, which is what a registered `$mod+k` action
|
|
15
|
+
toggles.
|
|
16
|
+
- **`items` and `onSearch`**, which need no registry at all. That is what
|
|
17
|
+
makes this usable in an app that wants a palette but not a hotkey system.
|
|
18
|
+
|
|
19
|
+
The search lifecycle is `fields/Combobox`'s, for the same reasons: a
|
|
20
|
+
keystroke does not start a request, `debounceMs` of quiet does; each request
|
|
21
|
+
gets an `AbortController` and starting a new one aborts the last, so a slow
|
|
22
|
+
first response cannot overwrite a fast second one; and the previous rows
|
|
23
|
+
stay on screen while the next request is in flight, because clearing on
|
|
24
|
+
every keystroke makes the list strobe and costs the reader the row they were
|
|
25
|
+
about to pick.
|
|
26
|
+
|
|
27
|
+
`shouldFilter` is off exactly when `onSearch` is given: async rows are
|
|
28
|
+
already the server's answer to the query, and bits-ui re-filtering them
|
|
29
|
+
would hide rows whose match is in a field the label doesn't show. Without
|
|
30
|
+
`onSearch`, bits-ui's own scoring is better than anything here. Registry and
|
|
31
|
+
`items` rows are filtered locally by label in the async case, so they do not
|
|
32
|
+
outlive the query either.
|
|
33
|
+
|
|
34
|
+
ARIA contract: `Dialog`'s (bits-ui: `aria-modal`, the focus trap, Escape,
|
|
35
|
+
the focus return) plus `Command`'s — the input is a `combobox` owning a
|
|
36
|
+
`listbox`, each row an `option`, `aria-activedescendant` follows the arrow
|
|
37
|
+
keys and Enter selects. `label` names the command menu for assistive
|
|
38
|
+
technology and also titles the dialog. The pending row is
|
|
39
|
+
`Command.Loading`, named by `loadingLabel`, so a reader is told the list is
|
|
40
|
+
still settling instead of hearing an empty list.
|
|
41
|
+
-->
|
|
42
|
+
<script lang="ts">import { Command } from "bits-ui";
|
|
43
|
+
import { untrack } from "svelte";
|
|
44
|
+
import { getComponentsContext } from "../context.svelte.js";
|
|
45
|
+
import Dialog from "../feedback/Dialog.svelte";
|
|
46
|
+
import HotkeyBadge from "../hotkeys/HotkeyBadge.svelte";
|
|
47
|
+
import { tryGetHotkeyRegistry } from "../hotkeys/context.svelte.js";
|
|
48
|
+
import Badge from "../ui/Badge.svelte";
|
|
49
|
+
import { defaultCommandGroups } from "./types.js";
|
|
50
|
+
let { open = $bindable(false), label = "Command palette", placeholder = "Type a command or search…", loadingLabel = "Searching…", emptyLabel = "No results", closeLabel = "Close", groups = defaultCommandGroups, items, onSearch, debounceMs = 150, onSelect, item } = $props();
|
|
51
|
+
const context = getComponentsContext();
|
|
52
|
+
const registry = tryGetHotkeyRegistry();
|
|
53
|
+
let query = $state("");
|
|
54
|
+
const trimmedQuery = $derived(query.trim());
|
|
55
|
+
// ── Open state ───────────────────────────────────────────────────
|
|
56
|
+
//
|
|
57
|
+
// With a registry, `paletteOpen` and `open` are two halves of one value:
|
|
58
|
+
// the registered `$mod+k` action writes the registry, a caller may write
|
|
59
|
+
// the prop, and both have to end up agreeing. The trackers below make the
|
|
60
|
+
// sync converge — whichever side changed is copied to the other and both
|
|
61
|
+
// trackers are updated, so the effect's own writes find nothing to do on
|
|
62
|
+
// the re-run.
|
|
63
|
+
let lastOpen = open;
|
|
64
|
+
let lastPaletteOpen = registry?.paletteOpen ?? open;
|
|
65
|
+
$effect(() => {
|
|
66
|
+
if (registry === undefined) return;
|
|
67
|
+
const fromProp = open;
|
|
68
|
+
const fromRegistry = registry.paletteOpen;
|
|
69
|
+
if (fromProp !== lastOpen) {
|
|
70
|
+
lastOpen = fromProp;
|
|
71
|
+
lastPaletteOpen = fromProp;
|
|
72
|
+
registry.paletteOpen = fromProp;
|
|
73
|
+
} else if (fromRegistry !== lastPaletteOpen) {
|
|
74
|
+
lastPaletteOpen = fromRegistry;
|
|
75
|
+
lastOpen = fromRegistry;
|
|
76
|
+
open = fromRegistry;
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
// bits-ui's `Dialog` moves focus to the first tabbable child as it opens,
|
|
80
|
+
// which here is the dialog's own close button. A palette has to open with
|
|
81
|
+
// the cursor in its query box — anything else costs the reader a Tab before
|
|
82
|
+
// they can type, and leaves the arrow keys (which `Command` handles on the
|
|
83
|
+
// input) doing nothing. Hence the frame's delay: focusing before the
|
|
84
|
+
// dialog's own focus move would simply be undone.
|
|
85
|
+
let input = $state(null);
|
|
86
|
+
$effect(() => {
|
|
87
|
+
const element = input;
|
|
88
|
+
if (!open || element === null) return;
|
|
89
|
+
const frame = requestAnimationFrame(() => element.focus());
|
|
90
|
+
return () => cancelAnimationFrame(frame);
|
|
91
|
+
});
|
|
92
|
+
const setOpen = (next) => {
|
|
93
|
+
open = next;
|
|
94
|
+
// Closing clears the query: a palette that reopens on the last search is
|
|
95
|
+
// a palette that shows yesterday's answer to today's question.
|
|
96
|
+
if (!next) query = "";
|
|
97
|
+
};
|
|
98
|
+
/** In async mode this component owns filtering, so local rows filter here. */
|
|
99
|
+
const matchesQuery = (candidate) => {
|
|
100
|
+
if (onSearch === undefined || trimmedQuery === "") return true;
|
|
101
|
+
return candidate.label.toLowerCase().includes(trimmedQuery.toLowerCase());
|
|
102
|
+
};
|
|
103
|
+
const registryRows = $derived((registry?.paletteActions ?? []).map((entry) => ({
|
|
104
|
+
item: {
|
|
105
|
+
id: entry.action.id,
|
|
106
|
+
label: entry.action.label,
|
|
107
|
+
group: entry.action.group,
|
|
108
|
+
run: entry.action.run
|
|
109
|
+
},
|
|
110
|
+
trigger: entry.action.trigger
|
|
111
|
+
})));
|
|
112
|
+
const staticRows = $derived((items ?? []).map((candidate) => ({
|
|
113
|
+
item: candidate,
|
|
114
|
+
trigger: undefined
|
|
115
|
+
})));
|
|
116
|
+
let results = $state([]);
|
|
117
|
+
const resultRows = $derived(results.map((candidate) => ({
|
|
118
|
+
item: candidate,
|
|
119
|
+
trigger: undefined
|
|
120
|
+
})));
|
|
121
|
+
const rows = $derived([
|
|
122
|
+
...registryRows,
|
|
123
|
+
...staticRows,
|
|
124
|
+
...resultRows
|
|
125
|
+
].filter((row) => matchesQuery(row.item)));
|
|
126
|
+
const sections = $derived(groups.map((group) => ({
|
|
127
|
+
group,
|
|
128
|
+
rows: rows.filter((row) => row.item.group === group.id)
|
|
129
|
+
})).filter((section) => section.rows.length > 0));
|
|
130
|
+
// ── Async search ─────────────────────────────────────────────────
|
|
131
|
+
let pending = $state(false);
|
|
132
|
+
let timer;
|
|
133
|
+
let inFlight;
|
|
134
|
+
const search = (text) => {
|
|
135
|
+
if (onSearch === undefined) return;
|
|
136
|
+
inFlight?.abort();
|
|
137
|
+
const own = new AbortController();
|
|
138
|
+
inFlight = own;
|
|
139
|
+
pending = true;
|
|
140
|
+
void onSearch(text, own.signal).then((next) => {
|
|
141
|
+
// A superseded request must not write: `inFlight` is the identity
|
|
142
|
+
// check, `signal.aborted` catches the caller aborting its own work.
|
|
143
|
+
if (own.signal.aborted || inFlight !== own) return;
|
|
144
|
+
results = next;
|
|
145
|
+
pending = false;
|
|
146
|
+
}).catch(() => {
|
|
147
|
+
if (own.signal.aborted || inFlight !== own) return;
|
|
148
|
+
pending = false;
|
|
149
|
+
});
|
|
150
|
+
};
|
|
151
|
+
const cancel = () => {
|
|
152
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
153
|
+
timer = undefined;
|
|
154
|
+
inFlight?.abort();
|
|
155
|
+
inFlight = undefined;
|
|
156
|
+
};
|
|
157
|
+
const emit = (text) => {
|
|
158
|
+
if (onSearch === undefined) return;
|
|
159
|
+
if (text === "") {
|
|
160
|
+
cancel();
|
|
161
|
+
pending = false;
|
|
162
|
+
results = [];
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
166
|
+
timer = setTimeout(() => {
|
|
167
|
+
timer = undefined;
|
|
168
|
+
search(text);
|
|
169
|
+
}, debounceMs);
|
|
170
|
+
};
|
|
171
|
+
// Emit only while the palette is open; closing it emits `''`, which resets
|
|
172
|
+
// the rows and cancels anything in flight.
|
|
173
|
+
$effect(() => {
|
|
174
|
+
const text = open ? trimmedQuery : "";
|
|
175
|
+
untrack(() => emit(text));
|
|
176
|
+
});
|
|
177
|
+
$effect(() => () => cancel());
|
|
178
|
+
const isEmpty = $derived(trimmedQuery.length > 0 && !pending && sections.length === 0);
|
|
179
|
+
// ── Activation ───────────────────────────────────────────────────
|
|
180
|
+
const activate = (candidate) => {
|
|
181
|
+
setOpen(false);
|
|
182
|
+
onSelect?.(candidate);
|
|
183
|
+
if (candidate.run !== undefined) void candidate.run();
|
|
184
|
+
if (candidate.href !== undefined) void context.navigate(context.href(candidate.href));
|
|
185
|
+
};
|
|
186
|
+
</script>
|
|
187
|
+
|
|
188
|
+
{#snippet row(entry: Row)}
|
|
189
|
+
<Command.Item
|
|
190
|
+
value={`${entry.item.group}:${entry.item.id}`}
|
|
191
|
+
keywords={[entry.item.label]}
|
|
192
|
+
onSelect={() => activate(entry.item)}
|
|
193
|
+
class="data-selected:bg-base-200 flex cursor-pointer items-center justify-between gap-4 rounded px-3 py-2"
|
|
194
|
+
>
|
|
195
|
+
{#if item}
|
|
196
|
+
{@render item(entry.item)}
|
|
197
|
+
{:else}
|
|
198
|
+
<span class="flex min-w-0 items-baseline gap-2">
|
|
199
|
+
<span class="truncate text-sm">{entry.item.label}</span>
|
|
200
|
+
{#if entry.item.hint !== undefined}
|
|
201
|
+
<span class="text-base-content/60 truncate text-xs">{entry.item.hint}</span>
|
|
202
|
+
{/if}
|
|
203
|
+
</span>
|
|
204
|
+
{#if entry.trigger !== undefined}
|
|
205
|
+
<HotkeyBadge trigger={entry.trigger} />
|
|
206
|
+
{:else if entry.item.badge !== undefined}
|
|
207
|
+
<Badge size="sm">{entry.item.badge}</Badge>
|
|
208
|
+
{/if}
|
|
209
|
+
{/if}
|
|
210
|
+
</Command.Item>
|
|
211
|
+
{/snippet}
|
|
212
|
+
|
|
213
|
+
<Dialog bind:open={() => open, setOpen} title={label} {closeLabel}>
|
|
214
|
+
<Command.Root shouldFilter={onSearch === undefined} loop {label} class="flex flex-col">
|
|
215
|
+
<Command.Input
|
|
216
|
+
bind:ref={input}
|
|
217
|
+
bind:value={query}
|
|
218
|
+
{placeholder}
|
|
219
|
+
class="input border-base-300 w-full rounded-none border-0 border-b focus:outline-none"
|
|
220
|
+
/>
|
|
221
|
+
<Command.List class="max-h-96 overflow-x-hidden overflow-y-auto pt-2">
|
|
222
|
+
<Command.Viewport>
|
|
223
|
+
{#each sections as section (section.group.id)}
|
|
224
|
+
<Command.Group value={section.group.id}>
|
|
225
|
+
<Command.GroupHeading
|
|
226
|
+
class="text-base-content/70 px-3 pt-3 pb-1 text-xs font-semibold tracking-wide uppercase"
|
|
227
|
+
>
|
|
228
|
+
{section.group.label}
|
|
229
|
+
</Command.GroupHeading>
|
|
230
|
+
<Command.GroupItems>
|
|
231
|
+
{#each section.rows as entry (`${entry.item.group}:${entry.item.id}`)}
|
|
232
|
+
{@render row(entry)}
|
|
233
|
+
{/each}
|
|
234
|
+
</Command.GroupItems>
|
|
235
|
+
</Command.Group>
|
|
236
|
+
{/each}
|
|
237
|
+
|
|
238
|
+
{#if pending && trimmedQuery.length > 0}
|
|
239
|
+
<!-- Through `child` so `aria-label` lands AFTER bits-ui's own
|
|
240
|
+
props: `Command.Loading` hard-codes the English "Loading...",
|
|
241
|
+
and in this library the name of anything is a prop. -->
|
|
242
|
+
<Command.Loading>
|
|
243
|
+
{#snippet child({ props })}
|
|
244
|
+
<div {...props} aria-label={loadingLabel} class="text-base-content/60 px-3 py-2 text-sm">
|
|
245
|
+
{loadingLabel}
|
|
246
|
+
</div>
|
|
247
|
+
{/snippet}
|
|
248
|
+
</Command.Loading>
|
|
249
|
+
{/if}
|
|
250
|
+
|
|
251
|
+
{#if isEmpty}
|
|
252
|
+
<p class="text-base-content/60 px-3 py-6 text-center text-sm">{emptyLabel}</p>
|
|
253
|
+
{/if}
|
|
254
|
+
</Command.Viewport>
|
|
255
|
+
</Command.List>
|
|
256
|
+
</Command.Root>
|
|
257
|
+
</Dialog>
|