feat: move away from carbon components and TinyMCE (#597)

* feat: remove tinymce & replace org sidebar

* chore: PR Fixes

* chore: move ui font into src dir

* remove files in vite config

* use google font import for loading font

* remove custom font

* more fonts update

* remove font bold

* more removals of font bold

* fix import

* checkpoint

* chore: checkpoint (add course & lms shadcn sidebar)

* minor css improvements and try out font sans

* chore: replace carbon components with shadcn's

* chore: replace carbon components with shadcn _ v3

* chore: ui fixes & storybook update

* feat: UI cleanup

* fix: quick ui fixes

* chore: update pnpm-lock.yaml with new dependencies and versions; fix output.css formatting

* chore: add isloading to button

* feat: add loading state to button stories

* chore: feedback fixes

* fix: feedback fixes

* chore: remove console

* fix: PR comments

* fix: PR comments

---------

Co-authored-by: Rotimi Best <[email protected]>
This commit is contained in:
Adeyemi Adetunji
2025-11-21 12:18:04 +01:00
committed by GitHub
co-authored by Rotimi Best
parent 2145782385
commit cbf6ae7e63
499 changed files with 19708 additions and 15603 deletions
+2
View File
@@ -13,6 +13,8 @@
"@cio/ui": "workspace:*"
},
"devDependencies": {
"@tiptap/pm": "^3.7.1",
"@tiptap/core": "^3.7.1",
"@storybook/addon-essentials": "^8.4.2",
"@storybook/addon-interactions": "^8.4.2",
"@storybook/addon-links": "^8.4.2",
@@ -0,0 +1,98 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import * as Accordion from '@cio/ui/base/accordion';
import { FIELDS } from './fields';
const { Story } = defineMeta({
title: 'Atom/Accordion',
component: Accordion.Root,
parameters: {
layout: 'centered',
controls: {
include: FIELDS
}
},
tags: ['autodocs']
});
</script>
<Story name="Default">
{#snippet template()}
<Accordion.Root type="single" class="w-[450px]">
<Accordion.Item value="item-1">
<Accordion.Trigger>Is it accessible?</Accordion.Trigger>
<Accordion.Content>
Yes. It adheres to the WAI-ARIA design pattern and includes proper keyboard navigation support.
</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="item-2">
<Accordion.Trigger>Is it styled?</Accordion.Trigger>
<Accordion.Content>
Yes. It comes with default styles that matches the other components' aesthetic.
</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="item-3">
<Accordion.Trigger>Is it animated?</Accordion.Trigger>
<Accordion.Content>Yes. It's animated by default, but you can disable it if you prefer.</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
{/snippet}
</Story>
<Story name="Single Expanded">
{#snippet template()}
<Accordion.Root type="single" class="w-[450px]">
<Accordion.Item value="item-1">
<Accordion.Trigger>What is ClassroomIO?</Accordion.Trigger>
<Accordion.Content>
ClassroomIO is an open-source learning management system designed to help educators create and manage online
courses efficiently.
</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="item-2">
<Accordion.Trigger>How do I get started?</Accordion.Trigger>
<Accordion.Content>
You can get started by creating an account, setting up your organization, and creating your first course. Our
documentation provides step-by-step guides.
</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="item-3">
<Accordion.Trigger>Is it free to use?</Accordion.Trigger>
<Accordion.Content>
Yes! ClassroomIO is open-source and free to use. We also offer premium features for organizations that need
additional capabilities.
</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
{/snippet}
</Story>
<Story name="Multiple Expanded">
{#snippet template()}
<Accordion.Root type="multiple" class="w-[450px]">
<Accordion.Item value="features">
<Accordion.Trigger>Features</Accordion.Trigger>
<Accordion.Content>
<ul class="list-disc space-y-1 pl-4">
<li>Course management</li>
<li>Student analytics</li>
<li>AI-powered grading</li>
<li>Custom certificates</li>
</ul>
</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="pricing">
<Accordion.Trigger>Pricing</Accordion.Trigger>
<Accordion.Content>
We offer flexible pricing plans for individuals, teams, and enterprises. Contact us for more information.
</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="support">
<Accordion.Trigger>Support</Accordion.Trigger>
<Accordion.Content>
We provide email support, documentation, and community forums to help you succeed.
</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
{/snippet}
</Story>
@@ -0,0 +1,12 @@
import { type ComponentProps } from 'svelte';
import { Accordion } from '@cio/ui/base/accordion';
export const FIELDS: (keyof ComponentProps<typeof Accordion>)[] = [
'type',
'value',
'onValueChange',
'disabled',
'name',
'loop',
'orientation'
];
@@ -0,0 +1,44 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import { Avatar, AvatarImage, AvatarFallback } from '@cio/ui/base/avatar';
import { FIELDS } from './fields';
const { Story } = defineMeta({
title: 'Atom/Avatar',
component: Avatar,
parameters: {
layout: 'centered',
controls: {
include: FIELDS
}
},
tags: ['autodocs']
});
</script>
<Story name="Default">
{#snippet template()}
<Avatar>
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
{/snippet}
</Story>
<Story name="Fallback">
{#snippet template()}
<Avatar>
<AvatarImage src="https://invalid-url.com/image.png" alt="@user" />
<AvatarFallback>AB</AvatarFallback>
</Avatar>
{/snippet}
</Story>
<Story name="Custom Size">
{#snippet template()}
<Avatar class="h-20 w-20">
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
{/snippet}
</Story>
@@ -0,0 +1,4 @@
import { type ComponentProps } from 'svelte';
import { Avatar } from '@cio/ui/base/avatar';
export const FIELDS: (keyof ComponentProps<typeof Avatar>)[] = ['loadingStatus', 'onLoadingStatusChange', 'delayMs'];
@@ -0,0 +1,48 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import { Badge } from '@cio/ui/base/badge';
const { Story } = defineMeta({
title: 'Atom/Badge',
component: Badge,
parameters: {
layout: 'centered'
},
tags: ['autodocs']
});
</script>
<Story name="Default">
{#snippet template()}
<Badge>Default Badge</Badge>
{/snippet}
</Story>
<Story name="Secondary">
{#snippet template()}
<Badge variant="secondary">Secondary</Badge>
{/snippet}
</Story>
<Story name="Destructive">
{#snippet template()}
<Badge variant="destructive">Destructive</Badge>
{/snippet}
</Story>
<Story name="Outline">
{#snippet template()}
<Badge variant="outline">Outline</Badge>
{/snippet}
</Story>
<Story name="Multiple Badges">
{#snippet template()}
<div class="flex gap-2">
<Badge>Default</Badge>
<Badge variant="secondary">Secondary</Badge>
<Badge variant="destructive">Destructive</Badge>
<Badge variant="outline">Outline</Badge>
</div>
{/snippet}
</Story>
@@ -0,0 +1,65 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import {
Breadcrumb,
BreadcrumbEllipsis,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator
} from '@cio/ui/base/breadcrumb';
const { Story } = defineMeta({
title: 'Atom/Breadcrumb',
component: Breadcrumb,
parameters: {
layout: 'centered'
},
tags: ['autodocs']
});
</script>
<Story name="Default">
{#snippet template()}
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/">Home</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink href="/components">Components</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Breadcrumb</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
{/snippet}
</Story>
<Story name="With Ellipsis">
{#snippet template()}
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/">Home</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbEllipsis />
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink href="/components">Components</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Breadcrumb</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
{/snippet}
</Story>
@@ -70,6 +70,12 @@
{/snippet}
</Story>
<Story name="Loading">
{#snippet template(args)}
<Button {...args} isLoading>Loading Button</Button>
{/snippet}
</Story>
<Story name="As Link">
{#snippet template(args)}
<Button {...args} href="#">Link Button</Button>
@@ -0,0 +1,57 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import { Checkbox } from '@cio/ui/base/checkbox';
import { Label } from '@cio/ui/base/label';
import { FIELDS } from './fields';
const { Story } = defineMeta({
title: 'Atom/Checkbox',
component: Checkbox,
parameters: {
layout: 'centered',
controls: {
include: FIELDS
}
},
tags: ['autodocs']
});
</script>
<Story name="Default">
{#snippet template()}
<Checkbox />
{/snippet}
</Story>
<Story name="With Label">
{#snippet template()}
<div class="flex items-center space-x-2">
<Checkbox id="terms" />
<Label for="terms">Accept terms and conditions</Label>
</div>
{/snippet}
</Story>
<Story name="Checked">
{#snippet template()}
<div class="flex items-center space-x-2">
<Checkbox checked={true} />
<Label>Checked by default</Label>
</div>
{/snippet}
</Story>
<Story name="Disabled">
{#snippet template()}
<div class="flex flex-col gap-4">
<div class="flex items-center space-x-2">
<Checkbox disabled />
<Label>Disabled unchecked</Label>
</div>
<div class="flex items-center space-x-2">
<Checkbox disabled checked={true} />
<Label>Disabled checked</Label>
</div>
</div>
{/snippet}
</Story>
@@ -0,0 +1,11 @@
import { type ComponentProps } from 'svelte';
import { Checkbox } from '@cio/ui/base/checkbox';
export const FIELDS: (keyof ComponentProps<typeof Checkbox>)[] = [
'checked',
'onCheckedChange',
'disabled',
'name',
'value',
'required'
];
@@ -0,0 +1,52 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@cio/ui/base/collapsible';
import { Button } from '@cio/ui';
import { FIELDS } from './fields';
const { Story } = defineMeta({
title: 'Atom/Collapsible',
component: Collapsible,
parameters: {
layout: 'centered',
controls: {
include: FIELDS
}
},
tags: ['autodocs']
});
</script>
<Story name="Default">
{#snippet template()}
<Collapsible class="w-[350px] space-y-2">
<div class="flex items-center justify-between space-x-4 px-4">
<h4 class="text-sm font-semibold">@peduarte starred 3 repositories</h4>
<CollapsibleTrigger>
<Button.Root variant="ghost" size="sm" class="w-9 p-0">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-4 w-4"
>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
<span class="sr-only">Toggle</span>
</Button.Root>
</CollapsibleTrigger>
</div>
<div class="rounded-md border px-4 py-3 font-mono text-sm">@radix-ui/primitives</div>
<CollapsibleContent class="space-y-2">
<div class="rounded-md border px-4 py-3 font-mono text-sm">@radix-ui/colors</div>
<div class="rounded-md border px-4 py-3 font-mono text-sm">@stitches/react</div>
</CollapsibleContent>
</Collapsible>
{/snippet}
</Story>
@@ -0,0 +1,9 @@
import { type ComponentProps } from 'svelte';
import { Collapsible } from '@cio/ui/base/collapsible';
export const FIELDS: (keyof ComponentProps<typeof Collapsible>)[] = [
'open',
'onOpenChange',
'onOpenChangeComplete',
'disabled'
];
@@ -0,0 +1,54 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import { Command } from '@cio/ui';
import { FIELDS } from './fields';
const { Story } = defineMeta({
title: 'Atom/Command',
component: Command.Root,
parameters: {
layout: 'centered',
controls: {
include: FIELDS
}
},
tags: ['autodocs']
});
</script>
<Story name="Default">
{#snippet template()}
<Command.Root class="rounded-lg border shadow-md">
<Command.CommandInput placeholder="Type a command or search..." />
<Command.CommandList>
<Command.CommandEmpty>No results found.</Command.CommandEmpty>
<Command.CommandGroup heading="Suggestions">
<Command.CommandItem>
<span>Calendar</span>
</Command.CommandItem>
<Command.CommandItem>
<span>Search Emoji</span>
</Command.CommandItem>
<Command.CommandItem>
<span>Calculator</span>
</Command.CommandItem>
</Command.CommandGroup>
<Command.CommandSeparator />
<Command.CommandGroup heading="Settings">
<Command.CommandItem>
<span>Profile</span>
<Command.CommandShortcut>⌘P</Command.CommandShortcut>
</Command.CommandItem>
<Command.CommandItem>
<span>Billing</span>
<Command.CommandShortcut>⌘B</Command.CommandShortcut>
</Command.CommandItem>
<Command.CommandItem>
<span>Settings</span>
<Command.CommandShortcut>⌘S</Command.CommandShortcut>
</Command.CommandItem>
</Command.CommandGroup>
</Command.CommandList>
</Command.Root>
{/snippet}
</Story>
@@ -0,0 +1,11 @@
import { type ComponentProps } from 'svelte';
import { Command } from '@cio/ui/base/command';
export const FIELDS: (keyof ComponentProps<typeof Command>)[] = [
'value',
'onValueChange',
'label',
'filter',
'columns',
'disablePointerSelection'
];
@@ -0,0 +1,82 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import { Dialog, Button, Input, Label } from '@cio/ui';
import { FIELDS } from './fields';
const { Story } = defineMeta({
title: 'Atom/Dialog',
component: Dialog.Root,
parameters: {
layout: 'centered',
controls: {
include: FIELDS
}
},
argTypes: {
open: {
control: 'boolean',
description: 'Whether the dialog is open',
table: { defaultValue: { summary: 'false' } }
},
onOpenChange: {
control: false,
description: 'Callback function called when the open state changes',
table: {
type: { summary: '(open: boolean) => void' },
defaultValue: { summary: 'undefined' }
}
}
},
tags: ['autodocs']
});
</script>
<Story name="Default">
{#snippet template(args)}
<Dialog.Root {...args}>
<Dialog.DialogTrigger>
<Button.Root>Open Dialog</Button.Root>
</Dialog.DialogTrigger>
<Dialog.DialogContent>
<Dialog.DialogHeader>
<Dialog.DialogTitle>Dialog Title</Dialog.DialogTitle>
<Dialog.DialogDescription>This is a description of what the dialog is for.</Dialog.DialogDescription>
</Dialog.DialogHeader>
<div class="py-4">
<p>Dialog content goes here.</p>
</div>
</Dialog.DialogContent>
</Dialog.Root>
{/snippet}
</Story>
<Story name="With Form">
{#snippet template(args)}
<Dialog.Root {...args}>
<Dialog.DialogTrigger>
<Button.Root>Edit Profile</Button.Root>
</Dialog.DialogTrigger>
<Dialog.DialogContent>
<Dialog.DialogHeader>
<Dialog.DialogTitle>Edit profile</Dialog.DialogTitle>
<Dialog.DialogDescription
>Make changes to your profile here. Click save when you're done.</Dialog.DialogDescription
>
</Dialog.DialogHeader>
<div class="grid gap-4 py-4">
<div class="grid grid-cols-4 items-center gap-4">
<Label.Root for="name" class="text-right">Name</Label.Root>
<Input.Root id="name" value="John Doe" class="col-span-3" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label.Root for="username" class="text-right">Username</Label.Root>
<Input.Root id="username" value="@johndoe" class="col-span-3" />
</div>
</div>
<Dialog.DialogFooter>
<Button.Root type="submit">Save changes</Button.Root>
</Dialog.DialogFooter>
</Dialog.DialogContent>
</Dialog.Root>
{/snippet}
</Story>
@@ -0,0 +1,4 @@
import { type ComponentProps } from 'svelte';
import * as Dialog from '@cio/ui/base/dialog';
export const FIELDS: (keyof ComponentProps<typeof Dialog.Root>)[] = ['open', 'onOpenChange'];
@@ -1,6 +1,7 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import { DropdownMenu, Button } from '@cio/ui';
import { Button } from '@cio/ui';
import * as DropdownMenu from '@cio/ui/base/dropdown-menu';
const { Story } = defineMeta({
title: 'Atom/DropdownMenu',
@@ -0,0 +1,4 @@
import { type ComponentProps } from 'svelte';
import { Label } from '@cio/ui/base/label';
export const FIELDS: (keyof ComponentProps<typeof Label>)[] = ['children', 'child'];
@@ -0,0 +1,45 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import { Label } from '@cio/ui/base/label';
import { Input } from '@cio/ui/base/input';
import { FIELDS } from './fields';
const { Story } = defineMeta({
title: 'Atom/Label',
component: Label,
parameters: {
layout: 'centered',
controls: {
include: FIELDS
}
},
tags: ['autodocs']
});
</script>
<Story name="Default">
{#snippet template()}
<Label>Email</Label>
{/snippet}
</Story>
<Story name="With Input">
{#snippet template()}
<div class="grid w-full max-w-sm items-center gap-1.5">
<Label for="email">Email</Label>
<Input type="email" id="email" placeholder="Email" />
</div>
{/snippet}
</Story>
<Story name="Required">
{#snippet template()}
<div class="grid w-full max-w-sm items-center gap-1.5">
<Label for="username">
Username
<span class="text-red-500">*</span>
</Label>
<Input id="username" placeholder="Username" required />
</div>
{/snippet}
</Story>
@@ -0,0 +1,10 @@
import { type ComponentProps } from 'svelte';
import { Menubar } from '@cio/ui/base/menubar';
export const FIELDS: (keyof ComponentProps<typeof Menubar>)[] = [
'onValueChange',
'value',
'dir',
'onOpenChange',
'disabled'
];
@@ -0,0 +1,90 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import * as Menubar from '@cio/ui/base/menubar';
import { FIELDS } from './fields';
const { Story } = defineMeta({
title: 'Atom/Menubar',
component: Menubar.Root,
parameters: {
layout: 'centered',
controls: {
include: FIELDS
}
},
argTypes: {
value: {
control: 'text',
description: 'The controlled value of the active menu'
},
onValueChange: {
control: false,
description: 'Callback function when the active menu changes'
},
dir: {
control: 'select',
options: ['ltr', 'rtl'],
description: 'The reading direction of the menubar'
},
onOpenChange: {
control: false,
description: 'Callback function when the open state changes'
},
ref: {
control: false,
description: 'The underlying DOM element reference'
},
disabled: {
control: 'boolean',
description: 'Whether the menubar is disabled'
}
},
tags: ['autodocs']
});
</script>
<Story name="Default">
{#snippet template()}
<Menubar.Root>
<Menubar.Menu>
<Menubar.Trigger>File</Menubar.Trigger>
<Menubar.Content>
<Menubar.Item>
New Tab <span class="ml-auto text-xs">⌘T</span>
</Menubar.Item>
<Menubar.Item>
New Window <span class="ml-auto text-xs">⌘N</span>
</Menubar.Item>
<Menubar.Separator />
<Menubar.Item>Share</Menubar.Item>
<Menubar.Separator />
<Menubar.Item>Print</Menubar.Item>
</Menubar.Content>
</Menubar.Menu>
<Menubar.Menu>
<Menubar.Trigger>Edit</Menubar.Trigger>
<Menubar.Content>
<Menubar.Item>
Undo <span class="ml-auto text-xs">⌘Z</span>
</Menubar.Item>
<Menubar.Item>
Redo <span class="ml-auto text-xs">⇧⌘Z</span>
</Menubar.Item>
<Menubar.Separator />
<Menubar.Item>Cut</Menubar.Item>
<Menubar.Item>Copy</Menubar.Item>
<Menubar.Item>Paste</Menubar.Item>
</Menubar.Content>
</Menubar.Menu>
<Menubar.Menu>
<Menubar.Trigger>View</Menubar.Trigger>
<Menubar.Content>
<Menubar.CheckboxItem checked={true}>Always Show Bookmarks Bar</Menubar.CheckboxItem>
<Menubar.CheckboxItem>Always Show Full URLs</Menubar.CheckboxItem>
<Menubar.Separator />
<Menubar.Item>Reload <span class="ml-auto text-xs">⌘R</span></Menubar.Item>
</Menubar.Content>
</Menubar.Menu>
</Menubar.Root>
{/snippet}
</Story>
@@ -0,0 +1,22 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import { ModeSwitcher } from '@cio/ui/base/dark-mode';
const { Story } = defineMeta({
title: 'Atom/Mode Switcher',
component: ModeSwitcher,
parameters: {
layout: 'centered'
},
tags: ['autodocs']
});
</script>
<Story name="Default">
{#snippet template()}
<div class="flex flex-col items-center gap-4">
<p class="text-muted-foreground text-sm">Click to toggle dark/light mode</p>
<ModeSwitcher />
</div>
{/snippet}
</Story>
@@ -0,0 +1,10 @@
import { type ComponentProps } from 'svelte';
import { Pagination } from '@cio/ui/base/pagination';
export const FIELDS: (keyof ComponentProps<typeof Pagination>)[] = [
'count',
'page',
'onPageChange',
'perPage',
'orientation'
];
@@ -0,0 +1,73 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import * as Pagination from '@cio/ui/base/pagination';
import { FIELDS } from './fields';
const { Story } = defineMeta({
title: 'Atom/Pagination',
component: Pagination.Root,
parameters: {
layout: 'centered',
controls: {
include: FIELDS
}
},
tags: ['autodocs']
});
</script>
<script>
let page = $state(1);
</script>
<Story name="Default">
{#snippet template()}
<Pagination.Root count={100} perPage={10} bind:page>
<Pagination.Content>
<Pagination.Item>
<Pagination.PrevButton />
</Pagination.Item>
{#each Array.from({ length: 10 }, (_, i) => i + 1) as pageNum}
<Pagination.Item>
<Pagination.Link page={pageNum} isActive={pageNum === page}>
{pageNum}
</Pagination.Link>
</Pagination.Item>
{/each}
<Pagination.Item>
<Pagination.NextButton />
</Pagination.Item>
</Pagination.Content>
</Pagination.Root>
{/snippet}
</Story>
<Story name="With Ellipsis">
{#snippet template()}
<Pagination.Root count={500} perPage={10} bind:page>
<Pagination.Content>
<Pagination.Item>
<Pagination.PrevButton />
</Pagination.Item>
<Pagination.Item>
<Pagination.Link page={1} isActive={page === 1}>1</Pagination.Link>
</Pagination.Item>
<Pagination.Item>
<Pagination.Ellipsis />
</Pagination.Item>
<Pagination.Item>
<Pagination.Link {page} isActive={true}>{page}</Pagination.Link>
</Pagination.Item>
<Pagination.Item>
<Pagination.Ellipsis />
</Pagination.Item>
<Pagination.Item>
<Pagination.Link page={50}>50</Pagination.Link>
</Pagination.Item>
<Pagination.Item>
<Pagination.NextButton />
</Pagination.Item>
</Pagination.Content>
</Pagination.Root>
{/snippet}
</Story>
@@ -0,0 +1,4 @@
import { type ComponentProps } from 'svelte';
import { Progress } from '@cio/ui/base/progress';
export const FIELDS: (keyof ComponentProps<typeof Progress>)[] = ['max', 'min', 'value'];
@@ -0,0 +1,48 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import { Progress } from '@cio/ui/base/progress';
import { FIELDS } from './fields';
const { Story } = defineMeta({
title: 'Atom/Progress',
component: Progress,
parameters: {
layout: 'centered',
controls: {
include: FIELDS
}
},
tags: ['autodocs']
});
</script>
<Story name="Default">
{#snippet template()}
<div class="w-[400px] space-y-4">
<Progress value={33} />
</div>
{/snippet}
</Story>
<Story name="Different Values">
{#snippet template()}
<div class="w-[400px] space-y-6">
<div class="space-y-2">
<p class="text-sm font-medium">25% Complete</p>
<Progress value={25} />
</div>
<div class="space-y-2">
<p class="text-sm font-medium">50% Complete</p>
<Progress value={50} />
</div>
<div class="space-y-2">
<p class="text-sm font-medium">75% Complete</p>
<Progress value={75} />
</div>
<div class="space-y-2">
<p class="text-sm font-medium">100% Complete</p>
<Progress value={100} />
</div>
</div>
{/snippet}
</Story>
@@ -0,0 +1,12 @@
import { type ComponentProps } from 'svelte';
import { RadioGroup } from '@cio/ui/base/radio-group';
export const FIELDS: (keyof ComponentProps<typeof RadioGroup>)[] = [
'value',
'onValueChange',
'disabled',
'required',
'name',
'loop',
'orientation'
];
@@ -0,0 +1,65 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import * as RadioGroup from '@cio/ui/base/radio-group';
import { Label } from '@cio/ui/base/label';
import { FIELDS } from './fields';
const { Story } = defineMeta({
title: 'Atom/RadioGroup',
component: RadioGroup.Root,
parameters: {
layout: 'centered',
controls: {
include: FIELDS
}
},
tags: ['autodocs']
});
</script>
<Story name="Default">
{#snippet template()}
<RadioGroup.Root value="option-1">
<div class="flex items-center space-x-2">
<RadioGroup.Item value="option-1" id="option-1" />
<Label for="option-1">Option 1</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="option-2" id="option-2" />
<Label for="option-2">Option 2</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="option-3" id="option-3" />
<Label for="option-3">Option 3</Label>
</div>
</RadioGroup.Root>
{/snippet}
</Story>
<Story name="With Descriptions">
{#snippet template()}
<RadioGroup.Root value="comfortable" class="space-y-4">
<div class="flex items-start space-x-2">
<RadioGroup.Item value="default" id="r1" class="mt-1" />
<div>
<Label for="r1" class="font-medium">Default</Label>
<p class="text-muted-foreground text-sm">The standard spacing for your app</p>
</div>
</div>
<div class="flex items-start space-x-2">
<RadioGroup.Item value="comfortable" id="r2" class="mt-1" />
<div>
<Label for="r2" class="font-medium">Comfortable</Label>
<p class="text-muted-foreground text-sm">More space for easier interaction</p>
</div>
</div>
<div class="flex items-start space-x-2">
<RadioGroup.Item value="compact" id="r3" class="mt-1" />
<div>
<Label for="r3" class="font-medium">Compact</Label>
<p class="text-muted-foreground text-sm">Less space, more content</p>
</div>
</div>
</RadioGroup.Root>
{/snippet}
</Story>
@@ -0,0 +1,14 @@
import { type ComponentProps } from 'svelte';
import * as Select from '@cio/ui/base/select';
export const FIELDS: (keyof ComponentProps<typeof Select.Root>)[] = [
'value',
'onValueChange',
'disabled',
'type',
'open',
'name',
'required',
'autocomplete',
'allowDeselect'
];
@@ -0,0 +1,88 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import * as Select from '@cio/ui/base/select';
import { FIELDS } from './fields';
const { Story } = defineMeta({
title: 'Atom/Select',
component: Select.Root,
parameters: {
layout: 'centered',
controls: {
include: FIELDS
}
},
argTypes: {
value: {
control: 'text',
description: 'The controlled value of the select'
},
onValueChange: {
control: false,
description: 'Callback function when the value changes'
},
disabled: {
control: 'boolean',
description: 'Whether the select is disabled'
},
type: {
control: 'select',
options: ['single', 'multiple'],
description: 'Whether the select allows single or multiple selection'
},
open: {
control: 'boolean',
description: 'The controlled open state of the select'
},
name: {
control: 'text',
description: 'The name attribute for the hidden input'
},
required: {
control: 'boolean',
description: 'Whether the select is required for form validation'
},
autocomplete: {
control: 'text',
description: 'The autocomplete attribute for the hidden input'
},
allowDeselect: {
control: 'boolean',
description: 'Whether to allow deselecting the selected value'
}
},
tags: ['autodocs']
});
const fruits = [
{ value: 'apple', label: 'Apple' },
{ value: 'banana', label: 'Banana' },
{ value: 'blueberry', label: 'Blueberry' },
{ value: 'grapes', label: 'Grapes' },
{ value: 'pineapple', label: 'Pineapple' }
];
let value = $state('');
const triggerContent = $derived(fruits.find((f) => f.value === value)?.label ?? 'Select a fruit');
</script>
<Story name="Default">
{#snippet template()}
<Select.Root type="single" name="favoriteFruit" bind:value>
<Select.Trigger class="w-[180px]">
{triggerContent}
</Select.Trigger>
<Select.Content>
<Select.Group>
<Select.Label>Fruits</Select.Label>
{#each fruits as fruit (fruit.value)}
<Select.Item value={fruit.value} label={fruit.label} disabled={fruit.value === 'grapes'}>
{fruit.label}
</Select.Item>
{/each}
</Select.Group>
</Select.Content>
</Select.Root>
{/snippet}
</Story>
@@ -0,0 +1,4 @@
import { type ComponentProps } from 'svelte';
import { Separator } from '@cio/ui/base/separator';
export const FIELDS: (keyof ComponentProps<typeof Separator>)[] = ['orientation', 'decorative'];
@@ -0,0 +1,51 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import { Separator } from '@cio/ui/base/separator';
import { FIELDS } from './fields';
const { Story } = defineMeta({
title: 'Atom/Separator',
component: Separator,
parameters: {
layout: 'centered',
controls: {
include: FIELDS
}
},
tags: ['autodocs']
});
</script>
<Story name="Horizontal">
{#snippet template()}
<div class="w-96 space-y-4">
<div>
<h4 class="text-sm font-medium">Section 1</h4>
<p class="text-muted-foreground text-sm">Content for the first section</p>
</div>
<Separator />
<div>
<h4 class="text-sm font-medium">Section 2</h4>
<p class="text-muted-foreground text-sm">Content for the second section</p>
</div>
</div>
{/snippet}
</Story>
<Story name="Vertical">
{#snippet template()}
<div class="flex h-20 items-center space-x-4">
<div class="space-y-1">
<h4 class="text-sm font-medium">Item 1</h4>
</div>
<Separator orientation="vertical" />
<div class="space-y-1">
<h4 class="text-sm font-medium">Item 2</h4>
</div>
<Separator orientation="vertical" />
<div class="space-y-1">
<h4 class="text-sm font-medium">Item 3</h4>
</div>
</div>
{/snippet}
</Story>
@@ -0,0 +1,4 @@
import { type ComponentProps } from 'svelte';
import { Sheet } from '@cio/ui/base/sheet';
export const FIELDS: (keyof ComponentProps<typeof Sheet>)[] = ['open', 'onOpenChange'];
@@ -0,0 +1,108 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import { Sheet, Button, Input, Label } from '@cio/ui';
import { FIELDS } from './fields';
const { Story } = defineMeta({
title: 'Atom/Sheet',
component: Sheet.Root,
parameters: {
layout: 'centered',
controls: {
include: FIELDS
}
},
argTypes: {
open: {
control: 'boolean',
description: 'The controlled open state of the sheet'
},
onOpenChange: {
control: false,
description: 'Callback function when the open state changes'
}
},
tags: ['autodocs']
});
</script>
<Story name="Right">
{#snippet template()}
<Sheet.Root>
<Sheet.SheetTrigger>
<Button.Root variant="outline">Open Sheet</Button.Root>
</Sheet.SheetTrigger>
<Sheet.SheetContent>
<Sheet.SheetHeader>
<Sheet.SheetTitle>Edit profile</Sheet.SheetTitle>
<Sheet.SheetDescription
>Make changes to your profile here. Click save when you're done.</Sheet.SheetDescription
>
</Sheet.SheetHeader>
<div class="grid gap-4 py-4">
<div class="grid grid-cols-4 items-center gap-4">
<Label.Root for="name" class="text-right">Name</Label.Root>
<Input.Root id="name" value="Pedro Duarte" class="col-span-3" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label.Root for="username" class="text-right">Username</Label.Root>
<Input.Root id="username" value="@peduarte" class="col-span-3" />
</div>
</div>
<Sheet.SheetFooter>
<Sheet.SheetClose>
<Button.Root type="submit">Save changes</Button.Root>
</Sheet.SheetClose>
</Sheet.SheetFooter>
</Sheet.SheetContent>
</Sheet.Root>
{/snippet}
</Story>
<Story name="Left">
{#snippet template()}
<Sheet.Root>
<Sheet.SheetTrigger>
<Button.Root>Open Left Sheet</Button.Root>
</Sheet.SheetTrigger>
<Sheet.SheetContent side="left">
<Sheet.SheetHeader>
<Sheet.SheetTitle>Left Sheet</Sheet.SheetTitle>
<Sheet.SheetDescription>This sheet opens from the left side.</Sheet.SheetDescription>
</Sheet.SheetHeader>
</Sheet.SheetContent>
</Sheet.Root>
{/snippet}
</Story>
<Story name="Top">
{#snippet template()}
<Sheet.Root>
<Sheet.SheetTrigger>
<Button.Root>Open Top Sheet</Button.Root>
</Sheet.SheetTrigger>
<Sheet.SheetContent side="top">
<Sheet.SheetHeader>
<Sheet.SheetTitle>Top Sheet</Sheet.SheetTitle>
<Sheet.SheetDescription>This sheet opens from the top.</Sheet.SheetDescription>
</Sheet.SheetHeader>
</Sheet.SheetContent>
</Sheet.Root>
{/snippet}
</Story>
<Story name="Bottom">
{#snippet template()}
<Sheet.Root>
<Sheet.SheetTrigger>
<Button.Root>Open Bottom Sheet</Button.Root>
</Sheet.SheetTrigger>
<Sheet.SheetContent side="bottom">
<Sheet.SheetHeader>
<Sheet.SheetTitle>Bottom Sheet</Sheet.SheetTitle>
<Sheet.SheetDescription>This sheet opens from the bottom.</Sheet.SheetDescription>
</Sheet.SheetHeader>
</Sheet.SheetContent>
</Sheet.Root>
{/snippet}
</Story>
@@ -0,0 +1,4 @@
import { type ComponentProps } from 'svelte';
import { Toaster as Sonner } from '@cio/ui/base/sonner';
export const FIELDS: (keyof ComponentProps<typeof Sonner>)[] = ['types', 'position', 'expand'];
@@ -0,0 +1,67 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import { Toaster, toast } from '@cio/ui/base/sonner';
import { Button } from '@cio/ui/base/button';
import { FIELDS } from './fields';
const { Story } = defineMeta({
title: 'Atom/Sonner (Toast)',
component: Toaster,
parameters: {
layout: 'centered',
controls: {
include: FIELDS
}
},
tags: ['autodocs']
});
</script>
<Story name="Default">
{#snippet template()}
<div class="flex flex-col gap-4">
<Toaster />
<Button onclick={() => toast('This is a default toast message')}>Show Toast</Button>
</div>
{/snippet}
</Story>
<Story name="Success">
{#snippet template()}
<div class="flex flex-col gap-4">
<Toaster />
<Button variant="outline" onclick={() => toast.success('Course created successfully!')}>
Show Success Toast
</Button>
</div>
{/snippet}
</Story>
<Story name="Error">
{#snippet template()}
<div class="flex flex-col gap-4">
<Toaster />
<Button variant="destructive" onclick={() => toast.error('Failed to save changes. Please try again.')}>
Show Error Toast
</Button>
</div>
{/snippet}
</Story>
<Story name="Warning">
{#snippet template()}
<div class="flex flex-col gap-4">
<Toaster />
<Button variant="outline" onclick={() => toast.warning('You have unsaved changes!')}>Show Warning Toast</Button>
</div>
{/snippet}
</Story>
<Story name="Info">
{#snippet template()}
<div class="flex flex-col gap-4">
<Toaster />
<Button variant="secondary" onclick={() => toast.info('New updates are available')}>Show Info Toast</Button>
</div>
{/snippet}
</Story>
@@ -0,0 +1,51 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import { Spinner } from '@cio/ui/base/spinner';
const { Story } = defineMeta({
title: 'Atom/Spinner',
component: Spinner,
parameters: {
layout: 'centered'
},
tags: ['autodocs']
});
</script>
<Story name="Default">
{#snippet template()}
<Spinner />
{/snippet}
</Story>
<Story name="Small">
{#snippet template()}
<Spinner class="h-4 w-4" />
{/snippet}
</Story>
<Story name="Large">
{#snippet template()}
<Spinner class="h-12 w-12" />
{/snippet}
</Story>
<Story name="Custom Color">
{#snippet template()}
<div class="flex gap-4">
<Spinner class="text-blue-500" />
<Spinner class="text-green-500" />
<Spinner class="text-red-500" />
<Spinner class="text-purple-500" />
</div>
{/snippet}
</Story>
<Story name="With Text">
{#snippet template()}
<div class="flex items-center gap-2">
<Spinner />
<span>Loading...</span>
</div>
{/snippet}
</Story>
@@ -0,0 +1,11 @@
import { type ComponentProps } from 'svelte';
import { Switch } from '@cio/ui/base/switch';
export const FIELDS: (keyof ComponentProps<typeof Switch>)[] = [
'checked',
'onCheckedChange',
'disabled',
'name',
'required',
'value'
];
@@ -0,0 +1,29 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import { Switch } from '@cio/ui/base/switch';
import { Label } from '@cio/ui/base/label';
const { Story } = defineMeta({
title: 'Atom/Switch',
component: Switch,
parameters: {
layout: 'centered'
},
tags: ['autodocs']
});
</script>
<Story name="Default">
{#snippet template()}
<Switch id="airplane-mode" />
{/snippet}
</Story>
<Story name="With Label">
{#snippet template()}
<div class="flex items-center space-x-2">
<Switch id="airplane-mode" />
<Label for="airplane-mode">Airplane Mode</Label>
</div>
{/snippet}
</Story>
@@ -0,0 +1,92 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import * as Table from '@cio/ui/base/table';
const { Story } = defineMeta({
title: 'Atom/Table',
component: Table.Root,
parameters: {
layout: 'padded'
},
tags: ['autodocs']
});
</script>
<Story name="Default">
{#snippet template()}
<Table.Root>
<Table.Caption>A list of your recent invoices.</Table.Caption>
<Table.Header>
<Table.Row>
<Table.Head class="w-[100px]">Invoice</Table.Head>
<Table.Head>Status</Table.Head>
<Table.Head>Method</Table.Head>
<Table.Head class="text-right">Amount</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.Cell class="font-medium">INV001</Table.Cell>
<Table.Cell>Paid</Table.Cell>
<Table.Cell>Credit Card</Table.Cell>
<Table.Cell class="text-right">$250.00</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell class="font-medium">INV002</Table.Cell>
<Table.Cell>Pending</Table.Cell>
<Table.Cell>PayPal</Table.Cell>
<Table.Cell class="text-right">$150.00</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell class="font-medium">INV003</Table.Cell>
<Table.Cell>Unpaid</Table.Cell>
<Table.Cell>Bank Transfer</Table.Cell>
<Table.Cell class="text-right">$350.00</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell class="font-medium">INV004</Table.Cell>
<Table.Cell>Paid</Table.Cell>
<Table.Cell>Credit Card</Table.Cell>
<Table.Cell class="text-right">$450.00</Table.Cell>
</Table.Row>
</Table.Body>
<Table.Footer>
<Table.Row>
<Table.Cell colspan={3}>Total</Table.Cell>
<Table.Cell class="text-right">$1,200.00</Table.Cell>
</Table.Row>
</Table.Footer>
</Table.Root>
{/snippet}
</Story>
<Story name="Simple Table">
{#snippet template()}
<Table.Root>
<Table.Header>
<Table.Row>
<Table.Head>Name</Table.Head>
<Table.Head>Email</Table.Head>
<Table.Head>Role</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.Cell>John Doe</Table.Cell>
<Table.Cell>[email protected]</Table.Cell>
<Table.Cell>Admin</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>Jane Smith</Table.Cell>
<Table.Cell>[email protected]</Table.Cell>
<Table.Cell>User</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>Bob Johnson</Table.Cell>
<Table.Cell>[email protected]</Table.Cell>
<Table.Cell>Editor</Table.Cell>
</Table.Row>
</Table.Body>
</Table.Root>
{/snippet}
</Story>
@@ -0,0 +1,10 @@
import { type ComponentProps } from 'svelte';
import { Tabs } from '@cio/ui/base/tabs';
export const FIELDS: (keyof ComponentProps<typeof Tabs>)[] = [
'value',
'onValueChange',
'disabled',
'activationMode',
'orientation'
];
@@ -0,0 +1,44 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import * as Tabs from '@cio/ui/base/tabs';
import { FIELDS } from './fields';
const { Story } = defineMeta({
title: 'Atom/Tabs',
component: Tabs.Root,
parameters: {
layout: 'centered',
controls: {
include: FIELDS
}
},
tags: ['autodocs']
});
</script>
<Story name="Default">
{#snippet template()}
<Tabs.Root value="overview" class="w-[500px]">
<Tabs.List class="grid w-full grid-cols-3">
<Tabs.Trigger value="overview">Overview</Tabs.Trigger>
<Tabs.Trigger value="analytics">Analytics</Tabs.Trigger>
<Tabs.Trigger value="reports">Reports</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="overview">
<div class="space-y-4 pt-4">
<p class="text-muted-foreground text-sm">View your overview dashboard with key metrics and insights.</p>
</div>
</Tabs.Content>
<Tabs.Content value="analytics">
<div class="space-y-4 pt-4">
<p class="text-muted-foreground text-sm">Deep dive into your analytics and user behavior patterns.</p>
</div>
</Tabs.Content>
<Tabs.Content value="reports">
<div class="space-y-4 pt-4">
<p class="text-muted-foreground text-sm">Generate and download detailed reports for your records.</p>
</div>
</Tabs.Content>
</Tabs.Root>
{/snippet}
</Story>
@@ -0,0 +1,9 @@
import { type ComponentProps } from 'svelte';
import { Tooltip } from '@cio/ui/base/tooltip';
export const FIELDS: (keyof ComponentProps<typeof Tooltip>)[] = [
'disableHoverableContent',
'delayDuration',
'ignoreNonKeyboardFocus',
'disabled'
];
@@ -2,13 +2,34 @@
import { defineMeta } from '@storybook/addon-svelte-csf';
import { Tooltip, Provider, Trigger, Content } from '@cio/ui/base/tooltip';
import { Button } from '@cio/ui/base/button';
// import { Arro}
import { FIELDS } from './fields';
const { Story } = defineMeta({
title: 'Atom/Tooltip',
component: Provider,
parameters: {
layout: 'centered'
layout: 'centered',
controls: {
include: FIELDS
}
},
argTypes: {
disableHoverableContent: {
control: 'boolean',
description: 'Prevents the content from being hoverable'
},
delayDuration: {
control: 'number',
description: 'The delay in milliseconds before the tooltip appears'
},
ignoreNonKeyboardFocus: {
control: 'boolean',
description: 'Whether to ignore non-keyboard focus events'
},
disabled: {
control: 'boolean',
description: 'Whether the tooltip is disabled'
}
},
tags: ['autodocs']
});
@@ -0,0 +1,72 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
SidebarTrigger
} from '@cio/ui/base/sidebar';
const { Story } = defineMeta({
title: 'Molecules/Sidebar',
component: Sidebar,
parameters: {
layout: 'fullscreen'
},
tags: ['autodocs']
});
</script>
<Story name="Default">
{#snippet template()}
<SidebarProvider>
<Sidebar>
<SidebarHeader>
<h2 class="px-4 text-lg font-semibold">My App</h2>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Navigation</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton>
<span>Dashboard</span>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton>
<span>Projects</span>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton>
<span>Settings</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<p class="text-muted-foreground px-4 text-sm">© 2024 My App</p>
</SidebarFooter>
</Sidebar>
<main class="flex-1 p-4">
<SidebarTrigger />
<div class="mt-4">
<h1 class="text-2xl font-bold">Main Content</h1>
<p class="mt-2">This is the main content area.</p>
</div>
</main>
</SidebarProvider>
{/snippet}
</Story>
+5 -1
View File
@@ -16,7 +16,10 @@
"@tailwindcss/typography": "^0.5.18",
"bits-ui": "^2.11.7",
"clsx": "^2.1.1",
"layerchart": "2.0.0-next.27",
"mode-watcher": "^1.1.0",
"svelte": "^5.39.11",
"svelte-sonner": "^1.0.5",
"tailwind-merge": "^3.3.1",
"tailwind-variants": "^3.1.1",
"tailwindcss": "^4.1.14",
@@ -42,9 +45,10 @@
"@tiptap/pm": "^3.7.1",
"@tiptap/starter-kit": "^3.7.1",
"@tiptap/suggestion": "^3.7.1",
"d3-array": "^3.2.4",
"d3-scale": "^4.0.2",
"katex": "^0.16.22",
"lowlight": "^3.3.0",
"mode-watcher": "^0.5.1",
"svelte-tiptap": "^3.0.0",
"tippy.js": "^6.3.7",
"tiptap-extension-auto-joiner": "^0.1.3",
@@ -0,0 +1,22 @@
<script lang="ts">
import { Accordion as AccordionPrimitive } from "bits-ui";
import { cn, type WithoutChild } from "../../tools";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithoutChild<AccordionPrimitive.ContentProps> = $props();
</script>
<AccordionPrimitive.Content
bind:ref
data-slot="accordion-content"
class="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
{...restProps}
>
<div class={cn("pb-4 pt-0", className)}>
{@render children?.()}
</div>
</AccordionPrimitive.Content>
@@ -0,0 +1,17 @@
<script lang="ts">
import { Accordion as AccordionPrimitive } from "bits-ui";
import { cn } from "../../tools";
let {
ref = $bindable(null),
class: className,
...restProps
}: AccordionPrimitive.ItemProps = $props();
</script>
<AccordionPrimitive.Item
bind:ref
data-slot="accordion-item"
class={cn("border-b last:border-b-0", className)}
{...restProps}
/>
@@ -0,0 +1,32 @@
<script lang="ts">
import { Accordion as AccordionPrimitive } from "bits-ui";
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
import { cn, type WithoutChild } from "../../tools";
let {
ref = $bindable(null),
class: className,
level = 3,
children,
...restProps
}: WithoutChild<AccordionPrimitive.TriggerProps> & {
level?: AccordionPrimitive.HeaderProps["level"];
} = $props();
</script>
<AccordionPrimitive.Header {level} class="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
bind:ref
class={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium outline-none transition-all hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
className
)}
{...restProps}
>
{@render children?.()}
<ChevronDownIcon
class="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200"
/>
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
@@ -0,0 +1,16 @@
<script lang="ts">
import { Accordion as AccordionPrimitive } from "bits-ui";
let {
ref = $bindable(null),
value = $bindable(),
...restProps
}: AccordionPrimitive.RootProps = $props();
</script>
<AccordionPrimitive.Root
bind:ref
bind:value={value as never}
data-slot="accordion"
{...restProps}
/>
+16
View File
@@ -0,0 +1,16 @@
import Root from "./accordion.svelte";
import Content from "./accordion-content.svelte";
import Item from "./accordion-item.svelte";
import Trigger from "./accordion-trigger.svelte";
export {
Root,
Content,
Item,
Trigger,
//
Root as Accordion,
Content as AccordionContent,
Item as AccordionItem,
Trigger as AccordionTrigger,
};
@@ -0,0 +1,13 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from 'bits-ui';
import { cn } from '../../tools';
let { ref = $bindable(null), class: className, ...restProps }: AvatarPrimitive.FallbackProps = $props();
</script>
<AvatarPrimitive.Fallback
bind:ref
data-slot="avatar-fallback"
class={cn('bg-muted flex size-full items-center justify-center rounded-full', className)}
{...restProps}
/>
@@ -0,0 +1,13 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from 'bits-ui';
import { cn } from '../../tools';
let { ref = $bindable(null), class: className, ...restProps }: AvatarPrimitive.ImageProps = $props();
</script>
<AvatarPrimitive.Image
bind:ref
data-slot="avatar-image"
class={cn('aspect-square size-full', className)}
{...restProps}
/>
+19
View File
@@ -0,0 +1,19 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from 'bits-ui';
import { cn } from '../../tools';
let {
ref = $bindable(null),
loadingStatus = $bindable('loading'),
class: className,
...restProps
}: AvatarPrimitive.RootProps = $props();
</script>
<AvatarPrimitive.Root
bind:ref
bind:loadingStatus
data-slot="avatar"
class={cn('relative flex size-8 shrink-0 overflow-hidden rounded-full', className)}
{...restProps}
/>
+13
View File
@@ -0,0 +1,13 @@
import Root from "./avatar.svelte";
import Image from "./avatar-image.svelte";
import Fallback from "./avatar-fallback.svelte";
export {
Root,
Image,
Fallback,
//
Root as Avatar,
Image as AvatarImage,
Fallback as AvatarFallback,
};
+48
View File
@@ -0,0 +1,48 @@
<script lang="ts" module>
import { type VariantProps, tv } from 'tailwind-variants';
export const badgeVariants = tv({
base: 'focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-full border px-2 py-0.5 text-xs font-medium transition-[color,box-shadow] focus-visible:ring-[3px] [&>svg]:pointer-events-none [&>svg]:size-3',
variants: {
variant: {
default: 'bg-primary text-primary-foreground [a&]:hover:bg-primary/90 border-transparent',
secondary: 'bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent',
destructive:
'bg-destructive [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70 border-transparent text-white',
outline: 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground'
}
},
defaultVariants: {
variant: 'default'
}
});
export type BadgeVariant = VariantProps<typeof badgeVariants>['variant'];
</script>
<script lang="ts">
import type { HTMLAnchorAttributes } from 'svelte/elements';
import { cn, type WithElementRef } from '../../tools';
let {
ref = $bindable(null),
href,
class: className,
variant = 'default',
children,
...restProps
}: WithElementRef<HTMLAnchorAttributes> & {
variant?: BadgeVariant;
} = $props();
</script>
<svelte:element
this={href ? 'a' : 'span'}
bind:this={ref}
data-slot="badge"
{href}
class={cn(badgeVariants({ variant }), className)}
{...restProps}
>
{@render children?.()}
</svelte:element>
+2
View File
@@ -0,0 +1,2 @@
export { default as Badge } from "./badge.svelte";
export { badgeVariants, type BadgeVariant } from "./badge.svelte";
@@ -0,0 +1,23 @@
<script lang="ts">
import EllipsisIcon from '@lucide/svelte/icons/ellipsis';
import type { HTMLAttributes } from 'svelte/elements';
import { cn, type WithElementRef, type WithoutChildren } from '../../tools';
let {
ref = $bindable(null),
class: className,
...restProps
}: WithoutChildren<WithElementRef<HTMLAttributes<HTMLSpanElement>>> = $props();
</script>
<span
bind:this={ref}
data-slot="breadcrumb-ellipsis"
role="presentation"
aria-hidden="true"
class={cn('flex size-9 items-center justify-center', className)}
{...restProps}
>
<EllipsisIcon class="size-4" />
<span class="sr-only">More</span>
</span>
@@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLLiAttributes } from "svelte/elements";
import { cn, type WithElementRef } from '../../tools';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLLiAttributes> = $props();
</script>
<li
bind:this={ref}
data-slot="breadcrumb-item"
class={cn("inline-flex items-center gap-1.5", className)}
{...restProps}
>
{@render children?.()}
</li>
@@ -0,0 +1,31 @@
<script lang="ts">
import type { HTMLAnchorAttributes } from 'svelte/elements';
import type { Snippet } from 'svelte';
import { cn, type WithElementRef } from '../../tools';
let {
ref = $bindable(null),
class: className,
href = undefined,
child,
children,
...restProps
}: WithElementRef<HTMLAnchorAttributes> & {
child?: Snippet<[{ props: HTMLAnchorAttributes }]>;
} = $props();
const attrs = $derived({
'data-slot': 'breadcrumb-link',
class: cn('hover:text-foreground transition-colors', className),
href,
...restProps
});
</script>
{#if child}
{@render child({ props: attrs })}
{:else}
<a bind:this={ref} {...attrs}>
{@render children?.()}
</a>
{/if}
@@ -0,0 +1,15 @@
<script lang="ts">
import type { HTMLOlAttributes } from 'svelte/elements';
import { cn, type WithElementRef } from '../../tools';
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLOlAttributes> = $props();
</script>
<ol
bind:this={ref}
data-slot="breadcrumb-list"
class={cn('text-muted-foreground flex flex-wrap items-center gap-1.5 break-words text-sm sm:gap-2.5', className)}
{...restProps}
>
{@render children?.()}
</ol>
@@ -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<HTMLSpanElement>> = $props();
</script>
<span
bind:this={ref}
data-slot="breadcrumb-page"
role="link"
aria-disabled="true"
aria-current="page"
class={cn('text-foreground font-normal', className)}
{...restProps}
>
{@render children?.()}
</span>
@@ -0,0 +1,22 @@
<script lang="ts">
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
import { cn, type WithElementRef } from '../../tools';
import type { HTMLLiAttributes } from 'svelte/elements';
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLLiAttributes> = $props();
</script>
<li
bind:this={ref}
data-slot="breadcrumb-separator"
role="presentation"
aria-hidden="true"
class={cn('[&>svg]:size-3.5', className)}
{...restProps}
>
{#if children}
{@render children?.()}
{:else}
<ChevronRightIcon />
{/if}
</li>
@@ -0,0 +1,15 @@
<script lang="ts">
import type { WithElementRef } from '../../tools';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
</script>
<nav bind:this={ref} data-slot="breadcrumb" class={className} aria-label="breadcrumb" {...restProps}>
{@render children?.()}
</nav>
+25
View File
@@ -0,0 +1,25 @@
import Root from "./breadcrumb.svelte";
import Ellipsis from "./breadcrumb-ellipsis.svelte";
import Item from "./breadcrumb-item.svelte";
import Separator from "./breadcrumb-separator.svelte";
import Link from "./breadcrumb-link.svelte";
import List from "./breadcrumb-list.svelte";
import Page from "./breadcrumb-page.svelte";
export {
Root,
Ellipsis,
Item,
Separator,
Link,
List,
Page,
//
Root as Breadcrumb,
Ellipsis as BreadcrumbEllipsis,
Item as BreadcrumbItem,
Separator as BreadcrumbSeparator,
Link as BreadcrumbLink,
List as BreadcrumbList,
Page as BreadcrumbPage,
};
+11 -1
View File
@@ -2,6 +2,7 @@
import { cn, type WithElementRef } from '../../tools';
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
import { type VariantProps, tv } from 'tailwind-variants';
import { Spinner } from '../spinner';
export const buttonVariants = tv({
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
@@ -38,6 +39,7 @@
WithElementRef<HTMLAnchorAttributes> & {
variant?: ButtonVariant;
size?: ButtonSize;
isLoading?: boolean;
};
</script>
@@ -48,11 +50,13 @@
size = 'default',
ref = $bindable(null),
href = undefined,
isLoading = false,
type = 'button',
disabled,
children,
...restProps
}: ButtonProps = $props();
let disabled = $derived(restProps.disabled || isLoading);
</script>
{#if href}
@@ -66,6 +70,9 @@
tabindex={disabled ? -1 : undefined}
{...restProps}
>
{#if isLoading}
<Spinner />
{/if}
{@render children?.()}
</a>
{:else}
@@ -77,6 +84,9 @@
{disabled}
{...restProps}
>
{#if isLoading}
<Spinner />
{/if}
{@render children?.()}
</button>
{/if}
@@ -0,0 +1,80 @@
<script lang="ts">
import { cn, type WithElementRef } from '../../tools';
import type { HTMLAttributes } from 'svelte/elements';
import ChartStyle from './chart-style.svelte';
import { setChartContext, type ChartConfig } from './chart-utils.js';
const uid = $props.id();
let {
ref = $bindable(null),
id = uid,
class: className,
children,
config,
...restProps
}: WithElementRef<HTMLAttributes<HTMLElement>> & {
config: ChartConfig;
} = $props();
const chartId = `chart-${id || uid.replace(/:/g, '')}`;
setChartContext({
get config() {
return config;
}
});
</script>
<div
bind:this={ref}
data-chart={chartId}
data-slot="chart"
class={cn(
'flex aspect-video justify-center overflow-visible text-xs',
// Overrides
//
// Stroke around dots/marks when hovering
'[&_.stroke-white]:stroke-transparent',
// override the default stroke color of lines
'[&_.lc-line]:stroke-border/50',
// by default, layerchart shows a line intersecting the point when hovering, this hides that
'[&_.lc-highlight-line]:stroke-0',
// by default, when you hover a point on a stacked series chart, it will drop the opacity
// of the other series, this overrides that
'[&_.lc-area-path]:opacity-100 [&_.lc-highlight-line]:opacity-100 [&_.lc-highlight-point]:opacity-100 [&_.lc-spline-path]:opacity-100 [&_.lc-text-svg]:overflow-visible [&_.lc-text]:text-xs',
// We don't want the little tick lines between the axis labels and the chart, so we remove
// the stroke. The alternative is to manually disable `tickMarks` on the x/y axis of every
// chart.
'[&_.lc-axis-tick]:stroke-0',
// We don't want to display the rule on the x/y axis, as there is already going to be
// a grid line there and rule ends up overlapping the marks because it is rendered after
// the marks
'[&_.lc-rule-x-line:not(.lc-grid-x-rule)]:stroke-0 [&_.lc-rule-y-line:not(.lc-grid-y-rule)]:stroke-0',
'[&_.lc-grid-x-radial-line]:stroke-border [&_.lc-grid-x-radial-circle]:stroke-border',
'[&_.lc-grid-y-radial-line]:stroke-border [&_.lc-grid-y-radial-circle]:stroke-border',
// Legend adjustments
'[&_.lc-legend-swatch-button]:items-center [&_.lc-legend-swatch-button]:gap-1.5',
'[&_.lc-legend-swatch-group]:items-center [&_.lc-legend-swatch-group]:gap-4',
'[&_.lc-legend-swatch]:size-2.5 [&_.lc-legend-swatch]:rounded-[2px]',
// Labels
'[&_.lc-labels-text:not([fill])]:fill-foreground [&_text]:stroke-transparent',
// Tick labels on th x/y axes
'[&_.lc-axis-tick-label]:fill-muted-foreground [&_.lc-axis-tick-label]:font-normal',
'[&_.lc-tooltip-rects-g]:fill-transparent',
'[&_.lc-layout-svg-g]:fill-transparent',
'[&_.lc-root-container]:w-full',
className
)}
{...restProps}
>
<ChartStyle id={chartId} {config} />
{@render children?.()}
</div>
@@ -0,0 +1,37 @@
<script lang="ts">
import { THEMES, type ChartConfig } from "./chart-utils.js";
let { id, config }: { id: string; config: ChartConfig } = $props();
const colorConfig = $derived(
config ? Object.entries(config).filter(([, config]) => config.theme || config.color) : null
);
const themeContents = $derived.by(() => {
if (!colorConfig || !colorConfig.length) return;
const themeContents = [];
for (let [_theme, prefix] of Object.entries(THEMES)) {
let content = `${prefix} [data-chart=${id}] {\n`;
const color = colorConfig.map(([key, itemConfig]) => {
const theme = _theme as keyof typeof itemConfig.theme;
const color = itemConfig.theme?.[theme] || itemConfig.color;
return color ? `\t--color-${key}: ${color};` : null;
});
content += color.join("\n") + "\n}";
themeContents.push(content);
}
return themeContents.join("\n");
});
</script>
{#if themeContents}
{#key id}
<svelte:element this={"style"}>
{themeContents}
</svelte:element>
{/key}
{/if}
@@ -0,0 +1,152 @@
<script lang="ts">
import { cn, type WithElementRef, type WithoutChildren } from '../../tools';
import type { HTMLAttributes } from 'svelte/elements';
import { getPayloadConfigFromPayload, useChart, type TooltipPayload } from './chart-utils.js';
import { getTooltipContext, Tooltip as TooltipPrimitive } from 'layerchart';
import type { Snippet } from 'svelte';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function defaultFormatter(value: any, _payload: TooltipPayload[]) {
return `${value}`;
}
let {
ref = $bindable(null),
class: className,
hideLabel = false,
indicator = 'dot',
hideIndicator = false,
labelKey,
label,
labelFormatter = defaultFormatter,
labelClassName,
formatter,
nameKey,
color,
...restProps
}: WithoutChildren<WithElementRef<HTMLAttributes<HTMLDivElement>>> & {
hideLabel?: boolean;
label?: string;
indicator?: 'line' | 'dot' | 'dashed';
nameKey?: string;
labelKey?: string;
hideIndicator?: boolean;
labelClassName?: string;
labelFormatter?: // eslint-disable-next-line @typescript-eslint/no-explicit-any
((value: any, payload: TooltipPayload[]) => string | number | Snippet) | null;
formatter?: Snippet<
[
{
value: unknown;
name: string;
item: TooltipPayload;
index: number;
payload: TooltipPayload[];
}
]
>;
} = $props();
const chart = useChart();
const tooltipCtx = getTooltipContext();
const formattedLabel = $derived.by(() => {
if (hideLabel || !tooltipCtx.payload?.length) return null;
const [item] = tooltipCtx.payload;
const key = labelKey ?? item?.label ?? item?.name ?? 'value';
const itemConfig = getPayloadConfigFromPayload(chart.config, item, key);
const value =
!labelKey && typeof label === 'string'
? (chart.config[label as keyof typeof chart.config]?.label ?? label)
: (itemConfig?.label ?? item.label);
if (value === undefined) return null;
if (!labelFormatter) return value;
return labelFormatter(value, tooltipCtx.payload);
});
const nestLabel = $derived(tooltipCtx.payload.length === 1 && indicator !== 'dot');
</script>
{#snippet TooltipLabel()}
{#if formattedLabel}
<div class={cn('font-medium', labelClassName)}>
{#if typeof formattedLabel === 'function'}
{@render formattedLabel()}
{:else}
{formattedLabel}
{/if}
</div>
{/if}
{/snippet}
<TooltipPrimitive.Root variant="none">
<div
class={cn(
'border-border/50 bg-background grid min-w-[9rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl',
className
)}
{...restProps}
>
{#if !nestLabel}
{@render TooltipLabel()}
{/if}
<div class="grid gap-1.5">
{#each tooltipCtx.payload as item, i (item.key + i)}
{@const key = `${nameKey || item.key || item.name || 'value'}`}
{@const itemConfig = getPayloadConfigFromPayload(chart.config, item, key)}
{@const indicatorColor = color || item.payload?.color || item.color}
<div
class={cn(
'[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:size-2.5',
indicator === 'dot' && 'items-center'
)}
>
{#if formatter && item.value !== undefined && item.name}
{@render formatter({
value: item.value,
name: item.name,
item,
index: i,
payload: tooltipCtx.payload
})}
{:else}
{#if itemConfig?.icon}
<itemConfig.icon />
{:else if !hideIndicator}
<div
style="--color-bg: {indicatorColor}; --color-border: {indicatorColor};"
class={cn('border-(--color-border) bg-(--color-bg) shrink-0 rounded-[2px]', {
'size-2.5': indicator === 'dot',
'h-full w-1': indicator === 'line',
'w-0 border-[1.5px] border-dashed bg-transparent': indicator === 'dashed',
'my-0.5': nestLabel && indicator === 'dashed'
})}
></div>
{/if}
<div
class={cn('flex flex-1 shrink-0 justify-between leading-none', nestLabel ? 'items-end' : 'items-center')}
>
<div class="grid gap-1.5">
{#if nestLabel}
{@render TooltipLabel()}
{/if}
<span class="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{#if item.value !== undefined}
<span class="text-foreground font-mono font-medium tabular-nums">
{item.value.toLocaleString()}
</span>
{/if}
</div>
{/if}
</div>
{/each}
</div>
</div>
</TooltipPrimitive.Root>
+66
View File
@@ -0,0 +1,66 @@
import type { Tooltip } from "layerchart";
import { getContext, setContext, type Component, type ComponentProps, type Snippet } from "svelte";
export const THEMES = { light: "", dark: ".dark" } as const;
export type ChartConfig = {
[k in string]: {
label?: string;
icon?: Component;
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
);
};
export type ExtractSnippetParams<T> = T extends Snippet<[infer P]> ? P : never;
export type TooltipPayload = ExtractSnippetParams<
ComponentProps<typeof Tooltip.Root>["children"]
>["payload"][number];
// Helper to extract item config from a payload.
export function getPayloadConfigFromPayload(
config: ChartConfig,
payload: TooltipPayload,
key: string
) {
if (typeof payload !== "object" || payload === null) return undefined;
const payloadPayload =
"payload" in payload && typeof payload.payload === "object" && payload.payload !== null
? payload.payload
: undefined;
let configLabelKey: string = key;
if (payload.key === key) {
configLabelKey = payload.key;
} else if (payload.name === key) {
configLabelKey = payload.name;
} else if (key in payload && typeof payload[key as keyof typeof payload] === "string") {
configLabelKey = payload[key as keyof typeof payload] as string;
} else if (
payloadPayload !== undefined &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string;
}
return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config];
}
type ChartContextValue = {
config: ChartConfig;
};
const chartContextKey = Symbol("chart-context");
export function setChartContext(value: ChartContextValue) {
return setContext(chartContextKey, value);
}
export function useChart() {
return getContext<ChartContextValue>(chartContextKey);
}
+9
View File
@@ -0,0 +1,9 @@
import ChartContainer from './chart-container.svelte';
import ChartTooltip from './chart-tooltip.svelte';
export { getPayloadConfigFromPayload, type ChartConfig } from './chart-utils.js';
export { ChartContainer, ChartTooltip, ChartContainer as Container, ChartTooltip as Tooltip };
export { BarChart, LineChart, AreaChart, PieChart } from 'layerchart';
export { scaleBand } from 'd3-scale';
@@ -0,0 +1,36 @@
<script lang="ts">
import { Checkbox as CheckboxPrimitive } from 'bits-ui';
import CheckIcon from '@lucide/svelte/icons/check';
import MinusIcon from '@lucide/svelte/icons/minus';
import { cn, type WithoutChildrenOrChild } from '../../tools';
let {
ref = $bindable(null),
checked = $bindable(false),
indeterminate = $bindable(false),
class: className,
...restProps
}: WithoutChildrenOrChild<CheckboxPrimitive.RootProps> = $props();
</script>
<CheckboxPrimitive.Root
bind:ref
data-slot="checkbox"
class={cn(
'border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive shadow-xs peer flex size-4 shrink-0 items-center justify-center rounded-[4px] border outline-none transition-shadow focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
className
)}
bind:checked
bind:indeterminate
{...restProps}
>
{#snippet children({ checked, indeterminate })}
<div data-slot="checkbox-indicator" class="text-current transition-none">
{#if checked}
<CheckIcon class="size-3.5" />
{:else if indeterminate}
<MinusIcon class="size-3.5" />
{/if}
</div>
{/snippet}
</CheckboxPrimitive.Root>
+6
View File
@@ -0,0 +1,6 @@
import Root from "./checkbox.svelte";
export {
Root,
//
Root as Checkbox,
};
@@ -0,0 +1,7 @@
<script lang="ts">
import { Collapsible as CollapsiblePrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps }: CollapsiblePrimitive.ContentProps = $props();
</script>
<CollapsiblePrimitive.Content bind:ref data-slot="collapsible-content" {...restProps} />
@@ -0,0 +1,7 @@
<script lang="ts">
import { Collapsible as CollapsiblePrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps }: CollapsiblePrimitive.TriggerProps = $props();
</script>
<CollapsiblePrimitive.Trigger bind:ref data-slot="collapsible-trigger" {...restProps} />
@@ -0,0 +1,11 @@
<script lang="ts">
import { Collapsible as CollapsiblePrimitive } from "bits-ui";
let {
ref = $bindable(null),
open = $bindable(false),
...restProps
}: CollapsiblePrimitive.RootProps = $props();
</script>
<CollapsiblePrimitive.Root bind:ref bind:open data-slot="collapsible" {...restProps} />
+13
View File
@@ -0,0 +1,13 @@
import Root from "./collapsible.svelte";
import Trigger from "./collapsible-trigger.svelte";
import Content from "./collapsible-content.svelte";
export {
Root,
Content,
Trigger,
//
Root as Collapsible,
Content as CollapsibleContent,
Trigger as CollapsibleTrigger,
};
@@ -0,0 +1,40 @@
<script lang="ts">
import type { Command as CommandPrimitive, Dialog as DialogPrimitive } from 'bits-ui';
import type { Snippet } from 'svelte';
import Command from './command.svelte';
import * as Dialog from '../dialog';
import type { WithoutChildrenOrChild } from '../../tools';
let {
open = $bindable(false),
ref = $bindable(null),
value = $bindable(''),
title = 'Command Palette',
description = 'Search for a command to run',
portalProps,
children,
...restProps
}: WithoutChildrenOrChild<DialogPrimitive.RootProps> &
WithoutChildrenOrChild<CommandPrimitive.RootProps> & {
portalProps?: DialogPrimitive.PortalProps;
children: Snippet;
title?: string;
description?: string;
} = $props();
</script>
<Dialog.Root bind:open {...restProps}>
<Dialog.Header class="sr-only">
<Dialog.Title>{title}</Dialog.Title>
<Dialog.Description>{description}</Dialog.Description>
</Dialog.Header>
<Dialog.Content class="overflow-hidden p-0" {portalProps}>
<Command
class="**:data-[slot=command-input-wrapper]:h-12 [&_[data-command-group]:not([hidden])_~[data-command-group]]:pt-0 [&_[data-command-group]]:px-2 [&_[data-command-input-wrapper]_svg]:h-5 [&_[data-command-input-wrapper]_svg]:w-5 [&_[data-command-input]]:h-12 [&_[data-command-item]]:px-2 [&_[data-command-item]]:py-3 [&_[data-command-item]_svg]:h-5 [&_[data-command-item]_svg]:w-5"
{...restProps}
bind:value
bind:ref
{children}
/>
</Dialog.Content>
</Dialog.Root>
@@ -0,0 +1,13 @@
<script lang="ts">
import { Command as CommandPrimitive } from 'bits-ui';
import { cn } from '../../tools';
let { ref = $bindable(null), class: className, ...restProps }: CommandPrimitive.EmptyProps = $props();
</script>
<CommandPrimitive.Empty
bind:ref
data-slot="command-empty"
class={cn('py-6 text-center text-sm', className)}
{...restProps}
/>
@@ -0,0 +1,30 @@
<script lang="ts">
import { Command as CommandPrimitive, useId } from 'bits-ui';
import { cn } from '../../tools';
let {
ref = $bindable(null),
class: className,
children,
heading,
value,
...restProps
}: CommandPrimitive.GroupProps & {
heading?: string;
} = $props();
</script>
<CommandPrimitive.Group
bind:ref
data-slot="command-group"
class={cn('text-foreground overflow-hidden p-1', className)}
value={value ?? heading ?? `----${useId()}`}
{...restProps}
>
{#if heading}
<CommandPrimitive.GroupHeading class="text-muted-foreground px-2 py-1.5 text-xs font-medium">
{heading}
</CommandPrimitive.GroupHeading>
{/if}
<CommandPrimitive.GroupItems {children} />
</CommandPrimitive.Group>
@@ -0,0 +1,26 @@
<script lang="ts">
import { Command as CommandPrimitive } from 'bits-ui';
import SearchIcon from '@lucide/svelte/icons/search';
import { cn } from '../../tools';
let {
ref = $bindable(null),
class: className,
value = $bindable(''),
...restProps
}: CommandPrimitive.InputProps = $props();
</script>
<div class="flex h-9 items-center gap-2 border-b pl-3 pr-8" data-slot="command-input-wrapper">
<SearchIcon class="size-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
data-slot="command-input"
class={cn(
'placeholder:text-muted-foreground outline-hidden flex h-10 w-full rounded-md bg-transparent py-3 text-sm disabled:cursor-not-allowed disabled:opacity-50',
className
)}
bind:ref
{...restProps}
bind:value
/>
</div>
@@ -0,0 +1,16 @@
<script lang="ts">
import { Command as CommandPrimitive } from 'bits-ui';
import { cn } from '../../tools';
let { ref = $bindable(null), class: className, ...restProps }: CommandPrimitive.ItemProps = $props();
</script>
<CommandPrimitive.Item
bind:ref
data-slot="command-item"
class={cn(
"aria-selected:bg-accent aria-selected:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...restProps}
/>
@@ -0,0 +1,16 @@
<script lang="ts">
import { Command as CommandPrimitive } from 'bits-ui';
import { cn } from '../../tools';
let { ref = $bindable(null), class: className, ...restProps }: CommandPrimitive.LinkItemProps = $props();
</script>
<CommandPrimitive.LinkItem
bind:ref
data-slot="command-item"
class={cn(
"aria-selected:bg-accent aria-selected:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...restProps}
/>
@@ -0,0 +1,13 @@
<script lang="ts">
import { Command as CommandPrimitive } from 'bits-ui';
import { cn } from '../../tools';
let { ref = $bindable(null), class: className, ...restProps }: CommandPrimitive.ListProps = $props();
</script>
<CommandPrimitive.List
bind:ref
data-slot="command-list"
class={cn('max-h-[300px] scroll-py-1 overflow-y-auto overflow-x-hidden', className)}
{...restProps}
/>
@@ -0,0 +1,13 @@
<script lang="ts">
import { Command as CommandPrimitive } from 'bits-ui';
import { cn } from '../../tools';
let { ref = $bindable(null), class: className, ...restProps }: CommandPrimitive.SeparatorProps = $props();
</script>
<CommandPrimitive.Separator
bind:ref
data-slot="command-separator"
class={cn('bg-border -mx-1 h-px', className)}
{...restProps}
/>
@@ -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<HTMLSpanElement>> = $props();
</script>
<span
bind:this={ref}
data-slot="command-shortcut"
class={cn('text-muted-foreground ml-auto text-xs tracking-widest', className)}
{...restProps}
>
{@render children?.()}
</span>
@@ -0,0 +1,25 @@
<script lang="ts">
import { cn } from '../../tools';
import { Command as CommandPrimitive } from 'bits-ui';
export type CommandRootApi = CommandPrimitive.Root;
let {
api = $bindable(null),
ref = $bindable(null),
value = $bindable(''),
class: className,
...restProps
}: CommandPrimitive.RootProps & {
api?: CommandRootApi | null;
} = $props();
</script>
<CommandPrimitive.Root
bind:this={api}
bind:value
bind:ref
data-slot="command"
class={cn('bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md', className)}
{...restProps}
/>
+40
View File
@@ -0,0 +1,40 @@
import { Command as CommandPrimitive } from "bits-ui";
import Root from "./command.svelte";
import Dialog from "./command-dialog.svelte";
import Empty from "./command-empty.svelte";
import Group from "./command-group.svelte";
import Item from "./command-item.svelte";
import Input from "./command-input.svelte";
import List from "./command-list.svelte";
import Separator from "./command-separator.svelte";
import Shortcut from "./command-shortcut.svelte";
import LinkItem from "./command-link-item.svelte";
const Loading = CommandPrimitive.Loading;
export {
Root,
Dialog,
Empty,
Group,
Item,
LinkItem,
Input,
List,
Separator,
Shortcut,
Loading,
//
Root as Command,
Dialog as CommandDialog,
Empty as CommandEmpty,
Group as CommandGroup,
Item as CommandItem,
LinkItem as CommandLinkItem,
Input as CommandInput,
List as CommandList,
Separator as CommandSeparator,
Shortcut as CommandShortcut,
Loading as CommandLoading,
};
@@ -0,0 +1,7 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps }: DialogPrimitive.CloseProps = $props();
</script>
<DialogPrimitive.Close bind:ref data-slot="dialog-close" {...restProps} />
@@ -0,0 +1,43 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from 'bits-ui';
import XIcon from '@lucide/svelte/icons/x';
import type { Snippet } from 'svelte';
import * as Dialog from './index.js';
import { cn, type WithoutChildrenOrChild } from '../../tools';
let {
ref = $bindable(null),
class: className,
portalProps,
children,
showCloseButton = true,
...restProps
}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
portalProps?: DialogPrimitive.PortalProps;
children: Snippet;
showCloseButton?: boolean;
} = $props();
</script>
<Dialog.Portal {...portalProps}>
<Dialog.Overlay />
<DialogPrimitive.Content
bind:ref
data-slot="dialog-content"
class={cn(
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed left-[50%] top-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
className
)}
{...restProps}
>
{@render children?.()}
{#if showCloseButton}
<DialogPrimitive.Close
class="ring-offset-background focus:ring-ring rounded-xs focus:outline-hidden absolute end-4 top-4 opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
>
<XIcon />
<span class="sr-only">Close</span>
</DialogPrimitive.Close>
{/if}
</DialogPrimitive.Content>
</Dialog.Portal>
@@ -0,0 +1,13 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from 'bits-ui';
import { cn } from '../../tools';
let { ref = $bindable(null), class: className, ...restProps }: DialogPrimitive.DescriptionProps = $props();
</script>
<DialogPrimitive.Description
bind:ref
data-slot="dialog-description"
class={cn('text-muted-foreground text-sm', className)}
{...restProps}
/>
@@ -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="dialog-footer"
class={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', 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<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="dialog-header"
class={cn('flex flex-col gap-2 text-center sm:text-left', className)}
{...restProps}
>
{@render children?.()}
</div>
@@ -0,0 +1,16 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from 'bits-ui';
import { cn } from '../../tools';
let { ref = $bindable(null), class: className, ...restProps }: DialogPrimitive.OverlayProps = $props();
</script>
<DialogPrimitive.Overlay
bind:ref
data-slot="dialog-overlay"
class={cn(
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50',
className
)}
{...restProps}
/>
@@ -0,0 +1,13 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from 'bits-ui';
import { cn } from '../../tools';
let { ref = $bindable(null), class: className, ...restProps }: DialogPrimitive.TitleProps = $props();
</script>
<DialogPrimitive.Title
bind:ref
data-slot="dialog-title"
class={cn('text-lg font-semibold leading-none', className)}
{...restProps}
/>
@@ -0,0 +1,7 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps }: DialogPrimitive.TriggerProps = $props();
</script>
<DialogPrimitive.Trigger bind:ref data-slot="dialog-trigger" {...restProps} />
+37
View File
@@ -0,0 +1,37 @@
import { Dialog as DialogPrimitive } from "bits-ui";
import Title from "./dialog-title.svelte";
import Footer from "./dialog-footer.svelte";
import Header from "./dialog-header.svelte";
import Overlay from "./dialog-overlay.svelte";
import Content from "./dialog-content.svelte";
import Description from "./dialog-description.svelte";
import Trigger from "./dialog-trigger.svelte";
import Close from "./dialog-close.svelte";
const Root = DialogPrimitive.Root;
const Portal = DialogPrimitive.Portal;
export {
Root,
Title,
Portal,
Footer,
Header,
Trigger,
Overlay,
Content,
Description,
Close,
//
Root as Dialog,
Title as DialogTitle,
Portal as DialogPortal,
Footer as DialogFooter,
Header as DialogHeader,
Trigger as DialogTrigger,
Overlay as DialogOverlay,
Content as DialogContent,
Description as DialogDescription,
Close as DialogClose,
};
+1 -1
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import { Label as LabelPrimitive } from 'bits-ui';
import { cn } from '$src/tools/index.js';
import { cn } from '../../tools';
let { ref = $bindable(null), class: className, ...restProps }: LabelPrimitive.RootProps = $props();
</script>
+54
View File
@@ -0,0 +1,54 @@
import { Menubar as MenubarPrimitive } from "bits-ui";
import Root from "./menubar.svelte";
import CheckboxItem from "./menubar-checkbox-item.svelte";
import Content from "./menubar-content.svelte";
import Item from "./menubar-item.svelte";
import Group from "./menubar-group.svelte";
import RadioItem from "./menubar-radio-item.svelte";
import Separator from "./menubar-separator.svelte";
import Shortcut from "./menubar-shortcut.svelte";
import SubContent from "./menubar-sub-content.svelte";
import SubTrigger from "./menubar-sub-trigger.svelte";
import Trigger from "./menubar-trigger.svelte";
import Label from "./menubar-label.svelte";
import GroupHeading from "./menubar-group-heading.svelte";
const Menu = MenubarPrimitive.Menu;
const Sub = MenubarPrimitive.Sub;
const RadioGroup = MenubarPrimitive.RadioGroup;
export {
Root,
CheckboxItem,
Content,
Item,
RadioItem,
Separator,
Shortcut,
SubContent,
SubTrigger,
Trigger,
Menu,
Group,
Sub,
RadioGroup,
Label,
GroupHeading,
//
Root as Menubar,
CheckboxItem as MenubarCheckboxItem,
Content as MenubarContent,
Item as MenubarItem,
RadioItem as MenubarRadioItem,
Separator as MenubarSeparator,
Shortcut as MenubarShortcut,
SubContent as MenubarSubContent,
SubTrigger as MenubarSubTrigger,
Trigger as MenubarTrigger,
Menu as MenubarMenu,
Group as MenubarGroup,
Sub as MenubarSub,
RadioGroup as MenubarRadioGroup,
Label as MenubarLabel,
GroupHeading as MenubarGroupHeading,
};
@@ -0,0 +1,41 @@
<script lang="ts">
import { Menubar as MenubarPrimitive } from 'bits-ui';
import CheckIcon from '@lucide/svelte/icons/check';
import MinusIcon from '@lucide/svelte/icons/minus';
import { cn, type WithoutChildrenOrChild } from '../../tools';
import type { Snippet } from 'svelte';
let {
ref = $bindable(null),
class: className,
checked = $bindable(false),
indeterminate = $bindable(false),
children: childrenProp,
...restProps
}: WithoutChildrenOrChild<MenubarPrimitive.CheckboxItemProps> & {
children?: Snippet;
} = $props();
</script>
<MenubarPrimitive.CheckboxItem
bind:ref
bind:checked
bind:indeterminate
data-slot="menubar-checkbox-item"
class={cn(
"focus:bg-accent focus:text-accent-foreground rounded-xs outline-hidden relative flex cursor-default select-none items-center gap-2 py-1.5 pl-8 pr-2 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...restProps}
>
{#snippet children({ checked, indeterminate })}
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
{#if indeterminate}
<MinusIcon class="size-4" />
{:else}
<CheckIcon class={cn('size-4', !checked && 'text-transparent')} />
{/if}
</span>
{@render childrenProp?.()}
{/snippet}
</MenubarPrimitive.CheckboxItem>

Some files were not shown because too many files have changed in this diff Show More