Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Email operations #11

Merged
merged 5 commits into from
Mar 19, 2025
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
lint fixes
gzp79 committed Mar 19, 2025
commit a6d81d761b7382dc02859e6edceeecac6fc8fdbe
10 changes: 5 additions & 5 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -18,12 +18,12 @@
}
],
"importOrder": [
"^@(.*)$",
"<THIRD_PARTY_MODULES>",
"^$app/(.*)$",
"^$lib/(.*)$",
"^$atoms/(.*)$",
"^$components/(.*)$",
"^\\$app/(.*)$",
"^\\$lib/(.*)$",
"^\\$atoms/(.*)$",
"^\\$components/(.*)$",
"^\\$features/(.*)$",
"^[./]"
],
"importOrderSeparation": false,
52 changes: 44 additions & 8 deletions src/components/atoms/Box.svelte → src/atoms/Box.svelte
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
<script lang="ts" module>
import { type Snippet, getContext, setContext } from 'svelte';
import { twMerge } from 'tailwind-merge';
import type { ActionColor, ElementProps } from './types';
import type { ActionColor, ElementProps, Size } from './types';
export type Variant = {
color: ActionColor;
};
export type Legend = {
text: string;
color?: ActionColor;
size?: Size;
};
export interface BoxInfo {
bgColor: string;
fgColor: string;
@@ -17,18 +23,32 @@
</script>

<script lang="ts">
import Typography from './Typography.svelte';
interface Props extends ElementProps {
variant?: Variant;
border?: boolean;
shadow?: boolean;
ghost?: boolean;
compact?: boolean;
level?: number;
legend?: string | Legend;
class?: string;
children?: Snippet;
}
let { border, shadow, variant, compact, ghost, level, class: className, children, ...rest }: Props = $props();
let {
border,
shadow,
variant,
compact,
ghost,
level,
legend,
class: className,
children,
...rest
}: Props = $props();
const colorRotation = ['container', 'sub-container', 'surface'];
@@ -38,8 +58,6 @@
setContext('Box_nestingLevel', nestingLevel);
setContext('Box_colorIndex', colorIndex);
let currentMargin = $derived(nestingLevel < 1 ? 'm-4' : nestingLevel < 3 ? 'm-2' : 'm-1');
let colors = $derived.by(() => {
if (variant) {
return {
@@ -71,10 +89,21 @@
});
setContext('Box_props', context);
let legendClass = $derived.by(() => {
let size = 'text-md';
let color = `text-${colors.border}`;
if (typeof legend !== 'string') {
size = legend?.size ? `text-${legend?.size}` : size;
color = legend?.color ? `text-on-${legend?.color}` : color;
}
return twMerge('p-2', size, color);
});
let legendText = $derived.by(() => (typeof legend === 'string' ? legend : legend?.text));
let boxClass = $derived(
twMerge(
'rounded-lg',
!compact && `p-4 ${currentMargin}`,
!compact && `p-4`,
!ghost && `bg-${colors.bgColor}`,
`text-${colors.fgColor}`,
border && `border border-${colors.border}`,
@@ -84,6 +113,13 @@
);
</script>

<div class={boxClass} {...rest}>
{@render children?.()}
</div>
{#if legend}
<fieldset class={boxClass} {...rest}>
<Typography variant="legend" class={legendClass}>{legendText}</Typography>
{@render children?.()}
</fieldset>
{:else}
<div class={boxClass} {...rest}>
{@render children?.()}
</div>
{/if}
File renamed without changes.
8 changes: 4 additions & 4 deletions src/components/atoms/Card.svelte → src/atoms/Card.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts" module>
import { isSnippet } from '$lib/utils';
import { type Component, type Snippet } from 'svelte';
import { twMerge } from 'tailwind-merge';
import { isSnippet } from '$lib/utils';
import Box, { type Variant as BoxVariant } from './Box.svelte';
import Typography from './Typography.svelte';
import type { ElementProps } from './types';
@@ -33,7 +33,7 @@
let boxClass = $derived(
twMerge(
'm-1 md:m-2 p-1 md:p-2',
'p-1 md:p-2',
'min-h-min',
'grid',
icon ? 'grid-cols-[fit-content(10%)_minmax(0,1fr)]' : 'grid-cols-1',
@@ -60,7 +60,7 @@
variant="h4"
element="p"
weight="emphasis"
class="h-fit w-full py-1 pe-2 {!icon && 'text-center'}"
class="h-fit w-full py-2 mx-2 pe-2 {!icon && 'text-center'}"
>
{caption}
</Typography>
@@ -73,7 +73,7 @@
</div>
{/if}

<div class="sticky flex h-fit w-full flex-row justify-end bg-inherit px-1 py-1 md:px-2">
<div class="sticky flex h-fit w-full flex-row justify-end bg-inherit px-1 p-2 md:px-2">
{#if actions}
{@render actions()}
{/if}
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -62,7 +62,16 @@
['highlight', 'hover:highlight'],
['icon-xs', 'icon-sm', 'icon-md', 'icon-lg']
['justify-start', 'justify-end', 'justify-center', 'justify-between', 'justify-around', 'justify-evenly'],
['items-start', 'items-end', 'items-center', 'items-baseline', 'items-stretch'],
['text-xs', 'text-sm', 'text-md', 'text-lg'],
['icon-xs', 'icon-sm', 'icon-md', 'icon-lg'],
['gap-1', 'gap-2', 'gap-3', 'gap-4', 'gap-5', 'gap-6', 'gap-7', 'gap-8', 'gap-9', 'gap-10', 'gap-11', 'gap-12'],
['xs:gap-1', 'xs:gap-2', 'xs:gap-3', 'xs:gap-4', 'xs:gap-5', 'xs:gap-6', 'xs:gap-7', 'xs:gap-8', 'xs:gap-9', 'xs:gap-10', 'xs:gap-11', 'xs:gap-12'],
['md:gap-1', 'md:gap-2', 'md:gap-3', 'md:gap-4', 'md:gap-5', 'md:gap-6', 'md:gap-7', 'md:gap-8', 'md:gap-9', 'md:gap-10', 'md:gap-11', 'md:gap-12'],
['lg:gap-1', 'lg:gap-2', 'lg:gap-3', 'lg:gap-4', 'lg:gap-5', 'lg:gap-6', 'lg:gap-7', 'lg:gap-8', 'lg:gap-9', 'lg:gap-10', 'lg:gap-11', 'lg:gap-12'],
];
void defaultSafeList;
16 changes: 16 additions & 0 deletions src/atoms/ContextProvider.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<script lang="ts" generics="T">
import type { Snippet } from 'svelte';
// generics="T" is not respected by the eslint
/* eslint no-undef: "off" */
interface Props<T> {
use: () => T;
children: Snippet;
}
let { children, use: useContext }: Props<T> = $props();
useContext();
</script>

{@render children()}
2 changes: 1 addition & 1 deletion src/components/atoms/Grid.svelte → src/atoms/Grid.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import CompileTailwindClasses from './CompileTailwindClasses.svelte';
import { type ResponsiveProp, toResponsiveClass, toResponsiveProp } from './types';
import { type ResponsiveProp, toResponsiveClass, toResponsiveProp } from './types/responsive-prop';
type Spaces = 0 | 0.5 | 1 | 2 | 4 | 8 | 12;
type Columns = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import CompileTailwindClasses from './CompileTailwindClasses.svelte';
import { type ResponsiveProp, toResponsiveClass } from './types';
import { type ResponsiveProp, toResponsiveClass } from './types/responsive-prop';
type Spans = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 'full';
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts" module>
import type { Nullable } from '$lib/utils';
import { type Snippet, onMount } from 'svelte';
import { twMerge } from 'tailwind-merge';
import type { Nullable } from '$lib/utils';
import Box from './Box.svelte';
import { portal } from './Portal.svelte';
import { Cross } from './icons/common';
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<script lang="ts" module>
import type { Middleware } from '@floating-ui/dom';
import * as floatingDom from '@floating-ui/dom';
import type { Nullable } from '$lib/utils';
import { type Snippet, onMount } from 'svelte';
import { twMerge } from 'tailwind-merge';
import type { Nullable } from '$lib/utils';
import { portal } from './Portal.svelte';
export type Behavior = 'click' | 'clickWithSelf' | 'toggle' | 'hover' | 'manual';
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -3,8 +3,8 @@
</script>

<script lang="ts" generics="T">
import type { AppError, Nullable } from '$lib/utils';
import type { Snippet } from 'svelte';
import type { AppError, Nullable } from '$lib/utils';
// generics="T" is not respected by the eslint
/* eslint no-undef: "off" */
@@ -17,9 +17,9 @@
error: Snippet<[error: AppError]>;
// read only bindable props indicating the current state
status?: Status;
readonly status?: Status;
// read only bindable props updated on each successful fetch
dataVersion?: number;
readonly dataVersion?: number;
}
let {
initial,
@@ -31,43 +31,38 @@
dataVersion = $bindable(0)
}: Props<T> = $props();
let loadingState = $state(false);
let resourceState = $state<Nullable<T>>(initial ?? null);
let errorState = $state<Nullable<AppError>>(null);
let invalidateCount = $state(0);
export function invalidate() {
invalidateCount += 1;
}
type FetchError = { __fetch_error: AppError };
const fetchResource = async (): Promise<T | null | FetchError> => {
let result;
const fetchResource = async (): Promise<void> => {
try {
result = await fetch();
resourceState = await fetch();
status = 'completed';
dataVersion += 1;
} catch (err) {
result = { __fetch_error: err as AppError };
resourceState = null;
status = 'error';
errorState = err as AppError;
}
return result;
};
$effect(() => {
loadingState = true;
status = 'loading';
fetchResource().then((data) => {
if (data instanceof Object && '__fetch_error' in data) {
resourceState = null;
status = 'error';
errorState = data.__fetch_error;
} else {
resourceState = data;
status = 'completed';
}
dataVersion += 1;
loadingState = false;
});
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
invalidateCount;
fetchResource();
});
</script>

{#if errorState}
{@render error(errorState)}
{:else if resourceState}
{@render content(resourceState, loadingState)}
{@render content(resourceState, status !== 'completed')}
{:else}
{@render loading()}
{/if}
34 changes: 34 additions & 0 deletions src/atoms/Stack.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<script lang="ts" module>
import { type Snippet } from 'svelte';
import { twMerge } from 'tailwind-merge';
import type { ElementProps } from './types';
import { type ResponsiveProp, toResponsiveClass } from './types/responsive-prop';
</script>

<script lang="ts">
type Spacing = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
interface Props extends ElementProps {
direction?: 'row' | 'column';
spacing?: Spacing | ResponsiveProp<Spacing>;
align?: 'start' | 'center' | 'end';
justify?: 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly';
class?: string;
children?: Snippet;
}
let { direction = 'column', spacing, align, justify, children, class: className, ...rest }: Props = $props();
let clsStack = twMerge([
'flex',
direction === 'row' ? 'flex-row w-full' : 'flex-col h-full',
spacing ? toResponsiveClass(spacing, (spacing) => `gap-${spacing}`) : 'gap-2',
align && `items-${align}`,
justify && `justify-${justify}`,
className
]);
</script>

<div class={clsStack} {...rest}>
{@render children?.()}
</div>
Original file line number Diff line number Diff line change
@@ -57,7 +57,7 @@
variant === 'filled' && [
`bg-${colorWithFallback} text-on-${colorWithFallback} placeholder:text-${colorWithFallback}-2 border-on-${colorWithFallback}`,
!group && ['m-1', 'rounded-lg', 'border-2'],
!group && ['rounded-lg', 'border-2'],
group &&
(group.vertical
? 'first:border-t-2 last:border-b-2 border-x-2 border-t-2 first:rounded-t-lg last:rounded-b-lg'
@@ -68,7 +68,7 @@
box && !color
? `text-${box.fgColor} placeholder:text-${box.fgColor2} border-${box.border}`
: `text-on-${colorWithFallback} placeholder:text-${colorWithFallback}-2 border-on-${colorWithFallback}`,
!group && ['m-1', 'rounded-lg', 'border-2'],
!group && ['rounded-lg', 'border-2'],
group &&
(group.vertical
? 'first:border-t-2 last:border-b-2 border-x-2 border-t-2 first:rounded-t-lg last:rounded-b-lg'
@@ -79,7 +79,7 @@
box && !color
? `text-${box.fgColor} placeholder:text-${box.fgColor2}`
: `text-on-${colorWithFallback} placeholder:text-${colorWithFallback}-2`,
!group && ['m-1', 'rounded-lg', 'border-2', 'border-transparent'],
!group && ['rounded-lg', 'border-2', 'border-transparent'],
!disabled && `hover:highlight-backdrop`
],
Original file line number Diff line number Diff line change
@@ -27,10 +27,10 @@
const transition = 'transition-all duration-100 easy-in-out';
const toggleSize: Record<Size, string> = {
xs: 'text-sm leading-none my-1',
sm: 'text-base leading-none my-1',
md: 'text-base leading-none my-1',
lg: 'text-lg leading-none my-1'
xs: 'text-sm leading-none',
sm: 'text-base leading-none',
md: 'text-base leading-none',
lg: 'text-lg leading-none'
};
let toggleClass = $derived(
twMerge(
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts" module>
export type Variant = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'text' | 'code';
export type Variant = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'text' | 'code' | 'legend';
export type Weight = 'normal' | 'emphasis' | 'bold';
</script>

@@ -27,7 +27,8 @@
h5: 'h5',
h6: 'h6',
text: 'p',
code: 'code'
code: 'code',
legend: 'legend'
};
const sharedHClasses = 'text-ellipsis text-pretty';
@@ -39,7 +40,8 @@
h5: `text-lg ${sharedHClasses}`,
h6: `text-base ${sharedHClasses}`,
text: 'text-base',
code: 'text-sm'
code: 'text-sm',
legend: 'text-base'
};
const weightClasses = {
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
1,979 changes: 1,979 additions & 0 deletions src/atoms/svg/_parser.js

Large diffs are not rendered by default.

File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -4,9 +4,6 @@ export interface ElementProps {
role?: string;
}

/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
export function dependsOn(a: unknown) {}

export const containerColorList = ['surface', 'container', 'sub-container'];
export type ContainerColor = (typeof containerColorList)[number];

@@ -30,6 +27,3 @@ export function simpleHash(str: string): string {
}
return (hash >>> 0).toString(16);
}

export * from './_responsive-prop';
export * from './_math';
142 changes: 142 additions & 0 deletions src/atoms/types/resource.svelte.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { v4 as uuid } from 'uuid';
import { logResource } from '$lib/loggers';
import type { AppError } from '$lib/utils';

type ResourceState<T> =
| { type: 'empty' }
| { type: 'content'; content: T; age: number; isDirty: boolean }
| { type: 'error'; error: AppError };

export interface ResourceService<T> {
load: () => Promise<T>;
}

export type ResourceRefreshOptions = {
// perform a forced reload and ignore any pending requests
force?: boolean;
// refresh if the resource is older than the given interval
interval?: number;
// keep the error state unless some other option forces a reload
// by default a failed resource will be reloaded
keepError?: boolean;
};

export class ResourceStore<T, S extends ResourceService<T>> {
private resource = $state<ResourceState<T>>({ type: 'empty' });
private isLoading = false;
private lastUpdate = 0;
private id = uuid();

constructor(public readonly service: S) {}

uniqueId(): string {
return `${this.constructor.name}-${this.id}`;
}

get isEmpty() {
return this.resource.type === 'empty';
}

get isDirty() {
return this.resource.type !== 'content' || this.resource.isDirty;
}

get isError() {
return this.resource.type === 'error';
}

get error(): AppError {
if (this.resource.type !== 'error') {
throw new Error('Resource is not in error state');
}
return this.resource.error;
}

get isContent() {
return this.resource.type === 'content';
}

get age(): number {
return Math.clamp(Date.now() - this.lastUpdate, 0, Number.MAX_VALUE);
}

get content(): T {
if (this.resource.type !== 'content') {
throw new Error('Resource is not in content state');
}
return this.resource.content;
}

async load() {
if (this.resource.type === 'content') {
this.resource.isDirty = true;
}

const now = Date.now();
logResource(`${this.uniqueId()}: Start loading resource at ${now}, loading: ${this.isLoading}`);
this.lastUpdate = now;
this.isLoading = true;
let newResource: ResourceState<T>;
try {
const content = await this.service.load();
newResource = { type: 'content', content, age: Date.now(), isDirty: false };
} catch (err) {
newResource = { type: 'error', error: err as AppError };
}

// when multiple load calls are made, only the last one should update the resource
if (this.lastUpdate === now) {
logResource(`${this.uniqueId()}: Loading resource completed with ${newResource.type}`);
this.resource = newResource;
this.isLoading = false;
} else {
logResource(
`${this.uniqueId()}: Loading resource ignored request as ${now} is replaced by ${this.lastUpdate}`
);
}
}

async refresh(options?: ResourceRefreshOptions) {
let reload = false;

if (options?.force) {
logResource(`${this.uniqueId()}: Refreshing resource forced`);
reload ||= true;
}

if (!this.isLoading) {
if (options?.interval && this.age > options.interval * 1000) {
logResource(`${this.uniqueId()}: Refreshing resource for outdated content`);
reload ||= true;
}
if (this.resource.type === 'empty') {
logResource(`${this.uniqueId()}: Refreshing resource for the initial load`);
reload ||= true;
}
if (this.resource.type === 'error' && !options?.keepError) {
logResource(`${this.uniqueId()}: Refreshing resource from error state`);
reload ||= true;
}
}

if (reload) {
await this.load();
} else {
logResource(`${this.uniqueId()}: Skipping resource refresh `);
}
}

protected async update(fn: (content: T) => Promise<T>) {
if (this.resource.type === 'content') {
this.resource.isDirty = true;

this.resource = {
type: 'content',
content: await fn(this.resource.content),
age: this.resource.age,
isDirty: this.resource.isDirty
};
}
await this.load();
}
}
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -4,9 +4,9 @@

<script lang="ts">
import type { Snippet } from 'svelte';
import Card, { type Variant as CardVariant } from './Card.svelte';
import * as icons from './icons/common';
import type { ElementProps } from './types';
import Card, { type Variant as CardVariant } from '$atoms/Card.svelte';
import * as icons from '$atoms/icons/common';
import type { ElementProps } from '$atoms/types';
interface Props extends ElementProps {
variant?: Variant;
39 changes: 39 additions & 0 deletions src/components/ErrorCard.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import { t } from '$lib/i18n/i18n.svelte';
import type { AppError } from '$lib/utils';
import Box from '$atoms/Box.svelte';
import Stack from '$atoms/Stack.svelte';
import Typography from '$atoms/Typography.svelte';
import type { ElementProps } from '$atoms/types';
import Alert from '$components/Alert.svelte';
interface Props extends Omit<ElementProps, 'role'> {
caption?: string;
error: AppError;
children?: Snippet;
actions?: Snippet;
}
let { caption, error, children, actions, ...rest }: Props = $props();
</script>

<div class="flex h-full w-full flex-col items-center justify-center">
<Alert variant="error" caption={caption ?? $t('common.somethingWentWrong')} {actions} {...rest}>
<Stack spacing={2}>
<Typography variant="text">
{error.message}
</Typography>
{#if error.detail}
<Box border class="min-h-max">
<Typography variant="code">
<pre class="whitespace-pre-wrap break-words">{JSON.stringify(error.detail, null, 2)}</pre>
</Typography>
</Box>
{/if}
{#if children}
{@render children()}
{/if}
</Stack>
</Alert>
</div>
4 changes: 2 additions & 2 deletions src/components/Turnstile.svelte
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<script lang="ts">
import { browser } from '$app/environment';
import { logAPI } from '$lib/loggers';
import { onMount } from 'svelte';
import type { Action } from 'svelte/action';
import type { RenderParameters, WidgetId } from 'turnstile-types';
import { browser } from '$app/environment';
import { logAPI } from '$lib/loggers';
interface Props {
/**
38 changes: 0 additions & 38 deletions src/components/atoms/ErrorCard.svelte

This file was deleted.

439 changes: 0 additions & 439 deletions src/components/atoms/ExtraMenu.svelte

This file was deleted.

29 changes: 0 additions & 29 deletions src/components/atoms/ExtraMenuItem.svelte

This file was deleted.

41 changes: 0 additions & 41 deletions src/components/atoms/SimpleMenu.svelte

This file was deleted.

13 changes: 0 additions & 13 deletions src/components/atoms/SimpleMenuItem.svelte

This file was deleted.

1,852 changes: 0 additions & 1,852 deletions src/components/atoms/svg/_parser.js

This file was deleted.

1 change: 1 addition & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { config as baseConfig } from '$generated/config';
import '$lib/prelude';

export interface Config {
environment: 'mock' | 'dev' | 'prod';
28 changes: 28 additions & 0 deletions src/features/account/ActiveSessionCard.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<script lang="ts" module>
import { t } from '$lib/i18n/i18n.svelte';
import Card from '$atoms/Card.svelte';
import LoadingCard from '$atoms/LoadingCard.svelte';
import Stack from '$atoms/Stack.svelte';
import ErrorCard from '$components/ErrorCard.svelte';
import ActiveSessionItem from './ActiveSessionItem.svelte';
import { getActiveSessionStore } from './activeSessionStore.svelte';
</script>

<script lang="ts">
const sessionStore = getActiveSessionStore();
sessionStore.refresh();
</script>

<Card caption={$t('account.activeSessions')}>
{#if sessionStore.isEmpty}
<LoadingCard />
{:else if sessionStore.isError}
<ErrorCard error={sessionStore.error} />
{:else}
<Stack>
{#each sessionStore.content as session (session.tokenHash)}
<ActiveSessionItem {session} />
{/each}
</Stack>
{/if}
</Card>
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<script lang="ts">
import Card from '$atoms/Card.svelte';
import KeyValueTable from '$atoms/KeyValueTable.svelte';
import * as clientIcons from '$atoms/icons/clients';
import { UAParser } from 'ua-parser-js';
import type { ActiveSession } from '$lib/api/identity-api';
import { t } from '$lib/i18n/i18n.svelte';
import { formatLocation } from '$lib/i18n/utils';
import { UAParser } from 'ua-parser-js';
import Card from '$atoms/Card.svelte';
import KeyValueTable from '$atoms/KeyValueTable.svelte';
import * as clientIcons from '$atoms/icons/clients';
interface Props {
session: ActiveSession;
28 changes: 28 additions & 0 deletions src/features/account/ActiveTokenCard.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<script lang="ts" module>
import { t } from '$lib/i18n/i18n.svelte';
import Card from '$atoms/Card.svelte';
import LoadingCard from '$atoms/LoadingCard.svelte';
import Stack from '$atoms/Stack.svelte';
import ErrorCard from '$components/ErrorCard.svelte';
import ActiveTokenItem from './ActiveTokenItem.svelte';
import { getActiveTokenStore } from './activeTokenStore.svelte';
</script>

<script lang="ts">
const tokenStore = getActiveTokenStore();
tokenStore.refresh();
</script>

<Card caption={$t('account.activeTokens')}>
{#if tokenStore.isEmpty}
<LoadingCard />
{:else if tokenStore.isError}
<ErrorCard error={tokenStore.error} />
{:else}
<Stack>
{#each tokenStore.content as token (token.tokenHash)}
<ActiveTokenItem {token} />
{/each}
</Stack>
{/if}
</Card>
Original file line number Diff line number Diff line change
@@ -1,26 +1,23 @@
<script lang="ts">
import Button from '$atoms/Button.svelte';
import Card from '$atoms/Card.svelte';
import KeyValueTable from '$atoms/KeyValueTable.svelte';
import type { ActiveToken } from '$lib/api/identity-api';
import { t } from '$lib/i18n/i18n.svelte';
import { formatLocation } from '$lib/i18n/utils';
import Button from '$atoms/Button.svelte';
import Card from '$atoms/Card.svelte';
import KeyValueTable from '$atoms/KeyValueTable.svelte';
import { getActiveTokenStore } from './activeTokenStore.svelte';
interface Props {
token: ActiveToken;
dataVersion: number;
onRevoke: (tokenHash: string) => Promise<void>;
}
const { token, dataVersion, onRevoke }: Props = $props();
let revokeVersion = $state(0);
const revoke = async () => {
// memorize the current dataVersion to prevent multiple revoke requests
revokeVersion = dataVersion;
await onRevoke(token.tokenHash);
};
const { token }: Props = $props();
const location = $derived(formatLocation(token));
const tokenStore = getActiveTokenStore();
const revoke = () => {
tokenStore.revoke(token.tokenHash);
};
</script>

<Card width="full">
@@ -65,6 +62,8 @@
/>

{#snippet actions()}
<Button disabled={revokeVersion >= dataVersion} color="danger" onclick={revoke}>{$t('account.revoke')}</Button>
<Button disabled={tokenStore.isDirty} color="danger" onclick={revoke}>
{$t('account.revoke')}
</Button>
{/snippet}
</Card>
194 changes: 194 additions & 0 deletions src/features/account/CurrentUserCard.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
<script lang="ts" module>
import { t } from '$lib/i18n/i18n.svelte';
import { FetchError } from '$lib/utils';
import Button from '$atoms/Button.svelte';
import Card from '$atoms/Card.svelte';
import ComboButton from '$atoms/ComboButton.svelte';
import KeyValueTable from '$atoms/KeyValueTable.svelte';
import LoadingCard from '$atoms/LoadingCard.svelte';
import Modal from '$atoms/Modal.svelte';
import TextArea from '$atoms/TextArea.svelte';
import Typography from '$atoms/Typography.svelte';
import { Spinner } from '$atoms/icons/animated';
import Warning from '$atoms/icons/common/_warning.svelte';
import Alert from '$components/Alert.svelte';
import ErrorCard from '$components/ErrorCard.svelte';
import { getCurrentUserStore } from './currentUser.svelte';
export interface EmailService {
startEmailConfirmation: () => Promise<void>;
startEmailChange: (newEmail: string) => Promise<void>;
getLogoutUrl: (all: boolean, redirectUrl: string) => string;
}
const DISABLE_EMAIL_OPS = true;
</script>

<script lang="ts">
const currentUserStore = getCurrentUserStore();
$effect(() => {
currentUserStore.refresh();
});
const service = $derived(currentUserStore.service);
const user = $derived(currentUserStore.isContent ? currentUserStore.content : null);
const logoutUrl = $derived(service.getLogoutUrl(false, '/'));
const logoutAllUrl = $derived(service.getLogoutUrl(true, '/'));
type EmailStatus = 'none' | 'waitingResponse' | 'gettingNewEmail' | 'gettingAcknowledge' | 'complete' | FetchError;
let emailStatus = $state<EmailStatus>('none');
let newEmail = $state('');
const startEmailConfirmation = async () => {
if (emailStatus === 'complete') {
// just to avoid multiple clicks and make the user feel better
emailStatus = 'gettingAcknowledge';
currentUserStore.refresh();
} else {
emailStatus = 'waitingResponse';
try {
await service.startEmailConfirmation();
emailStatus = 'gettingAcknowledge';
} catch (error) {
console.assert(error instanceof FetchError);
emailStatus = error as FetchError;
}
}
};
const startEmailChange = async () => {
if (emailStatus === 'complete') {
// just to avoid multiple clicks and make the user feel better
emailStatus = 'gettingAcknowledge';
currentUserStore.refresh();
} else {
emailStatus = 'gettingNewEmail';
}
};
const submitEmailChange = async (newEmail: string) => {
console.assert(emailStatus === 'gettingNewEmail');
emailStatus = 'waitingResponse';
try {
await service.startEmailChange(newEmail);
emailStatus = 'gettingAcknowledge';
} catch (error) {
console.assert(error instanceof FetchError);
emailStatus = error as FetchError;
}
};
const cancelEmailChange = async () => {
console.assert(emailStatus === 'gettingNewEmail');
emailStatus = 'none';
};
const clearEmailError = () => {
console.assert(emailStatus instanceof FetchError);
emailStatus = 'none';
};
const finishEmailOperation = async () => {
emailStatus = 'complete';
};
/*$effect(() => {
if (disableActions) {
// close popup if the user is not allowed to do anything
emailFlow.reset();
}
});*/
</script>

<Card caption={$t('account.userInfo')}>
{#if currentUserStore.isEmpty}
<LoadingCard />
{:else if currentUserStore.isError}
<ErrorCard error={currentUserStore.error} />
{:else if user}
{#if !user.isLinked}
<Alert variant="warning" caption={$t('account.linkWarning')} />
{/if}

{#snippet email()}
<div class="flex flex-col sm:flex-row space-y-1">
<div class="flex items-center space-x-2">
{#if user.email}
{#if !user.isEmailConfirmed}
<Warning size="sm" color="warning" />
{/if}
<span class="me-2">{user.email}</span>
{:else}
<i class="bg-warning text-on-warning">{$t('account.noEmail')}</i>
{/if}
</div>
<!-- {#if emailStatus !== 'complete'} -->
{#if !DISABLE_EMAIL_OPS}
<div class="flex justify-start ms-8 sm:ms-0">
{#if user.email && !user.isEmailConfirmed}
<ComboButton
size="xs"
disabled={currentUserStore.isDirty}
items={[
{
caption: $t('account.confirm'),
onclick: () => startEmailConfirmation()
},
{ caption: $t('account.updateEmail'), onclick: () => startEmailChange() }
]}
/>
{:else}
<Button size="xs" disabled={currentUserStore.isDirty} onclick={() => startEmailChange()}>
{$t('account.updateEmail')}
</Button>
{/if}
</div>
{/if}
</div>
{/snippet}

<KeyValueTable
size="xs"
items={[
{ key: $t('account.userName'), value: user.name, class: 'break-all' },
{ key: $t('account.userId'), value: user.userId, class: 'break-all' },
{ key: $t('account.email'), value: email },
{ key: $t('account.role'), value: user.roles.join(', ') }
]}
/>
{/if}

{#snippet actions()}
<ComboButton
disabled={currentUserStore.isDirty}
items={[
{ caption: $t('account.logout'), href: logoutUrl },
{ caption: $t('account.logoutAll'), href: logoutAllUrl }
]}
/>
{/snippet}
</Card>

<Modal caption={$t('account.confirmTitle')} isOpen={emailStatus !== 'none' && emailStatus !== 'complete'}>
{#if emailStatus instanceof FetchError}
<ErrorCard error={emailStatus} />
<div class="flex justify-end space-x-2">
<Button onclick={() => clearEmailError()}>{$t('account.ok')}</Button>
</div>
{:else if emailStatus == 'waitingResponse'}
<Typography variant="text" class="w-full text-justify">{$t('account.confirmPendingText')}</Typography>
<div class="flex justify-end space-x-2">
<Button disabled startIcon={Spinner}>
{$t('account.ok')}
</Button>
</div>
{:else if emailStatus == 'gettingNewEmail'}
<Typography variant="text" class="w-full text-justify">{$t('account.confirmPendingText')}</Typography>
<TextArea rows="single" placeholder={$t('account.newEmail')} class="w-full" bind:text={newEmail}></TextArea>
<div class="flex justify-end space-x-2">
<Button onclick={() => cancelEmailChange()}>{$t('common.cancel')}</Button>
<Button color="secondary" onclick={() => submitEmailChange(newEmail)}>{$t('account.updateEmail')}</Button>
</div>
{:else if emailStatus == 'gettingAcknowledge'}
<Typography variant="text" class="w-full text-justify">{$t('account.confirmCompleteText')}</Typography>
<div class="flex justify-end space-x-2">
<Button onclick={() => finishEmailOperation()}>{$t('account.ok')}</Button>
</div>
{/if}
</Modal>
71 changes: 71 additions & 0 deletions src/features/account/LinkedIdentityCard.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<script lang="ts" module>
import { page } from '$app/state';
import { t } from '$lib/i18n/i18n.svelte';
import Button from '$atoms/Button.svelte';
import Card from '$atoms/Card.svelte';
import LoadingCard from '$atoms/LoadingCard.svelte';
import Modal from '$atoms/Modal.svelte';
import Stack from '$atoms/Stack.svelte';
import { Link } from '$atoms/icons/common';
import ErrorCard from '$components/ErrorCard.svelte';
import LinkedIdentityItem from './LinkedIdentityItem.svelte';
import { getLinkedIdentityStore } from './linkedIdentityStore.svelte';
import { providerIcon } from './providers.svelte';
</script>

<script lang="ts">
interface Props {
providers: string[];
}
const { providers }: Props = $props();
const identityStore = getLinkedIdentityStore();
identityStore.refresh();
let showLinkModal = $state(false);
$effect(() => {
if (identityStore.isDirty) {
showLinkModal = false;
}
});
</script>

<Card caption={$t('account.linkedIdentities')}>
{#if identityStore.isEmpty}
<LoadingCard />
{:else if identityStore.isError}
<ErrorCard error={identityStore.error} />
{:else}
<Stack>
{#each identityStore.content as identity (`${identity.provider}/${identity.providerUserId}`)}
<LinkedIdentityItem {identity} />
{/each}
</Stack>
{/if}

{#snippet actions()}
<Button
color="secondary"
startIcon={Link}
disabled={identityStore.isDirty}
onclick={() => (showLinkModal = true)}
>
{$t('account.link')}
</Button>
{/snippet}
</Card>

<Modal closeButton closeOnClickOutside caption={$t('account.linkTitle')} bind:isOpen={showLinkModal} class="max-w-min">
{#each providers as provider}
<Button
variant="outline"
wide
startIcon={providerIcon(provider)}
class="mx-0"
href={identityStore.service.getLinkUrl(provider, page.url.pathname)}
>
{provider}
</Button>
{/each}
</Modal>
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
<script lang="ts">
import type { LinkedIdentity } from '$lib/api/identity-api';
import { t } from '$lib/i18n/i18n.svelte';
import Button from '$atoms/Button.svelte';
import Card from '$atoms/Card.svelte';
import KeyValueTable from '$atoms/KeyValueTable.svelte';
import * as social from '$atoms/icons/social';
import type { LinkedIdentity } from '$lib/api/identity-api';
import { t } from '$lib/i18n/i18n.svelte';
import { getLinkedIdentityStore } from './linkedIdentityStore.svelte';
interface Props {
identity: LinkedIdentity;
dataVersion: number;
onUnlink?: (provider: string, providerUserId: string) => Promise<void>;
}
const { identity, dataVersion, onUnlink }: Props = $props();
const { identity }: Props = $props();
const identityStore = getLinkedIdentityStore();
const ProviderImage = $derived.by(() => {
switch (identity.provider) {
@@ -28,11 +29,8 @@
}
});
let disableVersion = $state(0);
const unlink = async () => {
// memorize the current dataVersion to prevent multiple unlink requests
disableVersion = dataVersion;
await onUnlink?.(identity.provider, identity.providerUserId);
const unlink = () => {
identityStore.unlink(identity.provider, identity.providerUserId);
};
</script>

@@ -78,7 +76,7 @@
/>

{#snippet actions()}
<Button disabled={disableVersion >= dataVersion} color="danger" onclick={unlink}>
<Button disabled={identityStore.isDirty} color="danger" onclick={unlink}>
{$t('account.unlink')}
</Button>
{/snippet}
23 changes: 23 additions & 0 deletions src/features/account/activeSessionStore.svelte.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { getContext, setContext } from 'svelte';
import type { ActiveSession } from '$lib/api/identity-api';
import { type ResourceService, ResourceStore } from '$atoms/types/resource.svelte';

export type ActiveSessionService = ResourceService<ActiveSession[]>;

export class ActiveSessionStore extends ResourceStore<ActiveSession[], ActiveSessionService> {}

const contextKey = Symbol('activeSessionStore');

export function setActiveSessionStore(dataService: ActiveSessionService): ActiveSessionStore {
const store = new ActiveSessionStore(dataService);
setContext(contextKey, () => store);
return store;
}

export function getActiveSessionStore(): ActiveSessionStore {
const store = getContext<() => ActiveSessionStore>(contextKey);
if (!store) {
throw new Error('ActiveSessionStore not found, missing call to setActiveSessionStore');
}
return store();
}
37 changes: 37 additions & 0 deletions src/features/account/activeTokenStore.svelte.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { getContext, setContext } from 'svelte';
import type { ActiveToken } from '$lib/api/identity-api';
import { type ResourceService, ResourceStore } from '$atoms/types/resource.svelte';

export interface ActiveTokenService extends ResourceService<ActiveToken[]> {
revoke: (tokenHash: string) => Promise<void>;
}

export class ActiveTokenStore extends ResourceStore<ActiveToken[], ActiveTokenService> {
async revoke(tokenHash: string) {
this.update(async (tokens) => {
try {
await this.service.revoke(tokenHash);
} catch (error) {
// todo: show error toasts
console.error('Failed to revoke token', error);
}
return tokens.filter((token) => token.tokenHash !== tokenHash);
});
}
}

const contextKey = Symbol('activeTokenStore');

export function setActiveTokenStore(dataService: ActiveTokenService): ActiveTokenStore {
const store = new ActiveTokenStore(dataService);
setContext(contextKey, () => store);
return store;
}

export function getActiveTokenStore(): ActiveTokenStore {
const store = getContext<() => ActiveTokenStore>(contextKey);
if (!store) {
throw new Error('ActiveTokenStore not found, missing call to setActiveTokenStore');
}
return store();
}
34 changes: 34 additions & 0 deletions src/features/account/currentUser.svelte.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { getContext, setContext } from 'svelte';
import type { CurrentUser } from '$lib/api/identity-api';
import { type ResourceRefreshOptions, type ResourceService, ResourceStore } from '$atoms/types/resource.svelte';

// Update the current user every n seconds
const UPDATE_INTERVAL = 15 * 60;

export interface CurrentUserService extends ResourceService<CurrentUser> {
startEmailConfirmation: () => Promise<void>;
startEmailChange: (newEmail: string) => Promise<void>;
getLogoutUrl: (all: boolean, redirectUrl: string) => string;
}

export class CurrentUserStore extends ResourceStore<CurrentUser, CurrentUserService> {
async refresh(options?: ResourceRefreshOptions) {
await super.refresh({ ...options, interval: UPDATE_INTERVAL });
}
}

const contextKey = Symbol('currentUserStore');

export function setCurrentUserStore(dataService: CurrentUserService): CurrentUserStore {
const store = new CurrentUserStore(dataService);
setContext(contextKey, () => store);
return store;
}

export function getCurrentUserStore(): CurrentUserStore {
const store = getContext<() => CurrentUserStore>(contextKey);
if (!store) {
throw new Error('CurrentUserStore not found, missing call to setCurrentUserStore');
}
return store();
}
38 changes: 38 additions & 0 deletions src/features/account/linkedIdentityStore.svelte.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { getContext, setContext } from 'svelte';
import { type LinkedIdentity } from '$lib/api/identity-api';
import { type ResourceService, ResourceStore } from '$atoms/types/resource.svelte';

export interface LinkedIdentityService extends ResourceService<LinkedIdentity[]> {
unlink: (provider: string, providerUserId: string) => Promise<void>;
getLinkUrl: (provider: string, redirect: string) => string;
}

export class LinkedIdentityStore extends ResourceStore<LinkedIdentity[], LinkedIdentityService> {
async unlink(provider: string, providerUserId: string): Promise<void> {
return this.update(async (links) => {
try {
await this.service.unlink(provider, providerUserId);
} catch (error) {
// todo: show error toasts
console.error('Failed to unlink', provider, providerUserId, error);
}
return links.filter((link) => link.provider !== provider || link.providerUserId !== providerUserId);
});
}
}

const contextKey = Symbol('linkedIdentityStore');

export function setLinkedIdentityStore(dataService: LinkedIdentityService): LinkedIdentityStore {
const store = new LinkedIdentityStore(dataService);
setContext(contextKey, () => store);
return store;
}

export function getLinkedIdentityStore(): LinkedIdentityStore {
const store = getContext<() => LinkedIdentityStore>(contextKey);
if (!store) {
throw new Error('ActiveTokenStore not found, missing call to setActiveTokenStore');
}
return store();
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as icons from '$atoms/icons/social';
import type { Component } from 'svelte';
import * as icons from '$atoms/icons/social';

export const providerIcon = (provider: string): Component | undefined => {
switch (provider) {
2 changes: 1 addition & 1 deletion src/hooks.server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// hooks.server.ts
import type { Handle } from '@sveltejs/kit';
import { config } from '$config';
import type { Handle } from '@sveltejs/kit';
import { loadThemeServerSide } from '$lib/theme/theme.svelte';

if (config.environment === 'mock') {
33 changes: 0 additions & 33 deletions src/lib/account/ActiveSessionsCard.svelte

This file was deleted.

38 changes: 0 additions & 38 deletions src/lib/account/ActiveTokensCard.svelte

This file was deleted.

228 changes: 0 additions & 228 deletions src/lib/account/CurrentUserCard.svelte

This file was deleted.

49 changes: 0 additions & 49 deletions src/lib/account/LinkedIdentitiesCard.svelte

This file was deleted.

96 changes: 0 additions & 96 deletions src/lib/account/currentUser.svelte.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/lib/api/identity-api.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { config } from '$config';
import { z } from 'zod';
import { logAPI } from '$lib/loggers';
import { type Fetch, SchemaError, fetchCacheOption, fetchError } from '$lib/utils';
import { z } from 'zod';
import { DateStringSchema, OptionalSchema } from './schema-helpers';

export const GUEST_PROVIDER_ID = 'guest';
2 changes: 1 addition & 1 deletion src/lib/app/QuickConfig.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts">
import InputGroup from '$atoms/InputGroup.svelte';
import LangSwitch from '$lib/i18n/LangSwitch.svelte';
import ThemeSwitch from '$lib/theme/ThemeSwitch.svelte';
import InputGroup from '$atoms/InputGroup.svelte';
</script>

<InputGroup variant="ghost">
6 changes: 3 additions & 3 deletions src/lib/i18n/LangSwitch.svelte
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<script lang="ts">
import Button from '$atoms/Button.svelte';
import InputGroup from '$atoms/InputGroup.svelte';
import Popper from '$atoms/Popper.svelte';
import * as flags from '$atoms/icons/flags';
import Button from '$components/atoms/Button.svelte';
import InputGroup from '$components/atoms/InputGroup.svelte';
import Popper from '$components/atoms/Popper.svelte';
import { languageStore, t } from './i18n.svelte';
const items = [
1 change: 1 addition & 0 deletions src/lib/loggers.ts
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@ import debug from 'debug';
// - window.localStorage.setItem('debug', 'designer:design');

export const logAPI = debug('app:api');
export const logResource = debug('app:resources');
export const logUser = debug('app:user');
export const logI18n = debug('app:i18n');

File renamed without changes.
2 changes: 1 addition & 1 deletion src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { browser } from '$app/environment';
import { type Snippet } from 'svelte';
import { browser } from '$app/environment';

export type Nullable<T> = T | null;
export function maybeNull<T>(): Nullable<T> {
43 changes: 25 additions & 18 deletions src/routes/(auth)/+layout.svelte
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
<script lang="ts">
import { type Snippet, onMount } from 'svelte';
import { afterNavigate, goto } from '$app/navigation';
import { page } from '$app/state';
import Button from '$atoms/Button.svelte';
import ErrorCard from '$atoms/ErrorCard.svelte';
import LoadingCard from '$atoms/LoadingCard.svelte';
import { currentUserStore } from '$lib/account/currentUser.svelte';
import { identityApi } from '$lib/api/identity-api';
import App from '$lib/app/App.svelte';
import AppContent from '$lib/app/AppContent.svelte';
import { t } from '$lib/i18n/i18n.svelte';
import { logUser } from '$lib/loggers';
import { type Snippet, onMount } from 'svelte';
import Button from '$atoms/Button.svelte';
import LoadingCard from '$atoms/LoadingCard.svelte';
import ErrorCard from '$components/ErrorCard.svelte';
import { setCurrentUserStore } from '$features/account/currentUser.svelte';
interface Props {
children: Snippet;
}
let { children }: Props = $props();
let currentUser = currentUserStore();
let currentUserStore = setCurrentUserStore({
load: () => identityApi.getCurrentUser(fetch),
startEmailConfirmation: () => identityApi.startEmailConfirmation(),
startEmailChange: (email: string) => identityApi.startEmailChange(email),
getLogoutUrl: (all: boolean, redirectUrl: string) => identityApi.getLogoutUrl(all, redirectUrl)
});
$effect(() => {
currentUserStore.refresh();
});
// For links we ask for prompt always, but once the prompt is completed, a silent parameter is added to the link URL
// not to start en infinite loop of prompts
@@ -47,10 +56,7 @@
});
$effect(() => {
if (currentUser.isNull) {
logUser('Refreshing current user...');
currentUser.refresh();
} else if (currentUser.isLoaded && !currentUser.isAuthenticated) {
if (currentUserStore.isContent && !currentUserStore.content.isAuthenticated) {
logUser('Login required');
goto(loginUrl);
} else if (isPromptForLink) {
@@ -61,8 +67,8 @@
onMount(() => {
const handleDocumentVisibility = async () => {
if (document.visibilityState === 'visible') {
logUser('Checking user after document focus...');
currentUser.refresh();
logUser('Refresh user after document focus...');
currentUserStore.refresh();
}
};
@@ -74,28 +80,29 @@
});
afterNavigate(() => {
currentUserStore().refresh();
logUser('Refresh user after navigation...');
currentUserStore.refresh();
});
</script>

<App>
{#if currentUser.error}
{#if currentUserStore.isError}
<AppContent>
<div class="flex h-full items-center justify-center">
<ErrorCard caption={$t('account.failedToLoadUserInfo')} error={currentUser.error}>
<ErrorCard caption={$t('account.failedToLoadUserInfo')} error={currentUserStore.error}>
{#snippet actions()}
<Button onclick={() => currentUser.refresh()}>{$t('common.retry')}</Button>
<Button onclick={() => currentUserStore.refresh()}>{$t('common.retry')}</Button>
{/snippet}
</ErrorCard>
</div>
</AppContent>
{:else if !currentUser.isLoaded || isPromptForLink}
{:else if currentUserStore.isEmpty || isPromptForLink}
<AppContent>
<div class="flex h-full items-center justify-center">
<LoadingCard label={$t('common.loading')} />
</div>
</AppContent>
{:else if currentUser.isAuthenticated}
{:else if currentUserStore.content.isAuthenticated}
{@render children()}
{/if}
</App>
83 changes: 30 additions & 53 deletions src/routes/(auth)/account/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
<script lang="ts">
import { page } from '$app/state';
import Button from '$atoms/Button.svelte';
import Modal from '$atoms/Modal.svelte';
import ActiveSessionsCard from '$lib/account/ActiveSessionsCard.svelte';
import ActiveTokensCard from '$lib/account/ActiveTokensCard.svelte';
import CurrentUserCard from '$lib/account/CurrentUserCard.svelte';
import LinkedIdentitiesCard from '$lib/account/LinkedIdentitiesCard.svelte';
import { currentUserStore } from '$lib/account/currentUser.svelte';
import { providerIcon } from '$lib/account/utils.svelte';
import { identityApi } from '$lib/api/identity-api';
import AppContent from '$lib/app/AppContent.svelte';
import { t } from '$lib/i18n/i18n.svelte';
import Stack from '$atoms/Stack.svelte';
import ActiveSessionsCard from '$features/account/ActiveSessionCard.svelte';
import ActiveTokensCard from '$features/account/ActiveTokenCard.svelte';
import CurrentUserCard from '$features/account/CurrentUserCard.svelte';
import LinkedIdentitiesCard from '$features/account/LinkedIdentityCard.svelte';
import { setActiveSessionStore } from '$features/account/activeSessionStore.svelte';
import { setActiveTokenStore } from '$features/account/activeTokenStore.svelte';
import { setLinkedIdentityStore } from '$features/account/linkedIdentityStore.svelte';
interface Props {
data: {
@@ -19,51 +17,30 @@
}
let { data }: Props = $props();
let currentUser = currentUserStore();
let showLink = $state(false);
let sessions = identityApi.getActiveSessions();
let activeTokeStore = setActiveTokenStore({
load: () => identityApi.getActiveTokens(),
revoke: (tokenHash: string) => identityApi.revokeToken(tokenHash)
});
activeTokeStore.refresh();
let identities = $state(identityApi.getLinkedIdentities());
let unlinkIdentity = async (provider: string, providerUserId: string) => {
await identityApi.unlinkIdentity(provider, providerUserId);
identities = identityApi.getLinkedIdentities();
};
let linkIdentity = () => {
showLink = true;
};
let activeSessionStore = setActiveSessionStore({
load: () => identityApi.getActiveSessions()
});
activeSessionStore.refresh();
let tokens = $state(identityApi.getActiveTokens());
let revokeToken = async (tokenHash: string) => {
await identityApi.revokeToken(tokenHash);
tokens = identityApi.getActiveTokens();
};
let linkedIdentityStore = setLinkedIdentityStore({
load: () => identityApi.getLinkedIdentities(),
unlink: (provider: string, providerUserId: string) => identityApi.unlinkIdentity(provider, providerUserId),
getLinkUrl: (provider: string, redirect: string) => identityApi.getExternalLinkUrl(provider, redirect)
});
linkedIdentityStore.refresh();
</script>

<AppContent class="my-auto flex flex-col items-center overflow-y-auto px-4">
<CurrentUserCard
context={{
fetchUser: async () => currentUser.user,
refreshUser: async () => currentUser.refresh(true),
startEmailConfirmation: async () => identityApi.startEmailConfirmation(),
startEmailChange: async (newEmail: string) => identityApi.startEmailChange(newEmail),
getLogoutUrl: (all: boolean, redirectUrl: string) => identityApi.getLogoutUrl(all, redirectUrl)
}}
/>
<ActiveSessionsCard sessions={() => sessions} />
<LinkedIdentitiesCard identities={() => identities} onUnlink={unlinkIdentity} onLink={linkIdentity} />
<ActiveTokensCard tokens={() => tokens} onRevoke={revokeToken} />

<Modal closeButton closeOnClickOutside caption={$t('account.linkTitle')} bind:isOpen={showLink} class="max-w-min">
{#each data.providers as provider}
<Button
variant="outline"
wide
startIcon={providerIcon(provider)}
class="mx-0"
href={identityApi.getExternalLinkUrl(provider, page.url.pathname)}
>
{provider}
</Button>
{/each}
</Modal>
<AppContent class="p-4">
<Stack spacing={4} align="center">
<CurrentUserCard />
<ActiveSessionsCard />
<LinkedIdentitiesCard providers={data.providers} />
<ActiveTokensCard />
</Stack>
</AppContent>
10 changes: 5 additions & 5 deletions src/routes/(auth)/builder/[room]/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/state';
import Button from '$components/atoms/Button.svelte';
import InputGroup from '$components/atoms/InputGroup.svelte';
import TextArea from '$components/atoms/TextArea.svelte';
import Typography from '$components/atoms/Typography.svelte';
import { builderApi } from '$lib/api/builder-api';
import { onMount } from 'svelte';
import Button from '$atoms/Button.svelte';
import InputGroup from '$atoms/InputGroup.svelte';
import TextArea from '$atoms/TextArea.svelte';
import Typography from '$atoms/Typography.svelte';
type ChatMessage = {
from: string;
2 changes: 1 addition & 1 deletion src/routes/(auth)/game/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script>
import Button from '$components/atoms/Button.svelte';
import AppContent from '$lib/app/AppContent.svelte';
import Button from '$atoms/Button.svelte';
</script>

<AppContent class="flex flex-col items-center justify-center">
13 changes: 5 additions & 8 deletions src/routes/(auth)/link/email-verify/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
<script lang="ts">
import { page } from '$app/state';
import Button from '$components/atoms/Button.svelte';
import ErrorCard from '$components/atoms/ErrorCard.svelte';
import { currentUserStore } from '$lib/account/currentUser.svelte';
import { identityApi } from '$lib/api/identity-api';
import AppContent from '$lib/app/AppContent.svelte';
import Button from '$atoms/Button.svelte';
import ErrorCard from '$components/ErrorCard.svelte';
import { getCurrentUserStore } from '$features/account/currentUser.svelte';
let currentUser = currentUserStore();
let currentUserStore = getCurrentUserStore();
const task = async () => {
console.log('completeEmailConfirmation', page.url.searchParams.get('token'));
await identityApi.completeEmailOperation(page.url.searchParams.get('token') ?? '');
console.log('refreshing user');
currentUser.refresh(true);
console.log('done');
currentUserStore.refresh({ force: true });
};
</script>

2 changes: 1 addition & 1 deletion src/routes/+error.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script>
import { page } from '$app/state';
import ErrorCard from '$atoms/ErrorCard.svelte';
import ErrorCard from '$components/ErrorCard.svelte';
</script>

<ErrorCard error={{ errorKind: 'other', message: page.error?.message ?? '' }} />
2 changes: 1 addition & 1 deletion src/routes/+layout.svelte
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<script lang="ts">
import { type Snippet } from 'svelte';
import { afterNavigate } from '$app/navigation';
import { refreshLanguage } from '$lib/i18n/i18n.svelte';
import { refreshTheme } from '$lib/theme/theme.svelte';
import { type Snippet } from 'svelte';
import '../app.css';
interface Props {
2 changes: 1 addition & 1 deletion src/routes/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts">
import Button from '$atoms/Button.svelte';
import App from '$lib/app/App.svelte';
import AppContent from '$lib/app/AppContent.svelte';
import Button from '$atoms/Button.svelte';
</script>

<App>
28 changes: 17 additions & 11 deletions src/routes/design/+layout.svelte
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<script lang="ts">
import { config } from '$config';
import QuickConfig from '$lib/app/QuickConfig.svelte';
import Box from '$atoms/Box.svelte';
import Typography from '$atoms/Typography.svelte';
import { Hamburger } from '$atoms/icons/common';
import { config } from '$config';
import QuickConfig from '$lib/app/QuickConfig.svelte';
import { Section, settingsStore } from './_components';
import { STYLE } from './_components/_currentSettings.svelte';
@@ -25,16 +25,14 @@
{ title: 'Colors', href: 'atoms/colors' },
{ title: 'Typography', href: 'atoms/typography' },
{ title: 'Icons', href: 'atoms/icons' },
{ title: 'ResourceFetch', href: 'atoms/resource-fetch' },
{ title: 'Boxes', href: 'atoms/boxes' },
{ title: 'Stacks', href: 'atoms/stacks' },
{ title: 'Grids', href: 'atoms/grids' },
{ title: 'Popper', href: 'atoms/popper' },
{ title: 'Key-Value Table', href: 'atoms/key-value-tables' },
{ title: 'Cards', href: 'atoms/cards' },
{ title: 'Modal', href: 'atoms/modals' },
{ title: 'Alerts', href: 'atoms/alerts' },
{ title: 'Helper Cards', href: 'atoms/helper-cards' },
{ title: 'Simple Menu', href: 'atoms/simple-menu' },
{ title: 'Extra Menu', href: 'atoms/extra-menu' }
{ title: 'Modal', href: 'atoms/modals' }
]
},
{
@@ -47,17 +45,25 @@
{ title: 'ComboButtons', href: 'atoms/combo-buttons' }
]
},
{
title: 'Components',
items: [
{ title: 'Alerts', href: 'components/alerts' },
{ title: 'Loading Cards', href: 'components/loading-cards' },
{ title: 'Error Cards', href: 'components/error-cards' }
]
},
config.environment === 'mock' && {
title: 'Utils',
items: [{ title: 'Mock test', href: 'utils/mock-test' }]
},
{
title: 'Account',
items: [
{ title: 'Current User Card', href: 'account/current-user' },
{ title: 'Linked Identities', href: 'account/linked-identities' },
{ title: 'Active Sessions', href: 'account/active-sessions' },
{ title: 'Active Tokens', href: 'account/active-tokens' }
{ title: 'Current User Card', href: 'features/account/current-user' },
{ title: 'Linked Identities', href: 'features/account/linked-identities' },
{ title: 'Active Sessions', href: 'features/account/active-sessions' },
{ title: 'Active Tokens', href: 'features/account/active-tokens' }
]
}
].filter(Boolean) as MenuItem[];
2 changes: 1 addition & 1 deletion src/routes/design/_components/_CheckBox.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts">
import CompileTailwindClasses from '$atoms/CompileTailwindClasses.svelte';
import Typography from '$atoms/Typography.svelte';
import CompileTailwindClasses from '$components/atoms/CompileTailwindClasses.svelte';
import { STYLE } from './_currentSettings.svelte';
interface Props {
9 changes: 9 additions & 0 deletions src/routes/design/_components/_MediaSize.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<div class="sticky top-0 z-10 flex justify-center bg-primary p-1 text-on-primary">
<div class="inline-block">
Size:
<span class="inline-block">XS</span>
<span class="hidden md:inline-block">MD</span>
<span class="hidden lg:inline-block">LG</span>
<span class="hidden xl:inline-block">XL</span>
</div>
</div>
2 changes: 1 addition & 1 deletion src/routes/design/_components/_Story.svelte
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@
center: 'flex-col items-center'
};
let storyClass = $derived(twMerge('flex w-full gap-2 p-4', variantClass[variant]));
let storyClass = $derived(twMerge('flex w-full gap-4 p-4', variantClass[variant]));
</script>

<div class={storyClass}>
2 changes: 1 addition & 1 deletion src/routes/design/_components/_currentSettings.svelte.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Nullable } from '$lib/utils';
import { type Snippet, onDestroy } from 'svelte';
import type { Nullable } from '$lib/utils';

let settings = $state<Nullable<Snippet>>(null);

3 changes: 2 additions & 1 deletion src/routes/design/_components/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import CheckBox from './_CheckBox.svelte';
import MediaSize from './_MediaSize.svelte';
import Section from './_Section.svelte';
import Select from './_Select.svelte';
import Separator from './_Separator.svelte';
import Story from './_Story.svelte';
import { settingsStore } from './_currentSettings.svelte';
import { lorem } from './_lorem';

export { settingsStore, lorem, CheckBox, Section, Select, Separator, Story };
export { settingsStore, lorem, CheckBox, Section, Select, Separator, Story, MediaSize };
42 changes: 0 additions & 42 deletions src/routes/design/account/active-sessions/+page.svelte

This file was deleted.

108 changes: 0 additions & 108 deletions src/routes/design/account/active-tokens/+page.svelte

This file was deleted.

153 changes: 0 additions & 153 deletions src/routes/design/account/current-user/+page.svelte

This file was deleted.

112 changes: 0 additions & 112 deletions src/routes/design/account/linked-identities/+page.svelte

This file was deleted.

10 changes: 10 additions & 0 deletions src/routes/design/atoms/boxes/+page.svelte
Original file line number Diff line number Diff line change
@@ -105,6 +105,16 @@
</Box>
</Box>

<Box border shadow legend="Legend">
<Box border legend="Legend">
<Box border legend="Legend">
<Box border legend="Legend">Inner most</Box>
</Box>
<Box border legend={{ text: 'Legend', color, size: 'xl' }}>Custom legend</Box>
<Box border variant={{ color }} legend="Legend">Variant with legend</Box>
</Box>
</Box>

<Box border shadow>
Some custom class with dense layout
<Box border class="flex flex-row flex-wrap justify-center">
207 changes: 117 additions & 90 deletions src/routes/design/atoms/buttons/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<script lang="ts">
import { logDesigner } from '$lib/loggers';
import Box from '$atoms/Box.svelte';
import Button from '$atoms/Button.svelte';
import Stack from '$atoms/Stack.svelte';
import { Spinner } from '$atoms/icons/animated';
import { Firefox } from '$atoms/icons/clients';
import { Settings, Warning } from '$atoms/icons/common';
import { Twitter } from '$atoms/icons/social';
import { type ActionColor, actionColorList, sizeList } from '$atoms/types';
import { logDesigner } from '$lib/loggers';
import { Select, Story, settingsStore } from '../../_components';
import Separator from '../../_components/_Separator.svelte';
let color = $state<ActionColor>('primary');
let action = $state('click');
@@ -38,53 +38,65 @@
{/snippet}

<Story variant="dense">
<Box border class="flex h-max flex-col">
{#each sizeList as size}
<Button {size} {color} {...btnAction}>
Button-{size}
</Button>
{/each}
<Box border class="h-max">
<Stack>
{#each sizeList as size}
<Button {size} {color} {...btnAction}>
Button-{size}
</Button>
{/each}
</Stack>
</Box>

<Box border class="flex h-max flex-col">
<Button {color} {onclick}>Active button</Button>
<Button {color} disabled {onclick}>Disabled button</Button>
<Button {color} {href}>Active link</Button>
<Button {color} disabled {href}>Disabled link</Button>
<Box border class="h-max">
<Stack>
<Button {color} {onclick}>Active button</Button>
<Button {color} disabled {onclick}>Disabled button</Button>
<Button {color} {href}>Active link</Button>
<Button {color} disabled {href}>Disabled link</Button>
</Stack>
</Box>

<Box border class="flex h-max flex-col">
{#each sizeList as size}
<div>
<Button {size} {color} startIcon={Settings} {...btnAction} />
<Button variant="outline" {size} {color} startIcon={Settings} {...btnAction} />
<Button variant="ghost" {size} {color} startIcon={Settings} {...btnAction} />
</div>
{/each}
<Box border class="h-max">
<Stack>
{#each sizeList as size}
<Stack direction="row">
<Button {size} {color} startIcon={Settings} {...btnAction} />
<Button variant="outline" {size} {color} startIcon={Settings} {...btnAction} />
<Button variant="ghost" {size} {color} startIcon={Settings} {...btnAction} />
</Stack>
{/each}
</Stack>
</Box>

<Box border class="flex h-max flex-col">
{#each sizeList as size}
<Button {size} {color} startIcon={Twitter} {...btnAction}>
Button-{size}
</Button>
{/each}
<Box border class="h-max">
<Stack>
{#each sizeList as size}
<Button {size} {color} startIcon={Twitter} {...btnAction}>
Button-{size}
</Button>
{/each}
</Stack>
</Box>

<Box border class="flex h-max flex-col">
{#each sizeList as size}
<Button {size} {color} endIcon={Twitter} {...btnAction}>
Button-{size}
</Button>
{/each}
<Box border class="h-max">
<Stack>
{#each sizeList as size}
<Button {size} {color} endIcon={Twitter} {...btnAction}>
Button-{size}
</Button>
{/each}
</Stack>
</Box>

<Box border class="flex h-max flex-col">
{#each sizeList as size}
<Button {size} {color} startIcon={Twitter} endIcon={Twitter} {...btnAction}>
Button-{size}
</Button>
{/each}
<Box border class="h-max">
<Stack>
{#each sizeList as size}
<Button {size} {color} startIcon={Twitter} endIcon={Twitter} {...btnAction}>
Button-{size}
</Button>
{/each}
</Stack>
</Box>

<Box border class="flex h-max w-96 flex-col">
@@ -95,85 +107,100 @@
{/each}
</Box>

<Box border class="flex h-max flex-col">
<Button {color} {...btnAction} startIcon={Settings} />
<Button {color} {...btnAction} startIcon={Settings}>Button</Button>
<Button {color} {...btnAction} endIcon={Settings}>Button</Button>
<Button {color} {...btnAction} startIcon={Settings} endIcon={Spinner}>Button</Button>
<Box border class="h-max">
<Stack>
<Button {color} {...btnAction} startIcon={Settings} />
<Button {color} {...btnAction} startIcon={Settings}>Button</Button>
<Button {color} {...btnAction} endIcon={Settings}>Button</Button>
<Button {color} {...btnAction} startIcon={Settings} endIcon={Spinner}>Button</Button>
</Stack>
</Box>

<Box border class="flex h-max flex-row">
<div class="flex flex-col justify-center">
<Button {color} {...btnAction}>Button</Button>
<Button {color} disabled {...btnAction}>Button</Button>
</div>
<div class="flex flex-col justify-center">
<Button {color} variant="outline" {...btnAction}>Button</Button>
<Button {color} variant="outline" disabled {...btnAction}>Button</Button>
</div>
<div class="flex flex-col justify-center">
<Button {color} variant="ghost" {...btnAction}>Button</Button>
<Button {color} variant="ghost" disabled {...btnAction}>Button</Button>
</div>
<Box border class="h-max">
<Stack direction="row">
<Stack justify="center">
<Button {color} {...btnAction}>Button</Button>
<Button {color} disabled {...btnAction}>Button</Button>
</Stack>
<Stack justify="center">
<Button {color} variant="outline" {...btnAction}>Button</Button>
<Button {color} variant="outline" disabled {...btnAction}>Button</Button>
</Stack>
<Stack justify="center">
<Button {color} variant="ghost" {...btnAction}>Button</Button>
<Button {color} variant="ghost" disabled {...btnAction}>Button</Button>
</Stack>
</Stack>
</Box>

<Box border class="flex h-max flex-wrap">
<div class="flex flex-col justify-center">
<Stack justify="center">
<Button {color} {...btnAction} startIcon={Firefox}>Button</Button>
<Button {color} disabled {...btnAction} startIcon={Firefox}>Button</Button>
</div>
<div class="flex flex-col justify-center">
</Stack>
<Stack justify="center">
<Button {color} variant="outline" {...btnAction} startIcon={Firefox}>Button</Button>
<Button {color} variant="outline" disabled {...btnAction} startIcon={Firefox}>Button</Button>
</div>
<div class="flex flex-col justify-center">
</Stack>
<Stack justify="center">
<Button {color} variant="ghost" {...btnAction} startIcon={Firefox}>Button</Button>
<Button {color} variant="ghost" disabled {...btnAction} startIcon={Firefox}>Button</Button>
</div>
</Stack>
</Box>
<Box border class="flex h-max flex-wrap">
<div class="flex flex-col justify-center">
<Stack justify="center">
<Button {color} {...btnAction} startIcon={Warning}>Button</Button>
<Button {color} disabled {...btnAction} startIcon={Warning}>Button</Button>
</div>
<div class="flex flex-col justify-center">
</Stack>
<Stack justify="center">
<Button {color} variant="outline" {...btnAction} startIcon={Warning}>Button</Button>
<Button {color} variant="outline" disabled {...btnAction} startIcon={Warning}>Button</Button>
</div>
<div class="flex flex-col justify-center">
</Stack>
<Stack justify="center">
<Button {color} variant="ghost" {...btnAction} startIcon={Warning}>Button</Button>
<Button {color} variant="ghost" disabled {...btnAction} startIcon={Warning}>Button</Button>
</div>
</Stack>
</Box>

<Separator />

<div class="border p-2">
<p>Button without color and without a parent box</p>
<Button>Default</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
</div>
<Box border>
<p>Button without color takes the color of the parent box</p>
<Button>Default</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Box border>
<fieldset class="border p-2 h-max">
<legend>No parent box</legend>
<Stack direction="row">
<Button>Default</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Box border>
</Stack>
</fieldset>
<Box border class="h-max" legend="Parent box">
<Stack>
<Stack direction="row">
<Button>Default</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
</Stack>
<Box border>
<Stack>
<Stack direction="row">
<Button>Default</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
</Stack>
<Box border>
<Stack direction="row">
<Button>Default</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
</Stack>
</Box>
</Stack>
</Box>
</Box>
<Box border shadow variant={{ color }}>
<Button>Default</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
</Box>
<Box border shadow variant={{ color }}>
<Stack direction="row">
<Button>Default</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
</Stack>
</Box>
</Stack>
</Box>
<div id="bottom"></div>
</Story>
2 changes: 1 addition & 1 deletion src/routes/design/atoms/colors/_colorSample.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts">
import { type ActionColor, type ContainerColor } from '$atoms/types';
import { themeStore } from '$lib/theme/theme.svelte';
import { type ActionColor, type ContainerColor } from '$atoms/types';
interface Props {
color: ActionColor | ContainerColor;
8 changes: 4 additions & 4 deletions src/routes/design/atoms/combo-buttons/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<script lang="ts">
import { logDesigner } from '$lib/loggers';
import Box from '$atoms/Box.svelte';
import ComboButton from '$atoms/ComboButton.svelte';
import { range } from '$atoms/types';
import { logDesigner } from '$lib/loggers';
import { Story } from '../../_components';
let current = $state(3);
@@ -30,15 +30,15 @@
</script>

<Story variant="center">
<Box border compact class="p-1">
<Box border compact class="p-4">
<ComboButton bind:current {items} />
</Box>

<Box border compact class="w-full p-1">
<Box border compact class="w-full p-4">
<ComboButton wide bind:current {items} />
</Box>

<Box border compact class="p-1">
<Box border compact class="p-4">
<ComboButton current={3} items={range(0, 20).map((x) => ({ caption: `Item ${x}` }))} />
</Box>
</Story>
60 changes: 0 additions & 60 deletions src/routes/design/atoms/extra-menu/+page.svelte

This file was deleted.

Loading