@yewdiel21/motion-kit 0.10.0 → 0.14.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 +155 -93
- package/dist/auto/index.d.ts +53 -3
- package/dist/auto/index.js +101 -3
- package/dist/auto/index.js.map +1 -1
- package/dist/{chunk-IKQ5FODN.js → chunk-6P4P7NU6.js} +2 -218
- package/dist/chunk-6P4P7NU6.js.map +1 -0
- package/dist/chunk-CU6SXW3E.js +20334 -0
- package/dist/chunk-CU6SXW3E.js.map +1 -0
- package/dist/{chunk-XAKC2QY7.js → chunk-EY4BJKC4.js} +3 -28
- package/dist/chunk-EY4BJKC4.js.map +1 -0
- package/dist/chunk-QP4QJOEN.js +72 -0
- package/dist/chunk-QP4QJOEN.js.map +1 -0
- package/dist/css/components/activity-feed.css +240 -0
- package/dist/css/components/attachment.css +266 -0
- package/dist/css/components/breadcrumb.css +110 -0
- package/dist/css/components/chart-tooltip.css +168 -0
- package/dist/css/components/chart.css +296 -0
- package/dist/css/components/code-block.css +264 -0
- package/dist/css/components/color-area.css +82 -0
- package/dist/css/components/color-field.css +67 -0
- package/dist/css/components/color-picker.css +131 -0
- package/dist/css/components/color-slider.css +113 -0
- package/dist/css/components/command-search.css +252 -0
- package/dist/css/components/context-menu.css +23 -0
- package/dist/css/components/credit-card.css +288 -0
- package/dist/css/components/data-table.css +591 -0
- package/dist/css/components/date-range-picker.css +168 -0
- package/dist/css/components/date-time-picker.css +178 -0
- package/dist/css/components/drawer.css +58 -0
- package/dist/css/components/editable-table.css +201 -0
- package/dist/css/components/empty-state.css +61 -0
- package/dist/css/components/file-explorer.css +633 -0
- package/dist/css/components/filter-builder.css +232 -0
- package/dist/css/components/hold-confirm.css +87 -0
- package/dist/css/components/hover-card.css +13 -7
- package/dist/css/components/image-preview.css +195 -0
- package/dist/css/components/json-viewer.css +272 -0
- package/dist/css/components/like-button.css +78 -0
- package/dist/css/components/link-preview.css +275 -0
- package/dist/css/components/loading-button.css +77 -0
- package/dist/css/components/multi-select.css +267 -0
- package/dist/css/components/notification-bell.css +127 -0
- package/dist/css/components/password-strength.css +183 -0
- package/dist/css/components/popover.css +25 -0
- package/dist/css/components/progress.css +140 -0
- package/dist/css/components/questionnaire.css +217 -0
- package/dist/css/components/range-calendar.css +186 -0
- package/dist/css/components/range-slider.css +192 -0
- package/dist/css/components/resizable-panel.css +243 -0
- package/dist/css/components/rich-text-editor.css +284 -0
- package/dist/css/components/scratch-card.css +75 -0
- package/dist/css/components/skeleton.css +108 -0
- package/dist/css/components/split-view.css +19 -0
- package/dist/css/components/star-rating.css +80 -0
- package/dist/css/components/stat-card.css +164 -0
- package/dist/css/components/swipe-actions.css +105 -0
- package/dist/css/components/tag-input.css +244 -0
- package/dist/css/components/time-field.css +88 -0
- package/dist/css/components/timeline.css +219 -0
- package/dist/css/components/tooltip.css +79 -0
- package/dist/css/components/tree-view.css +210 -0
- package/dist/css/components/virtual-list.css +195 -0
- package/dist/css/tokens.css +185 -181
- package/dist/elements/index.d.ts +674 -5
- package/dist/elements/index.js +1361 -8
- package/dist/elements/index.js.map +1 -1
- package/dist/{engine-GDGwQnJE.d.ts → engine-Cn6IhE3z.d.ts} +1 -1
- package/dist/engine-QECQnGz1.d.ts +33 -0
- package/dist/index.css +9104 -189
- package/dist/index.d.ts +89 -5
- package/dist/index.js +243 -9
- package/dist/index.js.map +1 -1
- package/dist/react/index.d.ts +579 -23
- package/dist/react/index.js +2042 -7
- package/dist/react/index.js.map +1 -1
- package/dist/{reduced-BmBzKH94.d.ts → reduced-BBUusld6.d.ts} +9 -2
- package/dist/reveal-DEvp6U6T.d.ts +2381 -0
- package/dist/{store-DYb3tTsZ.d.ts → store-DjkTBX7f.d.ts} +2 -2
- package/package.json +1 -1
- package/registry.json +406 -1
- package/src/auto/auto-init.ts +98 -0
- package/src/components/activity-feed/README.md +56 -0
- package/src/components/activity-feed/activity-feed.css +239 -0
- package/src/components/activity-feed/element.ts +36 -0
- package/src/components/activity-feed/engine.ts +351 -0
- package/src/components/activity-feed/react.tsx +32 -0
- package/src/components/attachment/README.md +27 -0
- package/src/components/attachment/attachment.css +265 -0
- package/src/components/attachment/element.ts +50 -0
- package/src/components/attachment/engine.ts +469 -0
- package/src/components/attachment/react.tsx +65 -0
- package/src/components/breadcrumb/README.md +38 -0
- package/src/components/breadcrumb/breadcrumb.css +109 -0
- package/src/components/breadcrumb/element.ts +27 -0
- package/src/components/breadcrumb/engine.ts +249 -0
- package/src/components/breadcrumb/react.tsx +30 -0
- package/src/components/chart/README.md +24 -0
- package/src/components/chart/chart.css +295 -0
- package/src/components/chart/element.ts +41 -0
- package/src/components/chart/engine.ts +811 -0
- package/src/components/chart/react.tsx +50 -0
- package/src/components/chart-tooltip/README.md +53 -0
- package/src/components/chart-tooltip/chart-tooltip.css +167 -0
- package/src/components/chart-tooltip/element.ts +37 -0
- package/src/components/chart-tooltip/engine.ts +442 -0
- package/src/components/chart-tooltip/react.tsx +47 -0
- package/src/components/code-block/README.md +19 -0
- package/src/components/code-block/code-block.css +263 -0
- package/src/components/code-block/element.ts +34 -0
- package/src/components/code-block/engine.ts +254 -0
- package/src/components/code-block/highlight.ts +244 -0
- package/src/components/code-block/react.tsx +94 -0
- package/src/components/color-area/README.md +50 -0
- package/src/components/color-area/color-area.css +81 -0
- package/src/components/color-area/element.ts +26 -0
- package/src/components/color-area/engine.ts +246 -0
- package/src/components/color-area/react.tsx +42 -0
- package/src/components/color-field/README.md +43 -0
- package/src/components/color-field/color-field.css +66 -0
- package/src/components/color-field/element.ts +26 -0
- package/src/components/color-field/engine.ts +174 -0
- package/src/components/color-field/react.tsx +42 -0
- package/src/components/color-picker/README.md +45 -0
- package/src/components/color-picker/color-picker.css +130 -0
- package/src/components/color-picker/element.ts +45 -0
- package/src/components/color-picker/engine.ts +314 -0
- package/src/components/color-picker/react.tsx +47 -0
- package/src/components/color-slider/README.md +55 -0
- package/src/components/color-slider/color-slider.css +112 -0
- package/src/components/color-slider/element.ts +26 -0
- package/src/components/color-slider/engine.ts +234 -0
- package/src/components/color-slider/react.tsx +45 -0
- package/src/components/command-search/README.md +15 -0
- package/src/components/command-search/command-search.css +251 -0
- package/src/components/command-search/element.ts +36 -0
- package/src/components/command-search/engine.ts +356 -0
- package/src/components/command-search/match.ts +53 -0
- package/src/components/command-search/react.tsx +66 -0
- package/src/components/context-menu/README.md +31 -0
- package/src/components/context-menu/context-menu.css +22 -0
- package/src/components/context-menu/element.ts +40 -0
- package/src/components/context-menu/engine.ts +153 -0
- package/src/components/context-menu/react.tsx +57 -0
- package/src/components/credit-card/README.md +20 -0
- package/src/components/credit-card/card.ts +77 -0
- package/src/components/credit-card/credit-card.css +287 -0
- package/src/components/credit-card/element.ts +33 -0
- package/src/components/credit-card/engine.ts +410 -0
- package/src/components/credit-card/react.tsx +36 -0
- package/src/components/data-table/README.md +83 -0
- package/src/components/data-table/data-table.css +590 -0
- package/src/components/data-table/element.ts +54 -0
- package/src/components/data-table/engine.ts +1059 -0
- package/src/components/data-table/react.tsx +94 -0
- package/src/components/date-range-picker/README.md +51 -0
- package/src/components/date-range-picker/date-range-picker.css +167 -0
- package/src/components/date-range-picker/element.ts +52 -0
- package/src/components/date-range-picker/engine.ts +352 -0
- package/src/components/date-range-picker/react.tsx +57 -0
- package/src/components/date-time-picker/README.md +48 -0
- package/src/components/date-time-picker/date-time-picker.css +177 -0
- package/src/components/date-time-picker/element.ts +47 -0
- package/src/components/date-time-picker/engine.ts +350 -0
- package/src/components/date-time-picker/react.tsx +54 -0
- package/src/components/dialog/README.md +103 -102
- package/src/components/dialog/element.ts +123 -118
- package/src/components/dialog/engine.ts +449 -432
- package/src/components/drawer/README.md +28 -0
- package/src/components/drawer/drawer.css +57 -0
- package/src/components/drawer/element.ts +21 -0
- package/src/components/drawer/engine.ts +56 -0
- package/src/components/drawer/react.tsx +85 -0
- package/src/components/editable-table/README.md +67 -0
- package/src/components/editable-table/editable-table.css +200 -0
- package/src/components/editable-table/element.ts +45 -0
- package/src/components/editable-table/engine.ts +775 -0
- package/src/components/editable-table/react.tsx +79 -0
- package/src/components/empty-state/README.md +41 -0
- package/src/components/empty-state/element.ts +23 -0
- package/src/components/empty-state/empty-state.css +60 -0
- package/src/components/empty-state/engine.ts +232 -0
- package/src/components/empty-state/react.tsx +34 -0
- package/src/components/file-explorer/README.md +67 -0
- package/src/components/file-explorer/element.ts +39 -0
- package/src/components/file-explorer/engine.ts +1307 -0
- package/src/components/file-explorer/file-explorer.css +632 -0
- package/src/components/file-explorer/react.tsx +77 -0
- package/src/components/filter-builder/README.md +47 -0
- package/src/components/filter-builder/element.ts +34 -0
- package/src/components/filter-builder/engine.ts +365 -0
- package/src/components/filter-builder/filter-builder.css +231 -0
- package/src/components/filter-builder/react.tsx +36 -0
- package/src/components/hold-confirm/README.md +13 -0
- package/src/components/hold-confirm/element.ts +24 -0
- package/src/components/hold-confirm/engine.ts +216 -0
- package/src/components/hold-confirm/hold-confirm.css +86 -0
- package/src/components/hold-confirm/react.tsx +28 -0
- package/src/components/hover-card/README.md +6 -2
- package/src/components/hover-card/engine.ts +68 -6
- package/src/components/hover-card/hover-card.css +13 -7
- package/src/components/image-preview/README.md +16 -0
- package/src/components/image-preview/element.ts +26 -0
- package/src/components/image-preview/engine.ts +325 -0
- package/src/components/image-preview/image-preview.css +194 -0
- package/src/components/image-preview/react.tsx +43 -0
- package/src/components/json-viewer/README.md +56 -0
- package/src/components/json-viewer/element.ts +37 -0
- package/src/components/json-viewer/engine.ts +440 -0
- package/src/components/json-viewer/json-viewer.css +271 -0
- package/src/components/json-viewer/react.tsx +56 -0
- package/src/components/like-button/README.md +13 -0
- package/src/components/like-button/element.ts +47 -0
- package/src/components/like-button/engine.ts +145 -0
- package/src/components/like-button/like-button.css +77 -0
- package/src/components/like-button/react.tsx +39 -0
- package/src/components/link-preview/README.md +19 -0
- package/src/components/link-preview/element.ts +29 -0
- package/src/components/link-preview/engine.ts +192 -0
- package/src/components/link-preview/link-preview.css +274 -0
- package/src/components/link-preview/react.tsx +101 -0
- package/src/components/loading-button/README.md +45 -0
- package/src/components/loading-button/element.ts +34 -0
- package/src/components/loading-button/engine.ts +201 -0
- package/src/components/loading-button/loading-button.css +76 -0
- package/src/components/loading-button/react.tsx +43 -0
- package/src/components/multi-select/README.md +48 -0
- package/src/components/multi-select/element.ts +28 -0
- package/src/components/multi-select/engine.ts +294 -0
- package/src/components/multi-select/multi-select.css +266 -0
- package/src/components/multi-select/react.tsx +39 -0
- package/src/components/notification-bell/README.md +14 -0
- package/src/components/notification-bell/element.ts +48 -0
- package/src/components/notification-bell/engine.ts +202 -0
- package/src/components/notification-bell/notification-bell.css +126 -0
- package/src/components/notification-bell/react.tsx +57 -0
- package/src/components/password-strength/README.md +17 -0
- package/src/components/password-strength/element.ts +22 -0
- package/src/components/password-strength/engine.ts +212 -0
- package/src/components/password-strength/password-strength.css +182 -0
- package/src/components/password-strength/react.tsx +36 -0
- package/src/components/password-strength/score.ts +62 -0
- package/src/components/popover/README.md +44 -0
- package/src/components/popover/element.ts +31 -0
- package/src/components/popover/engine.ts +114 -0
- package/src/components/popover/popover.css +24 -0
- package/src/components/popover/react.tsx +50 -0
- package/src/components/progress/README.md +17 -0
- package/src/components/progress/element.ts +30 -0
- package/src/components/progress/engine.ts +173 -0
- package/src/components/progress/progress.css +139 -0
- package/src/components/progress/react.tsx +35 -0
- package/src/components/questionnaire/README.md +32 -0
- package/src/components/questionnaire/element.ts +118 -0
- package/src/components/questionnaire/engine.ts +255 -0
- package/src/components/questionnaire/questionnaire.css +216 -0
- package/src/components/questionnaire/react.tsx +140 -0
- package/src/components/range-calendar/README.md +58 -0
- package/src/components/range-calendar/element.ts +42 -0
- package/src/components/range-calendar/engine.ts +390 -0
- package/src/components/range-calendar/range-calendar.css +185 -0
- package/src/components/range-calendar/react.tsx +56 -0
- package/src/components/range-slider/README.md +23 -0
- package/src/components/range-slider/element.ts +53 -0
- package/src/components/range-slider/engine.ts +316 -0
- package/src/components/range-slider/range-slider.css +191 -0
- package/src/components/range-slider/react.tsx +66 -0
- package/src/components/resizable-panel/README.md +61 -0
- package/src/components/resizable-panel/element.ts +36 -0
- package/src/components/resizable-panel/engine.ts +551 -0
- package/src/components/resizable-panel/react.tsx +74 -0
- package/src/components/resizable-panel/resizable-panel.css +242 -0
- package/src/components/rich-text-editor/README.md +57 -0
- package/src/components/rich-text-editor/element.ts +34 -0
- package/src/components/rich-text-editor/engine.ts +673 -0
- package/src/components/rich-text-editor/react.tsx +60 -0
- package/src/components/rich-text-editor/rich-text-editor.css +283 -0
- package/src/components/scratch-card/README.md +15 -0
- package/src/components/scratch-card/element.ts +30 -0
- package/src/components/scratch-card/engine.ts +257 -0
- package/src/components/scratch-card/react.tsx +40 -0
- package/src/components/scratch-card/scratch-card.css +74 -0
- package/src/components/skeleton/README.md +15 -0
- package/src/components/skeleton/element.ts +32 -0
- package/src/components/skeleton/engine.ts +104 -0
- package/src/components/skeleton/react.tsx +41 -0
- package/src/components/skeleton/skeleton.css +107 -0
- package/src/components/split-view/README.md +50 -0
- package/src/components/split-view/element.ts +34 -0
- package/src/components/split-view/engine.ts +114 -0
- package/src/components/split-view/react.tsx +63 -0
- package/src/components/split-view/split-view.css +18 -0
- package/src/components/star-rating/README.md +14 -0
- package/src/components/star-rating/element.ts +26 -0
- package/src/components/star-rating/engine.ts +217 -0
- package/src/components/star-rating/react.tsx +33 -0
- package/src/components/star-rating/star-rating.css +79 -0
- package/src/components/stat-card/README.md +42 -0
- package/src/components/stat-card/element.ts +40 -0
- package/src/components/stat-card/engine.ts +283 -0
- package/src/components/stat-card/react.tsx +36 -0
- package/src/components/stat-card/stat-card.css +163 -0
- package/src/components/swipe-actions/README.md +19 -0
- package/src/components/swipe-actions/element.ts +26 -0
- package/src/components/swipe-actions/engine.ts +260 -0
- package/src/components/swipe-actions/react.tsx +80 -0
- package/src/components/swipe-actions/swipe-actions.css +104 -0
- package/src/components/tag-input/README.md +42 -0
- package/src/components/tag-input/element.ts +39 -0
- package/src/components/tag-input/engine.ts +313 -0
- package/src/components/tag-input/react.tsx +54 -0
- package/src/components/tag-input/tag-input.css +243 -0
- package/src/components/time-field/README.md +54 -0
- package/src/components/time-field/element.ts +38 -0
- package/src/components/time-field/engine.ts +110 -0
- package/src/components/time-field/react.tsx +45 -0
- package/src/components/time-field/time-field.css +87 -0
- package/src/components/timeline/README.md +57 -0
- package/src/components/timeline/element.ts +31 -0
- package/src/components/timeline/engine.ts +300 -0
- package/src/components/timeline/react.tsx +59 -0
- package/src/components/timeline/timeline.css +218 -0
- package/src/components/tooltip/README.md +46 -0
- package/src/components/tooltip/element.ts +22 -0
- package/src/components/tooltip/engine.ts +288 -0
- package/src/components/tooltip/react.tsx +30 -0
- package/src/components/tooltip/tooltip.css +78 -0
- package/src/components/tree-view/README.md +61 -0
- package/src/components/tree-view/element.ts +32 -0
- package/src/components/tree-view/engine.ts +569 -0
- package/src/components/tree-view/react.tsx +78 -0
- package/src/components/tree-view/tree-view.css +209 -0
- package/src/components/virtual-list/README.md +81 -0
- package/src/components/virtual-list/element.ts +42 -0
- package/src/components/virtual-list/engine.ts +678 -0
- package/src/components/virtual-list/react.tsx +101 -0
- package/src/components/virtual-list/virtual-list.css +194 -0
- package/src/core/chips.ts +196 -0
- package/src/core/color.ts +304 -0
- package/src/core/date-range.ts +67 -0
- package/src/core/dates.ts +11 -1
- package/src/core/files.ts +107 -0
- package/src/core/filter.ts +229 -0
- package/src/core/gsap.ts +80 -65
- package/src/core/listbox.ts +332 -0
- package/src/core/mark.ts +29 -0
- package/src/core/position.ts +89 -0
- package/src/core/relative-time.ts +77 -0
- package/src/core/segment-field.ts +373 -0
- package/src/core/segments.ts +153 -0
- package/src/core/tree.ts +116 -0
- package/src/core/virtual.ts +79 -0
- package/src/css/tokens.css +186 -182
- package/src/elements/index.ts +147 -0
- package/src/index.ts +72 -0
- package/src/motion/micro.ts +248 -0
- package/src/motion/sheet.ts +88 -65
- package/src/motion/surface.ts +9 -0
- package/src/react/index.ts +57 -0
- package/dist/chunk-IKQ5FODN.js.map +0 -1
- package/dist/chunk-J6OCFNGU.js +0 -177
- package/dist/chunk-J6OCFNGU.js.map +0 -1
- package/dist/chunk-TFNRIZHU.js +0 -3977
- package/dist/chunk-TFNRIZHU.js.map +0 -1
- package/dist/chunk-XAKC2QY7.js.map +0 -1
- package/dist/reveal-jsX-uwUz.d.ts +0 -625
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# resizable-panel
|
|
2
|
+
|
|
3
|
+
Paneles redimensionables con divisores arrastrables, uno al lado del otro o apilados. Cada divisor es una línea fina con un asa que crece y toma el color de acento al pasar el puntero, al enfocarlo y al arrastrarlo; mientras se mueve, una burbuja muestra los dos tamaños («32 % · 68 %»). Funciona con mouse, lápiz y pantalla táctil.
|
|
4
|
+
|
|
5
|
+
Al llegar al límite de un panel, el arrastre cede solo una cuarta parte del camino (efecto goma) y al soltarlo vuelve con un resorte (.45 s, curva swing). Un panel plegable arrastrado muy por debajo de su mínimo se cierra de golpe (.3 s, curva swing) y se vuelve a abrir si lo arrastras de regreso, con Intro sobre su divisor o con los botones del divisor (`handleButtons`). Doble clic en un divisor devuelve los tamaños iniciales (.45 s, curva signature). Los tamaños se pueden recordar entre visitas (`storageKey`).
|
|
6
|
+
|
|
7
|
+
## HTML / Astro
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<mk-resizable-panel class="mk-resizable-panel" sizes="22,48,30" collapsible="2" handle-buttons storage-key="correo">
|
|
11
|
+
<aside data-label="Carpetas" data-min="15">…</aside>
|
|
12
|
+
<main data-label="Mensajes">…</main>
|
|
13
|
+
<section data-label="Vista previa" data-min="20">…</section>
|
|
14
|
+
</mk-resizable-panel>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Cada panel puede llevar `data-size`, `data-min`, `data-max` (en %), `data-collapsible` y `data-label`. Conviene poner el relleno (`padding`) en un elemento interior: un panel cerrado no conserva el suyo.
|
|
18
|
+
|
|
19
|
+
## React
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
const panels = useRef<ResizablePanelHandle>(null);
|
|
23
|
+
|
|
24
|
+
<ResizablePanel ref={panels} sizes={[25, 75]} collapsible={[0]} handleButtons onResizeEnd={guardar} style={{ height: 420 }}>
|
|
25
|
+
<aside>…</aside>
|
|
26
|
+
<main>…</main>
|
|
27
|
+
</ResizablePanel>;
|
|
28
|
+
|
|
29
|
+
panels.current?.toggle(0);
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Unos `sizes` nuevos se deslizan hasta ahí; agregar o quitar un panel reinicia el componente.
|
|
33
|
+
|
|
34
|
+
## Opciones
|
|
35
|
+
|
|
36
|
+
| Opción | Atributo | Por defecto | Qué hace |
|
|
37
|
+
|---|---|---|---|
|
|
38
|
+
| `orientation` | `orientation` | `'horizontal'` | `'horizontal'`: lado a lado; `'vertical'`: apilados. |
|
|
39
|
+
| `sizes` | `sizes` (lista con comas) | `data-size`, si no, iguales | Tamaños en %; los que falten se reparten lo que sobra. |
|
|
40
|
+
| `min` / `max` | `min` / `max` | `10` / `100` | Límites en % (los `data-min` y `data-max` de cada panel mandan). |
|
|
41
|
+
| `collapsible` | `collapsible` (índices) | `[]` | Paneles que se cierran al arrastrarlos más allá de la mitad de su mínimo. |
|
|
42
|
+
| `collapsed` | `collapsed` (índices) | `[]` | Paneles cerrados al empezar. |
|
|
43
|
+
| `step` | `step` | `5` | Lo que mueven las flechas (Mayús: el cuádruple). |
|
|
44
|
+
| `storageKey` | `storage-key` | — | Recordar tamaños y paneles cerrados en `localStorage`. |
|
|
45
|
+
| `handleButtons` | `handle-buttons` | `false` | Botones en los divisores para cerrar y abrir los paneles de al lado. |
|
|
46
|
+
| `labels` | `labels` (lista) | `data-label` | Nombres de los paneles para las etiquetas de los divisores. |
|
|
47
|
+
| `onResize(sizes)` | — | — | Mientras cambia (una vez por cuadro). |
|
|
48
|
+
| `onResizeEnd(sizes)` | — | — | Al terminar un arrastre, una tecla o una animación. |
|
|
49
|
+
| `onCollapse(index, closed)` | — | — | Un panel se cerró o se abrió. |
|
|
50
|
+
|
|
51
|
+
## API y eventos
|
|
52
|
+
|
|
53
|
+
`setSizes(sizes, animate = true)`, `collapse(index)`, `expand(index)`, `toggle(index)`, `reset()`, `setOrientation(orientation)`, `sizes`, `collapsed`, `orientation`, `destroy()`. Eventos `mk:resize` con `{ sizes }` y `mk:collapse` con `{ index, collapsed }`.
|
|
54
|
+
|
|
55
|
+
## Teclado
|
|
56
|
+
|
|
57
|
+
Sobre un divisor: ← → (o ↑ ↓ si están apilados) mueven el 5 %, con Mayús el 20 %; Inicio y Fin llevan el panel a sus límites; Intro cierra o abre el panel plegable de al lado. En un límite, el asa se sacude un poco.
|
|
58
|
+
|
|
59
|
+
## Accesibilidad
|
|
60
|
+
|
|
61
|
+
Patrón *window splitter* de WAI-ARIA: cada divisor es un `separator` enfocable con `aria-valuenow`, `aria-valuemin`, `aria-valuemax`, un texto («40 %, cerrado»), `aria-controls` apuntando al panel anterior y una etiqueta con los nombres de los dos paneles. Un panel cerrado queda oculto también para los lectores de pantalla, y un estado discreto anuncia cada cierre y apertura. Con movimiento reducido los cambios son inmediatos.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { MkElement, defineElement, type OptionType } from '../../elements/base';
|
|
2
|
+
import { createResizablePanel, type ResizablePanelInstance } from './engine';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* <mk-resizable-panel class="mk-resizable-panel" sizes="25,75" collapsible="0" storage-key="layout">
|
|
6
|
+
* <aside data-min="15">…</aside><main>…</main>
|
|
7
|
+
* </mk-resizable-panel>. Emits mk:resize ({sizes}) and mk:collapse ({index, collapsed}).
|
|
8
|
+
*/
|
|
9
|
+
export class MkResizablePanelElement extends MkElement<ResizablePanelInstance> {
|
|
10
|
+
static options: Record<string, OptionType> = {
|
|
11
|
+
orientation: 'string',
|
|
12
|
+
sizes: 'list',
|
|
13
|
+
min: 'number',
|
|
14
|
+
max: 'number',
|
|
15
|
+
collapsible: 'list',
|
|
16
|
+
collapsed: 'list',
|
|
17
|
+
step: 'number',
|
|
18
|
+
storageKey: 'string',
|
|
19
|
+
handleButtons: 'boolean',
|
|
20
|
+
labels: 'list',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
protected create(options: Record<string, unknown>) {
|
|
24
|
+
const numbers = (value: unknown) => (Array.isArray(value) ? value.map(Number).filter(Number.isFinite) : undefined);
|
|
25
|
+
return createResizablePanel(this, { ...options, sizes: numbers(options.sizes), collapsible: numbers(options.collapsible), collapsed: numbers(options.collapsed) });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
attributeChangedCallback(name: string, previous: string | null, next: string | null) {
|
|
29
|
+
if (previous === next || !this.instance) return;
|
|
30
|
+
if (name === 'orientation') this.instance.setOrientation(next === 'vertical' ? 'vertical' : 'horizontal');
|
|
31
|
+
else if (name === 'sizes') this.instance.setSizes((next ?? '').split(',').map(Number).filter(Number.isFinite));
|
|
32
|
+
else super.attributeChangedCallback(name, previous, next);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const defineResizablePanel = () => defineElement('mk-resizable-panel', MkResizablePanelElement);
|
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
import { gsap, EASE, registerMotion } from '../../core/gsap';
|
|
2
|
+
import { reduced } from '../../core/reduced';
|
|
3
|
+
import { createScope } from '../../core/scope';
|
|
4
|
+
import { inert, type Instance } from '../../core/types';
|
|
5
|
+
import { fromJson } from '../../core/dates';
|
|
6
|
+
import { requestFrame } from '../../core/frame';
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
* Resizable panels: panels side by side (or stacked) with a separator between
|
|
10
|
+
* each pair that drags (mouse, pen, touch) and moves with the keyboard (the
|
|
11
|
+
* WAI-ARIA window splitter). A separator is a hairline with a grip that grows
|
|
12
|
+
* in the accent on hover and while dragging, and a bubble shows the two sizes
|
|
13
|
+
* ("32 % · 68 %") while it moves. Past a panel's limit the drag gives a quarter
|
|
14
|
+
* of the way (rubber band) and springs back on release (.45 s, swing). A
|
|
15
|
+
* collapsible panel dragged well under its minimum snaps shut (.3 s, swing)
|
|
16
|
+
* and opens again when dragged back out, with Enter on its separator, or with
|
|
17
|
+
* the separator's buttons (handleButtons). A double click on a separator puts
|
|
18
|
+
* the first sizes back (.45 s, signature). Sizes can be remembered.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export type PanelOrientation = 'horizontal' | 'vertical';
|
|
22
|
+
|
|
23
|
+
export interface ResizablePanelOptions {
|
|
24
|
+
/** 'horizontal' (default): side by side; 'vertical': stacked. */
|
|
25
|
+
orientation?: PanelOrientation;
|
|
26
|
+
/** Sizes in percent, one per panel (default: data-size on the panels, else equal). */
|
|
27
|
+
sizes?: number[];
|
|
28
|
+
/** Smallest size of a panel in percent (default 10; data-min on a panel wins). */
|
|
29
|
+
min?: number;
|
|
30
|
+
/** Largest size of a panel in percent (default 100; data-max on a panel wins). */
|
|
31
|
+
max?: number;
|
|
32
|
+
/** Panels (indexes) that snap shut when dragged well under their minimum; data-collapsible on a panel too. */
|
|
33
|
+
collapsible?: number[];
|
|
34
|
+
/** Panels that start shut. */
|
|
35
|
+
collapsed?: number[];
|
|
36
|
+
/** Arrow keys move this many percent (default 5; Shift: four times). */
|
|
37
|
+
step?: number;
|
|
38
|
+
/** Remember sizes in localStorage under this key. */
|
|
39
|
+
storageKey?: string;
|
|
40
|
+
/** Buttons on each separator to shut and open the panels beside it. */
|
|
41
|
+
handleButtons?: boolean;
|
|
42
|
+
/** Names of the panels, for the separators' labels (default: data-label, else "panel 1"…). */
|
|
43
|
+
labels?: string[];
|
|
44
|
+
/** While it moves (once a frame). */
|
|
45
|
+
onResize?: (sizes: number[]) => void;
|
|
46
|
+
/** When a drag, a key or an animation ends. */
|
|
47
|
+
onResizeEnd?: (sizes: number[]) => void;
|
|
48
|
+
onCollapse?: (index: number, collapsed: boolean) => void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ResizablePanelInstance extends Instance {
|
|
52
|
+
setSizes(sizes: number[], animate?: boolean): void;
|
|
53
|
+
collapse(index: number): void;
|
|
54
|
+
expand(index: number): void;
|
|
55
|
+
toggle(index: number): void;
|
|
56
|
+
/** The first sizes again. */
|
|
57
|
+
reset(): void;
|
|
58
|
+
setOrientation(orientation: PanelOrientation): void;
|
|
59
|
+
readonly sizes: number[];
|
|
60
|
+
readonly collapsed: number[];
|
|
61
|
+
readonly orientation: PanelOrientation;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const CHEVRON = (path: string) => `<svg aria-hidden="true" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><path d="${path}"/></svg>`;
|
|
65
|
+
const ARROWS = { left: CHEVRON('M15 6l-6 6 6 6'), right: CHEVRON('M9 6l6 6-6 6'), up: CHEVRON('M6 15l6-6 6 6'), down: CHEVRON('M6 9l6 6 6-6') };
|
|
66
|
+
let sequence = 0;
|
|
67
|
+
|
|
68
|
+
const round = (n: number) => Math.round(n * 10) / 10;
|
|
69
|
+
|
|
70
|
+
/** Sizes that add up to 100: missing ones share what is left, then all scale to 100 (all missing: equal). */
|
|
71
|
+
export function normaliseSizes(sizes: Array<number | undefined>, count: number): number[] {
|
|
72
|
+
const given = Array.from({ length: count }, (_, i) => {
|
|
73
|
+
const size = sizes[i];
|
|
74
|
+
return typeof size === 'number' && Number.isFinite(size) && size >= 0 ? size : null;
|
|
75
|
+
});
|
|
76
|
+
const known = given.reduce<number>((sum, size) => sum + (size ?? 0), 0);
|
|
77
|
+
const missing = given.filter((size) => size === null).length;
|
|
78
|
+
const share = missing ? Math.max(0, 100 - known) / missing : 0;
|
|
79
|
+
const list = given.map((size) => size ?? share);
|
|
80
|
+
const total = list.reduce((sum, size) => sum + size, 0);
|
|
81
|
+
if (!total) return Array.from({ length: count }, () => 100 / count);
|
|
82
|
+
return list.map((size) => (size / total) * 100);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function createResizablePanel(root: HTMLElement | null, options: ResizablePanelOptions = {}): ResizablePanelInstance {
|
|
86
|
+
if (!root) {
|
|
87
|
+
return inert({ setSizes() {}, collapse() {}, expand() {}, toggle() {}, reset() {}, setOrientation() {}, sizes: [], collapsed: [], orientation: 'horizontal' as PanelOrientation });
|
|
88
|
+
}
|
|
89
|
+
registerMotion();
|
|
90
|
+
const scope = createScope();
|
|
91
|
+
const uid = `mk-rp-${++sequence}`;
|
|
92
|
+
const panels = [...root.children].filter((child): child is HTMLElement => child instanceof HTMLElement && !child.hasAttribute('data-mk-resize-handle'));
|
|
93
|
+
const count = panels.length;
|
|
94
|
+
let orientation: PanelOrientation = options.orientation === 'vertical' ? 'vertical' : 'horizontal';
|
|
95
|
+
const step = options.step && options.step > 0 ? options.step : 5;
|
|
96
|
+
const number = (value: string | undefined) => (value !== undefined && value !== '' && Number.isFinite(Number(value)) ? Number(value) : undefined);
|
|
97
|
+
const mins = panels.map((panel) => Math.max(0, number(panel.dataset.min) ?? options.min ?? 10));
|
|
98
|
+
const maxs = panels.map((panel) => Math.min(100, number(panel.dataset.max) ?? options.max ?? 100));
|
|
99
|
+
const collapsibleList = fromJson(options.collapsible) ?? [];
|
|
100
|
+
const canCollapse = panels.map((panel, i) => panel.hasAttribute('data-collapsible') || collapsibleList.includes(i));
|
|
101
|
+
const names = panels.map((panel, i) => options.labels?.[i] ?? panel.dataset.label ?? panel.getAttribute('aria-label') ?? `panel ${i + 1}`);
|
|
102
|
+
const initial = normaliseSizes(fromJson(options.sizes) ?? panels.map((panel) => number(panel.dataset.size)), count);
|
|
103
|
+
let sizes = [...initial];
|
|
104
|
+
const collapsed = new Set<number>((fromJson(options.collapsed) ?? []).filter((i) => canCollapse[i]));
|
|
105
|
+
/** What a panel had before it shut, to open back to. */
|
|
106
|
+
const before = new Map<number, number>();
|
|
107
|
+
const effects = new Set<gsap.core.Animation>();
|
|
108
|
+
const keep = (animation: gsap.core.Animation | null) => {
|
|
109
|
+
if (!animation) return;
|
|
110
|
+
effects.add(animation);
|
|
111
|
+
// Finished ones leave as new ones arrive: the animation's own onComplete stays untouched.
|
|
112
|
+
for (const effect of effects) if (effect.totalProgress() === 1) effects.delete(effect);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Remembered sizes win over the given ones.
|
|
116
|
+
const storage = (() => {
|
|
117
|
+
try {
|
|
118
|
+
return options.storageKey ? window.localStorage : null;
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
})();
|
|
123
|
+
if (storage && options.storageKey) {
|
|
124
|
+
try {
|
|
125
|
+
const saved = JSON.parse(storage.getItem(options.storageKey) ?? 'null') as { sizes?: number[]; collapsed?: number[] } | null;
|
|
126
|
+
if (saved?.sizes?.length === count) {
|
|
127
|
+
sizes = normaliseSizes(saved.sizes, count);
|
|
128
|
+
collapsed.clear();
|
|
129
|
+
for (const i of saved.collapsed ?? []) if (canCollapse[i]) collapsed.add(i);
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
// Unreadable: the given sizes stand.
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// A shut panel gives its room to its neighbour; it opens back to what it had.
|
|
136
|
+
for (const i of collapsed) {
|
|
137
|
+
if (sizes[i]! > 0) {
|
|
138
|
+
before.set(i, sizes[i]!);
|
|
139
|
+
const neighbour = i + 1 < count ? i + 1 : i - 1;
|
|
140
|
+
if (neighbour >= 0) sizes[neighbour]! += sizes[i]!;
|
|
141
|
+
sizes[i] = 0;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── The parts ────────────────────────────────────────────────────────────
|
|
146
|
+
scope.attr(root, 'data-mk-ready', '');
|
|
147
|
+
scope.attr(root, 'data-mk-orientation', orientation);
|
|
148
|
+
panels.forEach((panel, i) => {
|
|
149
|
+
scope.attr(panel, 'data-mk-panel', '');
|
|
150
|
+
if (!panel.id) scope.attr(panel, 'id', `${uid}-panel-${i}`);
|
|
151
|
+
scope.attr(panel, 'style', panel.getAttribute('style'));
|
|
152
|
+
});
|
|
153
|
+
const bubble = document.createElement('span');
|
|
154
|
+
bubble.dataset.mkResizeBubble = '';
|
|
155
|
+
bubble.setAttribute('aria-hidden', 'true');
|
|
156
|
+
bubble.hidden = true;
|
|
157
|
+
const handles = panels.slice(0, -1).map((panel, i) => {
|
|
158
|
+
const handle = document.createElement('div');
|
|
159
|
+
handle.dataset.mkResizeHandle = '';
|
|
160
|
+
handle.dataset.index = String(i);
|
|
161
|
+
handle.setAttribute('role', 'separator');
|
|
162
|
+
handle.tabIndex = 0;
|
|
163
|
+
handle.setAttribute('aria-controls', panel.id);
|
|
164
|
+
handle.setAttribute('aria-label', `Redimensionar ${names[i]} y ${names[i + 1]}`);
|
|
165
|
+
const grip = document.createElement('span');
|
|
166
|
+
grip.dataset.mkResizeGrip = '';
|
|
167
|
+
grip.setAttribute('aria-hidden', 'true');
|
|
168
|
+
handle.append(grip);
|
|
169
|
+
if (options.handleButtons) {
|
|
170
|
+
for (const [side, target] of [['start', i], ['end', i + 1]] as const) {
|
|
171
|
+
if (!canCollapse[target]) continue;
|
|
172
|
+
const button = document.createElement('button');
|
|
173
|
+
button.type = 'button';
|
|
174
|
+
button.tabIndex = -1;
|
|
175
|
+
button.dataset.mkResizeButton = side;
|
|
176
|
+
handle.append(button);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
panel.after(handle);
|
|
180
|
+
return handle;
|
|
181
|
+
});
|
|
182
|
+
root.append(bubble);
|
|
183
|
+
scope.add(() => {
|
|
184
|
+
for (const effect of effects) effect.kill();
|
|
185
|
+
effects.clear();
|
|
186
|
+
for (const handle of handles) handle.remove();
|
|
187
|
+
bubble.remove();
|
|
188
|
+
for (const panel of panels) panel.removeAttribute('data-mk-collapsed');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// ── Drawing the sizes ────────────────────────────────────────────────────
|
|
192
|
+
const horizontal = () => orientation === 'horizontal';
|
|
193
|
+
const apply = (next: number[]) => {
|
|
194
|
+
panels.forEach((panel, i) => {
|
|
195
|
+
const size = Math.max(0, next[i]!);
|
|
196
|
+
panel.style.flex = `${round(size * 100) / 100} 1 0px`;
|
|
197
|
+
panel.toggleAttribute('data-mk-collapsed', collapsed.has(i) && size < 0.5);
|
|
198
|
+
});
|
|
199
|
+
handles.forEach((handle, i) => {
|
|
200
|
+
// A separator pushed to an edge by a shut panel keeps its buttons inside.
|
|
201
|
+
const edge = i === count - 2 && collapsed.has(i + 1) && next[i + 1]! < 0.5 ? 'end' : i === 0 && collapsed.has(0) && next[0]! < 0.5 ? 'start' : null;
|
|
202
|
+
if (edge) handle.dataset.edge = edge;
|
|
203
|
+
else delete handle.dataset.edge;
|
|
204
|
+
handle.setAttribute('aria-valuenow', String(Math.round(Math.max(0, next[i]!))));
|
|
205
|
+
handle.setAttribute('aria-valuemin', String(canCollapse[i] ? 0 : Math.round(mins[i]!)));
|
|
206
|
+
handle.setAttribute('aria-valuemax', String(Math.round(Math.min(maxs[i]!, 100 - (canCollapse[i + 1] ? 0 : mins[i + 1]!)))));
|
|
207
|
+
handle.setAttribute('aria-valuetext', `${Math.round(Math.max(0, next[i]!))} %${collapsed.has(i) ? ', cerrado' : ''}`);
|
|
208
|
+
for (const button of handle.querySelectorAll<HTMLButtonElement>('[data-mk-resize-button]')) {
|
|
209
|
+
const start = button.dataset.mkResizeButton === 'start';
|
|
210
|
+
const index = start ? i : i + 1;
|
|
211
|
+
const shut = collapsed.has(index);
|
|
212
|
+
// The arrow points where the panel goes: toward its edge to shut it, away to open it.
|
|
213
|
+
button.innerHTML = horizontal() ? (start === !shut ? ARROWS.left : ARROWS.right) : start === !shut ? ARROWS.up : ARROWS.down;
|
|
214
|
+
const label = `${shut ? 'Abrir' : 'Cerrar'} ${names[index]}`;
|
|
215
|
+
button.setAttribute('aria-label', label);
|
|
216
|
+
button.title = label;
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
};
|
|
220
|
+
const paintOrientation = () => {
|
|
221
|
+
root.setAttribute('data-mk-orientation', orientation);
|
|
222
|
+
for (const handle of handles) handle.setAttribute('aria-orientation', horizontal() ? 'vertical' : 'horizontal');
|
|
223
|
+
};
|
|
224
|
+
paintOrientation();
|
|
225
|
+
apply(sizes);
|
|
226
|
+
|
|
227
|
+
const save = () => {
|
|
228
|
+
if (!storage || !options.storageKey) return;
|
|
229
|
+
try {
|
|
230
|
+
storage.setItem(options.storageKey, JSON.stringify({ sizes: sizes.map(round), collapsed: [...collapsed] }));
|
|
231
|
+
} catch {
|
|
232
|
+
// Full or blocked: not remembered.
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
let pendingNotify: (() => void) | null = null;
|
|
236
|
+
const notify = () => {
|
|
237
|
+
if (pendingNotify || !options.onResize) return;
|
|
238
|
+
pendingNotify = requestFrame(() => {
|
|
239
|
+
pendingNotify = null;
|
|
240
|
+
options.onResize?.(sizes.map(round));
|
|
241
|
+
});
|
|
242
|
+
};
|
|
243
|
+
scope.add(() => pendingNotify?.());
|
|
244
|
+
const settle = () => {
|
|
245
|
+
save();
|
|
246
|
+
options.onResizeEnd?.(sizes.map(round));
|
|
247
|
+
root.dispatchEvent(new CustomEvent('mk:resize', { bubbles: true, detail: { sizes: sizes.map(round) } }));
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
let tween: gsap.core.Tween | null = null;
|
|
251
|
+
/** Glide to `target` (a spring by default). */
|
|
252
|
+
const animateTo = (target: number[], { duration = 0.45, ease = EASE.signature, done }: { duration?: number; ease?: string; done?: () => void } = {}) => {
|
|
253
|
+
tween?.kill();
|
|
254
|
+
if (reduced()) {
|
|
255
|
+
sizes = target;
|
|
256
|
+
apply(sizes);
|
|
257
|
+
done?.();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const from = [...sizes];
|
|
261
|
+
const progress = { t: 0 };
|
|
262
|
+
tween = gsap.to(progress, {
|
|
263
|
+
t: 1,
|
|
264
|
+
duration,
|
|
265
|
+
ease,
|
|
266
|
+
onUpdate: () => {
|
|
267
|
+
sizes = from.map((size, i) => Math.max(0, size + (target[i]! - size) * progress.t));
|
|
268
|
+
apply(sizes);
|
|
269
|
+
notify();
|
|
270
|
+
},
|
|
271
|
+
onComplete: () => {
|
|
272
|
+
sizes = [...target];
|
|
273
|
+
apply(sizes);
|
|
274
|
+
tween = null;
|
|
275
|
+
done?.();
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
keep(tween);
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
// ── Limits ───────────────────────────────────────────────────────────────
|
|
282
|
+
const lowest = (i: number) => (collapsed.has(i) ? 0 : mins[i]!);
|
|
283
|
+
/** How far the separator `i` may move (in percent) from `start` sizes: [most negative, most positive]. */
|
|
284
|
+
const room = (i: number, start: number[]) => {
|
|
285
|
+
const a = start[i]!;
|
|
286
|
+
const b = start[i + 1]!;
|
|
287
|
+
const down = Math.max(lowest(i) - a, b - maxs[i + 1]!);
|
|
288
|
+
const up = Math.min(maxs[i]! - a, b - lowest(i + 1));
|
|
289
|
+
return [Math.min(0, down), Math.max(0, up)] as const;
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const setCollapsed = (index: number, shut: boolean, animate = true) => {
|
|
293
|
+
if (!canCollapse[index] || collapsed.has(index) === shut) return;
|
|
294
|
+
// The neighbour that gives or takes the room: the next panel, or the one before the last.
|
|
295
|
+
const neighbour = index + 1 < count ? index + 1 : index - 1;
|
|
296
|
+
const target = [...sizes];
|
|
297
|
+
if (shut) {
|
|
298
|
+
before.set(index, sizes[index]! > 0.5 ? sizes[index]! : before.get(index) ?? initial[index]!);
|
|
299
|
+
target[neighbour] = sizes[neighbour]! + sizes[index]!;
|
|
300
|
+
target[index] = 0;
|
|
301
|
+
collapsed.add(index);
|
|
302
|
+
} else {
|
|
303
|
+
collapsed.delete(index);
|
|
304
|
+
const wanted = Math.max(mins[index]!, before.get(index) ?? initial[index]!);
|
|
305
|
+
const give = Math.min(wanted, Math.max(0, sizes[neighbour]! - mins[neighbour]!));
|
|
306
|
+
target[index] = give;
|
|
307
|
+
target[neighbour] = sizes[neighbour]! - give;
|
|
308
|
+
}
|
|
309
|
+
const finish = () => {
|
|
310
|
+
settle();
|
|
311
|
+
options.onCollapse?.(index, shut);
|
|
312
|
+
root.dispatchEvent(new CustomEvent('mk:collapse', { bubbles: true, detail: { index, collapsed: shut } }));
|
|
313
|
+
};
|
|
314
|
+
status.textContent = `${names[index]} ${shut ? 'cerrado' : 'abierto'}`;
|
|
315
|
+
if (animate) animateTo(target, { duration: 0.3, ease: EASE.swatch, done: finish });
|
|
316
|
+
else {
|
|
317
|
+
sizes = target;
|
|
318
|
+
apply(sizes);
|
|
319
|
+
finish();
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const status = document.createElement('span');
|
|
324
|
+
status.className = 'mk-sr-only';
|
|
325
|
+
status.setAttribute('role', 'status');
|
|
326
|
+
root.append(status);
|
|
327
|
+
scope.add(() => status.remove());
|
|
328
|
+
|
|
329
|
+
// ── Dragging ─────────────────────────────────────────────────────────────
|
|
330
|
+
interface Drag {
|
|
331
|
+
index: number;
|
|
332
|
+
pointerId: number;
|
|
333
|
+
start: number[];
|
|
334
|
+
origin: number;
|
|
335
|
+
/** Pixels the panels share (the root without its separators). */
|
|
336
|
+
space: number;
|
|
337
|
+
moved: boolean;
|
|
338
|
+
}
|
|
339
|
+
let drag: Drag | null = null;
|
|
340
|
+
const spaceOf = () => {
|
|
341
|
+
const box = root.getBoundingClientRect();
|
|
342
|
+
const handlesSize = handles.reduce((sum, handle) => sum + (horizontal() ? handle.offsetWidth : handle.offsetHeight), 0);
|
|
343
|
+
return Math.max(1, (horizontal() ? box.width : box.height) - handlesSize);
|
|
344
|
+
};
|
|
345
|
+
const showBubble = (i: number, at: number) => {
|
|
346
|
+
bubble.hidden = false;
|
|
347
|
+
bubble.textContent = `${Math.round(sizes[i]!)} % · ${Math.round(sizes[i + 1]!)} %`;
|
|
348
|
+
const handle = handles[i]!;
|
|
349
|
+
const box = handle.getBoundingClientRect();
|
|
350
|
+
const rootBox = root.getBoundingClientRect();
|
|
351
|
+
if (horizontal()) {
|
|
352
|
+
bubble.style.left = `${box.left - rootBox.left + box.width / 2}px`;
|
|
353
|
+
bubble.style.top = `${Math.max(16, Math.min(rootBox.height - 16, at - rootBox.top))}px`;
|
|
354
|
+
} else {
|
|
355
|
+
bubble.style.top = `${box.top - rootBox.top + box.height / 2}px`;
|
|
356
|
+
bubble.style.left = `${Math.max(40, Math.min(rootBox.width - 40, at - rootBox.left))}px`;
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
const hideBubble = () => {
|
|
360
|
+
if (bubble.hidden) return;
|
|
361
|
+
if (reduced()) {
|
|
362
|
+
bubble.hidden = true;
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
// Opacity only: the bubble is centred with CSS translate, which transforms would reset.
|
|
366
|
+
keep(gsap.to(bubble, { opacity: 0, duration: 0.18, ease: 'power1.in', onComplete: () => {
|
|
367
|
+
bubble.hidden = true;
|
|
368
|
+
gsap.set(bubble, { clearProps: 'opacity' });
|
|
369
|
+
} }));
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const move = (event: PointerEvent) => {
|
|
373
|
+
if (!drag || event.pointerId !== drag.pointerId) return;
|
|
374
|
+
event.preventDefault();
|
|
375
|
+
const i = drag.index;
|
|
376
|
+
const position = horizontal() ? event.clientX : event.clientY;
|
|
377
|
+
const wanted = ((position - drag.origin) / drag.space) * 100;
|
|
378
|
+
if (!drag.moved && Math.abs(wanted) > 0.2) {
|
|
379
|
+
drag.moved = true;
|
|
380
|
+
handles[i]!.setAttribute('data-mk-dragging', '');
|
|
381
|
+
if (!reduced()) keep(gsap.fromTo(bubble, { opacity: 0 }, { opacity: 1, duration: 0.2, ease: 'power1.out', clearProps: 'opacity' }));
|
|
382
|
+
}
|
|
383
|
+
// A collapsible panel dragged well under its minimum snaps shut; dragged back out, it opens.
|
|
384
|
+
for (const [index, sign] of [[i, 1], [i + 1, -1]] as const) {
|
|
385
|
+
if (!canCollapse[index]) continue;
|
|
386
|
+
const size = drag.start[index]! + sign * wanted;
|
|
387
|
+
const threshold = mins[index]! / 2;
|
|
388
|
+
if (!collapsed.has(index) && size < threshold) {
|
|
389
|
+
collapsed.add(index);
|
|
390
|
+
before.set(index, drag.start[index]! > 0.5 ? drag.start[index]! : before.get(index) ?? initial[index]!);
|
|
391
|
+
const target = [...drag.start];
|
|
392
|
+
target[index] = 0;
|
|
393
|
+
target[index === i ? i + 1 : i] = drag.start[i]! + drag.start[i + 1]!;
|
|
394
|
+
// If the drag is over when it lands, it settles then.
|
|
395
|
+
animateTo(target, { duration: 0.3, ease: EASE.swatch, done: () => (drag ? undefined : settle()) });
|
|
396
|
+
status.textContent = `${names[index]} cerrado`;
|
|
397
|
+
options.onCollapse?.(index, true);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (collapsed.has(index)) {
|
|
401
|
+
// Still shut while the pointer stays near it.
|
|
402
|
+
if (size <= threshold) return;
|
|
403
|
+
collapsed.delete(index);
|
|
404
|
+
status.textContent = `${names[index]} abierto`;
|
|
405
|
+
options.onCollapse?.(index, false);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (tween) return;
|
|
409
|
+
const [down, up] = room(i, drag.start);
|
|
410
|
+
// Past a limit the panel gives a quarter of the way (rubber band).
|
|
411
|
+
const delta = wanted < down ? down + (wanted - down) / 4 : wanted > up ? up + (wanted - up) / 4 : wanted;
|
|
412
|
+
const next = [...drag.start];
|
|
413
|
+
next[i] = Math.max(0, drag.start[i]! + delta);
|
|
414
|
+
next[i + 1] = Math.max(0, drag.start[i + 1]! - delta);
|
|
415
|
+
sizes = next;
|
|
416
|
+
apply(sizes);
|
|
417
|
+
showBubble(i, horizontal() ? event.clientY : event.clientX);
|
|
418
|
+
notify();
|
|
419
|
+
};
|
|
420
|
+
const end = (event: PointerEvent) => {
|
|
421
|
+
if (!drag || event.pointerId !== drag.pointerId) return;
|
|
422
|
+
const { index } = drag;
|
|
423
|
+
const moved = drag.moved;
|
|
424
|
+
drag = null;
|
|
425
|
+
window.removeEventListener('pointermove', move);
|
|
426
|
+
window.removeEventListener('pointerup', end);
|
|
427
|
+
window.removeEventListener('pointercancel', end);
|
|
428
|
+
handles[index]!.removeAttribute('data-mk-dragging');
|
|
429
|
+
root.removeAttribute('data-mk-resizing');
|
|
430
|
+
hideBubble();
|
|
431
|
+
// A snap on its way settles when it lands.
|
|
432
|
+
if (!moved || tween) return;
|
|
433
|
+
// Back inside the limits with a spring, if the drag went past them.
|
|
434
|
+
const clamped = [...sizes];
|
|
435
|
+
for (const i of [index, index + 1]) {
|
|
436
|
+
if (collapsed.has(i)) continue;
|
|
437
|
+
const fixed = Math.min(maxs[i]!, Math.max(mins[i]!, clamped[i]!));
|
|
438
|
+
const other = i === index ? index + 1 : index;
|
|
439
|
+
clamped[other] = clamped[other]! + clamped[i]! - fixed;
|
|
440
|
+
clamped[i] = fixed;
|
|
441
|
+
}
|
|
442
|
+
if (clamped.some((size, i) => Math.abs(size - sizes[i]!) > 0.05)) animateTo(clamped, { duration: 0.45, ease: EASE.swatch, done: settle });
|
|
443
|
+
else settle();
|
|
444
|
+
// The grip settles with a little pop.
|
|
445
|
+
const grip = handles[index]!.querySelector('[data-mk-resize-grip]');
|
|
446
|
+
if (grip && !reduced()) keep(gsap.fromTo(grip, { scale: 1.25 }, { scale: 1, duration: 0.4, ease: EASE.pop, clearProps: 'transform' }));
|
|
447
|
+
};
|
|
448
|
+
scope.add(() => {
|
|
449
|
+
window.removeEventListener('pointermove', move);
|
|
450
|
+
window.removeEventListener('pointerup', end);
|
|
451
|
+
window.removeEventListener('pointercancel', end);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
handles.forEach((handle, i) => {
|
|
455
|
+
scope.on(handle, 'pointerdown', (event) => {
|
|
456
|
+
const pointer = event as PointerEvent;
|
|
457
|
+
if (pointer.button !== 0 || (pointer.target as Element).closest('[data-mk-resize-button]')) return;
|
|
458
|
+
pointer.preventDefault();
|
|
459
|
+
handle.focus({ preventScroll: true });
|
|
460
|
+
tween?.kill();
|
|
461
|
+
tween = null;
|
|
462
|
+
drag = { index: i, pointerId: pointer.pointerId, start: [...sizes], origin: horizontal() ? pointer.clientX : pointer.clientY, space: spaceOf(), moved: false };
|
|
463
|
+
root.setAttribute('data-mk-resizing', '');
|
|
464
|
+
window.addEventListener('pointermove', move, { passive: false });
|
|
465
|
+
window.addEventListener('pointerup', end);
|
|
466
|
+
window.addEventListener('pointercancel', end);
|
|
467
|
+
});
|
|
468
|
+
scope.on(handle, 'dblclick', (event) => {
|
|
469
|
+
if ((event.target as Element).closest('[data-mk-resize-button]')) return;
|
|
470
|
+
collapsed.clear();
|
|
471
|
+
status.textContent = 'Tamaños iniciales';
|
|
472
|
+
animateTo([...initial], { done: settle });
|
|
473
|
+
});
|
|
474
|
+
scope.on(handle, 'click', (event) => {
|
|
475
|
+
const button = (event.target as Element).closest<HTMLElement>('[data-mk-resize-button]');
|
|
476
|
+
if (!button) return;
|
|
477
|
+
const index = button.dataset.mkResizeButton === 'start' ? i : i + 1;
|
|
478
|
+
setCollapsed(index, !collapsed.has(index));
|
|
479
|
+
});
|
|
480
|
+
scope.on(handle, 'keydown', (event) => {
|
|
481
|
+
const key = event as KeyboardEvent;
|
|
482
|
+
const less = horizontal() ? 'ArrowLeft' : 'ArrowUp';
|
|
483
|
+
const more = horizontal() ? 'ArrowRight' : 'ArrowDown';
|
|
484
|
+
let delta: number | null = null;
|
|
485
|
+
const [down, up] = room(i, sizes);
|
|
486
|
+
if (key.key === less) delta = -step * (key.shiftKey ? 4 : 1);
|
|
487
|
+
else if (key.key === more) delta = step * (key.shiftKey ? 4 : 1);
|
|
488
|
+
else if (key.key === 'Home') delta = down;
|
|
489
|
+
else if (key.key === 'End') delta = up;
|
|
490
|
+
else if (key.key === 'Enter') {
|
|
491
|
+
key.preventDefault();
|
|
492
|
+
// The panel before it shuts or opens (or the one after, when only that one can).
|
|
493
|
+
const index = canCollapse[i] ? i : canCollapse[i + 1] ? i + 1 : -1;
|
|
494
|
+
if (index >= 0) setCollapsed(index, !collapsed.has(index));
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
if (delta === null) return;
|
|
498
|
+
key.preventDefault();
|
|
499
|
+
const moveBy = Math.max(down, Math.min(up, delta));
|
|
500
|
+
if (Math.abs(moveBy) < 0.01) {
|
|
501
|
+
// At the limit: a nudge says so.
|
|
502
|
+
if (!reduced()) keep(gsap.fromTo(handle.querySelector('[data-mk-resize-grip]'), { [horizontal() ? 'x' : 'y']: Math.sign(delta) * 3 }, { [horizontal() ? 'x' : 'y']: 0, duration: 0.4, ease: EASE.pop, clearProps: 'transform' }));
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
const target = [...sizes];
|
|
506
|
+
target[i] = sizes[i]! + moveBy;
|
|
507
|
+
target[i + 1] = sizes[i + 1]! - moveBy;
|
|
508
|
+
animateTo(target, { duration: 0.2, ease: 'power2.out', done: settle });
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
return {
|
|
513
|
+
setSizes(next, animate = true) {
|
|
514
|
+
if (scope.disposed) return;
|
|
515
|
+
const target = normaliseSizes(next, count);
|
|
516
|
+
collapsed.clear();
|
|
517
|
+
target.forEach((size, i) => {
|
|
518
|
+
if (size < 0.5 && canCollapse[i]) collapsed.add(i);
|
|
519
|
+
});
|
|
520
|
+
if (animate) animateTo(target, { done: settle });
|
|
521
|
+
else {
|
|
522
|
+
tween?.kill();
|
|
523
|
+
sizes = target;
|
|
524
|
+
apply(sizes);
|
|
525
|
+
}
|
|
526
|
+
},
|
|
527
|
+
collapse: (index) => setCollapsed(index, true),
|
|
528
|
+
expand: (index) => setCollapsed(index, false),
|
|
529
|
+
toggle: (index) => setCollapsed(index, !collapsed.has(index)),
|
|
530
|
+
reset() {
|
|
531
|
+
collapsed.clear();
|
|
532
|
+
animateTo([...initial], { done: settle });
|
|
533
|
+
},
|
|
534
|
+
setOrientation(next) {
|
|
535
|
+
if (next === orientation || scope.disposed) return;
|
|
536
|
+
orientation = next;
|
|
537
|
+
paintOrientation();
|
|
538
|
+
apply(sizes);
|
|
539
|
+
},
|
|
540
|
+
get sizes() {
|
|
541
|
+
return sizes.map(round);
|
|
542
|
+
},
|
|
543
|
+
get collapsed() {
|
|
544
|
+
return [...collapsed];
|
|
545
|
+
},
|
|
546
|
+
get orientation() {
|
|
547
|
+
return orientation;
|
|
548
|
+
},
|
|
549
|
+
destroy: () => scope.dispose(),
|
|
550
|
+
};
|
|
551
|
+
}
|