sb-floating-panel-vue 1.0.0 โ†’ 3.0.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
@@ -3,14 +3,17 @@
3
3
  [![npm](https://img.shields.io/npm/v/sb-floating-panel-vue.svg)](https://www.npmjs.com/package/sb-floating-panel-vue)
4
4
  [![license](https://img.shields.io/npm/l/sb-floating-panel-vue.svg)](https://github.com/stefanBid/sb-floating-panel-vue/blob/main/LICENSE)
5
5
  [![Vue 3](https://img.shields.io/badge/vue-3.x-brightgreen.svg)](https://vuejs.org)
6
- [![build](https://img.shields.io/badge/build-passing-brightgreen.svg)](#)
6
+ [![CI](https://github.com/stefanBid/sb-floating-panel-vue/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/stefanBid/sb-floating-panel-vue/actions/workflows/ci.yml)
7
+ [![coverage](https://raw.githubusercontent.com/stefanBid/sb-floating-panel-vue/badges/coverage.svg)](https://github.com/stefanBid/sb-floating-panel-vue/actions/workflows/ci.yml)
8
+ [![branches](https://raw.githubusercontent.com/stefanBid/sb-floating-panel-vue/badges/coverage-branches.svg)](https://github.com/stefanBid/sb-floating-panel-vue/actions/workflows/ci.yml)
7
9
 
8
10
  ## ๐Ÿ”ฅ Features
9
11
 
10
- - ๐Ÿงฉ Floating panel positioning with arrow and smart shifting
11
- - โšก Built on top of [`@floating-ui/vue`](https://floating-ui.com/)
12
- - ๐ŸŽฏ Support for resize syncing and animated transitions
13
- - ๐Ÿงช Dual usage: composable or plug-and-play components
12
+ - ๐Ÿงฉ Three components (`SbContainer`, `SbReference`, `SbFloating`) for dropdowns, popovers, menus and tooltips
13
+ - โšก Built on top of [`@floating-ui/vue`](https://floating-ui.com/): `offset`, `flip`, `shift`, `size` and `hide` already configured
14
+ - ๐ŸŽจ Zero CSS shipped: the library adapts to your design system (plain CSS, Tailwind, CSS modules)
15
+ - โ™ฟ Accessible by default: ARIA attributes, keyboard support, Escape, click outside, focus management
16
+ - ๐Ÿ” Fully reactive props, `v-model:open`, nested panels
14
17
 
15
18
  ---
16
19
 
@@ -22,7 +25,7 @@ Created with โค๏ธ by [Stefano Biddau](https://www.stefano-biddau.com/)
22
25
 
23
26
  ## โ“ Why This Library?
24
27
 
25
- `sb-floating-panel-vue` was designed to make it extremely easy to implement dropdowns, tooltips, and floating panels in Vue 3, without worrying about placement logic, arrow positioning, or responsive syncing. It wraps `@floating-ui/vue` into a more ergonomic composable or ready-to-use components.
28
+ `sb-floating-panel-vue` makes it easy to build floating panels in Vue 3 without touching positioning logic. You write three components; the library handles placement, flipping, viewport collisions, open state, accessibility and teleporting. It never decides how your panel looks: no stylesheet, no default visual style, only data attributes and CSS variables your styles can read.
26
29
 
27
30
  ---
28
31
 
@@ -30,197 +33,321 @@ Created with โค๏ธ by [Stefano Biddau](https://www.stefano-biddau.com/)
30
33
 
31
34
  ```bash
32
35
  # npm
33
- npm install sb-floating-panel-vue
36
+ npm install sb-floating-panel-vue vue @floating-ui/vue
34
37
 
35
38
  # yarn
36
- yarn add sb-floating-panel-vue
39
+ yarn add sb-floating-panel-vue vue @floating-ui/vue
37
40
 
38
41
  # pnpm
39
- pnpm add sb-floating-panel-vue
42
+ pnpm add sb-floating-panel-vue vue @floating-ui/vue
40
43
  ```
41
44
 
45
+ `vue` (`^3.5.13`) and `@floating-ui/vue` (`^2.0.1`) are peer dependencies: your app provides them, so only one copy of each ends up in the bundle. Vue is usually already installed in your app.
46
+
47
+ There is no CSS file to import.
48
+
42
49
  ---
43
50
 
44
- ## ๐Ÿง  Composable Usage
51
+ ## ๐Ÿงฑ Usage
52
+
53
+ ```vue
54
+ <script setup lang="ts">
55
+ import { SbContainer, SbReference, SbFloating } from 'sb-floating-panel-vue';
56
+ </script>
57
+
58
+ <template>
59
+ <SbContainer placement="bottom-start" match-width="min">
60
+ <template #default="{ close }">
61
+ <SbReference class="my-trigger" aria-haspopup="menu">Account</SbReference>
62
+ <SbFloating role="menu" transition="pop" class="my-panel">
63
+ <button type="button" role="menuitem" @click="close()">Sign out</button>
64
+ </SbFloating>
65
+ </template>
66
+ </SbContainer>
67
+ </template>
68
+ ```
69
+
70
+ `SbContainer` shares its state with `SbReference` and `SbFloating` through `provide`/`inject`: no refs or styles to pass by hand. Both must be used inside an `SbContainer`.
71
+
72
+ ### `<SbContainer>`
73
+
74
+ Holds the state and the positioning. Renders no element of its own, only its slot.
75
+
76
+ | Prop | Type | Default | Description |
77
+ |-----------------------|----------------------------------------|------------------|----------------------------------------------------------------------------------------------|
78
+ | `placement` | `SbPlacement` | `'bottom-start'` | Preferred placement; the panel flips when there is not enough space |
79
+ | `strategy` | `'absolute' \| 'fixed'` | `'absolute'` | CSS position strategy. Use `fixed` inside scroll containers or fixed layouts |
80
+ | `offset` | `number` | `8` | Distance in px between reference and panel |
81
+ | `padding` | `number` | `8` | Minimum distance in px between the panel and the viewport edges |
82
+ | `matchWidth` | `boolean \| 'min'` | `false` | `true` sets `width`, `'min'` sets `min-width` to the reference width |
83
+ | `hideWhenDetached` | `boolean` | `false` | Sets `visibility: hidden` when the reference is scrolled out of view |
84
+ | `v-model:open` | `boolean` | โ€” | Open state. Leave it unset to let the container manage it |
85
+ | `defaultOpen` | `boolean` | `false` | Initial open state when `v-model:open` is not used |
86
+ | `disabled` | `boolean` | `false` | Prevents opening; closes the panel if it is open |
87
+ | `trigger` | `'click' \| 'manual'` | `'click'` | `manual`: the reference does not toggle; open with `v-model:open`, slot props or methods |
88
+ | `closeOnEscape` | `boolean` | `true` | Closes on Escape |
89
+ | `closeOnClickOutside` | `boolean` | `true` | Closes on a pointer press outside the reference and the panel |
90
+
91
+ | Event | Payload | Description |
92
+ |---------------|-----------|--------------------------|
93
+ | `update:open` | `boolean` | Used by `v-model:open` |
94
+ | `open` | โ€” | The panel has opened |
95
+ | `close` | โ€” | The panel has closed |
96
+
97
+ **Slot props**: `isOpen`, `open()`, `close()`, `toggle()`, `placement` (resolved after flip), `side`.
98
+
99
+ **Exposed methods** (template ref): `open()`, `close()`, `toggle()`, `update()` (recomputes the position).
100
+
101
+ ### `<SbReference>`
45
102
 
46
- Use the composable `useSbFloatingPanel()` for complete control.
103
+ The trigger the panel is anchored to.
47
104
 
48
- ### Options
105
+ | Prop | Type | Default | Description |
106
+ |----------------|----------|------------|--------------------------------------------------------------------------------------------------------------|
107
+ | `as` | `string` | `'button'` | Tag to render. `button` gets `type="button"`; tags other than `button`/`a` get `role="button"`, `tabindex="0"` and Enter/Space |
108
+ | `ariaHaspopup` | `'dialog' \| 'menu' \| 'listbox' \| 'tree' \| 'grid' \| 'true' \| 'false'` | `'dialog'` | Value of `aria-haspopup`, matching the `role` of the panel |
49
109
 
110
+ Sets `aria-expanded`, `aria-controls` (while open) and `data-sbf-state`. When the container is `disabled`, a `button` gets `disabled`, other tags get `aria-disabled="true"` and `tabindex="-1"`. Your own `class`, attributes and listeners (`@click`, `@mouseenter`, โ€ฆ) are applied to the element; the click is not stopped.
50
111
 
51
- | Option | Type | Required | Default | Description |
52
- |------------|------------|----------|---------|--------------------------------------------------|
53
- | placement | `Placement` | Yes | โ€” | Placement of the floating panel |
54
- | strategy | `'absolute'` \| `'fixed'` | Yes | โ€” | CSS positioning strategy |
55
- | offsetValue| `number` | Yes | โ€” | Offset between reference and floating |
56
- | hasArrow | `boolean` | No | `false` | Whether to render arrow |
57
- | hasResize | `boolean` | No | `false` | Sync width of floating to reference |
112
+ ### `<SbFloating>`
58
113
 
59
- ### Return Values
114
+ The panel, teleported to `body`. It is rendered while open and stays mounted until its leave transition has finished.
60
115
 
61
- | Property | Type | Description |
62
- |-----------------------|--------------------------------------|----------------------------------------|
63
- | reference | `Ref<HTMLElement \| null>` | Ref for the trigger element |
64
- | floating | `Ref<FloatingElement \| null>` | Ref for the floating panel |
65
- | floatingArrow | `Ref<HTMLElement \| null>` | Ref for the arrow element |
66
- | floatingPlacement | `Ref<Placement>` | The actual resolved placement |
67
- | floatingStyle | `ComputedRef<CSSProperties>` | Style for the floating panel |
68
- | floatingArrowStyle | `ComputedRef<CSSProperties>` | Style for the arrow |
69
- | isOpen | `Ref<boolean>` | Panel visibility |
70
- | toggle / open / close | `() => void` | Control methods to toggle visibility |
116
+ | Prop | Type | Default | Description |
117
+ |--------------------|-------------------------|------------|-----------------------------------------------------------------------------------------------------|
118
+ | `as` | `string` | `'div'` | Tag of the panel element |
119
+ | `role` | `string` | `'dialog'` | ARIA role of the panel |
120
+ | `transition` | `string` | โ€” | Name of a Vue `<Transition>` defined in your CSS. No animation when unset |
121
+ | `teleportTo` | `string \| HTMLElement` | `'body'` | Teleport target |
122
+ | `teleportDisabled` | `boolean` | `false` | Renders the panel in place |
123
+ | `zIndex` | `number \| string` | โ€” | `z-index` of the positioned wrapper |
124
+ | `autoFocus` | `boolean` | `false` | Once positioned, focuses the first `[autofocus]` or focusable element inside, else the panel itself |
71
125
 
126
+ | Event | Description |
127
+ |---------------|----------------------------------------------------------------|
128
+ | `after-enter` | The enter transition has finished |
129
+ | `after-leave` | The leave transition has finished and the panel is removed |
72
130
 
73
- ### Example
74
- In your Vue component, you can use the composable like this:
131
+ When focus is inside the panel as it closes, it goes back to the reference.
132
+
133
+ ### Types
75
134
 
76
- In your script tag, import the composable and use it:
77
135
  ```ts
78
- import { useSbFloatingPanel } from 'sb-floating-panel-vue';
79
-
80
- const {
81
- reference,
82
- floating,
83
- floatingArrow,
84
- floatingStyle,
85
- floatingArrowStyle,
86
- isOpen,
87
- toggle,
88
- } = useSbFloatingPanel({
89
- placement: 'bottom-start',
90
- strategy: 'absolute',
91
- offsetValue: 12,
92
- hasArrow: true,
93
- });
94
- ```
95
- In your template, you can bind the refs and styles:
96
- ```html
97
- <template>
98
- <button ref="reference" @click="toggle()">Open Panel</button>
99
- <div v-if="isOpen" ref="floating" :style="floatingStyle">
100
- Panel
101
- <div ref="floatingArrow" :style="floatingArrowStyle" />
102
- </div>
103
- </template>
136
+ import type {
137
+ SbContainerProps,
138
+ SbContainerSlotProps,
139
+ SbContainerExposed,
140
+ SbReferenceProps,
141
+ SbFloatingProps,
142
+ SbTrigger,
143
+ SbPlacement,
144
+ SbSide,
145
+ SbAlignment,
146
+ SbStrategy,
147
+ } from 'sb-floating-panel-vue';
104
148
  ```
105
149
 
106
150
  ---
107
151
 
108
- ## ๐Ÿงฑ Component-Based Usage
152
+ ## ๐Ÿงญ How It Works
109
153
 
110
- ### Import the style
154
+ ### Shared state
111
155
 
112
- If you use components, you **must** import styles in `main.ts`:
156
+ `SbContainer` owns the state (open state, positioning, ARIA ids) and shares it with its descendants through Vue `provide`/`inject`:
113
157
 
114
- ```ts
115
- import 'sb-floating-panel-vue/style.css';
158
+ ```
159
+ SbContainer โ”€โ”€ provides the state โ”€โ”€โ”ฌโ”€โ–ถ SbReference
160
+ โ””โ”€โ–ถ SbFloating
116
161
  ```
117
162
 
118
- ### Available Components
163
+ - `SbReference` and `SbFloating` find their container on their own: you pass no refs, styles or handlers, and never write `.value`.
164
+ - The lookup follows the component tree, not the DOM, so it keeps working although `SbFloating` is teleported to `body`.
165
+ - They can sit at any depth inside the container slot, also inside your own components.
166
+ - Used outside an `SbContainer`, they throw: `[sb-floating-panel-vue] <SbFloating> must be used inside <SbContainer>.`
119
167
 
120
- #### `<SbContainer>`
168
+ ### Nested containers
121
169
 
122
- Wraps everything and provides the context.
170
+ An `SbContainer` placed inside another one (e.g. a submenu in a menu panel) registers with its parent:
123
171
 
124
- | Prop | Type | Default | Description |
125
- |-------------|---------------------------|-----------------|-----------------------------------------------------------------------------|
126
- | placement | `Placement` | `'bottom-start'`| Placement of the floating panel relative to the reference element |
127
- | strategy | `'absolute' \| 'fixed'` | `'absolute'` | CSS positioning strategy for the floating panel |
128
- | offsetValue | `number` | `15` | Offset in pixels between reference and floating panel |
129
- | hasArrow | `boolean` | `false` | Whether to render an arrow pointing to the reference |
130
- | hasResize | `boolean` | `false` | Whether the floating panel should sync its width with the reference |
172
+ - a press inside the nested panel is not "outside" for the parent, so both stay open;
173
+ - Escape closes the innermost open panel first, then the next one;
174
+ - a press outside all of them closes them all.
131
175
 
176
+ ### From click to open panel
132
177
 
178
+ 1. **Click** on `SbReference` (or Enter/Space on a non-interactive tag). With `trigger="click"` and the container not `disabled`, the container toggles: it emits `update:open` and, without `v-model:open`, updates its own state.
179
+ 2. **Open state changes.** `SbReference` gets `aria-expanded="true"`, `aria-controls` and `data-sbf-state="open"`; the container starts listening for Escape and outside presses (only while open) and emits `open`.
180
+ 3. **The panel mounts.** `SbFloating` renders the wrapper in the teleport target and the panel inside it; the enter classes of your `transition`, if any, are applied to the panel.
181
+ 4. **Positioning.** Floating UI starts tracking the reference (scroll, resize, layout changes) and computes the position with `offset`, `flip`, `shift`, `size` and, with `hideWhenDetached`, `hide`. `size` writes the `--sbf-*` variables and applies `matchWidth`; the position is written on the wrapper.
182
+ 5. **Positioned.** With `autoFocus`, focus moves inside the panel now, not earlier, so the page does not jump to the panel's initial (0, 0) position.
133
183
 
134
- #### `<SbReference>`
184
+ Closing (click on the reference, `close()`, Escape, press outside, `v-model:open` set to `false`, or `disabled`):
135
185
 
136
- Trigger element
186
+ 1. If focus is inside the panel, it goes back to the reference; the container emits `close`.
187
+ 2. The leave classes of your `transition` are applied; the wrapper stays mounted and positioned until the animation ends (immediately without `transition`).
188
+ 3. The wrapper is removed, Floating UI stops tracking, `SbFloating` emits `after-leave`.
137
189
 
138
- | Prop | Type | Default | Description |
139
- |--------------|-----------------------------------|--------------|------------------------------------------------------------------------------|
140
- | referenceRef | `Ref<HTMLElement \| null>` | **required** | Reference element to which the floating panel is attached |
141
- | as | `'button' \| 'div' \| 'span' \| etc.` | `'button'` | HTML tag to render. Defaults to `<button>` for accessibility |
142
- | onClick | `() => void` | โ€” | Optional click handler, often used to toggle the floating panel visibility |
190
+ ---
191
+
192
+ ## ๐ŸŽจ Styling
143
193
 
194
+ The library writes values on the elements; your CSS reads them. `SbFloating` renders two elements:
195
+
196
+ ```html
197
+ <!-- wrapper: positioning only (Floating UI styles, z-index, CSS variables) -->
198
+ <div style="position: absolute; transform: translate(120px, 40px); --sbf-reference-width: 180px; โ€ฆ">
199
+ <!-- panel: your class, style and attributes -->
200
+ <div class="my-panel" role="dialog" data-sbf-state="open" data-sbf-side="bottom" data-sbf-align="start">
201
+ โ€ฆ
202
+ </div>
203
+ </div>
204
+ ```
144
205
 
145
- #### `<SbFloating>`
206
+ Your styles, transitions and transforms go on the panel, so they never fight with the positioning `transform` of the wrapper.
146
207
 
147
- Floating panel
208
+ ### Data attributes
148
209
 
149
- | Prop | Type | Default | Description |
150
- |---------------------|----------------------------------------------|-----------------|-----------------------------------------------------------------------------|
151
- | floatingRef | `Ref<HTMLElement \| null>` | **required** | The target element for the floating panel (popover, tooltip, etc.) |
152
- | floatingArrowRef | `Ref<HTMLElement \| null>` | optional | The element used as the floating arrow |
153
- | isOpen | `boolean` | **required** | Controls visibility of the floating panel |
154
- | floatingPlacement | `Placement` | **required** | Current placement as resolved by `@floating-ui/vue` |
155
- | floatingStyle | `CSSProperties` | **required** | Inline style object applied to the floating panel |
156
- | floatingArrowStyle | `CSSProperties` | optional | Inline style object applied to the arrow element |
157
- | arrowDimensions | `number` | optional | Size (width/height) of the arrow in pixels |
158
- | zIndex | `number` | `undefined` | Custom z-index applied to the floating and arrow elements |
159
- | animation | `'fade' \| 'scale-fade' \| 'none'` | `'scale-fade'` | Defines the animation style for enter/leave transitions |
160
- | duration | `number` | `300` | Duration in ms for the transition animations |
210
+ | Attribute | Values | On | Use |
211
+ |------------------|--------------------------------------|--------------------------|---------------------------------------------|
212
+ | `data-sbf-state` | `open` / `closed` | reference | Open/closed styles (e.g. rotate an icon) |
213
+ | `data-sbf-state` | `open` | panel | Always `open` while rendered: style the exit with the leave classes of `transition` |
214
+ | `data-sbf-side` | `top` / `bottom` / `left` / `right` | panel | Real side after flip: directional animation |
215
+ | `data-sbf-align` | `start` / `end` / absent | panel | Alignment part of the placement |
161
216
 
217
+ ### CSS variables
162
218
 
163
- ---
219
+ Set on the wrapper and inherited by the panel:
164
220
 
165
- ### ๐Ÿงช Component Usage Example
221
+ | Variable | Meaning |
222
+ |--------------------------|------------------------------------------------------|
223
+ | `--sbf-reference-width` | Width of the reference |
224
+ | `--sbf-available-width` | Horizontal space left before overflowing the viewport |
225
+ | `--sbf-available-height` | Vertical space left before overflowing the viewport |
166
226
 
167
- In your project, you can use the components like this:
227
+ All attributes and variables use the `sbf` prefix, so they do not clash with other libraries in the same app (e.g. Radix / Reka UI `data-state`).
168
228
 
169
- In your script tag, import the components:
170
- ```ts
171
- import { SbContainer, SbFloating, SbReference } from 'sb-floating-panel-vue';
229
+ ### Plain CSS
230
+
231
+ Panels are teleported to `body`, so their styles cannot be `scoped` to the component that renders them: use global styles, CSS modules or utility classes.
232
+
233
+ ```css
234
+ .my-panel {
235
+ background: white;
236
+ border: 1px solid #ddd;
237
+ border-radius: 8px;
238
+ max-height: var(--sbf-available-height);
239
+ overflow-y: auto;
240
+ }
241
+
242
+ .my-trigger[data-sbf-state='open'] .chevron {
243
+ transform: rotate(180deg);
244
+ }
245
+
246
+ /* <SbFloating transition="pop"> */
247
+ .pop-enter-active,
248
+ .pop-leave-active {
249
+ transition: opacity 150ms ease, transform 150ms ease;
250
+ }
251
+ .pop-enter-from,
252
+ .pop-leave-to {
253
+ opacity: 0;
254
+ transform: scale(0.96);
255
+ }
256
+
257
+ /* direction from the side resolved after flip */
258
+ .pop-enter-from[data-sbf-side='bottom'] { transform: translateY(-6px); }
259
+ .pop-enter-from[data-sbf-side='top'] { transform: translateY(6px); }
172
260
  ```
173
261
 
174
- In your template, you can use the components as follows:
175
- ```html
176
- <SbContainer :has-arrow="true" strategy="fixed">
177
- <template #default="{
178
- reference,
179
- floating,
180
- floatingArrow,
181
- floatingPlacement,
182
- floatingStyle,
183
- floatingArrowStyle,
184
- isOpen,
185
- toggle
186
- }">
187
- <SbReference :reference-ref="reference" :onClick="toggle">Toggle</SbReference>
262
+ ### Tailwind (v4)
263
+
264
+ ```vue
265
+ <SbReference class="group rounded border px-3 py-2">
266
+ Options
267
+ <span class="inline-block transition-transform group-data-[sbf-state=open]:rotate-180">โ–พ</span>
268
+ </SbReference>
269
+ <SbFloating class="max-h-(--sbf-available-height) w-(--sbf-reference-width) overflow-y-auto rounded border bg-white shadow-lg">
270
+ โ€ฆ
271
+ </SbFloating>
272
+ ```
273
+
274
+ ---
275
+
276
+ ## โ™ฟ Accessibility
277
+
278
+ - `SbReference` gets `aria-expanded`, `aria-controls` (pointing to the panel `id`) and `aria-haspopup`; non-interactive tags get `role="button"`, `tabindex="0"` and Enter/Space.
279
+ - `SbFloating` gets an `id` generated with Vue `useId()` (SSR safe) and a `role` (`dialog` by default).
280
+ - Escape closes the innermost open panel; a press outside closes the panel. A container nested inside a panel counts as part of it.
281
+ - With `autoFocus`, focus moves inside the panel after it is positioned.
282
+ - When the panel closes with focus inside it, focus returns to the reference (with or without `autoFocus`).
283
+
284
+ ---
285
+
286
+ ## ๐Ÿ”€ Migrating from v2
287
+
288
+ v3 is a breaking release.
289
+
290
+ - **The composable is no longer exported.** Use the components; for full control use `@floating-ui/vue` directly.
291
+ - **No refs or styles in the slot.** Remove `reference-ref`, `floating-ref`, `is-open`, `floating-placement`, `floating-style` and `:on-click`: the components share their state.
292
+ - **No arrow.** `hasArrow`, `floating-arrow-ref`, `floating-arrow-style` and `arrow-dimensions` are gone.
293
+ - **No CSS.** Remove `import 'sb-floating-panel-vue/style.css'`. `animation` and `duration` are replaced by `transition`, the name of your own `<Transition>` classes.
294
+ - **Renamed props:** `offsetValue` โ†’ `offset` (default `15` โ†’ `8`), `hasResize` โ†’ `matchWidth`.
295
+ - **New defaults:** `padding: 8` from the viewport edges; Escape and click outside close the panel.
296
+ - `SbContainer` no longer renders a `<div>`; `SbReference` no longer stops click propagation and sets `type="button"`.
297
+
298
+ ```vue
299
+ <!-- v2 -->
300
+ <SbContainer :has-resize="true">
301
+ <template #default="{ reference, floating, floatingPlacement, floatingStyle, isOpen, toggle }">
302
+ <SbReference :reference-ref="reference" :on-click="toggle">Toggle</SbReference>
188
303
  <SbFloating
189
304
  :is-open="isOpen.value"
190
305
  :floating-ref="floating"
191
- :floating-arrow-ref="floatingArrow"
192
306
  :floating-placement="floatingPlacement.value"
193
307
  :floating-style="floatingStyle.value"
194
- :floating-arrow-style="floatingArrowStyle.value"
195
- :arrow-dimensions="12"
196
- :z-index="200"
197
308
  >
198
309
  Hello Panel!
199
310
  </SbFloating>
200
311
  </template>
201
312
  </SbContainer>
313
+
314
+ <!-- v3 -->
315
+ <SbContainer match-width>
316
+ <SbReference>Toggle</SbReference>
317
+ <SbFloating>Hello Panel!</SbFloating>
318
+ </SbContainer>
202
319
  ```
203
320
 
204
321
  ---
205
322
 
206
323
  ## โš ๏ธ Directive Note (Teleport Limitation)
207
324
 
208
- Due to `<teleport>` in `<SbFloating>`, if you apply custom directives directly to the component, Vue will warn:
325
+ `<SbFloating>` renders a `<teleport>` root, so a custom directive applied directly to the component cannot be inherited and Vue warns about it. Move the directive to an element inside the slot:
209
326
 
210
- ```
211
- [Vue warn]: Extraneous non-props attributes ([directive-name]) were passed to component but could not be automatically inherited because component renders fragment or text or teleport root nodes.
327
+ ```vue
328
+ <SbFloating>
329
+ <div v-my-directive>...</div>
330
+ </SbFloating>
212
331
  ```
213
332
 
214
- **Solution**: Move the directive to an inner element using the slot.
333
+ ---
215
334
 
216
- ### Example Fix
335
+ ## ๐Ÿ› ๏ธ Development
217
336
 
218
- ```vue
219
- <SbFloating ...>
220
- <div v-click-outside>...</div>
221
- </SbFloating>
337
+ ```bash
338
+ npm install
339
+ npm run dev # playground at http://localhost:5173 (playground/App.vue)
340
+ npm run lint # ESLint
341
+ npm run check # type check of src/, playground/ and test/
342
+ npm test # Vitest + @vue/test-utils (happy-dom)
343
+ npm run test:coverage # tests with V8 coverage report in coverage/
344
+ npm run build # library build in dist/ (ES and UMD, with .d.ts)
222
345
  ```
223
346
 
347
+ The playground imports the components from `src/`, so changes reload instantly. It is not part of the published package.
348
+
349
+ On every push to `main`, CI runs the tests with coverage and `scripts/coverage-badge.mjs` publishes the coverage badges (lines, statements, branches, functions) as SVG files on the `badges` branch, which the badges above read. No external service is involved.
350
+
224
351
  ---
225
352
 
226
353
  ## ๐Ÿ“„ License
@@ -1,65 +1,34 @@
1
- import { Placement, Strategy } from '@floating-ui/vue';
2
- /**
3
- * Props for the SbContainer component.
4
- * Serves as a controller and provider for the floating panel system.
5
- */
6
- interface SbContainerProps {
7
- /**
8
- * Placement of the floating panel relative to the reference.
9
- * @default 'bottom-start'
10
- */
11
- placement?: Placement;
12
- /**
13
- * Positioning strategy to use ('absolute' or 'fixed').
14
- * @default 'absolute'
15
- */
16
- strategy?: Strategy;
17
- /**
18
- * Pixel offset between the reference and floating panel.
19
- * @default 15
20
- */
21
- offsetValue?: number;
22
- /**
23
- * Whether to render an arrow pointing toward the reference element.
24
- * @default false
25
- */
26
- hasArrow?: boolean;
27
- /**
28
- * When true, syncs the floating panel width to the reference width.
29
- * @default false
30
- */
31
- hasResize?: boolean;
32
- }
33
- declare function __VLS_template(): {
34
- attrs: Partial<{}>;
35
- slots: {
36
- default?(_: {
37
- reference: import('vue').Ref<HTMLElement | null, HTMLElement | null>;
38
- floating: import('vue').Ref<HTMLElement | null, HTMLElement | null>;
39
- floatingArrow: import('vue').Ref<HTMLElement | null, HTMLElement | null>;
40
- floatingPlacement: import('vue').Ref<Placement, Placement>;
41
- floatingStyle: import('vue').ComputedRef<import('vue').CSSProperties>;
42
- floatingArrowStyle: import('vue').ComputedRef<import('vue').CSSProperties>;
43
- isOpen: import('vue').Ref<boolean, boolean>;
44
- toggle: () => void;
45
- open: () => void;
46
- close: () => void;
47
- }): any;
48
- };
49
- refs: {};
50
- rootEl: any;
51
- };
52
- type __VLS_TemplateResult = ReturnType<typeof __VLS_template>;
53
- declare const __VLS_component: import('vue').DefineComponent<SbContainerProps, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<SbContainerProps> & Readonly<{}>, {
54
- placement: Placement;
55
- strategy: Strategy;
56
- offsetValue: number;
57
- hasArrow: boolean;
58
- hasResize: boolean;
59
- }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
60
- declare const _default: __VLS_WithTemplateSlots<typeof __VLS_component, __VLS_TemplateResult["slots"]>;
1
+ import { SbContainerExposed, SbContainerProps, SbContainerSlotProps } from '../types';
2
+ declare const _default: typeof __VLS_export;
61
3
  export default _default;
62
- type __VLS_WithTemplateSlots<T, S> = T & {
4
+ declare const __VLS_export: __VLS_WithSlots<import('vue').DefineComponent<Omit<SbContainerProps, "open"> & {
5
+ open?: boolean | undefined;
6
+ }, SbContainerExposed, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
7
+ open: () => any;
8
+ "update:open": (value: boolean | undefined) => any;
9
+ close: () => any;
10
+ }, string, import('vue').PublicProps, Readonly<Omit<SbContainerProps, "open"> & {
11
+ open?: boolean | undefined;
12
+ }> & Readonly<{
13
+ onOpen?: (() => any) | undefined;
14
+ "onUpdate:open"?: ((value: boolean | undefined) => any) | undefined;
15
+ onClose?: (() => any) | undefined;
16
+ }>, {
17
+ placement: import('..').SbPlacement;
18
+ strategy: import('..').SbStrategy;
19
+ padding: number;
20
+ offset: number;
21
+ matchWidth: boolean | "min";
22
+ hideWhenDetached: boolean;
23
+ defaultOpen: boolean;
24
+ disabled: boolean;
25
+ trigger: import('..').SbTrigger;
26
+ closeOnEscape: boolean;
27
+ closeOnClickOutside: boolean;
28
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>, {
29
+ default?: (props: SbContainerSlotProps) => unknown;
30
+ }>;
31
+ type __VLS_WithSlots<T, S> = T & {
63
32
  new (): {
64
33
  $slots: S;
65
34
  };