refactor: migrate to svelte 5, vite 6 and bits-ui 1.

This commit is contained in:
Shibo Lyu 2025-03-17 01:10:15 +08:00
parent 0bb201636a
commit 1e95dc0830
45 changed files with 1069 additions and 793 deletions

View file

@ -16,32 +16,32 @@
"packageManager": "pnpm@9.14.4", "packageManager": "pnpm@9.14.4",
"devDependencies": { "devDependencies": {
"@melt-ui/pp": "^0.3.2", "@melt-ui/pp": "^0.3.2",
"@sveltejs/adapter-auto": "^3.3.1", "@sveltejs/adapter-auto": "^4.0.0",
"@sveltejs/kit": "^2.9.0", "@sveltejs/kit": "^2.19.2",
"@sveltejs/vite-plugin-svelte": "^3.1.2", "@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tailwindcss/postcss": "^4.0.14", "@tailwindcss/postcss": "^4.0.14",
"@tailwindcss/typography": "^0.5.15", "@tailwindcss/typography": "^0.5.16",
"@types/eslint": "^9.6.1", "@types/eslint": "^9.6.1",
"eslint": "^9.22.0", "eslint": "^9.22.0",
"eslint-config-prettier": "^10.1.1", "eslint-config-prettier": "^10.1.1",
"eslint-plugin-svelte": "^2.46.1", "eslint-plugin-svelte": "^3.1.0",
"globals": "^16.0.0", "globals": "^16.0.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"prettier-plugin-svelte": "^3.3.3", "prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.11", "prettier-plugin-tailwindcss": "^0.6.11",
"svelte": "^4.2.19", "svelte": "^5.0.0",
"svelte-check": "^4.1.5", "svelte-check": "^4.1.5",
"tailwindcss": "^4.0.14", "tailwindcss": "^4.0.14",
"typescript": "^5.8.2", "typescript": "^5.8.2",
"typescript-eslint": "^8.26.1", "typescript-eslint": "^8.26.1",
"vite": "^5.1.8", "vite": "^6.2.2",
"vitest": "^2.1.8" "vitest": "^3.0.8"
}, },
"dependencies": { "dependencies": {
"@blah-im/core": "^0.4.2", "@blah-im/core": "^0.4.2",
"@melt-ui/svelte": "^0.86.4", "@melt-ui/svelte": "^0.86.4",
"@zeabur/svelte-adapter": "^1.0.0", "@zeabur/svelte-adapter": "^1.0.0",
"bits-ui": "^0.21.16", "bits-ui": "^1.3.12",
"canonicalize": "^2.0.0", "canonicalize": "^2.0.0",
"idb": "^8.0.2", "idb": "^8.0.2",
"svelte-boring-avatars": "^1.2.6", "svelte-boring-avatars": "^1.2.6",
@ -49,7 +49,7 @@
"svelte-persisted-store": "^0.12.0", "svelte-persisted-store": "^0.12.0",
"tailwind-merge": "^3.0.2", "tailwind-merge": "^3.0.2",
"typewriter-editor": "^0.12.9", "typewriter-editor": "^0.12.9",
"virtua": "^0.35.1", "virtua": "^0.40.3",
"zod": "^3.24.2" "zod": "^3.24.2"
} }
} }

1099
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -45,8 +45,7 @@
@apply [&_span[data-weblah-brt=underline]]:underline; @apply [&_span[data-weblah-brt=underline]]:underline;
} }
@layer base { @utility weblah-light-theme {
.weblah-light-theme {
--weblah-color-sb-overlay: var(--color-white); --weblah-color-sb-overlay: var(--color-white);
--weblah-color-sb-primary: var(--color-slate-50); --weblah-color-sb-primary: var(--color-slate-50);
--weblah-color-sb-secondary: var(--color-slate-100); --weblah-color-sb-secondary: var(--color-slate-100);
@ -60,7 +59,7 @@
--weblah-color-ss-secondary: --theme(--color-slate-300 / 60%); --weblah-color-ss-secondary: --theme(--color-slate-300 / 60%);
} }
.weblah-dark-theme { @utility weblah-dark-theme {
--weblah-color-sb-overlay: var(--color-slate-800); --weblah-color-sb-overlay: var(--color-slate-800);
--weblah-color-sb-primary: var(--color-slate-900); --weblah-color-sb-primary: var(--color-slate-900);
--weblah-color-sb-secondary: var(--color-slate-950); --weblah-color-sb-secondary: var(--color-slate-950);
@ -74,6 +73,7 @@
--weblah-color-ss-secondary: --theme(--color-slate-700 / 60%); --weblah-color-ss-secondary: --theme(--color-slate-700 / 60%);
} }
@layer base {
:root { :root {
@apply weblah-light-theme; @apply weblah-light-theme;
} }

View file

@ -2,10 +2,15 @@
import { tw } from '$lib/tw'; import { tw } from '$lib/tw';
import { patterns, type PatternName } from './BgPattern'; import { patterns, type PatternName } from './BgPattern';
export let pattern: PatternName = 'rain';
let className: string = ''; interface Props {
export { className as class }; pattern?: PatternName;
class?: string;
children?: import('svelte').Snippet;
}
let { pattern = 'rain', class: className = '', children }: Props = $props();
</script> </script>
<div <div
@ -15,5 +20,5 @@
)} )}
style:--pattern-image={`url("${patterns[pattern]}")`} style:--pattern-image={`url("${patterns[pattern]}")`}
> >
<slot /> {@render children?.()}
</div> </div>

View file

@ -1,42 +1,44 @@
<script lang="ts"> <script lang="ts">
import { tw } from '$lib/tw'; import { tw } from '$lib/tw';
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
type HTMLButtonOrAnchorAttributes = Partial<HTMLAnchorAttributes> & Partial<HTMLButtonAttributes>; interface Props {
interface $$Props extends HTMLButtonOrAnchorAttributes {
variant?: 'primary' | 'secondary'; variant?: 'primary' | 'secondary';
class?: string | null;
href?: string | null;
children?: import('svelte').Snippet;
[key: string]: unknown;
} }
export let variant: $$Props['variant'] = 'secondary'; let {
let className: string | null = ''; variant = 'secondary',
export { className as class }; class: className = '',
href = null,
export let href: string | null = null; children,
...rest
}: Props = $props();
</script> </script>
<svelte:element <svelte:element
this={href ? 'a' : 'button'} this={href ? 'a' : 'button'}
{href} {href}
class={tw( class={tw(
'inline-flex cursor-default items-center justify-center rounded-md px-2 py-1 text-sf-secondary shadow-xs ring-1 ring-ss-secondary', 'text-sf-secondary ring-ss-secondary inline-flex cursor-default items-center justify-center rounded-md px-2 py-1 shadow-xs ring-1',
'font-normal transition-shadow duration-200 hover:ring-ss-primary active:shadow-inner', 'hover:ring-ss-primary font-normal transition-shadow duration-200 active:shadow-inner',
variant === 'primary' && [ variant === 'primary' && [
'relative text-slate-50 ring-0 duration-200', 'relative text-slate-50 ring-0 duration-200',
'before:absolute before:-inset-px before:rounded-[7px]', 'before:absolute before:-inset-px before:rounded-[7px]',
'before:bg-linear-to-b before:from-accent-400 before:from-40% before:to-accent-500 before:ring-1 before:ring-inset before:ring-black/10', 'before:from-accent-400 before:to-accent-500 before:bg-linear-to-b before:from-40% before:ring-1 before:ring-black/10 before:ring-inset',
'before:transition-shadow active:before:shadow-inner dark:before:from-accent-500 dark:before:to-accent-600' 'dark:before:from-accent-500 dark:before:to-accent-600 before:transition-shadow active:before:shadow-inner'
], ],
className className
)} )}
{...$$restProps}
on:click
role="button" role="button"
tabindex="0" tabindex="0"
{...rest}
> >
{#if variant === 'primary'} {#if variant === 'primary'}
<div class="z-10 drop-shadow-[0_-1px_0_--theme(--color-black/0.2)]"><slot /></div> <div class="z-10 drop-shadow-[0_-1px_0_--theme(--color-black/0.2)]">{@render children?.()}</div>
{:else} {:else}
<slot /> {@render children?.()}
{/if} {/if}
</svelte:element> </svelte:element>

View file

@ -4,9 +4,15 @@
import { expoOut } from 'svelte/easing'; import { expoOut } from 'svelte/easing';
import { scale } from 'svelte/transition'; import { scale } from 'svelte/transition';
interface $$Props extends DropdownMenuContentProps {}
let className: $$Props['class'] = ''; interface Props {
export { className as class }; class?: $$Props['class'];
children?: import('svelte').Snippet;
[key: string]: any
}
let { class: className = '', children, ...rest }: Props = $props();
</script> </script>
<DropdownMenu.Content <DropdownMenu.Content
@ -17,7 +23,7 @@
sideOffset={4} sideOffset={4}
transition={scale} transition={scale}
transitionConfig={{ start: 0.96, duration: 300, easing: expoOut }} transitionConfig={{ start: 0.96, duration: 300, easing: expoOut }}
{...$$restProps} {...rest}
> >
<slot /> {@render children?.()}
</DropdownMenu.Content> </DropdownMenu.Content>

View file

@ -1,13 +1,15 @@
<script lang="ts"> <script lang="ts">
import { DropdownMenu, type DropdownMenuItemProps } from 'bits-ui'; import { DropdownMenu, type DropdownMenuItemProps } from 'bits-ui';
interface Props extends DropdownMenuItemProps {
children?: import('svelte').Snippet;
}
type $$Props = DropdownMenuItemProps; let { children, ...rest }: Props = $props();
</script> </script>
<DropdownMenu.Item <DropdownMenu.Item
class="cursor-default rounded-sm px-1.5 py-0.5 text-sf-primary transition-colors duration-200 hover:bg-accent-50 group-has-data-melt-dropdown-menu-radio-group:ps-6 dark:hover:bg-white/5" class="text-sf-primary hover:bg-accent-50 cursor-default rounded-sm px-1.5 py-0.5 transition-colors duration-200 group-has-data-melt-dropdown-menu-radio-group:ps-6 dark:hover:bg-white/5"
on:click {...rest}
{...$$restProps}
> >
<slot /> {@render children?.()}
</DropdownMenu.Item> </DropdownMenu.Item>

View file

@ -3,17 +3,21 @@
import { DropdownMenu } from 'bits-ui'; import { DropdownMenu } from 'bits-ui';
import { Check, Icon } from 'svelte-hero-icons'; import { Check, Icon } from 'svelte-hero-icons';
type $$Props = DropdownMenuRadioItemProps; interface Props extends DropdownMenuRadioItemProps {
export let value: string; children?: import('svelte').Snippet;
}
let { children: componentChildren, ...props }: Props = $props();
</script> </script>
<DropdownMenu.RadioItem <DropdownMenu.RadioItem
class="flex cursor-default items-center gap-1 rounded-sm px-1.5 py-0.5 text-sf-primary transition-colors duration-200 hover:bg-accent-50 dark:hover:bg-white/5" class="text-sf-primary hover:bg-accent-50 flex cursor-default items-center gap-1 rounded-sm px-1.5 py-0.5 transition-colors duration-200 dark:hover:bg-white/5"
{value} {...props}
{...$$props}
> >
<DropdownMenu.RadioIndicator class="relative size-4"> {#snippet children(itemProps)}
{#if itemProps.checked}
<Icon src={Check} class="size-full" micro /> <Icon src={Check} class="size-full" micro />
</DropdownMenu.RadioIndicator> {/if}
<slot /> {@render componentChildren?.()}
{/snippet}
</DropdownMenu.RadioItem> </DropdownMenu.RadioItem>

View file

@ -1,8 +1,14 @@
<script lang="ts"> <script lang="ts">
import { DropdownMenu, type DropdownMenuTriggerProps } from 'bits-ui'; import { DropdownMenu, type DropdownMenuTriggerProps } from 'bits-ui';
interface Props {
children?: import('svelte').Snippet;
[key: string]: any
}
let { children, ...rest }: Props = $props();
type $$Props = DropdownMenuTriggerProps; type $$Props = DropdownMenuTriggerProps;
</script> </script>
<DropdownMenu.Trigger class="cursor-default" {...$$restProps}> <DropdownMenu.Trigger class="cursor-default" {...rest}>
<slot /> {@render children?.()}
</DropdownMenu.Trigger> </DropdownMenu.Trigger>

View file

@ -1 +1,9 @@
<div class="mx-auto max-w-3xl"><slot /></div> <script lang="ts">
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
</script>
<div class="mx-auto max-w-3xl">{@render children?.()}</div>

View file

@ -1,5 +1,13 @@
<script lang="ts">
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
</script>
<label <label
class="flex gap-2 px-4 py-3 font-medium text-sf-primary [align-items:first_baseline] [&>input]:flex-1 [&>input]:bg-transparent [&>input]:text-end [&>input]:outline-hidden [&>input]:placeholder:opacity-50" class="flex gap-2 px-4 py-3 font-medium text-sf-primary [align-items:first_baseline] [&>input]:flex-1 [&>input]:bg-transparent [&>input]:text-end [&>input]:outline-hidden [&>input]:placeholder:opacity-50"
> >
<slot /> {@render children?.()}
</label> </label>

View file

@ -1,10 +1,23 @@
<script lang="ts"> <script lang="ts">
import { createBubbler } from 'svelte/legacy';
const bubble = createBubbler();
import { tw } from '$lib/tw'; import { tw } from '$lib/tw';
import { Icon, type IconSource } from 'svelte-hero-icons'; import { Icon, type IconSource } from 'svelte-hero-icons';
export let href: string | undefined = undefined; interface Props {
export let icon: IconSource | undefined = undefined; href?: string | undefined;
export let selected: boolean = false; icon?: IconSource | undefined;
selected?: boolean;
children?: import('svelte').Snippet;
}
let {
href = undefined,
icon = undefined,
selected = false,
children
}: Props = $props();
</script> </script>
<svelte:element <svelte:element
@ -16,7 +29,7 @@
)} )}
tabindex="0" tabindex="0"
role="button" role="button"
on:click onclick={bubble('click')}
> >
{#if icon} {#if icon}
<Icon <Icon
@ -25,5 +38,5 @@
mini mini
/> />
{/if} {/if}
<slot /> {@render children?.()}
</svelte:element> </svelte:element>

View file

@ -1,17 +1,27 @@
<script lang="ts">
interface Props {
header?: import('svelte').Snippet;
children?: import('svelte').Snippet;
footer?: import('svelte').Snippet;
}
let { header, children, footer }: Props = $props();
</script>
<section class="my-6 cursor-default px-4"> <section class="my-6 cursor-default px-4">
{#if $$slots.header} {#if header}
<h3 class="mb-1 truncate px-4 text-sm font-medium uppercase text-sf-tertiary"> <h3 class="mb-1 truncate px-4 text-sm font-medium uppercase text-sf-tertiary">
<slot name="header" /> {@render header?.()}
</h3> </h3>
{/if} {/if}
<div <div
class="divide-y-[0.5px] divide-ss-secondary overflow-hidden rounded-lg border-[0.5px] border-ss-secondary bg-sb-primary shadow-xs" class="divide-y-[0.5px] divide-ss-secondary overflow-hidden rounded-lg border-[0.5px] border-ss-secondary bg-sb-primary shadow-xs"
> >
<slot /> {@render children?.()}
</div> </div>
{#if $$slots.footer} {#if footer}
<div class="mt-1 px-4 text-sm text-sf-tertiary"> <div class="mt-1 px-4 text-sm text-sf-tertiary">
<slot name="footer" /> {@render footer?.()}
</div> </div>
{/if} {/if}
</section> </section>

View file

@ -1,8 +1,13 @@
<script lang="ts"> <script lang="ts">
import { tw } from '$lib/tw'; import { tw } from '$lib/tw';
let className = ''; interface Props {
export { className as class }; class?: string;
children?: import('svelte').Snippet;
}
let { class: className = '', children }: Props = $props();
</script> </script>
<label <label
@ -11,5 +16,5 @@
className className
)} )}
> >
<slot /> {@render children?.()}
</label> </label>

View file

@ -1,8 +1,13 @@
<script lang="ts"> <script lang="ts">
import { tw } from '$lib/tw'; import { tw } from '$lib/tw';
export let href: string; interface Props {
export let variant: 'primary' | 'secondary' = 'primary'; href: string;
variant?: 'primary' | 'secondary';
children?: import('svelte').Snippet;
}
let { href, variant = 'primary', children }: Props = $props();
</script> </script>
<a <a
@ -12,5 +17,5 @@
variant === 'primary' variant === 'primary'
? 'text-accent-600 dark:text-accent-500' ? 'text-accent-600 dark:text-accent-500'
: 'text-accent-400 dark:text-accent-500' : 'text-accent-400 dark:text-accent-500'
)}><slot /></a )}>{@render children?.()}</a
> >

View file

@ -1,8 +1,12 @@
<script lang="ts"> <script lang="ts">
import { tw } from '$lib/tw'; import { tw } from '$lib/tw';
let className = ''; interface Props {
export { className as class }; class?: string;
}
let { class: className = '' }: Props = $props();
</script> </script>
<div class={tw('loading-indicator relative inline-block size-5 [&>div]:bg-sf-tertiary', className)}> <div class={tw('loading-indicator relative inline-block size-5 [&>div]:bg-sf-tertiary', className)}>

View file

@ -1,8 +1,13 @@
<script lang="ts"> <script lang="ts">
import { tw } from '$lib/tw'; import { tw } from '$lib/tw';
let className = ''; interface Props {
export { className as class }; class?: string;
children?: import('svelte').Snippet;
}
let { class: className = '', children }: Props = $props();
</script> </script>
<header <header
@ -11,5 +16,5 @@
className className
)} )}
> >
<slot /> {@render children?.()}
</header> </header>

View file

@ -2,8 +2,12 @@
import type { Account } from '$lib/accounts/accountStore'; import type { Account } from '$lib/accounts/accountStore';
import { AvatarBeam } from 'svelte-boring-avatars'; import { AvatarBeam } from 'svelte-boring-avatars';
export let account: Account | undefined; interface Props {
export let size: number = 32; account: Account | undefined;
size?: number;
}
let { account, size = 32 }: Props = $props();
</script> </script>
{#if account} {#if account}
@ -13,9 +17,9 @@
<span class="sr-only">{account.profile.signee.payload.name}</span> <span class="sr-only">{account.profile.signee.payload.name}</span>
{:else} {:else}
<div <div
class="box-border size-(--weblah-profile-pic-size) rounded-full border-2 border-dashed border-ss-primary" class="border-ss-primary box-border size-(--weblah-profile-pic-size) rounded-full border-2 border-dashed"
style:--weblah-profile-pic-size={`${size}px`} style:--weblah-profile-pic-size={`${size}px`}
aria-hidden aria-hidden="true"
/> ></div>
<span class="sr-only">Account Unavailable</span> <span class="sr-only">Account Unavailable</span>
{/if} {/if}

View file

@ -4,14 +4,27 @@
import InputFrame from '$lib/components/InputFrame.svelte'; import InputFrame from '$lib/components/InputFrame.svelte';
import { tw } from '$lib/tw'; import { tw } from '$lib/tw';
export let delta: Delta | null = null;
export let plainText: string | undefined = undefined;
export let keyboardSubmitMethod: 'enter' | 'shiftEnter' | undefined = undefined;
export let placeholder: string = '';
export let editor: Editor | undefined;
let className = ''; interface Props {
export { className as class }; delta?: Delta | null;
plainText?: string | undefined;
keyboardSubmitMethod?: 'enter' | 'shiftEnter' | undefined;
placeholder?: string;
editor: Editor | undefined;
class?: string;
children?: import('svelte').Snippet;
}
let {
delta = $bindable(null),
plainText = $bindable(undefined),
keyboardSubmitMethod = undefined,
placeholder = '',
editor = $bindable(),
class: className = '',
children
}: Props = $props();
const loadClientComponent = async () => { const loadClientComponent = async () => {
if (!browser) return; if (!browser) return;
@ -26,8 +39,7 @@
<p>{placeholder}</p> <p>{placeholder}</p>
</div> </div>
{:then Input} {:then Input}
<svelte:component <Input
this={Input}
bind:delta bind:delta
bind:plainText bind:plainText
{placeholder} {placeholder}
@ -35,7 +47,7 @@
{keyboardSubmitMethod} {keyboardSubmitMethod}
on:keyboardSubmit on:keyboardSubmit
> >
<slot /> {@render children?.()}
</svelte:component> </Input>
{/await} {/await}
</InputFrame> </InputFrame>

View file

@ -1,24 +1,37 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte';
import { Delta, Editor, asRoot, h } from 'typewriter-editor'; import { Delta, Editor, asRoot, h } from 'typewriter-editor';
import { keyboardSubmit } from './keyboardSubmitModule'; import { keyboardSubmit } from './keyboardSubmitModule';
export let delta: Delta = new Delta(); interface Props {
export let plainText: string | undefined = undefined; delta?: Delta;
export let placeholder: string = ''; plainText?: string | undefined;
export let keyboardSubmitMethod: 'enter' | 'shiftEnter' | undefined = undefined; placeholder?: string;
keyboardSubmitMethod?: 'enter' | 'shiftEnter' | undefined;
onKeyboardSubmit?: () => void;
children?: import('svelte').Snippet;
}
const dispatch = createEventDispatcher<{ let {
keyboardSubmit: void; delta = $bindable(new Delta()),
}>(); plainText = $bindable(undefined),
placeholder = '',
keyboardSubmitMethod = undefined,
onKeyboardSubmit,
children
}: Props = $props();
let editor: Editor; let editor: Editor = $state(initEditor());
function initEditor() { function initEditor() {
const modules = keyboardSubmitMethod const modules = keyboardSubmitMethod
? { keyboardSubmit: keyboardSubmit(() => dispatch('keyboardSubmit'), keyboardSubmitMethod) } ? {
keyboardSubmit: keyboardSubmit(
() => onKeyboardSubmit && onKeyboardSubmit(),
keyboardSubmitMethod
)
}
: undefined; : undefined;
editor = new Editor({ modules }); const editor = new Editor({ modules });
editor.typeset.formats.add({ editor.typeset.formats.add({
name: 'underline', name: 'underline',
selector: 'span[data-weblah-brt=underline]', selector: 'span[data-weblah-brt=underline]',
@ -41,12 +54,20 @@
delta = editor.getDelta(); delta = editor.getDelta();
if (typeof plainText === 'string') plainText = editor.getText(); if (typeof plainText === 'string') plainText = editor.getText();
}); });
return editor;
} }
$: if (keyboardSubmitMethod || typeof keyboardSubmitMethod === 'undefined') initEditor(); $effect.pre(() => {
if (keyboardSubmitMethod || typeof keyboardSubmitMethod === 'undefined') editor = initEditor();
});
$: editor.setDelta(delta ?? new Delta()); $effect.pre(() => {
$: if (typeof plainText === 'string' && plainText !== editor.getText()) editor.setText(plainText); editor.setDelta(delta ?? new Delta());
});
$effect.pre(() => {
if (typeof plainText === 'string' && plainText !== editor.getText()) editor.setText(plainText);
});
</script> </script>
<div <div
@ -59,5 +80,5 @@
role="textbox" role="textbox"
tabindex="0" tabindex="0"
> >
<slot /> {@render children?.()}
</div> </div>

View file

@ -4,9 +4,13 @@
import RichTextSpan from './RichTextRenderer/RichTextSpan.svelte'; import RichTextSpan from './RichTextRenderer/RichTextSpan.svelte';
import PlainTextRenderer from './RichTextRenderer/PlainTextRenderer.svelte'; import PlainTextRenderer from './RichTextRenderer/PlainTextRenderer.svelte';
export let content: BlahRichText; interface Props {
let className = ''; content: BlahRichText;
export { className as class }; class?: string;
}
let { content, class: className = '' }: Props = $props();
</script> </script>
<div class={tw('rich-text', className)}> <div class={tw('rich-text', className)}>

View file

@ -1,5 +1,9 @@
<script lang="ts"> <script lang="ts">
export let text: string; interface Props {
text: string;
}
let { text }: Props = $props();
</script> </script>
{#each text.split('\n') as segment, idx} {#each text.split('\n') as segment, idx}

View file

@ -1,3 +1,4 @@
<!-- @migration-task Error while migrating Svelte code: $$props is used together with named props in a way that cannot be automatically migrated. -->
<script lang="ts"> <script lang="ts">
import type { BlahRichTextSpanAttributes } from '$lib/richText'; import type { BlahRichTextSpanAttributes } from '$lib/richText';
import PlainTextRenderer from './PlainTextRenderer.svelte'; import PlainTextRenderer from './PlainTextRenderer.svelte';

View file

@ -1,8 +1,13 @@
<script lang="ts"> <script lang="ts">
import { tw } from '$lib/tw'; import { tw } from '$lib/tw';
let className: string = ''; interface Props {
export { className as class }; class?: string;
children?: import('svelte').Snippet;
}
let { class: className = '', children }: Props = $props();
</script> </script>
<div <div
@ -11,5 +16,5 @@
className className
)} )}
> >
<slot /> {@render children?.()}
</div> </div>

View file

@ -1,8 +1,13 @@
<script> <script lang="ts">
import { page } from '$app/stores'; import { page } from '$app/stores';
import { onNavigate } from '$app/navigation'; import { onNavigate } from '$app/navigation';
import ChatList from './ChatList.svelte'; import ChatList from './ChatList.svelte';
import SettingsList from './settings/SettingsList.svelte'; import SettingsList from './settings/SettingsList.svelte';
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
onNavigate((navigation) => { onNavigate((navigation) => {
if (!document.startViewTransition || navigation.from?.url.href === navigation.to?.url.href) if (!document.startViewTransition || navigation.from?.url.href === navigation.to?.url.href)
@ -16,10 +21,10 @@
}); });
}); });
$: isSettings = $page.route.id?.startsWith('/(app)/settings') ?? true; let isSettings = $derived($page.route.id?.startsWith('/(app)/settings') ?? true);
$: mainVisible = let mainVisible =
!!$page.params.chatId || $derived(!!$page.params.chatId ||
(isSettings && !$page.route.id?.startsWith('/(app)/settings/_mobile_empty')); (isSettings && !$page.route.id?.startsWith('/(app)/settings/_mobile_empty')));
</script> </script>
<div <div
@ -38,10 +43,10 @@
<main <main
class="absolute inset-0 w-full bg-sb-secondary shadow-lg [view-transition-name:main] sm:relative sm:flex-1 sm:shadow-none" class="absolute inset-0 w-full bg-sb-secondary shadow-lg [view-transition-name:main] sm:relative sm:flex-1 sm:shadow-none"
> >
<slot></slot> {@render children?.()}
</main> </main>
{:else} {:else}
<div class="hidden flex-1 sm:block"><slot /></div> <div class="hidden flex-1 sm:block">{@render children?.()}</div>
{/if} {/if}
</div> </div>

View file

@ -9,8 +9,8 @@
const chatList = browser ? useChatList(chatServerConnectionPool.chatList) : null; const chatList = browser ? useChatList(chatServerConnectionPool.chatList) : null;
let isSearchFocused: boolean; let isSearchFocused: boolean = $state(false);
let searchQuery: string; let searchQuery: string = $state('');
</script> </script>
<div class="flex h-[100dvh] flex-col justify-stretch"> <div class="flex h-[100dvh] flex-col justify-stretch">
@ -18,14 +18,14 @@
<div class="relative min-h-0 flex-1 touch-pan-y overflow-y-auto"> <div class="relative min-h-0 flex-1 touch-pan-y overflow-y-auto">
<ul> <ul>
{#if $chatList} {#if $chatList}
{#each $chatList as chat} {#each $chatList as chat (chat.id)}
<ChatListItem {chat} /> <ChatListItem {chat} />
{/each} {/each}
{/if} {/if}
</ul> </ul>
{#if isSearchFocused || searchQuery} {#if isSearchFocused || searchQuery}
<div <div
class="absolute inset-0 size-full origin-top touch-pan-y overflow-y-auto bg-sb-primary" class="bg-sb-primary absolute inset-0 size-full origin-top touch-pan-y overflow-y-auto"
transition:scale={{ start: 0.9 }} transition:scale={{ start: 0.9 }}
> >
<SearchPanel {searchQuery} /> <SearchPanel {searchQuery} />

View file

@ -5,10 +5,14 @@
import { tw } from '$lib/tw'; import { tw } from '$lib/tw';
import CurrentAccountPicture from './CurrentAccountPicture.svelte'; import CurrentAccountPicture from './CurrentAccountPicture.svelte';
export let searchQuery: string = ''; interface Props {
export let isSearchFocused: boolean; searchQuery?: string;
isSearchFocused: boolean;
}
let inputElement: HTMLInputElement; let { searchQuery = $bindable(''), isSearchFocused = $bindable() }: Props = $props();
let inputElement: HTMLInputElement = $state();
function onTapClear(e: MouseEvent) { function onTapClear(e: MouseEvent) {
e.preventDefault(); e.preventDefault();
@ -37,10 +41,10 @@
class="w-full flex-1 bg-transparent text-sm leading-4 text-sf-primary focus:outline-hidden" class="w-full flex-1 bg-transparent text-sm leading-4 text-sf-primary focus:outline-hidden"
bind:value={searchQuery} bind:value={searchQuery}
bind:this={inputElement} bind:this={inputElement}
on:focus={() => { onfocus={() => {
isSearchFocused = true; isSearchFocused = true;
}} }}
on:blur={(e) => { onblur={(e) => {
// If the related target is an anchor element, trigger the click as the user is trying to navigate // If the related target is an anchor element, trigger the click as the user is trying to navigate
if ( if (
e.relatedTarget instanceof HTMLAnchorElement || e.relatedTarget instanceof HTMLAnchorElement ||
@ -57,7 +61,7 @@
'-mx-2 -my-1.5 flex size-8 cursor-text items-center justify-center text-slate-300 opacity-0 transition-[opacity,transform] duration-200 dark:text-slate-500', '-mx-2 -my-1.5 flex size-8 cursor-text items-center justify-center text-slate-300 opacity-0 transition-[opacity,transform] duration-200 dark:text-slate-500',
isSearchFocused && 'translate-x-full cursor-default opacity-100' isSearchFocused && 'translate-x-full cursor-default opacity-100'
)} )}
on:click={onTapClear} onclick={onTapClear}
> >
<Icon src={XCircle} class="size-4" mini /> <Icon src={XCircle} class="size-4" mini />
<span class="sr-only">Clear</span> <span class="sr-only">Clear</span>

View file

@ -1,4 +1,6 @@
<script lang="ts"> <script lang="ts">
import { run } from 'svelte/legacy';
import { AvatarBeam } from 'svelte-boring-avatars'; import { AvatarBeam } from 'svelte-boring-avatars';
import { formatMessageDate, formatUnreadCount } from '$lib/formatters'; import { formatMessageDate, formatUnreadCount } from '$lib/formatters';
@ -8,15 +10,19 @@
import { page } from '$app/stores'; import { page } from '$app/stores';
import { tw } from '$lib/tw'; import { tw } from '$lib/tw';
export let chat: Chat; interface Props {
chat: Chat;
let urlSafeEndpoint: string;
$: {
const url = new URL(chat.server);
urlSafeEndpoint = encodeURIComponent(url.hostname + url.pathname);
} }
$: isSelected = $page.params.chatId === chat.id; let { chat }: Props = $props();
let urlSafeEndpoint: string = $state();
run(() => {
const url = new URL(chat.server);
urlSafeEndpoint = encodeURIComponent(url.hostname + url.pathname);
});
let isSelected = $derived($page.params.chatId === chat.id);
</script> </script>
<li <li

View file

@ -7,9 +7,13 @@
import ProfilePicture from '$lib/components/ProfilePicture.svelte'; import ProfilePicture from '$lib/components/ProfilePicture.svelte';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
export let size: number = 32; interface Props {
size?: number;
}
let accountStore: AccountStore; let { size = 32 }: Props = $props();
let accountStore: AccountStore = $state();
onMount(() => { onMount(() => {
openAccountStore().then((store) => { openAccountStore().then((store) => {

View file

@ -3,8 +3,12 @@
import ChatListItem from './ChatListItem.svelte'; import ChatListItem from './ChatListItem.svelte';
export let name: string; interface Props {
export let results: Chat[]; name: string;
results: Chat[];
}
let { name, results }: Props = $props();
</script> </script>
<li> <li>

View file

@ -4,7 +4,11 @@
import ChatListItem from './ChatListItem.svelte'; import ChatListItem from './ChatListItem.svelte';
import SearchChatResultSection from './SearchChatResultSection.svelte'; import SearchChatResultSection from './SearchChatResultSection.svelte';
export let searchQuery: string; interface Props {
searchQuery: string;
}
let { searchQuery }: Props = $props();
async function search(query: string) { async function search(query: string) {
const results = await chatServerConnectionPool.searchManager.searchChats(query); const results = await chatServerConnectionPool.searchManager.searchChats(query);

View file

@ -1,4 +1,6 @@
<script lang="ts"> <script lang="ts">
import { run } from 'svelte/legacy';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { BlahChatServerConnection } from '$lib/blah/connection/chatServer'; import { BlahChatServerConnection } from '$lib/blah/connection/chatServer';
import { browser } from '$app/environment'; import { browser } from '$app/environment';
@ -7,22 +9,22 @@
import ChatPage from './ChatPage.svelte'; import ChatPage from './ChatPage.svelte';
import { useChat } from '$lib/chat'; import { useChat } from '$lib/chat';
$: roomId = $page.params.chatId; let roomId = $derived($page.params.chatId);
let serverEndpoint: string = ''; let serverEndpoint: string = $state('');
$: { run(() => {
const endpointString = decodeURIComponent($page.params.server); const endpointString = decodeURIComponent($page.params.server);
serverEndpoint = endpointString.startsWith('http') serverEndpoint = endpointString.startsWith('http')
? endpointString ? endpointString
: `https://${endpointString}`; : `https://${endpointString}`;
} });
let server: BlahChatServerConnection | null; let server: BlahChatServerConnection | null = $state();
$: { run(() => {
if (browser) { if (browser) {
server = chatServerConnectionPool.getConnection(serverEndpoint); server = chatServerConnectionPool.getConnection(serverEndpoint);
} }
} });
</script> </script>
<div class="flex h-full w-full flex-col items-center justify-center"> <div class="flex h-full w-full flex-col items-center justify-center">

View file

@ -5,8 +5,12 @@
import { AvatarBeam } from 'svelte-boring-avatars'; import { AvatarBeam } from 'svelte-boring-avatars';
import { ChevronLeft, Icon } from 'svelte-hero-icons'; import { ChevronLeft, Icon } from 'svelte-hero-icons';
export let info: Chat; interface Props {
export let outsideUnreadCount = 0; info: Chat;
outsideUnreadCount?: number;
}
let { info, outsideUnreadCount = 0 }: Props = $props();
</script> </script>
<div <div

View file

@ -1,4 +1,6 @@
<script lang="ts"> <script lang="ts">
import { run } from 'svelte/legacy';
import { VList } from 'virtua/svelte'; import { VList } from 'virtua/svelte';
import ChatMessage from './ChatMessage.svelte'; import ChatMessage from './ChatMessage.svelte';
@ -9,20 +11,27 @@
import ServiceMessage from '$lib/components/ServiceMessage.svelte'; import ServiceMessage from '$lib/components/ServiceMessage.svelte';
import { formatMessageSectionDate } from '$lib/formatters'; import { formatMessageSectionDate } from '$lib/formatters';
export let sectionedMessages: MessageSection[] = []; interface Props {
export let mySenderId: string; sectionedMessages?: MessageSection[];
mySenderId: string;
}
let ref: VList<MessageSection> | undefined; let { sectionedMessages = [], mySenderId }: Props = $props();
let ref: VList<MessageSection> | undefined = $state();
async function scrollToIndex(index: number, smooth = true) { async function scrollToIndex(index: number, smooth = true) {
await tick(); await tick();
ref?.scrollToIndex(index, { align: 'end', smooth }); ref?.scrollToIndex(index, { align: 'end', smooth });
} }
$: scrollToIndex(sectionedMessages.length - 1); run(() => {
scrollToIndex(sectionedMessages.length - 1);
});
</script> </script>
<VList data={sectionedMessages} let:item={messageSection} class="size-full pt-2" bind:this={ref}> <VList data={sectionedMessages} class="size-full pt-2" bind:this={ref}>
{#snippet children({ item: messageSection })}
{@const isMyself = mySenderId === messageSection.sender?.id} {@const isMyself = mySenderId === messageSection.sender?.id}
<div> <div>
@ -51,4 +60,5 @@
</div> </div>
</div> </div>
</div> </div>
{/snippet}
</VList> </VList>

View file

@ -5,10 +5,10 @@
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import type { Delta, Editor } from 'typewriter-editor'; import type { Delta, Editor } from 'typewriter-editor';
let editor: Editor | undefined; let editor: Editor | undefined = $state();
let delta: Delta; let delta: Delta | undefined = $state();
let plainText: string = ''; let plainText: string = $state('');
let form: HTMLFormElement | null = null; let form: HTMLFormElement | null = $state(null);
const dispatch = createEventDispatcher<{ sendMessage: BlahRichText }>(); const dispatch = createEventDispatcher<{ sendMessage: BlahRichText }>();
@ -17,8 +17,10 @@
form?.requestSubmit(); form?.requestSubmit();
} }
async function submit() { async function submit(event: SubmitEvent) {
if (plainText.trim() === '') return; event.preventDefault();
if (plainText.trim() === '' || !delta) return;
const brt = deltaToBlahRichText(delta); const brt = deltaToBlahRichText(delta);
dispatch('sendMessage', brt); dispatch('sendMessage', brt);
@ -28,9 +30,9 @@
</script> </script>
<form <form
class="flex w-full items-end gap-2 border-t border-ss-secondary bg-sb-primary p-2 shadow-xs" class="border-ss-secondary bg-sb-primary flex w-full items-end gap-2 border-t p-2 shadow-xs"
bind:this={form} bind:this={form}
on:submit|preventDefault={submit} onsubmit={submit}
> >
<Button class="p-1.5"> <Button class="p-1.5">
<svg <svg

View file

@ -3,9 +3,13 @@
import { tw } from '$lib/tw'; import { tw } from '$lib/tw';
import type { Message } from '$lib/types'; import type { Message } from '$lib/types';
export let message: Message; interface Props {
export let showBubbleTail: boolean = true; message: Message;
export let isMyself: boolean; showBubbleTail?: boolean;
isMyself: boolean;
}
let { message, showBubbleTail = true, isMyself }: Props = $props();
</script> </script>
<div class={tw('mb-1.5 flex items-end gap-2 px-2', isMyself && 'flex-row-reverse')}> <div class={tw('mb-1.5 flex items-end gap-2 px-2', isMyself && 'flex-row-reverse')}>

View file

@ -11,8 +11,12 @@
import type { BlahRichText } from '$lib/richText'; import type { BlahRichText } from '$lib/richText';
import type { MessageSection } from '$lib/chat'; import type { MessageSection } from '$lib/chat';
export let info: Readable<Chat>; interface Props {
export let sectionedMessages: Readable<MessageSection[]>; info: Readable<Chat>;
sectionedMessages: Readable<MessageSection[]>;
}
let { info, sectionedMessages }: Props = $props();
interface $$Events { interface $$Events {
sendMessage: CustomEvent<BlahRichText>; sendMessage: CustomEvent<BlahRichText>;

View file

@ -13,7 +13,7 @@
import { flip } from 'svelte/animate'; import { flip } from 'svelte/animate';
import { blur, scale } from 'svelte/transition'; import { blur, scale } from 'svelte/transition';
let accountStore: AccountStore; let accountStore: AccountStore = $state();
onMount(() => { onMount(() => {
openAccountStore().then((store) => { openAccountStore().then((store) => {

View file

@ -15,8 +15,12 @@
import PageHeader from '$lib/components/PageHeader.svelte'; import PageHeader from '$lib/components/PageHeader.svelte';
import SettingsAccountSections from './SettingsAccountSections.svelte'; import SettingsAccountSections from './SettingsAccountSections.svelte';
let className = ''; interface Props {
export { className as class }; class?: string;
}
let { class: className = '' }: Props = $props();
</script> </script>
<div <div

View file

@ -3,13 +3,18 @@
import { GroupedListItem } from '$lib/components/GroupedList'; import { GroupedListItem } from '$lib/components/GroupedList';
import { type IconSource } from 'svelte-hero-icons'; import { type IconSource } from 'svelte-hero-icons';
export let icon: IconSource | undefined = undefined; interface Props {
export let route: string | undefined = undefined; icon?: IconSource | undefined;
route?: string | undefined;
children?: import('svelte').Snippet;
}
$: selected = route let { icon = undefined, route = undefined, children }: Props = $props();
let selected = $derived(route
? $page.route.id?.startsWith(`/(app)/settings${route}`) ? $page.route.id?.startsWith(`/(app)/settings${route}`)
: $page.route.id === '/(app)/settings'; : $page.route.id === '/(app)/settings');
$: href = `/settings${route}`; let href = $derived(`/settings${route}`);
</script> </script>
<GroupedListItem {icon} {selected} {href}><slot /></GroupedListItem> <GroupedListItem {icon} {selected} {href}>{@render children?.()}</GroupedListItem>

View file

@ -7,6 +7,8 @@
<GroupedListContainer> <GroupedListContainer>
<GroupedListSection> <GroupedListSection>
<p slot="footer">New here? <a class="text-accent-500" href="new">Create a new account</a>.</p> {#snippet footer()}
<p >New here? <a class="text-accent-500" href="new">Create a new account</a>.</p>
{/snippet}
</GroupedListSection> </GroupedListSection>
</GroupedListContainer> </GroupedListContainer>

View file

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores'; import { page } from '$app/state';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { currentAccountStore, openAccountStore } from '$lib/accounts/accountStore'; import { currentAccountStore, openAccountStore } from '$lib/accounts/accountStore';
import Button from '$lib/components/Button.svelte'; import Button from '$lib/components/Button.svelte';
@ -18,17 +18,17 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import type { Delta, Editor } from 'typewriter-editor'; import type { Delta, Editor } from 'typewriter-editor';
let name: string = ''; let name: string = $state('');
let editor: Editor | undefined; let editor: Editor | undefined = $state();
let delta: Delta; let delta: Delta | undefined = $state();
let plainText: string = ''; let plainText: string = $state('');
let password: string = ''; let password: string = $state('');
let repeatPassword: string = ''; let repeatPassword: string = $state('');
let identityServer: string = 'other.blue'; let identityServer: string = $state('other.blue');
let isBusy: boolean = false; let isBusy: boolean = $state(false);
let bioPlaceholder = 'Introduce yourself.'; let bioPlaceholder = $state('Introduce yourself.');
const bioPlaceholders = [ const bioPlaceholders = [
'a 23 yo. designer from Tokyo.', 'a 23 yo. designer from Tokyo.',
@ -38,9 +38,9 @@
'a 28 yo. writer from London.' 'a 28 yo. writer from London.'
]; ];
$: passwordMatch = password === repeatPassword; let passwordMatch = $derived(password === repeatPassword);
$: canCreate = name.length > 0 && password.length > 0 && passwordMatch; let canCreate = $derived(name.length > 0 && password.length > 0 && passwordMatch);
$: customize = $page.url.hash === '#customize'; let customize = $derived(page.url.hash === '#customize');
onMount(() => { onMount(() => {
const bioPlaceholderRotateRef = setInterval(() => { const bioPlaceholderRotateRef = setInterval(() => {
@ -76,7 +76,7 @@
{#if isBusy} {#if isBusy}
<LoadingIndicator class="size-4" /> <LoadingIndicator class="size-4" />
{:else} {:else}
<Button variant="primary" disabled={!canCreate} on:click={createAccount}>Create</Button> <Button variant="primary" disabled={!canCreate} onclick={createAccount}>Create</Button>
{/if} {/if}
</PageHeader> </PageHeader>
@ -89,13 +89,15 @@
bind:value={name} bind:value={name}
placeholder="Your Name" placeholder="Your Name"
disabled={isBusy} disabled={isBusy}
class="ms-3 flex-1 bg-transparent text-lg leading-loose caret-accent-500 outline-hidden placeholder:opacity-50" class="caret-accent-500 ms-3 flex-1 bg-transparent text-lg leading-loose outline-hidden placeholder:opacity-50"
/> />
</GroupedListItem> </GroupedListItem>
</GroupedListSection> </GroupedListSection>
<GroupedListSection> <GroupedListSection>
<h4 slot="header">Bio</h4> {#snippet header()}
<h4>Bio</h4>
{/snippet}
<RichTextInput <RichTextInput
class="p-4 shadow-none ring-0" class="p-4 shadow-none ring-0"
bind:editor bind:editor
@ -103,11 +105,15 @@
bind:plainText bind:plainText
placeholder={bioPlaceholder} placeholder={bioPlaceholder}
/> />
<p slot="footer">Introduce yourself. This will be public for everyone to see.</p> {#snippet footer()}
<p>Introduce yourself. This will be public for everyone to see.</p>
{/snippet}
</GroupedListSection> </GroupedListSection>
<GroupedListSection> <GroupedListSection>
<h4 slot="header">Security</h4> {#snippet header()}
<h4>Security</h4>
{/snippet}
<GroupedListInputItem> <GroupedListInputItem>
Password Password
<input type="password" bind:value={password} placeholder="Password" disabled={isBusy} /> <input type="password" bind:value={password} placeholder="Password" disabled={isBusy} />
@ -121,7 +127,8 @@
disabled={isBusy} disabled={isBusy}
/> />
</GroupedListInputItem> </GroupedListInputItem>
<div slot="footer" class="space-y-1"> {#snippet footer()}
<div class="space-y-1">
<p> <p>
Sensitive actions like signing in on new devices require your password. Make sure it's Sensitive actions like signing in on new devices require your password. Make sure it's
unique and secure. You'll lose access to your account if you forget it. unique and secure. You'll lose access to your account if you forget it.
@ -130,24 +137,29 @@
<p><strong>Passwords do not match.</strong></p> <p><strong>Passwords do not match.</strong></p>
{/if} {/if}
</div> </div>
{/snippet}
</GroupedListSection> </GroupedListSection>
{#if customize} {#if customize}
<GroupedListSection> <GroupedListSection>
<h4 slot="header">Identity Service</h4> {#snippet header()}
<h4>Identity Service</h4>
{/snippet}
<GroupedListInputItem> <GroupedListInputItem>
Initial Service Initial Service
<input type="text" bind:value={identityServer} /> <input type="text" bind:value={identityServer} />
</GroupedListInputItem> </GroupedListInputItem>
<div slot="footer" class="space-y-1"> {#snippet footer()}
<div class="space-y-1">
<p> <p>
Your profile is stored and served to other users on the identity service. Your profile is stored and served to other users on the identity service.
<Link href="/" variant="secondary">Learn more about identity services...</Link> <Link href="/" variant="secondary">Learn more about identity services...</Link>
</p> </p>
<p>You can add, replace or remove identity services later in account settings.</p> <p>You can add, replace or remove identity services later in account settings.</p>
</div> </div>
{/snippet}
</GroupedListSection> </GroupedListSection>
{/if} {/if}
<div class="px-8 text-sm text-sf-tertiary"> <div class="text-sf-tertiary px-8 text-sm">
<p> <p>
By creating an account, you agree to Terms of Service and Privacy Policy of By creating an account, you agree to Terms of Service and Privacy Policy of
<em>{identityServer}</em>, which stores and serve your public profile to other users. <em>{identityServer}</em>, which stores and serve your public profile to other users.

View file

@ -1,5 +1,9 @@
<script lang="ts"> <script lang="ts">
export let title: string = 'Color Palette'; interface Props {
title?: string;
}
let { title = 'Color Palette' }: Props = $props();
</script> </script>
<div class="rounded-xl bg-sb-secondary p-4 text-sf-primary"> <div class="rounded-xl bg-sb-secondary p-4 text-sf-primary">

View file

@ -3,9 +3,9 @@
import { deltaToBlahRichText } from '$lib/richText'; import { deltaToBlahRichText } from '$lib/richText';
import type { Delta } from 'typewriter-editor'; import type { Delta } from 'typewriter-editor';
let delta: Delta; let delta: Delta = $state();
$: brt = delta ? deltaToBlahRichText(delta) : null; let brt = $derived(delta ? deltaToBlahRichText(delta) : null);
</script> </script>
<RichTextInput bind:delta class="m-4 max-h-32"> <RichTextInput bind:delta class="m-4 max-h-32">

View file

@ -1,5 +1,10 @@
<script lang="ts"> <script lang="ts">
import '../app.css'; import '../app.css';
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
</script> </script>
<slot /> {@render children?.()}