@noahbaumgartner/svelte-ui 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -38
- package/dist/components/accordion/Accordion.svelte +34 -0
- package/dist/components/accordion/Accordion.svelte.d.ts +11 -0
- package/dist/components/accordion/AccordionItem.svelte +106 -0
- package/dist/components/accordion/AccordionItem.svelte.d.ts +13 -0
- package/dist/components/accordion/context.d.ts +5 -0
- package/dist/components/accordion/context.js +2 -0
- package/dist/components/avatar-group/AvatarGroup.svelte +76 -0
- package/dist/components/avatar-group/AvatarGroup.svelte.d.ts +15 -0
- package/dist/components/drawer/Drawer.svelte +368 -0
- package/dist/components/drawer/Drawer.svelte.d.ts +23 -0
- package/dist/components/empty-state/EmptyState.svelte +86 -0
- package/dist/components/empty-state/EmptyState.svelte.d.ts +16 -0
- package/dist/components/entry/Entry.svelte +83 -0
- package/dist/components/entry/Entry.svelte.d.ts +15 -0
- package/dist/components/footer/Footer.svelte +89 -0
- package/dist/components/footer/Footer.svelte.d.ts +13 -0
- package/dist/components/footer/FooterColumn.svelte +34 -0
- package/dist/components/footer/FooterColumn.svelte.d.ts +10 -0
- package/dist/components/mesh-gradient/MeshGradient.svelte +86 -0
- package/dist/components/mesh-gradient/MeshGradient.svelte.d.ts +12 -0
- package/dist/components/number-stepper/NumberStepper.svelte +82 -0
- package/dist/components/number-stepper/NumberStepper.svelte.d.ts +16 -0
- package/dist/components/preview-card/PreviewCard.svelte +166 -0
- package/dist/components/preview-card/PreviewCard.svelte.d.ts +20 -0
- package/dist/components/progress/Progress.svelte +88 -0
- package/dist/components/progress/Progress.svelte.d.ts +12 -0
- package/dist/components/select/Select.svelte +391 -0
- package/dist/components/select/Select.svelte.d.ts +39 -0
- package/dist/components/sidebar/Sidebar.svelte +37 -0
- package/dist/components/sidebar/SidebarHeader.svelte +26 -2
- package/dist/components/tabs/Tabs.svelte +280 -0
- package/dist/components/tabs/Tabs.svelte.d.ts +48 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +14 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,65 +1,96 @@
|
|
|
1
|
-
#
|
|
1
|
+
# svelte-ui
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Read more about creating a library [in the docs](https://svelte.dev/docs/kit/packaging).
|
|
6
|
-
|
|
7
|
-
## Creating a project
|
|
8
|
-
|
|
9
|
-
If you're seeing this, you've probably already done this step. Congrats!
|
|
3
|
+
A minimal, monochrome component library for Svelte 5, built with runes, TypeScript and plain CSS custom properties.
|
|
10
4
|
|
|
11
5
|
```sh
|
|
12
|
-
|
|
13
|
-
npx sv create
|
|
14
|
-
|
|
15
|
-
# create a new project in my-app
|
|
16
|
-
npx sv create my-app
|
|
6
|
+
npm install @noahbaumgartner/svelte-ui
|
|
17
7
|
```
|
|
18
8
|
|
|
19
|
-
|
|
9
|
+
## Usage
|
|
20
10
|
|
|
21
|
-
|
|
22
|
-
# recreate this project
|
|
23
|
-
npx sv@0.17.1 create --template library --types ts --add storybook vitest="usages:unit,component" eslint prettier --install npm svelte-ui
|
|
24
|
-
```
|
|
11
|
+
Import the design tokens once, at the root of your app (e.g. `src/routes/+layout.svelte`):
|
|
25
12
|
|
|
26
|
-
|
|
13
|
+
```svelte
|
|
14
|
+
<script lang="ts">
|
|
15
|
+
import '@noahbaumgartner/svelte-ui/tokens.css';
|
|
27
16
|
|
|
28
|
-
|
|
17
|
+
let { children } = $props();
|
|
18
|
+
</script>
|
|
29
19
|
|
|
30
|
-
|
|
31
|
-
|
|
20
|
+
{@render children()}
|
|
21
|
+
```
|
|
32
22
|
|
|
33
|
-
|
|
34
|
-
|
|
23
|
+
Then use the components:
|
|
24
|
+
|
|
25
|
+
```svelte
|
|
26
|
+
<script lang="ts">
|
|
27
|
+
import { Button, Card, CardHeader, CardTitle, CardContent } from '@noahbaumgartner/svelte-ui';
|
|
28
|
+
import { Plus } from '@noahbaumgartner/svelte-ui/icons';
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<Card>
|
|
32
|
+
<CardHeader>
|
|
33
|
+
<CardTitle>Projects</CardTitle>
|
|
34
|
+
</CardHeader>
|
|
35
|
+
<CardContent>
|
|
36
|
+
<Button icon={Plus} variant="primary">New project</Button>
|
|
37
|
+
</CardContent>
|
|
38
|
+
</Card>
|
|
35
39
|
```
|
|
36
40
|
|
|
37
|
-
|
|
41
|
+
### Icons
|
|
38
42
|
|
|
39
|
-
|
|
43
|
+
[Lucide](https://lucide.dev) icons are re-exported from `@noahbaumgartner/svelte-ui/icons`, so you don't need a separate `@lucide/svelte` install. They live on their own entry point because some icon names (`Link`, `Image`, …) collide with component names.
|
|
40
44
|
|
|
41
|
-
|
|
45
|
+
### Theming
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
All colours are derived from two variables, `--base` and `--ink`, mixed into a grey scale. Light and dark mode follow `prefers-color-scheme` by default; force one with a `data-theme` attribute on `<html>`:
|
|
48
|
+
|
|
49
|
+
```html
|
|
50
|
+
<html data-theme="dark"></html>
|
|
45
51
|
```
|
|
46
52
|
|
|
47
|
-
|
|
53
|
+
Set `--accent` to colour checked controls, primary buttons and focus rings. It defaults to `--ink`, and the foreground colour on top of it is picked automatically for contrast:
|
|
48
54
|
|
|
49
|
-
```
|
|
50
|
-
|
|
55
|
+
```css
|
|
56
|
+
:root {
|
|
57
|
+
--accent: #2563eb;
|
|
58
|
+
}
|
|
51
59
|
```
|
|
52
60
|
|
|
53
|
-
|
|
61
|
+
See [`src/lib/styles/tokens.css`](src/lib/styles/tokens.css) for every token.
|
|
54
62
|
|
|
55
|
-
|
|
63
|
+
## Components
|
|
56
64
|
|
|
57
|
-
|
|
65
|
+
| Category | Components |
|
|
66
|
+
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
67
|
+
| Actions | `Button`, `ButtonGroup`, `Toggle`, `Link` |
|
|
68
|
+
| Forms | `Input`, `InputGroup`, `Textarea`, `Select`, `NumberStepper`, `Checkbox`, `RadioGroup`, `Switch`, `Label`, `Field` family |
|
|
69
|
+
| Pickers | `Calendar`, `TimePicker`, `ColorPicker` |
|
|
70
|
+
| Overlays | `Dialog`, `AlertDialog`, `Drawer`, `Popover`, `Tooltip`, `ContextMenu`, `Toaster` + `toast()` |
|
|
71
|
+
| Navigation | `Navbar`, `NavigationMenu`, `Sidebar` family, `Breadcrumb`, `Tabs` |
|
|
72
|
+
| Display | `Card` family, `Table`, `Accordion`, `Avatar`, `AvatarGroup`, `Badge`, `EmptyState`, `Kbd`, `Progress`, `Separator`, `Skeleton`, `Spinner` |
|
|
58
73
|
|
|
59
|
-
|
|
74
|
+
Every component is documented in Storybook, with one story per prop.
|
|
60
75
|
|
|
61
|
-
|
|
76
|
+
## Development
|
|
62
77
|
|
|
63
78
|
```sh
|
|
64
|
-
npm
|
|
79
|
+
npm install
|
|
80
|
+
npm run storybook # component workbench on http://localhost:6006
|
|
65
81
|
```
|
|
82
|
+
|
|
83
|
+
| Script | Description |
|
|
84
|
+
| ------------------- | ------------------------------------------------ |
|
|
85
|
+
| `npm run storybook` | Start Storybook |
|
|
86
|
+
| `npm run check` | Type-check with `svelte-check` |
|
|
87
|
+
| `npm run test` | Run unit and Storybook tests with Vitest |
|
|
88
|
+
| `npm run lint` | Check formatting (Prettier) and lint (ESLint) |
|
|
89
|
+
| `npm run format` | Format all files |
|
|
90
|
+
| `npm run build` | Build the package into `dist/` and run `publint` |
|
|
91
|
+
|
|
92
|
+
Components live in `src/lib/components/<name>/`, each next to its `.stories.svelte` file, and are exported from [`src/lib/index.ts`](src/lib/index.ts).
|
|
93
|
+
|
|
94
|
+
## Releasing
|
|
95
|
+
|
|
96
|
+
Publishing a GitHub release triggers [`.github/workflows/publish.yml`](.github/workflows/publish.yml), which sets the package version from the release tag (e.g. `v0.2.0` → `0.2.0`), type-checks, and stages the publish on npm with provenance. The staged version then has to be approved with 2FA (`npm stage approve <stage-id>` or on npmjs.com).
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
+
import { setAccordionContext } from './context.js';
|
|
5
|
+
|
|
6
|
+
type Props = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
|
|
7
|
+
/** Lets several items be open at once. By default opening one closes the others. */
|
|
8
|
+
multiple?: boolean;
|
|
9
|
+
/** AccordionItems. */
|
|
10
|
+
children: Snippet;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
let { multiple = false, class: className, children, ...rest }: Props = $props();
|
|
14
|
+
|
|
15
|
+
const name = $props.id();
|
|
16
|
+
|
|
17
|
+
setAccordionContext({
|
|
18
|
+
get name() {
|
|
19
|
+
return multiple ? undefined : name;
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<div {...rest} class={['accordion', className]}>
|
|
25
|
+
{@render children()}
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<style>
|
|
29
|
+
.accordion {
|
|
30
|
+
display: flex;
|
|
31
|
+
flex-direction: column;
|
|
32
|
+
width: 100%;
|
|
33
|
+
}
|
|
34
|
+
</style>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
type Props = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
|
|
4
|
+
/** Lets several items be open at once. By default opening one closes the others. */
|
|
5
|
+
multiple?: boolean;
|
|
6
|
+
/** AccordionItems. */
|
|
7
|
+
children: Snippet;
|
|
8
|
+
};
|
|
9
|
+
declare const Accordion: import("svelte").Component<Props, {}, "">;
|
|
10
|
+
type Accordion = ReturnType<typeof Accordion>;
|
|
11
|
+
export default Accordion;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLDetailsAttributes } from 'svelte/elements';
|
|
4
|
+
import { ChevronDown } from '@lucide/svelte';
|
|
5
|
+
import { getAccordionContext } from './context.js';
|
|
6
|
+
|
|
7
|
+
type Props = Omit<HTMLDetailsAttributes, 'children' | 'title' | 'name'> & {
|
|
8
|
+
/** Whether the item is expanded. Bindable. */
|
|
9
|
+
open?: boolean;
|
|
10
|
+
/** The always visible heading that toggles the item. */
|
|
11
|
+
title: string;
|
|
12
|
+
/** The content shown while the item is open. */
|
|
13
|
+
children: Snippet;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
let { open = $bindable(false), title, class: className, children, ...rest }: Props = $props();
|
|
17
|
+
|
|
18
|
+
const accordion = getAccordionContext();
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<details {...rest} bind:open name={accordion.name} class={['accordion-item', className]}>
|
|
22
|
+
<summary class="accordion-trigger">
|
|
23
|
+
<span class="accordion-title">{title}</span>
|
|
24
|
+
<ChevronDown class="accordion-chevron" aria-hidden="true" />
|
|
25
|
+
</summary>
|
|
26
|
+
<div class="accordion-content">{@render children()}</div>
|
|
27
|
+
</details>
|
|
28
|
+
|
|
29
|
+
<style>
|
|
30
|
+
.accordion-item {
|
|
31
|
+
font-size: 13px;
|
|
32
|
+
color: var(--color-text);
|
|
33
|
+
border-bottom: 1px solid var(--color-border);
|
|
34
|
+
/* Lets the height animate to and from `auto` where the browser supports it */
|
|
35
|
+
interpolate-size: allow-keywords;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.accordion-item:last-child {
|
|
39
|
+
border-bottom: 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.accordion-trigger {
|
|
43
|
+
display: flex;
|
|
44
|
+
align-items: center;
|
|
45
|
+
justify-content: space-between;
|
|
46
|
+
gap: 12px;
|
|
47
|
+
padding: 14px 0;
|
|
48
|
+
font-weight: 500;
|
|
49
|
+
list-style: none;
|
|
50
|
+
border-radius: 6px;
|
|
51
|
+
outline: none;
|
|
52
|
+
cursor: pointer;
|
|
53
|
+
user-select: none;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.accordion-trigger::-webkit-details-marker {
|
|
57
|
+
display: none;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.accordion-trigger:hover .accordion-title {
|
|
61
|
+
text-decoration: underline;
|
|
62
|
+
text-underline-offset: 3px;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.accordion-trigger:focus-visible {
|
|
66
|
+
outline: 2px solid var(--color-accent);
|
|
67
|
+
outline-offset: 2px;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.accordion-trigger :global(.accordion-chevron) {
|
|
71
|
+
width: 16px;
|
|
72
|
+
height: 16px;
|
|
73
|
+
flex-shrink: 0;
|
|
74
|
+
color: var(--color-text-muted);
|
|
75
|
+
transition: rotate 200ms ease;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.accordion-item[open] > .accordion-trigger :global(.accordion-chevron) {
|
|
79
|
+
rotate: 180deg;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.accordion-item::details-content {
|
|
83
|
+
block-size: 0;
|
|
84
|
+
overflow: clip;
|
|
85
|
+
transition:
|
|
86
|
+
block-size 200ms ease,
|
|
87
|
+
content-visibility 200ms allow-discrete;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.accordion-item[open]::details-content {
|
|
91
|
+
block-size: auto;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.accordion-content {
|
|
95
|
+
padding-bottom: 14px;
|
|
96
|
+
line-height: 1.5;
|
|
97
|
+
color: var(--color-text-secondary);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@media (prefers-reduced-motion: reduce) {
|
|
101
|
+
.accordion-item::details-content,
|
|
102
|
+
.accordion-trigger :global(.accordion-chevron) {
|
|
103
|
+
transition: none;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
</style>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLDetailsAttributes } from 'svelte/elements';
|
|
3
|
+
type Props = Omit<HTMLDetailsAttributes, 'children' | 'title' | 'name'> & {
|
|
4
|
+
/** Whether the item is expanded. Bindable. */
|
|
5
|
+
open?: boolean;
|
|
6
|
+
/** The always visible heading that toggles the item. */
|
|
7
|
+
title: string;
|
|
8
|
+
/** The content shown while the item is open. */
|
|
9
|
+
children: Snippet;
|
|
10
|
+
};
|
|
11
|
+
declare const AccordionItem: import("svelte").Component<Props, {}, "open">;
|
|
12
|
+
type AccordionItem = ReturnType<typeof AccordionItem>;
|
|
13
|
+
export default AccordionItem;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type AccordionContext = {
|
|
2
|
+
/** Shared `name` of the items' <details>, so the browser keeps only one open. Undefined with `multiple`. */
|
|
3
|
+
readonly name: string | undefined;
|
|
4
|
+
};
|
|
5
|
+
export declare const getAccordionContext: () => AccordionContext, setAccordionContext: (context: AccordionContext) => AccordionContext;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
+
|
|
5
|
+
type Props = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
|
|
6
|
+
/** Accessible name for the group, e.g. "Members". */
|
|
7
|
+
label?: string;
|
|
8
|
+
/** How many more there are than the Avatars shown; rendered as "+3" at the end. */
|
|
9
|
+
more?: number;
|
|
10
|
+
/** Size of the "+3"; match it to the Avatars' `size`. */
|
|
11
|
+
size?: 'sm' | 'md' | 'lg';
|
|
12
|
+
/** Avatars, overlapping each other. */
|
|
13
|
+
children: Snippet;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
let { label, more = 0, size = 'md', class: className, children, ...rest }: Props = $props();
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<div {...rest} role="group" aria-label={label} class={['avatar-group', className]}>
|
|
20
|
+
{@render children()}
|
|
21
|
+
{#if more > 0}
|
|
22
|
+
<span class={['avatar-group-more', `avatar-group-more--${size}`]}>+{more}</span>
|
|
23
|
+
{/if}
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<style>
|
|
27
|
+
.avatar-group {
|
|
28
|
+
display: inline-flex;
|
|
29
|
+
align-items: center;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* The ring in the page colour separates the overlapping avatars */
|
|
33
|
+
.avatar-group > :global(*) {
|
|
34
|
+
box-shadow: 0 0 0 2px var(--color-bg);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.avatar-group > :global(:not(:first-child)) {
|
|
38
|
+
margin-inline-start: -6px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.avatar-group-more {
|
|
42
|
+
display: inline-flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
justify-content: center;
|
|
45
|
+
box-sizing: border-box;
|
|
46
|
+
flex-shrink: 0;
|
|
47
|
+
padding: 0 6px;
|
|
48
|
+
font-weight: 500;
|
|
49
|
+
font-variant-numeric: tabular-nums;
|
|
50
|
+
color: var(--color-text-secondary);
|
|
51
|
+
background-color: var(--color-surface);
|
|
52
|
+
user-select: none;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* Same sizes and corners as Avatar */
|
|
56
|
+
.avatar-group-more--sm {
|
|
57
|
+
min-width: 24px;
|
|
58
|
+
height: 24px;
|
|
59
|
+
border-radius: 8px;
|
|
60
|
+
font-size: 10px;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.avatar-group-more--md {
|
|
64
|
+
min-width: 32px;
|
|
65
|
+
height: 32px;
|
|
66
|
+
border-radius: 10px;
|
|
67
|
+
font-size: 12px;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.avatar-group-more--lg {
|
|
71
|
+
min-width: 40px;
|
|
72
|
+
height: 40px;
|
|
73
|
+
border-radius: 12px;
|
|
74
|
+
font-size: 14px;
|
|
75
|
+
}
|
|
76
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
type Props = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
|
|
4
|
+
/** Accessible name for the group, e.g. "Members". */
|
|
5
|
+
label?: string;
|
|
6
|
+
/** How many more there are than the Avatars shown; rendered as "+3" at the end. */
|
|
7
|
+
more?: number;
|
|
8
|
+
/** Size of the "+3"; match it to the Avatars' `size`. */
|
|
9
|
+
size?: 'sm' | 'md' | 'lg';
|
|
10
|
+
/** Avatars, overlapping each other. */
|
|
11
|
+
children: Snippet;
|
|
12
|
+
};
|
|
13
|
+
declare const AvatarGroup: import("svelte").Component<Props, {}, "">;
|
|
14
|
+
type AvatarGroup = ReturnType<typeof AvatarGroup>;
|
|
15
|
+
export default AvatarGroup;
|