@k2b/ui 0.12.0 → 0.13.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/README.md +1 -1
- package/dist/browser/content/PdfPreview.js +5 -2
- package/dist/browser/content/PdfPreview.js.map +1 -1
- package/dist/browser/feedback/InstallGuide.js +267 -0
- package/dist/browser/feedback/InstallGuide.js.map +1 -0
- package/dist/browser/feedback/announce.js +50 -0
- package/dist/browser/feedback/announce.js.map +1 -0
- package/dist/browser/feedback/dialog-core.js +82 -1
- package/dist/browser/feedback/dialog-core.js.map +1 -1
- package/dist/browser/feedback/index.js +4 -0
- package/dist/browser/feedback/index.js.map +1 -1
- package/dist/browser/feedback/install.js +99 -0
- package/dist/browser/feedback/install.js.map +1 -0
- package/dist/browser/feedback/prompts.js +11 -1
- package/dist/browser/feedback/prompts.js.map +1 -1
- package/dist/browser/feedback/theme-color.js +15 -0
- package/dist/browser/feedback/theme-color.js.map +1 -0
- package/dist/browser/feedback/toast.js +10 -44
- package/dist/browser/feedback/toast.js.map +1 -1
- package/dist/browser/inputs/QrScanner.js +164 -0
- package/dist/browser/inputs/QrScanner.js.map +1 -0
- package/dist/browser/inputs/index.js +1 -0
- package/dist/browser/inputs/index.js.map +1 -1
- package/dist/browser/intl/messages.js +86 -2
- package/dist/browser/intl/messages.js.map +1 -1
- package/dist/browser/layout/Discussion.js +2 -1
- package/dist/browser/layout/Discussion.js.map +1 -1
- package/dist/browser/layout/MobileShell.js +242 -0
- package/dist/browser/layout/MobileShell.js.map +1 -0
- package/dist/browser/layout/TabBar.js +65 -0
- package/dist/browser/layout/TabBar.js.map +1 -0
- package/dist/browser/layout/index.js +2 -0
- package/dist/browser/layout/index.js.map +1 -1
- package/dist/browser/surfaces/NoticeCard.js +55 -60
- package/dist/browser/surfaces/NoticeCard.js.map +1 -1
- package/dist/browser/surfaces/index.js +1 -1
- package/dist/browser/surfaces/index.js.map +1 -1
- package/dist/ssr/content/PdfPreview.js +4 -2
- package/dist/ssr/content/PdfPreview.js.map +1 -1
- package/dist/ssr/feedback/InstallGuide.js +201 -0
- package/dist/ssr/feedback/InstallGuide.js.map +1 -0
- package/dist/ssr/feedback/announce.js +50 -0
- package/dist/ssr/feedback/announce.js.map +1 -0
- package/dist/ssr/feedback/dialog-core.js +82 -1
- package/dist/ssr/feedback/dialog-core.js.map +1 -1
- package/dist/ssr/feedback/index.js +4 -0
- package/dist/ssr/feedback/index.js.map +1 -1
- package/dist/ssr/feedback/install.js +99 -0
- package/dist/ssr/feedback/install.js.map +1 -0
- package/dist/ssr/feedback/prompts.js +11 -1
- package/dist/ssr/feedback/prompts.js.map +1 -1
- package/dist/ssr/feedback/theme-color.js +15 -0
- package/dist/ssr/feedback/theme-color.js.map +1 -0
- package/dist/ssr/feedback/toast.js +10 -44
- package/dist/ssr/feedback/toast.js.map +1 -1
- package/dist/ssr/inputs/QrScanner.js +128 -0
- package/dist/ssr/inputs/QrScanner.js.map +1 -0
- package/dist/ssr/inputs/index.js +1 -0
- package/dist/ssr/inputs/index.js.map +1 -1
- package/dist/ssr/intl/messages.js +86 -2
- package/dist/ssr/intl/messages.js.map +1 -1
- package/dist/ssr/layout/Discussion.js +2 -1
- package/dist/ssr/layout/Discussion.js.map +1 -1
- package/dist/ssr/layout/MobileShell.js +211 -0
- package/dist/ssr/layout/MobileShell.js.map +1 -0
- package/dist/ssr/layout/TabBar.js +24 -0
- package/dist/ssr/layout/TabBar.js.map +1 -0
- package/dist/ssr/layout/index.js +2 -0
- package/dist/ssr/layout/index.js.map +1 -1
- package/dist/ssr/surfaces/NoticeCard.js +21 -30
- package/dist/ssr/surfaces/NoticeCard.js.map +1 -1
- package/dist/ssr/surfaces/index.js +1 -1
- package/dist/ssr/surfaces/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/tabler.css +1 -1
- package/dist/types/feedback/InstallGuide.d.ts +26 -0
- package/dist/types/feedback/announce.d.ts +19 -0
- package/dist/types/feedback/dialog-core.d.ts +5 -0
- package/dist/types/feedback/index.d.ts +7 -0
- package/dist/types/feedback/install.d.ts +32 -0
- package/dist/types/feedback/prompts.d.ts +2 -0
- package/dist/types/feedback/theme-color.d.ts +6 -0
- package/dist/types/inputs/QrScanner.d.ts +20 -0
- package/dist/types/inputs/index.d.ts +2 -0
- package/dist/types/intl/messages.d.ts +42 -0
- package/dist/types/layout/MobileShell.d.ts +40 -0
- package/dist/types/layout/TabBar.d.ts +23 -0
- package/dist/types/layout/index.d.ts +4 -0
- package/dist/types/surfaces/NoticeCard.d.ts +4 -4
- package/dist/types/surfaces/index.d.ts +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -61,7 +61,7 @@ See the [TextInput guide](https://cloud.k2b.dev/en/ui/input/text) for details.
|
|
|
61
61
|
| Inputs | Text fields, selectors, date pickers, file inputs |
|
|
62
62
|
| Layout | Panels, settings, split panes, floating windows |
|
|
63
63
|
| Surfaces | Cards, avatars, badges, empty states |
|
|
64
|
-
| Feedback | Dialogs, prompts, tooltips, toasts |
|
|
64
|
+
| Feedback | Dialogs, prompts, tooltips, toasts, screen-reader announcements |
|
|
65
65
|
| Content | Tables, charts, calendars, Markdown, file previews |
|
|
66
66
|
| Chat | Timeline, messages, composer, context usage |
|
|
67
67
|
| Widgets | Dashboard summaries, lists, statistics |
|
|
@@ -47,6 +47,7 @@ export default function PdfPreview(props) {
|
|
|
47
47
|
// An automatic preview shows one fixed document; the open action reuses it instead of requesting it again.
|
|
48
48
|
let shownBlob = null;
|
|
49
49
|
let openButton;
|
|
50
|
+
let frame;
|
|
50
51
|
let renderButton;
|
|
51
52
|
let errorRetryButton;
|
|
52
53
|
let disposed = false;
|
|
@@ -90,8 +91,9 @@ export default function PdfPreview(props) {
|
|
|
90
91
|
setUrl(nextUrl);
|
|
91
92
|
setLoading(false);
|
|
92
93
|
if (previousUrl) URL.revokeObjectURL(previousUrl);
|
|
93
|
-
// The shown document removes the retry action; keep keyboard focus in the actions instead of the page
|
|
94
|
-
|
|
94
|
+
// The shown document removes the retry action; keep keyboard focus in the actions instead of the page, or on the
|
|
95
|
+
// document itself when the host shows only the content.
|
|
96
|
+
if (retryFocused && document.activeElement === document.body) (openButton?.isConnected ? openButton : frame)?.focus();
|
|
95
97
|
} catch (e) {
|
|
96
98
|
if (disposed || generation !== loadGeneration) return;
|
|
97
99
|
shownBlob = null;
|
|
@@ -272,6 +274,7 @@ export default function PdfPreview(props) {
|
|
|
272
274
|
},
|
|
273
275
|
children: currentUrl => (() => {
|
|
274
276
|
var _el$25 = _$getNextElement(_tmpl$6);
|
|
277
|
+
_$use(element => frame = element, _el$25);
|
|
275
278
|
_$effect(_p$ => {
|
|
276
279
|
var _v$5 = currentUrl(),
|
|
277
280
|
_v$6 = props.title ?? messages().pdfPreview;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":[],"sources":["content/PdfPreview.tsx"],"sourcesContent":["import { createSignal, type JSX, onCleanup, onMount, Show } from \"solid-js\";\nimport { resolveUiMessages, useUiMessages } from \"../intl/messages\";\nimport Placeholder from \"../surfaces/Placeholder\";\n\nexport type PdfPreviewRequest = () => Promise<Response | Blob>;\n\nexport type PdfPreviewProps = {\n request: PdfPreviewRequest;\n autoLoad?: boolean;\n disabled?: () => boolean;\n title?: string;\n buttonLabel?: string;\n openButtonLabel?: string;\n /**\n * Opens the document at this address in a new tab instead of a local copy. Pass a stable same-origin URL that\n * serves the PDF inline, so reloading and the viewer's file name keep working. Without it, the open action shows\n * a temporary copy of the requested document.\n */\n openHref?: string;\n /** Adds a Download action. The host owns the download, for example a fresh attachment URL. */\n onDownload?: () => void;\n emptyText?: string;\n class?: string;\n children?: (parts: { actions: JSX.Element; content: JSX.Element }) => JSX.Element;\n renderError?: (message: string) => JSX.Element;\n};\n\nconst readErrorMessage = async (response: Response): Promise<string> => {\n try {\n const data = (await response.json()) as unknown;\n if (data && typeof data === \"object\" && \"message\" in data && typeof data.message === \"string\") return data.message;\n } catch {\n // Fall through to the HTTP status fallback.\n }\n return resolveUiMessages().pdfPreviewHttpError({ status: response.status });\n};\n\nexport default function PdfPreview(props: PdfPreviewProps) {\n const messages = useUiMessages();\n const [url, setUrl] = createSignal<string | null>(null);\n // An automatic preview starts loading once mounted. The server and the page before hydration already show that\n // state, so they offer neither a retry nor an invitation to render while nothing has failed.\n const [loading, setLoading] = createSignal(Boolean(props.autoLoad) && !props.disabled?.());\n const [opening, setOpening] = createSignal(false);\n const [error, setError] = createSignal<string | null>(null);\n const [failed, setFailed] = createSignal(false);\n const [tabBlocked, setTabBlocked] = createSignal(false);\n // An automatic preview shows one fixed document; the open action reuses it instead of requesting it again.\n let shownBlob: Blob | null = null;\n let openButton: HTMLElement | undefined;\n let renderButton: HTMLButtonElement | undefined;\n let errorRetryButton: HTMLButtonElement | undefined;\n let disposed = false;\n let loadGeneration = 0;\n let openGeneration = 0;\n\n const revokeCurrent = () => {\n const current = url();\n if (current) URL.revokeObjectURL(current);\n setUrl(null);\n };\n\n onCleanup(() => {\n disposed = true;\n loadGeneration += 1;\n openGeneration += 1;\n revokeCurrent();\n });\n\n const readPdfBlob = async () => {\n const response = await props.request();\n const blob = response instanceof Response ? (response.ok ? await response.blob() : null) : response;\n if (!blob) throw new Error(await readErrorMessage(response as Response));\n if (blob.type && blob.type !== \"application/pdf\") throw new Error(`PDF preview returned ${blob.type} instead of application/pdf`);\n return blob;\n };\n\n const load = async () => {\n if (loading() || opening() || props.disabled?.()) return;\n const generation = ++loadGeneration;\n // A retry the shown document or the loading state removes hands keyboard focus on instead of dropping it.\n const retryFocused =\n (props.autoLoad && document.activeElement === renderButton) ||\n (errorRetryButton?.isConnected && document.activeElement === errorRetryButton);\n setLoading(true);\n setError(null);\n try {\n const blob = await readPdfBlob();\n const nextUrl = URL.createObjectURL(blob);\n if (disposed || generation !== loadGeneration) {\n URL.revokeObjectURL(nextUrl);\n return;\n }\n const previousUrl = url();\n shownBlob = blob;\n setFailed(false);\n setUrl(nextUrl);\n setLoading(false);\n if (previousUrl) URL.revokeObjectURL(previousUrl);\n // The shown document removes the retry action; keep keyboard focus in the actions instead of the page.\n if (retryFocused && document.activeElement === document.body) openButton?.focus();\n } catch (e) {\n if (disposed || generation !== loadGeneration) return;\n shownBlob = null;\n setFailed(true);\n setError(e instanceof Error ? e.message : \"PDF preview failed\");\n if (retryFocused && document.activeElement === document.body) errorRetryButton?.focus();\n } finally {\n if (!disposed && generation === loadGeneration) setLoading(false);\n }\n };\n\n onMount(() => {\n // Hand the initial loading state to the request itself, which also covers a preview disabled by now.\n setLoading(false);\n if (props.autoLoad) void load();\n });\n\n const openInNewTab = async () => {\n if (loading() || opening() || props.disabled?.()) return;\n const generation = ++openGeneration;\n const tab = window.open(\"\", \"_blank\");\n // A blocked tab is not a document error: keep the shown document and report it beside the actions.\n setTabBlocked(!tab);\n if (!tab) return;\n tab.opener = null;\n tab.document.title = props.title ?? messages().pdfPreview;\n tab.document.body.textContent = messages().pdfPreviewRendering;\n setOpening(true);\n setError(null);\n try {\n const blob = (props.autoLoad ? shownBlob : null) ?? (await readPdfBlob());\n if (disposed || generation !== openGeneration) {\n tab.close();\n return;\n }\n const objectUrl = URL.createObjectURL(blob);\n tab.location.href = objectUrl;\n window.setTimeout(() => URL.revokeObjectURL(objectUrl), 60_000);\n } catch (e) {\n tab.close();\n if (!disposed && generation === openGeneration) setError(e instanceof Error ? e.message : \"PDF preview failed\");\n } finally {\n if (!disposed && generation === openGeneration) setOpening(false);\n }\n };\n\n // With autoLoad, rendering again would show the same document: offer it only until one is shown. After a failure the\n // default error state carries it as the retry; beside a caller's error content it stays in place while a retry loads.\n const renderable = () => !props.autoLoad || (failed() ? Boolean(props.renderError) : !url() && !loading());\n const actions = () => (\n <div class=\"k2b-content-pdf-preview__actions\">\n <Show\n when={!props.disabled?.() && props.openHref}\n fallback={\n <button\n ref={(element) => (openButton = element)}\n type=\"button\"\n class=\"k2b-button\"\n data-variant=\"secondary\"\n data-size=\"sm\"\n onClick={() => void openInNewTab()}\n disabled={loading() || opening() || props.disabled?.()}\n >\n <i class={opening() ? \"ti ti-loader-2 k2b-spin\" : \"ti ti-external-link\"} aria-hidden=\"true\" />\n {props.openButtonLabel ?? messages().openPreview}\n </button>\n }\n >\n {(href) => (\n // A plain link to a real address: the browser opens it on the user's own click, and the tab can reload it.\n <a\n ref={(element) => (openButton = element)}\n class=\"k2b-button\"\n data-variant=\"secondary\"\n data-size=\"sm\"\n href={href()}\n target=\"_blank\"\n rel=\"noopener\"\n >\n <i class=\"ti ti-external-link\" aria-hidden=\"true\" />\n {props.openButtonLabel ?? messages().openPreview}\n </a>\n )}\n </Show>\n <Show when={props.onDownload}>\n <button\n type=\"button\"\n class=\"k2b-button\"\n data-variant=\"secondary\"\n data-size=\"sm\"\n onClick={() => props.onDownload?.()}\n disabled={props.disabled?.()}\n >\n <i class=\"ti ti-download\" aria-hidden=\"true\" />\n {messages().download}\n </button>\n </Show>\n <Show when={renderable()}>\n <button\n ref={renderButton}\n type=\"button\"\n class=\"k2b-button\"\n data-variant=\"secondary\"\n data-size=\"sm\"\n onClick={() => void load()}\n disabled={loading() || opening() || props.disabled?.()}\n >\n <i class={loading() ? \"ti ti-loader-2 k2b-spin\" : \"ti ti-file-type-pdf\"} aria-hidden=\"true\" />\n {props.buttonLabel ?? messages().previewPdf}\n </button>\n </Show>\n <Show when={tabBlocked()}>\n <span role=\"alert\" class=\"k2b-content-pdf-preview__notice\">\n {messages().pdfPreviewTabBlocked}\n </span>\n </Show>\n </div>\n );\n // Every state before a document is shown uses one placeholder in the viewer's box, so loading changes no layout.\n const content = () => (\n <Show\n when={error()}\n fallback={\n <Show\n when={url()}\n fallback={\n <Placeholder\n class=\"k2b-content-pdf-preview__placeholder\"\n state={loading() ? \"loading\" : \"empty\"}\n icon={loading() ? undefined : \"ti ti-file-type-pdf\"}\n title={loading() ? messages().loading : undefined}\n description={loading() ? undefined : (props.emptyText ?? messages().renderPdfPreview)}\n />\n }\n >\n {(currentUrl) => (\n <iframe class=\"k2b-content-pdf-preview__frame\" src={currentUrl()} title={props.title ?? messages().pdfPreview} />\n )}\n </Show>\n }\n >\n {(message) =>\n props.renderError?.(message()) ?? (\n <Placeholder\n class=\"k2b-content-pdf-preview__placeholder\"\n state=\"error\"\n description={message()}\n action={\n // One retry at a time: the render action moves here only where the toolbar no longer offers it.\n renderable() ? undefined : (\n <button\n ref={(element) => (errorRetryButton = element)}\n type=\"button\"\n class=\"k2b-button\"\n data-variant=\"secondary\"\n data-size=\"sm\"\n onClick={() => void load()}\n disabled={opening() || props.disabled?.()}\n >\n {props.buttonLabel ?? messages().retry}\n </button>\n )\n }\n />\n )\n }\n </Show>\n );\n\n if (props.children)\n return props.children({\n get actions() {\n return actions();\n },\n get content() {\n return content();\n },\n });\n return (\n <section class={`k2b-content-pdf-preview ${props.class ?? \"\"}`}>\n <div class=\"k2b-content-pdf-preview__toolbar\">\n <div class=\"k2b-content-pdf-preview__heading\">\n <Show when={props.title}>\n <h2 class=\"k2b-content-pdf-preview__title\">{props.title}</h2>\n </Show>\n </div>\n {actions()}\n </div>\n {content()}\n </section>\n );\n}\n"],"mappings":"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","ignoreList":[]}
|
|
1
|
+
{"version":3,"names":[],"sources":["content/PdfPreview.tsx"],"sourcesContent":["import { createSignal, type JSX, onCleanup, onMount, Show } from \"solid-js\";\nimport { resolveUiMessages, useUiMessages } from \"../intl/messages\";\nimport Placeholder from \"../surfaces/Placeholder\";\n\nexport type PdfPreviewRequest = () => Promise<Response | Blob>;\n\nexport type PdfPreviewProps = {\n request: PdfPreviewRequest;\n autoLoad?: boolean;\n disabled?: () => boolean;\n title?: string;\n buttonLabel?: string;\n openButtonLabel?: string;\n /**\n * Opens the document at this address in a new tab instead of a local copy. Pass a stable same-origin URL that\n * serves the PDF inline, so reloading and the viewer's file name keep working. Without it, the open action shows\n * a temporary copy of the requested document.\n */\n openHref?: string;\n /** Adds a Download action. The host owns the download, for example a fresh attachment URL. */\n onDownload?: () => void;\n emptyText?: string;\n class?: string;\n children?: (parts: { actions: JSX.Element; content: JSX.Element }) => JSX.Element;\n renderError?: (message: string) => JSX.Element;\n};\n\nconst readErrorMessage = async (response: Response): Promise<string> => {\n try {\n const data = (await response.json()) as unknown;\n if (data && typeof data === \"object\" && \"message\" in data && typeof data.message === \"string\") return data.message;\n } catch {\n // Fall through to the HTTP status fallback.\n }\n return resolveUiMessages().pdfPreviewHttpError({ status: response.status });\n};\n\nexport default function PdfPreview(props: PdfPreviewProps) {\n const messages = useUiMessages();\n const [url, setUrl] = createSignal<string | null>(null);\n // An automatic preview starts loading once mounted. The server and the page before hydration already show that\n // state, so they offer neither a retry nor an invitation to render while nothing has failed.\n const [loading, setLoading] = createSignal(Boolean(props.autoLoad) && !props.disabled?.());\n const [opening, setOpening] = createSignal(false);\n const [error, setError] = createSignal<string | null>(null);\n const [failed, setFailed] = createSignal(false);\n const [tabBlocked, setTabBlocked] = createSignal(false);\n // An automatic preview shows one fixed document; the open action reuses it instead of requesting it again.\n let shownBlob: Blob | null = null;\n let openButton: HTMLElement | undefined;\n let frame: HTMLIFrameElement | undefined;\n let renderButton: HTMLButtonElement | undefined;\n let errorRetryButton: HTMLButtonElement | undefined;\n let disposed = false;\n let loadGeneration = 0;\n let openGeneration = 0;\n\n const revokeCurrent = () => {\n const current = url();\n if (current) URL.revokeObjectURL(current);\n setUrl(null);\n };\n\n onCleanup(() => {\n disposed = true;\n loadGeneration += 1;\n openGeneration += 1;\n revokeCurrent();\n });\n\n const readPdfBlob = async () => {\n const response = await props.request();\n const blob = response instanceof Response ? (response.ok ? await response.blob() : null) : response;\n if (!blob) throw new Error(await readErrorMessage(response as Response));\n if (blob.type && blob.type !== \"application/pdf\") throw new Error(`PDF preview returned ${blob.type} instead of application/pdf`);\n return blob;\n };\n\n const load = async () => {\n if (loading() || opening() || props.disabled?.()) return;\n const generation = ++loadGeneration;\n // A retry the shown document or the loading state removes hands keyboard focus on instead of dropping it.\n const retryFocused =\n (props.autoLoad && document.activeElement === renderButton) ||\n (errorRetryButton?.isConnected && document.activeElement === errorRetryButton);\n setLoading(true);\n setError(null);\n try {\n const blob = await readPdfBlob();\n const nextUrl = URL.createObjectURL(blob);\n if (disposed || generation !== loadGeneration) {\n URL.revokeObjectURL(nextUrl);\n return;\n }\n const previousUrl = url();\n shownBlob = blob;\n setFailed(false);\n setUrl(nextUrl);\n setLoading(false);\n if (previousUrl) URL.revokeObjectURL(previousUrl);\n // The shown document removes the retry action; keep keyboard focus in the actions instead of the page, or on the\n // document itself when the host shows only the content.\n if (retryFocused && document.activeElement === document.body) (openButton?.isConnected ? openButton : frame)?.focus();\n } catch (e) {\n if (disposed || generation !== loadGeneration) return;\n shownBlob = null;\n setFailed(true);\n setError(e instanceof Error ? e.message : \"PDF preview failed\");\n if (retryFocused && document.activeElement === document.body) errorRetryButton?.focus();\n } finally {\n if (!disposed && generation === loadGeneration) setLoading(false);\n }\n };\n\n onMount(() => {\n // Hand the initial loading state to the request itself, which also covers a preview disabled by now.\n setLoading(false);\n if (props.autoLoad) void load();\n });\n\n const openInNewTab = async () => {\n if (loading() || opening() || props.disabled?.()) return;\n const generation = ++openGeneration;\n const tab = window.open(\"\", \"_blank\");\n // A blocked tab is not a document error: keep the shown document and report it beside the actions.\n setTabBlocked(!tab);\n if (!tab) return;\n tab.opener = null;\n tab.document.title = props.title ?? messages().pdfPreview;\n tab.document.body.textContent = messages().pdfPreviewRendering;\n setOpening(true);\n setError(null);\n try {\n const blob = (props.autoLoad ? shownBlob : null) ?? (await readPdfBlob());\n if (disposed || generation !== openGeneration) {\n tab.close();\n return;\n }\n const objectUrl = URL.createObjectURL(blob);\n tab.location.href = objectUrl;\n window.setTimeout(() => URL.revokeObjectURL(objectUrl), 60_000);\n } catch (e) {\n tab.close();\n if (!disposed && generation === openGeneration) setError(e instanceof Error ? e.message : \"PDF preview failed\");\n } finally {\n if (!disposed && generation === openGeneration) setOpening(false);\n }\n };\n\n // With autoLoad, rendering again would show the same document: offer it only until one is shown. After a failure the\n // default error state carries it as the retry; beside a caller's error content it stays in place while a retry loads.\n const renderable = () => !props.autoLoad || (failed() ? Boolean(props.renderError) : !url() && !loading());\n const actions = () => (\n <div class=\"k2b-content-pdf-preview__actions\">\n <Show\n when={!props.disabled?.() && props.openHref}\n fallback={\n <button\n ref={(element) => (openButton = element)}\n type=\"button\"\n class=\"k2b-button\"\n data-variant=\"secondary\"\n data-size=\"sm\"\n onClick={() => void openInNewTab()}\n disabled={loading() || opening() || props.disabled?.()}\n >\n <i class={opening() ? \"ti ti-loader-2 k2b-spin\" : \"ti ti-external-link\"} aria-hidden=\"true\" />\n {props.openButtonLabel ?? messages().openPreview}\n </button>\n }\n >\n {(href) => (\n // A plain link to a real address: the browser opens it on the user's own click, and the tab can reload it.\n <a\n ref={(element) => (openButton = element)}\n class=\"k2b-button\"\n data-variant=\"secondary\"\n data-size=\"sm\"\n href={href()}\n target=\"_blank\"\n rel=\"noopener\"\n >\n <i class=\"ti ti-external-link\" aria-hidden=\"true\" />\n {props.openButtonLabel ?? messages().openPreview}\n </a>\n )}\n </Show>\n <Show when={props.onDownload}>\n <button\n type=\"button\"\n class=\"k2b-button\"\n data-variant=\"secondary\"\n data-size=\"sm\"\n onClick={() => props.onDownload?.()}\n disabled={props.disabled?.()}\n >\n <i class=\"ti ti-download\" aria-hidden=\"true\" />\n {messages().download}\n </button>\n </Show>\n <Show when={renderable()}>\n <button\n ref={renderButton}\n type=\"button\"\n class=\"k2b-button\"\n data-variant=\"secondary\"\n data-size=\"sm\"\n onClick={() => void load()}\n disabled={loading() || opening() || props.disabled?.()}\n >\n <i class={loading() ? \"ti ti-loader-2 k2b-spin\" : \"ti ti-file-type-pdf\"} aria-hidden=\"true\" />\n {props.buttonLabel ?? messages().previewPdf}\n </button>\n </Show>\n <Show when={tabBlocked()}>\n <span role=\"alert\" class=\"k2b-content-pdf-preview__notice\">\n {messages().pdfPreviewTabBlocked}\n </span>\n </Show>\n </div>\n );\n // Every state before a document is shown uses one placeholder in the viewer's box, so loading changes no layout.\n const content = () => (\n <Show\n when={error()}\n fallback={\n <Show\n when={url()}\n fallback={\n <Placeholder\n class=\"k2b-content-pdf-preview__placeholder\"\n state={loading() ? \"loading\" : \"empty\"}\n icon={loading() ? undefined : \"ti ti-file-type-pdf\"}\n title={loading() ? messages().loading : undefined}\n description={loading() ? undefined : (props.emptyText ?? messages().renderPdfPreview)}\n />\n }\n >\n {(currentUrl) => (\n <iframe\n ref={(element) => (frame = element)}\n class=\"k2b-content-pdf-preview__frame\"\n src={currentUrl()}\n title={props.title ?? messages().pdfPreview}\n />\n )}\n </Show>\n }\n >\n {(message) =>\n props.renderError?.(message()) ?? (\n <Placeholder\n class=\"k2b-content-pdf-preview__placeholder\"\n state=\"error\"\n description={message()}\n action={\n // One retry at a time: the render action moves here only where the toolbar no longer offers it.\n renderable() ? undefined : (\n <button\n ref={(element) => (errorRetryButton = element)}\n type=\"button\"\n class=\"k2b-button\"\n data-variant=\"secondary\"\n data-size=\"sm\"\n onClick={() => void load()}\n disabled={opening() || props.disabled?.()}\n >\n {props.buttonLabel ?? messages().retry}\n </button>\n )\n }\n />\n )\n }\n </Show>\n );\n\n if (props.children)\n return props.children({\n get actions() {\n return actions();\n },\n get content() {\n return content();\n },\n });\n return (\n <section class={`k2b-content-pdf-preview ${props.class ?? \"\"}`}>\n <div class=\"k2b-content-pdf-preview__toolbar\">\n <div class=\"k2b-content-pdf-preview__heading\">\n <Show when={props.title}>\n <h2 class=\"k2b-content-pdf-preview__title\">{props.title}</h2>\n </Show>\n </div>\n {actions()}\n </div>\n {content()}\n </section>\n );\n}\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { template as _$template } from "solid-js/web";
|
|
2
|
+
import { memo as _$memo } from "solid-js/web";
|
|
3
|
+
import { setAttribute as _$setAttribute } from "solid-js/web";
|
|
4
|
+
import { className as _$className } from "solid-js/web";
|
|
5
|
+
import { effect as _$effect } from "solid-js/web";
|
|
6
|
+
import { getNextMarker as _$getNextMarker } from "solid-js/web";
|
|
7
|
+
import { createComponent as _$createComponent } from "solid-js/web";
|
|
8
|
+
import { getNextElement as _$getNextElement } from "solid-js/web";
|
|
9
|
+
import { insert as _$insert } from "solid-js/web";
|
|
10
|
+
var _tmpl$ = /*#__PURE__*/_$template(`<p role=alert>`),
|
|
11
|
+
_tmpl$2 = /*#__PURE__*/_$template(`<p role=status>`),
|
|
12
|
+
_tmpl$3 = /*#__PURE__*/_$template(`<div><!$><!/><!$><!/>`),
|
|
13
|
+
_tmpl$4 = /*#__PURE__*/_$template(`<p>`),
|
|
14
|
+
_tmpl$5 = /*#__PURE__*/_$template(`<i aria-hidden=true>`),
|
|
15
|
+
_tmpl$6 = /*#__PURE__*/_$template(`<code>`),
|
|
16
|
+
_tmpl$7 = /*#__PURE__*/_$template(`<ol class=k2b-install-guide__steps>`),
|
|
17
|
+
_tmpl$8 = /*#__PURE__*/_$template(`<li><span class=k2b-install-guide__step-icon><i aria-hidden=true></i></span><div><h3></h3><p>`);
|
|
18
|
+
import { clipboard } from "@k2b/stdlib/solid";
|
|
19
|
+
import { createMemo, For, Show } from "solid-js";
|
|
20
|
+
import { Button } from "../actions/Button";
|
|
21
|
+
import { useUiMessages } from "../intl/messages";
|
|
22
|
+
import { NoticeCard } from "../surfaces/NoticeCard";
|
|
23
|
+
/**
|
|
24
|
+
* Explains how to install the current page as an app on this platform: the browser's own dialog where it offers
|
|
25
|
+
* one, otherwise the steps for Safari on iPhone, iPad, and Mac, other browsers on iPhone and iPad, Android, and other
|
|
26
|
+
* browsers. A browser inside another app cannot install, so it gets a warning and the link to copy into Safari or
|
|
27
|
+
* Chrome instead. The host owns the surrounding surface, its heading, and dismissal.
|
|
28
|
+
*/
|
|
29
|
+
export function InstallGuide(props) {
|
|
30
|
+
const messages = useUiMessages();
|
|
31
|
+
// The host may hand over another state after mount, such as the browser's own in place of the server's view.
|
|
32
|
+
const state = () => props.install;
|
|
33
|
+
const copy = clipboard.createWriter({
|
|
34
|
+
write: url => navigator.clipboard.writeText(url)
|
|
35
|
+
});
|
|
36
|
+
const embedded = () => state().platform === "in-app" || state().platform === "apple-in-app";
|
|
37
|
+
/** A browser that installs, but less reliably than the platform's own. */
|
|
38
|
+
const hint = createMemo(() => {
|
|
39
|
+
const t = messages();
|
|
40
|
+
if (state().platform === "apple-browser") return {
|
|
41
|
+
title: t.installAppleBrowserTitle,
|
|
42
|
+
detail: t.installAppleBrowserDetail
|
|
43
|
+
};
|
|
44
|
+
if (state().platform === "android-browser") return {
|
|
45
|
+
title: t.installAndroidBrowserTitle,
|
|
46
|
+
detail: t.installAndroidBrowserDetail
|
|
47
|
+
};
|
|
48
|
+
return undefined;
|
|
49
|
+
});
|
|
50
|
+
const steps = createMemo(() => {
|
|
51
|
+
const t = messages();
|
|
52
|
+
if (state().platform === "apple-mobile" || state().platform === "apple-browser") return [{
|
|
53
|
+
icon: "ti ti-share-2",
|
|
54
|
+
title: t.installOpenShare,
|
|
55
|
+
detail: state().platform === "apple-browser" ? t.installOpenShareBrowserDetail : t.installOpenShareDetail
|
|
56
|
+
}, {
|
|
57
|
+
icon: "ti ti-square-plus",
|
|
58
|
+
title: t.installHomeScreen,
|
|
59
|
+
detail: t.installHomeScreenDetail
|
|
60
|
+
}, {
|
|
61
|
+
icon: "ti ti-check",
|
|
62
|
+
title: t.installConfirmAdd,
|
|
63
|
+
detail: t.installConfirmAddDetail
|
|
64
|
+
}];
|
|
65
|
+
if (state().platform === "apple-desktop") return [{
|
|
66
|
+
icon: "ti ti-share-2",
|
|
67
|
+
title: t.installSafariMenu,
|
|
68
|
+
detail: t.installSafariMenuDetail
|
|
69
|
+
}, {
|
|
70
|
+
icon: "ti ti-app-window",
|
|
71
|
+
title: t.installAddDock,
|
|
72
|
+
detail: t.installAddDockDetail
|
|
73
|
+
}];
|
|
74
|
+
return [{
|
|
75
|
+
icon: "ti ti-dots",
|
|
76
|
+
title: t.installBrowserMenu,
|
|
77
|
+
detail: t.installBrowserMenuDetail
|
|
78
|
+
}, {
|
|
79
|
+
icon: "ti ti-square-plus",
|
|
80
|
+
title: t.installApp,
|
|
81
|
+
detail: state().platform === "android" || state().platform === "android-browser" ? t.installAndroidDetail : t.installBrowserDetail
|
|
82
|
+
}];
|
|
83
|
+
});
|
|
84
|
+
return (() => {
|
|
85
|
+
var _el$ = _$getNextElement(_tmpl$3),
|
|
86
|
+
_el$4 = _el$.firstChild,
|
|
87
|
+
[_el$5, _co$] = _$getNextMarker(_el$4.nextSibling),
|
|
88
|
+
_el$6 = _el$5.nextSibling,
|
|
89
|
+
[_el$7, _co$2] = _$getNextMarker(_el$6.nextSibling);
|
|
90
|
+
_$insert(_el$, _$createComponent(Show, {
|
|
91
|
+
get when() {
|
|
92
|
+
return state().failed();
|
|
93
|
+
},
|
|
94
|
+
get children() {
|
|
95
|
+
var _el$2 = _$getNextElement(_tmpl$);
|
|
96
|
+
_$insert(_el$2, () => messages().installFailed);
|
|
97
|
+
return _el$2;
|
|
98
|
+
}
|
|
99
|
+
}), _el$5, _co$);
|
|
100
|
+
_$insert(_el$, _$createComponent(Show, {
|
|
101
|
+
get when() {
|
|
102
|
+
return state().requested();
|
|
103
|
+
},
|
|
104
|
+
get fallback() {
|
|
105
|
+
return _$createComponent(Show, {
|
|
106
|
+
get when() {
|
|
107
|
+
return state().canPrompt() || state().busy();
|
|
108
|
+
},
|
|
109
|
+
get fallback() {
|
|
110
|
+
return _$createComponent(Show, {
|
|
111
|
+
get when() {
|
|
112
|
+
return embedded();
|
|
113
|
+
},
|
|
114
|
+
get fallback() {
|
|
115
|
+
return [_$createComponent(Show, {
|
|
116
|
+
get when() {
|
|
117
|
+
return hint();
|
|
118
|
+
},
|
|
119
|
+
children: hint => _$createComponent(NoticeCard, {
|
|
120
|
+
tone: "info",
|
|
121
|
+
get title() {
|
|
122
|
+
return hint().title;
|
|
123
|
+
},
|
|
124
|
+
get detail() {
|
|
125
|
+
return hint().detail;
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
}), (() => {
|
|
129
|
+
var _el$11 = _$getNextElement(_tmpl$7);
|
|
130
|
+
_$insert(_el$11, _$createComponent(For, {
|
|
131
|
+
get each() {
|
|
132
|
+
return steps();
|
|
133
|
+
},
|
|
134
|
+
children: step => (() => {
|
|
135
|
+
var _el$15 = _$getNextElement(_tmpl$8),
|
|
136
|
+
_el$16 = _el$15.firstChild,
|
|
137
|
+
_el$17 = _el$16.firstChild,
|
|
138
|
+
_el$18 = _el$16.nextSibling,
|
|
139
|
+
_el$19 = _el$18.firstChild,
|
|
140
|
+
_el$20 = _el$19.nextSibling;
|
|
141
|
+
_$insert(_el$19, () => step.title);
|
|
142
|
+
_$insert(_el$20, () => step.detail);
|
|
143
|
+
_$effect(() => _$className(_el$17, step.icon));
|
|
144
|
+
return _el$15;
|
|
145
|
+
})()
|
|
146
|
+
}));
|
|
147
|
+
return _el$11;
|
|
148
|
+
})(), _$createComponent(Show, {
|
|
149
|
+
get when() {
|
|
150
|
+
return state().platform === "apple-mobile";
|
|
151
|
+
},
|
|
152
|
+
get children() {
|
|
153
|
+
var _el$12 = _$getNextElement(_tmpl$4);
|
|
154
|
+
_$insert(_el$12, () => messages().installSafariFallback);
|
|
155
|
+
return _el$12;
|
|
156
|
+
}
|
|
157
|
+
}), _$createComponent(Show, {
|
|
158
|
+
get when() {
|
|
159
|
+
return props.note;
|
|
160
|
+
},
|
|
161
|
+
get children() {
|
|
162
|
+
var _el$13 = _$getNextElement(_tmpl$4);
|
|
163
|
+
_$insert(_el$13, () => props.note);
|
|
164
|
+
return _el$13;
|
|
165
|
+
}
|
|
166
|
+
}), _$createComponent(Show, {
|
|
167
|
+
get when() {
|
|
168
|
+
return state().platform === "generic";
|
|
169
|
+
},
|
|
170
|
+
get children() {
|
|
171
|
+
var _el$14 = _$getNextElement(_tmpl$4);
|
|
172
|
+
_$insert(_el$14, () => messages().installUnavailable);
|
|
173
|
+
return _el$14;
|
|
174
|
+
}
|
|
175
|
+
})];
|
|
176
|
+
},
|
|
177
|
+
get children() {
|
|
178
|
+
return [_$createComponent(NoticeCard, {
|
|
179
|
+
tone: "warning",
|
|
180
|
+
get title() {
|
|
181
|
+
return _$memo(() => state().platform === "apple-in-app")() ? messages().installAppleInAppTitle : messages().installInAppTitle;
|
|
182
|
+
},
|
|
183
|
+
get detail() {
|
|
184
|
+
return _$memo(() => state().platform === "apple-in-app")() ? messages().installAppleInAppDetail({
|
|
185
|
+
appName: props.appName
|
|
186
|
+
}) : messages().installInAppDetail({
|
|
187
|
+
appName: props.appName
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}), _$createComponent(Button, {
|
|
191
|
+
variant: "secondary",
|
|
192
|
+
onClick: () => void copy.copy(props.url),
|
|
193
|
+
get children() {
|
|
194
|
+
return [(() => {
|
|
195
|
+
var _el$9 = _$getNextElement(_tmpl$5);
|
|
196
|
+
_$effect(() => _$className(_el$9, copy.wasCopied() ? "ti ti-check" : "ti ti-copy"));
|
|
197
|
+
return _el$9;
|
|
198
|
+
})(), _$memo(() => _$memo(() => !!copy.wasCopied())() ? messages().installLinkCopied : messages().installCopyLink)];
|
|
199
|
+
}
|
|
200
|
+
}), _$createComponent(Show, {
|
|
201
|
+
get when() {
|
|
202
|
+
return copy.error();
|
|
203
|
+
},
|
|
204
|
+
get children() {
|
|
205
|
+
return [(() => {
|
|
206
|
+
var _el$0 = _$getNextElement(_tmpl$);
|
|
207
|
+
_$insert(_el$0, () => messages().installCopyFailed);
|
|
208
|
+
return _el$0;
|
|
209
|
+
})(), (() => {
|
|
210
|
+
var _el$1 = _$getNextElement(_tmpl$6);
|
|
211
|
+
_$insert(_el$1, () => props.url);
|
|
212
|
+
return _el$1;
|
|
213
|
+
})()];
|
|
214
|
+
}
|
|
215
|
+
}), _$createComponent(Show, {
|
|
216
|
+
get when() {
|
|
217
|
+
return props.note;
|
|
218
|
+
},
|
|
219
|
+
get children() {
|
|
220
|
+
var _el$10 = _$getNextElement(_tmpl$4);
|
|
221
|
+
_$insert(_el$10, () => props.note);
|
|
222
|
+
return _el$10;
|
|
223
|
+
}
|
|
224
|
+
})];
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
},
|
|
228
|
+
get children() {
|
|
229
|
+
return [(() => {
|
|
230
|
+
var _el$8 = _$getNextElement(_tmpl$4);
|
|
231
|
+
_$insert(_el$8, () => messages().installNative({
|
|
232
|
+
appName: props.appName
|
|
233
|
+
}));
|
|
234
|
+
return _el$8;
|
|
235
|
+
})(), _$createComponent(Button, {
|
|
236
|
+
get loading() {
|
|
237
|
+
return state().busy();
|
|
238
|
+
},
|
|
239
|
+
onClick: () => void state().install(),
|
|
240
|
+
get children() {
|
|
241
|
+
return messages().installApp;
|
|
242
|
+
}
|
|
243
|
+
})];
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
},
|
|
247
|
+
get children() {
|
|
248
|
+
var _el$3 = _$getNextElement(_tmpl$2);
|
|
249
|
+
_$insert(_el$3, () => messages().installRequested);
|
|
250
|
+
return _el$3;
|
|
251
|
+
}
|
|
252
|
+
}), _el$7, _co$2);
|
|
253
|
+
_$effect(_p$ => {
|
|
254
|
+
var _v$ = props.class ? `k2b-install-guide ${props.class}` : "k2b-install-guide",
|
|
255
|
+
_v$2 = state().platform;
|
|
256
|
+
_v$ !== _p$.e && _$className(_el$, _p$.e = _v$);
|
|
257
|
+
_v$2 !== _p$.t && _$setAttribute(_el$, "data-platform", _p$.t = _v$2);
|
|
258
|
+
return _p$;
|
|
259
|
+
}, {
|
|
260
|
+
e: undefined,
|
|
261
|
+
t: undefined
|
|
262
|
+
});
|
|
263
|
+
return _el$;
|
|
264
|
+
})();
|
|
265
|
+
}
|
|
266
|
+
export default InstallGuide;
|
|
267
|
+
//# sourceMappingURL=InstallGuide.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":[],"sources":["feedback/InstallGuide.tsx"],"sourcesContent":["import { clipboard } from \"@k2b/stdlib/solid\";\nimport { createMemo, For, type JSX, Show } from \"solid-js\";\nimport { Button } from \"../actions/Button\";\nimport { useUiMessages } from \"../intl/messages\";\nimport { NoticeCard } from \"../surfaces/NoticeCard\";\nimport type { InstallPrompt } from \"./install\";\n\nexport type InstallGuideProps = {\n /** The name people see on their Home Screen, used in the guidance text. */\n appName: string;\n install: InstallPrompt;\n /**\n * The address a browser inside another app copies, to open it in Safari or Chrome. Shown as text when copying\n * fails, so it can be copied by hand.\n */\n url: string;\n /**\n * An application note under the installation steps or the embedded browser's link, for example what else the\n * installed app enables.\n */\n note?: string;\n class?: string;\n};\n\n/**\n * Explains how to install the current page as an app on this platform: the browser's own dialog where it offers\n * one, otherwise the steps for Safari on iPhone, iPad, and Mac, other browsers on iPhone and iPad, Android, and other\n * browsers. A browser inside another app cannot install, so it gets a warning and the link to copy into Safari or\n * Chrome instead. The host owns the surrounding surface, its heading, and dismissal.\n */\nexport function InstallGuide(props: InstallGuideProps): JSX.Element {\n const messages = useUiMessages();\n // The host may hand over another state after mount, such as the browser's own in place of the server's view.\n const state = () => props.install;\n const copy = clipboard.createWriter({ write: (url: string) => navigator.clipboard.writeText(url) });\n const embedded = () => state().platform === \"in-app\" || state().platform === \"apple-in-app\";\n /** A browser that installs, but less reliably than the platform's own. */\n const hint = createMemo(() => {\n const t = messages();\n if (state().platform === \"apple-browser\") return { title: t.installAppleBrowserTitle, detail: t.installAppleBrowserDetail };\n if (state().platform === \"android-browser\") return { title: t.installAndroidBrowserTitle, detail: t.installAndroidBrowserDetail };\n return undefined;\n });\n const steps = createMemo(() => {\n const t = messages();\n if (state().platform === \"apple-mobile\" || state().platform === \"apple-browser\")\n return [\n {\n icon: \"ti ti-share-2\",\n title: t.installOpenShare,\n detail: state().platform === \"apple-browser\" ? t.installOpenShareBrowserDetail : t.installOpenShareDetail,\n },\n { icon: \"ti ti-square-plus\", title: t.installHomeScreen, detail: t.installHomeScreenDetail },\n { icon: \"ti ti-check\", title: t.installConfirmAdd, detail: t.installConfirmAddDetail },\n ];\n if (state().platform === \"apple-desktop\")\n return [\n { icon: \"ti ti-share-2\", title: t.installSafariMenu, detail: t.installSafariMenuDetail },\n { icon: \"ti ti-app-window\", title: t.installAddDock, detail: t.installAddDockDetail },\n ];\n return [\n { icon: \"ti ti-dots\", title: t.installBrowserMenu, detail: t.installBrowserMenuDetail },\n {\n icon: \"ti ti-square-plus\",\n title: t.installApp,\n detail: state().platform === \"android\" || state().platform === \"android-browser\" ? t.installAndroidDetail : t.installBrowserDetail,\n },\n ];\n });\n return (\n <div class={props.class ? `k2b-install-guide ${props.class}` : \"k2b-install-guide\"} data-platform={state().platform}>\n <Show when={state().failed()}>\n <p role=\"alert\">{messages().installFailed}</p>\n </Show>\n <Show\n when={state().requested()}\n fallback={\n <Show\n when={state().canPrompt() || state().busy()}\n fallback={\n <Show\n when={embedded()}\n fallback={\n <>\n <Show when={hint()}>{(hint) => <NoticeCard tone=\"info\" title={hint().title} detail={hint().detail} />}</Show>\n <ol class=\"k2b-install-guide__steps\">\n <For each={steps()}>\n {(step) => (\n <li>\n <span class=\"k2b-install-guide__step-icon\">\n <i class={step.icon} aria-hidden=\"true\" />\n </span>\n <div>\n <h3>{step.title}</h3>\n <p>{step.detail}</p>\n </div>\n </li>\n )}\n </For>\n </ol>\n <Show when={state().platform === \"apple-mobile\"}>\n <p>{messages().installSafariFallback}</p>\n </Show>\n <Show when={props.note}>\n <p>{props.note}</p>\n </Show>\n <Show when={state().platform === \"generic\"}>\n <p>{messages().installUnavailable}</p>\n </Show>\n </>\n }\n >\n <NoticeCard\n tone=\"warning\"\n title={state().platform === \"apple-in-app\" ? messages().installAppleInAppTitle : messages().installInAppTitle}\n detail={\n state().platform === \"apple-in-app\"\n ? messages().installAppleInAppDetail({ appName: props.appName })\n : messages().installInAppDetail({ appName: props.appName })\n }\n />\n <Button variant=\"secondary\" onClick={() => void copy.copy(props.url)}>\n <i class={copy.wasCopied() ? \"ti ti-check\" : \"ti ti-copy\"} aria-hidden=\"true\" />\n {copy.wasCopied() ? messages().installLinkCopied : messages().installCopyLink}\n </Button>\n <Show when={copy.error()}>\n <p role=\"alert\">{messages().installCopyFailed}</p>\n <code>{props.url}</code>\n </Show>\n <Show when={props.note}>\n <p>{props.note}</p>\n </Show>\n </Show>\n }\n >\n <p>{messages().installNative({ appName: props.appName })}</p>\n <Button loading={state().busy()} onClick={() => void state().install()}>\n {messages().installApp}\n </Button>\n </Show>\n }\n >\n <p role=\"status\">{messages().installRequested}</p>\n </Show>\n </div>\n );\n}\n\nexport default InstallGuide;\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** A live region needs a moment between being found and being written to. */
|
|
2
|
+
const ANNOUNCE_DELAY_MS = 100;
|
|
3
|
+
const ANNOUNCEMENT_LIFETIME_MS = 7_000;
|
|
4
|
+
const LIVE_ATTRIBUTE = "data-k2b-live";
|
|
5
|
+
const VISUALLY_HIDDEN = "position:absolute;width:1px;height:1px;margin:-1px;padding:0;border:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap;";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* One pair of persistent, empty live regions per document, polite and assertive, shared by `announce` and every
|
|
9
|
+
* toast. A region that already exists when its text arrives is announced reliably, unlike one inserted with its text
|
|
10
|
+
* already inside. They sit directly in the body, outside every portal and the toast rail, which moves into a fresh
|
|
11
|
+
* top-layer element for every toast, so no scope that closes takes them along.
|
|
12
|
+
*/
|
|
13
|
+
export const ensureLiveRegions = doc => {
|
|
14
|
+
const existing = doc.querySelector(`[${LIVE_ATTRIBUTE}]`);
|
|
15
|
+
if (existing) return existing;
|
|
16
|
+
const live = doc.createElement("div");
|
|
17
|
+
live.setAttribute(LIVE_ATTRIBUTE, "");
|
|
18
|
+
live.style.cssText = VISUALLY_HIDDEN;
|
|
19
|
+
for (const politeness of ["polite", "assertive"]) {
|
|
20
|
+
const region = doc.createElement("div");
|
|
21
|
+
region.setAttribute("role", politeness === "assertive" ? "alert" : "status");
|
|
22
|
+
region.setAttribute("aria-live", politeness);
|
|
23
|
+
// Both roles are atomic by default, which would read every line still in the region again with each new one.
|
|
24
|
+
region.setAttribute("aria-atomic", "false");
|
|
25
|
+
region.dataset.politeness = politeness;
|
|
26
|
+
live.appendChild(region);
|
|
27
|
+
}
|
|
28
|
+
doc.body.appendChild(live);
|
|
29
|
+
return live;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Tells screen readers about an outcome without showing anything: a change the screen shows but the focused control
|
|
34
|
+
* does not say, because a shortcut acted, the control was busy, or it was replaced. Each message is its own line, so
|
|
35
|
+
* a burst of messages or the same message twice is still read. Calls without `document`, including during SSR, do
|
|
36
|
+
* nothing.
|
|
37
|
+
*/
|
|
38
|
+
export const announce = (message, options) => {
|
|
39
|
+
if (typeof document === "undefined" || !message) return;
|
|
40
|
+
const politeness = options?.politeness ?? "polite";
|
|
41
|
+
const region = ensureLiveRegions(document).querySelector(`[data-politeness="${politeness}"]`);
|
|
42
|
+
if (!region) return;
|
|
43
|
+
const line = document.createElement("div");
|
|
44
|
+
line.textContent = message;
|
|
45
|
+
setTimeout(() => {
|
|
46
|
+
region.appendChild(line);
|
|
47
|
+
setTimeout(() => line.remove(), ANNOUNCEMENT_LIFETIME_MS);
|
|
48
|
+
}, ANNOUNCE_DELAY_MS);
|
|
49
|
+
};
|
|
50
|
+
//# sourceMappingURL=announce.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":[],"sources":["feedback/announce.ts"],"sourcesContent":["export type AnnouncePoliteness = \"polite\" | \"assertive\";\n\nexport type AnnounceOptions = {\n /** `polite` waits until the screen reader is idle; `assertive` interrupts it. Defaults to `polite`. */\n politeness?: AnnouncePoliteness;\n};\n\n/** A live region needs a moment between being found and being written to. */\nconst ANNOUNCE_DELAY_MS = 100;\nconst ANNOUNCEMENT_LIFETIME_MS = 7_000;\nconst LIVE_ATTRIBUTE = \"data-k2b-live\";\nconst VISUALLY_HIDDEN =\n \"position:absolute;width:1px;height:1px;margin:-1px;padding:0;border:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap;\";\n\n/**\n * One pair of persistent, empty live regions per document, polite and assertive, shared by `announce` and every\n * toast. A region that already exists when its text arrives is announced reliably, unlike one inserted with its text\n * already inside. They sit directly in the body, outside every portal and the toast rail, which moves into a fresh\n * top-layer element for every toast, so no scope that closes takes them along.\n */\nexport const ensureLiveRegions = (doc: Document): HTMLElement => {\n const existing = doc.querySelector<HTMLElement>(`[${LIVE_ATTRIBUTE}]`);\n if (existing) return existing;\n const live = doc.createElement(\"div\");\n live.setAttribute(LIVE_ATTRIBUTE, \"\");\n live.style.cssText = VISUALLY_HIDDEN;\n for (const politeness of [\"polite\", \"assertive\"] as const) {\n const region = doc.createElement(\"div\");\n region.setAttribute(\"role\", politeness === \"assertive\" ? \"alert\" : \"status\");\n region.setAttribute(\"aria-live\", politeness);\n // Both roles are atomic by default, which would read every line still in the region again with each new one.\n region.setAttribute(\"aria-atomic\", \"false\");\n region.dataset.politeness = politeness;\n live.appendChild(region);\n }\n doc.body.appendChild(live);\n return live;\n};\n\n/**\n * Tells screen readers about an outcome without showing anything: a change the screen shows but the focused control\n * does not say, because a shortcut acted, the control was busy, or it was replaced. Each message is its own line, so\n * a burst of messages or the same message twice is still read. Calls without `document`, including during SSR, do\n * nothing.\n */\nexport const announce = (message: string, options?: AnnounceOptions): void => {\n if (typeof document === \"undefined\" || !message) return;\n const politeness = options?.politeness ?? \"polite\";\n const region = ensureLiveRegions(document).querySelector<HTMLElement>(`[data-politeness=\"${politeness}\"]`);\n if (!region) return;\n const line = document.createElement(\"div\");\n line.textContent = message;\n setTimeout(() => {\n region.appendChild(line);\n setTimeout(() => line.remove(), ANNOUNCEMENT_LIFETIME_MS);\n }, ANNOUNCE_DELAY_MS);\n};\n"],"mappings":"AAOA;AACA,MAAM,iBAAiB,GAAG,GAAG;AAC7B,MAAM,wBAAwB,GAAG,KAAK;AACtC,MAAM,cAAc,GAAG,eAAe;AACtC,MAAM,eAAe,GACnB,gIAAgI;;AAElI;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAM,iBAAiB,GAAI,GAAa,IAAkB;EAC/D,MAAM,QAAQ,GAAG,GAAG,CAAC,aAAa,CAAc,IAAI,cAAc,GAAG,CAAC;EACtE,IAAI,QAAQ,EAAE,OAAO,QAAQ;EAC7B,MAAM,IAAI,GAAG,GAAG,CAAC,aAAa,CAAC,KAAK,CAAC;EACrC,IAAI,CAAC,YAAY,CAAC,cAAc,EAAE,EAAE,CAAC;EACrC,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,eAAe;EACpC,KAAK,MAAM,UAAU,IAAI,CAAC,QAAQ,EAAE,WAAW,CAAC,EAAW;IACzD,MAAM,MAAM,GAAG,GAAG,CAAC,aAAa,CAAC,KAAK,CAAC;IACvC,MAAM,CAAC,YAAY,CAAC,MAAM,EAAE,UAAU,KAAK,WAAW,GAAG,OAAO,GAAG,QAAQ,CAAC;IAC5E,MAAM,CAAC,YAAY,CAAC,WAAW,EAAE,UAAU,CAAC;IAC5C;IACA,MAAM,CAAC,YAAY,CAAC,aAAa,EAAE,OAAO,CAAC;IAC3C,MAAM,CAAC,OAAO,CAAC,UAAU,GAAG,UAAU;IACtC,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC;EAC1B;EACA,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC;EAC1B,OAAO,IAAI;AACb,CAAC;;AAED;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAM,QAAQ,GAAG,CAAC,OAAe,EAAE,OAAyB,KAAW;EAC5E,IAAI,OAAO,QAAQ,KAAK,WAAW,IAAI,CAAC,OAAO,EAAE;EACjD,MAAM,UAAU,GAAG,OAAO,EAAE,UAAU,IAAI,QAAQ;EAClD,MAAM,MAAM,GAAG,iBAAiB,CAAC,QAAQ,CAAC,CAAC,aAAa,CAAc,qBAAqB,UAAU,IAAI,CAAC;EAC1G,IAAI,CAAC,MAAM,EAAE;EACb,MAAM,IAAI,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC;EAC1C,IAAI,CAAC,WAAW,GAAG,OAAO;EAC1B,UAAU,CAAC,MAAM;IACf,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC;IACxB,UAAU,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,CAAC,EAAE,wBAAwB,CAAC;EAC3D,CAAC,EAAE,iBAAiB,CAAC;AACvB,CAAC","ignoreList":[]}
|
|
@@ -6,6 +6,12 @@ import { isPointInsideToast } from "./toast";
|
|
|
6
6
|
const DEFAULT_PANEL_CLASS = "k2b-dialog";
|
|
7
7
|
const DEFAULT_CONTENT_CLASS = "k2b-dialog__viewport";
|
|
8
8
|
let nextDialogTitleId = 0;
|
|
9
|
+
/** The history state key that marks a dialog's same-URL entry. */
|
|
10
|
+
const HISTORY_MARKER = "k2bDialog";
|
|
11
|
+
const historyRecord = () => typeof history.state === "object" && history.state !== null ? {
|
|
12
|
+
...history.state
|
|
13
|
+
} : {};
|
|
14
|
+
const historyMarker = () => historyRecord()[HISTORY_MARKER];
|
|
9
15
|
const resolveInitialFocusTarget = (entry, dialog) => {
|
|
10
16
|
const {
|
|
11
17
|
initialFocus
|
|
@@ -47,6 +53,67 @@ export const createDialogCore = () => {
|
|
|
47
53
|
const state = {
|
|
48
54
|
stack: []
|
|
49
55
|
};
|
|
56
|
+
/** Markers of this core's entries in the browser history, oldest first. */
|
|
57
|
+
let historyIds = [];
|
|
58
|
+
let nextHistoryId = 0;
|
|
59
|
+
let historySyncScheduled = false;
|
|
60
|
+
let listeningToHistory = false;
|
|
61
|
+
/** Settles when the synthetic Back that removes closed dialogs' entries has arrived. */
|
|
62
|
+
let returning;
|
|
63
|
+
|
|
64
|
+
/** Closed dialogs leave the history with one Back to the newest dialog that is still open, or to the page. */
|
|
65
|
+
const syncHistory = () => {
|
|
66
|
+
historySyncScheduled = false;
|
|
67
|
+
const openIds = new Set(state.stack.map(entry => entry.historyId));
|
|
68
|
+
const current = historyIds.at(-1);
|
|
69
|
+
let steps = 0;
|
|
70
|
+
while (historyIds.length > 0 && !openIds.has(historyIds.at(-1))) {
|
|
71
|
+
historyIds.pop();
|
|
72
|
+
steps++;
|
|
73
|
+
}
|
|
74
|
+
// Someone else moved the history meanwhile; leave it alone.
|
|
75
|
+
if (steps === 0 || historyMarker() !== current) return;
|
|
76
|
+
const back = new Promise(resolve => window.addEventListener("popstate", () => resolve(), {
|
|
77
|
+
once: true
|
|
78
|
+
}));
|
|
79
|
+
returning = back;
|
|
80
|
+
void back.then(() => {
|
|
81
|
+
if (returning === back) returning = undefined;
|
|
82
|
+
});
|
|
83
|
+
history.go(-steps);
|
|
84
|
+
};
|
|
85
|
+
const releaseHistory = entry => {
|
|
86
|
+
if (entry.historyId === undefined || historySyncScheduled) return;
|
|
87
|
+
historySyncScheduled = true;
|
|
88
|
+
queueMicrotask(syncHistory);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/** Back or Forward moved to another entry: close every dialog opened after the one it shows. */
|
|
92
|
+
const onPopState = () => {
|
|
93
|
+
const marker = historyMarker();
|
|
94
|
+
const index = typeof marker === "number" ? historyIds.indexOf(marker) : -1;
|
|
95
|
+
historyIds = historyIds.slice(0, index + 1);
|
|
96
|
+
const gone = entry => entry.historyId !== undefined && !historyIds.includes(entry.historyId);
|
|
97
|
+
while (state.stack.some(gone)) popTop(undefined);
|
|
98
|
+
};
|
|
99
|
+
const pushHistory = entry => {
|
|
100
|
+
if (!listeningToHistory) {
|
|
101
|
+
window.addEventListener("popstate", onPopState);
|
|
102
|
+
listeningToHistory = true;
|
|
103
|
+
}
|
|
104
|
+
const previous = historyRecord();
|
|
105
|
+
// A reload or Forward never brings back a dialog or its sensitive transient state.
|
|
106
|
+
if (historyIds.length === 0 && previous[HISTORY_MARKER] !== undefined) {
|
|
107
|
+
delete previous[HISTORY_MARKER];
|
|
108
|
+
history.replaceState(previous, "");
|
|
109
|
+
}
|
|
110
|
+
entry.historyId = ++nextHistoryId;
|
|
111
|
+
historyIds.push(entry.historyId);
|
|
112
|
+
history.pushState({
|
|
113
|
+
...previous,
|
|
114
|
+
[HISTORY_MARKER]: entry.historyId
|
|
115
|
+
}, "");
|
|
116
|
+
};
|
|
50
117
|
const ensureDialogElement = () => {
|
|
51
118
|
if (typeof document === "undefined") throw new Error("@k2b/ui dialogs can only be opened in the browser");
|
|
52
119
|
// Reuse the shared element whenever it is still in the document. Resolving
|
|
@@ -159,6 +226,7 @@ export const createDialogCore = () => {
|
|
|
159
226
|
const popTop = result => {
|
|
160
227
|
const top = state.stack.pop();
|
|
161
228
|
if (!top) return;
|
|
229
|
+
releaseHistory(top);
|
|
162
230
|
top.dispose?.();
|
|
163
231
|
top.container.remove();
|
|
164
232
|
const dialog = state.element;
|
|
@@ -201,6 +269,7 @@ export const createDialogCore = () => {
|
|
|
201
269
|
dialog?.remove();
|
|
202
270
|
unlockPageScroll();
|
|
203
271
|
for (const entry of entries.reverse()) {
|
|
272
|
+
releaseHistory(entry);
|
|
204
273
|
entry.dispose?.();
|
|
205
274
|
entry.container.remove();
|
|
206
275
|
entry.resolve?.(undefined);
|
|
@@ -217,6 +286,11 @@ export const createDialogCore = () => {
|
|
|
217
286
|
};
|
|
218
287
|
const open = (view, options = {}) => {
|
|
219
288
|
if (options.signal?.aborted) return Promise.resolve(undefined);
|
|
289
|
+
if (options.history) {
|
|
290
|
+
// Dialogs closed just now leave the history first, or this dialog's entry would be the one removed.
|
|
291
|
+
if (historySyncScheduled) syncHistory();
|
|
292
|
+
if (returning) return returning.then(() => open(view, options));
|
|
293
|
+
}
|
|
220
294
|
const dialog = ensureDialogElement();
|
|
221
295
|
const previousTop = state.stack[state.stack.length - 1];
|
|
222
296
|
if (previousTop) previousTop.container.style.display = "none";
|
|
@@ -244,6 +318,7 @@ export const createDialogCore = () => {
|
|
|
244
318
|
if (index < 0) return;
|
|
245
319
|
if (index === state.stack.length - 1) popTop(undefined);else {
|
|
246
320
|
state.stack.splice(index, 1);
|
|
321
|
+
releaseHistory(entry);
|
|
247
322
|
entry.dispose?.();
|
|
248
323
|
entry.container.remove();
|
|
249
324
|
entry.resolve?.(undefined);
|
|
@@ -251,12 +326,18 @@ export const createDialogCore = () => {
|
|
|
251
326
|
};
|
|
252
327
|
entry.resolve = value => {
|
|
253
328
|
options.signal?.removeEventListener("abort", abort);
|
|
254
|
-
|
|
329
|
+
if (entry.historyId === undefined) {
|
|
330
|
+
resolve(value);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
// After the history sync, so a caller that navigates or opens the next dialog finds the history settled.
|
|
334
|
+
queueMicrotask(() => void (returning ?? Promise.resolve()).then(() => resolve(value)));
|
|
255
335
|
};
|
|
256
336
|
const closeTyped = result => {
|
|
257
337
|
if (state.stack[state.stack.length - 1] !== entry) return;
|
|
258
338
|
popTop(result);
|
|
259
339
|
};
|
|
340
|
+
if (options.history) pushHistory(entry);
|
|
260
341
|
state.stack.push(entry);
|
|
261
342
|
try {
|
|
262
343
|
entry.dispose = render(() => view(closeTyped, {
|