rei-kit 0.16.0 → 0.17.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 +8 -8
- package/dist/{GoogleButton-BxsrxZdX.js → GoogleButton-DV_RIpJE.js} +3 -2
- package/dist/{GoogleButton-BxsrxZdX.js.map → GoogleButton-DV_RIpJE.js.map} +1 -1
- package/dist/{SettingsRow-MnVleeTR.js → SettingsRow-3lPRJbBZ.js} +2 -9
- package/dist/{SettingsRow-MnVleeTR.js.map → SettingsRow-3lPRJbBZ.js.map} +1 -1
- package/dist/_plugin-vue_export-helper-BOaGB7Aw.js +8 -0
- package/dist/app.js +4 -3
- package/dist/app.js.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/pwa.js +2 -1
- package/dist/pwa.js.map +1 -1
- package/dist/styles.css +243 -7
- package/dist/web/BaseAccordion.vue.d.ts +41 -0
- package/dist/web/BaseModal.vue.d.ts +69 -0
- package/dist/web/NavLinks.vue.d.ts +32 -0
- package/dist/web/index.d.ts +30 -0
- package/dist/web.js +247 -0
- package/dist/web.js.map +1 -0
- package/package.json +5 -1
package/dist/pwa.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { a as addDays, f as todayKey, i as needsIosInstall, r as isInstalled, t as SettingsGroup_default } from "./SettingsGroup-DtEB_Hrd.js";
|
|
2
|
-
import { n as
|
|
2
|
+
import { n as BaseButton_default, t as SettingsRow_default } from "./SettingsRow-3lPRJbBZ.js";
|
|
3
|
+
import { t as _plugin_vue_export_helper_default } from "./_plugin-vue_export-helper-BOaGB7Aw.js";
|
|
3
4
|
import { Transition, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, openBlock, ref, resolveDynamicComponent, toDisplayString, unref, withCtx } from "vue";
|
|
4
5
|
import { CheckCircle2, Download, RefreshCw, Share, X } from "lucide-vue-next";
|
|
5
6
|
//#region src/pwa/use-install.ts
|
package/dist/pwa.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pwa.js","names":["$emit"],"sources":["../src/pwa/use-install.ts","../src/pwa/use-snooze.ts","../src/pwa/InstallPrompt.vue","../src/pwa/InstallPrompt.vue","../src/pwa/InstallSettings.vue","../src/pwa/InstallSettings.vue","../src/pwa/UpdatePrompt.vue","../src/pwa/UpdatePrompt.vue"],"sourcesContent":["import { computed, ref } from 'vue'\n\nimport { isInstalled, needsIosInstall } from '../utils/platform'\n\n/**\n * The event Chromium fires when it decides the app is installable.\n *\n * Not in lib.dom yet, and it is the only way to trigger the install sheet from\n * the app's own button rather than the browser's.\n */\ninterface BeforeInstallPromptEvent extends Event {\n prompt: () => Promise<void>\n userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>\n}\n\nconst deferred = ref<BeforeInstallPromptEvent | null>(null)\nconst installed = ref(false)\n\nlet listening = false\n\n/**\n * Starts listening for the install event.\n *\n * **Call this from the app's entry file, not from a component.**\n * `beforeinstallprompt` fires once, early, and only once per page load — a\n * listener attached when a component mounts has usually already missed it.\n *\n * It is a function rather than module-scope side effects so the package can be\n * imported on a server: the phone apps this came from ran it at module scope\n * and the kit spent a release learning why that is not free.\n *\n * @example\n * ```ts\n * // main.ts\n * import { watchInstallability } from 'rei-kit/pwa'\n * watchInstallability()\n * ```\n */\nexport function watchInstallability(): void {\n if (listening || typeof window === 'undefined') return\n\n listening = true\n installed.value = isInstalled()\n\n window.addEventListener('beforeinstallprompt', (event) => {\n // Without this the browser shows its own bar, and then the app's card and\n // the browser's bar are both on screen saying the same thing.\n event.preventDefault()\n deferred.value = event as BeforeInstallPromptEvent\n })\n\n window.addEventListener('appinstalled', () => {\n installed.value = true\n deferred.value = null\n })\n}\n\n/**\n * Adding the app to the Home Screen.\n *\n * Three states, because the platforms genuinely differ: Chromium hands over an\n * event that can be triggered from a button, Safari on iOS has no API at all\n * and needs the user walked through Share → Add to Home Screen, and everything\n * else can only be told that installing is possible.\n *\n * @example\n * ```ts\n * const install = useInstall()\n * if (install.canPrompt.value) await install.prompt()\n * ```\n */\nexport function useInstall() {\n async function prompt(): Promise<boolean> {\n const event = deferred.value\n if (!event) return false\n\n await event.prompt()\n const { outcome } = await event.userChoice\n\n // The event is single-use: Chromium refuses a second prompt() on it.\n deferred.value = null\n\n return outcome === 'accepted'\n }\n\n return {\n isInstalled: computed(() => installed.value),\n /** A button can open the real install sheet. */\n canPrompt: computed(() => !installed.value && deferred.value !== null),\n /** No API — the user has to be shown the Share menu. */\n needsManualSteps: computed(() => !installed.value && needsIosInstall()),\n prompt,\n }\n}\n","import { computed, ref } from 'vue'\n\nimport { addDays, todayKey } from '../utils/date'\n\n/**\n * A nudge that stops asking for a while after it is dismissed.\n *\n * Both phone apps had this inside their install card, identically: a date in\n * `localStorage`, compared against today. Pulled out because the install card\n * is not the only thing that should stop asking — a notification nudge wants\n * exactly the same behaviour, and had exactly the same code.\n *\n * Storage is wrapped in try/catch on both sides. A private window or a browser\n * set to block site data throws on access, and the honest failure there is a\n * nudge that reappears next session rather than one that crashes the screen it\n * is asking from.\n *\n * @param key - Where the date is kept. Namespace it to the app.\n * @param days - How long to stay quiet after a dismissal.\n */\nexport function useSnooze(key: string, days = 7) {\n const read = (): string => {\n try {\n return localStorage.getItem(key) ?? ''\n } catch {\n return ''\n }\n }\n\n const until = ref(read())\n\n return {\n /** False while the nudge is snoozed. */\n isOver: computed(() => todayKey() >= until.value),\n\n /** Stop asking for `days`. */\n snooze(): void {\n const next = addDays(todayKey(), days)\n until.value = next\n\n try {\n localStorage.setItem(key, next)\n } catch {\n // Storage blocked; it reappears next session rather than never.\n }\n },\n }\n}\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { Download, Share } from 'lucide-vue-next'\n\nimport BaseButton from '../components/BaseButton.vue'\nimport { useInstall } from './use-install'\nimport { useSnooze } from './use-snooze'\n\n/**\n * The card that offers to install the app, and remembers being turned down.\n *\n * Both phone apps had this, 111 lines, differing in a storage key and a colour.\n * What is here is the part that was the same: the three-way platform check, the\n * week-long snooze, and the shape.\n *\n * Every string is a prop. The kit does not know a language, and \"Add to Home\n * Screen\" is a sentence about a platform, not about this component.\n */\nconst {\n storageKey,\n title,\n body,\n action,\n later,\n iosTitle = '',\n iosBody = '',\n snoozeDays = 7,\n} = defineProps<{\n /** Where the dismissal is remembered. Namespace it: `hibi-install-nudge`. */\n storageKey: string\n title: string\n body: string\n /** The install button. Not shown on iOS, which has no API to call. */\n action: string\n later: string\n /** Shown instead of `title` where the user has to use the Share menu. */\n iosTitle?: string | undefined\n iosBody?: string | undefined\n snoozeDays?: number | undefined\n}>()\n\nconst { canPrompt, needsManualSteps, prompt } = useInstall()\nconst { isOver, snooze } = useSnooze(storageKey, snoozeDays)\n\nconst visible = computed(() => (canPrompt.value || needsManualSteps.value) && isOver.value)\n\nasync function install() {\n await prompt()\n\n // Accepted or dismissed, stop asking for a week. The prompt is what gets\n // tiring, not the answer.\n snooze()\n}\n</script>\n\n<template>\n <Transition name=\"install\">\n <section\n v-if=\"visible\"\n class=\"border-positive/25 bg-positive/5 rounded-card flex gap-3 border p-3.5\"\n >\n <span\n class=\"bg-positive/15 text-positive flex size-10 shrink-0 items-center justify-center rounded-xl\"\n aria-hidden=\"true\"\n >\n <component :is=\"needsManualSteps ? Share : Download\" class=\"size-5\" />\n </span>\n\n <div class=\"flex min-w-0 flex-1 flex-col gap-2\">\n <div>\n <p class=\"text-ink text-sm font-semibold\">\n {{ needsManualSteps && iosTitle ? iosTitle : title }}\n </p>\n <p class=\"text-ink-soft mt-0.5 text-xs leading-relaxed\">\n {{ needsManualSteps && iosBody ? iosBody : body }}\n </p>\n </div>\n\n <div class=\"flex items-center gap-2\">\n <!-- No button on iOS: Safari exposes no way to open the Share sheet\n from script, so a button here could only fail. -->\n <BaseButton\n v-if=\"canPrompt\"\n variant=\"positive\"\n pill\n size=\"xs\"\n class=\"font-semibold\"\n @click=\"install\"\n >\n {{ action }}\n </BaseButton>\n\n <BaseButton pill size=\"xs\" variant=\"quiet\" @click=\"snooze\">{{ later }}</BaseButton>\n </div>\n </div>\n </section>\n </Transition>\n</template>\n\n<style scoped>\n.install-enter-active,\n.install-leave-active {\n transition:\n opacity 200ms ease,\n transform 200ms ease;\n}\n\n.install-enter-from,\n.install-leave-to {\n opacity: 0;\n transform: translateY(-4px);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .install-enter-from,\n .install-leave-to {\n transform: none;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { Download, Share } from 'lucide-vue-next'\n\nimport BaseButton from '../components/BaseButton.vue'\nimport { useInstall } from './use-install'\nimport { useSnooze } from './use-snooze'\n\n/**\n * The card that offers to install the app, and remembers being turned down.\n *\n * Both phone apps had this, 111 lines, differing in a storage key and a colour.\n * What is here is the part that was the same: the three-way platform check, the\n * week-long snooze, and the shape.\n *\n * Every string is a prop. The kit does not know a language, and \"Add to Home\n * Screen\" is a sentence about a platform, not about this component.\n */\nconst {\n storageKey,\n title,\n body,\n action,\n later,\n iosTitle = '',\n iosBody = '',\n snoozeDays = 7,\n} = defineProps<{\n /** Where the dismissal is remembered. Namespace it: `hibi-install-nudge`. */\n storageKey: string\n title: string\n body: string\n /** The install button. Not shown on iOS, which has no API to call. */\n action: string\n later: string\n /** Shown instead of `title` where the user has to use the Share menu. */\n iosTitle?: string | undefined\n iosBody?: string | undefined\n snoozeDays?: number | undefined\n}>()\n\nconst { canPrompt, needsManualSteps, prompt } = useInstall()\nconst { isOver, snooze } = useSnooze(storageKey, snoozeDays)\n\nconst visible = computed(() => (canPrompt.value || needsManualSteps.value) && isOver.value)\n\nasync function install() {\n await prompt()\n\n // Accepted or dismissed, stop asking for a week. The prompt is what gets\n // tiring, not the answer.\n snooze()\n}\n</script>\n\n<template>\n <Transition name=\"install\">\n <section\n v-if=\"visible\"\n class=\"border-positive/25 bg-positive/5 rounded-card flex gap-3 border p-3.5\"\n >\n <span\n class=\"bg-positive/15 text-positive flex size-10 shrink-0 items-center justify-center rounded-xl\"\n aria-hidden=\"true\"\n >\n <component :is=\"needsManualSteps ? Share : Download\" class=\"size-5\" />\n </span>\n\n <div class=\"flex min-w-0 flex-1 flex-col gap-2\">\n <div>\n <p class=\"text-ink text-sm font-semibold\">\n {{ needsManualSteps && iosTitle ? iosTitle : title }}\n </p>\n <p class=\"text-ink-soft mt-0.5 text-xs leading-relaxed\">\n {{ needsManualSteps && iosBody ? iosBody : body }}\n </p>\n </div>\n\n <div class=\"flex items-center gap-2\">\n <!-- No button on iOS: Safari exposes no way to open the Share sheet\n from script, so a button here could only fail. -->\n <BaseButton\n v-if=\"canPrompt\"\n variant=\"positive\"\n pill\n size=\"xs\"\n class=\"font-semibold\"\n @click=\"install\"\n >\n {{ action }}\n </BaseButton>\n\n <BaseButton pill size=\"xs\" variant=\"quiet\" @click=\"snooze\">{{ later }}</BaseButton>\n </div>\n </div>\n </section>\n </Transition>\n</template>\n\n<style scoped>\n.install-enter-active,\n.install-leave-active {\n transition:\n opacity 200ms ease,\n transform 200ms ease;\n}\n\n.install-enter-from,\n.install-leave-to {\n opacity: 0;\n transform: translateY(-4px);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .install-enter-from,\n .install-leave-to {\n transform: none;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { CheckCircle2, Download, Share } from 'lucide-vue-next'\n\nimport BaseButton from '../components/BaseButton.vue'\nimport SettingsGroup from '../components/SettingsGroup.vue'\nimport SettingsRow from '../components/SettingsRow.vue'\nimport { useInstall } from './use-install'\n\n/**\n * The way back to installing after the card has been dismissed.\n *\n * The card snoozes for a week; without this row, someone who tapped \"Not now\"\n * and then changed their mind would have nowhere to go. Both phone apps had it,\n * thirty-four lines, with no difference at all.\n *\n * It renders nothing where installing is neither possible nor already done — a\n * settings group that says \"you cannot install this\" is worse than silence.\n */\ndefineProps<{\n /** The group's heading. */\n title: string\n /** The row's label while installing is still possible. */\n label: string\n /** The row's label once it is installed. */\n installedLabel: string\n body: string\n /** Shown instead of `body` on iOS, where the user must use the Share menu. */\n iosBody: string\n /** The install button. Absent on iOS, which has no API to call. */\n action: string\n}>()\n\nconst { isInstalled, canPrompt, needsManualSteps, prompt } = useInstall()\n</script>\n\n<template>\n <SettingsGroup v-if=\"isInstalled || canPrompt || needsManualSteps\" :title=\"title\">\n <SettingsRow\n :label=\"isInstalled ? installedLabel : label\"\n :description=\"isInstalled ? '' : needsManualSteps ? iosBody : body\"\n :icon=\"isInstalled ? CheckCircle2 : needsManualSteps ? Share : Download\"\n stacked\n >\n <BaseButton v-if=\"canPrompt\" variant=\"primary\" size=\"sm\" class=\"self-start\" @click=\"prompt\">\n {{ action }}\n </BaseButton>\n </SettingsRow>\n </SettingsGroup>\n</template>\n","<script setup lang=\"ts\">\nimport { CheckCircle2, Download, Share } from 'lucide-vue-next'\n\nimport BaseButton from '../components/BaseButton.vue'\nimport SettingsGroup from '../components/SettingsGroup.vue'\nimport SettingsRow from '../components/SettingsRow.vue'\nimport { useInstall } from './use-install'\n\n/**\n * The way back to installing after the card has been dismissed.\n *\n * The card snoozes for a week; without this row, someone who tapped \"Not now\"\n * and then changed their mind would have nowhere to go. Both phone apps had it,\n * thirty-four lines, with no difference at all.\n *\n * It renders nothing where installing is neither possible nor already done — a\n * settings group that says \"you cannot install this\" is worse than silence.\n */\ndefineProps<{\n /** The group's heading. */\n title: string\n /** The row's label while installing is still possible. */\n label: string\n /** The row's label once it is installed. */\n installedLabel: string\n body: string\n /** Shown instead of `body` on iOS, where the user must use the Share menu. */\n iosBody: string\n /** The install button. Absent on iOS, which has no API to call. */\n action: string\n}>()\n\nconst { isInstalled, canPrompt, needsManualSteps, prompt } = useInstall()\n</script>\n\n<template>\n <SettingsGroup v-if=\"isInstalled || canPrompt || needsManualSteps\" :title=\"title\">\n <SettingsRow\n :label=\"isInstalled ? installedLabel : label\"\n :description=\"isInstalled ? '' : needsManualSteps ? iosBody : body\"\n :icon=\"isInstalled ? CheckCircle2 : needsManualSteps ? Share : Download\"\n stacked\n >\n <BaseButton v-if=\"canPrompt\" variant=\"primary\" size=\"sm\" class=\"self-start\" @click=\"prompt\">\n {{ action }}\n </BaseButton>\n </SettingsRow>\n </SettingsGroup>\n</template>\n","<script setup lang=\"ts\">\nimport { RefreshCw, X } from 'lucide-vue-next'\n\nimport BaseButton from '../components/BaseButton.vue'\n\n/**\n * The card that says a new version is waiting.\n *\n * `registerType: 'prompt'` means a new service worker waits rather than taking\n * over, and this is what asks whether to apply it. Asking rather than reloading\n * is the decision worth keeping: an automatic swap mid-sentence loses whatever\n * was being typed.\n *\n * **The service worker stays the app's.** `virtual:pwa-register/vue` is a\n * build-time module from `vite-plugin-pwa`, and a library cannot import one —\n * so the app owns `useRegisterSW` and hands the answer down as `open`. That\n * split is also honest: whether an update is waiting is the app's business,\n * and what the card looks like is this component's.\n *\n * @example\n * ```vue\n * <script setup>\n * const { needRefresh, updateServiceWorker } = useRegisterSW()\n * <\\/script>\n *\n * <UpdatePrompt\n * :open=\"needRefresh\"\n * :title=\"t('pwa.updateTitle')\"\n * :body=\"t('pwa.updateBody')\"\n * :action=\"t('pwa.reload')\"\n * :dismiss-label=\"t('pwa.later')\"\n * @update=\"updateServiceWorker(true)\"\n * @dismiss=\"needRefresh = false\"\n * />\n * ```\n */\ndefineProps<{\n open: boolean\n title: string\n body: string\n action: string\n /** The X's accessible name. An X on its own has none. */\n dismissLabel: string\n}>()\n\ndefineEmits<{ update: []; dismiss: [] }>()\n</script>\n\n<template>\n <Transition name=\"update\">\n <aside v-if=\"open\" class=\"update-card\" role=\"status\">\n <span class=\"update-icon\" aria-hidden=\"true\">\n <RefreshCw class=\"size-4\" />\n </span>\n\n <div class=\"min-w-0 flex-1\">\n <p class=\"text-ink text-sm font-semibold\">{{ title }}</p>\n <p class=\"text-ink-soft text-xs leading-snug\">{{ body }}</p>\n </div>\n\n <BaseButton\n variant=\"primary\"\n pill\n size=\"xs\"\n class=\"shrink-0 font-semibold\"\n @click=\"$emit('update')\"\n >\n {{ action }}\n </BaseButton>\n\n <BaseButton\n variant=\"quiet\"\n icon\n pill\n size=\"sm\"\n class=\"shrink-0\"\n :aria-label=\"dismissLabel\"\n @click=\"$emit('dismiss')\"\n >\n <X class=\"size-4\" />\n </BaseButton>\n </aside>\n </Transition>\n</template>\n\n<style scoped>\n/* Sits above the tab bar and the action button, because it outranks both — but\n absolutely inside its container, so on a desktop-sized shell it does not\n float off into the page. Give the shell `relative`. */\n.update-card {\n border-color: var(--color-hair);\n background: color-mix(in srgb, var(--color-surface) 95%, transparent);\n border-radius: var(--radius-card);\n position: absolute;\n left: 50%;\n z-index: 50;\n display: flex;\n width: 100%;\n max-width: 360px;\n align-items: center;\n gap: 0.75rem;\n border-width: 1px;\n padding: 0.75rem;\n backdrop-filter: blur(12px);\n box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1);\n transform: translateX(-50%);\n bottom: calc(1rem + env(safe-area-inset-bottom, 0px));\n}\n\n.update-icon {\n background: color-mix(in srgb, var(--color-primary) 15%, transparent);\n color: var(--color-primary);\n display: flex;\n height: 2.25rem;\n width: 2.25rem;\n flex-shrink: 0;\n align-items: center;\n justify-content: center;\n border-radius: 0.75rem;\n}\n\n.update-enter-active,\n.update-leave-active {\n transition:\n opacity 250ms ease,\n transform 250ms cubic-bezier(0.32, 0.72, 0, 1);\n}\n\n.update-enter-from,\n.update-leave-to {\n opacity: 0;\n transform: translate(-50%, 1rem);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .update-enter-from,\n .update-leave-to {\n transform: translate(-50%, 0);\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { RefreshCw, X } from 'lucide-vue-next'\n\nimport BaseButton from '../components/BaseButton.vue'\n\n/**\n * The card that says a new version is waiting.\n *\n * `registerType: 'prompt'` means a new service worker waits rather than taking\n * over, and this is what asks whether to apply it. Asking rather than reloading\n * is the decision worth keeping: an automatic swap mid-sentence loses whatever\n * was being typed.\n *\n * **The service worker stays the app's.** `virtual:pwa-register/vue` is a\n * build-time module from `vite-plugin-pwa`, and a library cannot import one —\n * so the app owns `useRegisterSW` and hands the answer down as `open`. That\n * split is also honest: whether an update is waiting is the app's business,\n * and what the card looks like is this component's.\n *\n * @example\n * ```vue\n * <script setup>\n * const { needRefresh, updateServiceWorker } = useRegisterSW()\n * <\\/script>\n *\n * <UpdatePrompt\n * :open=\"needRefresh\"\n * :title=\"t('pwa.updateTitle')\"\n * :body=\"t('pwa.updateBody')\"\n * :action=\"t('pwa.reload')\"\n * :dismiss-label=\"t('pwa.later')\"\n * @update=\"updateServiceWorker(true)\"\n * @dismiss=\"needRefresh = false\"\n * />\n * ```\n */\ndefineProps<{\n open: boolean\n title: string\n body: string\n action: string\n /** The X's accessible name. An X on its own has none. */\n dismissLabel: string\n}>()\n\ndefineEmits<{ update: []; dismiss: [] }>()\n</script>\n\n<template>\n <Transition name=\"update\">\n <aside v-if=\"open\" class=\"update-card\" role=\"status\">\n <span class=\"update-icon\" aria-hidden=\"true\">\n <RefreshCw class=\"size-4\" />\n </span>\n\n <div class=\"min-w-0 flex-1\">\n <p class=\"text-ink text-sm font-semibold\">{{ title }}</p>\n <p class=\"text-ink-soft text-xs leading-snug\">{{ body }}</p>\n </div>\n\n <BaseButton\n variant=\"primary\"\n pill\n size=\"xs\"\n class=\"shrink-0 font-semibold\"\n @click=\"$emit('update')\"\n >\n {{ action }}\n </BaseButton>\n\n <BaseButton\n variant=\"quiet\"\n icon\n pill\n size=\"sm\"\n class=\"shrink-0\"\n :aria-label=\"dismissLabel\"\n @click=\"$emit('dismiss')\"\n >\n <X class=\"size-4\" />\n </BaseButton>\n </aside>\n </Transition>\n</template>\n\n<style scoped>\n/* Sits above the tab bar and the action button, because it outranks both — but\n absolutely inside its container, so on a desktop-sized shell it does not\n float off into the page. Give the shell `relative`. */\n.update-card {\n border-color: var(--color-hair);\n background: color-mix(in srgb, var(--color-surface) 95%, transparent);\n border-radius: var(--radius-card);\n position: absolute;\n left: 50%;\n z-index: 50;\n display: flex;\n width: 100%;\n max-width: 360px;\n align-items: center;\n gap: 0.75rem;\n border-width: 1px;\n padding: 0.75rem;\n backdrop-filter: blur(12px);\n box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1);\n transform: translateX(-50%);\n bottom: calc(1rem + env(safe-area-inset-bottom, 0px));\n}\n\n.update-icon {\n background: color-mix(in srgb, var(--color-primary) 15%, transparent);\n color: var(--color-primary);\n display: flex;\n height: 2.25rem;\n width: 2.25rem;\n flex-shrink: 0;\n align-items: center;\n justify-content: center;\n border-radius: 0.75rem;\n}\n\n.update-enter-active,\n.update-leave-active {\n transition:\n opacity 250ms ease,\n transform 250ms cubic-bezier(0.32, 0.72, 0, 1);\n}\n\n.update-enter-from,\n.update-leave-to {\n opacity: 0;\n transform: translate(-50%, 1rem);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .update-enter-from,\n .update-leave-to {\n transform: translate(-50%, 0);\n }\n}\n</style>\n"],"mappings":";;;;;AAeA,IAAM,WAAW,IAAqC,IAAI;AAC1D,IAAM,YAAY,IAAI,KAAK;AAE3B,IAAI,YAAY;;;;;;;;;;;;;;;;;;;AAoBhB,SAAgB,sBAA4B;CAC1C,IAAI,aAAa,OAAO,WAAW,aAAa;CAEhD,YAAY;CACZ,UAAU,QAAQ,YAAY;CAE9B,OAAO,iBAAiB,wBAAwB,UAAU;EAGxD,MAAM,eAAe;EACrB,SAAS,QAAQ;CACnB,CAAC;CAED,OAAO,iBAAiB,sBAAsB;EAC5C,UAAU,QAAQ;EAClB,SAAS,QAAQ;CACnB,CAAC;AACH;;;;;;;;;;;;;;;AAgBA,SAAgB,aAAa;CAC3B,eAAe,SAA2B;EACxC,MAAM,QAAQ,SAAS;EACvB,IAAI,CAAC,OAAO,OAAO;EAEnB,MAAM,MAAM,OAAO;EACnB,MAAM,EAAE,YAAY,MAAM,MAAM;EAGhC,SAAS,QAAQ;EAEjB,OAAO,YAAY;CACrB;CAEA,OAAO;EACL,aAAa,eAAe,UAAU,KAAK;;EAE3C,WAAW,eAAe,CAAC,UAAU,SAAS,SAAS,UAAU,IAAI;;EAErE,kBAAkB,eAAe,CAAC,UAAU,SAAS,gBAAgB,CAAC;EACtE;CACF;AACF;;;;;;;;;;;;;;;;;;;ACzEA,SAAgB,UAAU,KAAa,OAAO,GAAG;CAC/C,MAAM,aAAqB;EACzB,IAAI;GACF,OAAO,aAAa,QAAQ,GAAG,KAAK;EACtC,QAAQ;GACN,OAAO;EACT;CACF;CAEA,MAAM,QAAQ,IAAI,KAAK,CAAC;CAExB,OAAO;;EAEL,QAAQ,eAAe,SAAS,KAAK,MAAM,KAAK;;EAGhD,SAAe;GACb,MAAM,OAAO,QAAQ,SAAS,GAAG,IAAI;GACrC,MAAM,QAAQ;GAEd,IAAI;IACF,aAAa,QAAQ,KAAK,IAAI;GAChC,QAAQ,CAER;EACF;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ECNA,MAAM,EAAE,WAAW,kBAAkB,WAAW,WAAW;EAC3D,MAAM,EAAE,QAAQ,WAAW,UAAU,QAAA,YAAY,QAAA,UAAU;EAE3D,MAAM,UAAU,gBAAgB,UAAU,SAAS,iBAAiB,UAAU,OAAO,KAAK;EAE1F,eAAe,UAAU;GACvB,MAAM,OAAO;GAIb,OAAO;EACT;;GAIE,OAAA,UAAA,GAAA,YAwCa,YAAA,EAxCD,MAAK,UAAS,GAAA;IACxB,SAAA,cAsCU,CArCF,QAAA,SADR,UAAA,GAAA,mBAsCU,WAtCV,cAsCU,CAlCR,mBAKO,QALP,cAKO,EADL,UAAA,GAAA,YAAsE,wBAAtD,MAAA,gBAAA,IAAmB,MAAA,KAAA,IAAQ,MAAA,QAAA,CAAQ,GAAA,EAAE,OAAM,SAAQ,CAAA,EAAA,CAAA,GAGrE,mBA0BM,OA1BN,cA0BM,CAzBJ,mBAOM,OAAA,MAAA,CANJ,mBAEI,KAFJ,cAEI,gBADC,MAAA,gBAAA,KAAoB,QAAA,WAAW,QAAA,WAAW,QAAA,KAAK,GAAA,CAAA,GAEpD,mBAEI,KAFJ,cAEI,gBADC,MAAA,gBAAA,KAAoB,QAAA,UAAU,QAAA,UAAU,QAAA,IAAI,GAAA,CAAA,CAAA,CAAA,GAInD,mBAeM,OAfN,YAeM,CAXI,MAAA,SAAA,KADR,UAAA,GAAA,YASa,oBAAA;;KAPX,SAAQ;KACR,MAAA;KACA,MAAK;KACL,OAAM;KACL,SAAO;;KAER,SAAA,cAAY,CAAT,gBAAA,gBAAA,QAAA,MAAM,GAAA,CAAA,CAAA,CAAA;;IAGX,CAAA,KAAA,mBAAA,IAAA,IAAA,GAAA,YAAmF,oBAAA;KAAvE,MAAA;KAAK,MAAK;KAAK,SAAQ;KAAS,SAAO,MAAA,MAAA;;KAAQ,SAAA,cAAW,CAAR,gBAAA,gBAAA,QAAA,KAAK,GAAA,CAAA,CAAA,CAAA;;;;;;;;;;;;;;;;;;;;;EE5D7E,MAAM,EAAE,aAAa,WAAW,kBAAkB,WAAW,WAAW;;GAIjD,OAAA,MAAA,WAAA,KAAe,MAAA,SAAA,KAAa,MAAA,gBAAA,KAAjD,UAAA,GAAA,YAWgB,uBAAA;;IAXoD,OAAO,QAAA;;IACzE,SAAA,cASc,CATd,YASc,qBAAA;KARX,OAAO,MAAA,WAAA,IAAc,QAAA,iBAAiB,QAAA;KACtC,aAAa,MAAA,WAAA,IAAW,KAAQ,MAAA,gBAAA,IAAmB,QAAA,UAAU,QAAA;KAC7D,MAAM,MAAA,WAAA,IAAc,MAAA,YAAA,IAAe,MAAA,gBAAA,IAAmB,MAAA,KAAA,IAAQ,MAAA,QAAA;KAC/D,SAAA;;KAEA,SAAA,cAEa,CAFK,MAAA,SAAA,KAAlB,UAAA,GAAA,YAEa,oBAAA;;MAFgB,SAAQ;MAAU,MAAK;MAAK,OAAM;MAAc,SAAO,MAAA,MAAA;;MAClF,SAAA,cAAY,CAAT,gBAAA,gBAAA,QAAA,MAAM,GAAA,CAAA,CAAA,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GEKf,OAAA,UAAA,GAAA,YAiCa,YAAA,EAjCD,MAAK,SAAQ,GAAA;IACvB,SAAA,cA+BQ,CA/BK,QAAA,QAAb,UAAA,GAAA,mBA+BQ,SA/BR,YA+BQ;KA9BN,mBAEO,QAFP,YAEO,CADL,YAA4B,MAAA,SAAA,GAAA,EAAjB,OAAM,SAAQ,CAAA,CAAA,CAAA;KAG3B,mBAGM,OAHN,YAGM,CAFJ,mBAAyD,KAAzD,YAAyD,gBAAZ,QAAA,KAAK,GAAA,CAAA,GAClD,mBAA4D,KAA5D,YAA4D,gBAAX,QAAA,IAAI,GAAA,CAAA,CAAA,CAAA;KAGvD,YAQa,oBAAA;MAPX,SAAQ;MACR,MAAA;MACA,MAAK;MACL,OAAM;MACL,SAAK,OAAA,OAAA,OAAA,MAAA,WAAEA,KAAAA,MAAK,QAAA;;MAEb,SAAA,cAAY,CAAT,gBAAA,gBAAA,QAAA,MAAM,GAAA,CAAA,CAAA,CAAA;;;KAGX,YAUa,oBAAA;MATX,SAAQ;MACR,MAAA;MACA,MAAA;MACA,MAAK;MACL,OAAM;MACL,cAAY,QAAA;MACZ,SAAK,OAAA,OAAA,OAAA,MAAA,WAAEA,KAAAA,MAAK,SAAA;;MAEb,SAAA,cAAoB,CAApB,YAAoB,MAAA,CAAA,GAAA,EAAjB,OAAM,SAAQ,CAAA,CAAA,CAAA"}
|
|
1
|
+
{"version":3,"file":"pwa.js","names":["$emit"],"sources":["../src/pwa/use-install.ts","../src/pwa/use-snooze.ts","../src/pwa/InstallPrompt.vue","../src/pwa/InstallPrompt.vue","../src/pwa/InstallSettings.vue","../src/pwa/InstallSettings.vue","../src/pwa/UpdatePrompt.vue","../src/pwa/UpdatePrompt.vue"],"sourcesContent":["import { computed, ref } from 'vue'\n\nimport { isInstalled, needsIosInstall } from '../utils/platform'\n\n/**\n * The event Chromium fires when it decides the app is installable.\n *\n * Not in lib.dom yet, and it is the only way to trigger the install sheet from\n * the app's own button rather than the browser's.\n */\ninterface BeforeInstallPromptEvent extends Event {\n prompt: () => Promise<void>\n userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>\n}\n\nconst deferred = ref<BeforeInstallPromptEvent | null>(null)\nconst installed = ref(false)\n\nlet listening = false\n\n/**\n * Starts listening for the install event.\n *\n * **Call this from the app's entry file, not from a component.**\n * `beforeinstallprompt` fires once, early, and only once per page load — a\n * listener attached when a component mounts has usually already missed it.\n *\n * It is a function rather than module-scope side effects so the package can be\n * imported on a server: the phone apps this came from ran it at module scope\n * and the kit spent a release learning why that is not free.\n *\n * @example\n * ```ts\n * // main.ts\n * import { watchInstallability } from 'rei-kit/pwa'\n * watchInstallability()\n * ```\n */\nexport function watchInstallability(): void {\n if (listening || typeof window === 'undefined') return\n\n listening = true\n installed.value = isInstalled()\n\n window.addEventListener('beforeinstallprompt', (event) => {\n // Without this the browser shows its own bar, and then the app's card and\n // the browser's bar are both on screen saying the same thing.\n event.preventDefault()\n deferred.value = event as BeforeInstallPromptEvent\n })\n\n window.addEventListener('appinstalled', () => {\n installed.value = true\n deferred.value = null\n })\n}\n\n/**\n * Adding the app to the Home Screen.\n *\n * Three states, because the platforms genuinely differ: Chromium hands over an\n * event that can be triggered from a button, Safari on iOS has no API at all\n * and needs the user walked through Share → Add to Home Screen, and everything\n * else can only be told that installing is possible.\n *\n * @example\n * ```ts\n * const install = useInstall()\n * if (install.canPrompt.value) await install.prompt()\n * ```\n */\nexport function useInstall() {\n async function prompt(): Promise<boolean> {\n const event = deferred.value\n if (!event) return false\n\n await event.prompt()\n const { outcome } = await event.userChoice\n\n // The event is single-use: Chromium refuses a second prompt() on it.\n deferred.value = null\n\n return outcome === 'accepted'\n }\n\n return {\n isInstalled: computed(() => installed.value),\n /** A button can open the real install sheet. */\n canPrompt: computed(() => !installed.value && deferred.value !== null),\n /** No API — the user has to be shown the Share menu. */\n needsManualSteps: computed(() => !installed.value && needsIosInstall()),\n prompt,\n }\n}\n","import { computed, ref } from 'vue'\n\nimport { addDays, todayKey } from '../utils/date'\n\n/**\n * A nudge that stops asking for a while after it is dismissed.\n *\n * Both phone apps had this inside their install card, identically: a date in\n * `localStorage`, compared against today. Pulled out because the install card\n * is not the only thing that should stop asking — a notification nudge wants\n * exactly the same behaviour, and had exactly the same code.\n *\n * Storage is wrapped in try/catch on both sides. A private window or a browser\n * set to block site data throws on access, and the honest failure there is a\n * nudge that reappears next session rather than one that crashes the screen it\n * is asking from.\n *\n * @param key - Where the date is kept. Namespace it to the app.\n * @param days - How long to stay quiet after a dismissal.\n */\nexport function useSnooze(key: string, days = 7) {\n const read = (): string => {\n try {\n return localStorage.getItem(key) ?? ''\n } catch {\n return ''\n }\n }\n\n const until = ref(read())\n\n return {\n /** False while the nudge is snoozed. */\n isOver: computed(() => todayKey() >= until.value),\n\n /** Stop asking for `days`. */\n snooze(): void {\n const next = addDays(todayKey(), days)\n until.value = next\n\n try {\n localStorage.setItem(key, next)\n } catch {\n // Storage blocked; it reappears next session rather than never.\n }\n },\n }\n}\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { Download, Share } from 'lucide-vue-next'\n\nimport BaseButton from '../components/BaseButton.vue'\nimport { useInstall } from './use-install'\nimport { useSnooze } from './use-snooze'\n\n/**\n * The card that offers to install the app, and remembers being turned down.\n *\n * Both phone apps had this, 111 lines, differing in a storage key and a colour.\n * What is here is the part that was the same: the three-way platform check, the\n * week-long snooze, and the shape.\n *\n * Every string is a prop. The kit does not know a language, and \"Add to Home\n * Screen\" is a sentence about a platform, not about this component.\n */\nconst {\n storageKey,\n title,\n body,\n action,\n later,\n iosTitle = '',\n iosBody = '',\n snoozeDays = 7,\n} = defineProps<{\n /** Where the dismissal is remembered. Namespace it: `hibi-install-nudge`. */\n storageKey: string\n title: string\n body: string\n /** The install button. Not shown on iOS, which has no API to call. */\n action: string\n later: string\n /** Shown instead of `title` where the user has to use the Share menu. */\n iosTitle?: string | undefined\n iosBody?: string | undefined\n snoozeDays?: number | undefined\n}>()\n\nconst { canPrompt, needsManualSteps, prompt } = useInstall()\nconst { isOver, snooze } = useSnooze(storageKey, snoozeDays)\n\nconst visible = computed(() => (canPrompt.value || needsManualSteps.value) && isOver.value)\n\nasync function install() {\n await prompt()\n\n // Accepted or dismissed, stop asking for a week. The prompt is what gets\n // tiring, not the answer.\n snooze()\n}\n</script>\n\n<template>\n <Transition name=\"install\">\n <section\n v-if=\"visible\"\n class=\"border-positive/25 bg-positive/5 rounded-card flex gap-3 border p-3.5\"\n >\n <span\n class=\"bg-positive/15 text-positive flex size-10 shrink-0 items-center justify-center rounded-xl\"\n aria-hidden=\"true\"\n >\n <component :is=\"needsManualSteps ? Share : Download\" class=\"size-5\" />\n </span>\n\n <div class=\"flex min-w-0 flex-1 flex-col gap-2\">\n <div>\n <p class=\"text-ink text-sm font-semibold\">\n {{ needsManualSteps && iosTitle ? iosTitle : title }}\n </p>\n <p class=\"text-ink-soft mt-0.5 text-xs leading-relaxed\">\n {{ needsManualSteps && iosBody ? iosBody : body }}\n </p>\n </div>\n\n <div class=\"flex items-center gap-2\">\n <!-- No button on iOS: Safari exposes no way to open the Share sheet\n from script, so a button here could only fail. -->\n <BaseButton\n v-if=\"canPrompt\"\n variant=\"positive\"\n pill\n size=\"xs\"\n class=\"font-semibold\"\n @click=\"install\"\n >\n {{ action }}\n </BaseButton>\n\n <BaseButton pill size=\"xs\" variant=\"quiet\" @click=\"snooze\">{{ later }}</BaseButton>\n </div>\n </div>\n </section>\n </Transition>\n</template>\n\n<style scoped>\n.install-enter-active,\n.install-leave-active {\n transition:\n opacity 200ms ease,\n transform 200ms ease;\n}\n\n.install-enter-from,\n.install-leave-to {\n opacity: 0;\n transform: translateY(-4px);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .install-enter-from,\n .install-leave-to {\n transform: none;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { Download, Share } from 'lucide-vue-next'\n\nimport BaseButton from '../components/BaseButton.vue'\nimport { useInstall } from './use-install'\nimport { useSnooze } from './use-snooze'\n\n/**\n * The card that offers to install the app, and remembers being turned down.\n *\n * Both phone apps had this, 111 lines, differing in a storage key and a colour.\n * What is here is the part that was the same: the three-way platform check, the\n * week-long snooze, and the shape.\n *\n * Every string is a prop. The kit does not know a language, and \"Add to Home\n * Screen\" is a sentence about a platform, not about this component.\n */\nconst {\n storageKey,\n title,\n body,\n action,\n later,\n iosTitle = '',\n iosBody = '',\n snoozeDays = 7,\n} = defineProps<{\n /** Where the dismissal is remembered. Namespace it: `hibi-install-nudge`. */\n storageKey: string\n title: string\n body: string\n /** The install button. Not shown on iOS, which has no API to call. */\n action: string\n later: string\n /** Shown instead of `title` where the user has to use the Share menu. */\n iosTitle?: string | undefined\n iosBody?: string | undefined\n snoozeDays?: number | undefined\n}>()\n\nconst { canPrompt, needsManualSteps, prompt } = useInstall()\nconst { isOver, snooze } = useSnooze(storageKey, snoozeDays)\n\nconst visible = computed(() => (canPrompt.value || needsManualSteps.value) && isOver.value)\n\nasync function install() {\n await prompt()\n\n // Accepted or dismissed, stop asking for a week. The prompt is what gets\n // tiring, not the answer.\n snooze()\n}\n</script>\n\n<template>\n <Transition name=\"install\">\n <section\n v-if=\"visible\"\n class=\"border-positive/25 bg-positive/5 rounded-card flex gap-3 border p-3.5\"\n >\n <span\n class=\"bg-positive/15 text-positive flex size-10 shrink-0 items-center justify-center rounded-xl\"\n aria-hidden=\"true\"\n >\n <component :is=\"needsManualSteps ? Share : Download\" class=\"size-5\" />\n </span>\n\n <div class=\"flex min-w-0 flex-1 flex-col gap-2\">\n <div>\n <p class=\"text-ink text-sm font-semibold\">\n {{ needsManualSteps && iosTitle ? iosTitle : title }}\n </p>\n <p class=\"text-ink-soft mt-0.5 text-xs leading-relaxed\">\n {{ needsManualSteps && iosBody ? iosBody : body }}\n </p>\n </div>\n\n <div class=\"flex items-center gap-2\">\n <!-- No button on iOS: Safari exposes no way to open the Share sheet\n from script, so a button here could only fail. -->\n <BaseButton\n v-if=\"canPrompt\"\n variant=\"positive\"\n pill\n size=\"xs\"\n class=\"font-semibold\"\n @click=\"install\"\n >\n {{ action }}\n </BaseButton>\n\n <BaseButton pill size=\"xs\" variant=\"quiet\" @click=\"snooze\">{{ later }}</BaseButton>\n </div>\n </div>\n </section>\n </Transition>\n</template>\n\n<style scoped>\n.install-enter-active,\n.install-leave-active {\n transition:\n opacity 200ms ease,\n transform 200ms ease;\n}\n\n.install-enter-from,\n.install-leave-to {\n opacity: 0;\n transform: translateY(-4px);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .install-enter-from,\n .install-leave-to {\n transform: none;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { CheckCircle2, Download, Share } from 'lucide-vue-next'\n\nimport BaseButton from '../components/BaseButton.vue'\nimport SettingsGroup from '../components/SettingsGroup.vue'\nimport SettingsRow from '../components/SettingsRow.vue'\nimport { useInstall } from './use-install'\n\n/**\n * The way back to installing after the card has been dismissed.\n *\n * The card snoozes for a week; without this row, someone who tapped \"Not now\"\n * and then changed their mind would have nowhere to go. Both phone apps had it,\n * thirty-four lines, with no difference at all.\n *\n * It renders nothing where installing is neither possible nor already done — a\n * settings group that says \"you cannot install this\" is worse than silence.\n */\ndefineProps<{\n /** The group's heading. */\n title: string\n /** The row's label while installing is still possible. */\n label: string\n /** The row's label once it is installed. */\n installedLabel: string\n body: string\n /** Shown instead of `body` on iOS, where the user must use the Share menu. */\n iosBody: string\n /** The install button. Absent on iOS, which has no API to call. */\n action: string\n}>()\n\nconst { isInstalled, canPrompt, needsManualSteps, prompt } = useInstall()\n</script>\n\n<template>\n <SettingsGroup v-if=\"isInstalled || canPrompt || needsManualSteps\" :title=\"title\">\n <SettingsRow\n :label=\"isInstalled ? installedLabel : label\"\n :description=\"isInstalled ? '' : needsManualSteps ? iosBody : body\"\n :icon=\"isInstalled ? CheckCircle2 : needsManualSteps ? Share : Download\"\n stacked\n >\n <BaseButton v-if=\"canPrompt\" variant=\"primary\" size=\"sm\" class=\"self-start\" @click=\"prompt\">\n {{ action }}\n </BaseButton>\n </SettingsRow>\n </SettingsGroup>\n</template>\n","<script setup lang=\"ts\">\nimport { CheckCircle2, Download, Share } from 'lucide-vue-next'\n\nimport BaseButton from '../components/BaseButton.vue'\nimport SettingsGroup from '../components/SettingsGroup.vue'\nimport SettingsRow from '../components/SettingsRow.vue'\nimport { useInstall } from './use-install'\n\n/**\n * The way back to installing after the card has been dismissed.\n *\n * The card snoozes for a week; without this row, someone who tapped \"Not now\"\n * and then changed their mind would have nowhere to go. Both phone apps had it,\n * thirty-four lines, with no difference at all.\n *\n * It renders nothing where installing is neither possible nor already done — a\n * settings group that says \"you cannot install this\" is worse than silence.\n */\ndefineProps<{\n /** The group's heading. */\n title: string\n /** The row's label while installing is still possible. */\n label: string\n /** The row's label once it is installed. */\n installedLabel: string\n body: string\n /** Shown instead of `body` on iOS, where the user must use the Share menu. */\n iosBody: string\n /** The install button. Absent on iOS, which has no API to call. */\n action: string\n}>()\n\nconst { isInstalled, canPrompt, needsManualSteps, prompt } = useInstall()\n</script>\n\n<template>\n <SettingsGroup v-if=\"isInstalled || canPrompt || needsManualSteps\" :title=\"title\">\n <SettingsRow\n :label=\"isInstalled ? installedLabel : label\"\n :description=\"isInstalled ? '' : needsManualSteps ? iosBody : body\"\n :icon=\"isInstalled ? CheckCircle2 : needsManualSteps ? Share : Download\"\n stacked\n >\n <BaseButton v-if=\"canPrompt\" variant=\"primary\" size=\"sm\" class=\"self-start\" @click=\"prompt\">\n {{ action }}\n </BaseButton>\n </SettingsRow>\n </SettingsGroup>\n</template>\n","<script setup lang=\"ts\">\nimport { RefreshCw, X } from 'lucide-vue-next'\n\nimport BaseButton from '../components/BaseButton.vue'\n\n/**\n * The card that says a new version is waiting.\n *\n * `registerType: 'prompt'` means a new service worker waits rather than taking\n * over, and this is what asks whether to apply it. Asking rather than reloading\n * is the decision worth keeping: an automatic swap mid-sentence loses whatever\n * was being typed.\n *\n * **The service worker stays the app's.** `virtual:pwa-register/vue` is a\n * build-time module from `vite-plugin-pwa`, and a library cannot import one —\n * so the app owns `useRegisterSW` and hands the answer down as `open`. That\n * split is also honest: whether an update is waiting is the app's business,\n * and what the card looks like is this component's.\n *\n * @example\n * ```vue\n * <script setup>\n * const { needRefresh, updateServiceWorker } = useRegisterSW()\n * <\\/script>\n *\n * <UpdatePrompt\n * :open=\"needRefresh\"\n * :title=\"t('pwa.updateTitle')\"\n * :body=\"t('pwa.updateBody')\"\n * :action=\"t('pwa.reload')\"\n * :dismiss-label=\"t('pwa.later')\"\n * @update=\"updateServiceWorker(true)\"\n * @dismiss=\"needRefresh = false\"\n * />\n * ```\n */\ndefineProps<{\n open: boolean\n title: string\n body: string\n action: string\n /** The X's accessible name. An X on its own has none. */\n dismissLabel: string\n}>()\n\ndefineEmits<{ update: []; dismiss: [] }>()\n</script>\n\n<template>\n <Transition name=\"update\">\n <aside v-if=\"open\" class=\"update-card\" role=\"status\">\n <span class=\"update-icon\" aria-hidden=\"true\">\n <RefreshCw class=\"size-4\" />\n </span>\n\n <div class=\"min-w-0 flex-1\">\n <p class=\"text-ink text-sm font-semibold\">{{ title }}</p>\n <p class=\"text-ink-soft text-xs leading-snug\">{{ body }}</p>\n </div>\n\n <BaseButton\n variant=\"primary\"\n pill\n size=\"xs\"\n class=\"shrink-0 font-semibold\"\n @click=\"$emit('update')\"\n >\n {{ action }}\n </BaseButton>\n\n <BaseButton\n variant=\"quiet\"\n icon\n pill\n size=\"sm\"\n class=\"shrink-0\"\n :aria-label=\"dismissLabel\"\n @click=\"$emit('dismiss')\"\n >\n <X class=\"size-4\" />\n </BaseButton>\n </aside>\n </Transition>\n</template>\n\n<style scoped>\n/* Sits above the tab bar and the action button, because it outranks both — but\n absolutely inside its container, so on a desktop-sized shell it does not\n float off into the page. Give the shell `relative`. */\n.update-card {\n border-color: var(--color-hair);\n background: color-mix(in srgb, var(--color-surface) 95%, transparent);\n border-radius: var(--radius-card);\n position: absolute;\n left: 50%;\n z-index: 50;\n display: flex;\n width: 100%;\n max-width: 360px;\n align-items: center;\n gap: 0.75rem;\n border-width: 1px;\n padding: 0.75rem;\n backdrop-filter: blur(12px);\n box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1);\n transform: translateX(-50%);\n bottom: calc(1rem + env(safe-area-inset-bottom, 0px));\n}\n\n.update-icon {\n background: color-mix(in srgb, var(--color-primary) 15%, transparent);\n color: var(--color-primary);\n display: flex;\n height: 2.25rem;\n width: 2.25rem;\n flex-shrink: 0;\n align-items: center;\n justify-content: center;\n border-radius: 0.75rem;\n}\n\n.update-enter-active,\n.update-leave-active {\n transition:\n opacity 250ms ease,\n transform 250ms cubic-bezier(0.32, 0.72, 0, 1);\n}\n\n.update-enter-from,\n.update-leave-to {\n opacity: 0;\n transform: translate(-50%, 1rem);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .update-enter-from,\n .update-leave-to {\n transform: translate(-50%, 0);\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { RefreshCw, X } from 'lucide-vue-next'\n\nimport BaseButton from '../components/BaseButton.vue'\n\n/**\n * The card that says a new version is waiting.\n *\n * `registerType: 'prompt'` means a new service worker waits rather than taking\n * over, and this is what asks whether to apply it. Asking rather than reloading\n * is the decision worth keeping: an automatic swap mid-sentence loses whatever\n * was being typed.\n *\n * **The service worker stays the app's.** `virtual:pwa-register/vue` is a\n * build-time module from `vite-plugin-pwa`, and a library cannot import one —\n * so the app owns `useRegisterSW` and hands the answer down as `open`. That\n * split is also honest: whether an update is waiting is the app's business,\n * and what the card looks like is this component's.\n *\n * @example\n * ```vue\n * <script setup>\n * const { needRefresh, updateServiceWorker } = useRegisterSW()\n * <\\/script>\n *\n * <UpdatePrompt\n * :open=\"needRefresh\"\n * :title=\"t('pwa.updateTitle')\"\n * :body=\"t('pwa.updateBody')\"\n * :action=\"t('pwa.reload')\"\n * :dismiss-label=\"t('pwa.later')\"\n * @update=\"updateServiceWorker(true)\"\n * @dismiss=\"needRefresh = false\"\n * />\n * ```\n */\ndefineProps<{\n open: boolean\n title: string\n body: string\n action: string\n /** The X's accessible name. An X on its own has none. */\n dismissLabel: string\n}>()\n\ndefineEmits<{ update: []; dismiss: [] }>()\n</script>\n\n<template>\n <Transition name=\"update\">\n <aside v-if=\"open\" class=\"update-card\" role=\"status\">\n <span class=\"update-icon\" aria-hidden=\"true\">\n <RefreshCw class=\"size-4\" />\n </span>\n\n <div class=\"min-w-0 flex-1\">\n <p class=\"text-ink text-sm font-semibold\">{{ title }}</p>\n <p class=\"text-ink-soft text-xs leading-snug\">{{ body }}</p>\n </div>\n\n <BaseButton\n variant=\"primary\"\n pill\n size=\"xs\"\n class=\"shrink-0 font-semibold\"\n @click=\"$emit('update')\"\n >\n {{ action }}\n </BaseButton>\n\n <BaseButton\n variant=\"quiet\"\n icon\n pill\n size=\"sm\"\n class=\"shrink-0\"\n :aria-label=\"dismissLabel\"\n @click=\"$emit('dismiss')\"\n >\n <X class=\"size-4\" />\n </BaseButton>\n </aside>\n </Transition>\n</template>\n\n<style scoped>\n/* Sits above the tab bar and the action button, because it outranks both — but\n absolutely inside its container, so on a desktop-sized shell it does not\n float off into the page. Give the shell `relative`. */\n.update-card {\n border-color: var(--color-hair);\n background: color-mix(in srgb, var(--color-surface) 95%, transparent);\n border-radius: var(--radius-card);\n position: absolute;\n left: 50%;\n z-index: 50;\n display: flex;\n width: 100%;\n max-width: 360px;\n align-items: center;\n gap: 0.75rem;\n border-width: 1px;\n padding: 0.75rem;\n backdrop-filter: blur(12px);\n box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1);\n transform: translateX(-50%);\n bottom: calc(1rem + env(safe-area-inset-bottom, 0px));\n}\n\n.update-icon {\n background: color-mix(in srgb, var(--color-primary) 15%, transparent);\n color: var(--color-primary);\n display: flex;\n height: 2.25rem;\n width: 2.25rem;\n flex-shrink: 0;\n align-items: center;\n justify-content: center;\n border-radius: 0.75rem;\n}\n\n.update-enter-active,\n.update-leave-active {\n transition:\n opacity 250ms ease,\n transform 250ms cubic-bezier(0.32, 0.72, 0, 1);\n}\n\n.update-enter-from,\n.update-leave-to {\n opacity: 0;\n transform: translate(-50%, 1rem);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .update-enter-from,\n .update-leave-to {\n transform: translate(-50%, 0);\n }\n}\n</style>\n"],"mappings":";;;;;;AAeA,IAAM,WAAW,IAAqC,IAAI;AAC1D,IAAM,YAAY,IAAI,KAAK;AAE3B,IAAI,YAAY;;;;;;;;;;;;;;;;;;;AAoBhB,SAAgB,sBAA4B;CAC1C,IAAI,aAAa,OAAO,WAAW,aAAa;CAEhD,YAAY;CACZ,UAAU,QAAQ,YAAY;CAE9B,OAAO,iBAAiB,wBAAwB,UAAU;EAGxD,MAAM,eAAe;EACrB,SAAS,QAAQ;CACnB,CAAC;CAED,OAAO,iBAAiB,sBAAsB;EAC5C,UAAU,QAAQ;EAClB,SAAS,QAAQ;CACnB,CAAC;AACH;;;;;;;;;;;;;;;AAgBA,SAAgB,aAAa;CAC3B,eAAe,SAA2B;EACxC,MAAM,QAAQ,SAAS;EACvB,IAAI,CAAC,OAAO,OAAO;EAEnB,MAAM,MAAM,OAAO;EACnB,MAAM,EAAE,YAAY,MAAM,MAAM;EAGhC,SAAS,QAAQ;EAEjB,OAAO,YAAY;CACrB;CAEA,OAAO;EACL,aAAa,eAAe,UAAU,KAAK;;EAE3C,WAAW,eAAe,CAAC,UAAU,SAAS,SAAS,UAAU,IAAI;;EAErE,kBAAkB,eAAe,CAAC,UAAU,SAAS,gBAAgB,CAAC;EACtE;CACF;AACF;;;;;;;;;;;;;;;;;;;ACzEA,SAAgB,UAAU,KAAa,OAAO,GAAG;CAC/C,MAAM,aAAqB;EACzB,IAAI;GACF,OAAO,aAAa,QAAQ,GAAG,KAAK;EACtC,QAAQ;GACN,OAAO;EACT;CACF;CAEA,MAAM,QAAQ,IAAI,KAAK,CAAC;CAExB,OAAO;;EAEL,QAAQ,eAAe,SAAS,KAAK,MAAM,KAAK;;EAGhD,SAAe;GACb,MAAM,OAAO,QAAQ,SAAS,GAAG,IAAI;GACrC,MAAM,QAAQ;GAEd,IAAI;IACF,aAAa,QAAQ,KAAK,IAAI;GAChC,QAAQ,CAER;EACF;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ECNA,MAAM,EAAE,WAAW,kBAAkB,WAAW,WAAW;EAC3D,MAAM,EAAE,QAAQ,WAAW,UAAU,QAAA,YAAY,QAAA,UAAU;EAE3D,MAAM,UAAU,gBAAgB,UAAU,SAAS,iBAAiB,UAAU,OAAO,KAAK;EAE1F,eAAe,UAAU;GACvB,MAAM,OAAO;GAIb,OAAO;EACT;;GAIE,OAAA,UAAA,GAAA,YAwCa,YAAA,EAxCD,MAAK,UAAS,GAAA;IACxB,SAAA,cAsCU,CArCF,QAAA,SADR,UAAA,GAAA,mBAsCU,WAtCV,cAsCU,CAlCR,mBAKO,QALP,cAKO,EADL,UAAA,GAAA,YAAsE,wBAAtD,MAAA,gBAAA,IAAmB,MAAA,KAAA,IAAQ,MAAA,QAAA,CAAQ,GAAA,EAAE,OAAM,SAAQ,CAAA,EAAA,CAAA,GAGrE,mBA0BM,OA1BN,cA0BM,CAzBJ,mBAOM,OAAA,MAAA,CANJ,mBAEI,KAFJ,cAEI,gBADC,MAAA,gBAAA,KAAoB,QAAA,WAAW,QAAA,WAAW,QAAA,KAAK,GAAA,CAAA,GAEpD,mBAEI,KAFJ,cAEI,gBADC,MAAA,gBAAA,KAAoB,QAAA,UAAU,QAAA,UAAU,QAAA,IAAI,GAAA,CAAA,CAAA,CAAA,GAInD,mBAeM,OAfN,YAeM,CAXI,MAAA,SAAA,KADR,UAAA,GAAA,YASa,oBAAA;;KAPX,SAAQ;KACR,MAAA;KACA,MAAK;KACL,OAAM;KACL,SAAO;;KAER,SAAA,cAAY,CAAT,gBAAA,gBAAA,QAAA,MAAM,GAAA,CAAA,CAAA,CAAA;;IAGX,CAAA,KAAA,mBAAA,IAAA,IAAA,GAAA,YAAmF,oBAAA;KAAvE,MAAA;KAAK,MAAK;KAAK,SAAQ;KAAS,SAAO,MAAA,MAAA;;KAAQ,SAAA,cAAW,CAAR,gBAAA,gBAAA,QAAA,KAAK,GAAA,CAAA,CAAA,CAAA;;;;;;;;;;;;;;;;;;;;;EE5D7E,MAAM,EAAE,aAAa,WAAW,kBAAkB,WAAW,WAAW;;GAIjD,OAAA,MAAA,WAAA,KAAe,MAAA,SAAA,KAAa,MAAA,gBAAA,KAAjD,UAAA,GAAA,YAWgB,uBAAA;;IAXoD,OAAO,QAAA;;IACzE,SAAA,cASc,CATd,YASc,qBAAA;KARX,OAAO,MAAA,WAAA,IAAc,QAAA,iBAAiB,QAAA;KACtC,aAAa,MAAA,WAAA,IAAW,KAAQ,MAAA,gBAAA,IAAmB,QAAA,UAAU,QAAA;KAC7D,MAAM,MAAA,WAAA,IAAc,MAAA,YAAA,IAAe,MAAA,gBAAA,IAAmB,MAAA,KAAA,IAAQ,MAAA,QAAA;KAC/D,SAAA;;KAEA,SAAA,cAEa,CAFK,MAAA,SAAA,KAAlB,UAAA,GAAA,YAEa,oBAAA;;MAFgB,SAAQ;MAAU,MAAK;MAAK,OAAM;MAAc,SAAO,MAAA,MAAA;;MAClF,SAAA,cAAY,CAAT,gBAAA,gBAAA,QAAA,MAAM,GAAA,CAAA,CAAA,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GEKf,OAAA,UAAA,GAAA,YAiCa,YAAA,EAjCD,MAAK,SAAQ,GAAA;IACvB,SAAA,cA+BQ,CA/BK,QAAA,QAAb,UAAA,GAAA,mBA+BQ,SA/BR,YA+BQ;KA9BN,mBAEO,QAFP,YAEO,CADL,YAA4B,MAAA,SAAA,GAAA,EAAjB,OAAM,SAAQ,CAAA,CAAA,CAAA;KAG3B,mBAGM,OAHN,YAGM,CAFJ,mBAAyD,KAAzD,YAAyD,gBAAZ,QAAA,KAAK,GAAA,CAAA,GAClD,mBAA4D,KAA5D,YAA4D,gBAAX,QAAA,IAAI,GAAA,CAAA,CAAA,CAAA;KAGvD,YAQa,oBAAA;MAPX,SAAQ;MACR,MAAA;MACA,MAAK;MACL,OAAM;MACL,SAAK,OAAA,OAAA,OAAA,MAAA,WAAEA,KAAAA,MAAK,QAAA;;MAEb,SAAA,cAAY,CAAT,gBAAA,gBAAA,QAAA,MAAM,GAAA,CAAA,CAAA,CAAA;;;KAGX,YAUa,oBAAA;MATX,SAAQ;MACR,MAAA;MACA,MAAA;MACA,MAAK;MACL,OAAM;MACL,cAAY,QAAA;MACZ,SAAK,OAAA,OAAA,OAAA,MAAA,WAAEA,KAAAA,MAAK,SAAA;;MAEb,SAAA,cAAoB,CAApB,YAAoB,MAAA,CAAA,GAAA,EAAjB,OAAM,SAAQ,CAAA,CAAA,CAAA"}
|
package/dist/styles.css
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
.rk-fab-slot[data-v-
|
|
28
|
+
.rk-fab-slot[data-v-7e2bbe71] {
|
|
29
29
|
position: absolute;
|
|
30
30
|
bottom: calc(6rem + env(safe-area-inset-bottom, 0px));
|
|
31
31
|
left: 50%;
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
must not swallow taps meant for the page underneath it. */
|
|
41
41
|
pointer-events: none;
|
|
42
42
|
}
|
|
43
|
-
.rk-fab[data-v-
|
|
43
|
+
.rk-fab[data-v-7e2bbe71] {
|
|
44
44
|
pointer-events: auto;
|
|
45
45
|
display: flex;
|
|
46
46
|
height: 3rem;
|
|
@@ -57,26 +57,35 @@
|
|
|
57
57
|
0 0 0 4px var(--color-canvas),
|
|
58
58
|
0 10px 24px -8px color-mix(in srgb, var(--color-primary) 60%, transparent);
|
|
59
59
|
}
|
|
60
|
-
.rk-fab[data-v-
|
|
60
|
+
.rk-fab[data-v-7e2bbe71]:active {
|
|
61
61
|
transform: scale(0.95);
|
|
62
62
|
}
|
|
63
|
-
|
|
63
|
+
|
|
64
|
+
/* The ring is not decoration. This button was a `BaseButton variant="unstyled"`
|
|
65
|
+
in the app it came from, which meant it inherited a focus ring for free; a
|
|
66
|
+
bare <button> here would have dropped it, and a keyboard user would have had
|
|
67
|
+
no way to see where they were. */
|
|
68
|
+
.rk-fab[data-v-7e2bbe71]:focus-visible {
|
|
69
|
+
outline: 2px solid var(--color-primary);
|
|
70
|
+
outline-offset: 2px;
|
|
71
|
+
}
|
|
72
|
+
.rk-fab-icon[data-v-7e2bbe71] {
|
|
64
73
|
display: grid;
|
|
65
74
|
place-items: center;
|
|
66
75
|
flex-shrink: 0;
|
|
67
76
|
}
|
|
68
|
-
.rk-fab-icon[data-v-
|
|
77
|
+
.rk-fab-icon[data-v-7e2bbe71] svg {
|
|
69
78
|
width: 1.25rem;
|
|
70
79
|
height: 1.25rem;
|
|
71
80
|
stroke-width: 2.5px;
|
|
72
81
|
}
|
|
73
|
-
.rk-fab-label[data-v-
|
|
82
|
+
.rk-fab-label[data-v-7e2bbe71] {
|
|
74
83
|
font-size: 0.875rem;
|
|
75
84
|
font-weight: 600;
|
|
76
85
|
white-space: nowrap;
|
|
77
86
|
}
|
|
78
87
|
@media (prefers-reduced-motion: reduce) {
|
|
79
|
-
.rk-fab[data-v-
|
|
88
|
+
.rk-fab[data-v-7e2bbe71] {
|
|
80
89
|
transition: none;
|
|
81
90
|
}
|
|
82
91
|
}
|
|
@@ -607,4 +616,231 @@
|
|
|
607
616
|
transform: translate(-50%, 0);
|
|
608
617
|
}
|
|
609
618
|
}
|
|
619
|
+
|
|
620
|
+
.rk-accordion[data-v-00091812] {
|
|
621
|
+
border-top: 1px solid color-mix(in srgb, var(--color-hair) 70%, transparent);
|
|
622
|
+
}
|
|
623
|
+
.rk-accordion-item[data-v-00091812] {
|
|
624
|
+
border-bottom: 1px solid color-mix(in srgb, var(--color-hair) 70%, transparent);
|
|
625
|
+
}
|
|
626
|
+
.rk-accordion-control[data-v-00091812] {
|
|
627
|
+
display: flex;
|
|
628
|
+
width: 100%;
|
|
629
|
+
cursor: pointer;
|
|
630
|
+
align-items: center;
|
|
631
|
+
justify-content: space-between;
|
|
632
|
+
gap: 1.5rem;
|
|
633
|
+
padding: 1.25rem 0;
|
|
634
|
+
text-align: left;
|
|
635
|
+
transition: color 150ms;
|
|
636
|
+
}
|
|
637
|
+
.rk-accordion-control[data-v-00091812]:hover {
|
|
638
|
+
color: var(--color-primary);
|
|
639
|
+
}
|
|
640
|
+
.rk-accordion-control[data-v-00091812]:focus-visible {
|
|
641
|
+
outline: 2px solid var(--color-primary);
|
|
642
|
+
outline-offset: 2px;
|
|
643
|
+
}
|
|
644
|
+
.rk-accordion-title[data-v-00091812] {
|
|
645
|
+
color: var(--color-ink);
|
|
646
|
+
font-size: 1rem;
|
|
647
|
+
font-weight: 500;
|
|
648
|
+
}
|
|
649
|
+
.rk-accordion-mark[data-v-00091812] {
|
|
650
|
+
position: relative;
|
|
651
|
+
display: grid;
|
|
652
|
+
place-items: center;
|
|
653
|
+
flex-shrink: 0;
|
|
654
|
+
width: 1.5rem;
|
|
655
|
+
height: 1.5rem;
|
|
656
|
+
}
|
|
657
|
+
.rk-accordion-bar[data-v-00091812] {
|
|
658
|
+
position: absolute;
|
|
659
|
+
height: 1px;
|
|
660
|
+
width: 0.875rem;
|
|
661
|
+
background: var(--color-ink-soft);
|
|
662
|
+
}
|
|
663
|
+
.rk-accordion-bar-turn[data-v-00091812] {
|
|
664
|
+
transform: rotate(90deg);
|
|
665
|
+
transition: transform 420ms ease-out;
|
|
666
|
+
}
|
|
667
|
+
.rk-accordion-bar-turn.is-open[data-v-00091812] {
|
|
668
|
+
transform: rotate(0deg);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/* 0fr to 1fr is the whole trick: a grid row can be animated to a height that
|
|
672
|
+
was never measured, which is what `height: auto` cannot do. */
|
|
673
|
+
.rk-accordion-panel[data-v-00091812] {
|
|
674
|
+
display: grid;
|
|
675
|
+
grid-template-rows: 0fr;
|
|
676
|
+
transition: grid-template-rows 420ms ease-out;
|
|
677
|
+
}
|
|
678
|
+
.rk-accordion-panel.is-open[data-v-00091812] {
|
|
679
|
+
grid-template-rows: 1fr;
|
|
680
|
+
}
|
|
681
|
+
.rk-accordion-clip[data-v-00091812] {
|
|
682
|
+
overflow: hidden;
|
|
683
|
+
}
|
|
684
|
+
.rk-accordion-body[data-v-00091812] {
|
|
685
|
+
max-width: 68ch;
|
|
686
|
+
padding-bottom: 1.5rem;
|
|
687
|
+
color: var(--color-ink-soft);
|
|
688
|
+
line-height: 1.625;
|
|
689
|
+
opacity: 0;
|
|
690
|
+
transition: opacity 300ms;
|
|
691
|
+
}
|
|
692
|
+
.rk-accordion-body.is-open[data-v-00091812] {
|
|
693
|
+
opacity: 1;
|
|
694
|
+
}
|
|
695
|
+
@media (prefers-reduced-motion: reduce) {
|
|
696
|
+
.rk-accordion-panel[data-v-00091812],
|
|
697
|
+
.rk-accordion-bar-turn[data-v-00091812],
|
|
698
|
+
.rk-accordion-body[data-v-00091812] {
|
|
699
|
+
transition: none;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
.rk-modal-scrim[data-v-e092f731] {
|
|
704
|
+
position: fixed;
|
|
705
|
+
inset: 0;
|
|
706
|
+
z-index: 50;
|
|
707
|
+
display: grid;
|
|
708
|
+
place-items: center;
|
|
709
|
+
padding: 1.5rem;
|
|
710
|
+
background: color-mix(in srgb, var(--color-ink) 70%, transparent);
|
|
711
|
+
backdrop-filter: blur(4px);
|
|
712
|
+
}
|
|
713
|
+
.rk-modal-panel[data-v-e092f731] {
|
|
714
|
+
width: 100%;
|
|
715
|
+
max-width: 28rem;
|
|
716
|
+
max-height: 85vh;
|
|
717
|
+
overflow-y: auto;
|
|
718
|
+
border-radius: var(--radius-card);
|
|
719
|
+
background: var(--color-surface);
|
|
720
|
+
padding: 1.5rem;
|
|
721
|
+
box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.2);
|
|
722
|
+
}
|
|
723
|
+
.rk-modal-panel[data-v-e092f731]:focus {
|
|
724
|
+
outline: none;
|
|
725
|
+
}
|
|
726
|
+
.rk-modal-head[data-v-e092f731] {
|
|
727
|
+
display: flex;
|
|
728
|
+
align-items: flex-start;
|
|
729
|
+
justify-content: space-between;
|
|
730
|
+
gap: 1rem;
|
|
731
|
+
}
|
|
732
|
+
.rk-modal-title[data-v-e092f731] {
|
|
733
|
+
font-size: 1.0625rem;
|
|
734
|
+
font-weight: 600;
|
|
735
|
+
color: var(--color-ink);
|
|
736
|
+
}
|
|
737
|
+
.rk-modal-close[data-v-e092f731] {
|
|
738
|
+
display: grid;
|
|
739
|
+
place-items: center;
|
|
740
|
+
flex-shrink: 0;
|
|
741
|
+
width: 2rem;
|
|
742
|
+
height: 2rem;
|
|
743
|
+
border-radius: 9999px;
|
|
744
|
+
color: var(--color-ink-soft);
|
|
745
|
+
transition: background-color 150ms;
|
|
746
|
+
}
|
|
747
|
+
.rk-modal-close[data-v-e092f731]:hover {
|
|
748
|
+
background: var(--color-muted);
|
|
749
|
+
}
|
|
750
|
+
.rk-modal-close[data-v-e092f731]:focus-visible {
|
|
751
|
+
outline: 2px solid var(--color-primary);
|
|
752
|
+
outline-offset: 2px;
|
|
753
|
+
}
|
|
754
|
+
.rk-modal-close svg[data-v-e092f731] {
|
|
755
|
+
width: 1rem;
|
|
756
|
+
height: 1rem;
|
|
757
|
+
}
|
|
758
|
+
.rk-modal-body[data-v-e092f731] {
|
|
759
|
+
margin-top: 0.75rem;
|
|
760
|
+
color: var(--color-ink-soft);
|
|
761
|
+
font-size: 0.875rem;
|
|
762
|
+
line-height: 1.625;
|
|
763
|
+
}
|
|
764
|
+
.rk-modal-actions[data-v-e092f731] {
|
|
765
|
+
margin-top: 1.5rem;
|
|
766
|
+
display: flex;
|
|
767
|
+
justify-content: flex-end;
|
|
768
|
+
gap: 0.5rem;
|
|
769
|
+
}
|
|
770
|
+
.rk-modal-enter-active[data-v-e092f731],
|
|
771
|
+
.rk-modal-leave-active[data-v-e092f731] {
|
|
772
|
+
transition: opacity 180ms ease;
|
|
773
|
+
}
|
|
774
|
+
.rk-modal-enter-active .rk-modal-panel[data-v-e092f731],
|
|
775
|
+
.rk-modal-leave-active .rk-modal-panel[data-v-e092f731] {
|
|
776
|
+
transition:
|
|
777
|
+
transform 180ms cubic-bezier(0.32, 0.72, 0, 1),
|
|
778
|
+
opacity 180ms ease;
|
|
779
|
+
}
|
|
780
|
+
.rk-modal-enter-from[data-v-e092f731],
|
|
781
|
+
.rk-modal-leave-to[data-v-e092f731] {
|
|
782
|
+
opacity: 0;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/* From nowhere, not from an edge: a scale is what separates this from a sheet. */
|
|
786
|
+
.rk-modal-enter-from .rk-modal-panel[data-v-e092f731],
|
|
787
|
+
.rk-modal-leave-to .rk-modal-panel[data-v-e092f731] {
|
|
788
|
+
transform: scale(0.96);
|
|
789
|
+
opacity: 0;
|
|
790
|
+
}
|
|
791
|
+
@media (prefers-reduced-motion: reduce) {
|
|
792
|
+
.rk-modal-enter-active .rk-modal-panel[data-v-e092f731],
|
|
793
|
+
.rk-modal-leave-active .rk-modal-panel[data-v-e092f731] {
|
|
794
|
+
transition: opacity 180ms ease;
|
|
795
|
+
transform: none;
|
|
796
|
+
}
|
|
797
|
+
.rk-modal-enter-from .rk-modal-panel[data-v-e092f731],
|
|
798
|
+
.rk-modal-leave-to .rk-modal-panel[data-v-e092f731] {
|
|
799
|
+
transform: none;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
.rk-nav[data-v-91bb3b84] {
|
|
804
|
+
display: flex;
|
|
805
|
+
align-items: center;
|
|
806
|
+
gap: 0.25rem;
|
|
807
|
+
}
|
|
808
|
+
.rk-nav-link[data-v-91bb3b84] {
|
|
809
|
+
position: relative;
|
|
810
|
+
border-radius: var(--radius-cell);
|
|
811
|
+
padding: 0.5rem 0.75rem;
|
|
812
|
+
font-size: 0.875rem;
|
|
813
|
+
color: var(--color-ink-soft);
|
|
814
|
+
transition: color 150ms;
|
|
815
|
+
}
|
|
816
|
+
.rk-nav-link[data-v-91bb3b84]:hover {
|
|
817
|
+
color: var(--color-ink);
|
|
818
|
+
}
|
|
819
|
+
.rk-nav-link[data-v-91bb3b84]:focus-visible {
|
|
820
|
+
outline: 2px solid var(--color-primary);
|
|
821
|
+
outline-offset: 2px;
|
|
822
|
+
}
|
|
823
|
+
.rk-nav-link.is-active[data-v-91bb3b84] {
|
|
824
|
+
color: var(--color-ink);
|
|
825
|
+
font-weight: 500;
|
|
826
|
+
}
|
|
827
|
+
.rk-nav-rule[data-v-91bb3b84] {
|
|
828
|
+
position: absolute;
|
|
829
|
+
inset-inline: 0.75rem;
|
|
830
|
+
bottom: -1px;
|
|
831
|
+
height: 2px;
|
|
832
|
+
border-radius: 9999px;
|
|
833
|
+
background: var(--color-primary);
|
|
834
|
+
transform: scaleX(0);
|
|
835
|
+
transform-origin: left;
|
|
836
|
+
transition: transform 300ms;
|
|
837
|
+
}
|
|
838
|
+
.rk-nav-link.is-active .rk-nav-rule[data-v-91bb3b84] {
|
|
839
|
+
transform: scaleX(1);
|
|
840
|
+
}
|
|
841
|
+
@media (prefers-reduced-motion: reduce) {
|
|
842
|
+
.rk-nav-rule[data-v-91bb3b84] {
|
|
843
|
+
transition: none;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
610
846
|
/*$vite$:1*/
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface AccordionItem<K extends string> {
|
|
2
|
+
key: K;
|
|
3
|
+
/** The question. Already translated. */
|
|
4
|
+
title: string;
|
|
5
|
+
}
|
|
6
|
+
declare const __VLS_export: <K extends string>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>["props"], __VLS_ctx?: __VLS_PrettifyLocal<Pick<NonNullable<Awaited<typeof __VLS_setup>>, "attrs" | "emit" | "slots">>, __VLS_exposed?: NonNullable<Awaited<typeof __VLS_setup>>["expose"], __VLS_setup?: Promise<{
|
|
7
|
+
props: import('vue').PublicProps & __VLS_PrettifyLocal<{
|
|
8
|
+
items: readonly AccordionItem<K>[];
|
|
9
|
+
/** Allow several open at once. Off by default, which is what makes it an accordion. */
|
|
10
|
+
multiple?: boolean | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* Which heading the control sits inside.
|
|
13
|
+
*
|
|
14
|
+
* A heading, always — it is how a screen reader user moves between sections
|
|
15
|
+
* without opening any of them. The level is a prop because it depends on what
|
|
16
|
+
* is above it on the page, and getting it wrong breaks the outline.
|
|
17
|
+
*/
|
|
18
|
+
headingLevel?: 2 | 3 | 4 | undefined;
|
|
19
|
+
}> & (typeof globalThis extends {
|
|
20
|
+
__VLS_PROPS_FALLBACK: infer P;
|
|
21
|
+
} ? P : {});
|
|
22
|
+
expose: (exposed: {}) => void;
|
|
23
|
+
attrs: any;
|
|
24
|
+
slots: {
|
|
25
|
+
/** The answer, per item. */
|
|
26
|
+
default: (props: {
|
|
27
|
+
item: AccordionItem<K>;
|
|
28
|
+
open: boolean;
|
|
29
|
+
}) => unknown;
|
|
30
|
+
};
|
|
31
|
+
emit: {};
|
|
32
|
+
}>) => import('vue').VNode & {
|
|
33
|
+
__ctx?: NonNullable<Awaited<typeof __VLS_setup>>;
|
|
34
|
+
};
|
|
35
|
+
declare const _default: typeof __VLS_export;
|
|
36
|
+
export default _default;
|
|
37
|
+
type __VLS_PrettifyLocal<T> = (T extends any ? {
|
|
38
|
+
[K in keyof T]: T[K];
|
|
39
|
+
} : {
|
|
40
|
+
[K in keyof T as K]: T[K];
|
|
41
|
+
}) & {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A dialog that arrives from nowhere.
|
|
3
|
+
*
|
|
4
|
+
* Not `BaseSheet` with different padding. A sheet slides up from the bottom
|
|
5
|
+
* edge, is dismissed by dragging it back down, and belongs to a thumb; a modal
|
|
6
|
+
* appears in the middle of what you were reading and is dismissed by leaving
|
|
7
|
+
* it. They are two answers to two questions and merging them would give a
|
|
8
|
+
* component that is wrong on a phone and wrong on a desktop.
|
|
9
|
+
*
|
|
10
|
+
* ## The parts that are easy to get wrong
|
|
11
|
+
*
|
|
12
|
+
* This is why it exists rather than being written per dialog, which is what the
|
|
13
|
+
* one wide consumer was doing in two places. None of the following is visible
|
|
14
|
+
* on screen when it is missing:
|
|
15
|
+
*
|
|
16
|
+
* - **Focus moves in and comes back.** Opening without moving focus leaves a
|
|
17
|
+
* keyboard user behind the dialog, operating a page they cannot see. Closing
|
|
18
|
+
* without restoring it drops them at the top of the document.
|
|
19
|
+
* - **Tab does not leave.** A dialog you can Tab out of is a dialog that is
|
|
20
|
+
* modal only to somebody using a mouse.
|
|
21
|
+
* - **Escape closes.** Every dialog in every application does this.
|
|
22
|
+
* - **The page underneath does not scroll.** Otherwise dismissing the dialog
|
|
23
|
+
* returns you somewhere else.
|
|
24
|
+
* - **The backdrop closes it, the panel does not.** A click that starts inside
|
|
25
|
+
* the panel and ends on the backdrop is not a click on the backdrop, which is
|
|
26
|
+
* why this listens for a press on the backdrop itself rather than any click
|
|
27
|
+
* that reaches it.
|
|
28
|
+
*/
|
|
29
|
+
type __VLS_Props = {
|
|
30
|
+
/** Names the dialog for assistive tech. Required; an unnamed dialog is a box. */
|
|
31
|
+
title: string;
|
|
32
|
+
/** Accessible name for the close button. */
|
|
33
|
+
closeLabel: string;
|
|
34
|
+
/**
|
|
35
|
+
* `alert` marks it as a decision that interrupts — `role="alertdialog"`,
|
|
36
|
+
* which a screen reader announces immediately rather than on arrival.
|
|
37
|
+
*/
|
|
38
|
+
tone?: 'default' | 'alert' | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Whether Escape and the backdrop close it.
|
|
41
|
+
*
|
|
42
|
+
* Off for a dialog that must be answered — but never off as a way of forcing
|
|
43
|
+
* a reader to a decision they have not been given the information for.
|
|
44
|
+
*/
|
|
45
|
+
dismissible?: boolean | undefined;
|
|
46
|
+
};
|
|
47
|
+
type __VLS_Slots = {
|
|
48
|
+
/** The body. */
|
|
49
|
+
default: () => unknown;
|
|
50
|
+
/** Buttons, at the foot. */
|
|
51
|
+
actions?: () => unknown;
|
|
52
|
+
};
|
|
53
|
+
type __VLS_ModelProps = {
|
|
54
|
+
modelValue?: boolean;
|
|
55
|
+
};
|
|
56
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
57
|
+
declare const __VLS_base: import('vue').DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
|
|
58
|
+
"update:modelValue": (value: boolean) => any;
|
|
59
|
+
}, string, import('vue').PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
60
|
+
"onUpdate:modelValue"?: (value: boolean) => any;
|
|
61
|
+
}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
|
|
62
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
63
|
+
declare const _default: typeof __VLS_export;
|
|
64
|
+
export default _default;
|
|
65
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
66
|
+
new (): {
|
|
67
|
+
$slots: S;
|
|
68
|
+
};
|
|
69
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface NavLinkItem<K extends string> {
|
|
2
|
+
/** Identity, compared against `active`. */
|
|
3
|
+
key: K;
|
|
4
|
+
/** Router destination. */
|
|
5
|
+
to: string;
|
|
6
|
+
/** Already translated. */
|
|
7
|
+
label: string;
|
|
8
|
+
}
|
|
9
|
+
declare const __VLS_export: <K extends string>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>["props"], __VLS_ctx?: __VLS_PrettifyLocal<Pick<NonNullable<Awaited<typeof __VLS_setup>>, "attrs" | "emit" | "slots">>, __VLS_exposed?: NonNullable<Awaited<typeof __VLS_setup>>["expose"], __VLS_setup?: Promise<{
|
|
10
|
+
props: import('vue').PublicProps & __VLS_PrettifyLocal<{
|
|
11
|
+
items: readonly NavLinkItem<K>[];
|
|
12
|
+
/** Which item is current. Usually derived from the route. */
|
|
13
|
+
active?: K | undefined;
|
|
14
|
+
/** Accessible name for the navigation landmark. */
|
|
15
|
+
label?: string | undefined;
|
|
16
|
+
}> & (typeof globalThis extends {
|
|
17
|
+
__VLS_PROPS_FALLBACK: infer P;
|
|
18
|
+
} ? P : {});
|
|
19
|
+
expose: (exposed: {}) => void;
|
|
20
|
+
attrs: any;
|
|
21
|
+
slots: {};
|
|
22
|
+
emit: {};
|
|
23
|
+
}>) => import('vue').VNode & {
|
|
24
|
+
__ctx?: NonNullable<Awaited<typeof __VLS_setup>>;
|
|
25
|
+
};
|
|
26
|
+
declare const _default: typeof __VLS_export;
|
|
27
|
+
export default _default;
|
|
28
|
+
type __VLS_PrettifyLocal<T> = (T extends any ? {
|
|
29
|
+
[K in keyof T]: T[K];
|
|
30
|
+
} : {
|
|
31
|
+
[K in keyof T as K]: T[K];
|
|
32
|
+
}) & {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rei-kit/web — the parts a wide site is made of.
|
|
3
|
+
*
|
|
4
|
+
* Separate from the main entry for the same reason `rei-kit/app` is: these
|
|
5
|
+
* assume a page with a header, a mouse and room to spare. A phone app importing
|
|
6
|
+
* the kit should not download them.
|
|
7
|
+
*
|
|
8
|
+
* Smaller than it was meant to be, and deliberately. The plan for this release
|
|
9
|
+
* listed eight components; four of them had no user anywhere — `Pagination` was
|
|
10
|
+
* not written once in any consumer, and `Tabs` and `Breadcrumb` appear only in
|
|
11
|
+
* prose. A kit that ships a component nobody reached for is a kit guessing at
|
|
12
|
+
* a shape, and `BaseRadioGroup` is already one standing reminder of that.
|
|
13
|
+
*
|
|
14
|
+
* `Tooltip` is missing for a better reason: the one consumer's tooltip is
|
|
15
|
+
* CSS-only on purpose, because a tooltip that needs JavaScript to appear does
|
|
16
|
+
* not appear in a prerendered page. Shipping a JavaScript one would have been
|
|
17
|
+
* shipping the wrong answer in a nicer package.
|
|
18
|
+
*/
|
|
19
|
+
export { default as BaseAccordion } from './BaseAccordion.vue';
|
|
20
|
+
export type { AccordionItem } from './BaseAccordion.vue';
|
|
21
|
+
/**
|
|
22
|
+
* `BaseModal`, beside `BaseSheet` rather than named `Modal` on its own.
|
|
23
|
+
*
|
|
24
|
+
* The two are the pair this kit keeps insisting they are — a sheet from the
|
|
25
|
+
* bottom edge, a modal from nowhere — and naming them alike is the cheapest way
|
|
26
|
+
* to make somebody reaching for one notice the other exists.
|
|
27
|
+
*/
|
|
28
|
+
export { default as BaseModal } from './BaseModal.vue';
|
|
29
|
+
export { default as NavLinks } from './NavLinks.vue';
|
|
30
|
+
export type { NavLinkItem } from './NavLinks.vue';
|