@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,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The content-addressed Open Graph share-image URL builder.
|
|
3
|
+
*
|
|
4
|
+
* Shared by `<Head>` (which emits the URL as `og:image`) and whatever renders
|
|
5
|
+
* the card (an app route or Worker reading the same query params), so the
|
|
6
|
+
* producer and the consumer of the URL can never disagree on the param names
|
|
7
|
+
* or the length caps — import the constants below on both sides.
|
|
8
|
+
*
|
|
9
|
+
* The URL is **content-addressed**: the title and description ride in the
|
|
10
|
+
* query, and a `v` token derived from the content's last-modified instant
|
|
11
|
+
* changes the whole URL whenever the content changes. A new URL is how the
|
|
12
|
+
* image cache-busts — link-preview scrapers (Facebook, LinkedIn, federated
|
|
13
|
+
* servers) cache aggressively and only re-fetch a URL they have not seen, so
|
|
14
|
+
* the version token is load-bearing, not cosmetic.
|
|
15
|
+
*
|
|
16
|
+
* @module
|
|
17
|
+
*/
|
|
18
|
+
/** Query param carrying the card title. */
|
|
19
|
+
export const OG_TITLE_PARAM = 't';
|
|
20
|
+
/** Query param carrying the card's second line (the page description). */
|
|
21
|
+
export const OG_SUBTITLE_PARAM = 'd';
|
|
22
|
+
/** Query param carrying the cache-busting version token. */
|
|
23
|
+
export const OG_VERSION_PARAM = 'v';
|
|
24
|
+
/**
|
|
25
|
+
* Length caps on the title / description carried in the URL. A renderer
|
|
26
|
+
* should re-apply them defensively; here they keep the emitted URL short.
|
|
27
|
+
*/
|
|
28
|
+
export const OG_TITLE_MAX_LENGTH = 120;
|
|
29
|
+
/** Longest description, in characters, carried in a share-card URL; see `OG_TITLE_MAX_LENGTH`. */
|
|
30
|
+
export const OG_SUBTITLE_MAX_LENGTH = 200;
|
|
31
|
+
/** The rendered card's size — the 1.91:1 Open Graph standard. */
|
|
32
|
+
export const OG_IMAGE_WIDTH = 1200;
|
|
33
|
+
/** Share-card height in pixels; see `OG_IMAGE_WIDTH`. */
|
|
34
|
+
export const OG_IMAGE_HEIGHT = 630;
|
|
35
|
+
/**
|
|
36
|
+
* Reduce an identity to a short URL-safe slug. Cosmetic — a renderer should
|
|
37
|
+
* ignore it — so an empty or garbage id collapses to a stable fallback rather
|
|
38
|
+
* than an invalid path.
|
|
39
|
+
*/
|
|
40
|
+
const toSlug = (id) => {
|
|
41
|
+
const slug = id
|
|
42
|
+
.toLowerCase()
|
|
43
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
44
|
+
.replace(/^-+|-+$/g, '')
|
|
45
|
+
.slice(0, 64);
|
|
46
|
+
return slug === '' ? 'page' : slug;
|
|
47
|
+
};
|
|
48
|
+
/** Epoch milliseconds of `updatedAt`, or `undefined` when absent or unparseable. */
|
|
49
|
+
const versionToken = (updatedAt) => {
|
|
50
|
+
if (updatedAt === null || updatedAt === undefined)
|
|
51
|
+
return undefined;
|
|
52
|
+
const time = updatedAt instanceof Date
|
|
53
|
+
? updatedAt.getTime()
|
|
54
|
+
: typeof updatedAt === 'number'
|
|
55
|
+
? updatedAt
|
|
56
|
+
: Date.parse(updatedAt);
|
|
57
|
+
return Number.isFinite(time) ? String(time) : undefined;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Build the `og:image` URL for a page's share card: `<basePath>/<slug>.png`
|
|
61
|
+
* resolved through `href`, with the title, description and version token in
|
|
62
|
+
* the query. Returns `undefined` for a blank title, so the caller falls back
|
|
63
|
+
* to its default share image.
|
|
64
|
+
*/
|
|
65
|
+
export function ogImageUrl(card, options = {}) {
|
|
66
|
+
const title = card.title.trim();
|
|
67
|
+
if (title === '')
|
|
68
|
+
return undefined;
|
|
69
|
+
const params = new URLSearchParams();
|
|
70
|
+
params.set(OG_TITLE_PARAM, title.slice(0, OG_TITLE_MAX_LENGTH));
|
|
71
|
+
const description = card.description?.trim();
|
|
72
|
+
if (description !== undefined && description !== '') {
|
|
73
|
+
params.set(OG_SUBTITLE_PARAM, description.slice(0, OG_SUBTITLE_MAX_LENGTH));
|
|
74
|
+
}
|
|
75
|
+
const version = versionToken(card.updatedAt);
|
|
76
|
+
if (version !== undefined)
|
|
77
|
+
params.set(OG_VERSION_PARAM, version);
|
|
78
|
+
const basePath = (options.basePath ?? '/og').replace(/\/+$/, '');
|
|
79
|
+
const href = options.href ?? ((path) => path);
|
|
80
|
+
return `${href(`${basePath}/${toSlug(card.id)}.png`)}?${params.toString()}`;
|
|
81
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The data shapes `<Head>` takes.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
/** The Open Graph `og:type` values `<Head>` emits. */
|
|
7
|
+
export type OpenGraphType = 'website' | 'article' | 'profile';
|
|
8
|
+
/**
|
|
9
|
+
* The Schema.org page types `<Head>` understands, used only to derive
|
|
10
|
+
* `og:type` when `openGraphType` is not given (so it is never empty).
|
|
11
|
+
*/
|
|
12
|
+
export type SchemaPageType = 'WebPage' | 'WebSite' | 'CollectionPage' | 'Article' | 'BlogPosting' | 'ProfilePage';
|
|
13
|
+
/** The Twitter / X card variants. */
|
|
14
|
+
export type TwitterCard = 'summary' | 'summary_large_image';
|
|
15
|
+
/** A social preview image. `url` must be absolute. */
|
|
16
|
+
export interface HeadImage {
|
|
17
|
+
readonly url: string;
|
|
18
|
+
readonly alt?: string | undefined;
|
|
19
|
+
readonly width?: number | undefined;
|
|
20
|
+
readonly height?: number | undefined;
|
|
21
|
+
/** The MIME type, e.g. `image/png`. */
|
|
22
|
+
readonly type?: string | undefined;
|
|
23
|
+
}
|
|
24
|
+
/** One `<link rel="alternate" hreflang>`. */
|
|
25
|
+
export interface AlternateLink {
|
|
26
|
+
readonly href: string;
|
|
27
|
+
/** A BCP 47 tag, or `x-default`. */
|
|
28
|
+
readonly lang: string;
|
|
29
|
+
}
|
|
30
|
+
/** The `robots` meta directive. Both default to `true`. */
|
|
31
|
+
export interface Robots {
|
|
32
|
+
readonly index?: boolean | undefined;
|
|
33
|
+
readonly follow?: boolean | undefined;
|
|
34
|
+
}
|
|
35
|
+
/** A feed advertised with `<link rel="alternate">`. */
|
|
36
|
+
export interface FeedLink {
|
|
37
|
+
/** App-relative path, resolved through `canonicalHref` like the canonical URL. */
|
|
38
|
+
readonly path: string;
|
|
39
|
+
/** The feed's title. Default `'Latest changes'`. */
|
|
40
|
+
readonly title?: string | undefined;
|
|
41
|
+
/** The feed's MIME type. Default `application/feed+json` (JSON Feed 1.1). */
|
|
42
|
+
readonly type?: string | undefined;
|
|
43
|
+
}
|
package/dist/theme.css
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The library's design tokens: two daisyUI themes, `light` and `dark`, that
|
|
3
|
+
* every component in this package renders on.
|
|
4
|
+
*
|
|
5
|
+
* Shipped as the package's `./theme.css` export and imported by BOTH
|
|
6
|
+
* consumers — an application's CSS entry, and Storybook's
|
|
7
|
+
* `.storybook/preview.css` relatively — so there is exactly one copy of every
|
|
8
|
+
* token.
|
|
9
|
+
*
|
|
10
|
+
* **This is the whole styling vocabulary.** A component here reaches for a
|
|
11
|
+
* semantic name — `bg-base-200`, `text-error`, `btn-primary` — and never for
|
|
12
|
+
* a raw palette class (`bg-green-700`), a hex, or an `oklch()` of its own. Two
|
|
13
|
+
* vocabularies for one palette is how a design system drifts.
|
|
14
|
+
*
|
|
15
|
+
* The values below are DELIBERATELY NEUTRAL: an achromatic surface ramp and a
|
|
16
|
+
* single mid-slate brand hue, so the library looks like nothing in particular
|
|
17
|
+
* out of the box. They are a starting point, not a brand. An application
|
|
18
|
+
* overrides them by declaring its own `light` / `dark` themes after importing
|
|
19
|
+
* this file, or by naming its own themes entirely and never importing it.
|
|
20
|
+
*/
|
|
21
|
+
@plugin "daisyui/theme" {
|
|
22
|
+
name: 'light';
|
|
23
|
+
default: true;
|
|
24
|
+
color-scheme: light;
|
|
25
|
+
/* Surfaces: a neutral off-white ramp. */
|
|
26
|
+
--color-base-100: oklch(100% 0 0);
|
|
27
|
+
--color-base-200: oklch(96% 0 0);
|
|
28
|
+
--color-base-300: oklch(91% 0 0);
|
|
29
|
+
--color-base-content: oklch(22% 0 0);
|
|
30
|
+
/* Brand: slate. Replace these four pairs, not the surface ramp. */
|
|
31
|
+
--color-primary: oklch(45% 0.03 260);
|
|
32
|
+
--color-primary-content: oklch(98% 0 0);
|
|
33
|
+
--color-secondary: oklch(55% 0.02 260);
|
|
34
|
+
--color-secondary-content: oklch(98% 0 0);
|
|
35
|
+
--color-accent: oklch(60% 0.02 260);
|
|
36
|
+
--color-accent-content: oklch(98% 0 0);
|
|
37
|
+
--color-neutral: oklch(30% 0 0);
|
|
38
|
+
--color-neutral-content: oklch(96% 0 0);
|
|
39
|
+
/* Status: blue / green / amber / red, at their conventional roles. */
|
|
40
|
+
--color-info: oklch(62.3% 0.214 259.815);
|
|
41
|
+
--color-info-content: oklch(97% 0.01 260);
|
|
42
|
+
--color-success: oklch(59.6% 0.145 163.225);
|
|
43
|
+
--color-success-content: oklch(97% 0.02 163);
|
|
44
|
+
--color-warning: oklch(76.9% 0.188 70.08);
|
|
45
|
+
--color-warning-content: oklch(28% 0.07 70);
|
|
46
|
+
--color-error: oklch(57.7% 0.245 27.325);
|
|
47
|
+
--color-error-content: oklch(97% 0.01 27);
|
|
48
|
+
--radius-selector: 1rem;
|
|
49
|
+
--radius-field: 0.375rem;
|
|
50
|
+
--radius-box: 0.75rem;
|
|
51
|
+
--size-selector: 0.25rem;
|
|
52
|
+
--size-field: 0.25rem;
|
|
53
|
+
--border: 1px;
|
|
54
|
+
--depth: 1;
|
|
55
|
+
--noise: 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@plugin "daisyui/theme" {
|
|
59
|
+
name: 'dark';
|
|
60
|
+
prefersdark: true;
|
|
61
|
+
color-scheme: dark;
|
|
62
|
+
/* Surfaces: the same ramp inverted — not a different ramp. */
|
|
63
|
+
--color-base-100: oklch(18% 0 0);
|
|
64
|
+
--color-base-200: oklch(22% 0 0);
|
|
65
|
+
--color-base-300: oklch(28% 0 0);
|
|
66
|
+
--color-base-content: oklch(95% 0 0);
|
|
67
|
+
/* Brand: the light theme's hue, stepped for a dark surface rather than
|
|
68
|
+
flipped — a colour that clears contrast on white rarely clears it on
|
|
69
|
+
near-black. */
|
|
70
|
+
--color-primary: oklch(72% 0.04 260);
|
|
71
|
+
--color-primary-content: oklch(20% 0.01 260);
|
|
72
|
+
--color-secondary: oklch(66% 0.03 260);
|
|
73
|
+
--color-secondary-content: oklch(20% 0.01 260);
|
|
74
|
+
--color-accent: oklch(70% 0.03 260);
|
|
75
|
+
--color-accent-content: oklch(20% 0.01 260);
|
|
76
|
+
--color-neutral: oklch(26% 0 0);
|
|
77
|
+
--color-neutral-content: oklch(92% 0 0);
|
|
78
|
+
--color-info: oklch(70.7% 0.165 254.624);
|
|
79
|
+
--color-info-content: oklch(20% 0.05 255);
|
|
80
|
+
--color-success: oklch(76.5% 0.177 163.223);
|
|
81
|
+
--color-success-content: oklch(20% 0.05 163);
|
|
82
|
+
--color-warning: oklch(82.8% 0.189 84.429);
|
|
83
|
+
--color-warning-content: oklch(25% 0.07 84);
|
|
84
|
+
--color-error: oklch(70.4% 0.191 22.216);
|
|
85
|
+
--color-error-content: oklch(20% 0.05 22);
|
|
86
|
+
--radius-selector: 1rem;
|
|
87
|
+
--radius-field: 0.375rem;
|
|
88
|
+
--radius-box: 0.75rem;
|
|
89
|
+
--size-selector: 0.25rem;
|
|
90
|
+
--size-field: 0.25rem;
|
|
91
|
+
--border: 1px;
|
|
92
|
+
--depth: 1;
|
|
93
|
+
--noise: 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* ────────────────────────────────────────────────────────────────────────────
|
|
97
|
+
* Categorical chart slots — appended block, owned by `charts/`.
|
|
98
|
+
*
|
|
99
|
+
* The one part of the palette a component reads as a raw custom property
|
|
100
|
+
* rather than through a Tailwind class, because a series colour is chosen by
|
|
101
|
+
* INDEX at runtime and Tailwind only emits classes it can see spelled out.
|
|
102
|
+
*
|
|
103
|
+
* Four slots, and **validated rather than chosen by eye**: the figures below
|
|
104
|
+
* were measured against the two themes above (`--color-base-100` is
|
|
105
|
+
* `oklch(100% 0 0)` → #ffffff in light, `oklch(18% 0 0)` → #121212 in dark),
|
|
106
|
+
* with dichromacy simulated by the Machado (2009) severity-1.0 matrices on
|
|
107
|
+
* linear sRGB and separation as Euclidean ΔE in OKLab ×100.
|
|
108
|
+
*
|
|
109
|
+
* light — #008236 #4f39f6 #e17100 #155dfc
|
|
110
|
+
* contrast on the surface 4.94 / 6.44 / 3.19 / 5.26 : 1
|
|
111
|
+
* worst ADJACENT pair: normal ΔE 37.6, simulated-dichromat ΔE 8.3
|
|
112
|
+
* dark — #00a63e #615fff #e17100 #155dfc
|
|
113
|
+
* contrast on the surface 5.85 / 4.11 / 5.89 / 3.58 : 1
|
|
114
|
+
* worst ADJACENT pair: normal ΔE 38.6, simulated-dichromat ΔE 9.8
|
|
115
|
+
*
|
|
116
|
+
* Every slot clears 3:1 against its own surface; every adjacent pair clears the
|
|
117
|
+
* ΔE ≥ 15 normal-vision floor and the ΔE ≥ 8 dichromat floor in both modes.
|
|
118
|
+
* The dark set is STEPPED for the dark surface, not flipped: slots 1 and 2 move
|
|
119
|
+
* one step lighter, slots 3 and 4 already sit inside both modes' bands.
|
|
120
|
+
*
|
|
121
|
+
* Three rules the slots exist to make keepable:
|
|
122
|
+
*
|
|
123
|
+
* - **Order is the safety mechanism.** The gates above are measured on ADJACENT
|
|
124
|
+
* pairs, which is what a line, bar or stack puts side by side. Re-ordering
|
|
125
|
+
* the slots invalidates the measurement — slots 2 and 4 (indigo and blue) are
|
|
126
|
+
* ΔE 2.7 apart in light and 0.4 in dark under simulated dichromacy, so a
|
|
127
|
+
* chart that puts them side by side has no colour difference for that reader
|
|
128
|
+
* at all.
|
|
129
|
+
* - **Four is the cap, and three is the cap for a form that compares EVERY
|
|
130
|
+
* pair at once** (a scatter, a choropleth, a pie). A fifth series is a design
|
|
131
|
+
* problem — fold it into an "other" series, or facet — never a generated hue.
|
|
132
|
+
* `Chart` draws a fifth in `--color-neutral` and names it in the legend
|
|
133
|
+
* rather than inventing one. Even at three, the 1↔3 pair falls to ΔE 4.9
|
|
134
|
+
* under protanopia, which is the reason `Chart`'s `sr-only` data table is
|
|
135
|
+
* always in the DOM: colour is the fast path, never the only one.
|
|
136
|
+
* - **Status colours are not series colours.** `--color-success` / `-warning` /
|
|
137
|
+
* `-error` mean a STATE, and slot 3 is deliberately amber-600 rather than the
|
|
138
|
+
* amber-500 that IS `--color-warning`, so a series can never impersonate one.
|
|
139
|
+
*
|
|
140
|
+
* A consumer replacing the two themes above should replace these four with
|
|
141
|
+
* their own measured set, in the same order.
|
|
142
|
+
* ──────────────────────────────────────────────────────────────────────────── */
|
|
143
|
+
:root {
|
|
144
|
+
/*
|
|
145
|
+
* One declaration per slot, not one per mode: `light-dark()` resolves against
|
|
146
|
+
* the `color-scheme` each daisyUI theme already sets — `light` on the `light`
|
|
147
|
+
* theme, `dark` on the `dark` one, and on whichever the OS asks for by
|
|
148
|
+
* default. Writing the dark set out a second time under `[data-theme]` and a
|
|
149
|
+
* third under `prefers-color-scheme` is how two of the three drift apart.
|
|
150
|
+
*/
|
|
151
|
+
--color-chart-1: light-dark(
|
|
152
|
+
oklch(52.7% 0.154 150.069),
|
|
153
|
+
oklch(62.7% 0.194 149.214)
|
|
154
|
+
); /* green-700 / -600 */
|
|
155
|
+
--color-chart-2: light-dark(
|
|
156
|
+
oklch(51.1% 0.262 276.966),
|
|
157
|
+
oklch(58.5% 0.233 277.117)
|
|
158
|
+
); /* indigo-600 / -500 */
|
|
159
|
+
--color-chart-3: oklch(66.6% 0.179 58.318); /* amber-600, inside both bands */
|
|
160
|
+
--color-chart-4: oklch(54.6% 0.245 262.881); /* blue-600, inside both bands */
|
|
161
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<!-- @component
|
|
2
|
+
A `Tip` anchored beside the trigger it is about, which renders inside it.
|
|
3
|
+
-->
|
|
4
|
+
<!--
|
|
5
|
+
PopoverTip — a `Tip` anchored to the thing it is talking about: the
|
|
6
|
+
caller's own trigger renders inside, and the bubble floats beside it.
|
|
7
|
+
|
|
8
|
+
Built on bits-ui's `Popover` rather than the hand-measured `position: fixed`
|
|
9
|
+
span the old copy used (a layout read per scroll frame, page-wide, for a
|
|
10
|
+
bubble that never moves relative to what it points at) — bits-ui's
|
|
11
|
+
`Popover.Content` computes and keeps that position via Floating UI, and
|
|
12
|
+
handles focus containment and outside-click dismissal for free.
|
|
13
|
+
|
|
14
|
+
`open` is NOT bound: it is a pure function of the tip's own state
|
|
15
|
+
(`available` and not yet dismissed/completed), read back from the store. A
|
|
16
|
+
tip shows because the store's rules say it may, never because the reader
|
|
17
|
+
clicked the trigger — so the trigger element has its bits-ui click/keydown
|
|
18
|
+
toggle handlers stripped (the `onclick`/`onkeydown` overrides below); it
|
|
19
|
+
keeps only the id/ref wiring `Popover.Content` needs to float against it.
|
|
20
|
+
Otherwise a click on an ordinary anchor button could pop open an EMPTY
|
|
21
|
+
bubble for a tip the store has not decided to show yet (`Tip`'s own
|
|
22
|
+
`{#if visible}` gate would render nothing inside).
|
|
23
|
+
|
|
24
|
+
Escape closing is arbitrated by the shared `TipStack`, not by bits-ui's own
|
|
25
|
+
(unrelated) escape-layer stacking order: `onEscapeKeydown` blocks the
|
|
26
|
+
default close unless this tip is the page's topmost, so two tips opened
|
|
27
|
+
from two different `PopoverTip`s still resolve "which one does Escape
|
|
28
|
+
close" the same way two `Tip`s do. Whatever closes the popover — Escape
|
|
29
|
+
when topmost, or a click outside it — is treated as a dismissal and fed
|
|
30
|
+
back into the store via `onOpenChange`, so the store and the floating
|
|
31
|
+
layer never disagree about whether the tip is showing.
|
|
32
|
+
|
|
33
|
+
`Popover.Content` is `forceMount`ed: it stays in the DOM whether or not
|
|
34
|
+
`open` is true, with bits-ui only toggling its floating position/listeners.
|
|
35
|
+
That is what lets `Tip` — mounted unconditionally inside it — run its own
|
|
36
|
+
`shouldDisplay`/`markShown` effect and flip the store to `available` in the
|
|
37
|
+
first place. Without `forceMount`, `Content` would itself wait for `open`
|
|
38
|
+
before rendering `Tip` at all, and `open` here is derived FROM what `Tip`
|
|
39
|
+
decides — a deadlock neither side could break.
|
|
40
|
+
-->
|
|
41
|
+
<script lang="ts">import { Popover } from "bits-ui";
|
|
42
|
+
import Tip from "./Tip.svelte";
|
|
43
|
+
import { soloTipStack } from "./tip-stack.js";
|
|
44
|
+
let { config, store, stack = soloTipStack, placement = "top", children, dismissLabel = "Dismiss", class: className } = $props();
|
|
45
|
+
const placementSide = {
|
|
46
|
+
top: "top",
|
|
47
|
+
bottom: "bottom",
|
|
48
|
+
left: "left",
|
|
49
|
+
right: "right"
|
|
50
|
+
};
|
|
51
|
+
/** True exactly when `Tip` would render itself, read back from the store. */
|
|
52
|
+
const open = $derived(store.getState(config.id).status === "available");
|
|
53
|
+
const handleOpenChange = (next) => {
|
|
54
|
+
if (!next) store.dismiss(config.id);
|
|
55
|
+
};
|
|
56
|
+
const handleEscapeKeydown = (event) => {
|
|
57
|
+
if (!stack.isTopmost(config.id)) event.preventDefault();
|
|
58
|
+
};
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
<Popover.Root {open} onOpenChange={handleOpenChange}>
|
|
62
|
+
<Popover.Trigger>
|
|
63
|
+
{#snippet child({ props })}
|
|
64
|
+
<span {...props} onclick={undefined} onkeydown={undefined} class="contents">
|
|
65
|
+
{@render children()}
|
|
66
|
+
</span>
|
|
67
|
+
{/snippet}
|
|
68
|
+
</Popover.Trigger>
|
|
69
|
+
<Popover.Portal>
|
|
70
|
+
<Popover.Content
|
|
71
|
+
forceMount
|
|
72
|
+
side={placementSide[placement]}
|
|
73
|
+
sideOffset={8}
|
|
74
|
+
onEscapeKeydown={handleEscapeKeydown}
|
|
75
|
+
class={['z-50 w-64', className]}
|
|
76
|
+
>
|
|
77
|
+
<Tip {config} {store} {stack} {dismissLabel} />
|
|
78
|
+
</Popover.Content>
|
|
79
|
+
</Popover.Portal>
|
|
80
|
+
</Popover.Root>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import { type TipStack } from './tip-stack.ts';
|
|
3
|
+
import type { TipStore } from './tip-store.svelte.ts';
|
|
4
|
+
import type { TipConfig, TipPlacement } from './types.ts';
|
|
5
|
+
interface Props {
|
|
6
|
+
/** The tip’s id, copy and actions. */
|
|
7
|
+
config: TipConfig;
|
|
8
|
+
/** The store that decides whether this shows and remembers that it did. */
|
|
9
|
+
store: TipStore;
|
|
10
|
+
/**
|
|
11
|
+
* Shared with the page’s other tips so Escape closes only the top one.
|
|
12
|
+
* @defaultValue `soloTipStack` (shares Escape with no other tip)
|
|
13
|
+
*/
|
|
14
|
+
stack?: TipStack | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Which side of the trigger the bubble sits on.
|
|
17
|
+
* @defaultValue `'top'`
|
|
18
|
+
*/
|
|
19
|
+
placement?: TipPlacement | undefined;
|
|
20
|
+
/** The trigger — a button, a field, whatever the tip is about. */
|
|
21
|
+
children: Snippet<[]>;
|
|
22
|
+
/**
|
|
23
|
+
* Accessible name of the tip's dismiss button.
|
|
24
|
+
* @defaultValue `'Dismiss'`
|
|
25
|
+
*/
|
|
26
|
+
dismissLabel?: string | undefined;
|
|
27
|
+
/** Extra classes, merged last. */
|
|
28
|
+
class?: string | undefined;
|
|
29
|
+
}
|
|
30
|
+
/** A `Tip` anchored beside the trigger it is about, which renders inside it. */
|
|
31
|
+
declare const PopoverTip: import("svelte").Component<Props, {}, "">;
|
|
32
|
+
type PopoverTip = ReturnType<typeof PopoverTip>;
|
|
33
|
+
export default PopoverTip;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<!-- @component
|
|
2
|
+
One piece of inline guidance that shows only when its store allows and closes for good once dismissed or acted on.
|
|
3
|
+
-->
|
|
4
|
+
<!--
|
|
5
|
+
Tip — one piece of inline guidance: a title, an optional sentence, and the
|
|
6
|
+
actions it offers. Shows itself only if the store says it may, records that
|
|
7
|
+
it did, and closes for good when the reader dismisses it or takes an
|
|
8
|
+
action.
|
|
9
|
+
|
|
10
|
+
A daisyUI `alert` rather than a hand-rolled card — no hard-coded colours, no
|
|
11
|
+
hand-drawn arrow (that belongs to `PopoverTip`, where a tip is actually
|
|
12
|
+
anchored to something an arrow can point at; an inline tip sits in the flow
|
|
13
|
+
and has nothing to point at).
|
|
14
|
+
|
|
15
|
+
`role="status"` with `aria-live="polite"`: a tip is an offer, not an
|
|
16
|
+
interruption, so it is announced at the next pause rather than immediately.
|
|
17
|
+
|
|
18
|
+
The Escape-to-dismiss listener is hand-rolled rather than left to bits-ui:
|
|
19
|
+
this component has no bits-ui primitive backing it (it is a plain `alert`,
|
|
20
|
+
not a dialog or popover), and the key has to be scoped to "only the topmost
|
|
21
|
+
tip on the page" via the shared `TipStack` — a concern bits-ui's own
|
|
22
|
+
Escape handling knows nothing about. `PopoverTip` wires the same stack into
|
|
23
|
+
its bits-ui `Popover` instead, once there is a floating layer to hand it to.
|
|
24
|
+
-->
|
|
25
|
+
<script lang="ts">import { soloTipStack } from "./tip-stack.js";
|
|
26
|
+
let { config, store, stack = soloTipStack, dismissLabel = "Dismiss", class: className } = $props();
|
|
27
|
+
let visible = $state(false);
|
|
28
|
+
// The decision is made on mount, not during render: `markShown` writes, and a
|
|
29
|
+
// write inside a derivation would run again on every re-render.
|
|
30
|
+
$effect(() => {
|
|
31
|
+
if (!store.shouldDisplay(config.id)) return;
|
|
32
|
+
visible = true;
|
|
33
|
+
store.markShown(config.id);
|
|
34
|
+
});
|
|
35
|
+
$effect(() => {
|
|
36
|
+
if (!visible) return;
|
|
37
|
+
stack.push(config.id);
|
|
38
|
+
const onKey = (event) => {
|
|
39
|
+
if (event.key !== "Escape" || !stack.isTopmost(config.id)) return;
|
|
40
|
+
event.preventDefault();
|
|
41
|
+
dismiss();
|
|
42
|
+
};
|
|
43
|
+
globalThis.addEventListener("keydown", onKey);
|
|
44
|
+
return () => {
|
|
45
|
+
stack.pop(config.id);
|
|
46
|
+
globalThis.removeEventListener("keydown", onKey);
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
const dismiss = () => {
|
|
50
|
+
store.dismiss(config.id);
|
|
51
|
+
visible = false;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Taking an action COMPLETES the tip rather than dismissing it. The two are
|
|
55
|
+
* different facts — "they did the thing" and "they made it go away" — and
|
|
56
|
+
* the store keeps them apart so a later "show me what I skipped" can tell.
|
|
57
|
+
*/
|
|
58
|
+
const act = async (action) => {
|
|
59
|
+
await action.handler();
|
|
60
|
+
store.complete(config.id);
|
|
61
|
+
visible = false;
|
|
62
|
+
};
|
|
63
|
+
</script>
|
|
64
|
+
|
|
65
|
+
{#if visible}
|
|
66
|
+
<div class={['alert alert-info alert-soft items-start', className]} role="status" aria-live="polite">
|
|
67
|
+
<div class="flex-1 space-y-2">
|
|
68
|
+
<p class="font-semibold">{config.title}</p>
|
|
69
|
+
{#if config.message !== undefined && config.message !== ''}
|
|
70
|
+
<p class="text-sm">{config.message}</p>
|
|
71
|
+
{/if}
|
|
72
|
+
{#if config.actions !== undefined && config.actions.length > 0}
|
|
73
|
+
<div class="flex flex-wrap gap-2">
|
|
74
|
+
{#each config.actions as action (action.id)}
|
|
75
|
+
<button type="button" class="btn btn-sm" onclick={() => act(action)}>
|
|
76
|
+
{action.title}
|
|
77
|
+
</button>
|
|
78
|
+
{/each}
|
|
79
|
+
</div>
|
|
80
|
+
{/if}
|
|
81
|
+
</div>
|
|
82
|
+
<button
|
|
83
|
+
type="button"
|
|
84
|
+
class="btn btn-ghost btn-sm btn-circle"
|
|
85
|
+
onclick={dismiss}
|
|
86
|
+
aria-label={dismissLabel}
|
|
87
|
+
>
|
|
88
|
+
✕
|
|
89
|
+
</button>
|
|
90
|
+
</div>
|
|
91
|
+
{/if}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type TipStack } from './tip-stack.ts';
|
|
2
|
+
import type { TipStore } from './tip-store.svelte.ts';
|
|
3
|
+
import type { TipConfig } from './types.ts';
|
|
4
|
+
interface Props {
|
|
5
|
+
/** The tip’s id, copy and actions. */
|
|
6
|
+
config: TipConfig;
|
|
7
|
+
/** The store that decides whether this shows and remembers that it did. */
|
|
8
|
+
store: TipStore;
|
|
9
|
+
/**
|
|
10
|
+
* Shared with the page's other tips so Escape closes only the top one.
|
|
11
|
+
* @defaultValue `soloTipStack` (shares Escape with no other tip)
|
|
12
|
+
*/
|
|
13
|
+
stack?: TipStack | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Accessible name of the dismiss button.
|
|
16
|
+
* @defaultValue `'Dismiss'`
|
|
17
|
+
*/
|
|
18
|
+
dismissLabel?: string | undefined;
|
|
19
|
+
/** Extra classes, merged last. */
|
|
20
|
+
class?: string | undefined;
|
|
21
|
+
}
|
|
22
|
+
/** One piece of inline guidance that shows only when its store allows and closes for good once dismissed or acted on. */
|
|
23
|
+
declare const Tip: import("svelte").Component<Props, {}, "">;
|
|
24
|
+
type Tip = ReturnType<typeof Tip>;
|
|
25
|
+
export default Tip;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@thomasfosterau/components/tips` — a separate entry point so it never sits on
|
|
3
|
+
* the always-loaded path of a consumer that does not use it.
|
|
4
|
+
*
|
|
5
|
+
* TipKit — contextual, dismissible guidance: an inline tip, a tip anchored to
|
|
6
|
+
* its trigger, the store that decides when either may show, and the stack
|
|
7
|
+
* that decides which one Escape closes.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
export { default as PopoverTip } from './PopoverTip.svelte';
|
|
12
|
+
export { default as Tip } from './Tip.svelte';
|
|
13
|
+
export { createTipStack, soloTipStack, type TipStack } from './tip-stack.ts';
|
|
14
|
+
export { browserTipStorage, createTipStore, memoryTipStorage, type TipStorage, type TipStoreOptions, type TipStore, } from './tip-store.svelte.ts';
|
|
15
|
+
export type { TipAction, TipConfig, TipFrequency, TipPlacement, TipRule, TipState, TipStatus, } from './types.ts';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@thomasfosterau/components/tips` — a separate entry point so it never sits on
|
|
3
|
+
* the always-loaded path of a consumer that does not use it.
|
|
4
|
+
*
|
|
5
|
+
* TipKit — contextual, dismissible guidance: an inline tip, a tip anchored to
|
|
6
|
+
* its trigger, the store that decides when either may show, and the stack
|
|
7
|
+
* that decides which one Escape closes.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
export { default as PopoverTip } from './PopoverTip.svelte';
|
|
12
|
+
export { default as Tip } from './Tip.svelte';
|
|
13
|
+
export { createTipStack, soloTipStack } from "./tip-stack.js";
|
|
14
|
+
export { browserTipStorage, createTipStore, memoryTipStorage, } from "./tip-store.svelte.js";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tip stack: which tips are on screen, most recent last.
|
|
3
|
+
*
|
|
4
|
+
* It exists for one keystroke. Escape must close the tip the reader is looking
|
|
5
|
+
* at — the last one to appear — and not every tip on the page at once. Each
|
|
6
|
+
* visible tip asks the stack whether it is on top before acting on the key.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
export interface TipStack {
|
|
11
|
+
/** Mark a tip visible, moving it to the top if it was already there. */
|
|
12
|
+
push: (id: string) => void;
|
|
13
|
+
/** Mark a tip no longer visible. */
|
|
14
|
+
pop: (id: string) => void;
|
|
15
|
+
/** Whether this tip is the one Escape should close. */
|
|
16
|
+
isTopmost: (id: string) => boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A fresh stack. Create one per app (or per test) and hand it to the tips; a
|
|
20
|
+
* module-level singleton would make two mounted apps — or two tests — share a
|
|
21
|
+
* stack, which is exactly the bug this is meant to prevent.
|
|
22
|
+
*/
|
|
23
|
+
export declare const createTipStack: () => TipStack;
|
|
24
|
+
/** A stack that always says yes — for a tip mounted without one. */
|
|
25
|
+
export declare const soloTipStack: TipStack;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tip stack: which tips are on screen, most recent last.
|
|
3
|
+
*
|
|
4
|
+
* It exists for one keystroke. Escape must close the tip the reader is looking
|
|
5
|
+
* at — the last one to appear — and not every tip on the page at once. Each
|
|
6
|
+
* visible tip asks the stack whether it is on top before acting on the key.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* A fresh stack. Create one per app (or per test) and hand it to the tips; a
|
|
12
|
+
* module-level singleton would make two mounted apps — or two tests — share a
|
|
13
|
+
* stack, which is exactly the bug this is meant to prevent.
|
|
14
|
+
*/
|
|
15
|
+
export const createTipStack = () => {
|
|
16
|
+
const stack = [];
|
|
17
|
+
return {
|
|
18
|
+
push: (id) => {
|
|
19
|
+
const index = stack.indexOf(id);
|
|
20
|
+
if (index !== -1)
|
|
21
|
+
stack.splice(index, 1);
|
|
22
|
+
stack.push(id);
|
|
23
|
+
},
|
|
24
|
+
pop: (id) => {
|
|
25
|
+
const index = stack.indexOf(id);
|
|
26
|
+
if (index !== -1)
|
|
27
|
+
stack.splice(index, 1);
|
|
28
|
+
},
|
|
29
|
+
isTopmost: (id) => stack.length > 0 && stack[stack.length - 1] === id,
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
/** A stack that always says yes — for a tip mounted without one. */
|
|
33
|
+
export const soloTipStack = {
|
|
34
|
+
push: () => { },
|
|
35
|
+
pop: () => { },
|
|
36
|
+
isTopmost: () => true,
|
|
37
|
+
};
|