drab 2.8.1 → 2.8.3

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 CHANGED
@@ -1,7 +1,67 @@
1
- # drab
1
+ # An unstyled Svelte component library
2
2
 
3
- ## An unstyled Svelte component library
3
+ - [GitHub](https://github.com/rossrobino/drab)
4
+ - [npm](https://www.npmjs.com/package/drab)
5
+ - [MIT License](https://github.com/rossrobino/drab/blob/main/LICENSE.md)
6
+ - One dependency - [Svelte](https://svelte.dev)
4
7
 
5
- [drab.robino.dev](https://drab.robino.dev)
8
+ ## About
6
9
 
7
- ### MIT License
10
+ **drab** focuses on providing JavaScript functionality where it's most useful, while leaving out components that can be easily created using HTML, such as a label or badge. Whenever possible, components are [progressively enhanced](/docs/ShareButton) or provide a fallback [noscript](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/noscript) message. Additionally, transitions are disabled for users who prefer reduced motion.
11
+
12
+ This library takes a more opinionated approach compared to some headless UI libraries by providing the basic HTML structure for every component, as well as default positioning for elements like the [sheet](/docs/Sheet) or [popover](/docs/Popover). However, these components can still be further customized using styles, [slots](https://svelte.dev/tutorial/slots), and [slot props](https://svelte.dev/tutorial/slot-props).
13
+
14
+ Components without styles can appear rather drab. You have the freedom to bring your own styles to these components! Using unstyled components allows you to selectively choose what you need, seamlessly integrate with existing designs, and avoid being tied to any specific library.
15
+
16
+ To style the components, you can make use of [global styles](https://joyofcode.xyz/global-styles-in-sveltekit). This process can be expedited by utilizing CSS frameworks like [TailwindCSS](https://tailwindcss.com/). TailwindCSS generates a global stylesheet based on the utility classes used in your project. Each component exports `class` and `id` props that can be leveraged for this purpose.
17
+
18
+ ## Install
19
+
20
+ If you haven't used Svelte before, start with the [tutorial](https://svelte.dev/tutorial/basics).
21
+
22
+ - [SvelteKit](https://kit.svelte.dev)
23
+ - [Astro](https://docs.astro.build/en/tutorial/1-setup/2/)
24
+ - [Vite](https://vitejs.dev/guide/)
25
+
26
+ ```bash
27
+ npm install -D drab
28
+ ```
29
+
30
+ ## Documentation
31
+
32
+ The library provides inline documentation for each component, allowing you to conveniently access the documentation by hovering over the component in your text editor after importing it. Additionally, every prop is documented using JSDoc and TypeScript. By hovering over a prop, you can retrieve its type and description.
33
+
34
+ These docs use the [TailwindCSS Typography plugin](https://tailwindcss.com/docs/typography-plugin) for base styles along with a few custom utility classes you can find [here](https://github.com/rossrobino/drab/blob/main/src/app.postcss). Styles on this site are based on [shadcn/ui](https://ui.shadcn.com/).
35
+
36
+ ## Alternatives
37
+
38
+ If **drab** isn't what you are looking for, here are some other Svelte UI libraries to check out.
39
+
40
+ - [Skeleton](https://skeleton.dev)
41
+ - [Melt UI](https://www.melt-ui.com/)
42
+ - [shadcn-svelte](https://www.shadcn-svelte.com/)
43
+ - [Svelte-HeadlessUI](https://captaincodeman.github.io/svelte-headlessui/)
44
+
45
+ ## Contributing
46
+
47
+ Find an bug or have an idea? Feel free to create an issue on [GitHub](https://github.com/rossrobino/drab).
48
+
49
+ Since this is an unstyled library, simple components like a badge that can be easily created with HTML and CSS are not included.
50
+
51
+ ### Local Development
52
+
53
+ Contribute to the project, or use **drab** as a template for another component library. This library is built with SvelteKit, TypeScript, and npm. The package contents are located in `src/lib`, the site is contained within `src/routes` and `src/site`. The site is deployed to Vercel using `@sveltejs/adapter-vercel`. If you are using this project as a template, be sure to [update the adapter](https://kit.svelte.dev/docs/adapters) based on how you deploy.
54
+
55
+ #### Make changes
56
+
57
+ 1. Clone the [repository](https://github.com/rossrobino/drab)
58
+ 2. `npm install`
59
+ 3. `npm run dev -- --open`
60
+
61
+ #### Add or edit a component
62
+
63
+ 1. Add/edit the component in `src/lib/components/Component.svelte`, if you're adding a new one, copy and paste an existing one to get started with the conventions
64
+ 2. Add/edit the example in `src/routes/docs/Component/+page.svelte`
65
+ 3. Document the component with an `@component` comment, include a description, and the `@slots` available. Add a placeholder `@props` and `@example` to the comment. These sections will be generated based on the JSDoc comment above each prop and the example route created upon running `npm run doc`
66
+ 4. If new, add the link to `src/site/components/NavItems.svelte`
67
+ 5. Run `npm run build` to verify your build
@@ -3,20 +3,19 @@
3
3
 
4
4
  ### Accordion
5
5
 
6
- Displays a list of `details` elements with helpful defaults and transitions. Use `AccordionItem.data` to send any additional data through the slot props. Works without JavaScript.
6
+ Displays a list of `details` elements with helpful defaults and transitions.
7
7
 
8
8
  @props
9
9
 
10
10
  - `autoClose` - if `true`, other items close when a new one is opened
11
11
  - `classContent` - class of all the `div`s that wrap the `content` slot
12
12
  - `classDetails` - class of the `div` around each `details` element
13
- - `classHeader` - class of all the `summary` elements
14
- - `classIcon` - class of the `div` that wrap the icon if displayed
15
- - `classSummary` - class of all the `div`s that wrap the `summary` slot
13
+ - `classIcon` - class of the `div` that wraps the icon if displayed
14
+ - `classSummary` - class of all the `summary` elements
16
15
  - `class`
16
+ - `data` - data to display in the accordion
17
17
  - `icon`
18
18
  - `id`
19
- - `items` - array of `AccordionItem` elements
20
19
  - `transition` - rotates the icon, slides the content, set to `false` to remove
21
20
 
22
21
  @slots
@@ -31,18 +30,10 @@ Displays a list of `details` elements with helpful defaults and transitions. Use
31
30
 
32
31
  ```svelte
33
32
  <script lang="ts">
34
- import { Accordion } from "drab";
35
- import { FullscreenButton } from "drab";
36
- import { Chevron } from "../../site/svg/Chevron.svelte";
37
- </script>
33
+ import { Accordion, type AccordionItem, FullscreenButton } from "drab";
34
+ import Chevron from "../../site/svg/Chevron.svelte";
38
35
 
39
- <Accordion
40
- icon={Chevron}
41
- class="mb-12"
42
- classDetails="border-b"
43
- classHeader="flex gap-8 cursor-pointer items-center justify-between p-4 font-bold underline hover:decoration-dotted"
44
- classContent="pb-4 px-4"
45
- items={[
36
+ const data: AccordionItem[] = [
46
37
  { summary: "Is it accessible?", content: "Yes." },
47
38
  {
48
39
  summary: "Is it styled?",
@@ -52,35 +43,42 @@ Displays a list of `details` elements with helpful defaults and transitions. Use
52
43
  summary: "Is it animated?",
53
44
  content: "Yes, with the transition prop.",
54
45
  },
46
+ {
47
+ summary: "Is it customizable?",
48
+ content: "Yes, customize with slots.",
49
+ class: "uppercase",
50
+ component: FullscreenButton,
51
+ },
55
52
  { summary: "Does it work without JavaScript?", content: "Yes." },
56
- ]}
53
+ ];
54
+ </script>
55
+
56
+ <Accordion
57
+ {data}
58
+ icon={Chevron}
59
+ class="mb-12"
60
+ classDetails="border-b"
61
+ classSummary="flex gap-8 cursor-pointer items-center justify-between p-4 font-bold underline hover:decoration-dotted"
62
+ classContent="pb-4 px-4"
57
63
  />
58
64
 
59
65
  <Accordion
66
+ {data}
60
67
  icon={Chevron}
61
68
  classDetails="border-b"
62
- classHeader="flex gap-8 cursor-pointer items-center justify-between p-4 font-bold underline hover:decoration-dotted"
69
+ classSummary="flex gap-8 cursor-pointer items-center justify-between p-4 font-bold underline hover:decoration-dotted"
63
70
  classContent="pb-4 px-4"
64
71
  autoClose={false}
65
- items={[
66
- { summary: "Summary", content: "Content" },
67
- { summary: "Summary", content: "Content", data: { uppercase: true } },
68
- {
69
- summary: "Summary",
70
- content: "Content",
71
- data: { component: FullscreenButton },
72
- },
73
- ]}
74
72
  >
75
73
  <svelte:fragment slot="summary" let:item let:index>
76
- <span class:uppercase={item.data?.uppercase}>
74
+ <span class={item.class ? item.class : ""}>
75
+ {index + 1}.
77
76
  {item.summary}
78
- {index + 1}
79
77
  </span>
80
78
  </svelte:fragment>
81
79
  <svelte:fragment slot="content" let:item>
82
80
  <span>{item.content}</span>
83
- {#if item.data?.component === FullscreenButton}
81
+ {#if item.component === FullscreenButton}
84
82
  <div><svelte:component this={FullscreenButton} class="btn mt-4" /></div>
85
83
  {/if}
86
84
  </svelte:fragment>
@@ -97,23 +95,21 @@ import { duration } from "../util/transition";
97
95
  let className = "";
98
96
  export { className as class };
99
97
  export let id = "";
100
- export let items;
98
+ export let data;
101
99
  export let icon = "";
102
100
  export let classDetails = "";
103
- export let classHeader = "";
104
101
  export let classSummary = "";
105
102
  export let classContent = "";
106
103
  export let classIcon = "";
107
104
  export let transition = { duration };
108
- const cssDuration = transition ? transition.duration : 0;
109
105
  export let autoClose = true;
110
106
  let clientJs = false;
111
107
  const toggleOpen = (i) => {
112
- items[i].open = !items[i].open;
108
+ data[i].open = !data[i].open;
113
109
  if (autoClose) {
114
- for (let j = 0; j < items.length; j++) {
110
+ for (let j = 0; j < data.length; j++) {
115
111
  if (j !== i)
116
- items[j].open = false;
112
+ data[j].open = false;
117
113
  }
118
114
  }
119
115
  };
@@ -125,14 +121,14 @@ onMount(() => {
125
121
  </script>
126
122
 
127
123
  <div class={className} {id}>
128
- {#each items as item, index}
124
+ {#each data as item, index}
129
125
  <div class={classDetails}>
130
126
  <details bind:open={item.open}>
131
127
  <!-- svelte-ignore a11y-no-redundant-roles -->
132
128
  <summary
133
129
  role="button"
134
130
  tabindex="0"
135
- class={classHeader}
131
+ class={classSummary}
136
132
  on:click|preventDefault={() => toggleOpen(index)}
137
133
  on:keydown={(e) => {
138
134
  if (e.key === "Enter") {
@@ -141,16 +137,14 @@ onMount(() => {
141
137
  }
142
138
  }}
143
139
  >
144
- <div class={classSummary}>
145
- <slot name="summary" {item} {index}>{item.summary}</slot>
146
- </div>
140
+ <slot name="summary" {item} {index}>{item.summary}</slot>
147
141
  <slot name="icon" {item} {index}>
148
142
  {#if icon}
149
143
  <div
150
144
  class={classIcon}
151
145
  class:d-rotate-180={item.open}
152
146
  class:d-transition={transition}
153
- style="--duration: {cssDuration}ms;"
147
+ style="--duration: {transition ? transition.duration : 0}ms;"
154
148
  >
155
149
  {#if typeof icon !== "string"}
156
150
  <svelte:component this={icon} />
@@ -1,5 +1,5 @@
1
1
  import { SvelteComponent } from "svelte";
2
- export interface AccordionItem<T = any> {
2
+ export type AccordionItem = {
3
3
  /** text summary of the item */
4
4
  summary?: string;
5
5
  /** text content of the item */
@@ -7,21 +7,20 @@ export interface AccordionItem<T = any> {
7
7
  /** controls whether the content is displayed */
8
8
  open?: boolean;
9
9
  /** any data to pass back to the parent */
10
- data?: T;
11
- }
10
+ [key: string | number]: any;
11
+ };
12
12
  import { type ComponentType } from "svelte";
13
13
  import { type SlideParams } from "svelte/transition";
14
14
  declare const __propDef: {
15
15
  props: {
16
16
  class?: string | undefined;
17
17
  id?: string | undefined;
18
- /** array of `AccordionItem` elements */ items: AccordionItem[];
18
+ /** data to display in the accordion */ data: AccordionItem[];
19
19
  icon?: string | ComponentType | undefined;
20
20
  /** class of the `div` around each `details` element */ classDetails?: string | undefined;
21
- /** class of all the `summary` elements */ classHeader?: string | undefined;
22
- /** class of all the `div`s that wrap the `summary` slot */ classSummary?: string | undefined;
21
+ /** class of all the `summary` elements */ classSummary?: string | undefined;
23
22
  /** class of all the `div`s that wrap the `content` slot */ classContent?: string | undefined;
24
- /** class of the `div` that wrap the icon if displayed */ classIcon?: string | undefined;
23
+ /** class of the `div` that wraps the icon if displayed */ classIcon?: string | undefined;
25
24
  /** rotates the icon, slides the content, set to `false` to remove */ transition?: false | SlideParams | undefined;
26
25
  /** if `true`, other items close when a new one is opened */ autoClose?: boolean | undefined;
27
26
  };
@@ -30,15 +29,15 @@ declare const __propDef: {
30
29
  };
31
30
  slots: {
32
31
  summary: {
33
- item: AccordionItem<any>;
32
+ item: AccordionItem;
34
33
  index: any;
35
34
  };
36
35
  icon: {
37
- item: AccordionItem<any>;
36
+ item: AccordionItem;
38
37
  index: any;
39
38
  };
40
39
  content: {
41
- item: AccordionItem<any>;
40
+ item: AccordionItem;
42
41
  index: any;
43
42
  };
44
43
  };
@@ -49,20 +48,19 @@ export type AccordionSlots = typeof __propDef.slots;
49
48
  /**
50
49
  * ### Accordion
51
50
  *
52
- * Displays a list of `details` elements with helpful defaults and transitions. Use `AccordionItem.data` to send any additional data through the slot props. Works without JavaScript.
51
+ * Displays a list of `details` elements with helpful defaults and transitions.
53
52
  *
54
53
  * @props
55
54
  *
56
55
  * - `autoClose` - if `true`, other items close when a new one is opened
57
56
  * - `classContent` - class of all the `div`s that wrap the `content` slot
58
57
  * - `classDetails` - class of the `div` around each `details` element
59
- * - `classHeader` - class of all the `summary` elements
60
- * - `classIcon` - class of the `div` that wrap the icon if displayed
61
- * - `classSummary` - class of all the `div`s that wrap the `summary` slot
58
+ * - `classIcon` - class of the `div` that wraps the icon if displayed
59
+ * - `classSummary` - class of all the `summary` elements
62
60
  * - `class`
61
+ * - `data` - data to display in the accordion
63
62
  * - `icon`
64
63
  * - `id`
65
- * - `items` - array of `AccordionItem` elements
66
64
  * - `transition` - rotates the icon, slides the content, set to `false` to remove
67
65
  *
68
66
  * @slots
@@ -77,18 +75,10 @@ export type AccordionSlots = typeof __propDef.slots;
77
75
  *
78
76
  * ```svelte
79
77
  * <script lang="ts">
80
- * import { Accordion } from "drab";
81
- * import { FullscreenButton } from "drab";
82
- * import { Chevron } from "../../site/svg/Chevron.svelte";
83
- * </script>
78
+ * import { Accordion, type AccordionItem, FullscreenButton } from "drab";
79
+ * import Chevron from "../../site/svg/Chevron.svelte";
84
80
  *
85
- * <Accordion
86
- * icon={Chevron}
87
- * class="mb-12"
88
- * classDetails="border-b"
89
- * classHeader="flex gap-8 cursor-pointer items-center justify-between p-4 font-bold underline hover:decoration-dotted"
90
- * classContent="pb-4 px-4"
91
- * items={[
81
+ * const data: AccordionItem[] = [
92
82
  * { summary: "Is it accessible?", content: "Yes." },
93
83
  * {
94
84
  * summary: "Is it styled?",
@@ -98,35 +88,42 @@ export type AccordionSlots = typeof __propDef.slots;
98
88
  * summary: "Is it animated?",
99
89
  * content: "Yes, with the transition prop.",
100
90
  * },
91
+ * {
92
+ * summary: "Is it customizable?",
93
+ * content: "Yes, customize with slots.",
94
+ * class: "uppercase",
95
+ * component: FullscreenButton,
96
+ * },
101
97
  * { summary: "Does it work without JavaScript?", content: "Yes." },
102
- * ]}
98
+ * ];
99
+ * </script>
100
+ *
101
+ * <Accordion
102
+ * {data}
103
+ * icon={Chevron}
104
+ * class="mb-12"
105
+ * classDetails="border-b"
106
+ * classSummary="flex gap-8 cursor-pointer items-center justify-between p-4 font-bold underline hover:decoration-dotted"
107
+ * classContent="pb-4 px-4"
103
108
  * />
104
109
  *
105
110
  * <Accordion
111
+ * {data}
106
112
  * icon={Chevron}
107
113
  * classDetails="border-b"
108
- * classHeader="flex gap-8 cursor-pointer items-center justify-between p-4 font-bold underline hover:decoration-dotted"
114
+ * classSummary="flex gap-8 cursor-pointer items-center justify-between p-4 font-bold underline hover:decoration-dotted"
109
115
  * classContent="pb-4 px-4"
110
116
  * autoClose={false}
111
- * items={[
112
- * { summary: "Summary", content: "Content" },
113
- * { summary: "Summary", content: "Content", data: { uppercase: true } },
114
- * {
115
- * summary: "Summary",
116
- * content: "Content",
117
- * data: { component: FullscreenButton },
118
- * },
119
- * ]}
120
117
  * >
121
118
  * <svelte:fragment slot="summary" let:item let:index>
122
- * <span class:uppercase={item.data?.uppercase}>
119
+ * <span class={item.class ? item.class : ""}>
120
+ * {index + 1}.
123
121
  * {item.summary}
124
- * {index + 1}
125
122
  * </span>
126
123
  * </svelte:fragment>
127
124
  * <svelte:fragment slot="content" let:item>
128
125
  * <span>{item.content}</span>
129
- * {#if item.data?.component === FullscreenButton}
126
+ * {#if item.component === FullscreenButton}
130
127
  * <div><svelte:component this={FullscreenButton} class="btn mt-4" /></div>
131
128
  * {/if}
132
129
  * </svelte:fragment>
@@ -11,7 +11,7 @@ Displays when the `target` element is right clicked, or long pressed on mobile.
11
11
  - `display` - shows / hides the menu
12
12
  - `id`
13
13
  - `target` - target element to right click, defaults to the parent element
14
- - `transition` - fades the content, set to `false` to remove
14
+ - `transition` - scales the menu, set to `false` to disable
15
15
 
16
16
  @slots
17
17
 
@@ -40,7 +40,7 @@ Displays when the `target` element is right clicked, or long pressed on mobile.
40
40
  </ContextMenu>
41
41
  </div>
42
42
 
43
- <button class="btn" bind:this={target}>Target Right Click</button>
43
+ <button type="button" class="btn" bind:this={target}>Target Right Click</button>
44
44
  <ContextMenu {target}>
45
45
  <div class="flex w-48 flex-col gap-2 rounded border bg-white p-2 shadow">
46
46
  <div class="font-bold">Context Menu</div>
@@ -53,15 +53,15 @@ Displays when the `target` element is right clicked, or long pressed on mobile.
53
53
  -->
54
54
 
55
55
  <script>import { onMount, tick } from "svelte";
56
- import { fade } from "svelte/transition";
57
- import { duration } from "../util/transition";
56
+ import { scale } from "svelte/transition";
57
+ import { duration, start } from "../util/transition";
58
58
  import { prefersReducedMotion } from "../util/accessibility";
59
59
  import { delay } from "../util/delay";
60
60
  let className = "";
61
61
  export { className as class };
62
62
  export let id = "";
63
63
  export let display = false;
64
- export let transition = { duration };
64
+ export let transition = { duration, start };
65
65
  export let target = null;
66
66
  let contextMenu;
67
67
  let base;
@@ -102,11 +102,10 @@ const onTouchStart = (e) => {
102
102
  const onTouchEnd = () => {
103
103
  clearTimeout(timer);
104
104
  };
105
- onMount(() => {
106
- if (prefersReducedMotion()) {
107
- if (transition)
108
- transition.duration = 0;
109
- }
105
+ onMount(async () => {
106
+ if (prefersReducedMotion())
107
+ transition = false;
108
+ await tick();
110
109
  if (!target) {
111
110
  target = base.parentElement;
112
111
  }
@@ -131,7 +130,7 @@ onMount(() => {
131
130
  bind:this={contextMenu}
132
131
  style:top="{coordinates.y}px"
133
132
  style:left="{coordinates.x}px"
134
- transition:fade={transition ? transition : { duration: 0 }}
133
+ transition:scale={transition ? transition : { duration: 0 }}
135
134
  >
136
135
  <slot>Context Menu</slot>
137
136
  </div>
@@ -1,11 +1,11 @@
1
1
  import { SvelteComponent } from "svelte";
2
- import { type FadeParams } from "svelte/transition";
2
+ import { type ScaleParams } from "svelte/transition";
3
3
  declare const __propDef: {
4
4
  props: {
5
5
  class?: string | undefined;
6
6
  id?: string | undefined;
7
7
  /** shows / hides the menu */ display?: boolean | undefined;
8
- /** fades the content, set to `false` to remove */ transition?: false | FadeParams | undefined;
8
+ /** scales the menu, set to `false` to disable */ transition?: false | ScaleParams | undefined;
9
9
  /** target element to right click, defaults to the parent element */ target?: HTMLElement | null | undefined;
10
10
  };
11
11
  events: {
@@ -29,7 +29,7 @@ export type ContextMenuSlots = typeof __propDef.slots;
29
29
  * - `display` - shows / hides the menu
30
30
  * - `id`
31
31
  * - `target` - target element to right click, defaults to the parent element
32
- * - `transition` - fades the content, set to `false` to remove
32
+ * - `transition` - scales the menu, set to `false` to disable
33
33
  *
34
34
  * @slots
35
35
  *
@@ -58,7 +58,7 @@ export type ContextMenuSlots = typeof __propDef.slots;
58
58
  * </ContextMenu>
59
59
  * </div>
60
60
  *
61
- * <button class="btn" bind:this={target}>Target Right Click</button>
61
+ * <button type="button" class="btn" bind:this={target}>Target Right Click</button>
62
62
  * <ContextMenu {target}>
63
63
  * <div class="flex w-48 flex-col gap-2 rounded border bg-white p-2 shadow">
64
64
  * <div class="font-bold">Context Menu</div>