@alikhalilll/ui 1.1.0 → 1.2.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 CHANGED
@@ -1,13 +1,29 @@
1
1
  # @alikhalilll/ui
2
2
 
3
- Headless, [shadcn-vue](https://www.shadcn-vue.com/) style component library for Vue 3 (and Nuxt). Built on [reka-ui](https://reka-ui.com) and [vaul-vue](https://github.com/unovue/vaul-vue), fully typed, scalable, with sensible defaults you can override on every level.
3
+ [![npm](https://img.shields.io/npm/v/@alikhalilll/ui.svg?color=444)](https://www.npmjs.com/package/@alikhalilll/ui)
4
+ [![downloads](https://img.shields.io/npm/dm/@alikhalilll/ui.svg?color=444)](https://www.npmjs.com/package/@alikhalilll/ui)
5
+ [![size](https://img.shields.io/bundlephobia/minzip/@alikhalilll/ui?label=minzip&color=444)](https://bundlephobia.com/package/@alikhalilll/ui)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-444.svg)](./LICENSE)
4
7
 
5
- The first component shipped is **`ATellInput`**a phone-number input that:
8
+ Headless, shadcn-vue style Vue 3 component library every component lives behind its own subpath import so consumers only ship the components they actually use. Built on [reka-ui](https://reka-ui.com) and [vaul-vue](https://github.com/unovue/vaul-vue), themed via prefixed CSS variables, fully typed.
6
9
 
7
- - Detects the user's country automatically (IP geolocation → timezone → `navigator.language` → fallback).
8
- - Validates and formats input via [`libphonenumber-js`](https://www.npmjs.com/package/libphonenumber-js).
9
- - Renders a **popover on desktop, vaul-vue drawer on mobile** for the country picker.
10
- - Exposes every sub-primitive so you can compose your own variant if the default doesn't fit.
10
+ 📚 **Live docs:** <https://alikhalilll.github.io/ali-nuxt-toolkit/ui>
11
+ 📦 **npm:** <https://www.npmjs.com/package/@alikhalilll/ui>
12
+ 🐙 **Source:** <https://github.com/alikhalilll/ali-nuxt-toolkit/tree/master/packages/ui>
13
+
14
+ ---
15
+
16
+ ## Components
17
+
18
+ | Component | Subpath | Per-component README | Live docs |
19
+ | -------------------- | ------------------------------------ | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
20
+ | `ATellInput` | `@alikhalilll/ui/tell-input` | [./entries/tell-input/README.md](./entries/tell-input/README.md) | [/ui/tell-input](https://alikhalilll.github.io/ali-nuxt-toolkit/ui/tell-input) |
21
+ | `AInput` | `@alikhalilll/ui/input` | [./entries/input/README.md](./entries/input/README.md) | [/ui/input](https://alikhalilll.github.io/ali-nuxt-toolkit/ui/input) |
22
+ | `APopover` | `@alikhalilll/ui/popover` | [./entries/popover/README.md](./entries/popover/README.md) | [/ui/popover](https://alikhalilll.github.io/ali-nuxt-toolkit/ui/popover) |
23
+ | `ADrawer` | `@alikhalilll/ui/drawer` | [./entries/drawer/README.md](./entries/drawer/README.md) | [/ui/drawer](https://alikhalilll.github.io/ali-nuxt-toolkit/ui/drawer) |
24
+ | `AResponsivePopover` | `@alikhalilll/ui/responsive-popover` | [./entries/responsive-popover/README.md](./entries/responsive-popover/README.md) | [/ui/responsive-popover](https://alikhalilll.github.io/ali-nuxt-toolkit/ui/responsive-popover) |
25
+
26
+ Each README ships inside the npm tarball at `node_modules/@alikhalilll/ui/entries/<name>/README.md` and is rendered on the file browser at <https://www.npmjs.com/package/@alikhalilll/ui?activeTab=code>.
11
27
 
12
28
  ## Install
13
29
 
@@ -15,129 +31,164 @@ The first component shipped is **`ATellInput`** — a phone-number input that:
15
31
  pnpm add @alikhalilll/ui
16
32
  ```
17
33
 
18
- Peer dependency: `vue ^3.5.0`. The library bundles `reka-ui`, `vaul-vue`, `libphonenumber-js`, `lucide-vue-next`, `@vueuse/core`, `class-variance-authority`, `clsx`, and `tailwind-merge`.
19
-
20
- ### Per-component subpath imports
34
+ Peer dependency: `vue ^3.5.0`. Bundled deps: `reka-ui`, `vaul-vue`, `libphonenumber-js`, `lucide-vue-next`, `@vueuse/core`, `class-variance-authority`, `clsx`, `tailwind-merge`.
21
35
 
22
- Each component lives behind its own subpath so consumers can pull a single component without dragging the rest of the library into their bundle:
36
+ ### Subpath imports (recommended)
23
37
 
24
38
  ```ts
25
- // Minimal — only the tell-input chunk gets included.
39
+ // Only the tell-input chunk ships into the bundle.
26
40
  import { ATellInput } from '@alikhalilll/ui/tell-input';
27
41
 
28
- // Or the main entry (modern bundlers tree-shake unused exports via `sideEffects`).
29
- import { ATellInput } from '@alikhalilll/ui';
42
+ // Main entry bundlers still tree-shake unused exports via `sideEffects`.
43
+ import { ATellInput, APopover } from '@alikhalilll/ui';
30
44
  ```
31
45
 
32
46
  Available subpaths: `/tell-input`, `/input`, `/popover`, `/drawer`, `/responsive-popover`, `/utils`.
33
47
 
34
48
  ## Setup
35
49
 
36
- The components are styled with Tailwind utility classes (`bg-popover`, `text-destructive`, etc.) that resolve to CSS variables shipped in `@alikhalilll/ui/styles.css`. You need to:
50
+ Components style themselves with Tailwind utility classes (`bg-popover`, `text-muted-foreground`, ) resolving to CSS variables shipped at `@alikhalilll/ui/styles.css`. Three steps:
37
51
 
38
- 1. **Import the variables** somewhere global:
52
+ ### 1. Import the tokens
39
53
 
40
- ```ts
41
- // In a Nuxt config or your main entry
42
- import '@alikhalilll/ui/styles.css';
43
- ```
54
+ **Nuxt 3 / 4:**
44
55
 
45
- 2. **Expose the variables to Tailwind**. For Tailwind v4 add an `@theme inline` block to your global stylesheet:
56
+ ```ts
57
+ // nuxt.config.ts
58
+ export default defineNuxtConfig({
59
+ css: ['@alikhalilll/ui/styles.css'],
60
+ });
61
+ ```
46
62
 
47
- ```css
48
- @import 'tailwindcss';
49
- @import '@alikhalilll/ui/styles.css';
63
+ **Vue + Vite (no Nuxt):**
50
64
 
51
- @theme inline {
52
- --color-background: hsl(var(--ak-ui-background));
53
- --color-foreground: hsl(var(--ak-ui-foreground));
54
- --color-popover: hsl(var(--ak-ui-popover));
55
- --color-popover-foreground: hsl(var(--ak-ui-popover-foreground));
56
- --color-primary: hsl(var(--ak-ui-primary));
57
- --color-primary-foreground: hsl(var(--ak-ui-primary-foreground));
58
- --color-muted: hsl(var(--ak-ui-muted));
59
- --color-muted-foreground: hsl(var(--ak-ui-muted-foreground));
60
- --color-accent: hsl(var(--ak-ui-accent));
61
- --color-accent-foreground: hsl(var(--ak-ui-accent-foreground));
62
- --color-destructive: hsl(var(--ak-ui-destructive));
63
- --color-destructive-foreground: hsl(var(--ak-ui-destructive-foreground));
64
- --color-border: hsl(var(--ak-ui-border));
65
- --color-input: hsl(var(--ak-ui-input));
66
- --color-ring: hsl(var(--ak-ui-ring));
67
- }
68
- ```
65
+ ```ts
66
+ // main.ts
67
+ import { createApp } from 'vue';
68
+ import App from './App.vue';
69
+ import '@alikhalilll/ui/styles.css';
69
70
 
70
- For Tailwind v3, add the same colors to `theme.extend.colors` in `tailwind.config.ts`.
71
+ createApp(App).mount('#app');
72
+ ```
71
73
 
72
- 3. **Toggle dark mode** by adding `.dark` to a parent element (typically `<html>`).
74
+ Every variable is prefixed `--ak-ui-` guaranteed not to collide with your own CSS.
75
+
76
+ ### 2. Map to Tailwind v4
77
+
78
+ ```css
79
+ @import 'tailwindcss';
80
+ @import '@alikhalilll/ui/styles.css';
81
+ @source '../node_modules/@alikhalilll/ui/dist/index.mjs';
82
+
83
+ @theme inline {
84
+ --color-background: hsl(var(--ak-ui-background));
85
+ --color-foreground: hsl(var(--ak-ui-foreground));
86
+ --color-popover: hsl(var(--ak-ui-popover));
87
+ --color-popover-foreground: hsl(var(--ak-ui-popover-foreground));
88
+ --color-muted: hsl(var(--ak-ui-muted));
89
+ --color-muted-foreground: hsl(var(--ak-ui-muted-foreground));
90
+ --color-accent: hsl(var(--ak-ui-accent));
91
+ --color-accent-foreground: hsl(var(--ak-ui-accent-foreground));
92
+ --color-destructive: hsl(var(--ak-ui-destructive));
93
+ --color-destructive-foreground: hsl(var(--ak-ui-destructive-foreground));
94
+ --color-border: hsl(var(--ak-ui-border));
95
+ --color-input: hsl(var(--ak-ui-input));
96
+ --color-ring: hsl(var(--ak-ui-ring));
97
+ }
98
+ ```
99
+
100
+ `@source` tells Tailwind v4 to scan the lib's compiled templates — it skips `node_modules` by default. Inside a pnpm workspace, point at the source so HMR works:
73
101
 
74
- The lib ships **both** `:root, .light { … }` and `.dark { … }` blocks. You can lock the theme:
102
+ ```css
103
+ @source '../../packages/ui/index.ts';
104
+ @source '../../packages/ui/entries/**/*.{vue,ts}';
105
+ @source '../../packages/ui/utils/**/*.ts';
106
+ ```
75
107
 
76
- ```ts
77
- // Locked dark
78
- export default defineNuxtConfig({
79
- app: { head: { htmlAttrs: { class: 'dark' } } },
80
- });
81
- ```
108
+ ### 3. Dark mode
82
109
 
83
- Or run a tri-state Light / Dark / System switcher that follows `prefers-color-scheme`. The docs site under [`apps/docs/composables/useColorMode.ts`](https://github.com/alikhalilll/ali-nuxt-toolkit/blob/master/apps/docs/composables/useColorMode.ts) ships a complete working pattern (persisted preference, OS-change listener, pre-paint inline script to avoid flash of wrong theme) that you can copy as-is.
110
+ The lib ships both `.light` and `.dark` blocks. Toggle the class on `<html>` portaled popovers and drawers inherit via the cascade.
84
111
 
85
- ## Usage
112
+ **Nuxt 3 / 4 — locked dark:**
113
+
114
+ ```ts
115
+ // nuxt.config.ts
116
+ export default defineNuxtConfig({
117
+ app: { head: { htmlAttrs: { class: 'dark' } } },
118
+ });
119
+ ```
120
+
121
+ **Vue + Vite — locked dark:**
122
+
123
+ ```html
124
+ <!-- index.html -->
125
+ <html class="dark">
126
+ ...
127
+ </html>
128
+ ```
129
+
130
+ For Light / Dark / System (persisted, OS-aware, no flash of wrong theme), see [`apps/docs/composables/useColorMode.ts`](https://github.com/alikhalilll/ali-nuxt-toolkit/blob/master/apps/docs/composables/useColorMode.ts) — the pattern is framework-agnostic.
131
+
132
+ ## Quick start
86
133
 
87
134
  ```vue
88
135
  <script setup lang="ts">
89
136
  import { ref } from 'vue';
90
- import { ATellInput } from '@alikhalilll/ui';
137
+ import { ATellInput } from '@alikhalilll/ui/tell-input';
91
138
 
92
139
  const phone = ref('');
93
- const country = ref<number | null>(null);
140
+ const country = ref<number | null>(null); // dial number, e.g. 20 for Egypt
94
141
  </script>
95
142
 
96
143
  <template>
97
- <ATellInput
98
- v-model:phone="phone"
99
- v-model:country="country"
100
- default-country="20"
101
- show-validation
102
- />
144
+ <ATellInput v-model:phone="phone" v-model:country="country" show-validation />
103
145
  </template>
104
146
  ```
105
147
 
106
- ### Props
107
-
108
- | Prop | Type | Default | Description |
109
- | ------------------- | ------------------------------------------------ | ------------------------------ | ------------------------------------------------------------------------------------------ |
110
- | `v-model:phone` | `string` | `''` | Digits-only national number (no leading `+` or dial code). |
111
- | `v-model:country` | `string` | `''` | ISO 3166-1 alpha-2 code, e.g. `"EG"`. |
112
- | `placeholder` | `string` | `'Phone number'` | Falls back to the country's example number when empty. |
113
- | `disabled` | `boolean` | `false` | |
114
- | `loading` | `boolean` | `false` | Disables interaction. |
115
- | `size` | `'sm' \| 'default' \| 'lg'` | `'default'` | Controls input height (32 / 36 / 40 px). |
116
- | `allowedDialCodes` | `string[]` | _all_ | Whitelist of dial-digit codes (no `+`). Countries outside the list are shown but disabled. |
117
- | `showValidation` | `boolean` | `false` | Renders an error message below the input when invalid. |
118
- | `detectCountry` | `'auto' \| 'locale' \| 'none'` | `'auto'` | Strategy for the silent environment lookup (hint source for input detection). |
119
- | `defaultCountry` | `string` | `''` | Explicit initial country. When set, the picker is visible at mount. |
120
- | `detectFromInput` | `boolean` | `true` | Default mode: picker hidden until input matches a dial code. Set `false` for legacy. |
121
- | `detectDebounceMs` | `number` | `150` | Debounce window (ms) for input-driven detection. |
122
- | `ipEndpoint` | `string` | `'https://ipapi.co/json/'` | Override the geolocation endpoint. Must return JSON with `country_code` or `country`. |
123
- | `searchPlaceholder` | `string` | `'Search by country or code…'` | Country picker search input placeholder. |
124
- | `emptyText` | `string` | `'No countries found.'` | Shown when the search yields no results. |
125
- | `loadingText` | `string` | `'Loading…'` | Shown while the country list is loading. |
126
- | `errorMessages` | `Partial<Record<PhoneValidationReason, string>>` | English defaults | Override the validation error labels. |
127
- | `class` | `string \| any[] \| Record<string, boolean>` | — | Merged into the outer wrapper via `tailwind-merge`. |
128
-
129
- ### Exposed (via template ref)
148
+ The picker stays hidden until the user types or pastes something that matches a known dial code, then a flag-only trigger reveals at the end of the field with the dial code as an inline prefix, and `phone` is normalised to the national significant number (`01066105963` → `1066105963`, `+447911123456` → `7911123456`). RTL-aware, localisable via `locale` / `messages`, and accepts alternative numerals.
149
+
150
+ Full props, slots, theming recipes, and live demos → [tell-input docs](https://alikhalilll.github.io/ali-nuxt-toolkit/ui/tell-input) or [./entries/tell-input/README.md](./entries/tell-input/README.md).
151
+
152
+ ## Nuxt integration
153
+
154
+ `@alikhalilll/ui` is a plain Vue 3 library works in Nuxt 3 / 4 without a module wrapper. Full guide (auto-imports, SSR behaviour, source globs) on the [docs site](https://alikhalilll.github.io/ali-nuxt-toolkit/ui#nuxt-integration).
130
155
 
131
156
  ```ts
132
- const ref = ref<InstanceType<typeof ATellInput>>();
133
- ref.value.validation; // PhoneValidationResult — computed, reactive
134
- ref.value.required; // PhoneRequiredInfo | null — example, length range, format hint
135
- ref.value.selectedDialCode; // '+20' | null
157
+ // nuxt.config.ts optional auto-import
158
+ export default defineNuxtConfig({
159
+ components: [{ path: '@alikhalilll/ui', pathPrefix: false, global: true }],
160
+ });
161
+ ```
162
+
163
+ ## Theming
164
+
165
+ Override any `--ak-ui-*` variable globally, per wrapper, or inline:
166
+
167
+ ```css
168
+ .tenant-acme {
169
+ --ak-ui-popover: 220 70% 8%;
170
+ --ak-ui-accent: 220 50% 30%;
171
+ --ak-ui-ring: 220 100% 65%;
172
+ }
136
173
  ```
137
174
 
138
- ## Compose your own component
175
+ Values are HSL **triplets** — no `hsl(…)` wrapper. Full token list + recipes (brand-only, day/night, multi-tenant, server-driven, state-specific) → [theming guide](https://alikhalilll.github.io/ali-nuxt-toolkit/ui#theming).
176
+
177
+ ## Size scale
178
+
179
+ | Token | Height | Tailwind |
180
+ | ----- | ------------------- | ---------- |
181
+ | `xs` | 28 px | `h-7` |
182
+ | `sm` | 36 px | `h-9` |
183
+ | `md` | **43 px (default)** | `h-[43px]` |
184
+ | `lg` | 52 px | `h-[52px]` |
185
+ | `xl` | 60 px | `h-[60px]` |
186
+
187
+ `controlHeight`, `controlPaddingX`, `controlTextSize`, `controlHeightPx`, `SIZES`, `DEFAULT_SIZE`, and the `Size` type are exported so you can build size-aware components in lockstep with the library.
139
188
 
140
- Every primitive, composable, and CVA helper is re-exported from the package root, so you can build a custom variant without forking the library.
189
+ ## Compose your own variant
190
+
191
+ Every primitive, composable, and helper is re-exported — fork-free composition:
141
192
 
142
193
  ```ts
143
194
  import {
@@ -165,102 +216,13 @@ import {
165
216
  ACountryFlag,
166
217
  aTellInputVariants,
167
218
  DEFAULT_ERROR_MESSAGES,
219
+ defaultFlagUrl,
168
220
 
169
- // Utilities
221
+ // Helpers
170
222
  cn,
171
223
  } from '@alikhalilll/ui';
172
-
173
- // Build your own tel input from the same composables
174
- const { validate, getCountries, searchCountries } = usePhoneValidation();
175
- const detected = await detectCountry({ strategy: 'auto' });
176
224
  ```
177
225
 
178
- Or pull the country select on its own (any list-of-countries UI):
179
-
180
- ```vue
181
- <ACountrySelect v-model:selected="iso2" />
182
- ```
183
-
184
- Or build any responsive popover (popover-on-desktop, drawer-on-mobile):
185
-
186
- ```vue
187
- <AResponsivePopover v-model:open="open">
188
- <AResponsivePopoverTrigger as-child>
189
- <button>Open</button>
190
- </AResponsivePopoverTrigger>
191
- <AResponsivePopoverContent>
192
- <p>Content</p>
193
- </AResponsivePopoverContent>
194
- </AResponsivePopover>
195
- ```
196
-
197
- ## Full customization
198
-
199
- `ATellInput` exposes a deep customization surface across three vectors. Every override is opt-in — defaults are sensible, so a vanilla `<ATellInput />` works out of the box.
200
-
201
- **Slots (replace any rendered region):**
202
-
203
- ```
204
- prefix · suffix · trigger · chevron · flag · search · search-icon ·
205
- loading · empty · group-header · item · item-check · valid-icon ·
206
- error-icon · hint · error
207
- ```
208
-
209
- **Data-override props:**
210
-
211
- ```ts
212
- flagUrl?: (iso2, width) => string // swap flagcdn.com for any source
213
- countries?: CountryOption[] // bypass REST Countries; ship your own list
214
- searcher?: (query, country) => boolean // custom search (fuzzy/starts-with/locale-aware)
215
- detector?: (opts) => Promise<string | null> // custom country detection (e.g. server-driven)
216
- errorMessages?: Partial<Record<PhoneValidationReason, string>> // i18n
217
- kbdOpen?: string | null // override the '⌘K' hint
218
- kbdClose?: string | null // override the 'Esc' hint
219
- ```
220
-
221
- **Class-override props (every region):** `class`, `fieldClass`, `inputClass`, `contentClass`, `popoverClass`, `drawerClass`, `hintClass`, `errorClass`. All merged via `tailwind-merge`, so you only specify the bits you want to change.
222
-
223
- **Example — fully bespoke trigger, custom country list, custom searcher, custom detector:**
224
-
225
- ```vue
226
- <script setup lang="ts">
227
- import { ATellInput, defaultFlagUrl, type CountryOption } from '@alikhalilll/ui';
228
-
229
- const countries: CountryOption[] = [
230
- /* … your curated list … */
231
- ];
232
-
233
- const flagUrl = (iso: string) => `/flags/${iso.toLowerCase()}.svg`;
234
- const searcher = (q: string, c: CountryOption) =>
235
- c.raw_data.name.toLowerCase().startsWith(q.toLowerCase());
236
-
237
- async function detector() {
238
- // Pretend your backend tells you the user's country from the request
239
- const { country } = await $fetch('/api/locale');
240
- return country;
241
- }
242
- </script>
243
-
244
- <template>
245
- <ATellInput
246
- v-model:phone="phone"
247
- v-model:country="country"
248
- :countries="countries"
249
- :flag-url="flagUrl"
250
- :searcher="searcher"
251
- :detector="detector"
252
- >
253
- <template #trigger="{ selectedCountry, open }">
254
- <button :data-open="open">
255
- {{ selectedCountry?.raw_data.iso2 ?? '??' }}
256
- </button>
257
- </template>
258
- </ATellInput>
259
- </template>
260
- ```
261
-
262
- `AInput` also exposes `#prefix` and `#suffix` slots — when either is filled the component switches to a wrapped layout so the bordered field carries the focus ring while the slot content sits inside the border. See the [docs site](https://alikhalilll.github.io/ali-nuxt-toolkit/ui/input) for the full prop tables and a live demo gallery.
263
-
264
226
  ## Exported types
265
227
 
266
228
  ```ts
@@ -281,21 +243,11 @@ import type {
281
243
  } from '@alikhalilll/ui';
282
244
  ```
283
245
 
284
- The `defaultFlagUrl(iso2, width)` builder is also exported — handy for composing a custom builder on top of the default:
285
-
286
- ```ts
287
- import { defaultFlagUrl } from '@alikhalilll/ui';
288
- const hiRes = (iso: string) => defaultFlagUrl(iso, 80);
289
- ```
290
-
291
- ## Country detection chain
292
-
293
- 1. **IP geolocation** — fetch `ipEndpoint` (default `https://ipapi.co/json/`), aborted after `timeoutMs` (default 2 s). Result cached in `sessionStorage` so re-mounts skip the network call.
294
- 2. **Timezone** — `Intl.DateTimeFormat().resolvedOptions().timeZone` against a built-in IANA-zone-to-ISO2 map (~70 zones, covers most populated cities).
295
- 3. **`navigator.language`** — extracts the region from tags like `en-US`, `ar-EG`, `pt-BR`.
296
- 4. **`defaultCountry`** — final fallback (`'US'` if you don't set one).
246
+ ## Notes
297
247
 
298
- Pass `detect-country="locale"` to skip the IP step (no network), or `detect-country="none"` to use `defaultCountry` immediately.
248
+ - **Country detection** runs only in `onMounted` the input renders immediately with `defaultCountry` (or empty); the detected ISO2 patches in on hydration.
249
+ - The default tel-input behaviour is **picker hidden until detected** (`detect-from-input`). Pass `:detect-from-input="false"` to opt out, or `default-country="20"` / `default-country="EG"` to pre-fill the picker at mount.
250
+ - Import `@alikhalilll/ui/styles.css` **before** your overrides so your overrides win the cascade.
299
251
 
300
252
  ## License
301
253
 
@@ -0,0 +1,132 @@
1
+ import { defineComponent as u, openBlock as i, createBlock as p, unref as o, mergeProps as f, withCtx as c, renderSlot as m, normalizeProps as h, guardReactiveProps as B, onMounted as _, onBeforeUnmount as w, createElementBlock as b, normalizeClass as y, createCommentVNode as P, createVNode as A } from "vue";
2
+ import { useForwardPropsEmits as v, PopoverRoot as O, useForwardProps as C, PopoverTrigger as S, PopoverPortal as $, PopoverContent as k } from "reka-ui";
3
+ import { reactiveOmit as z } from "@vueuse/core";
4
+ import { c as g } from "./cn-B6yFEsav.mjs";
5
+ const j = /* @__PURE__ */ u({
6
+ __name: "APopover",
7
+ props: {
8
+ defaultOpen: { type: Boolean },
9
+ open: { type: Boolean },
10
+ modal: { type: Boolean, default: !0 }
11
+ },
12
+ emits: ["update:open"],
13
+ setup(a, { emit: r }) {
14
+ const s = v(a, r);
15
+ return (d, e) => (i(), p(o(O), f({ "data-slot": "popover" }, o(s)), {
16
+ default: c((l) => [
17
+ m(d.$slots, "default", h(B(l)))
18
+ ]),
19
+ _: 3
20
+ }, 16));
21
+ }
22
+ }), D = /* @__PURE__ */ u({
23
+ __name: "APopoverTrigger",
24
+ props: {
25
+ asChild: { type: Boolean },
26
+ as: {}
27
+ },
28
+ setup(a) {
29
+ const t = C(a);
30
+ return (n, s) => (i(), p(o(S), f({ "data-slot": "popover-trigger" }, o(t)), {
31
+ default: c(() => [
32
+ m(n.$slots, "default")
33
+ ]),
34
+ _: 3
35
+ }, 16));
36
+ }
37
+ }), F = /* @__PURE__ */ u({
38
+ inheritAttrs: !1,
39
+ __name: "APopoverOverlay",
40
+ props: {
41
+ class: { type: [Boolean, null, String, Object, Array] }
42
+ },
43
+ setup(a) {
44
+ const r = a;
45
+ let t = "", n = "", s = "";
46
+ function d() {
47
+ return typeof window > "u" ? 0 : window.innerWidth - document.documentElement.clientWidth;
48
+ }
49
+ return _(() => {
50
+ if (typeof document > "u") return;
51
+ const e = document.body, l = d();
52
+ t = e.style.overflow, n = e.style.touchAction, s = e.style.paddingRight, e.style.overflow = "hidden", e.style.touchAction = "none", l > 0 && (e.style.paddingRight = `${l}px`);
53
+ }), w(() => {
54
+ if (typeof document > "u") return;
55
+ const e = document.body;
56
+ e.style.overflow = t, e.style.touchAction = n, e.style.paddingRight = s;
57
+ }), (e, l) => (i(), b("div", {
58
+ "data-slot": "popover-overlay",
59
+ "aria-hidden": "true",
60
+ class: y(
61
+ o(g)(
62
+ // `fixed inset-0` covers the entire viewport; `pointer-events-auto` captures every
63
+ // click so it can never reach the page underneath. `z-50` keeps us above any normal
64
+ // page chrome; the popover content sits at `z-[60]`.
65
+ "fixed inset-0 z-50 bg-black/60 backdrop-blur-sm pointer-events-auto data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
66
+ r.class
67
+ )
68
+ )
69
+ }, null, 2));
70
+ }
71
+ }), T = /* @__PURE__ */ u({
72
+ inheritAttrs: !1,
73
+ __name: "APopoverContent",
74
+ props: {
75
+ forceMount: { type: Boolean },
76
+ side: {},
77
+ sideOffset: { default: 4 },
78
+ sideFlip: { type: Boolean },
79
+ align: { default: "center" },
80
+ alignOffset: {},
81
+ alignFlip: { type: Boolean },
82
+ avoidCollisions: { type: Boolean },
83
+ collisionBoundary: {},
84
+ collisionPadding: {},
85
+ arrowPadding: {},
86
+ hideShiftedArrow: { type: Boolean },
87
+ sticky: {},
88
+ hideWhenDetached: { type: Boolean },
89
+ positionStrategy: {},
90
+ updatePositionStrategy: {},
91
+ disableUpdateOnLayoutShift: { type: Boolean },
92
+ prioritizePosition: { type: Boolean },
93
+ reference: {},
94
+ asChild: { type: Boolean },
95
+ as: {},
96
+ disableOutsidePointerEvents: { type: Boolean },
97
+ class: { type: [Boolean, null, String, Object, Array] },
98
+ overlay: { type: Boolean, default: !1 },
99
+ overlayClass: { type: [Boolean, null, String, Object, Array] }
100
+ },
101
+ emits: ["escapeKeyDown", "pointerDownOutside", "focusOutside", "interactOutside", "openAutoFocus", "closeAutoFocus"],
102
+ setup(a, { emit: r }) {
103
+ const t = a, n = r, s = z(t, "class", "overlay", "overlayClass"), d = v(s, n);
104
+ return (e, l) => (i(), p(o($), null, {
105
+ default: c(() => [
106
+ t.overlay ? (i(), p(F, {
107
+ key: 0,
108
+ class: y(t.overlayClass)
109
+ }, null, 8, ["class"])) : P("", !0),
110
+ A(o(k), f({ "data-slot": "popover-content" }, { ...e.$attrs, ...o(d) }, {
111
+ class: o(g)(
112
+ "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-[60] w-72 rounded-md border p-4 shadow-md outline-none",
113
+ t.class
114
+ )
115
+ }), {
116
+ default: c(() => [
117
+ m(e.$slots, "default")
118
+ ]),
119
+ _: 3
120
+ }, 16, ["class"])
121
+ ]),
122
+ _: 3
123
+ }));
124
+ }
125
+ });
126
+ export {
127
+ j as _,
128
+ T as a,
129
+ F as b,
130
+ D as c
131
+ };
132
+ //# sourceMappingURL=APopoverContent.vue_vue_type_script_setup_true_lang-o1XhV9DM.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"APopoverContent.vue_vue_type_script_setup_true_lang-o1XhV9DM.mjs","sources":["../../entries/popover/components/APopover.vue","../../entries/popover/components/APopoverTrigger.vue","../../entries/popover/components/APopoverOverlay.vue","../../entries/popover/components/APopoverContent.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport type { PopoverRootEmits, PopoverRootProps } from 'reka-ui';\nimport { PopoverRoot, useForwardPropsEmits } from 'reka-ui';\n\n/**\n * Defaults `modal` to `true` so the popover locks page scroll, traps focus, and an overlay\n * (rendered by APopoverContent when `overlay` is set) actually dims the page.\n * Pass `:modal=\"false\"` for tooltip-style non-modal popovers.\n */\nconst props = withDefaults(defineProps<PopoverRootProps>(), { modal: true });\nconst emits = defineEmits<PopoverRootEmits>();\nconst forwarded = useForwardPropsEmits(props, emits);\n</script>\n\n<template>\n <PopoverRoot v-slot=\"slotProps\" data-slot=\"popover\" v-bind=\"forwarded\">\n <slot v-bind=\"slotProps\" />\n </PopoverRoot>\n</template>\n","<script setup lang=\"ts\">\nimport type { PopoverTriggerProps } from 'reka-ui';\nimport { PopoverTrigger, useForwardProps } from 'reka-ui';\n\nconst props = defineProps<PopoverTriggerProps>();\nconst forwarded = useForwardProps(props);\n</script>\n\n<template>\n <PopoverTrigger data-slot=\"popover-trigger\" v-bind=\"forwarded\">\n <slot />\n </PopoverTrigger>\n</template>\n","<script setup lang=\"ts\">\nimport type { HTMLAttributes } from 'vue';\nimport { onBeforeUnmount, onMounted } from 'vue';\nimport { cn } from '@/utils';\n\ndefineOptions({ inheritAttrs: false });\n\nconst props = defineProps<{ class?: HTMLAttributes['class'] }>();\n\n// Body-scroll-lock for the lifetime of the overlay. Mounted = lock; unmounted = restore.\n// Because this component is only rendered when `<APopoverContent :overlay=\"true\">` is\n// inside the open popover portal, mount/unmount tracks visibility 1:1.\nlet prevBodyOverflow = '';\nlet prevBodyTouchAction = '';\nlet prevPaddingRight = '';\n\nfunction getScrollbarWidth() {\n if (typeof window === 'undefined') return 0;\n return window.innerWidth - document.documentElement.clientWidth;\n}\n\nonMounted(() => {\n if (typeof document === 'undefined') return;\n const body = document.body;\n const sbw = getScrollbarWidth();\n prevBodyOverflow = body.style.overflow;\n prevBodyTouchAction = body.style.touchAction;\n prevPaddingRight = body.style.paddingRight;\n body.style.overflow = 'hidden';\n body.style.touchAction = 'none';\n // Compensate for the missing scrollbar so the layout doesn't jump.\n if (sbw > 0) body.style.paddingRight = `${sbw}px`;\n});\n\nonBeforeUnmount(() => {\n if (typeof document === 'undefined') return;\n const body = document.body;\n body.style.overflow = prevBodyOverflow;\n body.style.touchAction = prevBodyTouchAction;\n body.style.paddingRight = prevPaddingRight;\n});\n</script>\n\n<template>\n <div\n data-slot=\"popover-overlay\"\n aria-hidden=\"true\"\n :class=\"\n cn(\n // `fixed inset-0` covers the entire viewport; `pointer-events-auto` captures every\n // click so it can never reach the page underneath. `z-50` keeps us above any normal\n // page chrome; the popover content sits at `z-[60]`.\n 'fixed inset-0 z-50 bg-black/60 backdrop-blur-sm pointer-events-auto data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',\n props.class\n )\n \"\n />\n</template>\n","<script setup lang=\"ts\">\nimport type { HTMLAttributes } from 'vue';\nimport { reactiveOmit } from '@vueuse/core';\nimport {\n PopoverContent,\n type PopoverContentEmits,\n type PopoverContentProps,\n PopoverPortal,\n useForwardPropsEmits,\n} from 'reka-ui';\nimport { cn } from '@/utils';\nimport APopoverOverlay from './APopoverOverlay.vue';\n\ndefineOptions({ inheritAttrs: false });\n\nconst props = withDefaults(\n defineProps<\n PopoverContentProps & {\n class?: HTMLAttributes['class'];\n /** Dim the entire viewport behind the popover and block all interaction with the\n * page (clicks are captured by the overlay; body scroll is locked while open).\n * Pair with `<APopover :modal=\"true\">` (the default) for the full modal behaviour. */\n overlay?: boolean;\n overlayClass?: HTMLAttributes['class'];\n }\n >(),\n { align: 'center', sideOffset: 4, overlay: false }\n);\nconst emits = defineEmits<PopoverContentEmits>();\nconst delegated = reactiveOmit(props, 'class', 'overlay', 'overlayClass');\nconst forwarded = useForwardPropsEmits(delegated, emits);\n</script>\n\n<template>\n <PopoverPortal>\n <!--\n Overlay is a sibling of PopoverContent inside the same portal. Reka-ui's\n DismissableLayer treats any pointer-down outside the content as a dismiss,\n so clicking the overlay closes the popover for free. The overlay component\n locks body scroll on mount and restores it on unmount.\n -->\n <APopoverOverlay v-if=\"props.overlay\" :class=\"props.overlayClass\" />\n <PopoverContent\n data-slot=\"popover-content\"\n v-bind=\"{ ...$attrs, ...forwarded }\"\n :class=\"\n cn(\n 'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-[60] w-72 rounded-md border p-4 shadow-md outline-none',\n props.class\n )\n \"\n >\n <slot />\n </PopoverContent>\n </PopoverPortal>\n</template>\n"],"names":["forwarded","useForwardPropsEmits","__props","__emit","_openBlock","_createBlock","_unref","_mergeProps","_withCtx","slotProps","_renderSlot","_ctx","useForwardProps","props","prevBodyOverflow","prevBodyTouchAction","prevPaddingRight","getScrollbarWidth","onMounted","body","sbw","onBeforeUnmount","_createElementBlock","_normalizeClass","cn","emits","delegated","reactiveOmit","PopoverPortal","APopoverOverlay","_createVNode","PopoverContent","$attrs"],"mappings":";;;;;;;;;;;;;AAWA,UAAMA,IAAYC,EAFJC,GACAC,CACqC;sBAIjDC,EAAA,GAAAC,EAEcC,MAFdC,EAEc,EAFkB,aAAU,UAAA,GAAkBD,EAAAN,CAAA,CAAS,GAAA;AAAA,MACnE,SAAAQ,EAAA,CADmBC,MAAS;AAAA,QAC5BC,EAA2BC,yBAAbF,CAAS,CAAA,CAAA;AAAA,MAAA;;;;;;;;;;;ACX3B,UAAMT,IAAYY,EADJV,CACyB;sBAIrCE,EAAA,GAAAC,EAEiBC,MAFjBC,EAEiB,EAFD,aAAU,kBAAA,GAA0BD,EAAAN,CAAA,CAAS,GAAA;AAAA,iBAC3D,MAAQ;AAAA,QAARU,EAAQC,EAAA,QAAA,SAAA;AAAA,MAAA;;;;;;;;;;;ACHZ,UAAME,IAAQX;AAKd,QAAIY,IAAmB,IACnBC,IAAsB,IACtBC,IAAmB;AAEvB,aAASC,IAAoB;AAC3B,aAAI,OAAO,SAAW,MAAoB,IACnC,OAAO,aAAa,SAAS,gBAAgB;AAAA,IACtD;AAEA,WAAAC,EAAU,MAAM;AACd,UAAI,OAAO,WAAa,IAAa;AACrC,YAAMC,IAAO,SAAS,MAChBC,IAAMH,EAAA;AACZ,MAAAH,IAAmBK,EAAK,MAAM,UAC9BJ,IAAsBI,EAAK,MAAM,aACjCH,IAAmBG,EAAK,MAAM,cAC9BA,EAAK,MAAM,WAAW,UACtBA,EAAK,MAAM,cAAc,QAErBC,IAAM,MAAGD,EAAK,MAAM,eAAe,GAAGC,CAAG;AAAA,IAC/C,CAAC,GAEDC,EAAgB,MAAM;AACpB,UAAI,OAAO,WAAa,IAAa;AACrC,YAAMF,IAAO,SAAS;AACtB,MAAAA,EAAK,MAAM,WAAWL,GACtBK,EAAK,MAAM,cAAcJ,GACzBI,EAAK,MAAM,eAAeH;AAAA,IAC5B,CAAC,mBAICM,EAYE,OAAA;AAAA,MAXA,aAAU;AAAA,MACV,eAAY;AAAA,MACX,OAAKC;AAAAA,QAASjB,EAAAkB,CAAA;AAAA;AAAA;AAAA;AAAA;UAA0cX,EAAM;AAAA,QAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AChCne,UAAMA,IAAQX,GAaRuB,IAAQtB,GACRuB,IAAYC,EAAad,GAAO,SAAS,WAAW,cAAc,GAClEb,IAAYC,EAAqByB,GAAWD,CAAK;2BAIrDpB,EAoBgBC,EAAAsB,CAAA,GAAA,MAAA;AAAA,iBAbd,MAAoE;AAAA,QAA7Cf,EAAM,gBAA7BR,EAAoEwB,GAAA;AAAA;UAA7B,OAAKN,EAAEV,EAAM,YAAY;AAAA,QAAA;QAChEiB,EAWiBxB,EAAAyB,CAAA,GAXjBxB,EAWiB,EAVf,aAAU,kBAAA,GAAiB,EAAA,GACdyB,EAAAA,QAAM,GAAK1B,EAAAN,CAAA,KAAS;AAAA,UAChC,OAAgBM,EAAAkB,CAAA;AAAA;YAAwcX,EAAM;AAAA,UAAA;AAAA;qBAO/d,MAAQ;AAAA,YAARH,EAAQC,EAAA,QAAA,SAAA;AAAA,UAAA;;;;;;;;"}
@@ -1,8 +1,8 @@
1
- import { defineComponent as u, useModel as g, computed as c, openBlock as l, createBlock as r, resolveDynamicComponent as _, withCtx as p, renderSlot as i, unref as n, mergeModels as k, normalizeClass as v } from "vue";
1
+ import { defineComponent as c, useModel as g, computed as u, openBlock as l, createBlock as r, resolveDynamicComponent as _, withCtx as p, renderSlot as i, unref as n, mergeModels as k, normalizeClass as v } from "vue";
2
2
  import { useMediaQuery as f } from "@vueuse/core";
3
- import { _ as C, b as $, a as b } from "./APopoverContent.vue_vue_type_script_setup_true_lang-BRqULKIg.mjs";
3
+ import { _ as C, c as $, a as b } from "./APopoverContent.vue_vue_type_script_setup_true_lang-o1XhV9DM.mjs";
4
4
  import { _ as h, c as B, a as w } from "./ADrawerContent.vue_vue_type_script_setup_true_lang-BivIZvV1.mjs";
5
- const M = /* @__PURE__ */ u({
5
+ const M = /* @__PURE__ */ c({
6
6
  __name: "AResponsivePopover",
7
7
  props: /* @__PURE__ */ k({
8
8
  breakpoint: { default: "(min-width: 768px)" },
@@ -13,7 +13,7 @@ const M = /* @__PURE__ */ u({
13
13
  }),
14
14
  emits: ["update:open"],
15
15
  setup(a) {
16
- const e = a, o = g(a, "open"), s = f(() => e.breakpoint), t = c(() => s.value ? C : h);
16
+ const e = a, o = g(a, "open"), s = f(() => e.breakpoint), t = u(() => s.value ? C : h);
17
17
  return (d, m) => (l(), r(_(t.value), {
18
18
  open: o.value,
19
19
  "onUpdate:open": m[0] || (m[0] = (y) => o.value = y),
@@ -26,14 +26,14 @@ const M = /* @__PURE__ */ u({
26
26
  _: 3
27
27
  }, 40, ["open", "modal"]));
28
28
  }
29
- }), R = /* @__PURE__ */ u({
29
+ }), R = /* @__PURE__ */ c({
30
30
  __name: "AResponsivePopoverTrigger",
31
31
  props: {
32
32
  breakpoint: { default: "(min-width: 768px)" },
33
33
  asChild: { type: Boolean }
34
34
  },
35
35
  setup(a) {
36
- const e = a, o = f(() => e.breakpoint), s = c(() => o.value ? $ : B);
36
+ const e = a, o = f(() => e.breakpoint), s = u(() => o.value ? $ : B);
37
37
  return (t, d) => (l(), r(_(s.value), {
38
38
  "as-child": e.asChild,
39
39
  "data-slot": "responsive-popover-trigger"
@@ -44,7 +44,7 @@ const M = /* @__PURE__ */ u({
44
44
  _: 3
45
45
  }, 8, ["as-child"]));
46
46
  }
47
- }), S = /* @__PURE__ */ u({
47
+ }), S = /* @__PURE__ */ c({
48
48
  __name: "AResponsivePopoverContent",
49
49
  props: {
50
50
  breakpoint: { default: "(min-width: 768px)" },
@@ -56,7 +56,7 @@ const M = /* @__PURE__ */ u({
56
56
  sideOffset: { default: 4 }
57
57
  },
58
58
  setup(a) {
59
- const e = a, o = f(() => e.breakpoint), s = c(() => [
59
+ const e = a, o = f(() => e.breakpoint), s = u(() => [
60
60
  e.class,
61
61
  o.value ? e.popoverClass : e.drawerClass
62
62
  ]);
@@ -89,4 +89,4 @@ export {
89
89
  S as a,
90
90
  R as b
91
91
  };
92
- //# sourceMappingURL=AResponsivePopoverContent.vue_vue_type_script_setup_true_lang-CCGyy9Lp.mjs.map
92
+ //# sourceMappingURL=AResponsivePopoverContent.vue_vue_type_script_setup_true_lang-BYEb5UBL.mjs.map