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:
@@ -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,
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 };
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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}
|
||||
/>
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
@@ -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),
|
||||
|
||||
@@ -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>
|
||||
@@ -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>;
|
||||
@@ -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,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';
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
Reference in New Issue
Block a user