mirror of
https://github.com/Blah-IM/Weblah.git
synced 2025-05-01 00:31:08 +00:00
refactor: migrate to svelte 5, vite 6 and bits-ui 1.
This commit is contained in:
parent
0bb201636a
commit
1e95dc0830
45 changed files with 1069 additions and 793 deletions
20
package.json
20
package.json
|
@ -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
1099
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
56
src/app.css
56
src/app.css
|
@ -45,35 +45,35 @@
|
||||||
@apply [&_span[data-weblah-brt=underline]]:underline;
|
@apply [&_span[data-weblah-brt=underline]]:underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@utility weblah-light-theme {
|
||||||
|
--weblah-color-sb-overlay: var(--color-white);
|
||||||
|
--weblah-color-sb-primary: var(--color-slate-50);
|
||||||
|
--weblah-color-sb-secondary: var(--color-slate-100);
|
||||||
|
--weblah-color-sb-tertiary: var(--color-slate-200);
|
||||||
|
|
||||||
|
--weblah-color-sf-primary: var(--color-slate-900);
|
||||||
|
--weblah-color-sf-secondary: var(--color-slate-500);
|
||||||
|
--weblah-color-sf-tertiary: var(--color-slate-400);
|
||||||
|
|
||||||
|
--weblah-color-ss-primary: var(--color-slate-300);
|
||||||
|
--weblah-color-ss-secondary: --theme(--color-slate-300 / 60%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility weblah-dark-theme {
|
||||||
|
--weblah-color-sb-overlay: var(--color-slate-800);
|
||||||
|
--weblah-color-sb-primary: var(--color-slate-900);
|
||||||
|
--weblah-color-sb-secondary: var(--color-slate-950);
|
||||||
|
--weblah-color-sb-tertiary: var(--color-black);
|
||||||
|
|
||||||
|
--weblah-color-sf-primary: var(--color-slate-100);
|
||||||
|
--weblah-color-sf-secondary: var(--color-slate-400);
|
||||||
|
--weblah-color-sf-tertiary: var(--color-slate-500);
|
||||||
|
|
||||||
|
--weblah-color-ss-primary: var(--color-slate-700);
|
||||||
|
--weblah-color-ss-secondary: --theme(--color-slate-700 / 60%);
|
||||||
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
.weblah-light-theme {
|
|
||||||
--weblah-color-sb-overlay: var(--color-white);
|
|
||||||
--weblah-color-sb-primary: var(--color-slate-50);
|
|
||||||
--weblah-color-sb-secondary: var(--color-slate-100);
|
|
||||||
--weblah-color-sb-tertiary: var(--color-slate-200);
|
|
||||||
|
|
||||||
--weblah-color-sf-primary: var(--color-slate-900);
|
|
||||||
--weblah-color-sf-secondary: var(--color-slate-500);
|
|
||||||
--weblah-color-sf-tertiary: var(--color-slate-400);
|
|
||||||
|
|
||||||
--weblah-color-ss-primary: var(--color-slate-300);
|
|
||||||
--weblah-color-ss-secondary: --theme(--color-slate-300 / 60%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.weblah-dark-theme {
|
|
||||||
--weblah-color-sb-overlay: var(--color-slate-800);
|
|
||||||
--weblah-color-sb-primary: var(--color-slate-900);
|
|
||||||
--weblah-color-sb-secondary: var(--color-slate-950);
|
|
||||||
--weblah-color-sb-tertiary: var(--color-black);
|
|
||||||
|
|
||||||
--weblah-color-sf-primary: var(--color-slate-100);
|
|
||||||
--weblah-color-sf-secondary: var(--color-slate-400);
|
|
||||||
--weblah-color-sf-tertiary: var(--color-slate-500);
|
|
||||||
|
|
||||||
--weblah-color-ss-primary: var(--color-slate-700);
|
|
||||||
--weblah-color-ss-secondary: --theme(--color-slate-700 / 60%);
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
@apply weblah-light-theme;
|
@apply weblah-light-theme;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)}
|
||||||
<Icon src={Check} class="size-full" micro />
|
{#if itemProps.checked}
|
||||||
</DropdownMenu.RadioIndicator>
|
<Icon src={Check} class="size-full" micro />
|
||||||
<slot />
|
{/if}
|
||||||
|
{@render componentChildren?.()}
|
||||||
|
{/snippet}
|
||||||
</DropdownMenu.RadioItem>
|
</DropdownMenu.RadioItem>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
>
|
>
|
||||||
|
|
|
@ -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)}>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)}>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,46 +11,54 @@
|
||||||
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}>
|
||||||
{@const isMyself = mySenderId === messageSection.sender?.id}
|
{#snippet children({ item: messageSection })}
|
||||||
|
{@const isMyself = mySenderId === messageSection.sender?.id}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{#if messageSection.date}
|
{#if messageSection.date}
|
||||||
<div class="pb-1.5 text-center">
|
<div class="pb-1.5 text-center">
|
||||||
<ServiceMessage class="text-xs">
|
<ServiceMessage class="text-xs">
|
||||||
{formatMessageSectionDate(messageSection.date)}
|
{formatMessageSectionDate(messageSection.date)}
|
||||||
</ServiceMessage>
|
</ServiceMessage>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class={tw('flex w-full items-end px-2', isMyself && 'flex-row-reverse')}>
|
<div class={tw('flex w-full items-end px-2', isMyself && 'flex-row-reverse')}>
|
||||||
<div class="sticky bottom-1.5 mb-1.5 mt-1 w-8">
|
<div class="sticky bottom-1.5 mb-1.5 mt-1 w-8">
|
||||||
<AvatarBeam size={32} name={messageSection.sender?.id} />
|
<AvatarBeam size={32} name={messageSection.sender?.id} />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
{#if messageSection.sender && !isMyself}
|
{#if messageSection.sender && !isMyself}
|
||||||
<div class="px-3 py-0.5 text-xs text-sf-tertiary">{messageSection.sender.name}</div>
|
<div class="px-3 py-0.5 text-xs text-sf-tertiary">{messageSection.sender.name}</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#each messageSection.messages as message, idx}
|
{#each messageSection.messages as message, idx}
|
||||||
<ChatMessage
|
<ChatMessage
|
||||||
{message}
|
{message}
|
||||||
{isMyself}
|
{isMyself}
|
||||||
showBubbleTail={messageSection.messages.length - 1 === idx}
|
showBubbleTail={messageSection.messages.length - 1 === idx}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/snippet}
|
||||||
</VList>
|
</VList>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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')}>
|
||||||
|
|
|
@ -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>;
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,33 +127,39 @@
|
||||||
disabled={isBusy}
|
disabled={isBusy}
|
||||||
/>
|
/>
|
||||||
</GroupedListInputItem>
|
</GroupedListInputItem>
|
||||||
<div slot="footer" class="space-y-1">
|
{#snippet footer()}
|
||||||
<p>
|
<div class="space-y-1">
|
||||||
Sensitive actions like signing in on new devices require your password. Make sure it's
|
<p>
|
||||||
unique and secure. You'll lose access to your account if you forget it.
|
Sensitive actions like signing in on new devices require your password. Make sure it's
|
||||||
</p>
|
unique and secure. You'll lose access to your account if you forget it.
|
||||||
{#if !passwordMatch && repeatPassword}
|
</p>
|
||||||
<p><strong>Passwords do not match.</strong></p>
|
{#if !passwordMatch && repeatPassword}
|
||||||
{/if}
|
<p><strong>Passwords do not match.</strong></p>
|
||||||
</div>
|
{/if}
|
||||||
|
</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()}
|
||||||
<p>
|
<div class="space-y-1">
|
||||||
Your profile is stored and served to other users on the identity service.
|
<p>
|
||||||
<Link href="/" variant="secondary">Learn more about identity services...</Link>
|
Your profile is stored and served to other users on the identity service.
|
||||||
</p>
|
<Link href="/" variant="secondary">Learn more about identity services...</Link>
|
||||||
<p>You can add, replace or remove identity services later in account settings.</p>
|
</p>
|
||||||
</div>
|
<p>You can add, replace or remove identity services later in account settings.</p>
|
||||||
|
</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.
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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?.()}
|
||||||
|
|
Loading…
Add table
Reference in a new issue