Skip to content

Commit

Permalink
Merge pull request #42 from huntabyte/documentation/render-del
Browse files Browse the repository at this point in the history
  • Loading branch information
huntabyte authored Aug 17, 2023
2 parents e8a4c56 + d18e36b commit b7453cf
Show file tree
Hide file tree
Showing 21 changed files with 3,520 additions and 34 deletions.
7 changes: 7 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@
"options": {
"parser": "svelte"
}
},
{
"files": "*.md",
"options": {
"parser": "markdown",
"printWidth": 79
}
}
]
}
39 changes: 30 additions & 9 deletions mdsvex.config.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
import remarkGfm from "remark-gfm";
import rehypePrettyCode from "rehype-pretty-code";
import { fileURLToPath } from "url";
import { getHighlighter } from "shikiji";
import { toHtml } from "hast-util-to-html";
import { visit } from "unist-util-visit";
import { escapeSvelte } from "@huntabyte/mdsvex";
import { resolve } from "path";
import { readFileSync } from "fs";

const __dirname = fileURLToPath(new URL(".", import.meta.url));

/**
* @type {import('rehype-pretty-code').Options}
*/
const prettyCodeOptions = {
theme: "github-dark",
theme: {
dark: JSON.parse(
readFileSync(resolve(__dirname, "./src/styles/themes/tokyo-night-storm.json"))
),
light: JSON.parse(
readFileSync(resolve(__dirname, "./src/styles/themes/tokyo-night-light.json"))
)
},
keepBackground: false,
onVisitLine(node) {
if (node.children.length === 0) {
Expand All @@ -25,12 +32,6 @@ const prettyCodeOptions = {
},
onVisitHighlightedWord(node) {
node.properties.className = ["word--highlighted"];
},
getHighlighter: (options) => {
return getHighlighter({
...options,
langs: ["typescript", "javascript", "bash", "json", "svelte", "css"]
});
}
};

Expand All @@ -44,7 +45,7 @@ export const mdsvexOptions = {
backticks: false,
dashes: false
},
remarkPlugins: [remarkGfm],
remarkPlugins: [remarkGfm, remarkEscapeCode],
rehypePlugins: [
rehypeComponentPreToPre,
[rehypePrettyCode, prettyCodeOptions],
Expand All @@ -54,6 +55,25 @@ export const mdsvexOptions = {
]
};

const entities = [
[/</g, "&lt;"],
[/>/g, "&gt;"],
[/{/g, "&#123;"],
[/}/g, "&#125;"]
];

function remarkEscapeCode() {
return async (tree) => {
visit(tree, "inlineCode", escape);

function escape(node) {
for (let i = 0; i < entities.length; i += 1) {
node.value = node.value.replace(entities[i][0], entities[i][1]);
}
}
};
}

function rehypeComponentPreToPre() {
return async (tree) => {
visit(tree, (node) => {
Expand Down Expand Up @@ -127,6 +147,7 @@ function rehypeRenderCode() {
);

codeEl.type = "raw";

codeEl.value = `{@html \`${escapeSvelte(meltString)}\`}`;
}
});
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"@melt-ui/svelte": "0.34.6",
"@sveltejs/adapter-vercel": "^3.0.3",
"nanoid": "^4.0.2",
"shiki": "^0.14.3",
"tailwind-merge": "^1.14.0"
},
"peerDependencies": {
Expand Down
8 changes: 3 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion src/components/icons/aria.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<script lang="ts">
import type { IconProps } from "lucide-svelte";
type $$Props = IconProps;
</script>

Expand Down
10 changes: 10 additions & 0 deletions src/components/icons/arrow-square-out.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<script lang="ts">
import type { IconProps } from "lucide-svelte";
type $$Props = IconProps;
</script>

<svg xmlns="http://www.w3.org/2000/svg" {...$$restProps} fill="currentColor" viewBox="0 0 256 256"
><path
d="M224,104a8,8,0,0,1-16,0V59.32l-66.33,66.34a8,8,0,0,1-11.32-11.32L196.68,48H152a8,8,0,0,1,0-16h64a8,8,0,0,1,8,8Zm-40,24a8,8,0,0,0-8,8v72H48V80h72a8,8,0,0,0,0-16H48A16,16,0,0,0,32,80V208a16,16,0,0,0,16,16H176a16,16,0,0,0,16-16V136A8,8,0,0,0,184,128Z"
/></svg
>
3 changes: 2 additions & 1 deletion src/components/icons/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Icon as LucideIcon } from "lucide-svelte";
import type { HTMLAttributes } from "svelte/elements";

export { default as CaretRight } from "./caret-right.svelte";
export { default as SunDim } from "./sun-dim.svelte";
Expand All @@ -13,7 +14,7 @@ export { default as Pnpm } from "./pnpm.svelte";
export { default as Yarn } from "./yarn.svelte";
export { default as Check } from "./check.svelte";
export { default as CopySimple } from "./copy-simple.svelte";
import type { HTMLAttributes } from "svelte/elements";
export { default as ArrowSquareOut } from "./arrow-square-out.svelte";

export type IconProps = Partial<HTMLAttributes<SVGElement>> & {
class?: string;
Expand Down
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export { default as SiteHeader } from "./site-header.svelte";
export { default as TailwindIndicator } from "./tailwind-indicator.svelte";
export { default as SidebarNav } from "./navigation/sidebar-nav.svelte";
export { default as CopyCodeButton } from "./copy-code-button.svelte";
export { default as Steps } from "./steps.svelte";
16 changes: 13 additions & 3 deletions src/components/markdown/a.svelte
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
<script lang="ts">
import type { HTMLAnchorAttributes } from "svelte/elements";
import { ArrowSquareOut } from "@/components/icons";
import { cn } from "@/utils";
let className: string | undefined | null = undefined;
export { className as class };
export let href: HTMLAnchorAttributes["href"] = undefined;
export let href: string;
$: internal = href.startsWith("/") || href.startsWith("#");
$: rel = !internal ? "noopener noreferrer" : undefined;
$: target = !internal ? "_blank" : undefined;
</script>

<a {href} class={cn("font-medium underline underline-offset-4", className)} {...$$restProps}>
<a
{href}
{target}
{rel}
class={cn("inline-flex items-center gap-1 font-medium underline underline-offset-4", className)}
{...$$restProps}
>
<slot />
</a>
2 changes: 1 addition & 1 deletion src/components/markdown/h2.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
</script>

<h2
class={cn("mt-12 scroll-m-20 pb-2 text-2xl font-semibold tracking-tight first:mt-0", className)}
class={cn("mt-12 scroll-m-20 text-2xl font-bold tracking-tight first:mt-0", className)}
{...$$restProps}
>
<slot />
Expand Down
2 changes: 1 addition & 1 deletion src/components/site-header.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<div class="mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl">
<div class="flex items-center justify-between gap-3 h-16">
<div class="flex items-center gap-1.5">
<a href="/" class="font-bold leading-relaxed text-xl">Primitives</a>
<a href="/" class="font-bold tracking-tight text-2xl underline underline-offset-4">bits</a>
</div>
<div class="items-center justify-end gap-1.5">
<Button
Expand Down
3 changes: 3 additions & 0 deletions src/components/steps.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div class="[&>h3]:step mb-12 ml-4 border-l pl-8 [counter-reset:step]" {...$$restProps}>
<slot />
</div>
41 changes: 39 additions & 2 deletions src/content/getting-started.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,41 @@
---
title: Installation
description: Start using primitives in your project.
title: Getting Started
description: Learn how to get started using Bits in your app.
---

## Installation

Install bits using your favorite package manager.

```bash
npm install @huntabyte/bits
```

You can them import and start using them in your app.

```svelte
<script lang="ts">
import { Accordion } from "@huntabyte/bits";
</script>
<Accordion.Root>
<Accordion.Item value="first">
<Accordion.Header>
<Accordion.Trigger>First</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content>This is the first accordion content</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="second">
<Accordion.Header>
<Accordion.Trigger>Second</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content>This is the second accordion content</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="third">
<Accordion.Header>
<Accordion.Trigger>Third</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content>This is the third accordion content</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
```
8 changes: 6 additions & 2 deletions src/content/introduction.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
---
title: Introduction
description: A collection of headless Svelte components.
description: A collection of headless Svelte components built with Melt UI builders.
---

Primitives are headless components that provide a simple interface for composing your own components. They are designed to be customizable and composable, and to work with any UI framework.
Bits are headless component primitives that provide a simple interface for composing your own components. They have been thoughtfully designed to prioritize simplicity, customizability, and compatibility with any styling or UI framework you choose to work with.

Under the hood, these components are powered by [Melt UI](https://melt-ui.com), which provides an even lower-level builder API for creating headless components. Bits take that API and wrap it in a more familiar component interface which allows us to handle some quality of life improvements for you.

While our goal has been to keep the APIs as close to Melt's as possible, there are some slight differences, so be sure to check out the documentation for the Bit you're interested in to understand how it works.
65 changes: 65 additions & 0 deletions src/content/render-delegation.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,68 @@
title: Render Delegation
description: Learn how to use the asChild prop to delegate rendering to a child element.
---

## Usage

For certain components, we set a default underlying HTML element to wrap the `<slot/>` with. As a high level example, the `AccordionTrigger`, looks something like this:

```svelte
<button>
<slot />
</button>
```

While we do allow you to set any attribute that you normally could on a button, let's say you're one of those `<div role="button" />` people and want to use a `div` or some other element instead of the button we provide out of the box. That's when render delegation comes into play.

Each of the components that support render delegation have an optional prop called `asChild`. When set to `true`, the component will pass the `builder` as a slot prop, which you can then apply to the element of your choosing.

Let's take a look at an example using the `AccordionTrigger` component:

```svelte
<AccordionTrigger asChild let:builder>
<div use:builder.action {...builder}>Open accordion item</div>
</AccordionTrigger>
```

We're passing the Melt UI builder we would normally apply to the `<button>` as a prop which can then be applied to any element we want. Keep in mind, you will need to `use:builder.action`, as well as spread the builder props onto the element you're using to retain all functionality.

So behind the scenes this is what's happening (pseudo):

```svelte
<script lang="ts">
// other imports/props/logic omitted for brevity
const trigger = getTrigger();
export let asChild = false;
</script>
{#if asChild}
<slot builder={$trigger} />
{:else}
<button use:trigger.action {...trigger}>
<slot />
</button>
{/if}
```

## Use with components

Unfortunately, wanting to delegate rendering to a component isn't as simple as an element, since we can't apply actions directly to components. While still possible, there's some work that has to be done to each component you intend to use render delegation with.

At the time of writing this, we're still working on refined type / helpers functions on the Melt UI side for this exact purpose, but for the time being, we expose a few helper functions and types to make this easier.

Let's create a custom `<Button />` component that could be used with this pattern. This is a simplified version of the [Button](/docs/components/button) component we provide as a part of Bits, but it should give you an idea of how to implement this pattern.

```svelte
<script lang="ts">
import { builderActions, builderAttrs, type Builder } from "@huntabyte/bits"
export let builders: Builder[] = []
</script>
<button use:builderActions={{ builders }} {...builderAttrs(builders)}>
<slot />
<button>
```

We're using the `builderActions` and `builderAttrs` helpers to apply the actions and attributes to the button. We're also using the `Builder` type to type the `builders` prop we'd receive from whatever component we're using this with. We use an array here to cover the case where we may want to apply multiple builders to the button. The helper functions handle applying all the actions and attributes to the button for us.

If you do plan to pass multiple builders, the order in which you pass them matters. Certain actions/attributes may override others, so be sure to test your implementation to ensure it's working as expected.
Loading

0 comments on commit b7453cf

Please sign in to comment.