feat: redo settings and all org pages. (#601)

* feat: new components, deploy storybook

* refactor: simplify component field definitions and enhance button loading state

- Removed unnecessary type imports and simplified the FIELDS array definitions across multiple components in Storybook.
- Updated button component to use a loading state instead of isLoading, improving clarity and consistency in props.
- Adjusted button stories to reflect the new loading prop and enhanced the button's loading visual feedback.
- Minor CSS adjustments for better layout and styling consistency.

* chore: add field, card, password components

* feat: improve page component & update AuthUI path

* chore: update bits-ui to version 2.14.4 and all org related UI's

- Updated bits-ui dependency in pnpm-lock.yaml and package.json to version 2.14.4.
- Refactored imports for RoleBasedSecurity and UpgradeBanner components to use the new ui feature directory.
- Removed unused Course components and related files to clean up the codebase.
- Enhanced various components to improve consistency and maintainability.

* chore: move all setting buttons to the top and update auth ui styles

* chore: remove svelte-loading-spinners and replace with @cio/ui/base/spinner

- Removed all instances of svelte-loading-spinners from the project.
- Updated components to use the new Spinner from @cio/ui/base/spinner.
- Cleaned up pnpm-lock.yaml and package.json to reflect the removal of svelte-loading-spinners.
- Added new CSS animations for rocket launch effects in app.css.
- Enhanced email change flow with updated verification messages in translations.

* chore: remove supabase file upload from dashboard and move to r2

* chore: update organization API and UI components

- Added new API methods for organization plan operations and team management.
- Introduced middleware for API key authentication and organization admin checks.
- Refactored organization-related components to utilize the new API structure.
- Updated various UI components to improve consistency and maintainability.
- Enhanced error handling and validation for organization-related actions.
- Removed deprecated services and streamlined the codebase.
This commit is contained in:
Rotimi Best
2025-12-02 09:54:38 +01:00
committed by GitHub
parent 531516a8f7
commit c7854efae3
350 changed files with 13928 additions and 3895 deletions
@@ -0,0 +1,20 @@
<script lang="ts">
import { cn } from '../../tools';
import type { ComponentProps } from 'svelte';
import { Separator } from '../separator';
let {
ref = $bindable(null),
class: className,
orientation = 'vertical',
...restProps
}: ComponentProps<typeof Separator> = $props();
</script>
<Separator
bind:ref
data-slot="button-group-separator"
{orientation}
class={cn('ui:bg-input ui:relative ui:m-0! ui:self-stretch ui:data-[orientation=vertical]:h-auto', className)}
{...restProps}
/>
@@ -0,0 +1,30 @@
<script lang="ts">
import { cn, type WithElementRef } from '../../tools';
import type { HTMLAttributes } from 'svelte/elements';
import type { Snippet } from 'svelte';
let {
ref = $bindable(null),
class: className,
child,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
child?: Snippet<[{ props: Record<string, unknown> }]>;
} = $props();
const mergedProps = $derived({
...restProps,
class: cn(
"ui:bg-muted ui:shadow-xs ui:flex ui:items-center ui:gap-2 ui:rounded-md ui:border ui:px-4 ui:text-sm ui:font-medium ui:[&_svg:not([class*='size-'])]:size-4 ui:[&_svg]:pointer-events-none",
className
)
});
</script>
{#if child}
{@render child({ props: mergedProps })}
{:else}
<div bind:this={ref} {...mergedProps}>
{@render mergedProps.children?.()}
</div>
{/if}
@@ -0,0 +1,46 @@
<script lang="ts" module>
import { tv, type VariantProps } from 'tailwind-variants';
export const buttonGroupVariants = tv({
base: "ui:flex ui:w-fit ui:items-stretch ui:has-[>[data-slot=button-group]]:gap-2 ui:[&>*]:focus-visible:relative ui:[&>*]:focus-visible:z-10 ui:has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-e-md ui:[&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit ui:[&>input]:flex-1",
variants: {
orientation: {
horizontal:
'ui:[&>*:not(:first-child)]:rounded-s-none ui:[&>*:not(:first-child)]:border-s-0 ui:[&>*:not(:last-child)]:rounded-e-none',
vertical:
'ui:flex-col ui:[&>*:not(:first-child)]:rounded-t-none ui:[&>*:not(:first-child)]:border-t-0 ui:[&>*:not(:last-child)]:rounded-b-none'
}
},
defaultVariants: {
orientation: 'horizontal'
}
});
export type ButtonGroupOrientation = VariantProps<typeof buttonGroupVariants>['orientation'];
</script>
<script lang="ts">
import { cn, type WithElementRef } from '../../tools';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
orientation = 'horizontal',
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
orientation?: ButtonGroupOrientation;
} = $props();
</script>
<div
bind:this={ref}
role="group"
data-slot="button-group"
data-orientation={orientation}
class={cn(buttonGroupVariants({ orientation }), className)}
{...restProps}
>
{@render children?.()}
</div>
@@ -0,0 +1,13 @@
import Root from "./button-group.svelte";
import Text from "./button-group-text.svelte";
import Separator from "./button-group-separator.svelte";
export {
Root,
Text,
Separator,
//
Root as ButtonGroup,
Text as ButtonGroupText,
Separator as ButtonGroupSeparator,
};
+85 -51
View File
@@ -1,21 +1,20 @@
<script lang="ts" module>
import { cn, type WithElementRef } from '../../tools';
import type { WithChildren, WithoutChildren } from 'bits-ui';
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
import { type VariantProps, tv } from 'tailwind-variants';
import { Spinner } from '../spinner';
export const buttonVariants = tv({
base: "ui:focus-visible:border-ring ui:cursor-pointer ui:focus-visible:ring-ring/50 ui:aria-invalid:ring-destructive/20 ui:dark:aria-invalid:ring-destructive/40 ui:aria-invalid:border-destructive ui:inline-flex ui:shrink-0 ui:items-center ui:justify-center ui:gap-2 ui:whitespace-nowrap ui:rounded-md ui:text-sm ui:font-medium ui:outline-none ui:transition-all ui:focus-visible:ring-[3px] ui:disabled:pointer-events-none ui:disabled:opacity-50 ui:aria-disabled:pointer-events-none ui:aria-disabled:opacity-50 ui:[&_svg:not([class*='size-'])]:size-4 ui:[&_svg]:pointer-events-none ui:[&_svg]:shrink-0",
base: "ui:aria-invalid:ring-destructive/20 ui:dark:aria-invalid:ring-destructive/40 ui:aria-invalid:border-destructive ui:focus-visible:border-ring ui:focus-visible:ring-ring/50 ui:relative ui:inline-flex ui:shrink-0 ui:items-center ui:justify-center ui:gap-2 ui:overflow-hidden ui:rounded-md ui:text-sm ui:font-medium ui:whitespace-nowrap ui:outline-hidden ui:transition-all ui:select-none ui:focus-visible:ring-[3px] ui:disabled:pointer-events-none ui:disabled:opacity-50 ui:[&_svg]:pointer-events-none ui:[&_svg]:shrink-0 ui:[&_svg:not([class*='size-'])]:size-4 ui:cursor-pointer",
variants: {
variant: {
default: 'ui:bg-primary ui:text-primary-foreground ui:shadow-xs ui:hover:bg-primary/90',
default: 'ui:bg-primary ui:text-primary-foreground ui:[a&]:hover:bg-primary/90 ui:shadow-2xs',
destructive:
'ui:bg-destructive ui:shadow-xs ui:hover:bg-destructive/90 ui:focus-visible:ring-destructive/20 ui:dark:focus-visible:ring-destructive/40 ui:dark:bg-destructive/60 ui:text-white',
'ui:bg-destructive ui:[a&]:hover:bg-destructive/90 ui:focus-visible:ring-destructive/20 ui:dark:bg-destructive/60 ui:dark:focus-visible:ring-destructive/40 ui:text-white ui:shadow-2xs',
outline:
'ui:bg-background ui:shadow-xs ui:hover:bg-accent ui:hover:text-accent-foreground ui:dark:bg-input/30 ui:dark:border-input ui:dark:hover:bg-input/50 ui:border',
secondary: 'ui:bg-secondary ui:text-secondary-foreground ui:shadow-xs ui:hover:bg-secondary/80',
ghost: 'ui:hover:bg-accent ui:hover:text-accent-foreground ui:dark:hover:bg-accent/50',
link: 'ui:text-primary ui:underline-offset-4 ui:hover:underline'
'ui:bg-background ui:[a&]:hover:bg-accent ui:[a&]:hover:text-accent-foreground ui:dark:border-input ui:dark:bg-input/30 ui:dark:hover:bg-input/50 ui:border ui:shadow-2xs',
secondary: 'ui:bg-secondary ui:text-secondary-foreground ui:[a&]:hover:bg-secondary/80 ui:shadow-2xs',
ghost: 'ui:[a&]:hover:bg-accent ui:[a&]:hover:text-accent-foreground ui:dark:[a&]:hover:bg-accent/50',
link: 'ui:text-primary ui:underline-offset-4 ui:[a&]:hover:underline'
},
size: {
default: 'ui:h-9 ui:px-4 ui:py-2 ui:has-[>svg]:px-3',
@@ -35,58 +34,93 @@
export type ButtonVariant = VariantProps<typeof buttonVariants>['variant'];
export type ButtonSize = VariantProps<typeof buttonVariants>['size'];
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
WithElementRef<HTMLAnchorAttributes> & {
variant?: ButtonVariant;
size?: ButtonSize;
isLoading?: boolean;
export type ButtonPropsWithoutHTML = WithChildren<{
ref?: HTMLElement | null;
variant?: ButtonVariant;
size?: ButtonSize;
loading?: boolean;
onClickPromise?: (
e: MouseEvent & {
currentTarget: EventTarget & HTMLButtonElement;
}
) => Promise<void>;
}>;
export type AnchorElementProps = ButtonPropsWithoutHTML &
WithoutChildren<Omit<HTMLAnchorAttributes, 'href' | 'type'>> & {
href: HTMLAnchorAttributes['href'];
type?: never;
disabled?: HTMLButtonAttributes['disabled'];
};
export type ButtonElementProps = ButtonPropsWithoutHTML &
WithoutChildren<Omit<HTMLButtonAttributes, 'type' | 'href'>> & {
type?: HTMLButtonAttributes['type'];
href?: never;
disabled?: HTMLButtonAttributes['disabled'];
};
export type ButtonProps = AnchorElementProps | ButtonElementProps;
</script>
<script lang="ts">
import { cn } from '../../tools';
import LoaderCircleIcon from '@lucide/svelte/icons/loader-circle';
let {
class: className,
ref = $bindable(null),
variant = 'default',
size = 'default',
ref = $bindable(null),
href = undefined,
isLoading = false,
type = 'button',
loading = false,
disabled = false,
tabindex = 0,
onclick,
onClickPromise,
class: className,
children,
...restProps
...rest
}: ButtonProps = $props();
let disabled = $derived(restProps.disabled || isLoading);
</script>
{#if href}
<a
bind:this={ref}
data-slot="button"
class={cn(buttonVariants({ variant, size }), className)}
href={disabled ? undefined : href}
aria-disabled={disabled}
role={disabled ? 'link' : undefined}
tabindex={disabled ? -1 : undefined}
{...restProps}
>
{#if isLoading}
<Spinner />
{/if}
{@render children?.()}
</a>
{:else}
<button
bind:this={ref}
data-slot="button"
class={cn(buttonVariants({ variant, size }), className)}
{type}
{disabled}
{...restProps}
>
{#if isLoading}
<Spinner />
{/if}
{@render children?.()}
</button>
{/if}
<!-- This approach to disabled links is inspired by bits-ui see: https://github.com/huntabyte/bits-ui/pull/1055 -->
<svelte:element
this={href ? 'a' : 'button'}
{...rest}
data-slot="button"
type={href ? undefined : type}
href={href && !disabled ? href : undefined}
disabled={href ? undefined : disabled || loading}
aria-disabled={href ? disabled : undefined}
role={href && disabled ? 'link' : undefined}
tabindex={href && disabled ? -1 : tabindex}
class={cn(buttonVariants({ variant, size }), className)}
bind:this={ref}
onclick={async (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
e: any
) => {
onclick?.(e);
if (type === undefined) return;
if (onClickPromise) {
loading = true;
await onClickPromise(e);
loading = false;
}
}}
>
{#if type !== undefined && loading}
<div class="ui:absolute ui:flex ui:size-full ui:place-items-center ui:justify-center ui:bg-inherit">
<div class="ui:flex ui:animate-spin ui:place-items-center ui:justify-center">
<LoaderCircleIcon class="ui:size-4" />
</div>
</div>
<span class="ui:sr-only">Loading</span>
{/if}
{@render children?.()}
</svelte:element>
+23 -13
View File
@@ -1,17 +1,27 @@
/*
Installed from @ieedan/shadcn-svelte-extras
*/
import Root, {
type ButtonProps,
type ButtonSize,
type ButtonVariant,
buttonVariants,
} from "./button.svelte";
type ButtonProps,
type ButtonSize,
type ButtonVariant,
type AnchorElementProps,
type ButtonElementProps,
type ButtonPropsWithoutHTML,
buttonVariants
} from './button.svelte';
export {
Root,
type ButtonProps as Props,
//
Root as Button,
buttonVariants,
type ButtonProps,
type ButtonSize,
type ButtonVariant,
Root,
type ButtonProps as Props,
//
Root as Button,
buttonVariants,
type ButtonProps,
type ButtonSize,
type ButtonVariant,
type AnchorElementProps,
type ButtonElementProps,
type ButtonPropsWithoutHTML
};
@@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from '../../tools';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-action"
class={cn('ui:col-start-2 ui:row-span-2 ui:row-start-1 ui:self-start ui:justify-self-end', className)}
{...restProps}
>
{@render children?.()}
</div>
@@ -0,0 +1,15 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn, type WithElementRef } from '../../tools';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div bind:this={ref} data-slot="card-content" class={cn('ui:px-6', className)} {...restProps}>
{@render children?.()}
</div>
@@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn, type WithElementRef } from '../../tools';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
</script>
<p
bind:this={ref}
data-slot="card-description"
class={cn('ui:text-muted-foreground ui:text-sm', className)}
{...restProps}
>
{@render children?.()}
</p>
@@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from '../../tools';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-footer"
class={cn('ui:[.border-t]:pt-6 ui:flex ui:items-center ui:px-6', className)}
{...restProps}
>
{@render children?.()}
</div>
@@ -0,0 +1,23 @@
<script lang="ts">
import { cn, type WithElementRef } from '../../tools';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-header"
class={cn(
'@container/card-header ui:has-data-[slot=card-action]:grid-cols-[1fr_auto] ui:[.border-b]:pb-6 ui:grid ui:auto-rows-min ui:grid-rows-[auto_auto] ui:items-start ui:gap-1.5 ui:px-6',
className
)}
{...restProps}
>
{@render children?.()}
</div>
@@ -0,0 +1,15 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn, type WithElementRef } from '../../tools';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div bind:this={ref} data-slot="card-title" class={cn('ui:font-semibold ui:leading-none', className)} {...restProps}>
{@render children?.()}
</div>
+23
View File
@@ -0,0 +1,23 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn, type WithElementRef } from '../../tools';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card"
class={cn(
'ui:bg-card ui:text-card-foreground ui:flex ui:flex-col ui:gap-6 ui:rounded-xl ui:border ui:py-6 ui:shadow-sm',
className
)}
{...restProps}
>
{@render children?.()}
</div>
+25
View File
@@ -0,0 +1,25 @@
import Root from "./card.svelte";
import Content from "./card-content.svelte";
import Description from "./card-description.svelte";
import Footer from "./card-footer.svelte";
import Header from "./card-header.svelte";
import Title from "./card-title.svelte";
import Action from "./card-action.svelte";
export {
Root,
Content,
Description,
Footer,
Header,
Title,
Action,
//
Root as Card,
Content as CardContent,
Description as CardDescription,
Footer as CardFooter,
Header as CardHeader,
Title as CardTitle,
Action as CardAction,
};
@@ -0,0 +1,74 @@
<!--
Installed from @ieedan/shadcn-svelte-extras
-->
<script lang="ts">
import { Button } from '../button';
import { UseClipboard } from '../../hooks/use-clipboard.svelte';
import { cn } from '../../tools';
import CheckIcon from '@lucide/svelte/icons/check';
import CopyIcon from '@lucide/svelte/icons/copy';
import XIcon from '@lucide/svelte/icons/x';
import { scale } from 'svelte/transition';
import type { CopyButtonProps } from './types';
let {
ref = $bindable(null),
text,
icon,
animationDuration = 500,
variant = 'ghost',
size = 'icon',
onCopy,
class: className,
tabindex = -1,
children,
...rest
}: CopyButtonProps = $props();
// this way if the user passes text then the button will be the default size
if (size === 'icon' && children) {
size = 'default';
}
const clipboard = new UseClipboard();
</script>
<Button
{...rest}
bind:ref
{variant}
{size}
{tabindex}
class={cn('ui:flex ui:items-center ui:gap-2', className)}
type="button"
name="copy"
onclick={async () => {
// text prop is reactive, so it will use the latest value when clicked
const status = await clipboard.copy(text);
onCopy?.(status);
}}
>
{#if clipboard.status === 'success'}
<div in:scale={{ duration: animationDuration, start: 0.85 }}>
<CheckIcon tabindex={-1} />
<span class="ui:sr-only">Copied</span>
</div>
{:else if clipboard.status === 'failure'}
<div in:scale={{ duration: animationDuration, start: 0.85 }}>
<XIcon tabindex={-1} />
<span class="sr-only">Failed to copy</span>
</div>
{:else}
<div in:scale={{ duration: animationDuration, start: 0.85 }}>
{#if icon}
{@render icon()}
{:else}
<CopyIcon tabindex={-1} />
{/if}
<span class="sr-only">Copy</span>
</div>
{/if}
{@render children?.()}
</Button>
@@ -0,0 +1,7 @@
/*
Installed from @ieedan/shadcn-svelte-extras
*/
import CopyButton from './copy-button.svelte';
export { CopyButton };
+22
View File
@@ -0,0 +1,22 @@
/*
Installed from @ieedan/shadcn-svelte-extras
*/
import type { WithChildren, WithoutChildren } from 'bits-ui';
import type { ButtonPropsWithoutHTML } from '../button';
import type { HTMLAttributes } from 'svelte/elements';
import type { Snippet } from 'svelte';
import type { UseClipboard } from '../../hooks/use-clipboard.svelte';
export type CopyButtonPropsWithoutHTML = WithChildren<
Pick<ButtonPropsWithoutHTML, 'size' | 'variant'> & {
ref?: HTMLButtonElement | null;
text: string;
icon?: Snippet<[]>;
animationDuration?: number;
onCopy?: (status: UseClipboard['status']) => void;
}
>;
export type CopyButtonProps = CopyButtonPropsWithoutHTML & WithoutChildren<HTMLAttributes<HTMLButtonElement>>;
@@ -3,8 +3,8 @@
import MoonIcon from '@lucide/svelte/icons/moon';
import { resetMode, setMode } from 'mode-watcher';
import * as DropdownMenu from '$src/base/dropdown-menu';
import { buttonVariants } from '$src/base/button';
import * as DropdownMenu from '../dropdown-menu';
import { buttonVariants } from '../button';
</script>
<DropdownMenu.Root>
@@ -33,7 +33,7 @@
{@render children?.()}
{#if showCloseButton}
<DialogPrimitive.Close
class="ui:ring-offset-background ui:focus:ring-ring ui:rounded-xs ui:focus:outline-hidden ui:absolute ui:top-4 ui:opacity-70 ui:transition-opacity ui:hover:opacity-100 ui:focus:ring-2 ui:focus:ring-offset-2 ui:disabled:pointer-events-none ui:[&_svg:not([class*='size-'])]:size-4 ui:[&_svg]:pointer-events-none ui:[&_svg]:shrink-0 end-4"
class="ui:ring-offset-background ui:focus:ring-ring ui:rounded-xs ui:focus:outline-hidden ui:absolute ui:top-4 ui:opacity-70 ui:transition-opacity ui:hover:opacity-100 ui:focus:ring-2 ui:focus:ring-offset-2 ui:disabled:pointer-events-none ui:[&_svg:not([class*='size-'])]:size-4 ui:[&_svg]:pointer-events-none ui:[&_svg]:shrink-0 ui:end-4 ui:cursor-pointer"
>
<XIcon />
<span class="sr-only">Close</span>
@@ -13,7 +13,7 @@
<div
bind:this={ref}
data-slot="dialog-header"
class={cn('ui:flex ui:flex-col ui:gap-2 ui:text-center ui:sm:text-left', className)}
class={cn('ui:flex ui:flex-col ui:gap-2 ui:text-left', className)}
{...restProps}
>
{@render children?.()}
@@ -0,0 +1,23 @@
<script lang="ts">
import { cn, type WithElementRef } from '../../tools';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="empty-content"
class={cn(
'ui:flex ui:w-full ui:min-w-0 ui:max-w-sm ui:flex-col ui:items-center ui:gap-4 ui:text-balance ui:text-sm',
className
)}
{...restProps}
>
{@render children?.()}
</div>
@@ -0,0 +1,23 @@
<script lang="ts">
import { cn, type WithElementRef } from '../../tools';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="empty-description"
class={cn(
'ui:text-muted-foreground ui:[&>a:hover]:text-primary ui:text-sm/relaxed ui:[&>a]:underline ui:[&>a]:underline-offset-4',
className
)}
{...restProps}
>
{@render children?.()}
</div>
@@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from '../../tools';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="empty-header"
class={cn('ui:flex ui:max-w-sm ui:flex-col ui:items-center ui:gap-2 ui:text-center', className)}
{...restProps}
>
{@render children?.()}
</div>
@@ -0,0 +1,41 @@
<script lang="ts" module>
import { tv, type VariantProps } from 'tailwind-variants';
export const emptyMediaVariants = tv({
base: 'ui:mb-2 ui:flex ui:shrink-0 ui:items-center ui:justify-center ui:[&_svg]:pointer-events-none ui:[&_svg]:shrink-0',
variants: {
variant: {
default: 'ui:bg-transparent',
icon: "ui:bg-muted ui:text-foreground ui:flex ui:size-10 ui:shrink-0 ui:items-center ui:justify-center ui:rounded-lg ui:[&_svg:not([class*='size-'])]:size-6"
}
},
defaultVariants: {
variant: 'default'
}
});
export type EmptyMediaVariant = VariantProps<typeof emptyMediaVariants>['variant'];
</script>
<script lang="ts">
import { cn, type WithElementRef } from '../../tools';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
variant = 'default',
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { variant?: EmptyMediaVariant } = $props();
</script>
<div
bind:this={ref}
data-slot="empty-icon"
data-variant={variant}
class={cn(emptyMediaVariants({ variant }), className)}
{...restProps}
>
{@render children?.()}
</div>
@@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from '../../tools';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="empty-title"
class={cn('ui:text-lg ui:font-medium ui:tracking-tight', className)}
{...restProps}
>
{@render children?.()}
</div>
+23
View File
@@ -0,0 +1,23 @@
<script lang="ts">
import { cn, type WithElementRef } from '../../tools';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="empty"
class={cn(
'ui:flex ui:min-w-0 ui:flex-1 ui:flex-col ui:items-center ui:justify-center ui:gap-6 ui:text-balance ui:rounded-lg ui:border-dashed ui:p-6 ui:text-center ui:md:p-12',
className
)}
{...restProps}
>
{@render children?.()}
</div>
+22
View File
@@ -0,0 +1,22 @@
import Root from "./empty.svelte";
import Header from "./empty-header.svelte";
import Media from "./empty-media.svelte";
import Title from "./empty-title.svelte";
import Description from "./empty-description.svelte";
import Content from "./empty-content.svelte";
export {
Root,
Header,
Media,
Title,
Description,
Content,
//
Root as Empty,
Header as EmptyHeader,
Media as EmptyMedia,
Title as EmptyTitle,
Description as EmptyDescription,
Content as EmptyContent,
};
@@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from '../../tools';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="field-content"
class={cn('ui:group/field-content ui:flex ui:flex-1 ui:flex-col ui:gap-1.5 ui:leading-snug', className)}
{...restProps}
>
{@render children?.()}
</div>
@@ -0,0 +1,25 @@
<script lang="ts">
import { cn, type WithElementRef } from '../../tools';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
</script>
<p
bind:this={ref}
data-slot="field-description"
class={cn(
'ui:text-muted-foreground ui:text-sm ui:font-normal ui:leading-normal ui:group-has-data-[orientation=horizontal]/field:text-balance',
'ui:nth-last-2:-mt-1 ui:last:mt-0 ui:[[data-variant=legend]+&]:-mt-1.5',
'ui:[&>a:hover]:text-primary ui:[&>a]:underline ui:[&>a]:underline-offset-4',
className
)}
{...restProps}
>
{@render children?.()}
</p>
@@ -0,0 +1,58 @@
<script lang="ts">
import { cn, type WithElementRef } from '../../tools';
import type { HTMLAttributes } from 'svelte/elements';
import type { Snippet } from 'svelte';
let {
ref = $bindable(null),
class: className,
children,
errors,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
children?: Snippet;
errors?: { message?: string }[];
} = $props();
const hasContent = $derived.by(() => {
// has slotted error
if (children) return true;
// no errors
if (!errors) return false;
// has an error but no message
if (errors.length === 1 && !errors[0]?.message) {
return false;
}
return true;
});
const isMultipleErrors = $derived(errors && errors.length > 1);
const singleErrorMessage = $derived(errors && errors.length === 1 && errors[0]?.message);
</script>
{#if hasContent}
<div
bind:this={ref}
role="alert"
data-slot="field-error"
class={cn('ui:text-destructive ui:text-sm ui:font-normal', className)}
{...restProps}
>
{#if children}
{@render children()}
{:else if singleErrorMessage}
{singleErrorMessage}
{:else if isMultipleErrors}
<ul class="ui:ms-4 ui:flex ui:list-disc ui:flex-col ui:gap-1">
{#each errors ?? [] as error, index (index)}
{#if error?.message}
<li>{error.message}</li>
{/if}
{/each}
</ul>
{/if}
</div>
{/if}
@@ -0,0 +1,23 @@
<script lang="ts">
import { cn, type WithElementRef } from '../../tools';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="field-group"
class={cn(
'ui:group/field-group ui:@container/field-group ui:flex ui:w-full ui:flex-col ui:gap-7 ui:data-[slot=checkbox-group]:gap-3 ui:*:data-[slot=field-group]:gap-4',
className
)}
{...restProps}
>
{@render children?.()}
</div>
@@ -0,0 +1,21 @@
<script lang="ts">
import { Label } from '../label';
import { cn } from '../../tools';
import type { ComponentProps } from 'svelte';
let { ref = $bindable(null), class: className, children, ...restProps }: ComponentProps<typeof Label> = $props();
</script>
<Label
bind:ref
data-slot="field-label"
class={cn(
'ui:group/field-label ui:peer/field-label ui:flex ui:w-fit ui:gap-2 ui:leading-snug ui:group-data-[disabled=true]/field:opacity-50',
'ui:has-[>[data-slot=field]]:w-full ui:has-[>[data-slot=field]]:flex-col ui:has-[>[data-slot=field]]:rounded-md ui:has-[>[data-slot=field]]:border ui:*:data-[slot=field]:p-4',
'ui:has-data-[state=checked]:bg-primary/5 ui:has-data-[state=checked]:border-primary ui:dark:has-data-[state=checked]:bg-primary/10',
className
)}
{...restProps}
>
{@render children?.()}
</Label>
@@ -0,0 +1,29 @@
<script lang="ts">
import { cn, type WithElementRef } from '../../tools';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
variant = 'legend',
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLLegendElement>> & {
variant?: 'legend' | 'label';
} = $props();
</script>
<legend
bind:this={ref}
data-slot="field-legend"
data-variant={variant}
class={cn(
'ui:mb-3 ui:font-medium',
'ui:data-[variant=legend]:text-base',
'ui:data-[variant=label]:text-sm',
className
)}
{...restProps}
>
{@render children?.()}
</legend>
@@ -0,0 +1,35 @@
<script lang="ts">
import { Separator } from '../separator';
import { cn, type WithElementRef } from '../../tools';
import type { HTMLAttributes } from 'svelte/elements';
import type { Snippet } from 'svelte';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
children?: Snippet;
} = $props();
const hasContent = $derived(!!children);
</script>
<div
bind:this={ref}
data-slot="field-separator"
data-content={hasContent}
class={cn('ui:relative ui:-my-2 ui:h-5 ui:text-sm ui:group-data-[variant=outline]/field-group:-mb-2', className)}
{...restProps}
>
<Separator class="ui:absolute ui:inset-0 ui:top-1/2" />
{#if children}
<span
class="ui:bg-background ui:text-muted-foreground ui:relative ui:mx-auto ui:block ui:w-fit ui:px-2"
data-slot="field-separator-content"
>
{@render children()}
</span>
{/if}
</div>
@@ -0,0 +1,24 @@
<script lang="ts">
import { cn, type WithElementRef } from '../../tools';
import type { HTMLFieldsetAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLFieldsetAttributes> = $props();
</script>
<fieldset
bind:this={ref}
data-slot="field-set"
class={cn(
'ui:flex ui:flex-col ui:gap-6',
'ui:has-[>[data-slot=checkbox-group]]:gap-3 ui:has-[>[data-slot=radio-group]]:gap-3',
className
)}
{...restProps}
>
{@render children?.()}
</fieldset>
@@ -0,0 +1,23 @@
<script lang="ts">
import { cn, type WithElementRef } from '../../tools';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="field-title"
class={cn(
'ui:flex ui:w-fit ui:items-center ui:gap-2 ui:text-sm ui:font-medium ui:leading-snug ui:group-data-[disabled=true]/field:opacity-50',
className
)}
{...restProps}
>
{@render children?.()}
</div>
+53
View File
@@ -0,0 +1,53 @@
<script lang="ts" module>
import { tv, type VariantProps } from 'tailwind-variants';
export const fieldVariants = tv({
base: 'ui:group/field ui:data-[invalid=true]:text-destructive ui:flex ui:w-full ui:gap-3',
variants: {
orientation: {
vertical: 'ui:flex-col ui:*:w-full ui:*.sr-only:w-auto',
horizontal: [
'ui:flex-row ui:items-center',
'[&>[data-slot=field-label]]:flex-auto',
'ui:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px ui:has-[>[data-slot=field-content]]:items-start'
],
responsive: [
'@md/field-group:ui:flex-row @md/field-group:ui:items-center @md/field-group:ui:[&>*]:w-auto ui:flex-col ui:*:w-full ui:*.sr-only:w-auto',
'@md/field-group:ui:[&>[data-slot=field-label]]:flex-auto',
'@md/field-group:ui:has-[>[data-slot=field-content]]:items-start @md/field-group:ui:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px'
]
}
},
defaultVariants: {
orientation: 'vertical'
}
});
export type FieldOrientation = VariantProps<typeof fieldVariants>['orientation'];
</script>
<script lang="ts">
import { cn, type WithElementRef } from '../../tools';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
orientation = 'vertical',
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
orientation?: FieldOrientation;
} = $props();
</script>
<div
bind:this={ref}
role="group"
data-slot="field"
data-orientation={orientation}
class={cn(fieldVariants({ orientation }), className)}
{...restProps}
>
{@render children?.()}
</div>
+33
View File
@@ -0,0 +1,33 @@
import Content from './field-content.svelte';
import Description from './field-description.svelte';
import Error from './field-error.svelte';
import Field from './field.svelte';
import Group from './field-group.svelte';
import Label from './field-label.svelte';
import Legend from './field-legend.svelte';
import Separator from './field-separator.svelte';
import Set from './field-set.svelte';
import Title from './field-title.svelte';
export {
Field,
Set,
Legend,
Group,
Content,
Label,
Title,
Description,
Separator,
Error,
//
Set as FieldSet,
Legend as FieldLegend,
Group as FieldGroup,
Content as FieldContent,
Label as FieldLabel,
Title as FieldTitle,
Description as FieldDescription,
Separator as FieldSeparator,
Error as FieldError
};
@@ -0,0 +1,29 @@
<script lang="ts">
import { LinkPreview as HoverCardPrimitive } from 'bits-ui';
import { cn } from '../../tools';
let {
ref = $bindable(null),
class: className,
align = 'center',
sideOffset = 4,
portalProps,
...restProps
}: HoverCardPrimitive.ContentProps & {
portalProps?: HoverCardPrimitive.PortalProps;
} = $props();
</script>
<HoverCardPrimitive.Portal {...portalProps}>
<HoverCardPrimitive.Content
bind:ref
data-slot="hover-card-content"
{align}
{sideOffset}
class={cn(
'ui:bg-popover ui:text-popover-foreground ui:data-[state=open]:animate-in ui:data-[state=closed]:animate-out ui:data-[state=closed]:fade-out-0 ui:data-[state=open]:fade-in-0 ui:data-[state=closed]:zoom-out-95 ui:data-[state=open]:zoom-in-95 ui:data-[side=bottom]:slide-in-from-top-2 ui:data-[side=left]:slide-in-from-end-2 ui:data-[side=right]:slide-in-from-start-2 ui:data-[side=top]:slide-in-from-bottom-2 ui:outline-hidden ui:z-50 ui:mt-3 ui:w-64 ui:rounded-md ui:border ui:p-4 ui:shadow-md ui:outline-none',
className
)}
{...restProps}
/>
</HoverCardPrimitive.Portal>
@@ -0,0 +1,13 @@
<script lang="ts">
import { LinkPreview as HoverCardPrimitive } from 'bits-ui';
import { cn } from '../../tools';
let { ref = $bindable(null), class: className, ...restProps }: HoverCardPrimitive.TriggerProps = $props();
</script>
<HoverCardPrimitive.Trigger
bind:ref
data-slot="hover-card-trigger"
class={cn('ui:cursor-pointer', className)}
{...restProps}
/>
+14
View File
@@ -0,0 +1,14 @@
import { LinkPreview as HoverCardPrimitive } from "bits-ui";
import Content from "./hover-card-content.svelte";
import Trigger from "./hover-card-trigger.svelte";
const Root = HoverCardPrimitive.Root;
export {
Root,
Content,
Trigger,
Root as HoverCard,
Content as HoverCardContent,
Trigger as HoverCardTrigger,
};
@@ -22,7 +22,7 @@
<script lang="ts">
import { cn } from '../../tools';
import type { ComponentProps } from 'svelte';
import { Button } from '$src/base/button/index.js';
import { Button } from '../button';
let {
ref = $bindable(null),
@@ -1,7 +1,7 @@
<script lang="ts">
import { cn } from '../../tools';
import type { ComponentProps } from 'svelte';
import { Input } from '$src/base/input/index.js';
import { Input } from '../input';
let {
ref = $bindable(null),
@@ -1,6 +1,6 @@
<script lang="ts">
import { cn } from '../../tools';
import { Textarea } from '$src/base/textarea/index.js';
import { Textarea } from '../textarea';
import type { ComponentProps } from 'svelte';
let {
+3 -3
View File
@@ -39,9 +39,9 @@
bind:this={ref}
data-slot={dataSlot}
class={cn(
'border-input bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
'ui:border-input ui:bg-background ui:selection:bg-primary ui:dark:bg-input/30 ui:selection:text-primary-foreground ui:ring-offset-background ui:placeholder:text-muted-foreground ui:shadow-xs ui:flex ui:h-9 ui:w-full ui:min-w-0 ui:rounded-md ui:border ui:px-3 ui:py-1 ui:text-base ui:outline-none ui:transition-[color,box-shadow] ui:disabled:cursor-not-allowed ui:disabled:opacity-50 ui:md:text-sm',
'ui:focus-visible:border-ring ui:focus-visible:ring-ring/50 ui:focus-visible:ring-[3px]',
'ui:aria-invalid:ring-destructive/20 ui:dark:aria-invalid:ring-destructive/40 ui:aria-invalid:border-destructive',
className
)}
{type}
+1 -1
View File
@@ -2,7 +2,7 @@
import { tv, type VariantProps } from 'tailwind-variants';
export const itemVariants = tv({
base: 'group/item ui:[a]:hover:bg-accent/50 ui:[a]:transition-colors ui:focus-visible:border-ring ui:focus-visible:ring-ring/50 ui:flex ui:flex-wrap ui:items-center ui:rounded-md ui:border ui:border-transparent ui:text-sm ui:outline-none ui:transition-colors ui:duration-100 ui:focus-visible:ring-[3px]',
base: 'group/item ui:[a]:hover:bg-accent/50 ui:[a]:transition-colors ui:focus-visible:border-ring ui:focus-visible:ring-ring/50 ui:flex ui:flex-wrap ui:items-center ui:rounded-md ui:border ui:border-transparent ui:text-sm ui:outline-none ui:transition-colors ui:duration-100 ui:focus-visible:ring-[3px] ui:cursor-pointer',
variants: {
variant: {
default: 'ui:bg-transparent',
+28
View File
@@ -0,0 +1,28 @@
import Action from './page-action.svelte';
import Body from './page-body.svelte';
import BodyHeader from './page-body-header.svelte';
import Header from './page-header.svelte';
import HeaderContent from './page-header-content.svelte';
import Root from './page.svelte';
import Subtitle from './page-subtitle.svelte';
import Title from './page-title.svelte';
export {
Root,
Header,
HeaderContent,
Action,
Title,
Subtitle,
Body,
BodyHeader,
//
Root as Page,
Header as PageHeader,
HeaderContent as PageHeaderContent,
Action as PageAction,
Title as PageTitle,
Subtitle as PageSubtitle,
Body as PageBody,
BodyHeader as PageBodyHeader
};
@@ -0,0 +1,21 @@
<script lang="ts">
import { cn, type WithElementRef } from '../../tools';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="page-action"
class={cn('ui:flex ui:items-center ui:gap-2', className)}
{...restProps}
>
{@render children?.()}
</div>
@@ -0,0 +1,33 @@
<script lang="ts">
import { cn, type WithElementRef } from '../../tools';
import type { HTMLAttributes } from 'svelte/elements';
interface Props extends HTMLAttributes<HTMLDivElement> {
align?: 'left' | 'right' | 'none';
}
let {
ref = $bindable(null),
class: className,
children,
align = 'right',
...restProps
}: WithElementRef<Props> = $props();
const alignClasses = $derived(
{
left: 'ui:justify-start',
right: 'ui:justify-end',
none: 'ui:justify-center'
}[align]
);
</script>
<div
bind:this={ref}
data-slot="page-body-header"
class={cn('ui:flex ui:items-center ui:gap-2 ui:p-2', alignClasses, className)}
{...restProps}
>
{@render children?.()}
</div>
@@ -0,0 +1,25 @@
<script lang="ts">
import { cn, type WithElementRef } from '../../tools';
import type { HTMLAttributes } from 'svelte/elements';
import type { Snippet } from 'svelte';
let {
ref = $bindable(null),
class: className,
child,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
child?: Snippet;
} = $props();
</script>
<div
bind:this={ref}
data-slot="page-body"
class={cn('ui:flex-1 ui:flex ui:gap-4 ui:flex-col ui:overflow-y-auto', className)}
{...restProps}
>
{#if child}
{@render child()}
{/if}
</div>
@@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from '../../tools';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="page-header-content"
class={cn('ui:flex ui:flex-col ui:gap-1', className)}
{...restProps}
>
{@render children?.()}
</div>
@@ -0,0 +1,17 @@
<script lang="ts">
import { cn, type WithElementRef } from '../../tools';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div bind:this={ref} data-slot="page-header" class={cn('ui:flex ui:my-4 ui:flex-col', className)} {...restProps}>
<div class="ui:flex ui:items-start ui:justify-between ui:gap-4">
{@render children?.()}
</div>
</div>
@@ -0,0 +1,21 @@
<script lang="ts">
import { cn, type WithElementRef } from '../../tools';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
</script>
<p
bind:this={ref}
data-slot="page-subtitle"
class={cn('ui:text-muted-foreground ui:text-sm', className)}
{...restProps}
>
{@render children?.()}
</p>
@@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from '../../tools';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLHeadingElement>> = $props();
</script>
<h1
bind:this={ref}
data-slot="page-title"
class={cn('ui:text-2xl ui:font-normal ui:tracking-tight', className)}
{...restProps}
>
{@render children?.()}
</h1>
+20
View File
@@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from '../../tools';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="page"
class={cn('ui:flex ui:flex-col ui:gap-4 ui:min-h-[calc(100vh-48px)]', className)}
{...restProps}
>
{@render children?.()}
</div>
@@ -1,7 +1,7 @@
<script lang="ts">
import { Pagination as PaginationPrimitive } from 'bits-ui';
import { cn } from '../../tools';
import { type Props, buttonVariants } from '$src/base/button/index.js';
import { type Props, buttonVariants } from '../button';
let {
ref = $bindable(null),
+7
View File
@@ -0,0 +1,7 @@
import Root from './password.svelte';
import Input from './password-input.svelte';
import Strength from './password-strength.svelte';
import Copy from './password-copy.svelte';
import ToggleVisibility from './password-toggle-visibility.svelte';
export { Root, Input, Strength, Copy, ToggleVisibility };
@@ -0,0 +1,21 @@
<script lang="ts">
import { CopyButton } from '../copy-button';
import type { PasswordCopyButtonProps } from './types';
import { cn } from '../../tools';
import { usePasswordCopy } from './password-util.svelte';
let { ref = $bindable(null), class: className, ...rest }: PasswordCopyButtonProps = $props();
const state = usePasswordCopy();
</script>
<CopyButton
{...rest}
bind:ref
text={state.root.passwordState.value}
tabindex={-1}
class={cn(
'ui:text-muted-foreground ui:absolute ui:top-1/2 ui:right-0 ui:size-9 ui:min-w-0 ui:-translate-y-1/2 ui:hover:bg-transparent!',
className
)}
/>
@@ -0,0 +1,45 @@
<script lang="ts">
import { cn } from '../../tools';
import { box, mergeProps } from 'svelte-toolbelt';
import { usePasswordInput } from './password-util.svelte';
import type { PasswordInputProps } from './types';
import { Input } from '../input';
let {
ref = $bindable(null),
value = $bindable(''),
class: className,
children,
...rest
}: PasswordInputProps = $props();
const state = usePasswordInput({
value: box.with(
() => value,
(v) => (value = v)
),
ref: box.with(() => ref)
});
const mergedProps = $derived(mergeProps(rest, state.props));
</script>
<div class="relative">
<Input
{...mergedProps}
bind:value
bind:ref
type={state.root.opts.hidden.current ? 'password' : 'text'}
class={cn(
'ui:transition-all',
{
// either or is mounted (offset 36px)
'ui:pr-9': state.root.passwordState.copyMounted || state.root.passwordState.toggleMounted,
// both are mounted (offset 36px * 2)
'ui:pr-18': state.root.passwordState.copyMounted && state.root.passwordState.toggleMounted
},
className
)}
/>
{@render children?.()}
</div>
@@ -0,0 +1,48 @@
<script lang="ts">
import { tv } from 'tailwind-variants';
import { usePasswordStrength } from './password-util.svelte';
import type { PasswordStrengthProps } from './types';
import { Meter } from 'bits-ui';
import { cn } from '../../tools';
let { strength = $bindable(), class: className }: PasswordStrengthProps = $props();
const state = usePasswordStrength();
const score = $derived(state.strength.score);
$effect(() => {
strength = state.strength;
});
const color = tv({
base: '',
variants: {
score: {
0: 'ui:bg-red-500',
1: 'ui:bg-red-500',
2: 'ui:bg-yellow-500',
3: 'ui:bg-yellow-500',
4: 'ui:bg-green-500'
}
}
});
</script>
<Meter.Root
value={state.strength.score}
class={cn('ui:bg-accent ui:relative ui:h-[6px] ui:w-full ui:gap-1 ui:overflow-hidden ui:rounded-full', className)}
min={0}
max={4}
>
<div
class={cn('ui:h-full ui:transition-all ui:duration-500', color({ score }))}
style="width: {(score / 4) * 100}%;"
></div>
<!-- This creates the gaps between the bars -->
<div class="ui:absolute ui:left-0 ui:top-0 ui:z-10 ui:flex ui:h-[6px] ui:w-full ui:place-items-center ui:gap-1">
{#each Array.from({ length: 4 }) as _, i (i)}
<div class="ui:ring-background ui:ring-3 ui:h-[6px] ui:w-1/4 ui:rounded-full"></div>
{/each}
</div>
</Meter.Root>
@@ -0,0 +1,32 @@
<script lang="ts">
import { Toggle } from '../toggle';
import EyeIcon from '@lucide/svelte/icons/eye';
import EyeOffIcon from '@lucide/svelte/icons/eye-off';
import { usePasswordToggleVisibility } from './password-util.svelte';
import type { PasswordToggleVisibilityProps } from './types';
import { cn } from '../../tools';
let { ref = $bindable(null), class: className }: PasswordToggleVisibilityProps = $props();
const state = usePasswordToggleVisibility();
</script>
<Toggle
bind:ref
aria-label={state.root.opts.hidden.current ? 'Show password' : 'Hide password'}
bind:pressed={state.root.opts.hidden.current}
class={cn(
'ui:data-[state=off]:text-muted-foreground ui:data-[state=on]:text-muted-foreground ui:hover:data-[state=off]:text-accent-foreground ui:hover:data-[state=on]:text-accent-foreground ui:absolute ui:top-1/2 ui:right-0 ui:size-9 ui:min-w-0 ui:-translate-y-1/2 ui:p-0 ui:hover:bg-transparent! ui:data-[state=on]:bg-transparent',
{
'ui:right-9 ui:max-w-6': state.root.passwordState.copyMounted
},
className
)}
tabindex={-1}
>
{#if state.root.opts.hidden.current}
<EyeIcon class="ui:size-4" />
{:else}
<EyeOffIcon class="ui:size-4" />
{/if}
</Toggle>
@@ -0,0 +1,157 @@
import { Context, watch } from 'runed';
import type { ReadableBoxedValues, WritableBoxedValues } from 'svelte-toolbelt';
import { zxcvbn, zxcvbnOptions } from '@zxcvbn-ts/core';
import * as zxcvbnCommonPackage from '@zxcvbn-ts/language-common';
import * as zxcvbnEnPackage from '@zxcvbn-ts/language-en';
const passwordOptions = {
translations: zxcvbnEnPackage.translations,
graphs: zxcvbnCommonPackage.adjacencyGraphs,
dictionary: {
...zxcvbnCommonPackage.dictionary,
...zxcvbnEnPackage.dictionary
}
};
zxcvbnOptions.setOptions(passwordOptions);
type PasswordRootStateProps = WritableBoxedValues<{
hidden: boolean;
}> &
ReadableBoxedValues<{
minScore: number;
}>;
type PasswordState = {
value: string;
copyMounted: boolean;
toggleMounted: boolean;
strengthMounted: boolean;
tainted: boolean;
};
const defaultPasswordState: PasswordState = {
value: '',
copyMounted: false,
toggleMounted: false,
strengthMounted: false,
tainted: false
};
class PasswordRootState {
passwordState = $state(defaultPasswordState);
constructor(readonly opts: PasswordRootStateProps) {}
// only re-run when the password changes
strength = $derived.by(() => zxcvbn(this.passwordState.value));
}
type PasswordInputStateProps = WritableBoxedValues<{
value: string;
}> &
ReadableBoxedValues<{
ref: HTMLInputElement | null;
}>;
class PasswordInputState {
constructor(
readonly root: PasswordRootState,
readonly opts: PasswordInputStateProps
) {
watch(
() => this.opts.value.current,
() => {
if (this.root.passwordState.value !== this.opts.value.current) {
this.root.passwordState.tainted = true;
this.root.passwordState.value = this.opts.value.current;
}
}
);
$effect(() => {
if (!this.root.passwordState.strengthMounted) return;
// if the password is empty, we let the `required` attribute handle the validation
if (
this.root.passwordState.value !== '' &&
this.root.strength.score < this.root.opts.minScore.current
) {
this.opts.ref.current?.setCustomValidity('Password is too weak');
} else {
this.opts.ref.current?.setCustomValidity('');
}
});
}
props = $derived.by(() => ({
'aria-invalid':
this.root.strength.score < this.root.opts.minScore.current &&
this.root.passwordState.tainted &&
this.root.passwordState.strengthMounted
}));
}
class PasswordToggleVisibilityState {
constructor(readonly root: PasswordRootState) {
this.root.passwordState.toggleMounted = true;
// this way we go back to the correct padding when toggle is unmounted
$effect(() => {
return () => {
this.root.passwordState.toggleMounted = false;
};
});
}
}
class PasswordCopyState {
constructor(readonly root: PasswordRootState) {
this.root.passwordState.copyMounted = true;
// this way we go back to the correct padding when copy is unmounted
$effect(() => {
return () => {
this.root.passwordState.copyMounted = false;
};
});
}
}
class PasswordStrengthState {
constructor(readonly root: PasswordRootState) {
this.root.passwordState.strengthMounted = true;
$effect(() => {
return () => {
this.root.passwordState.strengthMounted = false;
};
});
}
get strength() {
return this.root.strength;
}
}
const ctx = new Context<PasswordRootState>('password-root-state');
export function usePassword(props: PasswordRootStateProps) {
return ctx.set(new PasswordRootState(props));
}
export function usePasswordInput(props: PasswordInputStateProps) {
return new PasswordInputState(ctx.get(), props);
}
export function usePasswordToggleVisibility() {
return new PasswordToggleVisibilityState(ctx.get());
}
export function usePasswordCopy() {
return new PasswordCopyState(ctx.get());
}
export function usePasswordStrength() {
return new PasswordStrengthState(ctx.get());
}
@@ -0,0 +1,26 @@
<script lang="ts">
import { box } from 'svelte-toolbelt';
import { usePassword } from './password-util.svelte';
import type { PasswordRootProps } from './types';
import { cn } from '../../tools';
let {
ref = $bindable(null),
hidden = $bindable(true),
minScore = 3,
class: className,
children
}: PasswordRootProps = $props();
usePassword({
hidden: box.with(
() => hidden,
(v) => (hidden = v)
),
minScore: box.with(() => minScore)
});
</script>
<div bind:this={ref} class={cn('ui:flex ui:flex-col ui:gap-2', className)}>
{@render children?.()}
</div>
+47
View File
@@ -0,0 +1,47 @@
import type {
WithChildren,
WithoutChildren,
Meter as MeterPrimitive,
Toggle as TogglePrimitive
} from 'bits-ui';
import type { HTMLAttributes, HTMLInputAttributes } from 'svelte/elements';
import type { CopyButtonProps } from '../copy-button/types';
import type { ZxcvbnResult } from '@zxcvbn-ts/core';
export type PasswordRootPropsWithoutHTML = WithChildren<{
ref?: HTMLDivElement | null;
hidden?: boolean;
/** The minimum acceptable score for a password. (0-4)
*
* @default 3
*/
minScore?: 0 | 1 | 2 | 3 | 4;
}>;
export type PasswordRootProps = WithoutChildren<HTMLAttributes<HTMLDivElement>> &
PasswordRootPropsWithoutHTML;
export type PasswordInputPropsWithoutHTML = WithChildren<{
ref?: HTMLInputElement | null;
value?: string;
}>;
export type PasswordInputProps = Omit<
WithoutChildren<HTMLInputAttributes>,
'type' | 'files' | 'aria-invalid' | 'value'
> &
PasswordInputPropsWithoutHTML;
export type PasswordToggleVisibilityProps = Omit<
TogglePrimitive.RootProps,
'children' | 'pressed' | 'aria-label' | 'tabindex'
>;
export type PasswordCopyButtonProps = Omit<CopyButtonProps, 'children' | 'text'>;
export type PasswordStrengthPropsWithoutHTML = {
strength?: ZxcvbnResult;
};
export type PasswordStrengthProps = PasswordStrengthPropsWithoutHTML &
WithoutChildren<MeterPrimitive.RootProps>;
+7 -8
View File
@@ -1,14 +1,13 @@
import { Select as SelectPrimitive } from 'bits-ui';
import Group from './select-group.svelte';
import Label from './select-label.svelte';
import Item from './select-item.svelte';
import Content from './select-content.svelte';
import Trigger from './select-trigger.svelte';
import Separator from './select-separator.svelte';
import Group from './select-group.svelte';
import GroupHeading from './select-group-heading.svelte';
import Item from './select-item.svelte';
import Label from './select-label.svelte';
import ScrollDownButton from './select-scroll-down-button.svelte';
import ScrollUpButton from './select-scroll-up-button.svelte';
import GroupHeading from './select-group-heading.svelte';
import { Select as SelectPrimitive } from 'bits-ui';
import Separator from './select-separator.svelte';
import Trigger from './select-trigger.svelte';
const Root = SelectPrimitive.Root;
@@ -1,6 +1,6 @@
<script lang="ts">
import type { ComponentProps } from 'svelte';
import { Input } from '$src/base/input/index.js';
import { Input } from '../input';
import { cn } from '../../tools';
let {
@@ -26,7 +26,7 @@
</script>
<script lang="ts">
import * as Tooltip from '$src/base/tooltip/index.js';
import * as Tooltip from '../tooltip';
import { cn, type WithElementRef, type WithoutChildrenOrChild } from '../../tools';
import { mergeProps } from 'bits-ui';
import type { ComponentProps, Snippet } from 'svelte';
@@ -1,6 +1,6 @@
<script lang="ts">
import { cn, type WithElementRef } from '../../tools';
import { Skeleton } from '$src/base/skeleton/index.js';
import { Skeleton } from '../skeleton';
import type { HTMLAttributes } from 'svelte/elements';
let {
@@ -1,5 +1,5 @@
<script lang="ts">
import * as Tooltip from '$src/base/tooltip/index.js';
import * as Tooltip from '../tooltip';
import { cn, type WithElementRef } from '../../tools';
import type { HTMLAttributes } from 'svelte/elements';
import { SIDEBAR_COOKIE_MAX_AGE, SIDEBAR_COOKIE_NAME, SIDEBAR_WIDTH, SIDEBAR_WIDTH_ICON } from './constants.js';
@@ -1,5 +1,5 @@
<script lang="ts">
import { Separator } from '$src/base/separator/index.js';
import { Separator } from '../separator';
import { cn } from '../../tools';
import type { ComponentProps } from 'svelte';
@@ -1,5 +1,5 @@
<script lang="ts">
import { Button } from '$src/base/button/index.js';
import { Button } from '../button';
import { cn } from '../../tools';
import PanelLeftIcon from '@lucide/svelte/icons/panel-left';
import type { ComponentProps } from 'svelte';
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import * as Sheet from '$src/base/sheet/index.js';
import * as Sheet from '../sheet';
import { cn, type WithElementRef } from '../../../src/tools';
import type { HTMLAttributes } from 'svelte/elements';
import { SIDEBAR_WIDTH_MOBILE } from './constants.js';
+1 -6
View File
@@ -5,9 +5,4 @@
let { ...restProps }: SonnerProps = $props();
</script>
<Sonner
theme={mode.current}
class="toaster group"
style="--normal-bg: var(--color-popover); --normal-text: var(--color-popover-foreground); --normal-border: var(--color-border);"
{...restProps}
/>
<Sonner theme={mode.current} class="toaster ui:z-999 group" {...restProps} />
+13
View File
@@ -0,0 +1,13 @@
import Root from './toggle.svelte';
export {
toggleVariants,
type ToggleSize,
type ToggleVariant,
type ToggleVariants
} from './toggle.svelte';
export {
Root,
//
Root as Toggle
};
+52
View File
@@ -0,0 +1,52 @@
<script lang="ts" module>
import { type VariantProps, tv } from 'tailwind-variants';
export const toggleVariants = tv({
base: "ui:hover:bg-muted ui:hover:text-muted-foreground ui:data-[state=on]:bg-accent ui:data-[state=on]:text-accent-foreground ui:focus-visible:border-ring ui:focus-visible:ring-ring/50 ui:aria-invalid:ring-destructive/20 ui:dark:aria-invalid:ring-destructive/40 ui:aria-invalid:border-destructive ui:inline-flex ui:items-center ui:justify-center ui:gap-2 ui:rounded-md ui:text-sm ui:font-medium ui:whitespace-nowrap ui:transition-[color,box-shadow] ui:outline-none ui:focus-visible:ring-[3px] ui:disabled:pointer-events-none ui:disabled:opacity-50 ui:[&_svg]:pointer-events-none ui:[&_svg]:shrink-0 ui:[&_svg:not([class*='size-'])]:size-4",
variants: {
variant: {
default: 'ui:bg-transparent',
outline:
'ui:border-input ui:hover:bg-accent ui:hover:text-accent-foreground ui:border ui:bg-transparent ui:shadow-xs'
},
size: {
default: 'ui:h-9 ui:min-w-9 ui:px-2',
sm: 'ui:h-8 ui:min-w-8 ui:px-1.5',
lg: 'ui:h-10 ui:min-w-10 ui:px-2.5'
}
},
defaultVariants: {
variant: 'default',
size: 'default'
}
});
export type ToggleVariant = VariantProps<typeof toggleVariants>['variant'];
export type ToggleSize = VariantProps<typeof toggleVariants>['size'];
export type ToggleVariants = VariantProps<typeof toggleVariants>;
</script>
<script lang="ts">
import { Toggle as TogglePrimitive } from 'bits-ui';
import { cn } from '../../tools';
let {
ref = $bindable(null),
pressed = $bindable(false),
class: className,
size = 'default',
variant = 'default',
...restProps
}: TogglePrimitive.RootProps & {
variant?: ToggleVariant;
size?: ToggleSize;
} = $props();
</script>
<TogglePrimitive.Root
bind:ref
bind:pressed
data-slot="toggle"
class={cn(toggleVariants({ variant, size }), className)}
{...restProps}
/>