@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,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure key-matching helpers for the hotkey registry. No Svelte, no DOM
|
|
3
|
+
* mutation — just functions over `KeyboardEvent`s, trigger grammars and the
|
|
4
|
+
* sequence buffer, so the matching rules stay testable and `HotkeyProvider`
|
|
5
|
+
* stays thin.
|
|
6
|
+
*
|
|
7
|
+
* Trigger model:
|
|
8
|
+
*
|
|
9
|
+
* - **Modifier chords** (`'$mod+k'`) — held together. `$mod` is ⌘ on macOS
|
|
10
|
+
* and Ctrl elsewhere. Matched on their own; never buffered.
|
|
11
|
+
* - **Plain keys / sequences** (`'e'`, `'/'`, `['g', 'p']`) — pressed in
|
|
12
|
+
* order with no modifier held. A plain single key is just a sequence of
|
|
13
|
+
* length one, so matching is uniform.
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
/** Sequence buffer entries expire after this many milliseconds. */
|
|
18
|
+
export const SEQUENCE_TIMEOUT_MS = 1000;
|
|
19
|
+
/** Whether the runtime platform treats `$mod` as ⌘ (macOS / iOS). */
|
|
20
|
+
export function isApplePlatform() {
|
|
21
|
+
// `globalThis.navigator` rather than the bare identifier: on a runtime
|
|
22
|
+
// without one, a direct reference would throw before the guard could run.
|
|
23
|
+
if (typeof globalThis.navigator === 'undefined')
|
|
24
|
+
return false;
|
|
25
|
+
return /mac|iphone|ipad|ipod/i.test(globalThis.navigator.platform);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Whether a keyboard event's target is an editable surface — a form field or
|
|
29
|
+
* a contenteditable region (which covers a rich-text editor's root). Hotkeys
|
|
30
|
+
* without `allowInEditable` stay inert there so typing never fires actions.
|
|
31
|
+
*/
|
|
32
|
+
export function isEditableTarget(target) {
|
|
33
|
+
if (!(target instanceof HTMLElement))
|
|
34
|
+
return false;
|
|
35
|
+
return target.matches('input, textarea, select') || target.isContentEditable;
|
|
36
|
+
}
|
|
37
|
+
/** Whether a trigger is in the modifier-chord form (`'$mod+…'`). */
|
|
38
|
+
function isModifierChord(trigger) {
|
|
39
|
+
return typeof trigger === 'string' && trigger.includes('+');
|
|
40
|
+
}
|
|
41
|
+
/** Normalise a plain-key trigger to its sequence form. `undefined` for a chord. */
|
|
42
|
+
function asSequence(trigger) {
|
|
43
|
+
if (typeof trigger === 'string') {
|
|
44
|
+
return isModifierChord(trigger) ? undefined : [trigger];
|
|
45
|
+
}
|
|
46
|
+
return trigger;
|
|
47
|
+
}
|
|
48
|
+
/** Whether the event matches a `'$mod+key'` chord. */
|
|
49
|
+
// Modifier matching is a flat run of guard comparisons (mod / alt / shift /
|
|
50
|
+
// cross-platform exclusion); each early return is one rule of the chord
|
|
51
|
+
// grammar, and extracting them would obscure the contract.
|
|
52
|
+
function eventMatchesChord(event, chord) {
|
|
53
|
+
const parts = chord.split('+');
|
|
54
|
+
const key = parts.at(-1) ?? '';
|
|
55
|
+
// A plain array, not a `Set`: a chord carries at most three modifiers, so
|
|
56
|
+
// three `includes` scans beat building a hashed collection on every
|
|
57
|
+
// keystroke.
|
|
58
|
+
const modifiers = parts.slice(0, -1);
|
|
59
|
+
const wantsMod = modifiers.includes('$mod');
|
|
60
|
+
const wantsAlt = modifiers.includes('alt');
|
|
61
|
+
const wantsShift = modifiers.includes('shift');
|
|
62
|
+
const apple = isApplePlatform();
|
|
63
|
+
const modPressed = apple ? event.metaKey : event.ctrlKey;
|
|
64
|
+
// The non-$mod modifier must not also be held, so e.g. Ctrl+K on macOS
|
|
65
|
+
// doesn't trigger a ⌘K hotkey.
|
|
66
|
+
const otherModPressed = apple ? event.ctrlKey : event.metaKey;
|
|
67
|
+
if (wantsMod !== modPressed || otherModPressed)
|
|
68
|
+
return false;
|
|
69
|
+
if (wantsAlt !== event.altKey)
|
|
70
|
+
return false;
|
|
71
|
+
if (wantsShift && !event.shiftKey)
|
|
72
|
+
return false;
|
|
73
|
+
return event.key.toLowerCase() === key.toLowerCase();
|
|
74
|
+
}
|
|
75
|
+
/** Whether an event carries a chord modifier (⌘ / Ctrl / Alt). */
|
|
76
|
+
export function hasChordModifier(event) {
|
|
77
|
+
return event.metaKey || event.ctrlKey || event.altKey;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Whether an event's key can participate in a sequence: a single printable
|
|
81
|
+
* character pressed without chord modifiers.
|
|
82
|
+
*/
|
|
83
|
+
export function isSequenceKey(event) {
|
|
84
|
+
return !hasChordModifier(event) && event.key.length === 1;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Find the action whose modifier chord matches the event. `actions` must
|
|
88
|
+
* already be in precedence order (page before global, later registrations
|
|
89
|
+
* first).
|
|
90
|
+
*/
|
|
91
|
+
export function matchChord(event, actions) {
|
|
92
|
+
return actions.find(({ action }) => typeof action.trigger === 'string' &&
|
|
93
|
+
action.trigger.includes('+') &&
|
|
94
|
+
eventMatchesChord(event, action.trigger))?.action;
|
|
95
|
+
}
|
|
96
|
+
// A single-pass scan that classifies every trigger as exact-match / prefix /
|
|
97
|
+
// miss in one loop; splitting it into separate passes would be simpler per
|
|
98
|
+
// function but iterate the registrations three times per keystroke.
|
|
99
|
+
/**
|
|
100
|
+
* Match a candidate key buffer against the active plain-key triggers.
|
|
101
|
+
* `actions` must already be in precedence order.
|
|
102
|
+
*/
|
|
103
|
+
export function matchSequence(buffer, actions) {
|
|
104
|
+
return actions.reduce((match, { action }) => {
|
|
105
|
+
const trigger = action.trigger;
|
|
106
|
+
if (trigger === undefined)
|
|
107
|
+
return match;
|
|
108
|
+
const sequence = asSequence(trigger);
|
|
109
|
+
if (sequence === undefined)
|
|
110
|
+
return match;
|
|
111
|
+
if (sequence.length === buffer.length) {
|
|
112
|
+
return match.exact === undefined && sequence.every((key, i) => key === buffer[i])
|
|
113
|
+
? { ...match, exact: action }
|
|
114
|
+
: match;
|
|
115
|
+
}
|
|
116
|
+
return sequence.length > buffer.length && buffer.every((key, i) => key === sequence[i])
|
|
117
|
+
? { ...match, isPrefix: true }
|
|
118
|
+
: match;
|
|
119
|
+
}, { exact: undefined, isPrefix: false });
|
|
120
|
+
}
|
|
121
|
+
/** Project a trigger into its display form for help overlays and badges. */
|
|
122
|
+
export function triggerDisplay(trigger) {
|
|
123
|
+
if (typeof trigger === 'string') {
|
|
124
|
+
if (isModifierChord(trigger)) {
|
|
125
|
+
return {
|
|
126
|
+
keys: trigger.split('+').map((part) => (part === '$mod' ? part : part.toUpperCase())),
|
|
127
|
+
sequential: false,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return { keys: [trigger.toUpperCase()], sequential: false };
|
|
131
|
+
}
|
|
132
|
+
return { keys: trigger.map((key) => key.toUpperCase()), sequential: true };
|
|
133
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The reactive hotkey / command registry.
|
|
3
|
+
*
|
|
4
|
+
* One instance lives in the app's Svelte context. The root layout registers
|
|
5
|
+
* `global` actions; pages register `page` actions that are torn down when the
|
|
6
|
+
* page unmounts. The registry is the single source of truth for three
|
|
7
|
+
* consumers:
|
|
8
|
+
*
|
|
9
|
+
* - `HotkeyProvider` — matches keyboard events against active triggers.
|
|
10
|
+
* - `CommandPalette` — lists palette-visible actions.
|
|
11
|
+
* - `HotkeyHelpDialog` — lists actions that carry triggers.
|
|
12
|
+
*
|
|
13
|
+
* Role gating happens here: actions registered with `requiresAdmin` are
|
|
14
|
+
* dropped from every read surface when the viewer is not an administrator, so
|
|
15
|
+
* non-admin users never see (or can fire) them.
|
|
16
|
+
*
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
19
|
+
import type { CommandAction, HotkeyScope, RegisteredAction } from './types.ts';
|
|
20
|
+
/** What `createHotkeyRegistry` needs from the app: the admin check and the dev flag. */
|
|
21
|
+
export interface HotkeyRegistryOptions {
|
|
22
|
+
/** Whether the current viewer is an administrator. Read lazily so it can be reactive. */
|
|
23
|
+
isAdmin: () => boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Whether this is a dev build. When true, registering a batch whose trigger
|
|
26
|
+
* shadows an existing one logs a console warning. An app wires this to its
|
|
27
|
+
* bundler's dev flag (SvelteKit's `$app/environment` `dev`); defaults to
|
|
28
|
+
* `false`.
|
|
29
|
+
*/
|
|
30
|
+
dev?: boolean | undefined;
|
|
31
|
+
}
|
|
32
|
+
/** The reactive set of registered hotkey actions that `HotkeyProvider`, `CommandPalette` and `HotkeyHelpDialog` read; page registrations win over global ones. */
|
|
33
|
+
export declare class HotkeyRegistry {
|
|
34
|
+
#private;
|
|
35
|
+
/** Whether the command palette is open. Bound by `CommandPalette`. */
|
|
36
|
+
paletteOpen: boolean;
|
|
37
|
+
/** Whether the hotkey help overlay is open. Bound by `HotkeyHelpDialog`. */
|
|
38
|
+
helpOpen: boolean;
|
|
39
|
+
constructor(options: HotkeyRegistryOptions);
|
|
40
|
+
/**
|
|
41
|
+
* Register a batch of actions under a scope. Returns an unregister
|
|
42
|
+
* function; callers are expected to invoke it on unmount (the
|
|
43
|
+
* `registerHotkeys` helper wires this to `$effect` cleanup).
|
|
44
|
+
*/
|
|
45
|
+
register(scope: HotkeyScope, actions: readonly CommandAction[]): () => void;
|
|
46
|
+
/**
|
|
47
|
+
* Role-filtered active registrations in match-precedence order: page scope
|
|
48
|
+
* before global, later registrations first within a scope.
|
|
49
|
+
*/
|
|
50
|
+
readonly active: readonly RegisteredAction[];
|
|
51
|
+
/** Active actions that should appear in the command palette, in registration order. */
|
|
52
|
+
readonly paletteActions: readonly RegisteredAction[];
|
|
53
|
+
/** Active actions that carry a keyboard trigger, for the help overlay. */
|
|
54
|
+
readonly hotkeys: readonly RegisteredAction[];
|
|
55
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The reactive hotkey / command registry.
|
|
3
|
+
*
|
|
4
|
+
* One instance lives in the app's Svelte context. The root layout registers
|
|
5
|
+
* `global` actions; pages register `page` actions that are torn down when the
|
|
6
|
+
* page unmounts. The registry is the single source of truth for three
|
|
7
|
+
* consumers:
|
|
8
|
+
*
|
|
9
|
+
* - `HotkeyProvider` — matches keyboard events against active triggers.
|
|
10
|
+
* - `CommandPalette` — lists palette-visible actions.
|
|
11
|
+
* - `HotkeyHelpDialog` — lists actions that carry triggers.
|
|
12
|
+
*
|
|
13
|
+
* Role gating happens here: actions registered with `requiresAdmin` are
|
|
14
|
+
* dropped from every read surface when the viewer is not an administrator, so
|
|
15
|
+
* non-admin users never see (or can fire) them.
|
|
16
|
+
*
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Whether two triggers denote the same keystroke. A structural comparison
|
|
21
|
+
* rather than a JSON round-trip: a chord is a string and a sequence is an
|
|
22
|
+
* array, so the two forms never collide.
|
|
23
|
+
*/
|
|
24
|
+
function sameTrigger(self, that) {
|
|
25
|
+
return typeof self === 'string' || typeof that === 'string'
|
|
26
|
+
? self === that
|
|
27
|
+
: self.length === that.length && self.every((key, index) => key === that[index]);
|
|
28
|
+
}
|
|
29
|
+
/** A trigger rendered for the dev warning below. */
|
|
30
|
+
function formatTrigger(trigger) {
|
|
31
|
+
return typeof trigger === 'string' ? `"${trigger}"` : `["${trigger.join('", "')}"]`;
|
|
32
|
+
}
|
|
33
|
+
/** Dev-only console warning when a new registration shadows an existing trigger. */
|
|
34
|
+
function warnOnTriggerConflicts(added, existing) {
|
|
35
|
+
for (const entry of added) {
|
|
36
|
+
const trigger = entry.action.trigger;
|
|
37
|
+
if (trigger === undefined)
|
|
38
|
+
continue;
|
|
39
|
+
const conflict = existing.find((other) => other.action.trigger !== undefined && sameTrigger(other.action.trigger, trigger));
|
|
40
|
+
if (conflict) {
|
|
41
|
+
// A dev-build diagnostic: it has to reach the browser console the
|
|
42
|
+
// developer is looking at, which is why this is the one `console` call
|
|
43
|
+
// in the package.
|
|
44
|
+
console.warn(`[hotkeys] Trigger ${formatTrigger(trigger)} of "${entry.action.id}" ` +
|
|
45
|
+
`shadows "${conflict.action.id}" (${conflict.scope}). ` +
|
|
46
|
+
`Page-scope and later registrations win.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Page scope sorts before global scope. */
|
|
51
|
+
const scopeRank = (entry) => (entry.scope === 'page' ? 0 : 1);
|
|
52
|
+
/**
|
|
53
|
+
* Match precedence: page scope first, then the most recent registration.
|
|
54
|
+
* A plain comparator — the composed `Order` this was ported from is the only
|
|
55
|
+
* thing that changed.
|
|
56
|
+
*/
|
|
57
|
+
const byMatchPrecedence = (a, b) => scopeRank(a) - scopeRank(b) || b.order - a.order;
|
|
58
|
+
/** Ascending registration order (earlier registrations first). */
|
|
59
|
+
const byOrder = (a, b) => a.order - b.order;
|
|
60
|
+
/** The reactive set of registered hotkey actions that `HotkeyProvider`, `CommandPalette` and `HotkeyHelpDialog` read; page registrations win over global ones. */
|
|
61
|
+
export class HotkeyRegistry {
|
|
62
|
+
#registrations = $state([]);
|
|
63
|
+
#nextOrder = 0;
|
|
64
|
+
#isAdmin;
|
|
65
|
+
#dev;
|
|
66
|
+
/** Whether the command palette is open. Bound by `CommandPalette`. */
|
|
67
|
+
paletteOpen = $state(false);
|
|
68
|
+
/** Whether the hotkey help overlay is open. Bound by `HotkeyHelpDialog`. */
|
|
69
|
+
helpOpen = $state(false);
|
|
70
|
+
constructor(options) {
|
|
71
|
+
this.#isAdmin = options.isAdmin;
|
|
72
|
+
this.#dev = options.dev ?? false;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Register a batch of actions under a scope. Returns an unregister
|
|
76
|
+
* function; callers are expected to invoke it on unmount (the
|
|
77
|
+
* `registerHotkeys` helper wires this to `$effect` cleanup).
|
|
78
|
+
*/
|
|
79
|
+
register(scope, actions) {
|
|
80
|
+
const added = actions.map((action) => ({ scope, action, order: this.#nextOrder++ }));
|
|
81
|
+
if (this.#dev)
|
|
82
|
+
warnOnTriggerConflicts(added, this.#registrations);
|
|
83
|
+
this.#registrations = [...this.#registrations, ...added];
|
|
84
|
+
return () => {
|
|
85
|
+
// Remove by the stable monotonic `order`, NOT object identity. `$state`
|
|
86
|
+
// deep-proxies `#registrations`, so entries read back here are proxies
|
|
87
|
+
// that never `===` the raw objects captured in `added` — a `Set(added)`
|
|
88
|
+
// membership test would match nothing and remove nothing, leaking the
|
|
89
|
+
// batch. `registerHotkeys` wires this closure to `$effect` cleanup, so
|
|
90
|
+
// every reactive re-registration would otherwise accumulate duplicates.
|
|
91
|
+
const orders = new Set(added.map((entry) => entry.order));
|
|
92
|
+
this.#registrations = this.#registrations.filter((entry) => !orders.has(entry.order));
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Role-filtered active registrations in match-precedence order: page scope
|
|
97
|
+
* before global, later registrations first within a scope.
|
|
98
|
+
*/
|
|
99
|
+
active = $derived(this.#registrations
|
|
100
|
+
.filter((entry) => !entry.action.requiresAdmin || this.#isAdmin())
|
|
101
|
+
.toSorted(byMatchPrecedence));
|
|
102
|
+
/** Active actions that should appear in the command palette, in registration order. */
|
|
103
|
+
paletteActions = $derived(this.#registrations
|
|
104
|
+
.filter((entry) => (!entry.action.requiresAdmin || this.#isAdmin()) && entry.action.paletteVisible !== false)
|
|
105
|
+
.toSorted(byOrder));
|
|
106
|
+
/** Active actions that carry a keyboard trigger, for the help overlay. */
|
|
107
|
+
hotkeys = $derived(this.#registrations
|
|
108
|
+
.filter((entry) => (!entry.action.requiresAdmin || this.#isAdmin()) && entry.action.trigger !== undefined)
|
|
109
|
+
.toSorted(byOrder));
|
|
110
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The registration contract shared by the hotkey registry and the command
|
|
3
|
+
* palette.
|
|
4
|
+
*
|
|
5
|
+
* A {@link CommandAction} is the unit of both systems: a labelled, runnable
|
|
6
|
+
* action that may carry a keyboard trigger (making it a hotkey) and may
|
|
7
|
+
* appear in the command palette (making it discoverable). Pages and the
|
|
8
|
+
* global shell register actions through the registry; the palette and the
|
|
9
|
+
* help overlay read the same registrations back, so there is exactly one
|
|
10
|
+
* source of truth for "what can the viewer do right now".
|
|
11
|
+
*
|
|
12
|
+
* Every optional member is declared `T | undefined`: the workspace compiles
|
|
13
|
+
* with `exactOptionalPropertyTypes`, and these objects are written as plain
|
|
14
|
+
* literals by application code that forwards its own optional values.
|
|
15
|
+
*
|
|
16
|
+
* @module
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* A single-keystroke trigger.
|
|
20
|
+
*
|
|
21
|
+
* Grammar: zero or more `+`-separated modifiers followed by a key, e.g.
|
|
22
|
+
* `'$mod+k'`, `'/'`, `'?'`, `'e'`. `$mod` resolves to ⌘ on macOS and Ctrl
|
|
23
|
+
* elsewhere. Shift is never written explicitly — shifted characters (like
|
|
24
|
+
* `?`) already arrive shifted in `KeyboardEvent.key`.
|
|
25
|
+
*/
|
|
26
|
+
export type HotkeyChord = string;
|
|
27
|
+
/**
|
|
28
|
+
* A multi-keystroke trigger pressed in order, e.g. `['g', 'p']`. Only plain
|
|
29
|
+
* (unmodified) printable keys participate in sequences.
|
|
30
|
+
*/
|
|
31
|
+
export type HotkeySequence = readonly string[];
|
|
32
|
+
/** A hotkey trigger: a chord pressed together (`'$mod+k'`) or a sequence pressed in turn (`['g', 'p']`). */
|
|
33
|
+
export type HotkeyTrigger = HotkeyChord | HotkeySequence;
|
|
34
|
+
/** Palette grouping. Drives section headings and ordering. */
|
|
35
|
+
export type CommandActionGroup = 'actions' | 'navigation';
|
|
36
|
+
/**
|
|
37
|
+
* Where a registration lives. Page registrations are torn down when the
|
|
38
|
+
* registering page unmounts and win over global registrations on trigger
|
|
39
|
+
* conflicts.
|
|
40
|
+
*/
|
|
41
|
+
export type HotkeyScope = 'global' | 'page';
|
|
42
|
+
/** One action the hotkey registry and the command palette offer: an id, a label, what it runs and, optionally, its trigger. */
|
|
43
|
+
export interface CommandAction {
|
|
44
|
+
/** Stable unique id, e.g. `'nav.posts'`, `'posts.create'`. */
|
|
45
|
+
id: string;
|
|
46
|
+
/** Human-readable label. An i18n app resolves its message at registration. */
|
|
47
|
+
label: string;
|
|
48
|
+
/** What runs on activation (navigate, focus, open a dialog, …). */
|
|
49
|
+
run: () => void | Promise<void>;
|
|
50
|
+
/** Palette section. */
|
|
51
|
+
group: CommandActionGroup;
|
|
52
|
+
/** Optional keyboard trigger. Actions without one are palette-only. */
|
|
53
|
+
trigger?: HotkeyTrigger | undefined;
|
|
54
|
+
/**
|
|
55
|
+
* When true the registry drops the action entirely for non-admin viewers —
|
|
56
|
+
* it neither fires as a hotkey nor appears in the palette.
|
|
57
|
+
*/
|
|
58
|
+
requiresAdmin?: boolean | undefined;
|
|
59
|
+
/** Set false to keep an action hotkey-only (hidden from the palette). */
|
|
60
|
+
paletteVisible?: boolean | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* Allow the trigger to fire while focus is inside an editable element
|
|
63
|
+
* (input / textarea / contenteditable). Reserved for the palette opener;
|
|
64
|
+
* everything else stays inert while typing.
|
|
65
|
+
*/
|
|
66
|
+
allowInEditable?: boolean | undefined;
|
|
67
|
+
}
|
|
68
|
+
/** A registration entry: an action plus the scope it was registered under. */
|
|
69
|
+
export interface RegisteredAction {
|
|
70
|
+
scope: HotkeyScope;
|
|
71
|
+
action: CommandAction;
|
|
72
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The registration contract shared by the hotkey registry and the command
|
|
3
|
+
* palette.
|
|
4
|
+
*
|
|
5
|
+
* A {@link CommandAction} is the unit of both systems: a labelled, runnable
|
|
6
|
+
* action that may carry a keyboard trigger (making it a hotkey) and may
|
|
7
|
+
* appear in the command palette (making it discoverable). Pages and the
|
|
8
|
+
* global shell register actions through the registry; the palette and the
|
|
9
|
+
* help overlay read the same registrations back, so there is exactly one
|
|
10
|
+
* source of truth for "what can the viewer do right now".
|
|
11
|
+
*
|
|
12
|
+
* Every optional member is declared `T | undefined`: the workspace compiles
|
|
13
|
+
* with `exactOptionalPropertyTypes`, and these objects are written as plain
|
|
14
|
+
* literals by application code that forwards its own optional values.
|
|
15
|
+
*
|
|
16
|
+
* @module
|
|
17
|
+
*/
|
|
18
|
+
export {};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accessible Svelte 5 UI primitives.
|
|
3
|
+
*
|
|
4
|
+
* Everything exported here is app-agnostic: it takes props and snippets,
|
|
5
|
+
* spreads the native attributes of the element it renders, reads no global
|
|
6
|
+
* state, and gets every user-facing string from a prop with an English
|
|
7
|
+
* default. Styling is daisyUI semantic classes over the tokens in
|
|
8
|
+
* `./theme.css`, which ships beside this barrel as the package's
|
|
9
|
+
* `./theme.css` export. The one injected dependency is
|
|
10
|
+
* {@link ComponentsContext}: locale, href resolution, navigation and the
|
|
11
|
+
* current-page check.
|
|
12
|
+
*
|
|
13
|
+
* The heavier families — `citations`, `canvas`, `charts`, `tips`, `seo` —
|
|
14
|
+
* are separate entry points (`@thomasfosterau/components/<family>`), as is
|
|
15
|
+
* `EntityForm` (`@thomasfosterau/components/entity-form`, which needs the
|
|
16
|
+
* optional peer `@thomasfosterau/effect-forms-svelte`); none is re-exported
|
|
17
|
+
* here. The maps family lives one package over, in
|
|
18
|
+
* `@thomasfosterau/components-mapbox`: its behaviour needs a specific
|
|
19
|
+
* third-party engine (`mapbox-gl`), and a component that needs one lives in
|
|
20
|
+
* its own package so this one stays free of it.
|
|
21
|
+
*
|
|
22
|
+
* @module @thomasfosterau/components
|
|
23
|
+
*/
|
|
24
|
+
export * from './context.svelte.ts';
|
|
25
|
+
export * from './ui/index.ts';
|
|
26
|
+
export * from './feedback/index.ts';
|
|
27
|
+
export * from './layout/index.ts';
|
|
28
|
+
export * from './fields/index.ts';
|
|
29
|
+
export * from './forms/index.ts';
|
|
30
|
+
export * from './data/index.ts';
|
|
31
|
+
export * from './infobox/index.ts';
|
|
32
|
+
export * from './formatting/index.ts';
|
|
33
|
+
export * from './links/index.ts';
|
|
34
|
+
export * from './media/index.ts';
|
|
35
|
+
export * from './hotkeys/index.ts';
|
|
36
|
+
export * from './search/index.ts';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accessible Svelte 5 UI primitives.
|
|
3
|
+
*
|
|
4
|
+
* Everything exported here is app-agnostic: it takes props and snippets,
|
|
5
|
+
* spreads the native attributes of the element it renders, reads no global
|
|
6
|
+
* state, and gets every user-facing string from a prop with an English
|
|
7
|
+
* default. Styling is daisyUI semantic classes over the tokens in
|
|
8
|
+
* `./theme.css`, which ships beside this barrel as the package's
|
|
9
|
+
* `./theme.css` export. The one injected dependency is
|
|
10
|
+
* {@link ComponentsContext}: locale, href resolution, navigation and the
|
|
11
|
+
* current-page check.
|
|
12
|
+
*
|
|
13
|
+
* The heavier families — `citations`, `canvas`, `charts`, `tips`, `seo` —
|
|
14
|
+
* are separate entry points (`@thomasfosterau/components/<family>`), as is
|
|
15
|
+
* `EntityForm` (`@thomasfosterau/components/entity-form`, which needs the
|
|
16
|
+
* optional peer `@thomasfosterau/effect-forms-svelte`); none is re-exported
|
|
17
|
+
* here. The maps family lives one package over, in
|
|
18
|
+
* `@thomasfosterau/components-mapbox`: its behaviour needs a specific
|
|
19
|
+
* third-party engine (`mapbox-gl`), and a component that needs one lives in
|
|
20
|
+
* its own package so this one stays free of it.
|
|
21
|
+
*
|
|
22
|
+
* @module @thomasfosterau/components
|
|
23
|
+
*/
|
|
24
|
+
export * from "./context.svelte.js";
|
|
25
|
+
export * from "./ui/index.js";
|
|
26
|
+
export * from "./feedback/index.js";
|
|
27
|
+
export * from "./layout/index.js";
|
|
28
|
+
export * from "./fields/index.js";
|
|
29
|
+
export * from "./forms/index.js";
|
|
30
|
+
export * from "./data/index.js";
|
|
31
|
+
export * from "./infobox/index.js";
|
|
32
|
+
export * from "./formatting/index.js";
|
|
33
|
+
export * from "./links/index.js";
|
|
34
|
+
export * from "./media/index.js";
|
|
35
|
+
export * from "./hotkeys/index.js";
|
|
36
|
+
export * from "./search/index.js";
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<!-- @component
|
|
2
|
+
The summary rail beside an entity page’s prose: an optional header and image above a body of labelled facts.
|
|
3
|
+
-->
|
|
4
|
+
<!--
|
|
5
|
+
Infobox — the summary rail beside an entity page's prose: an optional
|
|
6
|
+
header, an optional image, and a body of labelled facts.
|
|
7
|
+
|
|
8
|
+
Two ways to fill it, both live at once. The composable path — `children`
|
|
9
|
+
taking `InfoboxSection`/`InfoboxRow` (or anything else):
|
|
10
|
+
a page builds bespoke markup, one piece of markup per concern, and a fix
|
|
11
|
+
to row layout lands once because there is only one row layout. `rows` renders through the SAME `InfoboxSection`/
|
|
12
|
+
`InfoboxRow`/`InfoboxImage` primitives `children` would use — never a
|
|
13
|
+
second copy of the markup — so both paths stay in lock step. Both are
|
|
14
|
+
accepted together: `rows` first, then `children` below them.
|
|
15
|
+
|
|
16
|
+
`float`: floats the box to the right of the article's prose; instead give it its own grid column in the
|
|
17
|
+
page layout and never float it. Off by default, since a grid-column aside
|
|
18
|
+
is the more common shape here, and opt-in per page with `float`.
|
|
19
|
+
|
|
20
|
+
ARIA contract: `aria-label={title ?? label}` names the region — a real
|
|
21
|
+
`title` is more specific than the generic default, so it wins.
|
|
22
|
+
`data-testid="infobox"` is kept for e2e parity with the source apps.
|
|
23
|
+
-->
|
|
24
|
+
<script lang="ts">import Link from "../ui/Link.svelte";
|
|
25
|
+
import InfoboxHeader from "./InfoboxHeader.svelte";
|
|
26
|
+
import InfoboxImage from "./InfoboxImage.svelte";
|
|
27
|
+
import InfoboxRow from "./InfoboxRow.svelte";
|
|
28
|
+
import InfoboxSection from "./InfoboxSection.svelte";
|
|
29
|
+
import { infoboxRowIsPresent } from "./types.js";
|
|
30
|
+
let { title, subtitle, label = "Key facts", image, rows, float = false, children, class: className, ...rest } = $props();
|
|
31
|
+
const presentRows = $derived((rows ?? []).filter(infoboxRowIsPresent));
|
|
32
|
+
const imageRows = $derived(presentRows.filter((row) => row.kind === "image"));
|
|
33
|
+
const bodyRows = $derived(presentRows.filter((row) => row.kind !== "image"));
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<aside
|
|
37
|
+
{...rest}
|
|
38
|
+
data-testid="infobox"
|
|
39
|
+
aria-label={title ?? label}
|
|
40
|
+
class={[
|
|
41
|
+
'card card-border bg-base-100 shadow-sm',
|
|
42
|
+
float && 'float-right mb-4 ml-4 w-full sm:w-80',
|
|
43
|
+
className,
|
|
44
|
+
]}
|
|
45
|
+
>
|
|
46
|
+
{#if title !== undefined}
|
|
47
|
+
<InfoboxHeader {title} {subtitle} />
|
|
48
|
+
{/if}
|
|
49
|
+
{#if image !== undefined}
|
|
50
|
+
<InfoboxImage {...image} />
|
|
51
|
+
{/if}
|
|
52
|
+
{#each imageRows as row (row.src)}
|
|
53
|
+
<InfoboxImage {...row} />
|
|
54
|
+
{/each}
|
|
55
|
+
{#if bodyRows.length > 0}
|
|
56
|
+
<InfoboxSection>
|
|
57
|
+
{#each bodyRows as row (row.label)}
|
|
58
|
+
{#if row.kind === 'fact'}
|
|
59
|
+
<InfoboxRow label={row.label} value={row.value} />
|
|
60
|
+
{:else if row.kind === 'link'}
|
|
61
|
+
<InfoboxRow label={row.label} value={row.text} href={row.href} external={row.external} />
|
|
62
|
+
{:else if row.kind === 'links'}
|
|
63
|
+
<InfoboxRow label={row.label}>
|
|
64
|
+
<ul class="flex flex-col gap-1">
|
|
65
|
+
{#each row.links ?? [] as link (link.href)}
|
|
66
|
+
<li><Link href={link.href}>{link.label}</Link></li>
|
|
67
|
+
{/each}
|
|
68
|
+
</ul>
|
|
69
|
+
</InfoboxRow>
|
|
70
|
+
{:else if row.kind === 'custom'}
|
|
71
|
+
<InfoboxRow label={row.label}>
|
|
72
|
+
{@render row.content()}
|
|
73
|
+
</InfoboxRow>
|
|
74
|
+
{/if}
|
|
75
|
+
{/each}
|
|
76
|
+
</InfoboxSection>
|
|
77
|
+
{/if}
|
|
78
|
+
{@render children?.()}
|
|
79
|
+
</aside>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
import { type InfoboxImageSpec, type InfoboxRowSpec as Row } from './types.ts';
|
|
4
|
+
interface Props extends Omit<HTMLAttributes<HTMLElement>, 'children' | 'title'> {
|
|
5
|
+
/** Heading of the rail; also its accessible name. */
|
|
6
|
+
title?: string | undefined;
|
|
7
|
+
/** A line under the title. */
|
|
8
|
+
subtitle?: string | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* Accessible name for the region when there is no `title`.
|
|
11
|
+
* @defaultValue `'Key facts'`
|
|
12
|
+
*/
|
|
13
|
+
label?: string | undefined;
|
|
14
|
+
/** Renders an `InfoboxImage` above the body. */
|
|
15
|
+
image?: InfoboxImageSpec | undefined;
|
|
16
|
+
/** Rendered through the primitives below, inside one `InfoboxSection`. */
|
|
17
|
+
rows?: readonly Row[] | undefined;
|
|
18
|
+
/** float-right rail; off renders in normal flow (a grid column). */
|
|
19
|
+
float?: boolean | undefined;
|
|
20
|
+
/** Extra composed markup — `InfoboxSection`/`InfoboxRow`, or anything else. Rendered after `rows`. */
|
|
21
|
+
children?: Snippet<[]> | undefined;
|
|
22
|
+
}
|
|
23
|
+
/** The summary rail beside an entity page’s prose: an optional header and image above a body of labelled facts. */
|
|
24
|
+
declare const Infobox: import("svelte").Component<Props, {}, "">;
|
|
25
|
+
type Infobox = ReturnType<typeof Infobox>;
|
|
26
|
+
export default Infobox;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<!-- @component
|
|
2
|
+
The title block at the top of an `Infobox`.
|
|
3
|
+
-->
|
|
4
|
+
<!--
|
|
5
|
+
InfoboxHeader — the title block at the top of an `Infobox`.
|
|
6
|
+
|
|
7
|
+
An `h3` rather than an `h2`: the box sits beside an article's prose, and
|
|
8
|
+
its title names the summary, not a section of the document. Promoting it
|
|
9
|
+
to the level a real section heading would use puts a phantom entry in the
|
|
10
|
+
page's outline between two real ones — the box is deliberately outside
|
|
11
|
+
the outline.
|
|
12
|
+
-->
|
|
13
|
+
<script lang="ts">let { title, subtitle, class: className, ...rest } = $props();
|
|
14
|
+
export {};
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<div
|
|
18
|
+
{...rest}
|
|
19
|
+
class={["border-base-300 bg-base-300 border-b px-4 py-3", className]}
|
|
20
|
+
>
|
|
21
|
+
<h3 class="text-center text-lg font-bold select-none">{title}</h3>
|
|
22
|
+
{#if subtitle !== undefined && subtitle !== ""}
|
|
23
|
+
<p class="text-base-content/60 mt-1 text-center text-sm select-none">
|
|
24
|
+
{subtitle}
|
|
25
|
+
</p>
|
|
26
|
+
{/if}
|
|
27
|
+
</div>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
2
|
+
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
3
|
+
/** The infobox heading text. */
|
|
4
|
+
title: string;
|
|
5
|
+
/** A line under the heading. */
|
|
6
|
+
subtitle?: string | undefined;
|
|
7
|
+
}
|
|
8
|
+
/** The title block at the top of an `Infobox`. */
|
|
9
|
+
declare const InfoboxHeader: import("svelte").Component<Props, {}, "">;
|
|
10
|
+
type InfoboxHeader = ReturnType<typeof InfoboxHeader>;
|
|
11
|
+
export default InfoboxHeader;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<!-- @component
|
|
2
|
+
The picture at the top of an `Infobox`, with its caption.
|
|
3
|
+
-->
|
|
4
|
+
<!--
|
|
5
|
+
InfoboxImage — the picture at the top of an `Infobox`, with its caption.
|
|
6
|
+
|
|
7
|
+
`width`/`height` are passed through so the browser can reserve the box
|
|
8
|
+
before the bytes arrive — the difference between a page that settles and
|
|
9
|
+
one that jumps under the reader's eye — but only reserve that space when
|
|
10
|
+
BOTH are present; the caller supplies them together or not at all.
|
|
11
|
+
|
|
12
|
+
`eager` (default off: an infobox usually sits beside the prose rather than
|
|
13
|
+
above it, so its picture is rarely the first thing painted) sets
|
|
14
|
+
`loading="eager"` and `fetchpriority="high"` together.
|
|
15
|
+
-->
|
|
16
|
+
<script lang="ts">let { src, alt, caption, width, height, eager = false, class: className, ...rest } = $props();
|
|
17
|
+
const loading = $derived(eager ? "eager" : "lazy");
|
|
18
|
+
const fetchpriority = $derived(eager ? "high" : undefined);
|
|
19
|
+
export {};
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<figure class="border-base-300 border-b">
|
|
23
|
+
<img
|
|
24
|
+
{...rest}
|
|
25
|
+
{src}
|
|
26
|
+
{alt}
|
|
27
|
+
{width}
|
|
28
|
+
{height}
|
|
29
|
+
{loading}
|
|
30
|
+
{fetchpriority}
|
|
31
|
+
class={['h-auto w-full', className]}
|
|
32
|
+
/>
|
|
33
|
+
{#if caption !== undefined && caption !== ''}
|
|
34
|
+
<figcaption class="text-base-content/60 px-4 py-2 text-center text-xs italic">
|
|
35
|
+
{caption}
|
|
36
|
+
</figcaption>
|
|
37
|
+
{/if}
|
|
38
|
+
</figure>
|