@joewinke/jatui 0.39.0 → 0.40.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/CLAUDE.md +23 -5
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/dist/actions/scrollEdge.d.ts +55 -0
- package/dist/actions/scrollEdge.js +144 -0
- package/dist/artifact-blocks/blocks/diagram/DiagramBlock.svelte.d.ts +1 -1
- package/dist/artifact-blocks/blocks/diagram/schema.d.ts +1 -1
- package/dist/components/BottomSheet.svelte +7 -6
- package/dist/components/BottomSheet.svelte.d.ts +2 -0
- package/dist/components/CloseButton.svelte +75 -0
- package/dist/components/CloseButton.svelte.d.ts +29 -0
- package/dist/components/Drawer.svelte +10 -11
- package/dist/components/Drawer.svelte.d.ts +2 -8
- package/dist/components/IconButton.svelte +200 -0
- package/dist/components/IconButton.svelte.d.ts +46 -0
- package/dist/components/LinkButton.svelte +117 -0
- package/dist/components/LinkButton.svelte.d.ts +32 -0
- package/dist/components/MediaLightbox.svelte +2 -7
- package/dist/components/Modal.svelte +2 -10
- package/dist/components/PinInput.svelte +94 -31
- package/dist/components/PinInput.svelte.d.ts +4 -0
- package/dist/components/TextArea.svelte +67 -5
- package/dist/components/TextArea.svelte.d.ts +4 -6
- package/dist/components/ThemeSelector.svelte +29 -9
- package/dist/components/ThemeSelector.svelte.d.ts +6 -0
- package/dist/components/ThemeToggle.svelte +28 -3
- package/dist/components/ThemeToggle.svelte.d.ts +9 -0
- package/dist/components/basic/Breadcrumb.svelte +226 -11
- package/dist/components/basic/Breadcrumb.svelte.d.ts +27 -1
- package/dist/components/basic/breadcrumbCollapse.d.ts +19 -0
- package/dist/components/basic/breadcrumbCollapse.js +12 -0
- package/dist/components/graph/GraphCanvas.svelte.d.ts +2 -2
- package/dist/index.d.ts +14 -1
- package/dist/index.js +10 -1
- package/dist/styles/tokens.css +80 -1
- package/dist/utils/pinInput.d.ts +33 -0
- package/dist/utils/pinInput.js +63 -0
- package/dist/utils/textareaRows.d.ts +23 -0
- package/dist/utils/textareaRows.js +13 -0
- package/dist/utils/themeWipe.d.ts +36 -17
- package/dist/utils/themeWipe.js +145 -35
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -22,9 +22,10 @@ Then import:
|
|
|
22
22
|
| Component | Purpose | Key Props |
|
|
23
23
|
|-----------|---------|-----------|
|
|
24
24
|
| `TextInput` | Text input with label, error, icon slots | `label`, `value` (bindable), `placeholder`, `error`, `disabled` |
|
|
25
|
-
| `TextArea` | Multiline text input | `label`, `value` (bindable), `placeholder`, `rows`, `error`, `maxlength` |
|
|
25
|
+
| `TextArea` | Multiline text input. Auto-grow (`autoResize`, or implied by `maxRows`) grows a whole line at a time from `minRows` (default `rows`) to `maxRows`, then scrolls — CSS `field-sizing: content` where supported, row-quantized JS fallback (`textareaRowsHeight`) elsewhere. | `label`, `value` (bindable), `placeholder`, `rows`, `minRows?`, `maxRows?`, `autoResize?`, `error`, `maxlength` |
|
|
26
26
|
| `SelectInput` | Dropdown select | `label`, `value` (bindable), `options: SelectOption[]`, `placeholder` |
|
|
27
27
|
| `PhoneInput` | Intl phone (country dropdown + tel input, outputs E.164; caps national digits per country). Fires `onChange(value)` on every change. | `value` (bindable), `name`, `defaultCountry`, `required`, `onChange` |
|
|
28
|
+
| `PinInput` | One box per character for PINs and one-time codes. Every cell is `autocomplete="one-time-code"` with no maxlength, so SMS autofill / WebOTP (which drop the whole code into the focused cell) fill all cells from the first; paste distributes from the caret; `dir="ltr"`, `inputmode`/`pattern` numeric. Pure helpers: `applyPinCellInput`, `distributePin`, `isPinComplete`. | `value` (bindable), `length` (6), `numeric`, `masked`, `name?`, `autocomplete?` (`one-time-code`), `error`, `label`, `oncomplete` |
|
|
28
29
|
| `ChipInput` | Tag/chip entry with suggestions | `chips` (bindable), `suggestions: SuggestionGroup[]`, `placeholder` |
|
|
29
30
|
| `SearchDropdown` | Searchable dropdown with grouped results, keyboard nav, avatar/initials options. `appendToBody` escapes table/overflow stacking contexts; `variant='chip'` renders a compact pill; `size='sm'` a smaller trigger; `dropup` opens upward; `clearable` shows an × when a value is set. `onSearch` turns the built-in search box into a debounced server-side search hook (fires on open/clear with `''`; internal filtering is bypassed — the consumer updates `groups`). Exposes `openDropdown()`. | `groups: SearchDropdownGroup[]`, `value`, `onChange`, `placeholder`, `variant`, `size`, `dropup`, `appendToBody`, `clearable`, `colorFn`, `onSearch`, `searchDebounceMs` |
|
|
30
31
|
| `FilterDropdown` | Multi-select filter dropdown | `label`, `options: string[]`, `selected: Set<string>` (bindable) |
|
|
@@ -56,13 +57,16 @@ Then import:
|
|
|
56
57
|
| Component | Purpose | Key Props |
|
|
57
58
|
|-----------|---------|-----------|
|
|
58
59
|
| `Button` | Styled button with variants | `variant`, `size`, `loading`, `disabled`, `onclick` |
|
|
60
|
+
| `IconButton` | Icon-only button with a **required** `label` (the aria-label; also the tooltip text with `tooltip`). Square/circle, toggle `pressed` (aria-pressed + selection surface), `href` → `<a>`, `variant="overlay"` for chrome over media. Use instead of hand-written `btn btn-sm btn-circle btn-ghost`. | `label`, `variant?` (ghost\|outline\|soft\|primary\|secondary\|error\|overlay), `size?`, `shape?` ('square'\|'circle'), `pressed?`, `tooltip?: boolean \| string`, `href?`, `children` (icon) |
|
|
61
|
+
| `CloseButton` | The one ✕ for dialogs, drawers, sheets, toasts and lightboxes — an IconButton with a built-in X whose stroke is a true CSS pixel at every size. Modal, Drawer, BottomSheet and MediaLightbox use it. | `label?` ('Close'), `size?` ('sm'), `variant?`, `shape?` ('circle'), `tooltip?`, `onclick` |
|
|
62
|
+
| `LinkButton` | Text action styled as a link: `<a>` with `href`, `<button>` without; underline on hover (or always). `external` adds new-tab + rel=noopener + an SR hint. | `href?`, `tone?` (inherit\|primary\|muted\|error), `size?`, `underline?`, `external?`, `disabled?`, `onclick?`, `children` |
|
|
59
63
|
| `ProgressButton` | Multi-phase async submit button (upstreamed from the JAT IDE): walks through the **named phases** of the work (`phases: [{key,label}]` in order) instead of a blank spinner — spinner + current phase label with shimmer, a fill bar that advances to `(index+1)/N`, disabled while busy, green check-flash on `success`. Caller owns the state machine (`phase` / `success`). Renders through the shared `Button`, so `class` composes with join patterns. | `phase?` (null = idle), `phases?: Phase[]`, `idleLabel?`, `success?`, `successLabel?`, `variant?`, `size?`, `disabled?`, `type?`, `class?`, `onclick?`, `children` |
|
|
60
64
|
| `Badge` | Status/label badge | `variant`, `size`, `children` (snippet) |
|
|
61
65
|
| `StatusBadge` | Status indicator with dot | `status: StatusValue`, `label` |
|
|
62
66
|
| `Card` | Content card container | `title`, `children` (snippet) |
|
|
63
67
|
| `Modal` | Dialog overlay. `zIndex` accepts `number` OR any CSS `z-index` string, including `"var(--z-lightbox)"`. **Escape is owned by the dialog element itself** (not the window handler) — Modal focuses the dialog on open, so `trapKeys`' stopPropagation would otherwise swallow Esc before the window listener sees it; a Modal opened inside a host Drawer closes before the drawer. The component ships its own open-state `.modal-box` opacity rule, so it renders under DaisyUI 5 with no host global override. | `open` (bindable), `title`, `children` (snippet), `zIndex?: string \| number`, `trapKeys?`, `closeOnEscape?` |
|
|
64
68
|
| `ConfirmModal` | Yes/no confirmation dialog | `open` (bindable), `title`, `message`, `onConfirm`, `onCancel` |
|
|
65
|
-
| `Drawer` | Slide-out side panel | `open` (bindable), `side`, `children` (snippet) |
|
|
69
|
+
| `Drawer` | Slide-out side panel. Header ✕ is a `CloseButton`; the body carries `use:scrollEdge` (opt out with `scrollEdge={false}`). | `open` (bindable), `side`, `scrollEdge?`, `children` (snippet) |
|
|
66
70
|
| `LoadingSpinner` | Spinner animation | `size`, `color` |
|
|
67
71
|
| `Sparkline` | Inline mini chart | `data: number[]`, `width`, `height`, `color` |
|
|
68
72
|
| `LazyImage` | Image with lazy loading + placeholder | `src`, `alt`, `placeholder` |
|
|
@@ -220,11 +224,13 @@ import { loadLiveKit, isLiveKitAvailable, LiveKitNotInstalledError } from 'jatui
|
|
|
220
224
|
| Component | Purpose | Key Props |
|
|
221
225
|
|-----------|---------|-----------|
|
|
222
226
|
| `PageHeader` | The standard route header (upstreamed from the JAT IDE): uppercase/tracked "instrument label" title + slots for a count chip, subtitle, leading `icon` snippet, right-aligned `actions` cluster, `meta` metrics strip and content below the header row. `sticky` pins it to the top of the scroll container; `size='lg'` for landing screens. All color via `--color-*` tokens; entrance animation reduced-motion gated (`animate`, default off). | `title`, `subtitle?`, `count?`, `size?` ('md'\|'lg'), `sticky?`, `animate?`, `icon?`/`actions?`/`meta?`/`children` (snippets) |
|
|
227
|
+
| `Breadcrumb` | Location trail in a `<nav aria-label>`; the last item is `aria-current="page"`. `maxItems` keeps the root + the last `maxItems - 1` and folds the middle into a `…` button whose menu lists the hidden levels (arrows/Home/End/Escape, click-outside; pure `collapseBreadcrumb`). Leading icons via `item.icon` (component taking `{size,class}`) or the `icon` snippet. | `items: BreadcrumbItem[]`, `maxItems?`, `icon?` (snippet `(item, i)`), `ariaLabel?` |
|
|
223
228
|
| `ResizablePanel` | Resizable container | `minWidth`, `maxWidth`, `defaultWidth` |
|
|
224
229
|
| `ResizableDivider` | Drag handle between panels | `onResize` |
|
|
225
230
|
| `InfiniteGrid` | Cursor-paged infinite-scroll grid (from chimaro). `loadMore(cursor, {signal})` resolves `{items, nextCursor, total?}` (`nextCursor: null` = end); cells render through the `renderItem(item, i)` snippet. Change `resetKey` to start over — the in-flight page is aborted and a late page for the old key is dropped. One request in flight; items de-duped by `getKey`; short pages auto-chain until the sentinel leaves the screen; distinct skeleton / empty / exhausted / error(+Retry) states; non-item rows are `overflow-anchor: none` so scroll anchoring can't runaway-load. Scroll root = `root` or nearest scrollable ancestor. Exposes `reset()`/`retry()`. Not virtualized. Pure core: `createInfiniteLoader`. | `loadMore`, `renderItem` (snippet), `getKey?`, `resetKey?`, `columns?` / `minItemWidth?`, `gap?`, `root?`, `loadAhead?` (400px), `skeleton?`/`empty?`/`end?`/`error?` (snippets), `emptyText?`, `endText?`, `onStateChange?` |
|
|
226
231
|
| `InfiniteSearchGrid` | Search box + `InfiniteGrid`. Query is debounced, then becomes the grid's `resetKey` (Enter commits now, Escape clears). Below `minQueryLength` no request is made and `prompt` shows. All other `InfiniteGrid` props pass through. | `search(query, cursor, {signal})`, `renderItem` (snippet), `query` (bindable), `debounceMs?` (300), `minQueryLength?` (0), `placeholder?`, `noResultsText?`/`noResults?`, `prompt?`/`promptText?`, `toolbar?` (snippet) |
|
|
227
232
|
| `SectionNav` | Sticky in-page section index for long single-column pages — vertical rail beside the content on xl+, wrapping chip row above it below xl. Built-in scroll-spy (omit `activeId` and it just works; `bind:activeId` to observe/control — the spy writes back to it): on every observer callback + rAF-throttled scroll/resize it measures ALL sections and activates the last one whose top is at/above the activation line (`activationRatio`, default 0.3 of the scroller height); at the bottom of the page the last section wins. Clicking an item activates it immediately and holds the spy off until the smooth scroll lands (`scrollend`, or 800ms after the last scroll event); reduced-motion scrolls instantly. Pure decision fn exported as `computeActiveSection(rects, viewport, scroll, ratio?)`. Amber dot per `dirtyIds` marks sections holding unsaved edits. `go()` scrolls AND moves focus so keyboard/SR users land where the click did. j/k/Enter via `listNav`. Layout contract: parent is a `flex flex-wrap gap-8 items-start` row, content column `flex-1 min-w-0`; sections need `id` + `scroll-mt-*`. | `sections: {id,label}[]`, `activeId?` (bindable), `dirtyIds?: Set<string>`, `ariaLabel?`, `spy?`, `activationRatio?` (0.3) |
|
|
233
|
+
| `use:scrollEdge` (action) | Progressive-blur fade on a scroll region's edge once content scrolls under it: a stationary `aria-hidden` sticky strip inside the scroller with two masked backdrop-blur layers + a surface→transparent gradient, each ramping opacity 0→1 over the first `size` px of scroll distance (nothing drawn at rest). Keep headers OUTSIDE the scroller; chat threads (stick-to-bottom) want `edges: 'bottom'`. Opacity-only, reduced-motion safe. Pure: `edgeOpacity`, `edgeDistances`. | `edges?` ('top'\|'bottom'\|'both'), `size?` (24), `surface?` (`--jatui-surface`), `blur?` (true), `enabled?` |
|
|
228
234
|
|
|
229
235
|
**Windowing (subpath: `jatui/virtual`, optional peer `@tanstack/virtual-core`).** `createRowWindow(opts)` is headless variable-height windowing built on **spacer rows**, not absolute positioning, so it works inside a `<table>` and keeps `table-layout: fixed` and sticky `<thead>`s. It returns reactive `items` (`{index, key, start, size, end, gapBefore, sticky, pinned}`), `paddingTop`, `paddingBottom`, `totalSize` and `windowed`, plus `use:win.anchor` (on the `<tbody>` or list container, whose top is item 0's top) and `use:win.measure={item.key}` (on each item; a shared ResizeObserver re-measures it). Reading a row's height for the first time, or later re-measuring one above the viewport, shifts `scrollTop` by the delta so nothing jumps. The anchor gets `overflow-anchor: none` so native scroll anchoring doesn't correct the shift a second time. The options are reactive getters: `count`, `getKey`, `estimateSize`, `pinned()` (keys kept rendered, e.g. an open editor or exiting row), `stickyIndices()` (a flat list, or one array per level `[projectHdrs, epicHdrs]`; the active header of each level stays rendered, and an inner header never outlives its outer group), and `threshold` (render everything below N; pass it as a getter to make it reactive). Also `scrollRoot` (`'auto'` = the nearest ancestor that really scrolls, else the window; it re-resolves on resize), `overscan` (6), `scrollPaddingStart/End` and `initialViewportSize` (800; used for SSR and first paint). Methods: `scrollToIndex`/`scrollToKey(…, {align})` (queued until mount), `indexOf`, `remeasure()`. Render `item.gapBefore` as a spacer before the item; it is non-zero only when a pinned or sticky item sits outside the slice. Pure policy helpers are exported too (`extractWindowIndices`, `activeStickyIndices`, `layoutWindow`, `findScrollRoot`, `RowWindowEngine`). Demo: `/components/row-window`.
|
|
230
236
|
|
|
@@ -363,8 +369,8 @@ const callbacks: AnnotationCallbacks = {
|
|
|
363
369
|
|
|
364
370
|
| Component | Purpose | Key Props |
|
|
365
371
|
|-----------|---------|-----------|
|
|
366
|
-
| `ThemeSelector` | DaisyUI theme switcher with preview dots | `themes`, `defaultTheme`, `dropUp`, `storageKey`, `onThemeChange` |
|
|
367
|
-
| `ThemeToggle` | Icon-only sun<->moon light/dark toggle with an animated morph (transform+opacity on motion tokens; instant under reduced-motion; spring flattens on `data-motion="calm"`). A pure control — flips the bindable `theme` and fires `onchange`; storage stays with the consumer. | `theme` (bindable), `lightTheme`, `darkTheme`, `onchange`, `size` |
|
|
372
|
+
| `ThemeSelector` | DaisyUI theme switcher with preview dots. `reveal` wipes the new theme in from the clicked item (`themeWipe`). | `themes`, `defaultTheme`, `dropUp`, `storageKey`, `onThemeChange`, `reveal?: boolean \| 'hard' \| 'soft'` |
|
|
373
|
+
| `ThemeToggle` | Icon-only sun<->moon light/dark toggle with an animated morph (transform+opacity on motion tokens; instant under reduced-motion; spring flattens on `data-motion="calm"`). A pure control — flips the bindable `theme` and fires `onchange`; storage stays with the consumer. `reveal` runs the flip inside a `themeWipe` circular View Transition from the button (keyboard activation grows from its centre). | `theme` (bindable), `lightTheme`, `darkTheme`, `onchange`, `size`, `reveal?: boolean \| 'hard' \| 'soft'` |
|
|
368
374
|
|
|
369
375
|
### Keyboard Navigation
|
|
370
376
|
|
|
@@ -526,6 +532,12 @@ import { groupRows, groupPathKey, type GroupLevel, type GroupedItem } from "jatu
|
|
|
526
532
|
// groupRows(rows, [{ key: t => t.project }, { key: t => t.epic }], { getRowId, collapsed, label? })
|
|
527
533
|
// → { kind:'group', key, path, depth, label, rows, count, collapsed } | { kind:'row', row, id, depth, groupPath, position }
|
|
528
534
|
|
|
535
|
+
// Theme change reveal — circular View Transition from the click (or, for a
|
|
536
|
+
// keyboard activation, the control's centre). Single-flight, level-scaled
|
|
537
|
+
// duration, reduced-motion = instant. ThemeToggle/ThemeSelector `reveal` use it.
|
|
538
|
+
import { themeWipe } from "jatui"
|
|
539
|
+
// themeWipe(event, () => setTheme(next), { edge: 'soft' }) // edge: 'hard' (default) | 'soft'
|
|
540
|
+
|
|
529
541
|
// Date formatting
|
|
530
542
|
import { formatRelativeTime, formatDate, formatShortDate, formatDateTime } from "jatui"
|
|
531
543
|
|
|
@@ -645,7 +657,13 @@ Three rules keep DaisyUI swappable instead of load-bearing:
|
|
|
645
657
|
internal skin, like vendored CSS — never part of a component's public
|
|
646
658
|
props/API.
|
|
647
659
|
3. **jatui owns the token layer.** Components consume `--jatui-*`
|
|
648
|
-
(`styles/tokens.css`: radius/spacing/border
|
|
660
|
+
(`styles/tokens.css`: radius/spacing/border, plus the colour-intent layer —
|
|
661
|
+
`--jatui-focus-ring{,-width,-offset}`, `--jatui-surface`, `--jatui-hover-bg`,
|
|
662
|
+
`--jatui-pressed-bg`, `--jatui-separator`, `--jatui-selection-{bg,edge,solid,solid-fg}`,
|
|
663
|
+
`--jatui-chart-{1..5,grid,axis}`, `--jatui-event-{1..6}{,-bg,-fg}`,
|
|
664
|
+
`--jatui-status-{positive,caution,negative,neutral}`; all `color-mix` over DaisyUI
|
|
665
|
+
semantic vars, re-declared on `[data-theme]` so nested themes recompute — use
|
|
666
|
+
them with an inline fallback) and `--motion-*`
|
|
649
667
|
(`styles/motion.css`: easing/duration) custom properties. DaisyUI theme
|
|
650
668
|
variables feed INTO those tokens (as fallback-chained inputs), never
|
|
651
669
|
appear directly in component styles for values the tokens cover.
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -49,3 +49,19 @@ Files adapted from BoardUI free components:
|
|
|
49
49
|
BoardUI Pro items are not covered by the MIT licence. `AgentTaskList`,
|
|
50
50
|
`Questionnaire` and `ContextMeter` were built from public behaviour only; no Pro
|
|
51
51
|
source was read or copied.
|
|
52
|
+
|
|
53
|
+
### Wave 1 (jatui-owg2z) — ideas only, written fresh for Svelte 5
|
|
54
|
+
|
|
55
|
+
None of these files is a close port, so none carries the BoardUI notice. They
|
|
56
|
+
follow ideas from BoardUI's free items:
|
|
57
|
+
|
|
58
|
+
- `IconButton`, `CloseButton`, `LinkButton`: BoardUI icon-button, close-button
|
|
59
|
+
and link-button.
|
|
60
|
+
- `use:scrollEdge`: BoardUI's scroll-region progressive-blur guidance.
|
|
61
|
+
- Colour-intent tokens in `styles/tokens.css`: BoardUI's semantic theme vars,
|
|
62
|
+
reduced to a thin `color-mix` layer over DaisyUI.
|
|
63
|
+
- `PinInput` one-time-code autofill: BoardUI input-otp.
|
|
64
|
+
- `TextArea` `minRows`/`maxRows`: BoardUI textarea.
|
|
65
|
+
- `themeWipe` fixes and `ThemeToggle`/`ThemeSelector` `reveal`: BoardUI theme
|
|
66
|
+
toggle.
|
|
67
|
+
- `Breadcrumb` `maxItems` and icons: BoardUI breadcrumb.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scrollEdge — progressive-blur fade on the edge of a scroll region.
|
|
3
|
+
*
|
|
4
|
+
* <div class="overflow-y-auto" use:scrollEdge>…</div> top edge
|
|
5
|
+
* <div class="overflow-y-auto" use:scrollEdge={{ edges: 'both' }}>…</div>
|
|
6
|
+
* <div use:scroller.action use:scrollEdge={{ edges: 'bottom' }}>…</div> chat (stick-to-bottom)
|
|
7
|
+
*
|
|
8
|
+
* When content has scrolled under an edge, that edge softens: a stationary,
|
|
9
|
+
* `pointer-events: none`, `aria-hidden` overlay with three layers —
|
|
10
|
+
*
|
|
11
|
+
* 1. a tall band of light backdrop blur (1px), masked to fade out,
|
|
12
|
+
* 2. a shorter band of heavier blur (4px), masked the same way,
|
|
13
|
+
* 3. a surface → transparent gradient.
|
|
14
|
+
*
|
|
15
|
+
* Each layer's opacity ramps 0 → 1 over the first `size` px of scroll
|
|
16
|
+
* distance from that edge, so at rest (scrollTop 0) nothing is drawn and
|
|
17
|
+
* the first line of content is never dimmed. Opacity goes on the LAYERS,
|
|
18
|
+
* never their parent: a translucent parent becomes a backdrop root and the
|
|
19
|
+
* blur would sample nothing.
|
|
20
|
+
*
|
|
21
|
+
* The overlay lives inside the scroller as a `position: sticky` strip with a
|
|
22
|
+
* negative margin, so it needs no positioned wrapper and takes no layout
|
|
23
|
+
* space. Keep headers OUTSIDE the scroller — the edge belongs to the scroll
|
|
24
|
+
* area, not the chrome above it. Opacity-only, so it is reduced-motion safe.
|
|
25
|
+
*
|
|
26
|
+
* Idea from BoardUI's scroll-region guidance (MIT); written fresh.
|
|
27
|
+
*/
|
|
28
|
+
export type ScrollEdgeSide = 'top' | 'bottom' | 'both';
|
|
29
|
+
export interface ScrollEdgeOptions {
|
|
30
|
+
/** Which edge(s) fade. Default 'top'. Chat threads that stick to the bottom want 'bottom'. */
|
|
31
|
+
edges?: ScrollEdgeSide;
|
|
32
|
+
/** Height of the fade band and the scroll distance the ramp runs over, px. Default 24. */
|
|
33
|
+
size?: number;
|
|
34
|
+
/** Colour the gradient blends into. Default the `--jatui-surface` token (base-100). */
|
|
35
|
+
surface?: string;
|
|
36
|
+
/** Backdrop blur layers. false = gradient only (cheaper on huge lists). Default true. */
|
|
37
|
+
blur?: boolean;
|
|
38
|
+
/** Turn it off without unmounting the action. Default true. */
|
|
39
|
+
enabled?: boolean;
|
|
40
|
+
}
|
|
41
|
+
/** Opacity for an edge given how far content has scrolled past it. Pure. */
|
|
42
|
+
export declare function edgeOpacity(distance: number, size: number): number;
|
|
43
|
+
/** Scroll distance past the top and bottom edges of a scroller. Pure. */
|
|
44
|
+
export declare function edgeDistances(m: {
|
|
45
|
+
scrollTop: number;
|
|
46
|
+
scrollHeight: number;
|
|
47
|
+
clientHeight: number;
|
|
48
|
+
}): {
|
|
49
|
+
top: number;
|
|
50
|
+
bottom: number;
|
|
51
|
+
};
|
|
52
|
+
export declare function scrollEdge(node: HTMLElement, options?: ScrollEdgeOptions): {
|
|
53
|
+
update(next?: ScrollEdgeOptions): void;
|
|
54
|
+
destroy(): void;
|
|
55
|
+
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scrollEdge — progressive-blur fade on the edge of a scroll region.
|
|
3
|
+
*
|
|
4
|
+
* <div class="overflow-y-auto" use:scrollEdge>…</div> top edge
|
|
5
|
+
* <div class="overflow-y-auto" use:scrollEdge={{ edges: 'both' }}>…</div>
|
|
6
|
+
* <div use:scroller.action use:scrollEdge={{ edges: 'bottom' }}>…</div> chat (stick-to-bottom)
|
|
7
|
+
*
|
|
8
|
+
* When content has scrolled under an edge, that edge softens: a stationary,
|
|
9
|
+
* `pointer-events: none`, `aria-hidden` overlay with three layers —
|
|
10
|
+
*
|
|
11
|
+
* 1. a tall band of light backdrop blur (1px), masked to fade out,
|
|
12
|
+
* 2. a shorter band of heavier blur (4px), masked the same way,
|
|
13
|
+
* 3. a surface → transparent gradient.
|
|
14
|
+
*
|
|
15
|
+
* Each layer's opacity ramps 0 → 1 over the first `size` px of scroll
|
|
16
|
+
* distance from that edge, so at rest (scrollTop 0) nothing is drawn and
|
|
17
|
+
* the first line of content is never dimmed. Opacity goes on the LAYERS,
|
|
18
|
+
* never their parent: a translucent parent becomes a backdrop root and the
|
|
19
|
+
* blur would sample nothing.
|
|
20
|
+
*
|
|
21
|
+
* The overlay lives inside the scroller as a `position: sticky` strip with a
|
|
22
|
+
* negative margin, so it needs no positioned wrapper and takes no layout
|
|
23
|
+
* space. Keep headers OUTSIDE the scroller — the edge belongs to the scroll
|
|
24
|
+
* area, not the chrome above it. Opacity-only, so it is reduced-motion safe.
|
|
25
|
+
*
|
|
26
|
+
* Idea from BoardUI's scroll-region guidance (MIT); written fresh.
|
|
27
|
+
*/
|
|
28
|
+
/** Opacity for an edge given how far content has scrolled past it. Pure. */
|
|
29
|
+
export function edgeOpacity(distance, size) {
|
|
30
|
+
if (!(size > 0) || !Number.isFinite(distance))
|
|
31
|
+
return 0;
|
|
32
|
+
return Math.min(1, Math.max(0, distance / size));
|
|
33
|
+
}
|
|
34
|
+
/** Scroll distance past the top and bottom edges of a scroller. Pure. */
|
|
35
|
+
export function edgeDistances(m) {
|
|
36
|
+
return {
|
|
37
|
+
top: Math.max(0, m.scrollTop),
|
|
38
|
+
bottom: Math.max(0, m.scrollHeight - m.clientHeight - m.scrollTop)
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const DEFAULT_SURFACE = 'var(--jatui-surface, var(--color-base-100, Canvas))';
|
|
42
|
+
function strip(side, o) {
|
|
43
|
+
const el = document.createElement('div');
|
|
44
|
+
el.setAttribute('aria-hidden', 'true');
|
|
45
|
+
el.dataset.scrollEdge = side;
|
|
46
|
+
const s = el.style;
|
|
47
|
+
s.position = 'sticky';
|
|
48
|
+
s.display = 'block';
|
|
49
|
+
s[side] = '0';
|
|
50
|
+
s.height = `${o.size}px`;
|
|
51
|
+
s[side === 'top' ? 'marginBottom' : 'marginTop'] = `-${o.size}px`;
|
|
52
|
+
s.pointerEvents = 'none';
|
|
53
|
+
s.zIndex = '1';
|
|
54
|
+
s.gridColumn = '1 / -1'; // a grid scroller must not give the strip its own cell
|
|
55
|
+
s.flexShrink = '0';
|
|
56
|
+
s.alignSelf = 'stretch';
|
|
57
|
+
const dir = side === 'top' ? 'to bottom' : 'to top';
|
|
58
|
+
const anchor = side === 'top' ? 'top: 0' : 'bottom: 0';
|
|
59
|
+
const layers = [];
|
|
60
|
+
if (o.blur) {
|
|
61
|
+
layers.push(`${anchor}; height: 100%; backdrop-filter: blur(1px); -webkit-backdrop-filter: blur(1px); mask-image: linear-gradient(${dir}, #000, transparent); -webkit-mask-image: linear-gradient(${dir}, #000, transparent)`, `${anchor}; height: 66%; backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); mask-image: linear-gradient(${dir}, #000, transparent); -webkit-mask-image: linear-gradient(${dir}, #000, transparent)`);
|
|
62
|
+
}
|
|
63
|
+
layers.push(`${anchor}; height: 100%; background: linear-gradient(${dir}, ${o.surface}, transparent)`);
|
|
64
|
+
for (const css of layers) {
|
|
65
|
+
const layer = document.createElement('div');
|
|
66
|
+
layer.style.cssText = `position: absolute; left: 0; right: 0; ${css}; opacity: var(--jatui-scroll-edge-o, 0); transition: opacity var(--motion-instant, 80ms) linear;`;
|
|
67
|
+
el.appendChild(layer);
|
|
68
|
+
}
|
|
69
|
+
return el;
|
|
70
|
+
}
|
|
71
|
+
export function scrollEdge(node, options = {}) {
|
|
72
|
+
let opts = resolve(options);
|
|
73
|
+
let top = null;
|
|
74
|
+
let bottom = null;
|
|
75
|
+
let frame = 0;
|
|
76
|
+
let ro = null;
|
|
77
|
+
function resolve(o) {
|
|
78
|
+
return {
|
|
79
|
+
edges: o.edges ?? 'top',
|
|
80
|
+
size: o.size ?? 24,
|
|
81
|
+
surface: o.surface ?? DEFAULT_SURFACE,
|
|
82
|
+
blur: o.blur ?? true,
|
|
83
|
+
enabled: o.enabled ?? true
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function measure() {
|
|
87
|
+
frame = 0;
|
|
88
|
+
const d = edgeDistances(node);
|
|
89
|
+
top?.style.setProperty('--jatui-scroll-edge-o', String(edgeOpacity(d.top, opts.size)));
|
|
90
|
+
bottom?.style.setProperty('--jatui-scroll-edge-o', String(edgeOpacity(d.bottom, opts.size)));
|
|
91
|
+
}
|
|
92
|
+
function schedule() {
|
|
93
|
+
if (frame)
|
|
94
|
+
return;
|
|
95
|
+
frame = requestAnimationFrame(measure);
|
|
96
|
+
}
|
|
97
|
+
function mount() {
|
|
98
|
+
unmount();
|
|
99
|
+
if (!opts.enabled)
|
|
100
|
+
return;
|
|
101
|
+
if (opts.edges !== 'bottom') {
|
|
102
|
+
top = strip('top', opts);
|
|
103
|
+
node.prepend(top);
|
|
104
|
+
}
|
|
105
|
+
if (opts.edges !== 'top') {
|
|
106
|
+
bottom = strip('bottom', opts);
|
|
107
|
+
node.append(bottom);
|
|
108
|
+
}
|
|
109
|
+
measure();
|
|
110
|
+
}
|
|
111
|
+
function unmount() {
|
|
112
|
+
top?.remove();
|
|
113
|
+
bottom?.remove();
|
|
114
|
+
top = bottom = null;
|
|
115
|
+
}
|
|
116
|
+
node.addEventListener('scroll', schedule, { passive: true });
|
|
117
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
118
|
+
ro = new ResizeObserver(schedule);
|
|
119
|
+
ro.observe(node);
|
|
120
|
+
}
|
|
121
|
+
// Content growth changes the bottom distance without a scroll event.
|
|
122
|
+
const mo = typeof MutationObserver !== 'undefined' ? new MutationObserver(schedule) : null;
|
|
123
|
+
mo?.observe(node, { childList: true, subtree: true, characterData: true });
|
|
124
|
+
mount();
|
|
125
|
+
return {
|
|
126
|
+
update(next = {}) {
|
|
127
|
+
const prev = opts;
|
|
128
|
+
opts = resolve(next);
|
|
129
|
+
const same = Object.keys(opts).every((k) => opts[k] === prev[k]);
|
|
130
|
+
if (same)
|
|
131
|
+
schedule();
|
|
132
|
+
else
|
|
133
|
+
mount();
|
|
134
|
+
},
|
|
135
|
+
destroy() {
|
|
136
|
+
node.removeEventListener('scroll', schedule);
|
|
137
|
+
ro?.disconnect();
|
|
138
|
+
mo?.disconnect();
|
|
139
|
+
if (frame)
|
|
140
|
+
cancelAnimationFrame(frame);
|
|
141
|
+
unmount();
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
@@ -6,7 +6,7 @@ declare const DiagramBlock: import("svelte").Component<BlockProps<{
|
|
|
6
6
|
id: string;
|
|
7
7
|
label: string;
|
|
8
8
|
sublabel?: string | undefined;
|
|
9
|
-
kind?: "data" | "input" | "output" | "note" | "default" | "
|
|
9
|
+
kind?: "data" | "input" | "output" | "note" | "default" | "external" | "decision" | "terminal" | "process" | "subroutine" | undefined;
|
|
10
10
|
tone?: "error" | "warning" | "info" | "success" | "primary" | "secondary" | "neutral" | "accent" | undefined;
|
|
11
11
|
shape?: "circle" | "rect" | "stadium" | "pill" | "diamond" | undefined;
|
|
12
12
|
}[];
|
|
@@ -39,8 +39,8 @@ export declare const diagramParamsSchema: z.ZodObject<{
|
|
|
39
39
|
output: "output";
|
|
40
40
|
note: "note";
|
|
41
41
|
default: "default";
|
|
42
|
-
decision: "decision";
|
|
43
42
|
external: "external";
|
|
43
|
+
decision: "decision";
|
|
44
44
|
terminal: "terminal";
|
|
45
45
|
process: "process";
|
|
46
46
|
subroutine: "subroutine";
|
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
settles on the snap nearest the post-drag height.
|
|
27
27
|
-->
|
|
28
28
|
<script lang="ts">
|
|
29
|
+
import CloseButton from './CloseButton.svelte';
|
|
30
|
+
import { scrollEdge as scrollEdgeAction } from '../actions/scrollEdge';
|
|
29
31
|
import { onMount, untrack } from 'svelte';
|
|
30
32
|
import type { ShortcutRegistry } from '../utils/keyboardShortcutRegistry';
|
|
31
33
|
import type { Snippet } from 'svelte';
|
|
@@ -49,6 +51,7 @@
|
|
|
49
51
|
onclose = () => {},
|
|
50
52
|
shortcutRegistry,
|
|
51
53
|
shortcutContext = 'bottom-sheet',
|
|
54
|
+
scrollEdge = true,
|
|
52
55
|
children
|
|
53
56
|
}: {
|
|
54
57
|
open?: boolean;
|
|
@@ -70,6 +73,8 @@
|
|
|
70
73
|
shortcutRegistry?: ShortcutRegistry;
|
|
71
74
|
/** Context name pushed while open. Default `'bottom-sheet'`. */
|
|
72
75
|
shortcutContext?: string;
|
|
76
|
+
/** Soft progressive-blur fade at the top of the content once it scrolls (`use:scrollEdge`). Ignored with `fullHeight` (the slot owns scrolling). Default true. */
|
|
77
|
+
scrollEdge?: boolean;
|
|
73
78
|
children?: Snippet;
|
|
74
79
|
} = $props();
|
|
75
80
|
|
|
@@ -254,12 +259,7 @@
|
|
|
254
259
|
{#if title}
|
|
255
260
|
<div class="flex-shrink-0 flex items-center justify-between px-4 py-2 border-b border-base-200">
|
|
256
261
|
<span class="text-sm font-semibold">{title}</span>
|
|
257
|
-
<
|
|
258
|
-
type="button"
|
|
259
|
-
class="btn btn-sm btn-ghost btn-circle"
|
|
260
|
-
aria-label="Close"
|
|
261
|
-
onclick={dismiss}
|
|
262
|
-
>✕</button>
|
|
262
|
+
<CloseButton label="Close" onclick={dismiss} />
|
|
263
263
|
</div>
|
|
264
264
|
{/if}
|
|
265
265
|
|
|
@@ -273,6 +273,7 @@
|
|
|
273
273
|
: keyboardInset > 0
|
|
274
274
|
? `padding-bottom: ${keyboardInset}px;`
|
|
275
275
|
: ''}
|
|
276
|
+
use:scrollEdgeAction={{ enabled: scrollEdge && !(fullHeight && !isDual) }}
|
|
276
277
|
>
|
|
277
278
|
{@render children?.()}
|
|
278
279
|
</div>
|
|
@@ -20,6 +20,8 @@ type $$ComponentProps = {
|
|
|
20
20
|
shortcutRegistry?: ShortcutRegistry;
|
|
21
21
|
/** Context name pushed while open. Default `'bottom-sheet'`. */
|
|
22
22
|
shortcutContext?: string;
|
|
23
|
+
/** Soft progressive-blur fade at the top of the content once it scrolls (`use:scrollEdge`). Ignored with `fullHeight` (the slot owns scrolling). Default true. */
|
|
24
|
+
scrollEdge?: boolean;
|
|
23
25
|
children?: Snippet;
|
|
24
26
|
};
|
|
25
27
|
declare const BottomSheet: import("svelte").Component<$$ComponentProps, {}, "open">;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* CloseButton — the one ✕ every dialog, drawer, sheet, toast and lightbox
|
|
4
|
+
* needs. An IconButton with a built-in X glyph whose stroke is a true CSS
|
|
5
|
+
* pixel width at every size (`vector-effect: non-scaling-stroke`), so the
|
|
6
|
+
* cross never turns hairline at xs or chunky at lg.
|
|
7
|
+
*
|
|
8
|
+
* <CloseButton onclick={close} />
|
|
9
|
+
* <CloseButton label="Close drawer" size="md" onclick={close} />
|
|
10
|
+
* <CloseButton variant="overlay" onclick={close} /> <!-- over media -->
|
|
11
|
+
*
|
|
12
|
+
* Idea from BoardUI's close-button (MIT); written fresh for Svelte 5.
|
|
13
|
+
*/
|
|
14
|
+
import IconButton, { type IconButtonSize, type IconButtonVariant } from './IconButton.svelte';
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
/** Accessible name. Default "Close" — name the thing ("Close drawer") when several are on screen. */
|
|
18
|
+
label?: string;
|
|
19
|
+
size?: IconButtonSize;
|
|
20
|
+
variant?: IconButtonVariant;
|
|
21
|
+
shape?: 'square' | 'circle';
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
/** Show the label as a tooltip (string = custom text, e.g. "Close (Esc)"). */
|
|
24
|
+
tooltip?: boolean | string;
|
|
25
|
+
class?: string;
|
|
26
|
+
onclick?: (e: MouseEvent) => void;
|
|
27
|
+
[key: string]: unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let {
|
|
31
|
+
label = 'Close',
|
|
32
|
+
size = 'sm',
|
|
33
|
+
variant = 'ghost',
|
|
34
|
+
shape = 'circle',
|
|
35
|
+
disabled = false,
|
|
36
|
+
tooltip = false,
|
|
37
|
+
class: className = '',
|
|
38
|
+
onclick,
|
|
39
|
+
...rest
|
|
40
|
+
}: Props = $props();
|
|
41
|
+
|
|
42
|
+
/** Glyph box per size — the X itself is inset to ~58% of it. */
|
|
43
|
+
const GLYPH: Record<IconButtonSize, number> = { xs: 12, sm: 16, md: 18, lg: 22 };
|
|
44
|
+
const STROKE: Record<IconButtonSize, number> = { xs: 1.5, sm: 1.75, md: 2, lg: 2 };
|
|
45
|
+
const px = $derived(GLYPH[size] ?? 16);
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<IconButton
|
|
49
|
+
{...rest}
|
|
50
|
+
{label}
|
|
51
|
+
{size}
|
|
52
|
+
{variant}
|
|
53
|
+
{shape}
|
|
54
|
+
{disabled}
|
|
55
|
+
{tooltip}
|
|
56
|
+
{onclick}
|
|
57
|
+
class="jatui-close-btn {className}"
|
|
58
|
+
>
|
|
59
|
+
<svg
|
|
60
|
+
width={px}
|
|
61
|
+
height={px}
|
|
62
|
+
viewBox="0 0 24 24"
|
|
63
|
+
fill="none"
|
|
64
|
+
stroke="currentColor"
|
|
65
|
+
stroke-linecap="round"
|
|
66
|
+
aria-hidden="true"
|
|
67
|
+
focusable="false"
|
|
68
|
+
>
|
|
69
|
+
<path
|
|
70
|
+
d="M17 7L7 17M7 7l10 10"
|
|
71
|
+
stroke-width={STROKE[size] ?? 1.75}
|
|
72
|
+
vector-effect="non-scaling-stroke"
|
|
73
|
+
/>
|
|
74
|
+
</svg>
|
|
75
|
+
</IconButton>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CloseButton — the one ✕ every dialog, drawer, sheet, toast and lightbox
|
|
3
|
+
* needs. An IconButton with a built-in X glyph whose stroke is a true CSS
|
|
4
|
+
* pixel width at every size (`vector-effect: non-scaling-stroke`), so the
|
|
5
|
+
* cross never turns hairline at xs or chunky at lg.
|
|
6
|
+
*
|
|
7
|
+
* <CloseButton onclick={close} />
|
|
8
|
+
* <CloseButton label="Close drawer" size="md" onclick={close} />
|
|
9
|
+
* <CloseButton variant="overlay" onclick={close} /> <!-- over media -->
|
|
10
|
+
*
|
|
11
|
+
* Idea from BoardUI's close-button (MIT); written fresh for Svelte 5.
|
|
12
|
+
*/
|
|
13
|
+
import { type IconButtonSize, type IconButtonVariant } from './IconButton.svelte';
|
|
14
|
+
interface Props {
|
|
15
|
+
/** Accessible name. Default "Close" — name the thing ("Close drawer") when several are on screen. */
|
|
16
|
+
label?: string;
|
|
17
|
+
size?: IconButtonSize;
|
|
18
|
+
variant?: IconButtonVariant;
|
|
19
|
+
shape?: 'square' | 'circle';
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
/** Show the label as a tooltip (string = custom text, e.g. "Close (Esc)"). */
|
|
22
|
+
tooltip?: boolean | string;
|
|
23
|
+
class?: string;
|
|
24
|
+
onclick?: (e: MouseEvent) => void;
|
|
25
|
+
[key: string]: unknown;
|
|
26
|
+
}
|
|
27
|
+
declare const CloseButton: import("svelte").Component<Props, {}, "">;
|
|
28
|
+
type CloseButton = ReturnType<typeof CloseButton>;
|
|
29
|
+
export default CloseButton;
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* class (e.g. `'w-[42rem]'`) which takes precedence over `size`.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import CloseButton from './CloseButton.svelte';
|
|
12
|
+
import { scrollEdge as scrollEdgeAction } from '../actions/scrollEdge';
|
|
11
13
|
import { untrack, type Snippet } from 'svelte';
|
|
12
14
|
import type { ShortcutRegistry } from '../utils/keyboardShortcutRegistry';
|
|
13
15
|
|
|
@@ -22,6 +24,8 @@
|
|
|
22
24
|
width?: string;
|
|
23
25
|
showCloseButton?: boolean;
|
|
24
26
|
contentClass?: string;
|
|
27
|
+
/** Soft progressive-blur fade at the top of the body once it scrolls (`use:scrollEdge`). Default true. */
|
|
28
|
+
scrollEdge?: boolean;
|
|
25
29
|
/**
|
|
26
30
|
* When true (default), keydown events fired inside the drawer panel are
|
|
27
31
|
* stopped from propagating to window — preventing page-level list/keyboard
|
|
@@ -59,6 +63,7 @@
|
|
|
59
63
|
width,
|
|
60
64
|
showCloseButton = true,
|
|
61
65
|
contentClass = '',
|
|
66
|
+
scrollEdge = true,
|
|
62
67
|
trapKeys = true,
|
|
63
68
|
shortcutRegistry,
|
|
64
69
|
shortcutContext = 'drawer',
|
|
@@ -150,22 +155,16 @@
|
|
|
150
155
|
{@render headerActions()}
|
|
151
156
|
{/if}
|
|
152
157
|
{#if showCloseButton}
|
|
153
|
-
<
|
|
154
|
-
type="button"
|
|
155
|
-
class="btn btn-sm btn-circle btn-ghost"
|
|
156
|
-
onclick={handleClose}
|
|
157
|
-
aria-label="Close drawer"
|
|
158
|
-
>
|
|
159
|
-
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
160
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
161
|
-
</svg>
|
|
162
|
-
</button>
|
|
158
|
+
<CloseButton label="Close drawer" onclick={handleClose} />
|
|
163
159
|
{/if}
|
|
164
160
|
</div>
|
|
165
161
|
</div>
|
|
166
162
|
{/if}
|
|
167
163
|
|
|
168
|
-
<div
|
|
164
|
+
<div
|
|
165
|
+
class="flex-1 overflow-y-auto {contentClass}"
|
|
166
|
+
use:scrollEdgeAction={{ enabled: scrollEdge, surface: 'var(--color-base-200)' }}
|
|
167
|
+
>
|
|
169
168
|
{#if open && children}
|
|
170
169
|
{@render children()}
|
|
171
170
|
{/if}
|
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Drawer Component
|
|
3
|
-
*
|
|
4
|
-
* Sliding panel overlay with backdrop, escape key, and header actions.
|
|
5
|
-
* Supports a `footer` snippet (pinned below the scrollable body —
|
|
6
|
-
* matches mardukos DrawerShell) and an arbitrary Tailwind `width`
|
|
7
|
-
* class (e.g. `'w-[42rem]'`) which takes precedence over `size`.
|
|
8
|
-
*/
|
|
9
1
|
import { type Snippet } from 'svelte';
|
|
10
2
|
import type { ShortcutRegistry } from '../utils/keyboardShortcutRegistry';
|
|
11
3
|
interface Props {
|
|
@@ -19,6 +11,8 @@ interface Props {
|
|
|
19
11
|
width?: string;
|
|
20
12
|
showCloseButton?: boolean;
|
|
21
13
|
contentClass?: string;
|
|
14
|
+
/** Soft progressive-blur fade at the top of the body once it scrolls (`use:scrollEdge`). Default true. */
|
|
15
|
+
scrollEdge?: boolean;
|
|
22
16
|
/**
|
|
23
17
|
* When true (default), keydown events fired inside the drawer panel are
|
|
24
18
|
* stopped from propagating to window — preventing page-level list/keyboard
|