@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,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The citation vocabulary and its formatter.
|
|
3
|
+
*
|
|
4
|
+
* The shape a `Citation` carries is deliberately not any particular app's
|
|
5
|
+
* publication model: this package is app-agnostic and presentational, and a
|
|
6
|
+
* component that imported a models package would put the whole model layer
|
|
7
|
+
* into every consumer's bundle for the sake of a title and a year. The app
|
|
8
|
+
* maps its resource onto this shape at its own seam, which is also what lets
|
|
9
|
+
* a Storybook story or a test render one from a literal.
|
|
10
|
+
*
|
|
11
|
+
* ## Why the formatter is a pure function here
|
|
12
|
+
*
|
|
13
|
+
* Formatting inside the component recomputes on every re-render and cannot
|
|
14
|
+
* be tested without mounting a component. Pulling it out fixes both — the
|
|
15
|
+
* caller can memoise, the list derives once — and a citation style is a pile
|
|
16
|
+
* of conditional punctuation, which is exactly the kind of code that is
|
|
17
|
+
* wrong in the fourth branch and never noticed by eye.
|
|
18
|
+
*
|
|
19
|
+
* The style is a compact author–date one. It is not CSL: a full CSL
|
|
20
|
+
* implementation is a dependency and a style-file pipeline, and most
|
|
21
|
+
* consumers need one readable rendering rather than eleven switchable ones.
|
|
22
|
+
*
|
|
23
|
+
* @module
|
|
24
|
+
*/
|
|
25
|
+
// Absence is the empty string throughout the helpers below, not `T | undefined`.
|
|
26
|
+
// Every one of them feeds a join whose contract is already "skip the empty
|
|
27
|
+
// parts", so a missing clause genuinely IS "print nothing here".
|
|
28
|
+
/**
|
|
29
|
+
* The author phrase a compact style prints: one name, two joined by `&`, or
|
|
30
|
+
* the first followed by `et al.` A four-author paper printed in full is what
|
|
31
|
+
* makes a reference list unreadable, and the full byline is on the
|
|
32
|
+
* publication's own page.
|
|
33
|
+
*/
|
|
34
|
+
const authorPhrase = (authors) => {
|
|
35
|
+
const [first, second] = authors;
|
|
36
|
+
if (first === undefined)
|
|
37
|
+
return '';
|
|
38
|
+
if (authors.length === 1)
|
|
39
|
+
return first;
|
|
40
|
+
if (authors.length === 2)
|
|
41
|
+
return `${first} & ${second}`;
|
|
42
|
+
return `${first} et al.`;
|
|
43
|
+
};
|
|
44
|
+
/** Join the non-empty parts of a clause with `separator`. */
|
|
45
|
+
const joinPresent = (parts, separator) => parts.filter((part) => part !== '').join(separator);
|
|
46
|
+
// The detail clause is the one part of a citation that differs by kind, and
|
|
47
|
+
// the eleven kinds collapse into FIVE shapes. Each shape is its own function
|
|
48
|
+
// below rather than an arm of one switch: the arms do not share a line of
|
|
49
|
+
// logic, and a single switch over eleven labels with a nested ternary in
|
|
50
|
+
// three of them is the shape that is wrong in the fourth branch and never
|
|
51
|
+
// noticed by eye.
|
|
52
|
+
/** A serial's clause: container, volume(issue), pages. Articles, journals, conference papers. */
|
|
53
|
+
const serialClause = (citation) => {
|
|
54
|
+
if (citation.container === undefined)
|
|
55
|
+
return '';
|
|
56
|
+
const volume = citation.volume === undefined
|
|
57
|
+
? ''
|
|
58
|
+
: citation.issue === undefined
|
|
59
|
+
? `, ${citation.volume}`
|
|
60
|
+
: `, ${citation.volume}(${citation.issue})`;
|
|
61
|
+
const pages = citation.pages === undefined ? '' : `, ${citation.pages}`;
|
|
62
|
+
return `${citation.container}${volume}${pages}`;
|
|
63
|
+
};
|
|
64
|
+
/** A chapter's clause: the volume it is in, and where in it. */
|
|
65
|
+
const chapterClause = (citation) => joinPresent([
|
|
66
|
+
citation.container === undefined ? '' : `In: ${citation.container}`,
|
|
67
|
+
citation.pages === undefined ? '' : `pp. ${citation.pages}`,
|
|
68
|
+
], ', ');
|
|
69
|
+
/**
|
|
70
|
+
* An imprint clause: `place: publisher` when both are known, whichever one is
|
|
71
|
+
* known alone otherwise — the shape every compact style prints for a book, a
|
|
72
|
+
* thesis or a report.
|
|
73
|
+
*/
|
|
74
|
+
const imprintClause = (citation) => joinPresent([citation.place ?? '', citation.publisher ?? ''], ': ');
|
|
75
|
+
/**
|
|
76
|
+
* A web source's clause. The access date is the whole point of citing one: it
|
|
77
|
+
* says which version of a mutable page the claim rests on.
|
|
78
|
+
*/
|
|
79
|
+
const webClause = (citation) => {
|
|
80
|
+
if (citation.url === undefined)
|
|
81
|
+
return '';
|
|
82
|
+
const accessed = citation.accessed === undefined
|
|
83
|
+
? ''
|
|
84
|
+
: ` (accessed ${citation.accessed.toISOString().slice(0, 10)})`;
|
|
85
|
+
return `Available at: ${citation.url}${accessed}`;
|
|
86
|
+
};
|
|
87
|
+
/** A hosted artefact's clause: whichever of publisher / repository is known. */
|
|
88
|
+
const hostClause = (citation) => citation.publisher ?? citation.container ?? '';
|
|
89
|
+
/** Kind → the shape its detail clause takes. Exhaustive over {@link CitationKind}. */
|
|
90
|
+
const DETAIL_CLAUSES = {
|
|
91
|
+
article: serialClause,
|
|
92
|
+
periodical: serialClause,
|
|
93
|
+
conference: serialClause,
|
|
94
|
+
chapter: chapterClause,
|
|
95
|
+
book: imprintClause,
|
|
96
|
+
thesis: imprintClause,
|
|
97
|
+
report: imprintClause,
|
|
98
|
+
web: webClause,
|
|
99
|
+
dataset: hostClause,
|
|
100
|
+
software: hostClause,
|
|
101
|
+
media: hostClause,
|
|
102
|
+
};
|
|
103
|
+
/** The publication-detail clause for a citation's kind. */
|
|
104
|
+
const detailClause = (citation) => DETAIL_CLAUSES[citation.kind](citation);
|
|
105
|
+
/**
|
|
106
|
+
* Render a citation as one formatted string.
|
|
107
|
+
*
|
|
108
|
+
* Pure and total: every branch is reachable from real data and none throws on
|
|
109
|
+
* a citation carrying nothing but a title, which is a state the corpus can
|
|
110
|
+
* genuinely be in (an ingested reference awaiting enrichment).
|
|
111
|
+
*/
|
|
112
|
+
export const formatCitation = (citation) => {
|
|
113
|
+
const parts = joinPresent([
|
|
114
|
+
citation.authors === undefined ? '' : authorPhrase(citation.authors),
|
|
115
|
+
citation.year === undefined ? '' : `(${citation.year})`,
|
|
116
|
+
`“${citation.title}”`,
|
|
117
|
+
detailClause(citation),
|
|
118
|
+
citation.doi === undefined ? '' : `DOI: ${citation.doi}`,
|
|
119
|
+
], '. ');
|
|
120
|
+
return `${parts}.`;
|
|
121
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The record shapes the CSL-type citation renderers (`BookCitation`,
|
|
3
|
+
* `ChapterCitation`, `ScholarlyArticleCitation`, `ThesisCitation`,
|
|
4
|
+
* `ReportCitation`, `Publisher`) take.
|
|
5
|
+
*
|
|
6
|
+
* Defined HERE, locally, because the bibliographic model they will align with
|
|
7
|
+
* — `@thomasfosterau/biblio`. When it is, these types are
|
|
8
|
+
* expected to become structural subsets of its records (or re-exports of
|
|
9
|
+
* them), so an app maps once, at the seam, from its resource to these shapes.
|
|
10
|
+
* Until then they are deliberately plain: every contributor arm is already
|
|
11
|
+
* RESOLVED (a `Credit` with a name, not a linkage ref), every member is
|
|
12
|
+
* optional except what a reference cannot be written without, and nothing
|
|
13
|
+
* here knows about JSON:API, Effect or any app's resource vocabulary.
|
|
14
|
+
*
|
|
15
|
+
* Names follow Schema.org (`datePublished`, `numberOfPages`, `pageStart`),
|
|
16
|
+
* which is what the renderers emit as microdata.
|
|
17
|
+
*
|
|
18
|
+
* @module
|
|
19
|
+
*/
|
|
20
|
+
import type { Credit } from './CreditList.svelte';
|
|
21
|
+
/** A year, or an instant whose UTC year is the one cited. See `formatting/Year`. */
|
|
22
|
+
export type CitationDate = Date | number | string;
|
|
23
|
+
/** A publisher (or awarding institution): a Person for a self-published work, usually an Organization. */
|
|
24
|
+
export interface PublisherRecord {
|
|
25
|
+
readonly name: string;
|
|
26
|
+
/** The Schema.org type. Default `Organization`. */
|
|
27
|
+
readonly itemtype?: 'Person' | 'Organization' | undefined;
|
|
28
|
+
/** Rendered as a PostalAddress before the name ("Oxford, UK: …"); only present parts. */
|
|
29
|
+
readonly address?: {
|
|
30
|
+
readonly locality?: string | undefined;
|
|
31
|
+
readonly region?: string | undefined;
|
|
32
|
+
readonly country?: string | undefined;
|
|
33
|
+
} | undefined;
|
|
34
|
+
}
|
|
35
|
+
/** The credited contributors a record carries, in credit order. Order is meaning; the renderers never re-sort. */
|
|
36
|
+
export interface CreditedRecord {
|
|
37
|
+
readonly authors?: readonly Credit[] | undefined;
|
|
38
|
+
readonly editors?: readonly Credit[] | undefined;
|
|
39
|
+
readonly translators?: readonly Credit[] | undefined;
|
|
40
|
+
}
|
|
41
|
+
/** A book (CSL `book`). */
|
|
42
|
+
export interface BookRecord extends CreditedRecord {
|
|
43
|
+
readonly title?: string | undefined;
|
|
44
|
+
readonly datePublished?: CitationDate | undefined;
|
|
45
|
+
/** The edition as written: `3rd`, `Revised`. */
|
|
46
|
+
readonly edition?: string | undefined;
|
|
47
|
+
readonly numberOfPages?: number | undefined;
|
|
48
|
+
readonly isbn?: string | undefined;
|
|
49
|
+
readonly url?: string | undefined;
|
|
50
|
+
readonly publisher?: PublisherRecord | undefined;
|
|
51
|
+
/** The series it is part of — rendered as a Schema.org BookSeries. */
|
|
52
|
+
readonly series?: {
|
|
53
|
+
readonly name: string;
|
|
54
|
+
} | undefined;
|
|
55
|
+
}
|
|
56
|
+
/** A chapter in a book (CSL `chapter`). */
|
|
57
|
+
export interface ChapterRecord {
|
|
58
|
+
readonly title?: string | undefined;
|
|
59
|
+
readonly authors?: readonly Credit[] | undefined;
|
|
60
|
+
readonly chapterNumber?: string | number | undefined;
|
|
61
|
+
readonly pageStart?: string | number | undefined;
|
|
62
|
+
readonly pageEnd?: string | number | undefined;
|
|
63
|
+
/** The containing book, when resolved — rendered as a nested `BookCitation`. */
|
|
64
|
+
readonly book?: BookRecord | undefined;
|
|
65
|
+
}
|
|
66
|
+
/** A journal article (CSL `article-journal`). */
|
|
67
|
+
export interface ScholarlyArticleRecord {
|
|
68
|
+
readonly title?: string | undefined;
|
|
69
|
+
readonly authors?: readonly Credit[] | undefined;
|
|
70
|
+
/** Usually the issue's publication year. */
|
|
71
|
+
readonly datePublished?: CitationDate | undefined;
|
|
72
|
+
readonly periodical?: {
|
|
73
|
+
readonly title?: string | undefined;
|
|
74
|
+
readonly issn?: string | undefined;
|
|
75
|
+
} | undefined;
|
|
76
|
+
readonly volumeNumber?: string | number | undefined;
|
|
77
|
+
readonly issueNumber?: string | number | undefined;
|
|
78
|
+
readonly pageStart?: string | number | undefined;
|
|
79
|
+
readonly pageEnd?: string | number | undefined;
|
|
80
|
+
readonly doi?: string | undefined;
|
|
81
|
+
readonly url?: string | undefined;
|
|
82
|
+
}
|
|
83
|
+
/** A thesis (CSL `thesis`). */
|
|
84
|
+
export interface ThesisRecord {
|
|
85
|
+
readonly title?: string | undefined;
|
|
86
|
+
readonly authors?: readonly Credit[] | undefined;
|
|
87
|
+
readonly datePublished?: CitationDate | undefined;
|
|
88
|
+
/** The degree, as written: `PhD`, `MSc`. */
|
|
89
|
+
readonly degree?: string | undefined;
|
|
90
|
+
/** The awarding institution. */
|
|
91
|
+
readonly institution?: PublisherRecord | undefined;
|
|
92
|
+
readonly url?: string | undefined;
|
|
93
|
+
}
|
|
94
|
+
/** A report (CSL `report`). */
|
|
95
|
+
export interface ReportRecord extends CreditedRecord {
|
|
96
|
+
readonly title?: string | undefined;
|
|
97
|
+
readonly datePublished?: CitationDate | undefined;
|
|
98
|
+
readonly numberOfPages?: number | undefined;
|
|
99
|
+
readonly url?: string | undefined;
|
|
100
|
+
/** Usually the issuing Organization. */
|
|
101
|
+
readonly publisher?: PublisherRecord | undefined;
|
|
102
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The record shapes the CSL-type citation renderers (`BookCitation`,
|
|
3
|
+
* `ChapterCitation`, `ScholarlyArticleCitation`, `ThesisCitation`,
|
|
4
|
+
* `ReportCitation`, `Publisher`) take.
|
|
5
|
+
*
|
|
6
|
+
* Defined HERE, locally, because the bibliographic model they will align with
|
|
7
|
+
* — `@thomasfosterau/biblio`. When it is, these types are
|
|
8
|
+
* expected to become structural subsets of its records (or re-exports of
|
|
9
|
+
* them), so an app maps once, at the seam, from its resource to these shapes.
|
|
10
|
+
* Until then they are deliberately plain: every contributor arm is already
|
|
11
|
+
* RESOLVED (a `Credit` with a name, not a linkage ref), every member is
|
|
12
|
+
* optional except what a reference cannot be written without, and nothing
|
|
13
|
+
* here knows about JSON:API, Effect or any app's resource vocabulary.
|
|
14
|
+
*
|
|
15
|
+
* Names follow Schema.org (`datePublished`, `numberOfPages`, `pageStart`),
|
|
16
|
+
* which is what the renderers emit as microdata.
|
|
17
|
+
*
|
|
18
|
+
* @module
|
|
19
|
+
*/
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@thomasfosterau/components/citations` — a separate entry point so it never sits on
|
|
3
|
+
* the always-loaded path of a consumer that does not use it.
|
|
4
|
+
*
|
|
5
|
+
* A page's `<Citation>` and `<Footnote>` markers register into a shared
|
|
6
|
+
* `BibliographyManager` (provided by `setupBibliography`); `<ReferenceList>`
|
|
7
|
+
* and `<FootnoteList>` render what accumulated. `<CreditList>` renders a
|
|
8
|
+
* locale-joined byline for one role (authors, editors, translators, or a
|
|
9
|
+
* caller's own). `formatCitation` is the pure formatter the lists use, and
|
|
10
|
+
* `DOILink` / `ISBNLink` / `ISSNLink` render the three identifiers a
|
|
11
|
+
* citation commonly carries as resolvable links.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
export { BibliographyManager, getBibliographyContext, setupBibliography, type BibliographyEntry, } from './bibliography.svelte.ts';
|
|
16
|
+
export { formatCitation, type Citation as CitationData, type CitationKind, type Footnote as FootnoteData, } from './citation.ts';
|
|
17
|
+
export { default as Citation } from './Citation.svelte';
|
|
18
|
+
export { default as CreditList, type Credit } from './CreditList.svelte';
|
|
19
|
+
export { default as DOILink } from './DOILink.svelte';
|
|
20
|
+
export { default as Footnote } from './Footnote.svelte';
|
|
21
|
+
export { default as FootnoteList } from './FootnoteList.svelte';
|
|
22
|
+
export { default as ISBNLink } from './ISBNLink.svelte';
|
|
23
|
+
export { default as ISSNLink } from './ISSNLink.svelte';
|
|
24
|
+
export { default as ReferenceList } from './ReferenceList.svelte';
|
|
25
|
+
export { default as BookCitation } from './BookCitation.svelte';
|
|
26
|
+
export { default as ChapterCitation } from './ChapterCitation.svelte';
|
|
27
|
+
export { default as ScholarlyArticleCitation } from './ScholarlyArticleCitation.svelte';
|
|
28
|
+
export { default as ThesisCitation } from './ThesisCitation.svelte';
|
|
29
|
+
export { default as ReportCitation } from './ReportCitation.svelte';
|
|
30
|
+
export { default as Publisher } from './Publisher.svelte';
|
|
31
|
+
export type { BookRecord, ChapterRecord, CitationDate, CreditedRecord, PublisherRecord, ReportRecord, ScholarlyArticleRecord, ThesisRecord, } from './csl.ts';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@thomasfosterau/components/citations` — a separate entry point so it never sits on
|
|
3
|
+
* the always-loaded path of a consumer that does not use it.
|
|
4
|
+
*
|
|
5
|
+
* A page's `<Citation>` and `<Footnote>` markers register into a shared
|
|
6
|
+
* `BibliographyManager` (provided by `setupBibliography`); `<ReferenceList>`
|
|
7
|
+
* and `<FootnoteList>` render what accumulated. `<CreditList>` renders a
|
|
8
|
+
* locale-joined byline for one role (authors, editors, translators, or a
|
|
9
|
+
* caller's own). `formatCitation` is the pure formatter the lists use, and
|
|
10
|
+
* `DOILink` / `ISBNLink` / `ISSNLink` render the three identifiers a
|
|
11
|
+
* citation commonly carries as resolvable links.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
export { BibliographyManager, getBibliographyContext, setupBibliography, } from "./bibliography.svelte.js";
|
|
16
|
+
// The two data shapes are re-exported under `*Data` names: `Citation` and
|
|
17
|
+
// `Footnote` are already taken by the components below, and a module cannot
|
|
18
|
+
// export a type and a Svelte component under one identifier (the
|
|
19
|
+
// component's binding is a value in the same declaration space).
|
|
20
|
+
export { formatCitation, } from "./citation.js";
|
|
21
|
+
export { default as Citation } from './Citation.svelte';
|
|
22
|
+
export { default as CreditList } from './CreditList.svelte';
|
|
23
|
+
export { default as DOILink } from './DOILink.svelte';
|
|
24
|
+
export { default as Footnote } from './Footnote.svelte';
|
|
25
|
+
export { default as FootnoteList } from './FootnoteList.svelte';
|
|
26
|
+
export { default as ISBNLink } from './ISBNLink.svelte';
|
|
27
|
+
export { default as ISSNLink } from './ISSNLink.svelte';
|
|
28
|
+
export { default as ReferenceList } from './ReferenceList.svelte';
|
|
29
|
+
// The CSL-type renderers, over the local record shapes in `./csl.ts` (which
|
|
30
|
+
// will align with `@thomasfosterau/biblio` once that is published).
|
|
31
|
+
export { default as BookCitation } from './BookCitation.svelte';
|
|
32
|
+
export { default as ChapterCitation } from './ChapterCitation.svelte';
|
|
33
|
+
export { default as ScholarlyArticleCitation } from './ScholarlyArticleCitation.svelte';
|
|
34
|
+
export { default as ThesisCitation } from './ThesisCitation.svelte';
|
|
35
|
+
export { default as ReportCitation } from './ReportCitation.svelte';
|
|
36
|
+
export { default as Publisher } from './Publisher.svelte';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small `Intl.ListFormat` helper for `CreditList`.
|
|
3
|
+
*
|
|
4
|
+
* `CreditList` needs to interleave real markup (a name, possibly a link)
|
|
5
|
+
* between a locale's list separators ("A, B and C") — exactly what
|
|
6
|
+
* `Intl.ListFormat.formatToParts` is for. This is a deliberately independent
|
|
7
|
+
* implementation rather than a dependency on the `formatting/` family's
|
|
8
|
+
* `FormatList`: that family is owned by a different part of this build, and
|
|
9
|
+
* this is a handful of lines, not a shared abstraction worth a cross-family
|
|
10
|
+
* edge for.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
/** One piece of a formatted list: an item from the input, or literal separator text. */
|
|
15
|
+
export type ListFormatPart<T> = {
|
|
16
|
+
readonly kind: 'item';
|
|
17
|
+
readonly value: T;
|
|
18
|
+
} | {
|
|
19
|
+
readonly kind: 'separator';
|
|
20
|
+
readonly value: string;
|
|
21
|
+
};
|
|
22
|
+
/** The `Intl.ListFormat` options `formatListParts` takes; omitted fields use the runtime defaults. */
|
|
23
|
+
export interface ListFormatOptions {
|
|
24
|
+
readonly locales?: string | readonly string[] | undefined;
|
|
25
|
+
readonly style?: Intl.ListFormatOptions['style'] | undefined;
|
|
26
|
+
readonly type?: Intl.ListFormatOptions['type'] | undefined;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Interleave `items` with the given locale's list punctuation, without
|
|
30
|
+
* losing each item's identity — `formatToParts` only ever hands back
|
|
31
|
+
* strings, so this maps its "element" parts back to the original items by
|
|
32
|
+
* the index it was asked to format.
|
|
33
|
+
*/
|
|
34
|
+
export declare const formatListParts: <T>(items: readonly T[], options?: ListFormatOptions) => readonly ListFormatPart<T>[];
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small `Intl.ListFormat` helper for `CreditList`.
|
|
3
|
+
*
|
|
4
|
+
* `CreditList` needs to interleave real markup (a name, possibly a link)
|
|
5
|
+
* between a locale's list separators ("A, B and C") — exactly what
|
|
6
|
+
* `Intl.ListFormat.formatToParts` is for. This is a deliberately independent
|
|
7
|
+
* implementation rather than a dependency on the `formatting/` family's
|
|
8
|
+
* `FormatList`: that family is owned by a different part of this build, and
|
|
9
|
+
* this is a handful of lines, not a shared abstraction worth a cross-family
|
|
10
|
+
* edge for.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Interleave `items` with the given locale's list punctuation, without
|
|
16
|
+
* losing each item's identity — `formatToParts` only ever hands back
|
|
17
|
+
* strings, so this maps its "element" parts back to the original items by
|
|
18
|
+
* the index it was asked to format.
|
|
19
|
+
*/
|
|
20
|
+
export const formatListParts = (items, options = {}) => {
|
|
21
|
+
const format = new Intl.ListFormat(options.locales, {
|
|
22
|
+
style: options.style ?? 'long',
|
|
23
|
+
type: options.type ?? 'conjunction',
|
|
24
|
+
});
|
|
25
|
+
const indices = items.map((_, index) => String(index));
|
|
26
|
+
return format.formatToParts(indices).map((part) => {
|
|
27
|
+
if (part.type !== 'element')
|
|
28
|
+
return { kind: 'separator', value: part.value };
|
|
29
|
+
// `formatToParts` only ever hands back the index strings this function
|
|
30
|
+
// itself generated one line up, one per `items` entry — the lookup
|
|
31
|
+
// cannot miss.
|
|
32
|
+
const index = Number.parseInt(part.value, 10);
|
|
33
|
+
const value = items[index];
|
|
34
|
+
if (value === undefined)
|
|
35
|
+
throw new Error(`formatListParts: no item at index ${index}`);
|
|
36
|
+
return { kind: 'item', value };
|
|
37
|
+
});
|
|
38
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The app-integration seam.
|
|
3
|
+
*
|
|
4
|
+
* A component here must not import `$app/*`: that would tie the package to
|
|
5
|
+
* SvelteKit and make it untestable outside one. The four things an app has
|
|
6
|
+
* to supply — how a path becomes an href, how to navigate, which href is the
|
|
7
|
+
* current page, and which locale `Intl` should format for — are read from
|
|
8
|
+
* this injected context instead. It carries NO copy: every user-facing
|
|
9
|
+
* string a component renders is a prop with an English default, so an i18n
|
|
10
|
+
* app passes its own messages at the call site.
|
|
11
|
+
*
|
|
12
|
+
* The app calls {@link setComponentsContext} once in its root layout. When
|
|
13
|
+
* no provider is mounted — unit tests, Storybook, a plain Svelte app —
|
|
14
|
+
* {@link defaultComponentsContext} supplies identity hrefs, `window.location`
|
|
15
|
+
* for navigation and the current-page check, and the runtime's default
|
|
16
|
+
* locale, so every component works unwired.
|
|
17
|
+
*
|
|
18
|
+
* @module
|
|
19
|
+
*/
|
|
20
|
+
/** What an app supplies through `setComponentsContext`: link resolution, navigation, the current-page check and the formatting locale. */
|
|
21
|
+
export interface ComponentsContext {
|
|
22
|
+
/**
|
|
23
|
+
* The locale(s) `Intl` formatters use when a component is given none.
|
|
24
|
+
* `undefined` means the runtime default. A function, not a value, so a
|
|
25
|
+
* reactive i18n runtime is re-read on every render.
|
|
26
|
+
*/
|
|
27
|
+
readonly locale: () => string | readonly string[] | undefined;
|
|
28
|
+
/** Resolve an app-relative path (`/books/1`) to the href to render. */
|
|
29
|
+
readonly href: (path: string) => string;
|
|
30
|
+
/** Client-side navigation (SvelteKit's `goto` in an app). */
|
|
31
|
+
readonly navigate: (url: string) => Promise<void> | void;
|
|
32
|
+
/** Whether `href` is the page currently shown — drives `aria-current="page"`. */
|
|
33
|
+
readonly isCurrent: (href: string) => boolean;
|
|
34
|
+
}
|
|
35
|
+
/** The context used when no provider is mounted. */
|
|
36
|
+
export declare const defaultComponentsContext: ComponentsContext;
|
|
37
|
+
/**
|
|
38
|
+
* Provide the app-integration context. Call once, high in the tree (the root
|
|
39
|
+
* layout). Omitted members — or members passed as `undefined` — fall back to
|
|
40
|
+
* {@link defaultComponentsContext} individually.
|
|
41
|
+
*/
|
|
42
|
+
export declare function setComponentsContext(context: {
|
|
43
|
+
readonly [K in keyof ComponentsContext]?: ComponentsContext[K] | undefined;
|
|
44
|
+
}): void;
|
|
45
|
+
/** Read the app-integration context, falling back to {@link defaultComponentsContext}. */
|
|
46
|
+
export declare function getComponentsContext(): ComponentsContext;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The app-integration seam.
|
|
3
|
+
*
|
|
4
|
+
* A component here must not import `$app/*`: that would tie the package to
|
|
5
|
+
* SvelteKit and make it untestable outside one. The four things an app has
|
|
6
|
+
* to supply — how a path becomes an href, how to navigate, which href is the
|
|
7
|
+
* current page, and which locale `Intl` should format for — are read from
|
|
8
|
+
* this injected context instead. It carries NO copy: every user-facing
|
|
9
|
+
* string a component renders is a prop with an English default, so an i18n
|
|
10
|
+
* app passes its own messages at the call site.
|
|
11
|
+
*
|
|
12
|
+
* The app calls {@link setComponentsContext} once in its root layout. When
|
|
13
|
+
* no provider is mounted — unit tests, Storybook, a plain Svelte app —
|
|
14
|
+
* {@link defaultComponentsContext} supplies identity hrefs, `window.location`
|
|
15
|
+
* for navigation and the current-page check, and the runtime's default
|
|
16
|
+
* locale, so every component works unwired.
|
|
17
|
+
*
|
|
18
|
+
* @module
|
|
19
|
+
*/
|
|
20
|
+
import { getContext, setContext } from 'svelte';
|
|
21
|
+
const hasWindow = () => typeof globalThis.window !== 'undefined';
|
|
22
|
+
/** The pathname of the document currently shown, or `''` off the client. */
|
|
23
|
+
const currentPathname = () => (hasWindow() ? globalThis.window.location.pathname : '');
|
|
24
|
+
/** The context used when no provider is mounted. */
|
|
25
|
+
export const defaultComponentsContext = {
|
|
26
|
+
locale: () => undefined,
|
|
27
|
+
href: (path) => path,
|
|
28
|
+
navigate: (url) => {
|
|
29
|
+
if (hasWindow())
|
|
30
|
+
globalThis.window.location.assign(url);
|
|
31
|
+
},
|
|
32
|
+
isCurrent: (href) => href !== '' && href === currentPathname(),
|
|
33
|
+
};
|
|
34
|
+
const CONTEXT_KEY = Symbol('@thomasfosterau/components:context');
|
|
35
|
+
/**
|
|
36
|
+
* Provide the app-integration context. Call once, high in the tree (the root
|
|
37
|
+
* layout). Omitted members — or members passed as `undefined` — fall back to
|
|
38
|
+
* {@link defaultComponentsContext} individually.
|
|
39
|
+
*/
|
|
40
|
+
export function setComponentsContext(context) {
|
|
41
|
+
setContext(CONTEXT_KEY, {
|
|
42
|
+
locale: context.locale ?? defaultComponentsContext.locale,
|
|
43
|
+
href: context.href ?? defaultComponentsContext.href,
|
|
44
|
+
navigate: context.navigate ?? defaultComponentsContext.navigate,
|
|
45
|
+
isCurrent: context.isCurrent ?? defaultComponentsContext.isCurrent,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/** Read the app-integration context, falling back to {@link defaultComponentsContext}. */
|
|
49
|
+
export function getComponentsContext() {
|
|
50
|
+
return getContext(CONTEXT_KEY) ?? defaultComponentsContext;
|
|
51
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
<!-- @component
|
|
2
|
+
A `Table` with client-side sorting, filtering across every column and paging, for a list that is already entirely in hand.
|
|
3
|
+
-->
|
|
4
|
+
<!--
|
|
5
|
+
DataGrid — the three controls a reader wants over a list that is already
|
|
6
|
+
entirely in hand: sort a column, filter across every column, page through
|
|
7
|
+
the result.
|
|
8
|
+
|
|
9
|
+
It draws no table of its own. The rows go through `Table` and the pager
|
|
10
|
+
through `Pagination`, so the cell markup, the empty state, the sort control
|
|
11
|
+
and the disabled-arm rule all have exactly one implementation, and this file
|
|
12
|
+
is the decisions rather than the markup.
|
|
13
|
+
|
|
14
|
+
**Client-side, and that is the constraint.** Every derivation below runs
|
|
15
|
+
over the whole `rows` array on every keystroke, so this is for a bounded set
|
|
16
|
+
the page already holds — a vocabulary, an account's devices, a taxon's
|
|
17
|
+
synonyms. A collection the API pages belongs on `Table` + `PaginationFooter`,
|
|
18
|
+
where the server does the work; pointing this at one would silently sort and
|
|
19
|
+
filter the current page only, which reads as data loss rather than as a
|
|
20
|
+
limitation.
|
|
21
|
+
|
|
22
|
+
ARIA contract: the `Table`'s and the `Pagination`'s. The filter is a
|
|
23
|
+
`<input type="search">` with a real label (visually hidden, because the
|
|
24
|
+
control sits in a toolbar where a visible label would be the only one), and
|
|
25
|
+
the rows-per-page `<select>` is named by `rowsPerPageLabel`.
|
|
26
|
+
-->
|
|
27
|
+
<script lang="ts" generics="T">import { getComponentsContext } from "../context.svelte.js";
|
|
28
|
+
import Pagination from "./Pagination.svelte";
|
|
29
|
+
import Table from "./Table.svelte";
|
|
30
|
+
let { rows, columns, rowKey, filterable = false, paginated = true, pageSize = 10, pageSizeOptions = [
|
|
31
|
+
10,
|
|
32
|
+
25,
|
|
33
|
+
50,
|
|
34
|
+
100
|
|
35
|
+
], caption, captionHidden, size, zebra, pinRows, sort = $bindable(undefined), onSelect, emptyMessage, emptyState, filterLabel = "Search", filterPlaceholder = "Search…", rowsPerPageLabel = "Rows per page", showingLabel = (start, end, total) => `Showing ${start} to ${end} of ${total}`, pageOfLabel = (page, pages) => `Page ${page} of ${pages}`, previousLabel = "Previous", nextLabel = "Next", paginationLabel = "Pagination", sortLabel, locales = getComponentsContext().locale(), class: className, ...rest } = $props();
|
|
36
|
+
let filter = $state("");
|
|
37
|
+
// 0 is "the reader has not chosen": the `pageSize` prop stays authoritative
|
|
38
|
+
// until they pick from the menu, so a page that changes the prop is honoured
|
|
39
|
+
// rather than overridden by state seeded once at mount.
|
|
40
|
+
let chosenSize = $state(0);
|
|
41
|
+
let page = $state(0);
|
|
42
|
+
const limit = $derived(chosenSize === 0 ? pageSize : chosenSize);
|
|
43
|
+
const filtered = $derived.by(() => {
|
|
44
|
+
const needle = filter.trim().toLowerCase();
|
|
45
|
+
if (needle === "") return rows;
|
|
46
|
+
// The filter searches the column's VALUE, not its rendered cell: a cell
|
|
47
|
+
// may be a badge or a link, and a reader typing what they can see is
|
|
48
|
+
// typing the value either way.
|
|
49
|
+
return rows.filter((row) => columns.some((column) => String(column.value(row)).toLowerCase().includes(needle)));
|
|
50
|
+
});
|
|
51
|
+
const sorted = $derived.by(() => {
|
|
52
|
+
const active = sort;
|
|
53
|
+
if (active === undefined) return filtered;
|
|
54
|
+
const column = columns.find((candidate) => candidate.key === active.key);
|
|
55
|
+
if (column === undefined) return filtered;
|
|
56
|
+
const direction = active.direction === "asc" ? 1 : -1;
|
|
57
|
+
// `localeCompare` in the active locale, so a Swedish reader gets Swedish
|
|
58
|
+
// collation rather than code-point order; numbers compare numerically,
|
|
59
|
+
// because "10" sorts before "9" as a string and that is always wrong.
|
|
60
|
+
return filtered.toSorted((left, right) => {
|
|
61
|
+
const a = column.value(left);
|
|
62
|
+
const b = column.value(right);
|
|
63
|
+
if (typeof a === "number" && typeof b === "number") return (a - b) * direction;
|
|
64
|
+
return String(a).localeCompare(String(b), locales) * direction;
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
const total = $derived(sorted.length);
|
|
68
|
+
const pageCount = $derived(paginated ? Math.max(1, Math.ceil(total / limit)) : 1);
|
|
69
|
+
// A filter that shrinks the result out from under the current page would
|
|
70
|
+
// otherwise leave the reader looking at an empty body with rows they cannot
|
|
71
|
+
// reach. Clamping in a derivation rather than in the filter's handler keeps
|
|
72
|
+
// it true however the page count changed — including a `rows` prop that
|
|
73
|
+
// shrank without anyone typing.
|
|
74
|
+
const current = $derived(Math.min(page, pageCount - 1));
|
|
75
|
+
const visible = $derived(paginated ? sorted.slice(current * limit, current * limit + limit) : sorted);
|
|
76
|
+
const from = $derived(total === 0 ? 0 : current * limit + 1);
|
|
77
|
+
const to = $derived(Math.min((current + 1) * limit, total));
|
|
78
|
+
</script>
|
|
79
|
+
|
|
80
|
+
<div {...rest} class={['space-y-3', className]}>
|
|
81
|
+
{#if filterable}
|
|
82
|
+
<label class="input input-sm w-full max-w-xs">
|
|
83
|
+
<span class="sr-only">{filterLabel}</span>
|
|
84
|
+
<input
|
|
85
|
+
type="search"
|
|
86
|
+
bind:value={filter}
|
|
87
|
+
oninput={() => {
|
|
88
|
+
page = 0;
|
|
89
|
+
}}
|
|
90
|
+
placeholder={filterPlaceholder}
|
|
91
|
+
aria-label={filterLabel}
|
|
92
|
+
/>
|
|
93
|
+
</label>
|
|
94
|
+
{/if}
|
|
95
|
+
|
|
96
|
+
<Table
|
|
97
|
+
rows={visible}
|
|
98
|
+
{columns}
|
|
99
|
+
{rowKey}
|
|
100
|
+
{caption}
|
|
101
|
+
{captionHidden}
|
|
102
|
+
{size}
|
|
103
|
+
{zebra}
|
|
104
|
+
{pinRows}
|
|
105
|
+
{onSelect}
|
|
106
|
+
{emptyMessage}
|
|
107
|
+
{emptyState}
|
|
108
|
+
{sortLabel}
|
|
109
|
+
bind:sort
|
|
110
|
+
onSort={() => {
|
|
111
|
+
// A new sort re-orders the whole result, so the reader's place in the
|
|
112
|
+
// old order means nothing; page 1 is the only honest answer.
|
|
113
|
+
page = 0;
|
|
114
|
+
}}
|
|
115
|
+
/>
|
|
116
|
+
|
|
117
|
+
{#if paginated && total > 0}
|
|
118
|
+
<div class="flex flex-wrap items-center justify-between gap-3">
|
|
119
|
+
<div class="flex items-center gap-2">
|
|
120
|
+
<p class="text-base-content/70 text-sm tabular-nums">{showingLabel(from, to, total)}</p>
|
|
121
|
+
<select
|
|
122
|
+
class="select select-sm w-auto"
|
|
123
|
+
aria-label={rowsPerPageLabel}
|
|
124
|
+
value={limit}
|
|
125
|
+
onchange={(event) => {
|
|
126
|
+
chosenSize = Number(event.currentTarget.value);
|
|
127
|
+
page = 0;
|
|
128
|
+
}}
|
|
129
|
+
>
|
|
130
|
+
{#each pageSizeOptions as option (option)}
|
|
131
|
+
<option value={option}>{option}</option>
|
|
132
|
+
{/each}
|
|
133
|
+
</select>
|
|
134
|
+
</div>
|
|
135
|
+
<Pagination
|
|
136
|
+
offset={current * limit}
|
|
137
|
+
{limit}
|
|
138
|
+
{total}
|
|
139
|
+
variant="prevNext"
|
|
140
|
+
label={paginationLabel}
|
|
141
|
+
{previousLabel}
|
|
142
|
+
{nextLabel}
|
|
143
|
+
statusLabel={pageOfLabel}
|
|
144
|
+
onPage={(offset) => {
|
|
145
|
+
page = Math.floor(offset / limit);
|
|
146
|
+
}}
|
|
147
|
+
/>
|
|
148
|
+
</div>
|
|
149
|
+
{/if}
|
|
150
|
+
</div>
|