init
All checks were successful
Deployment / Deploy Storybook (push) Successful in 5m47s

This commit is contained in:
Benjamin Palko 2025-04-01 13:49:37 -04:00
commit a48f0603b2
79 changed files with 3589 additions and 0 deletions

13
.editorconfig Normal file
View file

@ -0,0 +1,13 @@
root = true
[*]
end_of_line = lf
insert_final_newline = false
[*.{js,ts}]
indent_style = tab
indent_size = 4
[{*.{yml,mjs,json}}]
indent_style = space
indent_size = 2

1
.env Normal file
View file

@ -0,0 +1 @@
NPM_TOKEN=

37
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,37 @@
name: PR Gate
on:
pull_request:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.event.number || github.sha }}
cancel-in-progress: true
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
checks:
name: Checks
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Bun Setup
uses: oven-sh/setup-bun@v2
with:
bun-version-file: '.tool-versions'
- name: Install
run: bun install
- name: Lint
run: bun lint
- name: Sync
run: bun run build
- name: Svelte Check
run: bun check
- name: Prisma Check
run: bun prisma:validate
- name: Test
run: bun run test:unit

27
.github/workflows/deploy.yml vendored Normal file
View file

@ -0,0 +1,27 @@
name: Deployment
on:
push:
branches:
- main
jobs:
theia-storybook:
name: Deploy Storybook
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
registry: ${{ env.GITHUB_SERVER_URL }}
username: ${{ env.GITHUB_REPOSITORY_OWNER }}
password: ${{ secrets.NPM_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
push: true
file: Dockerfile.storybook
tags: git.palko.ca/${{ env.GITHUB_REPOSITORY }}-storybook:latest

185
.gitignore vendored Normal file
View file

@ -0,0 +1,185 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
# Storybook
storybook-static
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Caches
.cache
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

1
.husky/pre-commit Normal file
View file

@ -0,0 +1 @@
bunx lint-staged

6
.prettierignore Normal file
View file

@ -0,0 +1,6 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock
bun.lock
*.md

21
.prettierrc Normal file
View file

@ -0,0 +1,21 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 100,
"endOfLine": "lf",
"arrowParens": "always",
"jsxSingleQuote": false,
"semi": true,
"quoteProps": "as-needed",
"tabWidth": 4,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
}

17
.storybook/main.js Normal file
View file

@ -0,0 +1,17 @@
/** @type { import('@storybook/sveltekit').StorybookConfig } */
const config = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|ts|svelte)'],
addons: [
'@chromatic-com/storybook',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'@storybook/addon-styling-webpack',
'@storybook/addon-svelte-csf',
'@storybook/addon-themes',
],
framework: {
name: '@storybook/sveltekit',
options: {},
},
};
export default config;

2
.storybook/preview.css Normal file
View file

@ -0,0 +1,2 @@
@import 'tailwindcss';
@plugin "daisyui";

28
.storybook/preview.js Normal file
View file

@ -0,0 +1,28 @@
import { withThemeByDataAttribute } from '@storybook/addon-themes';
import './preview.css';
/** @type { import('@storybook/svelte').Preview } */
const preview = {
tags: ['autodocs'],
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
decorators: [
withThemeByDataAttribute({
themes: {
light: 'light',
dark: 'dark',
night: 'night',
},
defaultTheme: 'dark',
attributeName: 'data-theme',
}),
],
};
export default preview;

1
.tool-versions Normal file
View file

@ -0,0 +1 @@
bun 1.2.4

11
Dockerfile.storybook Normal file
View file

@ -0,0 +1,11 @@
FROM oven/bun:1.2-alpine AS build
WORKDIR /opt/build
COPY . .
RUN bun install --frozen-lockfile \
&& bun run build-storybook
FROM nginx:alpine3.21
COPY --from=build --chown=1000:1000 /opt/build/storybook-static /usr/share/nginx/html

11
README.md Normal file
View file

@ -0,0 +1,11 @@
# Theia
Common Svelte components
To run:
```bash
bun storybook
```
This project was created using `bun init` in bun v1.2.0. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.

1365
bun.lock Normal file

File diff suppressed because it is too large Load diff

3
bunfig.toml Normal file
View file

@ -0,0 +1,3 @@
[install.scopes]
# registry with token
"@pantheon" = { token = "$NPM_TOKEN", url = "https://git.palko.ca/api/packages/pantheon/npm/" }

55
eslint.config.js Normal file
View file

@ -0,0 +1,55 @@
import prettier from 'eslint-config-prettier';
import js from '@eslint/js';
import { includeIgnoreFile } from '@eslint/compat';
import svelte from 'eslint-plugin-svelte';
import globals from 'globals';
import { fileURLToPath } from 'node:url';
import ts from 'typescript-eslint';
const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
export default ts.config(
includeIgnoreFile(gitignorePath),
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs['flat/recommended'],
prettier,
...svelte.configs['flat/prettier'],
{
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
},
},
{
files: ['**/*.svelte'],
languageOptions: {
parserOptions: {
parser: ts.parser,
},
},
},
{
rules: {
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
},
],
'no-restricted-syntax': [
'error',
{
selector:
'CallExpression:matches([callee.object.object.name="prisma"], [callee.object.object.name="prismaTransactionClient"], [callee.object.object.name="transactionClient"]):matches([callee.property.name="findFirst"], [callee.property.name="findMany"], [callee.property.name="updateMany"], [callee.property.name="deleteMany"], [callee.property.name="count"], [callee.property.name="aggregate"], [callee.property.name="groupBy"]):not(:has(ObjectExpression > Property[key.name="where"] > ObjectExpression > Property[key.name="tenantId"]))',
message:
'Please filter on the current tenant when using findFirst, findMany, updateMany, deleteMany, count, aggregate or groupBy.',
},
],
},
}
);

97
package.json Normal file
View file

@ -0,0 +1,97 @@
{
"name": "@pantheon/theia",
"type": "module",
"version": "1.0.0",
"exports": {
".": {
"types": "./dist/index.d.ts",
"svelte": "./dist/index.js"
},
"./common": {
"types": "./dist/common/components/index.d.ts",
"svelte": "./dist/common/components/index.js"
}
},
"types": "./dist/index.d.ts",
"files": [
"!dist/**/*.spec.*",
"!dist/**/*.test.*",
"!dist/**/*.stories.*",
"dist"
],
"keywords": [
"svelte",
"UI",
"daisyui",
"component",
"library"
],
"scripts": {
"build-storybook": "storybook build",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"docker:build-storybook": "docker buildx -t $APP_NAME-storybook -f Dockerfile.storybook .",
"try": "echo ${APP_NAME}",
"format": "prettier --write .",
"lint": "prettier --check . && eslint .",
"test:unit": "vitest",
"test": "bun run test:unit -- --run && bun run test:e2e",
"storybook": "storybook dev -p 6006",
"test:e2e": "playwright test",
"package": "svelte-kit sync && svelte-package",
"prepublishOnly": "bun package",
"prepare": "husky || true"
},
"devDependencies": {
"@chromatic-com/storybook": "^3.2.2",
"@playwright/test": "^1.45.3",
"@storybook/addon-essentials": "^8.5.0",
"@storybook/addon-interactions": "^8.5.0",
"@storybook/addon-styling-webpack": "^1.0.1",
"@storybook/addon-svelte-csf": "^5.0.0-next.13",
"@storybook/addon-themes": "^8.5.0",
"@storybook/blocks": "^8.5.0",
"@storybook/svelte": "^8.5.0",
"@storybook/sveltekit": "^8.5.0",
"@storybook/test": "^8.5.0",
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.15.3",
"@sveltejs/package": "^2.3.9",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tailwindcss/postcss": "^4.0.15",
"@types/bun": "^1.1.15",
"eslint": "^9.7.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.36.0",
"globals": "^15.0.0",
"husky": "^9.1.7",
"lucide-svelte": "^0.483.0",
"lint-staged": "^15.3.0",
"prettier": "^3.3.2",
"prettier-plugin-svelte": "^3.2.6",
"prettier-plugin-tailwindcss": "^0.6.11",
"storybook": "^8.5.0",
"svelte-check": "^4.0.0",
"tailwindcss": "^4.0.15",
"typescript": "^5.8.2",
"typescript-eslint": "^8.0.0",
"vite": "^6.0.7",
"vitest": "^2.0.4"
},
"dependencies": {
"clsx": "^2.1.1",
"tailwind-merge": "^2.5.5"
},
"peerDependencies": {
"daisyui": "^5.0.6",
"svelte": "^5.0.0"
},
"svelte": "./dist/index.js",
"lint-staged": {
"*.{json,yml,yaml,css}": "prettier --write",
"*.{js,ts,svelte}": [
"prettier --write",
"eslint --fix"
]
}
}

5
postcss.config.js Normal file
View file

@ -0,0 +1,5 @@
export default {
plugins: {
'@tailwindcss/postcss': {},
},
};

13
src/app.d.ts vendored Normal file
View file

@ -0,0 +1,13 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

12
src/app.html Normal file
View file

@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div>%sveltekit.body%</div>
</body>
</html>

View file

@ -0,0 +1,55 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import { fn } from '@storybook/test';
import type { ComponentProps } from 'svelte';
import Button from './Button.svelte';
const { Story } = defineMeta({
title: 'Actions/Button',
component: Button,
args: {
onclick: fn(),
},
argTypes: {
active: { control: 'boolean' },
block: { control: 'boolean' },
color: {
control: 'select',
options: [
undefined,
'neutral',
'primary',
'secondary',
'accent',
'ghost',
'info',
'success',
'warning',
'error',
],
},
disabled: { control: 'boolean' },
full: { control: 'boolean' },
shape: {
control: 'select',
options: ['square', 'circle'],
},
size: {
control: 'select',
options: ['xs', 'sm', 'md', 'lg'],
defaultValue: 'md',
},
style: {
control: 'select',
options: [undefined, 'outline', 'dash', 'soft', 'ghost', 'link'],
},
wide: { control: 'boolean' },
},
});
</script>
{#snippet template({ children: _, ...props }: Partial<ComponentProps<typeof Button>>)}
<Button {...props}>Button</Button>
{/snippet}
<Story name="Default" args={{}} children={template} />

View file

@ -0,0 +1,70 @@
<script lang="ts" module>
export type ButtonShape = 'square' | 'circle';
export type ButtonStyle = 'outline' | 'dash' | 'soft' | 'ghost' | 'link';
</script>
<script lang="ts">
import type { DaisyColor, DaisySize } from '$lib/types';
import clsx from 'clsx';
import type { SvelteHTMLElements } from 'svelte/elements';
import { twMerge } from 'tailwind-merge';
type Props = {
active?: boolean;
block?: boolean;
color?: DaisyColor;
full?: boolean;
shape?: ButtonShape;
size?: DaisySize;
style?: ButtonStyle;
wide?: boolean;
} & SvelteHTMLElements['button'];
let {
active = false,
block = false,
children,
class: className,
color,
full = false,
shape,
size,
style,
wide = false,
...props
}: Props = $props();
</script>
<button
{...props}
class={twMerge('btn', clsx(className))}
class:btn-active={active}
class:btn-block={block}
class:btn-neutral={color === 'neutral'}
class:btn-primary={color === 'primary'}
class:btn-secondary={color === 'secondary'}
class:btn-accent={color === 'accent'}
class:btn-info={color === 'info'}
class:btn-success={color === 'success'}
class:btn-warning={color === 'warning'}
class:btn-error={color === 'error'}
class:btn-disabled={props.disabled}
class:w-full={full}
class:btn-circle={shape === 'circle'}
class:btn-square={shape === 'square'}
class:btn-xs={size === 'xs'}
class:btn-sm={size === 'sm'}
class:btn-md={size === 'md'}
class:btn-lg={size === 'lg'}
class:btn-xl={size === 'xl'}
class:btn-outline={style === 'outline'}
class:btn-dash={style === 'dash'}
class:btn-soft={style === 'soft'}
class:btn-ghost={style === 'ghost'}
class:btn-link={style === 'link'}
class:btn-wide={wide}
>
{@render children?.()}
</button>
<style></style>

View file

@ -0,0 +1,44 @@
<script lang="ts" module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import Dropdown from './Dropdown.svelte';
const { Story } = defineMeta({
title: 'Actions/Dropdown',
component: Dropdown,
argTypes: {
hover: {
control: 'boolean',
},
open: {
control: 'boolean',
},
},
});
</script>
<Story name="Dropdown Menu">
<Dropdown>
{#snippet title()}
Click me!
{/snippet}
<ul class={'menu bg-base-200 rounded-box w-56'}>
<li><a>Item 1</a></li>
<li><a>Item 2</a></li>
<li><a>Item 3</a></li>
</ul>
</Dropdown>
</Story>
<Story name="Dropdown Card">
<Dropdown>
{#snippet title()}
Click me!
{/snippet}
<div class="card card-sm bg-base-100 z-1 w-64 shadow-md">
<div class="card-body">
<h2 class="card-title">You needed more info?</h2>
<p>This is a card. You can use any element as a dropdown.</p>
</div>
</div>
</Dropdown>
</Story>

View file

@ -0,0 +1,40 @@
<script lang="ts" module>
export type DropdownAlignment = {
vertical?: 'top' | 'bottom';
horizontal?: 'left' | 'right';
content?: 'start' | 'center' | 'end';
};
</script>
<script lang="ts">
import type { Snippet } from 'svelte';
type Props = {
alignment?: DropdownAlignment;
children: Snippet;
hover?: boolean;
open?: boolean;
title: Snippet;
};
let { alignment, children, hover, open, title }: Props = $props();
</script>
<div
class="dropdown"
class:dropdown-start={alignment?.content === 'start'}
class:dropdown-center={alignment?.content === 'center'}
class:dropdown-end={alignment?.content === 'end'}
class:dropdown-top={alignment?.vertical === 'top'}
class:dropdown-bottom={alignment?.vertical === 'bottom'}
class:dropdown-left={alignment?.horizontal === 'left'}
class:dropdown-right={alignment?.horizontal === 'right'}
class:dropdown-hover={hover}
class:dropdown-open={open}
>
<div tabindex="0" role="button" class="btn m-1">{@render title()}</div>
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<div tabindex="0" class="dropdown-content z-0">
{@render children?.()}
</div>
</div>

View file

@ -0,0 +1,32 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import type { ComponentProps } from 'svelte';
import Button from './Button.svelte';
import Modal from './Modal.svelte';
const { Story } = defineMeta({
title: 'Actions/Modal',
component: Modal,
argTypes: {
open: {
control: 'boolean',
defaultValue: false,
},
},
});
let dialog: HTMLDialogElement | undefined = $state(undefined);
</script>
{#snippet template({ children: _, ...props }: Partial<ComponentProps<typeof Modal>>)}
<Button onclick={() => dialog?.showModal()}>Open</Button>
<Modal {...props} backdrop bind:dialog>
<h3 class="text-lg font-bold">Hello!</h3>
<p class="py-4">Press ESC key or click the button below to close</p>
{#snippet actions()}
<Button onclick={() => dialog?.close()}>Close</Button>
{/snippet}
</Modal>
{/snippet}
<Story name="Default" children={template} />

View file

@ -0,0 +1,37 @@
<script lang="ts">
import clsx from 'clsx';
import type { Snippet } from 'svelte';
import type { SvelteHTMLElements } from 'svelte/elements';
import { twMerge } from 'tailwind-merge';
type Props = SvelteHTMLElements['dialog'] & {
actions?: Snippet;
backdrop?: boolean;
dialog?: HTMLDialogElement;
};
let {
actions,
backdrop,
dialog = $bindable(),
children,
class: className,
...props
}: Props = $props();
</script>
<dialog {...props} class={twMerge('modal', clsx(className))} bind:this={dialog}>
<div class="modal-box">
{@render children?.()}
{#if actions}
<div class="modal-action">
{@render actions()}
</div>
{/if}
</div>
{#if backdrop}
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
{/if}
</dialog>

View file

@ -0,0 +1,29 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import { type ComponentProps } from 'svelte';
import Swap from './Swap.svelte';
const { Story } = defineMeta({
title: 'Actions/Swap',
component: Swap,
argTypes: {
style: {
control: 'radio',
options: [undefined, 'flip', 'rotate'],
},
},
});
</script>
{#snippet template(props: Partial<ComponentProps<typeof Swap>>)}
<Swap {...props}>
{#snippet on()}
ON
{/snippet}
{#snippet off()}
OFF
{/snippet}
</Swap>
{/snippet}
<Story name="Default" children={template} />

View file

@ -0,0 +1,31 @@
<script lang="ts" module>
export type SwapStyle = 'flip' | 'rotate';
</script>
<script lang="ts">
import type { Snippet } from 'svelte';
type Props = {
active?: boolean;
indeterminate?: Snippet;
on: Snippet;
off: Snippet;
style?: SwapStyle;
};
let { active, indeterminate, on, off, style }: Props = $props();
</script>
<label
class="swap"
class:swap-active={active}
class:swap-flip={style === 'flip'}
class:swap-rotate={style === 'rotate'}
>
<input type="checkbox" />
{#if indeterminate}
{@render indeterminate()}
{/if}
<div class="swap-on fill-current">{@render on()}</div>
<div class="swap-off fill-current">{@render off()}</div>
</label>

View file

@ -0,0 +1,4 @@
export { default as Button } from './Button.svelte';
export { default as Dropdown } from './Dropdown.svelte';
export { default as Modal } from './Modal.svelte';
export { default as Swap } from './Swap.svelte';

View file

@ -0,0 +1,55 @@
<script lang="ts" module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import Accordion from './Accordion.svelte';
import { type ComponentProps } from 'svelte';
const { Story } = defineMeta({
title: 'Data Display/Accordion',
component: Accordion,
argTypes: {
close: {
control: 'boolean',
defaultValue: false,
},
open: {
control: 'boolean',
defaultValue: false,
},
style: {
control: 'radio',
options: [undefined, 'arrow', 'plus'],
},
},
});
</script>
{#snippet template(props: Partial<ComponentProps<typeof Accordion>>)}
<div class="flex flex-col gap-2">
<Accordion name={props.name!} style={props.style} initialOpen>
{#snippet title()}
Option 1
{/snippet}
{#snippet content()}
Description here!
{/snippet}
</Accordion>
<Accordion name={props.name!} style={props.style}>
{#snippet title()}
Option 2
{/snippet}
{#snippet content()}
Description here!
{/snippet}
</Accordion>
<Accordion name={props.name!} style={props.style}>
{#snippet title()}
Option 3
{/snippet}
{#snippet content()}
Description here!
{/snippet}
</Accordion>
</div>
{/snippet}
<Story name="Default" children={template} args={{ name: 'accordion-story' }} />

View file

@ -0,0 +1,35 @@
<script lang="ts" module>
export type AccordionStyle = 'arrow' | 'plus';
</script>
<script lang="ts">
import type { Snippet } from 'svelte';
type Props = {
close?: boolean;
content?: Snippet;
initialOpen?: boolean;
name: string;
open?: boolean;
style?: AccordionStyle;
title?: Snippet;
};
let { close, content, initialOpen, name, open, style, title }: Props = $props();
</script>
<div
class="bg-base-100 border-base-300 collapse border"
class:collapse-arrow={style === 'arrow'}
class:collapse-close={close}
class:collapse-open={open}
class:collapse-plus={style === 'plus'}
>
<input type="radio" {name} checked={initialOpen} />
{#if title}
<div class="collapse-title font-semibold">{@render title()}</div>
{/if}
{#if content}
<div class="collapse-content text-sm">{@render content()}</div>
{/if}
</div>

View file

@ -0,0 +1,92 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import { type ComponentProps } from 'svelte';
import Avatar from './Avatar.svelte';
import AvatarGroups from './AvatarGroup.svelte';
const { Story } = defineMeta({
title: 'Data display/Avatar',
component: Avatar,
argTypes: {
img: {
control: 'text',
},
placeholder: {
control: 'text',
defaultValue: 'DP',
},
presence: {
control: 'radio',
options: [undefined, 'online', 'offline'],
},
ring: {
control: 'radio',
options: [
undefined,
'neutral',
'primary',
'secondary',
'accent',
'info',
'success',
'warning',
'error',
],
},
shape: {
control: 'radio',
options: ['square', 'circle'],
defaultValue: 'circle',
},
size: {
control: 'radio',
options: ['xs', 'sm', 'md', 'lg', 'xl'],
},
},
});
</script>
{#snippet template(props: Partial<ComponentProps<typeof Avatar>>)}
<Avatar {...props} placeholder={props.placeholder || 'PC'} />
{/snippet}
<Story
name="Default"
args={{
img: 'https://img.daisyui.com/images/stock/photo-1534528741775-53994a69daeb.webp',
}}
children={template}
/>
<Story name="Placeholder" args={{ placeholder: 'BP' }} children={template} />
<Story name="Sizes">
{@render template({
size: 'xs',
placeholder: 'XS',
})}
{@render template({
size: 'sm',
placeholder: 'SM',
})}
{@render template({
size: 'md',
placeholder: 'MD',
})}
{@render template({
size: 'lg',
placeholder: 'LG',
})}
{@render template({
size: 'xl',
placeholder: 'XL',
})}
</Story>
<Story name="Avatar Group">
<AvatarGroups>
{@render template({ placeholder: 'BP' })}
{@render template({ placeholder: 'MS' })}
{@render template({ placeholder: 'DM' })}
</AvatarGroups>
</Story>

View file

@ -0,0 +1,75 @@
<script lang="ts" module>
export type AvatarShape = 'circle' | 'square';
export type AvatarPresence = 'offline' | 'online';
</script>
<script lang="ts">
import type { DaisyColor, DaisySize } from '$lib/types';
import clsx from 'clsx';
import type { SvelteHTMLElements } from 'svelte/elements';
import { twMerge } from 'tailwind-merge';
type Props = {
img?: string;
placeholder: string;
presence?: AvatarPresence;
ring?: DaisyColor;
shape?: AvatarShape;
size?: DaisySize;
} & Omit<SvelteHTMLElements['div'], 'children'>;
let {
class: className,
img,
placeholder,
presence,
ring,
shape = 'circle',
size = 'md',
...props
}: Props = $props();
</script>
<div
{...props}
class={twMerge('avatar', clsx(className))}
class:avatar-online={presence === 'online'}
class:avatar-offline={presence === 'offline'}
class:avatar-placeholder={placeholder}
>
<div
class={twMerge(
'rounded-xl',
clsx({
'rounded-xl': shape === 'square',
'rounded-full': shape === 'circle',
'w-12': size === 'xs',
'w-16': size === 'sm',
'w-20': size === 'md',
'w-24': size === 'lg',
'w-32': size === 'xl',
'avatar-ring ring-offset-base-100 ring ring-offset-2': !!ring,
})
)}
class:bg-neutral={placeholder}
class:ring-neutral={ring === 'neutral'}
class:ring-primary={ring === 'primary'}
class:ring-secondary={ring === 'secondary'}
class:ring-accent={ring === 'accent'}
class:ring-info={ring === 'info'}
class:ring-success={ring === 'success'}
class:ring-warning={ring === 'warning'}
class:ring-error={ring === 'error'}
>
{#if img}
<img src={img} alt={placeholder} />
{:else}
<span
class:text-5xl={size === 'xl'}
class:text-3xl={size === 'lg'}
class:text-2xl={size === 'md'}
class:text-xs={size === 'xs'}>{placeholder}</span
>
{/if}
</div>
</div>

View file

@ -0,0 +1,13 @@
<script lang="ts">
import type { Snippet } from 'svelte';
type Props = {
children?: Snippet;
};
let { children }: Props = $props();
</script>
<div class="avatar-group -space-x-6">
{@render children?.()}
</div>

View file

@ -0,0 +1,96 @@
<script lang="ts" module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import { type ComponentProps } from 'svelte';
import Badge from './Badge.svelte';
const { Story } = defineMeta({
title: 'Data Display/Badge',
component: Badge,
argTypes: {
color: {
control: 'radio',
options: [
undefined,
'primary',
'secondary',
'accent',
'neutral',
'info',
'success',
'warning',
'error',
],
},
size: {
control: 'radio',
options: ['xs', 'sm', 'md', 'lg', 'xl'],
},
style: {
control: 'radio',
options: [undefined, 'outline', 'dash', 'soft', 'ghost'],
},
},
});
</script>
{#snippet template(props: Partial<ComponentProps<typeof Badge>>)}
<Badge {...props}>Badge</Badge>
{/snippet}
<Story name="Default" children={template} />
<Story name="Sizes">
<Badge size="xs">x-small</Badge>
<Badge size="sm">small</Badge>
<Badge size="md">medium</Badge>
<Badge size="lg">large</Badge>
<Badge size="xl">x-large</Badge>
</Story>
<Story name="Colors">
<Badge color="primary">Primary</Badge>
<Badge color="secondary">Secondary</Badge>
<Badge color="accent">Accent</Badge>
<Badge color="neutral">Neutral</Badge>
<Badge color="info">Info</Badge>
<Badge color="success">Success</Badge>
<Badge color="warning">Warning</Badge>
<Badge color="error">Error</Badge>
</Story>
<Story name="Outline">
<Badge style="outline" color="primary">Primary</Badge>
<Badge style="outline" color="secondary">Secondary</Badge>
<Badge style="outline" color="accent">Accent</Badge>
<Badge style="outline" color="neutral">Neutral</Badge>
<Badge style="outline" color="info">Info</Badge>
<Badge style="outline" color="success">Success</Badge>
<Badge style="outline" color="warning">Warning</Badge>
<Badge style="outline" color="error">Error</Badge>
</Story>
<Story name="Dash">
<Badge style="dash" color="primary">Primary</Badge>
<Badge style="dash" color="secondary">Secondary</Badge>
<Badge style="dash" color="accent">Accent</Badge>
<Badge style="dash" color="neutral">Neutral</Badge>
<Badge style="dash" color="info">Info</Badge>
<Badge style="dash" color="success">Success</Badge>
<Badge style="dash" color="warning">Warning</Badge>
<Badge style="dash" color="error">Error</Badge>
</Story>
<Story name="Soft">
<Badge style="soft" color="primary">Primary</Badge>
<Badge style="soft" color="secondary">Secondary</Badge>
<Badge style="soft" color="accent">Accent</Badge>
<Badge style="soft" color="neutral">Neutral</Badge>
<Badge style="soft" color="info">Info</Badge>
<Badge style="soft" color="success">Success</Badge>
<Badge style="soft" color="warning">Warning</Badge>
<Badge style="soft" color="error">Error</Badge>
</Story>
<Story name="Ghost">
<Badge style="ghost">Ghost</Badge>
</Story>

View file

@ -0,0 +1,40 @@
<script lang="ts" module>
export type BadgeStyle = 'outline' | 'dash' | 'soft' | 'ghost';
</script>
<script lang="ts">
import type { DaisyColor, DaisySize } from '$lib/types';
import type { Snippet } from 'svelte';
type Props = {
children: Snippet;
color?: DaisyColor;
size?: DaisySize;
style?: BadgeStyle;
};
let { children, color, size, style }: Props = $props();
</script>
<span
class="badge"
class:badge-primary={color === 'primary'}
class:badge-secondary={color === 'secondary'}
class:badge-accent={color === 'accent'}
class:badge-neutral={color === 'neutral'}
class:badge-info={color === 'info'}
class:badge-success={color === 'success'}
class:badge-warning={color === 'warning'}
class:badge-error={color === 'error'}
class:badge-xs={size === 'xs'}
class:badge-sm={size === 'sm'}
class:badge-md={size === 'md'}
class:badge-lg={size === 'lg'}
class:badge-xl={size === 'xl'}
class:badge-outline={style === 'outline'}
class:badge-dash={style === 'dash'}
class:badge-soft={style === 'soft'}
class:badge-ghost={style === 'ghost'}
>
{@render children?.()}
</span>

View file

@ -0,0 +1,4 @@
export { default as Accordion } from './Accordion.svelte';
export { default as Avatar } from './Avatar.svelte';
export { default as AvatarGroup } from './AvatarGroup.svelte';
export { default as Badge } from './Badge.svelte';

View file

@ -0,0 +1,80 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import { User } from 'lucide-svelte';
import InputField from './InputField.svelte';
const { Story } = defineMeta({
title: 'Data Input/Input Field',
component: InputField,
argTypes: {
block: {
control: 'boolean',
},
color: {
control: 'select',
options: ['primary', 'secondary', 'accent', 'info', 'success', 'warning', 'error'],
},
disabled: {
control: 'boolean',
},
start: { control: 'text' },
end: { control: 'text' },
placeholder: { control: 'text' },
size: {
control: 'select',
options: ['xs', 'sm', 'md', 'lg', 'xl'],
},
style: {
control: 'radio',
options: [undefined, 'ghost'],
},
type: {
control: 'select',
options: ['email', 'password', 'search', 'tel', 'text', 'url'],
},
},
});
</script>
<Story name="Default" args={{ name: 'text' }} />
<Story name="Labels">
<div class="flex flex-col gap-2">
<InputField name="text" start="Label" />
<InputField name="text">
{#snippet start()}
<User />
{/snippet}
</InputField>
<InputField name="text">
{#snippet end()}
<User />
{/snippet}
</InputField>
</div>
</Story>
<Story name="Colors">
<div class="flex flex-col gap-2">
<InputField name="text" color="primary" placeholder="Primary" />
<InputField name="text" color="secondary" placeholder="Secondary" />
<InputField name="text" color="accent" placeholder="Accent" />
<InputField name="text" color="neutral" placeholder="Neutral" />
<InputField name="text" color="info" placeholder="Info" />
<InputField name="text" color="success" placeholder="Success" />
<InputField name="text" color="warning" placeholder="Warning" />
<InputField name="text" color="error" placeholder="Error" />
</div>
</Story>
<Story name="Sizes">
<div class="flex flex-col gap-2">
<InputField name="text" size="xs" placeholder="XS" />
<InputField name="text" size="sm" placeholder="SM" />
<InputField name="text" size="md" placeholder="MD" />
<InputField name="text" size="lg" placeholder="LG" />
<InputField name="text" size="xl" placeholder="XL" />
</div>
</Story>
<Story name="Disabled" args={{ name: 'text', placeholder: 'Disabled', disabled: true }} />

View file

@ -0,0 +1,69 @@
<script lang="ts" module>
export type InputFieldStyle = 'ghost';
</script>
<script lang="ts">
import type { DaisyColor, DaisySize } from '$lib/types';
import clsx from 'clsx';
import type { Snippet } from 'svelte';
import type { HTMLInputTypeAttribute, SvelteHTMLElements } from 'svelte/elements';
import { twMerge } from 'tailwind-merge';
type Props = {
block?: boolean;
color?: DaisyColor;
error?: string | Snippet;
fade?: boolean;
start?: string | Snippet;
end?: string | Snippet;
label?: string | Snippet;
size?: DaisySize;
style?: InputFieldStyle;
type?: Extract<
HTMLInputTypeAttribute,
'email' | 'password' | 'search' | 'tel' | 'text' | 'url'
>;
} & Omit<SvelteHTMLElements['input'], 'type' | 'size'>;
let {
block,
class: className,
color,
start,
end,
size = 'md',
style,
...props
}: Props = $props();
</script>
<div
class="input flex items-center gap-2 transition-[width] delay-150 duration-300 ease-in-out"
class:w-full={block}
class:input-primary={color === 'primary'}
class:input-secondary={color === 'secondary'}
class:input-accent={color === 'accent'}
class:input-neutral={color === 'neutral'}
class:input-info={color === 'info'}
class:input-success={color === 'success'}
class:input-warning={color === 'warning'}
class:input-error={color === 'error'}
class:input-ghost={style === 'ghost'}
class:input-xs={size === 'xs'}
class:input-sm={size === 'sm'}
class:input-md={size === 'md'}
class:input-lg={size === 'lg'}
class:input-xl={size === 'xl'}
>
{#if typeof start === 'string'}
{start}
{:else}
{@render start?.()}
{/if}
<input {...props} class={twMerge('grow', clsx(className))} />
{#if typeof end === 'string'}
{end}
{:else}
{@render end?.()}
{/if}
</div>

View file

@ -0,0 +1,51 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import Textarea from './Textarea.svelte';
import type { ComponentProps } from 'svelte';
const { Story } = defineMeta({
title: 'Data Input/Textarea',
component: Textarea,
argTypes: {
bordered: {
control: 'boolean',
},
color: {
control: 'select',
options: [
'default',
'ghost',
'primary',
'secondary',
'accent',
'info',
'success',
'warning',
'error',
],
},
disabled: {
control: 'boolean',
},
error: {
control: 'text',
},
label: {
control: 'text',
},
placeholder: {
control: 'text',
},
size: {
control: 'select',
options: ['xs', 'sm', 'md', 'lg', 'xl'],
},
},
});
</script>
{#snippet template(props: ComponentProps<typeof Textarea>)}
<Textarea {...props} />
{/snippet}
<Story name="Default" args={{ label: 'Label' }} children={template} />

View file

@ -0,0 +1,76 @@
<script lang="ts">
import type { DaisyColor, DaisySize } from '$lib/types';
import clsx from 'clsx';
import type { Snippet } from 'svelte';
import type { SvelteHTMLElements } from 'svelte/elements';
import { twMerge } from 'tailwind-merge';
type Props = {
bordered?: boolean;
color?: Omit<DaisyColor, 'neutral'>;
error?: string | Snippet;
label?: string | Snippet;
resizable?: boolean | 'yes' | 'no' | 'x' | 'y';
size?: DaisySize;
} & SvelteHTMLElements['textarea'];
let {
bordered,
class: className,
color,
error,
label,
resizable,
size,
...props
}: Props = $props();
</script>
<label class="form-control w-full">
<div class="label">
<span
class="label-text flex items-center gap-2"
class:text-primary={color === 'primary'}
class:text-secondary={color === 'secondary'}
class:text-accent={color === 'accent'}
class:text-info={color === 'info'}
class:text-success={color === 'success'}
class:text-warning={color === 'warning'}
class:text-error={color === 'error' || error}
>
{#if typeof label === 'string'}
{label}
{:else if label}
{@render label()}
{/if}
</span>
<span class="label-text-alt text-error font-semibold">
{#if typeof error === 'string'}
{error}
{:else if error}
{@render error()}
{/if}
</span>
</div>
<textarea
{...props}
class={twMerge('textarea', clsx(className))}
class:textarea-bordered={bordered}
class:textarea-xs={size === 'xs'}
class:textarea-sm={size === 'sm'}
class:textarea-md={size === 'md'}
class:textarea-lg={size === 'lg'}
class:textarea-xl={size === 'xl'}
class:textarea-ghost={color === 'ghost'}
class:textarea-primary={color === 'primary'}
class:textarea-secondary={color === 'secondary'}
class:textarea-accent={color === 'accent'}
class:textarea-info={color === 'info'}
class:textarea-success={color === 'success'}
class:textarea-warning={color === 'warning'}
class:textarea-error={color === 'error' || error}
class:resize={resizable === true || resizable === 'yes'}
class:resize-x={resizable === 'x'}
class:resize-y={resizable === 'y'}
class:resize-none={resizable === false || resizable === 'no'}
></textarea>
</label>

View file

@ -0,0 +1,2 @@
export { default as Textarea } from './Textarea.svelte';
export { default as TextInput } from './TextInput.svelte';

View file

@ -0,0 +1,25 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import { Info } from 'lucide-svelte';
import type { ComponentProps } from 'svelte';
import Alert from './Alert.svelte';
const { Story } = defineMeta({
title: 'Feedback/Alert',
component: Alert,
argTypes: {
status: { control: 'select', options: ['info', 'success', 'warning', 'error'] },
},
});
</script>
{#snippet template(props: ComponentProps<typeof Alert>)}
<Alert {...props}>
{#snippet icon()}
<Info />
{/snippet}
<span>Hello world!</span>
</Alert>
{/snippet}
<Story name="Default" args={{}} children={template} />

View file

@ -0,0 +1,28 @@
<script lang="ts">
import type { DaisyColor } from '$lib/types';
import clsx from 'clsx';
import type { Snippet } from 'svelte';
import type { SvelteHTMLElements } from 'svelte/elements';
import { twMerge } from 'tailwind-merge';
type Props = {
children?: Snippet;
icon?: Snippet;
status?: Extract<DaisyColor, 'info' | 'success' | 'warning' | 'error'>;
} & SvelteHTMLElements['div'];
let { children, class: className, icon, status: color, ...props }: Props = $props();
</script>
<div
{...props}
role="alert"
class={twMerge('alert', clsx(className))}
class:alert-info={color === 'info'}
class:alert-success={color === 'success'}
class:alert-warning={color === 'warning'}
class:alert-error={color === 'error'}
>
{@render icon?.()}
{@render children?.()}
</div>

View file

@ -0,0 +1,11 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import Loader from './Loader.svelte';
const { Story } = defineMeta({
title: 'Feedback/Loader',
component: Loader,
});
</script>
<Story name="Default" args={{}} />

View file

@ -0,0 +1,51 @@
<script lang="ts"></script>
<span class="loader"></span>
<style>
.loader {
width: 48px;
height: 48px;
border: 3px dotted #fff;
border-style: solid solid dotted dotted;
border-radius: 50%;
display: inline-block;
position: relative;
box-sizing: border-box;
animation: rotation 2s linear infinite;
}
.loader::after {
content: '';
box-sizing: border-box;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;
border: 3px dotted #ff3d00;
border-style: solid solid dotted;
width: 24px;
height: 24px;
border-radius: 50%;
animation: rotationBack 1s linear infinite;
transform-origin: center center;
}
@keyframes rotation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes rotationBack {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(-360deg);
}
}
</style>

View file

@ -0,0 +1,40 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import type { ComponentProps } from 'svelte';
import Loading from './Loading.svelte';
const { Story } = defineMeta({
title: 'Feedback/Loading',
component: Loading,
argTypes: {
color: {
control: 'select',
options: [
'neutral',
'primary',
'secondary',
'accent',
'info',
'success',
'warning',
'error',
],
},
size: { control: 'select', options: ['xs', 'sm', 'md', 'lg', 'xl'] },
variant: {
control: 'select',
options: ['spinner', 'dots', 'ring', 'ball', 'bars', 'infinity'],
},
},
});
</script>
{#snippet template(props: ComponentProps<typeof Loading>)}
<Loading {...props} />
{/snippet}
<Story
name="Default"
args={{ color: 'neutral', size: 'md', variant: 'spinner' }}
children={template}
/>

View file

@ -0,0 +1,35 @@
<script lang="ts">
import type { DaisyColor, DaisySize } from '$lib/types';
import clsx from 'clsx';
import type { SvelteHTMLElements } from 'svelte/elements';
import { twMerge } from 'tailwind-merge';
type Props = {
color?: Exclude<DaisyColor, 'ghost'>;
size?: DaisySize;
variant?: 'spinner' | 'dots' | 'ring' | 'ball' | 'bars' | 'infinity';
} & Pick<SvelteHTMLElements['span'], 'class'>;
let { class: className, color, size = 'md', variant = 'spinner' }: Props = $props();
</script>
<span
class={twMerge('loading', clsx(className))}
class:text-primary={color === 'primary'}
class:text-secondary={color === 'secondary'}
class:text-accent={color === 'accent'}
class:text-info={color === 'info'}
class:text-success={color === 'success'}
class:text-warning={color === 'warning'}
class:text-error={color === 'error'}
class:loading-xs={size === 'xs'}
class:loading-sm={size === 'sm'}
class:loading-md={size === 'md'}
class:loading-lg={size === 'lg'}
class:loading-xl={size === 'xl'}
class:loading-spinner={variant === 'spinner'}
class:loading-dots={variant === 'dots'}
class:loading-ring={variant === 'ring'}
class:loading-ball={variant === 'ball'}
class:loading-bars={variant === 'bars'}
class:loading-infinity={variant === 'infinity'}
></span>

View file

@ -0,0 +1,24 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import type { ComponentProps } from 'svelte';
import Progress from './Progress.svelte';
const { Story } = defineMeta({
title: 'Feedback/Progress',
component: Progress,
argTypes: {
color: {
control: 'select',
options: ['primary', 'secondary', 'accent', 'info', 'success', 'warning', 'error'],
},
value: { control: 'number' },
max: { control: 'number' },
},
});
</script>
{#snippet template(props: ComponentProps<typeof Progress>)}
<Progress {...props} />
{/snippet}
<Story name="Default" args={{}} children={template} />

View file

@ -0,0 +1,25 @@
<script lang="ts">
import clsx from 'clsx';
import type { DaisyColor } from '$lib/types';
import type { SvelteHTMLElements } from 'svelte/elements';
import { twMerge } from 'tailwind-merge';
type Props = {
color?: Exclude<DaisyColor, 'neutral' | 'ghost'>;
} & SvelteHTMLElements['progress'];
let { children, class: className, color, ...props }: Props = $props();
</script>
<progress
{...props}
class={twMerge('progress', clsx(className))}
class:progress-primary={color === 'primary'}
class:progress-secondary={color === 'secondary'}
class:progress-accent={color === 'accent'}
class:progress-info={color === 'info'}
class:progress-success={color === 'success'}
class:progress-warning={color === 'warning'}
class:progress-error={color === 'error'}
>
{@render children?.()}
</progress>

View file

@ -0,0 +1,18 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import Skeleton from './Skeleton.svelte';
import type { ComponentProps } from 'svelte';
const { Story } = defineMeta({
title: 'Feedback/Skeleton',
component: Skeleton,
});
</script>
{#snippet template(props: ComponentProps<typeof Skeleton>)}
<Skeleton {...props} />
{/snippet}
<Story name="Default" args={{ class: 'h-32 w-32' }} children={template} />
<Story name="Circle" args={{ class: 'h-32 w-32 rounded-full' }} children={template} />

View file

@ -0,0 +1,10 @@
<script lang="ts">
import clsx from 'clsx';
import type { SvelteHTMLElements } from 'svelte/elements';
import { twMerge } from 'tailwind-merge';
type Props = SvelteHTMLElements['div'];
let { children, class: className, ...props }: Props = $props();
</script>
<div {...props} class={twMerge('skeleton', clsx(className))}>{@render children?.()}</div>

View file

@ -0,0 +1,27 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import Tooltip from './Tooltip.svelte';
import { Button } from '../Actions';
import type { ComponentProps } from 'svelte';
const { Story } = defineMeta({
title: 'Feedback/Tooltip',
component: Tooltip,
argTypes: {
color: {
control: 'select',
options: ['primary', 'secondary', 'accent', 'info', 'success', 'warning', 'error'],
},
open: { control: 'boolean' },
position: { control: 'select', options: ['top', 'bottom', 'left', 'right'] },
},
});
</script>
{#snippet template(props: ComponentProps<typeof Tooltip>)}
<Tooltip {...props}>
<Button color="primary">Button</Button>
</Tooltip>
{/snippet}
<Story name="Default" args={{ tip: "It's a button" }} children={template} />

View file

@ -0,0 +1,34 @@
<script lang="ts">
import type { DaisyColor } from '$lib/types';
import clsx from 'clsx';
import type { SvelteHTMLElements } from 'svelte/elements';
import { twMerge } from 'tailwind-merge';
type Props = {
color?: Exclude<DaisyColor, 'neutral' | 'ghost'>;
open?: boolean;
position?: 'top' | 'bottom' | 'left' | 'right';
tip?: string;
} & SvelteHTMLElements['div'];
let { children, class: className, color, open, position, tip, ...props }: Props = $props();
</script>
<div
{...props}
class={twMerge('tooltip', clsx(className))}
class:tooltip-primary={color === 'primary'}
class:tooltip-secondary={color === 'secondary'}
class:tooltip-accent={color === 'accent'}
class:tooltip-info={color === 'info'}
class:tooltip-success={color === 'success'}
class:tooltip-warning={color === 'warning'}
class:tooltip-error={color === 'error'}
class:tooltip-open={open}
class:tooltip-top={position === 'top'}
class:tooltip-bottom={position === 'bottom'}
class:tooltip-left={position === 'left'}
class:tooltip-right={position === 'right'}
data-tip={tip}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,6 @@
export { default as Alert } from './Alert.svelte';
export { default as Loader } from './Loader.svelte';
export { default as Loading } from './Loading.svelte';
export { default as Progress } from './Progress.svelte';
export { default as Skeleton } from './Skeleton.svelte';
export { default as Tooltip } from './Tooltip.svelte';

View file

@ -0,0 +1,41 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import Divider from './Divider.svelte';
import type { ComponentProps } from 'svelte';
const { Story } = defineMeta({
title: 'Layout/Divider',
component: Divider,
argTypes: {
color: {
control: 'select',
options: [
'neutral',
'primary',
'secondary',
'accent',
'info',
'success',
'warning',
'error',
],
},
direction: { control: 'select', options: ['horizontal', 'vertical'] },
variant: { control: 'select', options: ['start', 'end'] },
},
});
</script>
{#snippet template(props: ComponentProps<typeof Divider>)}
<div
class="flex"
class:flex-row={props.direction === 'horizontal'}
class:flex-col={props.direction === 'vertical'}
>
<span>Side A</span>
<Divider {...props} />
<span>Side B</span>
</div>
{/snippet}
<Story name="Default" args={{}} children={template} />

View file

@ -0,0 +1,32 @@
<script lang="ts">
import type { DaisyColor } from '$lib/types';
import clsx from 'clsx';
import type { SvelteHTMLElements } from 'svelte/elements';
import { twMerge } from 'tailwind-merge';
type Props = {
color?: Exclude<DaisyColor, 'ghost'>;
direction?: 'horizontal' | 'vertical';
variant?: 'start' | 'end';
} & SvelteHTMLElements['div'];
let { children, class: className, color, direction, variant, ...props }: Props = $props();
</script>
<div
{...props}
class={twMerge('divider', clsx(className))}
class:divider-neutral={color === 'neutral'}
class:divider-primary={color === 'primary'}
class:divider-secondary={color === 'secondary'}
class:divider-accent={color === 'accent'}
class:divider-info={color === 'info'}
class:divider-success={color === 'success'}
class:divider-warning={color === 'warning'}
class:divider-error={color === 'error'}
class:divider-horizontal={direction === 'horizontal'}
class:divider-vertical={direction === 'vertical'}
class:divider-start={variant === 'start'}
class:divider-end={variant === 'end'}
>
{@render children?.()}
</div>

View file

@ -0,0 +1 @@
export { default as Divider } from './Divider.svelte';

View file

@ -0,0 +1,31 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import type { ComponentProps } from 'svelte';
import Link from './Link.svelte';
const { Story } = defineMeta({
title: 'Navigation/Link',
component: Link,
argTypes: {
color: {
control: 'select',
options: [
'primary',
'secondary',
'accent',
'neutral',
'info',
'success',
'warning',
'error',
],
},
},
});
</script>
{#snippet template(props: ComponentProps<typeof Link>)}
<Link {...props}>Hello world!</Link>
{/snippet}
<Story name="Default" children={template} />

View file

@ -0,0 +1,24 @@
<script lang="ts">
import type { DaisyColor } from '$lib/types';
import clsx from 'clsx';
import type { SvelteHTMLElements } from 'svelte/elements';
import { twMerge } from 'tailwind-merge';
type Props = { color?: DaisyColor; hover?: boolean } & SvelteHTMLElements['a'];
let { children, class: className, color, hover, ...props }: Props = $props();
</script>
<a
{...props}
class={twMerge('link', clsx(className))}
class:link-primary={color === 'primary'}
class:link-secondary={color === 'secondary'}
class:link-accent={color === 'accent'}
class:link-neutral={color === 'neutral'}
class:link-info={color === 'info'}
class:link-success={color === 'success'}
class:link-warning={color === 'warning'}
class:link-error={color === 'error'}
class:link-hover={hover}>{@render children?.()}</a
>

View file

@ -0,0 +1,27 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import Navbar from './Navbar.svelte';
const { Story } = defineMeta({
title: 'Navigation/Navbar',
component: Navbar,
});
</script>
<Story name="Default">
<Navbar>
{#snippet start()}
<button class="btn">Home</button>
{/snippet}
{#snippet center()}
<h2 class="text-xl">Title</h2>
{/snippet}
{#snippet end()}
<div class="avatar placeholder">
<div class="bg-neutral text-neutral-content w-8 rounded-full">
<span class="text-xs">UI</span>
</div>
</div>
{/snippet}
</Navbar>
</Story>

View file

@ -0,0 +1,11 @@
<script lang="ts">
import type { Snippet } from 'svelte';
let { start, center, end }: { start?: Snippet; center?: Snippet; end?: Snippet } = $props();
</script>
<header class="navbar rounded-box bg-base-200 justify-between px-4">
<div class="navbar-start">{@render start?.()}</div>
<div class="navbar-center">{@render center?.()}</div>
<div class="navbar-end">{@render end?.()}</div>
</header>

View file

@ -0,0 +1,3 @@
import Navbar from './Navbar.svelte';
export default Navbar;

View file

@ -0,0 +1,5 @@
<script lang="ts">
let { active, label, onclick } = $props();
</script>
<input aria-label={label} type="radio" role="tab" class="tab" class:tab-active={active} {onclick} />

View file

@ -0,0 +1,27 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import type { ComponentProps } from 'svelte';
import Tabs from './Tabs.svelte';
const { Story } = defineMeta({
title: 'Navigation/Tabs',
component: Tabs,
tags: ['autodocs'],
argTypes: {
size: {
control: 'select',
options: ['xs', 'sm', 'md', 'lg', 'xl'],
},
variant: {
control: 'select',
options: ['none', 'bordered', 'lifted', 'boxed'],
},
},
});
</script>
{#snippet template(args: Partial<ComponentProps<typeof Tabs>>)}
<Tabs tabs={['Tab 1', 'Tab 2']} {...args} />
{/snippet}
<Story name="Default" args={{}} children={template} />

View file

@ -0,0 +1,37 @@
<script lang="ts">
import type { DaisySize } from '$lib/types';
import Tab from './Tab.svelte';
type Props = {
size?: DaisySize;
tabs: string[];
selected?: number;
variant?: 'none' | 'bordered' | 'lifted' | 'boxed';
};
let { size, tabs, selected: value = $bindable(0), variant = 'none' }: Props = $props();
</script>
<div
role="tablist"
class="tabs w-full"
class:tabs-xs={size === 'xs'}
class:tabs-sm={size === 'sm'}
class:tabs-lg={size === 'lg'}
class:tabs-xl={size === 'xl'}
class:tabs-bordered={variant === 'bordered'}
class:tabs-lifted={variant === 'lifted'}
class:tabs-boxed={variant === 'boxed'}
>
{#each tabs as tab, index}
{#key [tab, value]}
<Tab
active={index === value}
label={tab}
onclick={() => {
value = index;
}}
/>
{/key}
{/each}
</div>

View file

@ -0,0 +1,4 @@
import Tabs from './Tabs.svelte';
export default Tabs;
export { default as Tabs } from './Tabs.svelte';

View file

@ -0,0 +1,5 @@
import Navbar from './Navbar';
import Tabs from './Tabs';
export { default as Link } from './Link.svelte';
export { Navbar, Tabs };

View file

@ -0,0 +1,6 @@
export * from './Actions/';
export * from './DataDisplay';
export * from './DataInput/';
export * from './Feedback/';
export * from './Layout/';
export * from './Navigation/';

1
src/lib/common/index.ts Normal file
View file

@ -0,0 +1 @@
export * from './components/';

View file

@ -0,0 +1,9 @@
export type DaisyColor =
| 'neutral'
| 'primary'
| 'secondary'
| 'accent'
| 'info'
| 'success'
| 'warning'
| 'error';

View file

@ -0,0 +1 @@
export type DaisySize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';

View file

@ -0,0 +1,2 @@
export * from './daisy-colors';
export * from './daisy-sizes';

1
src/lib/index.ts Normal file
View file

@ -0,0 +1 @@
export {};

12
svelte.config.js Normal file
View file

@ -0,0 +1,12 @@
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {},
};
export default config;

5
tailwind.config.ts Normal file
View file

@ -0,0 +1,5 @@
import type { Config } from 'tailwindcss';
export default {
content: ['./src/**/*.{html,js,svelte,ts}'],
} satisfies Config;

19
tsconfig.json Normal file
View file

@ -0,0 +1,19 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

10
vite.config.ts Normal file
View file

@ -0,0 +1,10 @@
import { defineConfig } from 'vitest/config';
import { sveltekit } from '@sveltejs/kit/vite';
export default defineConfig({
plugins: [sveltekit()],
test: {
include: ['src/**/*.{test,spec}.{js,ts}'],
},
});