diff --git a/.changeset/two-hard-ones.md b/.changeset/two-hard-ones.md new file mode 100644 index 0000000..0ffa937 --- /dev/null +++ b/.changeset/two-hard-ones.md @@ -0,0 +1,54 @@ +--- +'sve-ui': minor +--- + +Two new components: `Carousel` and `Resizable`. + +Both hand-built — Bits UI ships neither — and neither adds a dependency. These are +the Tier 2 gaps from the same catalog diff that produced `Kbd`, `ButtonGroup`, +`Empty` and `InputGroup`: harder than those four, because both have real behaviour +and a real accessibility contract rather than just a styling problem. + +**`Carousel`** is built on CSS scroll-snap rather than transforms, which is what +buys touch swiping, trackpad flicks, momentum and the browser's own arrow-key +scrolling for free. Position is _read_ from the scroll container on every scroll +and never remembered: a swipe, a flick and the arrow keys are all drivers a +component cannot intercept, and a stored index would disagree with the screen +after any of them — which would make the "3 of 5" slide label a lie. + +Two things it deliberately does not do: + +- **It does not loop.** An infinite track makes "slide 3 of 5" meaningless and + removes the only honest position affordance a carousel has: a Previous or Next + that goes flat at the boundary. +- **It does not auto-rotate.** That is only acceptable with a visible pause + control, and stopping on hover, and stopping on focus, and honouring + `prefers-reduced-motion` — a chain that gets half-built more often than not. + `useCarousel()` exposes `next`, `prev` and `goTo` for anyone who takes on the + obligation that comes with it. + +Previous and Next are disabled from the measured scroll extent, not the slide +index, so they stay honest when several slides are visible at once. + +**`Resizable`** implements the APG window-splitter pattern, which is mostly a +keyboard contract — and that is the half every hand-rolled divider skips. The +handle is a focusable `separator` with `aria-orientation` and live +`aria-valuenow`; arrow keys move it, Home and End collapse and expand. `label` is +required, because "splitter, 40" names a number and not the thing it sizes. + +A drag moves one boundary and touches exactly two panes, clamped against _both_ +of their limits before anything is applied. Clamping only the pane being grown is +what lets its neighbour shrink past its own minimum, and distributing a drag +across the whole group is what makes resizing a sidebar quietly reflow the far +side of a layout. It does not persist sizes: where that belongs is an application +decision. + +Two smaller notes: + +- The drag uses `setPointerCapture`, so it survives the pointer leaving a + hairline target — which it does immediately — and the grabbable area is grown to + 12px by a pseudo-element, so the divider can stay 1px wide without being + unreasonable to hit. +- `Resizable.Pane` registers its `min`/`max` as getters. Registration happens once + at init because the handle beside it needs an index and id on first render, and + snapshotting the numbers there would freeze limits that depend on state. diff --git a/apps/docs/render-baseline.json b/apps/docs/render-baseline.json index 62ee540..5f3c072 100644 --- a/apps/docs/render-baseline.json +++ b/apps/docs/render-baseline.json @@ -1,263 +1,271 @@ { "components.html": { - "elements": 709, - "hash": "332fdabcd63893d7" + "elements": 729, + "hash": "36b67426a9bb46c1" }, "components/accordion.html": { - "elements": 341, - "hash": "3b213d024fa1b54d" + "elements": 345, + "hash": "5a9ea1d304893cb5" }, "components/alert-dialog.html": { - "elements": 683, - "hash": "88eec42746d68d7a" + "elements": 687, + "hash": "2664831fb134d30b" }, "components/alert.html": { - "elements": 369, - "hash": "3d7c4b7c77c02879" + "elements": 373, + "hash": "76201e5d23d108fd" }, "components/aspect-ratio.html": { - "elements": 307, - "hash": "76f476c4d2a03099" + "elements": 311, + "hash": "932daa2c2d566523" }, "components/avatar.html": { - "elements": 489, - "hash": "27cd1e8df09b6861" + "elements": 493, + "hash": "d2708db2209a3de6" }, "components/badge.html": { - "elements": 317, - "hash": "b736b3bc1b0ec15e" + "elements": 321, + "hash": "aeb5262b57e3b2b5" }, "components/breadcrumb.html": { - "elements": 342, - "hash": "cc55c699229b3d84" + "elements": 346, + "hash": "f07bdd543c55641e" }, "components/busy.html": { - "elements": 301, - "hash": "fc3ac55258547f59" + "elements": 305, + "hash": "fceef033ba25a783" }, "components/button-group.html": { - "elements": 312, - "hash": "17758f8d65aadafb" + "elements": 316, + "hash": "4264ea4e663c2977" }, "components/button.html": { - "elements": 438, - "hash": "4b93f780128bfd65" + "elements": 442, + "hash": "0644e7f5120bf95e" }, "components/calendar.html": { - "elements": 509, - "hash": "11ded231214159eb" + "elements": 513, + "hash": "a23166207adbfbdc" }, "components/card.html": { - "elements": 392, - "hash": "8cbbc17dc7107aed" + "elements": 396, + "hash": "113f0697869fa6cd" + }, + "components/carousel.html": { + "elements": 514, + "hash": "c62d6dc9a7c3aa03" }, "components/checkbox.html": { - "elements": 407, - "hash": "0827340163b13bf1" + "elements": 411, + "hash": "c879e63193060b76" }, "components/code.html": { - "elements": 303, - "hash": "9a0e3ce40e807695" + "elements": 307, + "hash": "d0ff5ada3d9e758d" }, "components/collapsible.html": { - "elements": 447, - "hash": "6199acac5fb8fc93" + "elements": 451, + "hash": "5893b016acb5a9aa" }, "components/combobox.html": { - "elements": 819, - "hash": "7a76447e78ec509d" + "elements": 823, + "hash": "65c24ac2061b560b" }, "components/command.html": { - "elements": 717, - "hash": "af29c786620e8073" + "elements": 721, + "hash": "9f0895be74af63df" }, "components/context-menu.html": { - "elements": 732, - "hash": "d2c96b1bb8c01bd4" + "elements": 736, + "hash": "afb533bcc667ed1f" }, "components/date-field.html": { - "elements": 385, - "hash": "27997f87fe5df75f" + "elements": 389, + "hash": "1e3377f2751210e3" }, "components/date-picker.html": { - "elements": 416, - "hash": "c4fbda49e626da17" + "elements": 420, + "hash": "15dea0d69efe586b" }, "components/date-range-field.html": { - "elements": 383, - "hash": "c3fc63a830ad1be3" + "elements": 387, + "hash": "5519e79331036d02" }, "components/date-range-picker.html": { - "elements": 443, - "hash": "2a2f3fba1dcf17f9" + "elements": 447, + "hash": "92d3b4ff196f2fd7" }, "components/dialog.html": { - "elements": 627, - "hash": "e6469db080360afb" + "elements": 631, + "hash": "6635ef41e1f13284" }, "components/dropdown-menu.html": { - "elements": 705, - "hash": "c88f7ac922bfd474" + "elements": 709, + "hash": "56102a5550179597" }, "components/empty.html": { - "elements": 417, - "hash": "280223d0ec0d2fe0" + "elements": 421, + "hash": "a4fadb0001129839" }, "components/field.html": { - "elements": 431, - "hash": "83a4ff847457a1cb" + "elements": 435, + "hash": "9837d270816701b5" }, "components/flex.html": { - "elements": 333, - "hash": "7cebd3b7172cde30" + "elements": 337, + "hash": "e755c061477f16ae" }, "components/heading.html": { - "elements": 330, - "hash": "dfa0086953d9b4e9" + "elements": 334, + "hash": "45051e3917b8436d" }, "components/input-group.html": { - "elements": 370, - "hash": "38d087c48d9fb275" + "elements": 374, + "hash": "77b6b5e19493a322" }, "components/input.html": { - "elements": 313, - "hash": "8dd845a7898b0a59" + "elements": 317, + "hash": "5d9a7108b85254f8" }, "components/kbd.html": { - "elements": 332, - "hash": "7f12ae66f5a68839" + "elements": 336, + "hash": "8f037d302ad624ad" }, "components/label.html": { - "elements": 348, - "hash": "0de1812f76cc4bee" + "elements": 352, + "hash": "128c01807126027c" }, "components/link-preview.html": { - "elements": 580, - "hash": "72377331989b1642" + "elements": 584, + "hash": "ab67fa57a91d3055" }, "components/menubar.html": { - "elements": 811, - "hash": "dd5672a56e1fa71d" + "elements": 815, + "hash": "89ab6f2e293a48ff" }, "components/meter.html": { - "elements": 350, - "hash": "79c622d8bdc9db92" + "elements": 354, + "hash": "cb9b7ec323b35d96" }, "components/navigation-menu.html": { - "elements": 548, - "hash": "9957b74eb05fc990" + "elements": 552, + "hash": "3b0546569efde694" }, "components/pagination.html": { - "elements": 418, - "hash": "c650108df46f3b2b" + "elements": 422, + "hash": "706105b0a78f1f1a" }, "components/pin-input.html": { - "elements": 457, - "hash": "5ab1e6221908a37b" + "elements": 461, + "hash": "6d59d5db368619c5" }, "components/popover.html": { - "elements": 665, - "hash": "8945d3a4e064c0e3" + "elements": 669, + "hash": "a86b6caecbf64709" }, "components/progress.html": { - "elements": 378, - "hash": "3dfb861019c1210c" + "elements": 382, + "hash": "f63f81d56f580313" }, "components/radio-group.html": { - "elements": 463, - "hash": "2c163d91543392df" + "elements": 467, + "hash": "ad145d1e7ec20f63" }, "components/range-calendar.html": { - "elements": 479, - "hash": "746ebde7c7ae7e15" + "elements": 483, + "hash": "d8ae6fc3005aa3ea" }, "components/rating-group.html": { - "elements": 500, - "hash": "042ec265187ac9b0" + "elements": 504, + "hash": "dabaea6868cab1c5" + }, + "components/resizable.html": { + "elements": 419, + "hash": "26fede51e3acab12" }, "components/scroll-area.html": { - "elements": 412, - "hash": "6a2045fdc645b839" + "elements": 416, + "hash": "71e6a1dc607953d5" }, "components/select.html": { - "elements": 795, - "hash": "2d5644d16ee9dd79" + "elements": 799, + "hash": "465c0b890354df7a" }, "components/separator.html": { - "elements": 330, - "hash": "32791f98c412e3d3" + "elements": 334, + "hash": "c95761bae4ee787c" }, "components/sheet.html": { - "elements": 448, - "hash": "9415d71a1c667c63" + "elements": 452, + "hash": "d22bcb4e9a68af8f" }, "components/sidebar.html": { - "elements": 501, - "hash": "1e4436b1e3c09107" + "elements": 505, + "hash": "e1aa8cbf0d33d067" }, "components/skeleton.html": { - "elements": 335, - "hash": "a085451738a9ba79" + "elements": 339, + "hash": "63f2bc6c6f8adb6e" }, "components/slider.html": { - "elements": 340, - "hash": "66706a641f377763" + "elements": 344, + "hash": "5c36cfab881f233e" }, "components/spinner.html": { - "elements": 306, - "hash": "23c18b319909ad07" + "elements": 310, + "hash": "c470aa1817d0c923" }, "components/stack.html": { - "elements": 315, - "hash": "5b11a52f0fdae935" + "elements": 319, + "hash": "87a5938c8de21b4d" }, "components/switch.html": { - "elements": 372, - "hash": "cb8443522f91d1d6" + "elements": 376, + "hash": "5a022147fb6d34a7" }, "components/table.html": { - "elements": 548, - "hash": "04ca4a70fb1d5fd1" + "elements": 552, + "hash": "55525f39c54386a0" }, "components/tabs.html": { - "elements": 508, - "hash": "f427929f4b8274b6" + "elements": 512, + "hash": "62d8b219b6e48b40" }, "components/text.html": { - "elements": 345, - "hash": "093785679a970f11" + "elements": 349, + "hash": "218ce36bc53b045b" }, "components/textarea.html": { - "elements": 382, - "hash": "24ec43b0d17c78f2" + "elements": 386, + "hash": "37c873cde3413739" }, "components/time-field.html": { - "elements": 386, - "hash": "469926979bd431c4" + "elements": 390, + "hash": "c86abfe244d6e71a" }, "components/time-range-field.html": { - "elements": 393, - "hash": "2f88b9815d66cd45" + "elements": 397, + "hash": "f00fad291afb9add" }, "components/toast.html": { - "elements": 363, - "hash": "751cf31fc7d02e1e" + "elements": 367, + "hash": "289e3a6914f89944" }, "components/toggle-group.html": { - "elements": 445, - "hash": "488538015c0176f2" + "elements": 449, + "hash": "aca3bce4717e68d5" }, "components/toggle.html": { - "elements": 366, - "hash": "4f6f300d14c10551" + "elements": 370, + "hash": "235fcda2646299c6" }, "components/toolbar.html": { - "elements": 515, - "hash": "cdfd884d034c73d0" + "elements": 519, + "hash": "8ef49cc4fef061e6" }, "components/tooltip.html": { - "elements": 714, - "hash": "4bffa39fedfe58bb" + "elements": 718, + "hash": "11788922ab296efc" }, "docs.html": { "elements": 96, diff --git a/apps/docs/src/lib/docs/generated/props.json b/apps/docs/src/lib/docs/generated/props.json index 96953d8..9e5979a 100644 --- a/apps/docs/src/lib/docs/generated/props.json +++ b/apps/docs/src/lib/docs/generated/props.json @@ -1759,6 +1759,170 @@ ], "spreads": "Omit, 'class'>" }, + "CarouselIndicator": { + "props": [ + { + "prop": "index", + "type": "number", + "required": true, + "description": "Zero-based index of the slide this dot scrolls to." + }, + { + "prop": "label", + "type": "string", + "required": false, + "description": "Accessible name. Defaults to \"Go to slide N\"." + }, + { + "prop": "class", + "type": "string", + "required": false, + "description": "Extra classes merged onto the button." + } + ], + "spreads": "Omit" + }, + "CarouselIndicators": { + "props": [ + { + "prop": "label", + "type": "string", + "required": false, + "default": "'Choose slide'", + "description": "Accessible name for the set of dots. Defaults to \"Choose slide\"." + }, + { + "prop": "class", + "type": "string", + "required": false, + "description": "Extra classes merged onto the container." + }, + { + "prop": "children", + "type": "Snippet", + "required": true + } + ], + "spreads": "Omit, 'class' | 'aria-label'>" + }, + "CarouselNext": { + "props": [ + { + "prop": "label", + "type": "string", + "required": false, + "default": "'Next slide'", + "description": "Accessible name. Defaults to \"Previous slide\"." + }, + { + "prop": "class", + "type": "string", + "required": false, + "description": "Extra classes merged onto the button." + }, + { + "prop": "children", + "type": "Snippet", + "required": false + } + ], + "spreads": "Omit" + }, + "CarouselPrevious": { + "props": [ + { + "prop": "label", + "type": "string", + "required": false, + "default": "'Previous slide'", + "description": "Accessible name. Defaults to \"Previous slide\"." + }, + { + "prop": "class", + "type": "string", + "required": false, + "description": "Extra classes merged onto the button." + }, + { + "prop": "children", + "type": "Snippet", + "required": false + } + ], + "spreads": "Omit" + }, + "CarouselRoot": { + "props": [ + { + "prop": "orientation", + "type": "Orientation", + "required": false, + "default": "'horizontal'" + }, + { + "prop": "label", + "type": "string", + "required": false, + "description": "The carousel's accessible name, e.g. \"Product photos\". Required, and it is not decoration. The root is a labelled region so a screen reader user can find and skip the whole thing in one move; an unnamed region is an obstacle they have to read through to identify." + }, + { + "prop": "labelledby", + "type": "string", + "required": false, + "description": "Id of an existing visible heading, as an alternative to `label`." + }, + { + "prop": "class", + "type": "string", + "required": false, + "description": "Extra classes merged onto the root element." + }, + { + "prop": "children", + "type": "Snippet", + "required": true + } + ], + "spreads": "Omit, 'class' | 'aria-label'>" + }, + "CarouselSlide": { + "props": [ + { + "prop": "label", + "type": "string", + "required": false, + "description": "Overrides the generated \"N of M\" name. Pass something meaningful when the slide has an identity of its own — a product name beats a position. The default exists because a slide with no name at all is announced as \"group\", which tells the user nothing about where they are." + }, + { + "prop": "class", + "type": "string", + "required": false, + "description": "Extra classes merged onto the slide." + }, + { + "prop": "children", + "type": "Snippet", + "required": true + } + ], + "spreads": "Omit, 'class' | 'aria-label'>" + }, + "CarouselViewport": { + "props": [ + { + "prop": "class", + "type": "string", + "required": false, + "description": "Extra classes merged onto the scroll container." + }, + { + "prop": "children", + "type": "Snippet", + "required": true + } + ], + "spreads": "Omit, 'class'>" + }, "CheckboxRoot": { "props": [ { @@ -7290,6 +7454,88 @@ ], "spreads": "Omit" }, + "ResizableGroup": { + "props": [ + { + "prop": "direction", + "type": "Direction", + "required": false, + "default": "'horizontal'" + }, + { + "prop": "class", + "type": "string", + "required": false, + "description": "Extra classes merged onto the group." + }, + { + "prop": "children", + "type": "Snippet", + "required": true + } + ], + "spreads": "Omit, 'class'>" + }, + "ResizableHandle": { + "props": [ + { + "prop": "index", + "type": "number", + "required": true, + "description": "Index of the pane on the leading side of this handle — the one whose size `aria-valuenow` reports." + }, + { + "prop": "label", + "type": "string", + "required": false, + "description": "The handle's accessible name, e.g. \"Resize sidebar\". Required. A `separator` with a tabindex is a control, and a control that announces only \"splitter, 40\" tells a keyboard user a number without saying what it sizes." + }, + { + "prop": "step", + "type": "number", + "required": false, + "default": "5", + "description": "Percent moved per arrow-key press." + }, + { + "prop": "class", + "type": "string", + "required": false, + "description": "Extra classes merged onto the handle." + } + ], + "spreads": "Omit, 'class' | 'aria-label'>" + }, + "ResizablePane": { + "props": [ + { + "prop": "min", + "type": "number", + "required": false, + "default": "10", + "description": "Smallest share of the group this pane accepts, in percent." + }, + { + "prop": "max", + "type": "number", + "required": false, + "default": "90", + "description": "Largest share, in percent." + }, + { + "prop": "class", + "type": "string", + "required": false, + "description": "Extra classes merged onto the pane." + }, + { + "prop": "children", + "type": "Snippet", + "required": true + } + ], + "spreads": "Omit, 'class'>" + }, "ScrollAreaRoot": { "props": [ { diff --git a/apps/docs/src/lib/docs/registry.ts b/apps/docs/src/lib/docs/registry.ts index 2825166..4a5ac0b 100644 --- a/apps/docs/src/lib/docs/registry.ts +++ b/apps/docs/src/lib/docs/registry.ts @@ -28,6 +28,13 @@ export const componentGroups: ComponentGroup[] = [ { slug: 'avatar', name: 'Avatar', blurb: 'Image with graceful fallback.', ready: true }, { slug: 'badge', name: 'Badge', blurb: 'Compact status and count labels.', ready: true }, { slug: 'card', name: 'Card', blurb: 'Surface container for grouped content.', ready: true }, + { + slug: 'carousel', + name: 'Carousel', + blurb: 'A scrolling track of slides with controls.', + status: 'new', + ready: true + }, { slug: 'heading', name: 'Heading', blurb: 'Semantic, sized display titles.', ready: true }, { slug: 'text', name: 'Text', blurb: 'Body copy with size and tone.', ready: true }, { @@ -379,6 +386,13 @@ export const componentGroups: ComponentGroup[] = [ { label: 'Layout', items: [ + { + slug: 'resizable', + name: 'Resizable', + blurb: 'Panes with a draggable, keyboard-operable divider.', + status: 'new', + ready: true + }, { slug: 'stack', name: 'Stack', diff --git a/apps/docs/src/routes/components/carousel/+page.svelte b/apps/docs/src/routes/components/carousel/+page.svelte new file mode 100644 index 0000000..3865f0b --- /dev/null +++ b/apps/docs/src/routes/components/carousel/+page.svelte @@ -0,0 +1,234 @@ + + + +
+

Usage

+

+ Built on CSS scroll-snap, not transforms. Touch swiping, trackpad flicks, momentum and the + browser's own arrow-key scrolling all come from the platform — there is no gesture code here, + and no new dependency. +

+

+ label is required: the root is a labelled region, so a screen reader user + can find the whole carousel and skip past it in one move. Each slide is announced as "slide, 3 of + 5" unless you give it a better name. +

+ +
+ + + {#each slides as slide (slide)} + + + + {slide} + + + + {/each} + +
+ + + {#each indices as i (i)} + + {/each} + + +
+
+
+
+
+ +
+

More than one at a time

+

+ Set --sve-carousel-slide-size on the root. The controls are disabled from + the measured scroll extent rather than from the slide index, which matters here: with three slides + visible the last index is reachable while there is still track left to scroll, and a control disabled + then would be lying. +

+ +
+ + + {#each slides as slide (slide)} + + + {slide} + + + {/each} + +
+ + +
+
+
+
+
+ +
+

Vertical

+

+ The arrow glyphs and the snap axis follow orientation. +

+ +
+ + + {#each slides as slide (slide)} + + + {slide} + + + {/each} + +
+ + +
+
+
+
+
+ +
+

Why it does not loop

+

+ An infinite track makes "slide 3 of 5" meaningless — there is no third of five on a ring — and + it removes the only honest position affordance a carousel has: a + Previous or Next that goes flat at the boundary. + A sighted user loses a scrollbar; everyone else loses the ability to know they have reached the + end. +

+

+ Position is also read from the scroll container on every scroll rather than remembered, + because a swipe, a flick and the arrow keys are all drivers this component cannot intercept. A stored + index would disagree with the screen after any of them, and the slide label would start lying. +

+
+ +
+

Why it does not auto-rotate

+

+ Auto-rotation is only acceptable with all of: a visible pause control, stopping on hover, + stopping on focus, and honouring prefers-reduced-motion. That chain is + exactly what gets half-built, and content that moves on its own is one of the most documented + accessibility failures on the web — it steals attention from the rest of the page and it moves + the target out from under anyone who reads or clicks slowly. +

+

+ So it is not built in. useCarousel() exposes + next(), prev() and + goTo(), so a consumer who genuinely needs rotation can drive it — and + owns the pause button that has to come with it. +

+ + The controls above are the whole API. Rotation is yours to justify. + +
+ +
+

Props

+

Each part also takes its native element's attributes via prop spreading.

+ + + + +
+
+ + diff --git a/apps/docs/src/routes/components/carousel/+page.ts b/apps/docs/src/routes/components/carousel/+page.ts new file mode 100644 index 0000000..189f71e --- /dev/null +++ b/apps/docs/src/routes/components/carousel/+page.ts @@ -0,0 +1 @@ +export const prerender = true; diff --git a/apps/docs/src/routes/components/resizable/+page.svelte b/apps/docs/src/routes/components/resizable/+page.svelte new file mode 100644 index 0000000..a664a5e --- /dev/null +++ b/apps/docs/src/routes/components/resizable/+page.svelte @@ -0,0 +1,204 @@ + + + +
+

Usage

+

+ Handle index is the pane on its leading side — the one whose + share it reports as aria-valuenow and points + aria-controls at. +

+

+ label is required. A separator with a tab stop is + a control, and one that announces only "splitter, 40" gives a keyboard user a number without saying + what it sizes. +

+ +
+ + +
Sidebar
+
+ + +
Content
+
+
+
+
+
+ +
+

The keyboard contract

+

+ This is the half every hand-rolled divider skips, and it is the difference between a splitter + and a decoration. Tab to the handle above and try it. +

+
    +
  • + Arrow keys — move the boundary by + step (5% by default). Left/Right in a horizontal group, Up/Down in a vertical + one. +
  • +
  • + Home / End — collapse and expand, as far as the + panes' own limits allow. +
  • +
  • + Focus ring — the handle is a real tab stop + with role="separator", aria-orientation and + live aria-valuenow. +
  • +
+

+ Two implementation details that only show up in use: the drag uses + setPointerCapture, so it survives the pointer leaving a hairline + target — which it does immediately — and the grabbable area is grown to 12px by a + pseudo-element, so the divider can stay 1px wide without being unreasonable to hit. +

+
+ +
+

Vertical

+

+ Note that aria-orientation on the handle is the opposite of the group's + direction: it describes the separator's own line, not the direction of travel. +

+ +
+ + +
Editor
+
+ + +
Console
+
+
+
+
+
+ +
+

Limits and clamping

+

+ A drag moves one boundary and touches exactly + two panes, clamped against + both of their limits before anything is applied. Distributing a drag across the whole group + is what produces the familiar bug where dragging one divider quietly reflows the far side of the + layout. +

+

+ Clamping only the pane being grown is the other half of that bug: it lets the neighbour shrink + past its own minimum. min defaults to 10 — pass + min={0} if a pane should be able to disappear entirely. +

+

+ Sizes are not persisted. Where that belongs — a + cookie, a user profile, localStorage — is an application decision, and guessing + it wrong is worse than not guessing. +

+ +
+ + +
Files
+
+ + +
Editor
+
+ + +
Inspector
+
+
+
+
+
+ +
+

Props

+

Each part also takes its native element's attributes via prop spreading.

+ + + +
+
+ + diff --git a/apps/docs/src/routes/components/resizable/+page.ts b/apps/docs/src/routes/components/resizable/+page.ts new file mode 100644 index 0000000..189f71e --- /dev/null +++ b/apps/docs/src/routes/components/resizable/+page.ts @@ -0,0 +1 @@ +export const prerender = true; diff --git a/apps/docs/tests/__screenshots__/carousel-0-dark.png b/apps/docs/tests/__screenshots__/carousel-0-dark.png new file mode 100644 index 0000000..46b53e7 Binary files /dev/null and b/apps/docs/tests/__screenshots__/carousel-0-dark.png differ diff --git a/apps/docs/tests/__screenshots__/carousel-0-light.png b/apps/docs/tests/__screenshots__/carousel-0-light.png new file mode 100644 index 0000000..462a370 Binary files /dev/null and b/apps/docs/tests/__screenshots__/carousel-0-light.png differ diff --git a/apps/docs/tests/__screenshots__/carousel-1-dark.png b/apps/docs/tests/__screenshots__/carousel-1-dark.png new file mode 100644 index 0000000..ab5f4d2 Binary files /dev/null and b/apps/docs/tests/__screenshots__/carousel-1-dark.png differ diff --git a/apps/docs/tests/__screenshots__/carousel-1-light.png b/apps/docs/tests/__screenshots__/carousel-1-light.png new file mode 100644 index 0000000..4ac97e5 Binary files /dev/null and b/apps/docs/tests/__screenshots__/carousel-1-light.png differ diff --git a/apps/docs/tests/__screenshots__/carousel-2-dark.png b/apps/docs/tests/__screenshots__/carousel-2-dark.png new file mode 100644 index 0000000..afc4c69 Binary files /dev/null and b/apps/docs/tests/__screenshots__/carousel-2-dark.png differ diff --git a/apps/docs/tests/__screenshots__/carousel-2-light.png b/apps/docs/tests/__screenshots__/carousel-2-light.png new file mode 100644 index 0000000..aeae6fc Binary files /dev/null and b/apps/docs/tests/__screenshots__/carousel-2-light.png differ diff --git a/apps/docs/tests/__screenshots__/carousel-3-dark.png b/apps/docs/tests/__screenshots__/carousel-3-dark.png new file mode 100644 index 0000000..cd8ea6d Binary files /dev/null and b/apps/docs/tests/__screenshots__/carousel-3-dark.png differ diff --git a/apps/docs/tests/__screenshots__/carousel-3-light.png b/apps/docs/tests/__screenshots__/carousel-3-light.png new file mode 100644 index 0000000..0a24769 Binary files /dev/null and b/apps/docs/tests/__screenshots__/carousel-3-light.png differ diff --git a/apps/docs/tests/__screenshots__/resizable-0-dark.png b/apps/docs/tests/__screenshots__/resizable-0-dark.png new file mode 100644 index 0000000..72830a9 Binary files /dev/null and b/apps/docs/tests/__screenshots__/resizable-0-dark.png differ diff --git a/apps/docs/tests/__screenshots__/resizable-0-light.png b/apps/docs/tests/__screenshots__/resizable-0-light.png new file mode 100644 index 0000000..6e228fe Binary files /dev/null and b/apps/docs/tests/__screenshots__/resizable-0-light.png differ diff --git a/apps/docs/tests/__screenshots__/resizable-1-dark.png b/apps/docs/tests/__screenshots__/resizable-1-dark.png new file mode 100644 index 0000000..0e739e3 Binary files /dev/null and b/apps/docs/tests/__screenshots__/resizable-1-dark.png differ diff --git a/apps/docs/tests/__screenshots__/resizable-1-light.png b/apps/docs/tests/__screenshots__/resizable-1-light.png new file mode 100644 index 0000000..723f7b1 Binary files /dev/null and b/apps/docs/tests/__screenshots__/resizable-1-light.png differ diff --git a/apps/docs/tests/__screenshots__/resizable-2-dark.png b/apps/docs/tests/__screenshots__/resizable-2-dark.png new file mode 100644 index 0000000..d7376d5 Binary files /dev/null and b/apps/docs/tests/__screenshots__/resizable-2-dark.png differ diff --git a/apps/docs/tests/__screenshots__/resizable-2-light.png b/apps/docs/tests/__screenshots__/resizable-2-light.png new file mode 100644 index 0000000..e224f93 Binary files /dev/null and b/apps/docs/tests/__screenshots__/resizable-2-light.png differ diff --git a/apps/example/src/routes/browse/+page.svelte b/apps/example/src/routes/browse/+page.svelte index 9645921..636dafb 100644 --- a/apps/example/src/routes/browse/+page.svelte +++ b/apps/example/src/routes/browse/+page.svelte @@ -5,6 +5,7 @@ Badge, Button, Card, + Carousel, Collapsible, Combobox, Command, @@ -20,6 +21,7 @@ NavigationMenu, Pagination, Popover, + Resizable, ScrollArea, Sheet, Skeleton, @@ -143,6 +145,30 @@ Security + + + + {#each rows.slice(0, 6) as row (row.id)} + + + + {row.title} + {row.kind} + + + + {/each} + +
+ + +
+
+