Reference-library parity (Radix Themes, Chakra v3): - size: 'xs' | 'sm' | 'md' | 'lg' (Chakra t-shirt scale) → thickness via --scroll-area-scrollbar-size. - radius: 'none' | 'sm' | 'md' | 'lg' | 'full' (Radix Themes) → thumb curvature via --scroll-area-thumb-radius. - scrollbars: 'vertical' | 'horizontal' | 'both' (Radix Themes) → recipe-level sugar that auto-mounts Scrollbar / Thumb / Corner from the root. Explicit composition still supported (omit the prop). Bug fix — type='scroll' + horizontal sometimes stuck hidden: - `mounted` on ScrollAreaScrollbarProvider is now $state. Previously a plain field, so the `shouldShow` derived didn't invalidate when the requestFrame mount-flag flipped during the first frame. - The reveal-on-scroll $effect now skips its first run via a `firstRun` flag. That first run was the subscription pass with scrollTop/scrollLeft still 0 — it was eagerly calling show() and setting `visible=true` before `mounted` flipped. Subsequent real scrolls then assigned visible=true again (no-op), so shouldShow didn't re-run and the bar stayed hidden until the hide timer expired. Now the bar only reveals on genuine scroll deltas. Demo: chip controls for size + radius, switched to scrollbars prop on the root (drops the explicit Scrollbar/Thumb children), eidos snippet shows the recipe-sugar form, soma snippet still shows the explicit Radix-style composition. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>active-uix
parent
560ce8f66c
commit
e11d3539ef
@ -1,23 +1,81 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
/**
|
/**
|
||||||
* Eidos `<ScrollArea>` — root component. Wraps Soma's
|
* Eidos `<ScrollArea>` — root component. Wraps Soma's
|
||||||
* `<ScrollArea.Provider>` to set up the headless context; children
|
* `<ScrollArea.Provider>` to set up the headless context.
|
||||||
* compose via the named members attached in `index.ts`:
|
|
||||||
*
|
*
|
||||||
* <ScrollArea type="hover">
|
* <ScrollArea type="hover" size="md" radius="md" scrollbars="vertical">
|
||||||
* <ScrollArea.Viewport>{content}</ScrollArea.Viewport>
|
* <ScrollArea.Viewport>{content}</ScrollArea.Viewport>
|
||||||
* <ScrollArea.Scrollbar orientation="vertical">
|
|
||||||
* <ScrollArea.Thumb />
|
|
||||||
* </ScrollArea.Scrollbar>
|
|
||||||
* <ScrollArea.Corner />
|
|
||||||
* </ScrollArea>
|
* </ScrollArea>
|
||||||
|
*
|
||||||
|
* Two composition modes:
|
||||||
|
*
|
||||||
|
* - Explicit (default, Radix-style) — `scrollbars` omitted; user
|
||||||
|
* composes Viewport + Scrollbar + Thumb + Corner manually.
|
||||||
|
* - Sugar (Radix-Themes style) — `scrollbars="vertical|horizontal|both"`
|
||||||
|
* auto-mounts the right parts; user only composes Viewport.
|
||||||
|
*
|
||||||
|
* `size` / `radius` are pure CSS-variable bindings; they affect
|
||||||
|
* recipe paint only, not Soma's scroll math.
|
||||||
*/
|
*/
|
||||||
import * as ScrollArea from '$soma/components/scroll-area';
|
import * as ScrollArea from '$soma/components/scroll-area';
|
||||||
import type { ScrollAreaProps } from './types';
|
import Viewport from './scroll-area-viewport.svelte';
|
||||||
|
import Scrollbar from './scroll-area-scrollbar.svelte';
|
||||||
|
import Thumb from './scroll-area-thumb.svelte';
|
||||||
|
import Corner from './scroll-area-corner.svelte';
|
||||||
|
import type { ScrollAreaProps, ScrollAreaSize, ScrollAreaRadius } from './types';
|
||||||
|
|
||||||
|
let {
|
||||||
|
children,
|
||||||
|
size,
|
||||||
|
radius,
|
||||||
|
scrollbars,
|
||||||
|
style,
|
||||||
|
...rest
|
||||||
|
}: ScrollAreaProps = $props();
|
||||||
|
|
||||||
|
// Map t-shirt size → `--space-*` token. Matches Chakra's xs/sm/md/lg
|
||||||
|
// scale. The CSS recipe consumes `--scroll-area-scrollbar-size` for
|
||||||
|
// both track thickness and corner dimensions.
|
||||||
|
const SIZE_TOKEN: Record<ScrollAreaSize, string> = {
|
||||||
|
xs: 'var(--space-1-5)',
|
||||||
|
sm: 'var(--space-2)',
|
||||||
|
md: 'var(--space-2-5)',
|
||||||
|
lg: 'var(--space-3)'
|
||||||
|
};
|
||||||
|
|
||||||
|
const RADIUS_TOKEN: Record<ScrollAreaRadius, string> = {
|
||||||
|
none: '0',
|
||||||
|
sm: 'var(--radius-sm)',
|
||||||
|
md: 'var(--radius-md)',
|
||||||
|
lg: 'var(--radius-lg)',
|
||||||
|
full: 'var(--radius-full)'
|
||||||
|
};
|
||||||
|
|
||||||
|
const recipeStyle = $derived(() => {
|
||||||
|
const parts: string[] = [];
|
||||||
|
if (size) parts.push(`--scroll-area-scrollbar-size: ${SIZE_TOKEN[size]};`);
|
||||||
|
if (radius) parts.push(`--scroll-area-thumb-radius: ${RADIUS_TOKEN[radius]};`);
|
||||||
|
return parts.join(' ');
|
||||||
|
});
|
||||||
|
|
||||||
let { children, ...rest }: ScrollAreaProps = $props();
|
const mergedStyle = $derived(
|
||||||
|
[recipeStyle(), typeof style === 'string' ? style : ''].filter(Boolean).join(' ')
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ScrollArea.Provider {...rest}>
|
<ScrollArea.Provider {...rest} style={mergedStyle || undefined}>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
|
{#if scrollbars === 'vertical' || scrollbars === 'both'}
|
||||||
|
<Scrollbar orientation="vertical">
|
||||||
|
<Thumb />
|
||||||
|
</Scrollbar>
|
||||||
|
{/if}
|
||||||
|
{#if scrollbars === 'horizontal' || scrollbars === 'both'}
|
||||||
|
<Scrollbar orientation="horizontal">
|
||||||
|
<Thumb />
|
||||||
|
</Scrollbar>
|
||||||
|
{/if}
|
||||||
|
{#if scrollbars === 'both'}
|
||||||
|
<Corner />
|
||||||
|
{/if}
|
||||||
</ScrollArea.Provider>
|
</ScrollArea.Provider>
|
||||||
|
|||||||
Loading…
Reference in new issue