rei-kit 2.7.0 → 2.9.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 +4 -4
- package/dist/{SettingsRow-BXFn8tCk.js → BaseButton-RN2an975.js} +5 -66
- package/dist/BaseButton-RN2an975.js.map +1 -0
- package/dist/BaseSkeleton-BWTNXVwP.js +171 -0
- package/dist/BaseSkeleton-BWTNXVwP.js.map +1 -0
- package/dist/{GoogleButton-nIRz9G8s.js → GoogleButton-DS08ayQ-.js} +4 -67
- package/dist/GoogleButton-DS08ayQ-.js.map +1 -0
- package/dist/SettingsRow-Ct1p2C9Q.js +65 -0
- package/dist/SettingsRow-Ct1p2C9Q.js.map +1 -0
- package/dist/app.js +4 -3
- package/dist/app.js.map +1 -1
- package/dist/components/BaseChip.vue.d.ts +4 -0
- package/dist/components/BaseListbox.vue.d.ts +38 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +40 -69
- package/dist/index.js.map +1 -1
- package/dist/inert-BRQcZwpX.js +117 -0
- package/dist/inert-BRQcZwpX.js.map +1 -0
- package/dist/pwa.js +2 -1
- package/dist/pwa.js.map +1 -1
- package/dist/styles.css +460 -59
- package/dist/web/BaseSplitter.vue.d.ts +47 -0
- package/dist/web/BaseToolbar.vue.d.ts +30 -0
- package/dist/web/BaseTree.vue.d.ts +42 -0
- package/dist/web/CommandMenu.vue.d.ts +50 -0
- package/dist/web/DataTable.vue.d.ts +61 -0
- package/dist/web/TransferList.vue.d.ts +35 -0
- package/dist/web/index.d.ts +22 -0
- package/dist/web.js +884 -53
- package/dist/web.js.map +1 -1
- package/package.json +2 -2
- package/dist/GoogleButton-nIRz9G8s.js.map +0 -1
- package/dist/SettingsRow-BXFn8tCk.js.map +0 -1
- package/dist/inert-B441LoCD.js +0 -53
- package/dist/inert-B441LoCD.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# rei-kit
|
|
2
2
|
|
|
3
|
-
**One kit. Every look.**
|
|
3
|
+
**One kit. Every look.** 84 accessible components for Vue 3 and Tailwind 4.
|
|
4
4
|
Change the **material** with one attribute and the **palette** with another —
|
|
5
5
|
every component follows, and none of them knows your brand.
|
|
6
6
|
|
|
@@ -112,7 +112,7 @@ Each claim here is enforced by something that fails, not by a promise.
|
|
|
112
112
|
|
|
113
113
|
| Standard | How it is held |
|
|
114
114
|
| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
115
|
-
| **Open for extension, closed for edits** | A new look is a token, a material or a palette — never an edit to a component. Materials and palettes restyle all
|
|
115
|
+
| **Open for extension, closed for edits** | A new look is a token, a material or a palette — never an edit to a component. Materials and palettes restyle all 84 without touching one, and a test fails if a component holds a colour. |
|
|
116
116
|
| **Single responsibility** | One part, one job. Label-and-error wiring lives in `FormField`, not in five inputs; taking the page behind a layer out of reach lives in one helper the modal, the sheet and the guide share. |
|
|
117
117
|
| **Depend on roles, not values** | Components read `primary`, `surface`, `--shadow-card` — never a hex or a pixel shadow. An app's brand wins in both modes, tested. |
|
|
118
118
|
| **WAI-ARIA Authoring Practices** | Menus, comboboxes, tabs, sliders, dialogs and accordions follow their APG pattern: arrows move, Tab leaves, Escape closes, focus returns. Behaviour tests drive each with the keyboard. |
|
|
@@ -124,11 +124,11 @@ Each claim here is enforced by something that fails, not by a promise.
|
|
|
124
124
|
|
|
125
125
|
## Status
|
|
126
126
|
|
|
127
|
-
**v2.
|
|
127
|
+
**v2.9.0 — three consumers.**
|
|
128
128
|
|
|
129
129
|
| | |
|
|
130
130
|
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
131
|
-
| Components |
|
|
131
|
+
| Components | 84 (`AuthForm`, `BaseTable`, `BaseCombobox`, `BaseSlider`, `TabShell`, `BaseModal`, `BaseTabs`, `BaseTooltip`, `BasePagination`, `BaseBreadcrumb`, `BaseDisclosure`, `BaseAccordion`, `NavLinks`, `OfflineBanner`, `FabButton`, `BaseButton`, `BaseCard`, `BaseInput`, `BaseSelect`, `BaseTextarea`, `BaseCheckbox`, `BaseSwitch`, `BaseRadioGroup`, `BaseMenu`, `BaseAvatar`, `BaseSpinner`, `BaseAlert`, `BaseBadge`, `BaseSheet`, `ProgressBar`, `PriceCard`, `ToastHost`, `TabBar`, `GoogleButton`, `LocaleLinks`, `LocaleSheet`, `AuthShell`, `TourShell`, `InstallPrompt`, `UpdatePrompt`, `InstallSettings`, `SkeletonList`, `PageContainer`, `ErrorBoundary`, etc.) |
|
|
132
132
|
| Composables | 16 (`useToast`, `useTheme`, `useMaterial`, `usePalette`, `useToday`, `useMediaQuery`, `useInstall`, `watchInstallability`, `createTabTransition`, `useThemeSync`, `useVisualViewport`, etc.) |
|
|
133
133
|
| Utilities | 33 (`applyTheme`, `applyMaterial`, `applyPalette`, `MATERIALS`, `PALETTES`, `formatDate`, `fieldErrors`, `toAuthMessageKey`, `createAuthGuard`, `createQueryDefaults`, `createWriteReport`, `toRedirectPath`, `Supabase error mapper`, i18n runtime, etc.) |
|
|
134
134
|
| Entry Points | `rei-kit`, `rei-kit/app`, `rei-kit/web`, `rei-kit/pwa`, `rei-kit/supabase`, `rei-kit/mobile.css`, `rei-kit/web.css`, and each stylesheet on its own |
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { computed, createBlock, createCommentVNode, createElementBlock,
|
|
2
|
-
import { ChevronRight } from "lucide-vue-next";
|
|
1
|
+
import { computed, createBlock, createCommentVNode, createElementBlock, defineComponent, mergeProps, openBlock, renderSlot, resolveDynamicComponent, withCtx } from "vue";
|
|
3
2
|
//#region src/components/BaseButton.vue?vue&type=script&setup=true&lang.ts
|
|
4
|
-
var _hoisted_1
|
|
3
|
+
var _hoisted_1 = {
|
|
5
4
|
key: 0,
|
|
6
5
|
class: "size-4 animate-spin rounded-full border-2 border-current border-t-transparent",
|
|
7
6
|
"aria-hidden": "true"
|
|
@@ -131,7 +130,7 @@ var BaseButton_default = /* @__PURE__ */ defineComponent({
|
|
|
131
130
|
__props.block ? "w-full" : ""
|
|
132
131
|
]]
|
|
133
132
|
}), {
|
|
134
|
-
default: withCtx(() => [__props.loading ? (openBlock(), createElementBlock("span", _hoisted_1
|
|
133
|
+
default: withCtx(() => [__props.loading ? (openBlock(), createElementBlock("span", _hoisted_1)) : createCommentVNode("", true), renderSlot(_ctx.$slots, "default")]),
|
|
135
134
|
_: 3
|
|
136
135
|
}, 16, [
|
|
137
136
|
"type",
|
|
@@ -145,66 +144,6 @@ var BaseButton_default = /* @__PURE__ */ defineComponent({
|
|
|
145
144
|
}
|
|
146
145
|
});
|
|
147
146
|
//#endregion
|
|
148
|
-
|
|
149
|
-
var _hoisted_1 = { class: "flex items-center gap-3" };
|
|
150
|
-
var _hoisted_2 = {
|
|
151
|
-
key: 0,
|
|
152
|
-
class: "bg-muted text-ink-soft flex size-9 shrink-0 items-center justify-center rounded-xl",
|
|
153
|
-
"aria-hidden": "true"
|
|
154
|
-
};
|
|
155
|
-
var _hoisted_3 = { class: "min-w-0 flex-1" };
|
|
156
|
-
var _hoisted_4 = { class: "text-ink text-sm font-medium" };
|
|
157
|
-
var _hoisted_5 = {
|
|
158
|
-
key: 0,
|
|
159
|
-
class: "text-ink-soft mt-0.5 text-xs leading-snug"
|
|
160
|
-
};
|
|
161
|
-
var _hoisted_6 = {
|
|
162
|
-
key: 1,
|
|
163
|
-
class: "shrink-0"
|
|
164
|
-
};
|
|
165
|
-
var _hoisted_7 = { key: 0 };
|
|
166
|
-
//#endregion
|
|
167
|
-
//#region src/components/SettingsRow.vue
|
|
168
|
-
var SettingsRow_default = /* @__PURE__ */ defineComponent({
|
|
169
|
-
__name: "SettingsRow",
|
|
170
|
-
props: {
|
|
171
|
-
label: {},
|
|
172
|
-
description: { default: "" },
|
|
173
|
-
icon: { default: () => void 0 },
|
|
174
|
-
interactive: {
|
|
175
|
-
type: Boolean,
|
|
176
|
-
default: false
|
|
177
|
-
},
|
|
178
|
-
stacked: {
|
|
179
|
-
type: Boolean,
|
|
180
|
-
default: false
|
|
181
|
-
}
|
|
182
|
-
},
|
|
183
|
-
emits: ["click"],
|
|
184
|
-
setup(__props, { emit: __emit }) {
|
|
185
|
-
const emit = __emit;
|
|
186
|
-
return (_ctx, _cache) => {
|
|
187
|
-
return openBlock(), createBlock(resolveDynamicComponent(__props.interactive ? "button" : "div"), {
|
|
188
|
-
type: __props.interactive ? "button" : void 0,
|
|
189
|
-
class: normalizeClass(["flex w-full items-center gap-3 px-4 py-3 text-left", [__props.interactive ? "hover:bg-muted/60 transition-colors active:scale-[0.99]" : "", __props.stacked ? "flex-col items-stretch gap-3" : ""]]),
|
|
190
|
-
onClick: _cache[0] || (_cache[0] = ($event) => __props.interactive && emit("click"))
|
|
191
|
-
}, {
|
|
192
|
-
default: withCtx(() => [createElementVNode("div", _hoisted_1, [
|
|
193
|
-
__props.icon ? (openBlock(), createElementBlock("span", _hoisted_2, [(openBlock(), createBlock(resolveDynamicComponent(__props.icon), { class: "size-[18px]" }))])) : createCommentVNode("", true),
|
|
194
|
-
createElementVNode("div", _hoisted_3, [createElementVNode("p", _hoisted_4, toDisplayString(__props.label), 1), __props.description ? (openBlock(), createElementBlock("p", _hoisted_5, toDisplayString(__props.description), 1)) : createCommentVNode("", true)]),
|
|
195
|
-
!__props.stacked ? (openBlock(), createElementBlock("div", _hoisted_6, [renderSlot(_ctx.$slots, "default")])) : createCommentVNode("", true),
|
|
196
|
-
__props.interactive ? (openBlock(), createBlock(unref(ChevronRight), {
|
|
197
|
-
key: 2,
|
|
198
|
-
class: "text-ink-soft size-4 shrink-0",
|
|
199
|
-
"aria-hidden": "true"
|
|
200
|
-
})) : createCommentVNode("", true)
|
|
201
|
-
]), __props.stacked ? (openBlock(), createElementBlock("div", _hoisted_7, [renderSlot(_ctx.$slots, "default")])) : createCommentVNode("", true)]),
|
|
202
|
-
_: 3
|
|
203
|
-
}, 8, ["type", "class"]);
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
});
|
|
207
|
-
//#endregion
|
|
208
|
-
export { BaseButton_default as n, SettingsRow_default as t };
|
|
147
|
+
export { BaseButton_default as t };
|
|
209
148
|
|
|
210
|
-
//# sourceMappingURL=
|
|
149
|
+
//# sourceMappingURL=BaseButton-RN2an975.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BaseButton-RN2an975.js","names":[],"sources":["../src/components/BaseButton.vue","../src/components/BaseButton.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport type { HTMLAttributes } from 'vue'\n\n/**\n * The kit's button, and — when asked — its link.\n *\n * `as` exists because a button and a link are the same shape and a different\n * element, and the app was resolving that by nesting them: a consumer had\n * `<RouterLink><BaseButton>` in every call to action, which is an `<a>` around\n * a `<button>`. That is invalid HTML, two stops in the tab order and two\n * controls to a screen reader, for one thing on the screen. Whether something\n * navigates is the app's decision; carrying it is this component's job.\n *\n * `router-link` is resolved by name rather than imported, so `vue-router` stays\n * the optional peer it is. Only an app that passes `as=\"router-link\"` needs it,\n * and an app that passes it has it.\n */\nconst {\n as = 'button',\n variant = 'primary',\n size = 'md',\n loading = false,\n disabled = false,\n type = 'button',\n icon = false,\n block = false,\n pill = false,\n pressed = undefined,\n to = undefined,\n href = undefined,\n} = defineProps<\n {\n /** What to render. `button` unless this navigates. */\n as?: 'button' | 'a' | 'router-link' | undefined\n /**\n * `link` is a real action that should read as text — \"clear this note\",\n * \"remove\", \"change category\". It has no surface at all, so it also has no\n * height and no padding: giving it either would make it a ghost button,\n * which is a different thing and was already here.\n */\n variant?:\n | 'primary'\n | 'secondary'\n | 'ghost'\n | 'quiet'\n | 'destructive'\n | 'row'\n | 'danger'\n | 'positive'\n | 'warning'\n | 'accent'\n | 'link'\n | 'unstyled'\n | undefined\n /** `xs` is the action inside a prompt or a nudge, not on a page. */\n size?: 'xs' | 'sm' | 'md' | 'lg' | undefined\n loading?: boolean | undefined\n disabled?: boolean | undefined\n /** Ignored unless `as` is `button`. */\n type?: 'button' | 'submit' | undefined\n /**\n * Square, sized to its icon, with no label beside it.\n *\n * **Pass `aria-label`.** An icon on its own has no accessible name, and a\n * control a screen reader announces as \"button\" is not usable. Attributes\n * fall through, so `aria-label` lands where it should — nothing here can\n * check that you passed one, which is why it is said this loudly.\n */\n icon?: boolean | undefined\n /** Fills its container. The ordinary case under a form. */\n block?: boolean | undefined\n /** For `as=\"router-link\"`. */\n to?: string | Record<string, unknown> | undefined\n /** For `as=\"a\"`. */\n href?: string | undefined\n /**\n * Fully rounded rather than card-cornered.\n *\n * Every install prompt, update prompt and nudge across the apps used the\n * same pair — a filled pill to act and a quiet one to dismiss — and none of\n * them could use this component, because it only knew one corner radius.\n */\n pill?: boolean | undefined\n /**\n * That this button is a switch, and whether it is on.\n *\n * Omit it and the button is an action. Pass it and the button becomes a\n * toggle: `aria-pressed` is written, and the variants that have an \"off\"\n * look — ghost, quiet, secondary — take a filled one when on.\n *\n * There were 18 of these hand-written across the three apps, every one a\n * picker cell or a filter chip, and almost none of them said `aria-pressed`\n * at all. A screen reader met a row of identical buttons with no way to know\n * which was chosen.\n */\n pressed?: boolean | undefined\n /* The root element's own attributes and listeners — `@click`, `aria-label`,\n `role` — typed for editors and for vue-tsc's strictTemplates, where an\n undeclared one is an error. `@vue-ignore` keeps them out of the runtime\n props, so they still fall through to the element as before. */\n } & /* @vue-ignore */ HTMLAttributes\n>()\n\nconst VARIANT_CLASS = {\n primary: 'bg-primary text-on-primary shadow-(--shadow-control) hover:bg-primary/90',\n /*\n * An action that is real but not the one being urged.\n *\n * `ghost` had been standing in for this and cannot: with no border and no\n * fill it reads as text, so \"Save draft\" sitting next to \"Publish\" looked\n * like a caption rather than the other half of a choice. Ghost is for a\n * control that should recede until it is wanted — a toolbar, a menu row —\n * and that is a different job.\n */\n secondary: 'control text-ink hover:bg-muted',\n ghost: 'bg-transparent text-ink hover:bg-muted',\n /*\n * Quiet until you reach for it, and then plainly destructive: a delete at\n * the end of a row, a \"remove this note\", an archive.\n *\n * Not `danger`, which is filled and shouts before it is needed — a red\n * button in a list of rows makes the list look like a warning. And not\n * `quiet` with a `hover:text-negative` class beside it, which is how all\n * three apps were doing it: that class and the variant's own\n * `hover:text-ink` set the same property at the same specificity, so which\n * one wins depends on the order they happen to land in the stylesheet.\n *\n * Fifteen of these across the three apps, and every one of them was that\n * coin toss.\n */\n destructive: 'bg-transparent text-ink-soft hover:text-negative',\n /*\n * A line in a list that is also a control: a settings row, a node in a tree,\n * a heading that opens something.\n *\n * Full width, aligned to the start, and a hover that fills the whole line\n * rather than a box inside it. Every app had written this — `.tree-row`,\n * `.row`, `.header-action` — because a button that centres its content\n * cannot be a row, and the alignment is the only thing that had to change.\n *\n * Padding stays the app's: a menu row and a tree node are not the same\n * height, and the kit has no opinion about which one this is.\n */\n row: 'w-full justify-start text-left bg-transparent text-ink hover:bg-muted',\n /*\n * The control that is present without asking for attention: a dismiss beside\n * an install prompt, a chevron beside a month, a delete at the end of a row.\n *\n * `ghost` is not this. Ghost keeps full-strength ink; this one starts soft\n * and darkens, which is the difference between a control waiting to be used\n * and one that is merely available. The pair `text-ink-soft hover:text-ink`\n * was hand-written 47 times across the three apps.\n *\n * It fills on hover, and 0.11.0 got that half-right by fill... only for\n * icons. The evidence said otherwise once the third app was read: an editor\n * toolbar's buttons carry text and fill exactly the same way. The shape is\n * \"a control in a strip\", not \"a control with a glyph in it\". A text action\n * that should have no surface at all is `link`.\n */\n quiet: 'bg-transparent text-ink-soft hover:bg-muted hover:text-ink',\n danger: 'bg-negative text-on-negative shadow-(--shadow-control) hover:bg-negative/90',\n /*\n * The rest of the roles the kit already declares.\n *\n * `tokens.css` names five colour roles and this component exposed two of\n * them, so an app that wanted a success-coloured action had to hand-write\n * the button — which is what Hibi's green install button is. A component\n * that cannot use a role its own design system declares is not avoiding a\n * guess; it is incomplete.\n */\n positive: 'bg-positive text-on-positive shadow-(--shadow-control) hover:bg-positive/90',\n warning: 'bg-warning text-on-warning shadow-(--shadow-control) hover:bg-warning/90',\n accent: 'bg-accent text-on-accent shadow-(--shadow-control) hover:bg-accent/90',\n /* No fill, no border, no box: underlined so it is still obviously a control\n without one. `ghost` cannot stand in — it has a hover surface and a\n radius, so it reads as a button that happens to be empty. */\n link: 'bg-transparent underline underline-offset-2 hover:opacity-80',\n /*\n * Everything this component is, except the paint.\n *\n * The reason it exists is measurable: across the three apps there were 58\n * raw `<button>` elements sitting in 24 files that already imported and used\n * `BaseButton`. The developer reached for the kit and gave up halfway down\n * the same file — because the kit offered all of its appearance or none of\n * itself, and what those places needed was everything but the appearance.\n *\n * A picker cell, a chip, a calendar day: the surface is the app's, and it\n * should be. The element, the focus ring, the disabled handling, the\n * `aria-pressed` bookkeeping and the `as` switch are not, and were being\n * rewritten every time — usually without the focus ring.\n */\n unstyled: '',\n} as const\n\n/* Two scales, because a square control cannot take horizontal padding and\n still be square. `lg` is here for a wide page's call to action: a 44px\n button is right under a thumb and undersized under a headline. */\nconst SIZE_CLASS = {\n xs: 'h-8 px-3 text-xs',\n sm: 'h-9 px-3 text-sm',\n md: 'h-11 px-4 text-base',\n lg: 'h-14 px-6 text-lg',\n} as const\n\nconst ICON_SIZE_CLASS = {\n xs: 'size-8 text-xs',\n sm: 'size-9 text-sm',\n md: 'size-11 text-base',\n lg: 'size-14 text-lg',\n} as const\n\n/* A row is sized by its padding, not by a height. A settings line holds one\n line of text and a tree node can hold two, and a fixed height turns the\n second into an overflow. */\nconst ROW_SIZE_CLASS = {\n xs: 'px-2 py-1.5 text-xs',\n sm: 'px-3 py-2 text-sm',\n md: 'px-3 py-2.5 text-base',\n lg: 'px-4 py-3 text-lg',\n} as const\n\n/* A link takes the type size and nothing else. Height and padding are what\n make a surface, and this variant is the one without one. */\nconst LINK_SIZE_CLASS = {\n xs: 'text-xs',\n sm: 'text-sm',\n md: 'text-base',\n lg: 'text-lg',\n} as const\n\nconst sizing = computed(() => {\n // Unstyled owns no box, so it takes no size: the app's own classes decide.\n if (variant === 'unstyled') return ''\n if (variant === 'link') return LINK_SIZE_CLASS[size]\n if (variant === 'row') return ROW_SIZE_CLASS[size]\n return icon ? ICON_SIZE_CLASS[size] : SIZE_CLASS[size]\n})\n\n/* The variants with an \"off\" look, and what \"on\" looks like for them. The\n filled ones are already on; link and unstyled have no surface to fill. */\nconst PRESSED_CLASS: Partial<Record<string, string>> = {\n ghost: 'bg-primary text-on-primary hover:bg-primary/90',\n quiet: 'bg-primary text-on-primary hover:bg-primary/90',\n secondary: 'bg-primary border-primary text-on-primary hover:bg-primary/90',\n /* A selected row is filled, not recoloured: the line stays a line, and the\n fill is what a list uses to say \"this one\". Filling it with the primary\n colour instead would make one row of a list shout. */\n row: 'w-full justify-start text-left bg-muted text-ink hover:bg-muted',\n}\n\nconst surface = computed(() => {\n if (pressed === true) return PRESSED_CLASS[variant] ?? VARIANT_CLASS[variant]\n\n /* Destructive tints its own fill rather than borrowing the neutral one: a\n red glyph on a grey wash reads as two different states at once. */\n if (variant === 'destructive') return `${VARIANT_CLASS.destructive} hover:bg-negative/10`\n\n return VARIANT_CLASS[variant]\n})\n\n/* Layout and feel, which unstyled does not impose either — but the focus ring\n and the disabled handling stay, because those are the floor. A raw <button>\n is what happens when a component makes them optional.\n \n Colour is in the transition, not just transform. Every variant here changes\n colour on hover and none of them animated it, so every button in every\n consuming app snapped while the hand-written controls beside them faded —\n `transition-colors` appears 106 times across the three apps, which is the\n convention this component was the only thing not following. */\nconst shell = computed(() => {\n if (variant === 'unstyled') return ''\n\n const feel =\n 'transition-[transform,translate,scale,color,background-color,border-color,box-shadow] duration-(--duration-fast) ease-standard select-none'\n\n /* A row does not press. Scaling a full-width line looks like the list itself\n flinched, and every hand-written row in the apps animated colour only. */\n if (variant === 'row') return `inline-flex items-center gap-2 font-medium ${feel}`\n\n /* The press belongs to the material. A shrink by default; a material that\n draws hard offset shadows moves the button *into* its shadow instead, and\n drops the shadow, so pressing reads as pressing on every one of them. */\n return `inline-flex items-center justify-center gap-2 font-medium ${feel} active:scale-(--press-scale) active:translate-x-(--press-offset) active:translate-y-(--press-offset) active:shadow-(--shadow-control-pressed)`\n})\n\nconst radius = computed(() => {\n if (variant === 'unstyled') return ''\n if (variant === 'link') return 'rounded-xs'\n return pill ? 'rounded-full' : 'rounded-card'\n})\n\n/** Anything that is not a `<button>` cannot be `disabled`; it has to be told. */\nconst inactive = computed(() => disabled || loading)\n\nconst linkProps = computed(() => {\n if (as === 'router-link') return { to }\n // The href is dropped rather than kept alongside aria-disabled: an anchor\n // without one is not focusable and not activatable, which is the whole of\n // what \"disabled\" means for a link.\n if (as === 'a') return inactive.value ? {} : { href }\n return {}\n})\n</script>\n\n<template>\n <component\n :is=\"as\"\n v-bind=\"linkProps\"\n :type=\"as === 'button' ? type : undefined\"\n :disabled=\"as === 'button' ? inactive : undefined\"\n :aria-disabled=\"as !== 'button' && inactive ? 'true' : undefined\"\n :aria-busy=\"loading\"\n :aria-pressed=\"pressed === undefined ? undefined : String(pressed)\"\n class=\"focus-visible:outline-primary focus-visible:outline-2 focus-visible:outline-offset-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50\"\n :class=\"[shell, surface, sizing, radius, block ? 'w-full' : '']\"\n >\n <span\n v-if=\"loading\"\n class=\"size-4 animate-spin rounded-full border-2 border-current border-t-transparent\"\n aria-hidden=\"true\"\n />\n <slot />\n </component>\n</template>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport type { HTMLAttributes } from 'vue'\n\n/**\n * The kit's button, and — when asked — its link.\n *\n * `as` exists because a button and a link are the same shape and a different\n * element, and the app was resolving that by nesting them: a consumer had\n * `<RouterLink><BaseButton>` in every call to action, which is an `<a>` around\n * a `<button>`. That is invalid HTML, two stops in the tab order and two\n * controls to a screen reader, for one thing on the screen. Whether something\n * navigates is the app's decision; carrying it is this component's job.\n *\n * `router-link` is resolved by name rather than imported, so `vue-router` stays\n * the optional peer it is. Only an app that passes `as=\"router-link\"` needs it,\n * and an app that passes it has it.\n */\nconst {\n as = 'button',\n variant = 'primary',\n size = 'md',\n loading = false,\n disabled = false,\n type = 'button',\n icon = false,\n block = false,\n pill = false,\n pressed = undefined,\n to = undefined,\n href = undefined,\n} = defineProps<\n {\n /** What to render. `button` unless this navigates. */\n as?: 'button' | 'a' | 'router-link' | undefined\n /**\n * `link` is a real action that should read as text — \"clear this note\",\n * \"remove\", \"change category\". It has no surface at all, so it also has no\n * height and no padding: giving it either would make it a ghost button,\n * which is a different thing and was already here.\n */\n variant?:\n | 'primary'\n | 'secondary'\n | 'ghost'\n | 'quiet'\n | 'destructive'\n | 'row'\n | 'danger'\n | 'positive'\n | 'warning'\n | 'accent'\n | 'link'\n | 'unstyled'\n | undefined\n /** `xs` is the action inside a prompt or a nudge, not on a page. */\n size?: 'xs' | 'sm' | 'md' | 'lg' | undefined\n loading?: boolean | undefined\n disabled?: boolean | undefined\n /** Ignored unless `as` is `button`. */\n type?: 'button' | 'submit' | undefined\n /**\n * Square, sized to its icon, with no label beside it.\n *\n * **Pass `aria-label`.** An icon on its own has no accessible name, and a\n * control a screen reader announces as \"button\" is not usable. Attributes\n * fall through, so `aria-label` lands where it should — nothing here can\n * check that you passed one, which is why it is said this loudly.\n */\n icon?: boolean | undefined\n /** Fills its container. The ordinary case under a form. */\n block?: boolean | undefined\n /** For `as=\"router-link\"`. */\n to?: string | Record<string, unknown> | undefined\n /** For `as=\"a\"`. */\n href?: string | undefined\n /**\n * Fully rounded rather than card-cornered.\n *\n * Every install prompt, update prompt and nudge across the apps used the\n * same pair — a filled pill to act and a quiet one to dismiss — and none of\n * them could use this component, because it only knew one corner radius.\n */\n pill?: boolean | undefined\n /**\n * That this button is a switch, and whether it is on.\n *\n * Omit it and the button is an action. Pass it and the button becomes a\n * toggle: `aria-pressed` is written, and the variants that have an \"off\"\n * look — ghost, quiet, secondary — take a filled one when on.\n *\n * There were 18 of these hand-written across the three apps, every one a\n * picker cell or a filter chip, and almost none of them said `aria-pressed`\n * at all. A screen reader met a row of identical buttons with no way to know\n * which was chosen.\n */\n pressed?: boolean | undefined\n /* The root element's own attributes and listeners — `@click`, `aria-label`,\n `role` — typed for editors and for vue-tsc's strictTemplates, where an\n undeclared one is an error. `@vue-ignore` keeps them out of the runtime\n props, so they still fall through to the element as before. */\n } & /* @vue-ignore */ HTMLAttributes\n>()\n\nconst VARIANT_CLASS = {\n primary: 'bg-primary text-on-primary shadow-(--shadow-control) hover:bg-primary/90',\n /*\n * An action that is real but not the one being urged.\n *\n * `ghost` had been standing in for this and cannot: with no border and no\n * fill it reads as text, so \"Save draft\" sitting next to \"Publish\" looked\n * like a caption rather than the other half of a choice. Ghost is for a\n * control that should recede until it is wanted — a toolbar, a menu row —\n * and that is a different job.\n */\n secondary: 'control text-ink hover:bg-muted',\n ghost: 'bg-transparent text-ink hover:bg-muted',\n /*\n * Quiet until you reach for it, and then plainly destructive: a delete at\n * the end of a row, a \"remove this note\", an archive.\n *\n * Not `danger`, which is filled and shouts before it is needed — a red\n * button in a list of rows makes the list look like a warning. And not\n * `quiet` with a `hover:text-negative` class beside it, which is how all\n * three apps were doing it: that class and the variant's own\n * `hover:text-ink` set the same property at the same specificity, so which\n * one wins depends on the order they happen to land in the stylesheet.\n *\n * Fifteen of these across the three apps, and every one of them was that\n * coin toss.\n */\n destructive: 'bg-transparent text-ink-soft hover:text-negative',\n /*\n * A line in a list that is also a control: a settings row, a node in a tree,\n * a heading that opens something.\n *\n * Full width, aligned to the start, and a hover that fills the whole line\n * rather than a box inside it. Every app had written this — `.tree-row`,\n * `.row`, `.header-action` — because a button that centres its content\n * cannot be a row, and the alignment is the only thing that had to change.\n *\n * Padding stays the app's: a menu row and a tree node are not the same\n * height, and the kit has no opinion about which one this is.\n */\n row: 'w-full justify-start text-left bg-transparent text-ink hover:bg-muted',\n /*\n * The control that is present without asking for attention: a dismiss beside\n * an install prompt, a chevron beside a month, a delete at the end of a row.\n *\n * `ghost` is not this. Ghost keeps full-strength ink; this one starts soft\n * and darkens, which is the difference between a control waiting to be used\n * and one that is merely available. The pair `text-ink-soft hover:text-ink`\n * was hand-written 47 times across the three apps.\n *\n * It fills on hover, and 0.11.0 got that half-right by fill... only for\n * icons. The evidence said otherwise once the third app was read: an editor\n * toolbar's buttons carry text and fill exactly the same way. The shape is\n * \"a control in a strip\", not \"a control with a glyph in it\". A text action\n * that should have no surface at all is `link`.\n */\n quiet: 'bg-transparent text-ink-soft hover:bg-muted hover:text-ink',\n danger: 'bg-negative text-on-negative shadow-(--shadow-control) hover:bg-negative/90',\n /*\n * The rest of the roles the kit already declares.\n *\n * `tokens.css` names five colour roles and this component exposed two of\n * them, so an app that wanted a success-coloured action had to hand-write\n * the button — which is what Hibi's green install button is. A component\n * that cannot use a role its own design system declares is not avoiding a\n * guess; it is incomplete.\n */\n positive: 'bg-positive text-on-positive shadow-(--shadow-control) hover:bg-positive/90',\n warning: 'bg-warning text-on-warning shadow-(--shadow-control) hover:bg-warning/90',\n accent: 'bg-accent text-on-accent shadow-(--shadow-control) hover:bg-accent/90',\n /* No fill, no border, no box: underlined so it is still obviously a control\n without one. `ghost` cannot stand in — it has a hover surface and a\n radius, so it reads as a button that happens to be empty. */\n link: 'bg-transparent underline underline-offset-2 hover:opacity-80',\n /*\n * Everything this component is, except the paint.\n *\n * The reason it exists is measurable: across the three apps there were 58\n * raw `<button>` elements sitting in 24 files that already imported and used\n * `BaseButton`. The developer reached for the kit and gave up halfway down\n * the same file — because the kit offered all of its appearance or none of\n * itself, and what those places needed was everything but the appearance.\n *\n * A picker cell, a chip, a calendar day: the surface is the app's, and it\n * should be. The element, the focus ring, the disabled handling, the\n * `aria-pressed` bookkeeping and the `as` switch are not, and were being\n * rewritten every time — usually without the focus ring.\n */\n unstyled: '',\n} as const\n\n/* Two scales, because a square control cannot take horizontal padding and\n still be square. `lg` is here for a wide page's call to action: a 44px\n button is right under a thumb and undersized under a headline. */\nconst SIZE_CLASS = {\n xs: 'h-8 px-3 text-xs',\n sm: 'h-9 px-3 text-sm',\n md: 'h-11 px-4 text-base',\n lg: 'h-14 px-6 text-lg',\n} as const\n\nconst ICON_SIZE_CLASS = {\n xs: 'size-8 text-xs',\n sm: 'size-9 text-sm',\n md: 'size-11 text-base',\n lg: 'size-14 text-lg',\n} as const\n\n/* A row is sized by its padding, not by a height. A settings line holds one\n line of text and a tree node can hold two, and a fixed height turns the\n second into an overflow. */\nconst ROW_SIZE_CLASS = {\n xs: 'px-2 py-1.5 text-xs',\n sm: 'px-3 py-2 text-sm',\n md: 'px-3 py-2.5 text-base',\n lg: 'px-4 py-3 text-lg',\n} as const\n\n/* A link takes the type size and nothing else. Height and padding are what\n make a surface, and this variant is the one without one. */\nconst LINK_SIZE_CLASS = {\n xs: 'text-xs',\n sm: 'text-sm',\n md: 'text-base',\n lg: 'text-lg',\n} as const\n\nconst sizing = computed(() => {\n // Unstyled owns no box, so it takes no size: the app's own classes decide.\n if (variant === 'unstyled') return ''\n if (variant === 'link') return LINK_SIZE_CLASS[size]\n if (variant === 'row') return ROW_SIZE_CLASS[size]\n return icon ? ICON_SIZE_CLASS[size] : SIZE_CLASS[size]\n})\n\n/* The variants with an \"off\" look, and what \"on\" looks like for them. The\n filled ones are already on; link and unstyled have no surface to fill. */\nconst PRESSED_CLASS: Partial<Record<string, string>> = {\n ghost: 'bg-primary text-on-primary hover:bg-primary/90',\n quiet: 'bg-primary text-on-primary hover:bg-primary/90',\n secondary: 'bg-primary border-primary text-on-primary hover:bg-primary/90',\n /* A selected row is filled, not recoloured: the line stays a line, and the\n fill is what a list uses to say \"this one\". Filling it with the primary\n colour instead would make one row of a list shout. */\n row: 'w-full justify-start text-left bg-muted text-ink hover:bg-muted',\n}\n\nconst surface = computed(() => {\n if (pressed === true) return PRESSED_CLASS[variant] ?? VARIANT_CLASS[variant]\n\n /* Destructive tints its own fill rather than borrowing the neutral one: a\n red glyph on a grey wash reads as two different states at once. */\n if (variant === 'destructive') return `${VARIANT_CLASS.destructive} hover:bg-negative/10`\n\n return VARIANT_CLASS[variant]\n})\n\n/* Layout and feel, which unstyled does not impose either — but the focus ring\n and the disabled handling stay, because those are the floor. A raw <button>\n is what happens when a component makes them optional.\n \n Colour is in the transition, not just transform. Every variant here changes\n colour on hover and none of them animated it, so every button in every\n consuming app snapped while the hand-written controls beside them faded —\n `transition-colors` appears 106 times across the three apps, which is the\n convention this component was the only thing not following. */\nconst shell = computed(() => {\n if (variant === 'unstyled') return ''\n\n const feel =\n 'transition-[transform,translate,scale,color,background-color,border-color,box-shadow] duration-(--duration-fast) ease-standard select-none'\n\n /* A row does not press. Scaling a full-width line looks like the list itself\n flinched, and every hand-written row in the apps animated colour only. */\n if (variant === 'row') return `inline-flex items-center gap-2 font-medium ${feel}`\n\n /* The press belongs to the material. A shrink by default; a material that\n draws hard offset shadows moves the button *into* its shadow instead, and\n drops the shadow, so pressing reads as pressing on every one of them. */\n return `inline-flex items-center justify-center gap-2 font-medium ${feel} active:scale-(--press-scale) active:translate-x-(--press-offset) active:translate-y-(--press-offset) active:shadow-(--shadow-control-pressed)`\n})\n\nconst radius = computed(() => {\n if (variant === 'unstyled') return ''\n if (variant === 'link') return 'rounded-xs'\n return pill ? 'rounded-full' : 'rounded-card'\n})\n\n/** Anything that is not a `<button>` cannot be `disabled`; it has to be told. */\nconst inactive = computed(() => disabled || loading)\n\nconst linkProps = computed(() => {\n if (as === 'router-link') return { to }\n // The href is dropped rather than kept alongside aria-disabled: an anchor\n // without one is not focusable and not activatable, which is the whole of\n // what \"disabled\" means for a link.\n if (as === 'a') return inactive.value ? {} : { href }\n return {}\n})\n</script>\n\n<template>\n <component\n :is=\"as\"\n v-bind=\"linkProps\"\n :type=\"as === 'button' ? type : undefined\"\n :disabled=\"as === 'button' ? inactive : undefined\"\n :aria-disabled=\"as !== 'button' && inactive ? 'true' : undefined\"\n :aria-busy=\"loading\"\n :aria-pressed=\"pressed === undefined ? undefined : String(pressed)\"\n class=\"focus-visible:outline-primary focus-visible:outline-2 focus-visible:outline-offset-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50\"\n :class=\"[shell, surface, sizing, radius, block ? 'w-full' : '']\"\n >\n <span\n v-if=\"loading\"\n class=\"size-4 animate-spin rounded-full border-2 border-current border-t-transparent\"\n aria-hidden=\"true\"\n />\n <slot />\n </component>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAwGA,MAAM,gBAAgB;GACpB,SAAS;GAUT,WAAW;GACX,OAAO;GAeP,aAAa;GAab,KAAK;GAgBL,OAAO;GACP,QAAQ;GAUR,UAAU;GACV,SAAS;GACT,QAAQ;GAIR,MAAM;GAeN,UAAU;EACZ;EAKA,MAAM,aAAa;GACjB,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;EACN;EAEA,MAAM,kBAAkB;GACtB,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;EACN;EAKA,MAAM,iBAAiB;GACrB,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;EACN;EAIA,MAAM,kBAAkB;GACtB,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;EACN;EAEA,MAAM,SAAS,eAAe;GAE5B,IAAI,QAAA,YAAY,YAAY,OAAO;GACnC,IAAI,QAAA,YAAY,QAAQ,OAAO,gBAAgB,QAAA;GAC/C,IAAI,QAAA,YAAY,OAAO,OAAO,eAAe,QAAA;GAC7C,OAAO,QAAA,OAAO,gBAAgB,QAAA,QAAQ,WAAW,QAAA;EACnD,CAAC;EAID,MAAM,gBAAiD;GACrD,OAAO;GACP,OAAO;GACP,WAAW;GAIX,KAAK;EACP;EAEA,MAAM,UAAU,eAAe;GAC7B,IAAI,QAAA,YAAY,MAAM,OAAO,cAAc,QAAA,YAAY,cAAc,QAAA;GAIrE,IAAI,QAAA,YAAY,eAAe,OAAO,GAAG,cAAc,YAAY;GAEnE,OAAO,cAAc,QAAA;EACvB,CAAC;EAWD,MAAM,QAAQ,eAAe;GAC3B,IAAI,QAAA,YAAY,YAAY,OAAO;GAEnC,MAAM,OACJ;GAIF,IAAI,QAAA,YAAY,OAAO,OAAO,8CAA8C;GAK5E,OAAO,6DAA6D,KAAK;EAC3E,CAAC;EAED,MAAM,SAAS,eAAe;GAC5B,IAAI,QAAA,YAAY,YAAY,OAAO;GACnC,IAAI,QAAA,YAAY,QAAQ,OAAO;GAC/B,OAAO,QAAA,OAAO,iBAAiB;EACjC,CAAC;;EAGD,MAAM,WAAW,eAAe,QAAA,YAAY,QAAA,OAAO;EAEnD,MAAM,YAAY,eAAe;GAC/B,IAAI,QAAA,OAAO,eAAe,OAAO,EAAE,IAAC,QAAA,GAAE;GAItC,IAAI,QAAA,OAAO,KAAK,OAAO,SAAS,QAAQ,CAAC,IAAI,EAAE,MAAG,QAAA,KAAE;GACpD,OAAO,CAAC;EACV,CAAC;;GAIC,OAAA,UAAA,GAAA,YAiBY,wBAhBL,QAAA,EAAE,GADT,WAEU,UAeE,OAfO;IAChB,MAAM,QAAA,OAAE,WAAgB,QAAA,OAAO,KAAA;IAC/B,UAAU,QAAA,OAAE,WAAgB,SAAA,QAAW,KAAA;IACvC,iBAAe,QAAA,OAAE,YAAiB,SAAA,QAAQ,SAAY,KAAA;IACtD,aAAW,QAAA;IACX,gBAAc,QAAA,YAAY,KAAA,IAAY,KAAA,IAAY,OAAO,QAAA,OAAO;IACjE,OAAK,CAAC,oMAAkM;KAC/L,MAAA;KAAO,QAAA;KAAS,OAAA;KAAQ,OAAA;KAAQ,QAAA,QAAK,WAAA;IAAA,CAAA;;IAE9C,SAAA,cAIE,CAHM,QAAA,WADR,UAAA,GAAA,mBAIE,QAJF,UAIE,KAAA,mBAAA,IAAA,IAAA,GACF,WAAQ,KAAA,QAAA,SAAA,CAAA,CAAA"}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { n as useBoundValue } from "./inert-BRQcZwpX.js";
|
|
2
|
+
import { t as _plugin_vue_export_helper_default } from "./_plugin-vue_export-helper-BOaGB7Aw.js";
|
|
3
|
+
import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, defineComponent, normalizeClass, normalizeStyle, openBlock, ref, renderList, toDisplayString, unref, useId } from "vue";
|
|
4
|
+
import { Check } from "lucide-vue-next";
|
|
5
|
+
//#region src/components/BaseListbox.vue?vue&type=script&setup=true&lang.ts
|
|
6
|
+
var _hoisted_1 = [
|
|
7
|
+
"aria-label",
|
|
8
|
+
"aria-multiselectable",
|
|
9
|
+
"aria-activedescendant"
|
|
10
|
+
];
|
|
11
|
+
var _hoisted_2 = [
|
|
12
|
+
"id",
|
|
13
|
+
"aria-selected",
|
|
14
|
+
"aria-disabled",
|
|
15
|
+
"onClick"
|
|
16
|
+
];
|
|
17
|
+
var _hoisted_3 = { class: "min-w-0 flex-1" };
|
|
18
|
+
var _hoisted_4 = { class: "rk-listbox-label" };
|
|
19
|
+
var _hoisted_5 = {
|
|
20
|
+
key: 0,
|
|
21
|
+
class: "rk-listbox-description"
|
|
22
|
+
};
|
|
23
|
+
var _hoisted_6 = {
|
|
24
|
+
key: 0,
|
|
25
|
+
class: "rk-listbox-empty"
|
|
26
|
+
};
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/components/BaseListbox.vue
|
|
29
|
+
var BaseListbox_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__ */ defineComponent({
|
|
30
|
+
__name: "BaseListbox",
|
|
31
|
+
props: {
|
|
32
|
+
modelValue: { default: () => void 0 },
|
|
33
|
+
options: {},
|
|
34
|
+
label: {},
|
|
35
|
+
mode: { default: "single" },
|
|
36
|
+
height: { default: "14rem" }
|
|
37
|
+
},
|
|
38
|
+
emits: ["update:modelValue"],
|
|
39
|
+
setup(__props, { emit: __emit }) {
|
|
40
|
+
const emit = __emit;
|
|
41
|
+
const model = useBoundValue(() => __props.modelValue, (value) => emit("update:modelValue", value));
|
|
42
|
+
const id = useId();
|
|
43
|
+
const active = ref(0);
|
|
44
|
+
let typed = "";
|
|
45
|
+
let typedAt = 0;
|
|
46
|
+
const chosen = computed(() => {
|
|
47
|
+
const value = model.value;
|
|
48
|
+
if (Array.isArray(value)) return new Set(value);
|
|
49
|
+
return new Set(value === void 0 ? [] : [value]);
|
|
50
|
+
});
|
|
51
|
+
function select(option) {
|
|
52
|
+
if (option.disabled) return;
|
|
53
|
+
if (__props.mode === "multiple") {
|
|
54
|
+
const next = new Set(chosen.value);
|
|
55
|
+
if (next.has(option.value)) next.delete(option.value);
|
|
56
|
+
else next.add(option.value);
|
|
57
|
+
model.value = __props.options.map((one) => one.value).filter((value) => next.has(value));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
model.value = option.value;
|
|
61
|
+
}
|
|
62
|
+
function moveTo(index) {
|
|
63
|
+
if (__props.options.length === 0) return;
|
|
64
|
+
active.value = Math.max(0, Math.min(__props.options.length - 1, index));
|
|
65
|
+
const option = __props.options[active.value];
|
|
66
|
+
if (__props.mode === "single" && option && !option.disabled) select(option);
|
|
67
|
+
document.getElementById(`${id}-${active.value}`)?.scrollIntoView?.({ block: "nearest" });
|
|
68
|
+
}
|
|
69
|
+
/** The next option after the current one that starts with what was typed. */
|
|
70
|
+
function jumpTo(letter) {
|
|
71
|
+
const now = Date.now();
|
|
72
|
+
typed = now - typedAt > 700 ? letter : typed + letter;
|
|
73
|
+
typedAt = now;
|
|
74
|
+
const from = typed.length === 1 ? active.value + 1 : active.value;
|
|
75
|
+
const found = [...__props.options.slice(from), ...__props.options.slice(0, from)].find((option) => !option.disabled && option.label.toLowerCase().startsWith(typed.toLowerCase()));
|
|
76
|
+
if (found) moveTo(__props.options.indexOf(found));
|
|
77
|
+
}
|
|
78
|
+
function onKeydown(event) {
|
|
79
|
+
const moves = {
|
|
80
|
+
ArrowDown: active.value + 1,
|
|
81
|
+
ArrowUp: active.value - 1,
|
|
82
|
+
Home: 0,
|
|
83
|
+
End: __props.options.length - 1
|
|
84
|
+
};
|
|
85
|
+
if (event.key in moves) {
|
|
86
|
+
event.preventDefault();
|
|
87
|
+
moveTo(moves[event.key]);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (event.key === " " || event.key === "Enter") {
|
|
91
|
+
const option = __props.options[active.value];
|
|
92
|
+
if (!option) return;
|
|
93
|
+
event.preventDefault();
|
|
94
|
+
select(option);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (event.key.length === 1 && !event.metaKey && !event.ctrlKey && !event.altKey) jumpTo(event.key);
|
|
98
|
+
}
|
|
99
|
+
return (_ctx, _cache) => {
|
|
100
|
+
return openBlock(), createElementBlock("div", {
|
|
101
|
+
class: "rk-listbox control",
|
|
102
|
+
style: normalizeStyle({ maxHeight: __props.height }),
|
|
103
|
+
role: "listbox",
|
|
104
|
+
tabindex: "0",
|
|
105
|
+
"aria-label": __props.label,
|
|
106
|
+
"aria-multiselectable": __props.mode === "multiple" ? true : void 0,
|
|
107
|
+
"aria-activedescendant": __props.options.length ? `${unref(id)}-${active.value}` : void 0,
|
|
108
|
+
onKeydown
|
|
109
|
+
}, [(openBlock(true), createElementBlock(Fragment, null, renderList(__props.options, (option, index) => {
|
|
110
|
+
return openBlock(), createElementBlock("div", {
|
|
111
|
+
id: `${unref(id)}-${index}`,
|
|
112
|
+
key: option.value,
|
|
113
|
+
class: normalizeClass(["rk-listbox-option", {
|
|
114
|
+
"is-active": index === active.value,
|
|
115
|
+
"is-chosen": chosen.value.has(option.value),
|
|
116
|
+
"is-disabled": option.disabled
|
|
117
|
+
}]),
|
|
118
|
+
role: "option",
|
|
119
|
+
"aria-selected": chosen.value.has(option.value),
|
|
120
|
+
"aria-disabled": option.disabled,
|
|
121
|
+
onClick: ($event) => (active.value = index, select(option))
|
|
122
|
+
}, [createElementVNode("span", _hoisted_3, [createElementVNode("span", _hoisted_4, toDisplayString(option.label), 1), option.description ? (openBlock(), createElementBlock("span", _hoisted_5, toDisplayString(option.description), 1)) : createCommentVNode("", true)]), chosen.value.has(option.value) ? (openBlock(), createBlock(unref(Check), {
|
|
123
|
+
key: 0,
|
|
124
|
+
class: "size-4 shrink-0",
|
|
125
|
+
"aria-hidden": "true"
|
|
126
|
+
})) : createCommentVNode("", true)], 10, _hoisted_2);
|
|
127
|
+
}), 128)), __props.options.length === 0 ? (openBlock(), createElementBlock("p", _hoisted_6)) : createCommentVNode("", true)], 44, _hoisted_1);
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}), [["__scopeId", "data-v-95e230a5"]]);
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region src/components/BaseSkeleton.vue
|
|
133
|
+
var BaseSkeleton_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__ */ defineComponent({
|
|
134
|
+
__name: "BaseSkeleton",
|
|
135
|
+
props: {
|
|
136
|
+
width: { default: "100%" },
|
|
137
|
+
height: { default: "1rem" },
|
|
138
|
+
shape: { default: "block" }
|
|
139
|
+
},
|
|
140
|
+
setup(__props) {
|
|
141
|
+
/**
|
|
142
|
+
* One grey box standing in for content that has not arrived.
|
|
143
|
+
*
|
|
144
|
+
* `SkeletonList` is rows of these for a list; this is the primitive, for the
|
|
145
|
+
* shapes a list does not cover — an avatar, a heading, a chart.
|
|
146
|
+
*
|
|
147
|
+
* A height is a CSS length, never a class: `h-4` inside a component the app
|
|
148
|
+
* does not control would render at zero the day that utility is not in the
|
|
149
|
+
* app's stylesheet, which is exactly how every skeleton in one app came out
|
|
150
|
+
* invisible.
|
|
151
|
+
*
|
|
152
|
+
* It is hidden from assistive tech. The thing that is loading says so —
|
|
153
|
+
* through `aria-busy`, or a status message — and a screen reader reading
|
|
154
|
+
* "blank, blank, blank" helps nobody.
|
|
155
|
+
*/
|
|
156
|
+
return (_ctx, _cache) => {
|
|
157
|
+
return openBlock(), createElementBlock("span", {
|
|
158
|
+
class: normalizeClass(["rk-skeleton", `is-${__props.shape}`]),
|
|
159
|
+
style: normalizeStyle({
|
|
160
|
+
width: __props.shape === "circle" ? __props.height : __props.width,
|
|
161
|
+
height: __props.height
|
|
162
|
+
}),
|
|
163
|
+
"aria-hidden": "true"
|
|
164
|
+
}, null, 6);
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}), [["__scopeId", "data-v-37327de6"]]);
|
|
168
|
+
//#endregion
|
|
169
|
+
export { BaseListbox_default as n, BaseSkeleton_default as t };
|
|
170
|
+
|
|
171
|
+
//# sourceMappingURL=BaseSkeleton-BWTNXVwP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BaseSkeleton-BWTNXVwP.js","names":[],"sources":["../src/components/BaseListbox.vue","../src/components/BaseListbox.vue","../src/components/BaseSkeleton.vue","../src/components/BaseSkeleton.vue"],"sourcesContent":["<script setup lang=\"ts\" generic=\"V extends string, M extends 'single' | 'multiple' = 'single'\">\nimport { Check } from 'lucide-vue-next'\nimport { computed, ref, useId } from 'vue'\n\nimport { useBoundValue } from '../composables/use-bound-value'\n\nexport interface ListboxOption<V extends string> {\n value: V\n /** Already translated. */\n label: string\n /** A line under the label. */\n description?: string | undefined\n disabled?: boolean | undefined\n}\n\n/**\n * A list you choose from, open on the page.\n *\n * Between `BaseRadioGroup` and `BaseSelect`: a radio group is a handful of\n * options that are all visible and all equal; a select opens the platform's\n * own picker; this is a scrollable list of many, choosing one or several,\n * that stays on screen — a list of accounts, of tags, of files to act on.\n *\n * One Tab stop. The list itself takes focus and `aria-activedescendant`\n * says which option is current, so the arrows move without thirty stops in\n * the tab order. Typing a letter jumps to the next option that starts with\n * it, which is how every native list has always worked and the first thing\n * people try.\n *\n * In `single` mode the selection follows the arrows, because that is what a\n * listbox does; in `multiple` mode Space and Enter toggle the current one.\n */\nconst {\n modelValue = undefined,\n options,\n label,\n mode = 'single' as M,\n height = '14rem',\n} = defineProps<{\n /** The chosen option, or options, with `v-model`. */\n modelValue?: Value | undefined\n options: readonly ListboxOption<V>[]\n /** The list's accessible name. */\n label: string\n /** One option, or any number of them. */\n mode?: M | undefined\n /** How tall before it scrolls. Any CSS length. */\n height?: string | undefined\n}>()\n\n/* Multiple always hands back an array — toggling one produces a list, never\n nothing — so only the single value can be undefined. */\ntype Value = M extends 'multiple' ? V[] : V | undefined\n\n/* Declared by hand rather than with defineModel, which cannot both accept\n `undefined` and promise never to emit it. See `use-bound-value.ts`. */\nconst emit = defineEmits<{ 'update:modelValue': [value: Value] }>()\nconst model = useBoundValue<Value>(\n () => modelValue as Value | undefined,\n (value) => emit('update:modelValue', value),\n)\n\nconst id = useId()\nconst active = ref(0)\n\nlet typed = ''\nlet typedAt = 0\n\nconst chosen = computed(() => {\n const value = model.value\n if (Array.isArray(value)) return new Set<V>(value)\n\n return new Set<V>(value === undefined ? [] : [value as V])\n})\n\nfunction select(option: ListboxOption<V>) {\n if (option.disabled) return\n\n if (mode === 'multiple') {\n const next = new Set(chosen.value)\n if (next.has(option.value)) next.delete(option.value)\n else next.add(option.value)\n\n // In the order of the options, so the value reads the same however the\n // reader got there.\n model.value = options\n .map((one) => one.value)\n .filter((value) => next.has(value)) as typeof model.value\n return\n }\n\n model.value = option.value as typeof model.value\n}\n\nfunction moveTo(index: number) {\n if (options.length === 0) return\n\n active.value = Math.max(0, Math.min(options.length - 1, index))\n const option = options[active.value]\n\n // Single: the selection follows the focus, which is what a listbox does.\n if (mode === 'single' && option && !option.disabled) select(option)\n\n document.getElementById(`${id}-${active.value}`)?.scrollIntoView?.({ block: 'nearest' })\n}\n\n/** The next option after the current one that starts with what was typed. */\nfunction jumpTo(letter: string) {\n const now = Date.now()\n typed = now - typedAt > 700 ? letter : typed + letter\n typedAt = now\n\n const from = typed.length === 1 ? active.value + 1 : active.value\n const order = [...options.slice(from), ...options.slice(0, from)]\n const found = order.find(\n (option) => !option.disabled && option.label.toLowerCase().startsWith(typed.toLowerCase()),\n )\n\n if (found) moveTo(options.indexOf(found))\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n const moves: Record<string, number> = {\n ArrowDown: active.value + 1,\n ArrowUp: active.value - 1,\n Home: 0,\n End: options.length - 1,\n }\n\n if (event.key in moves) {\n event.preventDefault()\n moveTo(moves[event.key]!)\n return\n }\n\n if (event.key === ' ' || event.key === 'Enter') {\n const option = options[active.value]\n if (!option) return\n event.preventDefault()\n select(option)\n return\n }\n\n // A single printable character: the typeahead every native list has.\n if (event.key.length === 1 && !event.metaKey && !event.ctrlKey && !event.altKey) {\n jumpTo(event.key)\n }\n}\n</script>\n\n<template>\n <div\n class=\"rk-listbox control\"\n :style=\"{ maxHeight: height }\"\n role=\"listbox\"\n tabindex=\"0\"\n :aria-label=\"label\"\n :aria-multiselectable=\"mode === 'multiple' ? true : undefined\"\n :aria-activedescendant=\"options.length ? `${id}-${active}` : undefined\"\n @keydown=\"onKeydown\"\n >\n <div\n v-for=\"(option, index) in options\"\n :id=\"`${id}-${index}`\"\n :key=\"option.value\"\n class=\"rk-listbox-option\"\n :class=\"{\n 'is-active': index === active,\n 'is-chosen': chosen.has(option.value),\n 'is-disabled': option.disabled,\n }\"\n role=\"option\"\n :aria-selected=\"chosen.has(option.value)\"\n :aria-disabled=\"option.disabled\"\n @click=\"((active = index), select(option))\"\n >\n <span class=\"min-w-0 flex-1\">\n <span class=\"rk-listbox-label\">{{ option.label }}</span>\n <span v-if=\"option.description\" class=\"rk-listbox-description\">{{\n option.description\n }}</span>\n </span>\n\n <Check v-if=\"chosen.has(option.value)\" class=\"size-4 shrink-0\" aria-hidden=\"true\" />\n </div>\n\n <p v-if=\"options.length === 0\" class=\"rk-listbox-empty\" />\n </div>\n</template>\n\n<style scoped>\n.rk-listbox {\n overflow-y: auto;\n border-radius: var(--radius-card);\n padding: 0.25rem;\n}\n\n.rk-listbox:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 1px;\n}\n\n.rk-listbox-option {\n display: flex;\n cursor: pointer;\n align-items: center;\n gap: 0.625rem;\n border-radius: var(--radius-cell);\n padding: 0.5rem 0.625rem;\n font-size: 0.875rem;\n color: var(--color-ink);\n}\n\n.rk-listbox-option.is-active {\n background: var(--color-muted);\n}\n\n.rk-listbox-option.is-chosen {\n color: var(--color-primary);\n font-weight: 500;\n}\n\n/* Both at once: the current option and the chosen one are different things,\n and a list that shows only one of them loses the reader's place. */\n.rk-listbox-option.is-chosen.is-active {\n background: color-mix(in oklab, var(--color-primary) 12%, transparent);\n}\n\n.rk-listbox-option.is-disabled {\n cursor: not-allowed;\n opacity: 0.45;\n}\n\n.rk-listbox-label {\n display: block;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rk-listbox-description {\n display: block;\n font-size: 0.75rem;\n color: var(--color-ink-soft);\n}\n\n.rk-listbox-empty {\n padding: 1rem;\n}\n</style>\n","<script setup lang=\"ts\" generic=\"V extends string, M extends 'single' | 'multiple' = 'single'\">\nimport { Check } from 'lucide-vue-next'\nimport { computed, ref, useId } from 'vue'\n\nimport { useBoundValue } from '../composables/use-bound-value'\n\nexport interface ListboxOption<V extends string> {\n value: V\n /** Already translated. */\n label: string\n /** A line under the label. */\n description?: string | undefined\n disabled?: boolean | undefined\n}\n\n/**\n * A list you choose from, open on the page.\n *\n * Between `BaseRadioGroup` and `BaseSelect`: a radio group is a handful of\n * options that are all visible and all equal; a select opens the platform's\n * own picker; this is a scrollable list of many, choosing one or several,\n * that stays on screen — a list of accounts, of tags, of files to act on.\n *\n * One Tab stop. The list itself takes focus and `aria-activedescendant`\n * says which option is current, so the arrows move without thirty stops in\n * the tab order. Typing a letter jumps to the next option that starts with\n * it, which is how every native list has always worked and the first thing\n * people try.\n *\n * In `single` mode the selection follows the arrows, because that is what a\n * listbox does; in `multiple` mode Space and Enter toggle the current one.\n */\nconst {\n modelValue = undefined,\n options,\n label,\n mode = 'single' as M,\n height = '14rem',\n} = defineProps<{\n /** The chosen option, or options, with `v-model`. */\n modelValue?: Value | undefined\n options: readonly ListboxOption<V>[]\n /** The list's accessible name. */\n label: string\n /** One option, or any number of them. */\n mode?: M | undefined\n /** How tall before it scrolls. Any CSS length. */\n height?: string | undefined\n}>()\n\n/* Multiple always hands back an array — toggling one produces a list, never\n nothing — so only the single value can be undefined. */\ntype Value = M extends 'multiple' ? V[] : V | undefined\n\n/* Declared by hand rather than with defineModel, which cannot both accept\n `undefined` and promise never to emit it. See `use-bound-value.ts`. */\nconst emit = defineEmits<{ 'update:modelValue': [value: Value] }>()\nconst model = useBoundValue<Value>(\n () => modelValue as Value | undefined,\n (value) => emit('update:modelValue', value),\n)\n\nconst id = useId()\nconst active = ref(0)\n\nlet typed = ''\nlet typedAt = 0\n\nconst chosen = computed(() => {\n const value = model.value\n if (Array.isArray(value)) return new Set<V>(value)\n\n return new Set<V>(value === undefined ? [] : [value as V])\n})\n\nfunction select(option: ListboxOption<V>) {\n if (option.disabled) return\n\n if (mode === 'multiple') {\n const next = new Set(chosen.value)\n if (next.has(option.value)) next.delete(option.value)\n else next.add(option.value)\n\n // In the order of the options, so the value reads the same however the\n // reader got there.\n model.value = options\n .map((one) => one.value)\n .filter((value) => next.has(value)) as typeof model.value\n return\n }\n\n model.value = option.value as typeof model.value\n}\n\nfunction moveTo(index: number) {\n if (options.length === 0) return\n\n active.value = Math.max(0, Math.min(options.length - 1, index))\n const option = options[active.value]\n\n // Single: the selection follows the focus, which is what a listbox does.\n if (mode === 'single' && option && !option.disabled) select(option)\n\n document.getElementById(`${id}-${active.value}`)?.scrollIntoView?.({ block: 'nearest' })\n}\n\n/** The next option after the current one that starts with what was typed. */\nfunction jumpTo(letter: string) {\n const now = Date.now()\n typed = now - typedAt > 700 ? letter : typed + letter\n typedAt = now\n\n const from = typed.length === 1 ? active.value + 1 : active.value\n const order = [...options.slice(from), ...options.slice(0, from)]\n const found = order.find(\n (option) => !option.disabled && option.label.toLowerCase().startsWith(typed.toLowerCase()),\n )\n\n if (found) moveTo(options.indexOf(found))\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n const moves: Record<string, number> = {\n ArrowDown: active.value + 1,\n ArrowUp: active.value - 1,\n Home: 0,\n End: options.length - 1,\n }\n\n if (event.key in moves) {\n event.preventDefault()\n moveTo(moves[event.key]!)\n return\n }\n\n if (event.key === ' ' || event.key === 'Enter') {\n const option = options[active.value]\n if (!option) return\n event.preventDefault()\n select(option)\n return\n }\n\n // A single printable character: the typeahead every native list has.\n if (event.key.length === 1 && !event.metaKey && !event.ctrlKey && !event.altKey) {\n jumpTo(event.key)\n }\n}\n</script>\n\n<template>\n <div\n class=\"rk-listbox control\"\n :style=\"{ maxHeight: height }\"\n role=\"listbox\"\n tabindex=\"0\"\n :aria-label=\"label\"\n :aria-multiselectable=\"mode === 'multiple' ? true : undefined\"\n :aria-activedescendant=\"options.length ? `${id}-${active}` : undefined\"\n @keydown=\"onKeydown\"\n >\n <div\n v-for=\"(option, index) in options\"\n :id=\"`${id}-${index}`\"\n :key=\"option.value\"\n class=\"rk-listbox-option\"\n :class=\"{\n 'is-active': index === active,\n 'is-chosen': chosen.has(option.value),\n 'is-disabled': option.disabled,\n }\"\n role=\"option\"\n :aria-selected=\"chosen.has(option.value)\"\n :aria-disabled=\"option.disabled\"\n @click=\"((active = index), select(option))\"\n >\n <span class=\"min-w-0 flex-1\">\n <span class=\"rk-listbox-label\">{{ option.label }}</span>\n <span v-if=\"option.description\" class=\"rk-listbox-description\">{{\n option.description\n }}</span>\n </span>\n\n <Check v-if=\"chosen.has(option.value)\" class=\"size-4 shrink-0\" aria-hidden=\"true\" />\n </div>\n\n <p v-if=\"options.length === 0\" class=\"rk-listbox-empty\" />\n </div>\n</template>\n\n<style scoped>\n.rk-listbox {\n overflow-y: auto;\n border-radius: var(--radius-card);\n padding: 0.25rem;\n}\n\n.rk-listbox:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 1px;\n}\n\n.rk-listbox-option {\n display: flex;\n cursor: pointer;\n align-items: center;\n gap: 0.625rem;\n border-radius: var(--radius-cell);\n padding: 0.5rem 0.625rem;\n font-size: 0.875rem;\n color: var(--color-ink);\n}\n\n.rk-listbox-option.is-active {\n background: var(--color-muted);\n}\n\n.rk-listbox-option.is-chosen {\n color: var(--color-primary);\n font-weight: 500;\n}\n\n/* Both at once: the current option and the chosen one are different things,\n and a list that shows only one of them loses the reader's place. */\n.rk-listbox-option.is-chosen.is-active {\n background: color-mix(in oklab, var(--color-primary) 12%, transparent);\n}\n\n.rk-listbox-option.is-disabled {\n cursor: not-allowed;\n opacity: 0.45;\n}\n\n.rk-listbox-label {\n display: block;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rk-listbox-description {\n display: block;\n font-size: 0.75rem;\n color: var(--color-ink-soft);\n}\n\n.rk-listbox-empty {\n padding: 1rem;\n}\n</style>\n","<script setup lang=\"ts\">\n/**\n * One grey box standing in for content that has not arrived.\n *\n * `SkeletonList` is rows of these for a list; this is the primitive, for the\n * shapes a list does not cover — an avatar, a heading, a chart.\n *\n * A height is a CSS length, never a class: `h-4` inside a component the app\n * does not control would render at zero the day that utility is not in the\n * app's stylesheet, which is exactly how every skeleton in one app came out\n * invisible.\n *\n * It is hidden from assistive tech. The thing that is loading says so —\n * through `aria-busy`, or a status message — and a screen reader reading\n * \"blank, blank, blank\" helps nobody.\n */\nconst {\n width = '100%',\n height = '1rem',\n shape = 'block',\n} = defineProps<{\n /** Any CSS length: `12rem`, `60%`, `8ch`. */\n width?: string | undefined\n height?: string | undefined\n /** `circle` makes a round one and squares the width to the height. */\n shape?: 'block' | 'text' | 'circle' | undefined\n}>()\n</script>\n\n<template>\n <span\n class=\"rk-skeleton\"\n :class=\"`is-${shape}`\"\n :style=\"{ width: shape === 'circle' ? height : width, height }\"\n aria-hidden=\"true\"\n />\n</template>\n\n<style scoped>\n.rk-skeleton {\n display: block;\n flex-shrink: 0;\n background: var(--color-muted);\n animation: rk-skeleton-pulse 1.6s ease-in-out infinite;\n}\n\n.rk-skeleton.is-block {\n border-radius: var(--radius-cell);\n}\n\n/* A line of text, rounded like one and a little short of its box. */\n.rk-skeleton.is-text {\n border-radius: 9999px;\n}\n\n.rk-skeleton.is-circle {\n border-radius: 9999px;\n}\n\n@keyframes rk-skeleton-pulse {\n 50% {\n opacity: 0.55;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .rk-skeleton {\n animation: none;\n }\n}\n</style>\n","<script setup lang=\"ts\">\n/**\n * One grey box standing in for content that has not arrived.\n *\n * `SkeletonList` is rows of these for a list; this is the primitive, for the\n * shapes a list does not cover — an avatar, a heading, a chart.\n *\n * A height is a CSS length, never a class: `h-4` inside a component the app\n * does not control would render at zero the day that utility is not in the\n * app's stylesheet, which is exactly how every skeleton in one app came out\n * invisible.\n *\n * It is hidden from assistive tech. The thing that is loading says so —\n * through `aria-busy`, or a status message — and a screen reader reading\n * \"blank, blank, blank\" helps nobody.\n */\nconst {\n width = '100%',\n height = '1rem',\n shape = 'block',\n} = defineProps<{\n /** Any CSS length: `12rem`, `60%`, `8ch`. */\n width?: string | undefined\n height?: string | undefined\n /** `circle` makes a round one and squares the width to the height. */\n shape?: 'block' | 'text' | 'circle' | undefined\n}>()\n</script>\n\n<template>\n <span\n class=\"rk-skeleton\"\n :class=\"`is-${shape}`\"\n :style=\"{ width: shape === 'circle' ? height : width, height }\"\n aria-hidden=\"true\"\n />\n</template>\n\n<style scoped>\n.rk-skeleton {\n display: block;\n flex-shrink: 0;\n background: var(--color-muted);\n animation: rk-skeleton-pulse 1.6s ease-in-out infinite;\n}\n\n.rk-skeleton.is-block {\n border-radius: var(--radius-cell);\n}\n\n/* A line of text, rounded like one and a little short of its box. */\n.rk-skeleton.is-text {\n border-radius: 9999px;\n}\n\n.rk-skeleton.is-circle {\n border-radius: 9999px;\n}\n\n@keyframes rk-skeleton-pulse {\n 50% {\n opacity: 0.55;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .rk-skeleton {\n animation: none;\n }\n}\n</style>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAwDA,MAAM,OAAO;EACb,MAAM,QAAQ,oBACN,QAAA,aACL,UAAU,KAAK,qBAAqB,KAAK,CAC5C;EAEA,MAAM,KAAK,MAAM;EACjB,MAAM,SAAS,IAAI,CAAC;EAEpB,IAAI,QAAQ;EACZ,IAAI,UAAU;EAEd,MAAM,SAAS,eAAe;GAC5B,MAAM,QAAQ,MAAM;GACpB,IAAI,MAAM,QAAQ,KAAK,GAAG,OAAO,IAAI,IAAO,KAAK;GAEjD,OAAO,IAAI,IAAO,UAAU,KAAA,IAAY,CAAC,IAAI,CAAC,KAAU,CAAC;EAC3D,CAAC;EAED,SAAS,OAAO,QAA0B;GACxC,IAAI,OAAO,UAAU;GAErB,IAAI,QAAA,SAAS,YAAY;IACvB,MAAM,OAAO,IAAI,IAAI,OAAO,KAAK;IACjC,IAAI,KAAK,IAAI,OAAO,KAAK,GAAG,KAAK,OAAO,OAAO,KAAK;SAC/C,KAAK,IAAI,OAAO,KAAK;IAI1B,MAAM,QAAQ,QAAA,QACX,KAAK,QAAQ,IAAI,KAAK,CAAA,CACtB,QAAQ,UAAU,KAAK,IAAI,KAAK,CAAC;IACpC;GACF;GAEA,MAAM,QAAQ,OAAO;EACvB;EAEA,SAAS,OAAO,OAAe;GAC7B,IAAI,QAAA,QAAQ,WAAW,GAAG;GAE1B,OAAO,QAAQ,KAAK,IAAI,GAAG,KAAK,IAAI,QAAA,QAAQ,SAAS,GAAG,KAAK,CAAC;GAC9D,MAAM,SAAS,QAAA,QAAQ,OAAO;GAG9B,IAAI,QAAA,SAAS,YAAY,UAAU,CAAC,OAAO,UAAU,OAAO,MAAM;GAElE,SAAS,eAAe,GAAG,GAAG,GAAG,OAAO,OAAO,CAAC,EAAE,iBAAiB,EAAE,OAAO,UAAU,CAAC;EACzF;;EAGA,SAAS,OAAO,QAAgB;GAC9B,MAAM,MAAM,KAAK,IAAI;GACrB,QAAQ,MAAM,UAAU,MAAM,SAAS,QAAQ;GAC/C,UAAU;GAEV,MAAM,OAAO,MAAM,WAAW,IAAI,OAAO,QAAQ,IAAI,OAAO;GAE5D,MAAM,QAAQ,CADC,GAAG,QAAA,QAAQ,MAAM,IAAI,GAAG,GAAG,QAAA,QAAQ,MAAM,GAAG,IAAI,CACjD,CAAA,CAAM,MACjB,WAAW,CAAC,OAAO,YAAY,OAAO,MAAM,YAAY,CAAC,CAAC,WAAW,MAAM,YAAY,CAAC,CAC3F;GAEA,IAAI,OAAO,OAAO,QAAA,QAAQ,QAAQ,KAAK,CAAC;EAC1C;EAEA,SAAS,UAAU,OAAsB;GACvC,MAAM,QAAgC;IACpC,WAAW,OAAO,QAAQ;IAC1B,SAAS,OAAO,QAAQ;IACxB,MAAM;IACN,KAAK,QAAA,QAAQ,SAAS;GACxB;GAEA,IAAI,MAAM,OAAO,OAAO;IACtB,MAAM,eAAe;IACrB,OAAO,MAAM,MAAM,IAAK;IACxB;GACF;GAEA,IAAI,MAAM,QAAQ,OAAO,MAAM,QAAQ,SAAS;IAC9C,MAAM,SAAS,QAAA,QAAQ,OAAO;IAC9B,IAAI,CAAC,QAAQ;IACb,MAAM,eAAe;IACrB,OAAO,MAAM;IACb;GACF;GAGA,IAAI,MAAM,IAAI,WAAW,KAAK,CAAC,MAAM,WAAW,CAAC,MAAM,WAAW,CAAC,MAAM,QACvE,OAAO,MAAM,GAAG;EAEpB;;GAIE,OAAA,UAAA,GAAA,mBAoCM,OAAA;IAnCJ,OAAM;IACL,OAAK,eAAA,EAAA,WAAe,QAAA,OAAM,CAAA;IAC3B,MAAK;IACL,UAAS;IACR,cAAY,QAAA;IACZ,wBAAsB,QAAA,SAAI,aAAA,OAAyB,KAAA;IACnD,yBAAuB,QAAA,QAAQ,SAAM,GAAM,MAAA,EAAA,EAAE,GAAI,OAAA,UAAW,KAAA;IACnD;GAEV,GAAA,EAAA,UAAA,IAAA,GAAA,mBAuBM,UAAA,MAAA,WAtBsB,QAAA,UAAlB,QAAQ,UAAK;IADvB,OAAA,UAAA,GAAA,mBAuBM,OAAA;KArBH,IAAE,GAAK,MAAA,EAAA,EAAE,GAAI;KACb,KAAK,OAAO;KACb,OAAK,eAAA,CAAC,qBAAmB;MACM,aAAA,UAAU,OAAA;MAA6B,aAAA,OAAA,MAAO,IAAI,OAAO,KAAK;MAA0B,eAAA,OAAO;;KAK9H,MAAK;KACJ,iBAAe,OAAA,MAAO,IAAI,OAAO,KAAK;KACtC,iBAAe,OAAO;KACtB,UAAK,YAAI,OAAA,QAAS,OAAQ,OAAO,MAAM;IAExC,GAAA,CAAA,mBAKO,QALP,YAKO,CAJL,mBAAwD,QAAxD,YAAwD,gBAAtB,OAAO,KAAK,GAAA,CAAA,GAClC,OAAO,eAAnB,UAAA,GAAA,mBAES,QAFT,YAES,gBADP,OAAO,WAAW,GAAA,CAAA,KAAA,mBAAA,IAAA,IAAA,CAAA,CAAA,GAIT,OAAA,MAAO,IAAI,OAAO,KAAK,KAApC,UAAA,GAAA,YAAoF,MAAA,KAAA,GAAA;;KAA7C,OAAM;KAAkB,eAAY;;GAGpE,CAAA,GAAA,GAAA,IAAA,QAAA,QAAQ,WAAM,KAAvB,UAAA,GAAA,mBAA0D,KAA1D,UAA0D,KAAA,mBAAA,IAAA,IAAA,CAAA,GAAA,IAAA,UAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GE5J5D,OAAA,UAAA,GAAA,mBAKE,QAAA;IAJA,OAAK,eAAA,CAAC,eAAa,MACL,QAAA,OAAK,CAAA;IAClB,OAAK,eAAA;KAAA,OAAW,QAAA,UAAK,WAAgB,QAAA,SAAS,QAAA;KAAK,QAAE,QAAA;IAAM,CAAA;IAC5D,eAAY"}
|
|
@@ -1,38 +1,8 @@
|
|
|
1
|
+
import { i as ensureSheetRoot, n as useBoundValue, t as inertOutside } from "./inert-BRQcZwpX.js";
|
|
1
2
|
import { t as _plugin_vue_export_helper_default } from "./_plugin-vue_export-helper-BOaGB7Aw.js";
|
|
2
|
-
import {
|
|
3
|
-
import { t as inertOutside } from "./inert-B441LoCD.js";
|
|
3
|
+
import { t as BaseButton_default } from "./BaseButton-RN2an975.js";
|
|
4
4
|
import { Teleport, Transition, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createStaticVNode, createTextVNode, createVNode, defineComponent, isRef, mergeModels, mergeProps, nextTick, normalizeClass, normalizeStyle, onMounted, onScopeDispose, onUnmounted, openBlock, readonly, ref, renderSlot, toDisplayString, unref, useId, useModel, vModelCheckbox, vModelDynamic, watch, withCtx, withDirectives } from "vue";
|
|
5
5
|
import { X } from "lucide-vue-next";
|
|
6
|
-
//#region src/utils/sheet-root.ts
|
|
7
|
-
/** The node every sheet teleports into. */
|
|
8
|
-
var SHEET_ROOT_ID = "sheet-root";
|
|
9
|
-
/**
|
|
10
|
-
* Returns the sheet root, creating it if the page has none.
|
|
11
|
-
*
|
|
12
|
-
* `BaseSheet` teleports out of the app's tree so the app behind it can be
|
|
13
|
-
* marked `inert` — a sheet inside the element it is disabling would disable
|
|
14
|
-
* itself. That means it needs a mount point outside `#app`.
|
|
15
|
-
*
|
|
16
|
-
* It used to need the consuming app to put `<div id="sheet-root">` in its
|
|
17
|
-
* `index.html`, and nothing said so. A page without it got a sheet that opened,
|
|
18
|
-
* blocked the page, and rendered nothing: Vue warns to the console about a
|
|
19
|
-
* missing teleport target and carries on, so the build is green, the types are
|
|
20
|
-
* fine, and the screen is wrong. The kit's own showcase had exactly that bug,
|
|
21
|
-
* which is how it was found.
|
|
22
|
-
*
|
|
23
|
-
* So the node is made on demand. An app that already declares one keeps it —
|
|
24
|
-
* this only fills a gap, it never replaces.
|
|
25
|
-
*/
|
|
26
|
-
function ensureSheetRoot() {
|
|
27
|
-
const selector = `#${SHEET_ROOT_ID}`;
|
|
28
|
-
if (typeof document === "undefined") return selector;
|
|
29
|
-
if (document.getElementById("sheet-root")) return selector;
|
|
30
|
-
const root = document.createElement("div");
|
|
31
|
-
root.id = SHEET_ROOT_ID;
|
|
32
|
-
document.body.appendChild(root);
|
|
33
|
-
return selector;
|
|
34
|
-
}
|
|
35
|
-
//#endregion
|
|
36
6
|
//#region src/utils/redirect.ts
|
|
37
7
|
/**
|
|
38
8
|
* Resolves a `?redirect=` query value into a safe in-app path.
|
|
@@ -498,39 +468,6 @@ var FormField_default = /* @__PURE__ */ defineComponent({
|
|
|
498
468
|
}
|
|
499
469
|
});
|
|
500
470
|
//#endregion
|
|
501
|
-
//#region src/composables/use-bound-value.ts
|
|
502
|
-
/**
|
|
503
|
-
* A `v-model` that accepts `undefined` and emits only what a field produces.
|
|
504
|
-
*
|
|
505
|
-
* `defineModel` could not be typed both ways. Without a default it declares
|
|
506
|
-
* that it may emit `undefined`, so under vue-tsc's strictTemplates a
|
|
507
|
-
* `ref('')` cannot be bound to it. With a default it drops `undefined` from
|
|
508
|
-
* what it accepts, so under `exactOptionalPropertyTypes` a form library's
|
|
509
|
-
* `string | undefined` cannot be. Hibi binds exactly that, and 2.4.0 as first
|
|
510
|
-
* written broke it — the consumer check caught it before the tag.
|
|
511
|
-
*
|
|
512
|
-
* So the prop and the event are declared by hand — the prop as
|
|
513
|
-
* `T | undefined`, the event as `T` — and this is the rest of what
|
|
514
|
-
* `defineModel` did: follow the prop, and keep a value of its own when
|
|
515
|
-
* nothing is bound. Vue applies `.trim` and `.number` to the event itself, so
|
|
516
|
-
* modifiers behave as they did.
|
|
517
|
-
*
|
|
518
|
-
* Internal: not exported from the package.
|
|
519
|
-
*/
|
|
520
|
-
function useBoundValue(read, write) {
|
|
521
|
-
const local = ref(read());
|
|
522
|
-
watch(read, (next) => {
|
|
523
|
-
local.value = next;
|
|
524
|
-
});
|
|
525
|
-
return computed({
|
|
526
|
-
get: () => local.value,
|
|
527
|
-
set: (next) => {
|
|
528
|
-
local.value = next;
|
|
529
|
-
write(next);
|
|
530
|
-
}
|
|
531
|
-
});
|
|
532
|
-
}
|
|
533
|
-
//#endregion
|
|
534
471
|
//#region src/components/BaseInput.vue?vue&type=script&setup=true&lang.ts
|
|
535
472
|
var _hoisted_1$3 = [
|
|
536
473
|
"id",
|
|
@@ -811,6 +748,6 @@ var GoogleButton_default = /* @__PURE__ */ defineComponent({
|
|
|
811
748
|
}
|
|
812
749
|
});
|
|
813
750
|
//#endregion
|
|
814
|
-
export {
|
|
751
|
+
export { toRedirectPath as _, FormField_default as a, useVisualViewport as c, isThemePreference as d, readStoredTheme as f, safeRedirect as g, tapFeedback as h, BaseInput_default as i, useOnline as l, useTheme as m, BaseCheckbox_default as n, BaseAlert_default as o, setThemeStorageKey as p, BaseSheet_default as r, useToast as s, GoogleButton_default as t, applyTheme as u };
|
|
815
752
|
|
|
816
|
-
//# sourceMappingURL=GoogleButton-
|
|
753
|
+
//# sourceMappingURL=GoogleButton-DS08ayQ-.js.map
|