Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 76 additions & 37 deletions docs/src/lib/components/Code.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,62 +11,101 @@
import { CopyButton } from 'svelte-ux';
import { cls } from '@layerstack/tailwind';
import type { HTMLAttributes } from 'svelte/elements';
import { Icon } from 'svelte-ux';
import SimpleIconsCss from '~icons/simple-icons/css';
import SimpleIconsJavascript from '~icons/simple-icons/javascript';
import SimpleIconsTypescript from '~icons/simple-icons/typescript';
import SimpleIconsJson from '~icons/simple-icons/json';
import SimpleIconsTerminal from '~icons/simple-icons/windowsterminal';
import SimpleIconsSvelte from '~icons/simple-icons/svelte';
import SimpleIconsHTML5 from '~icons/simple-icons/html5';

interface Props {
source?: string | null;
language?: string;
includeCopyButton?: boolean;
title?: string;
copyButton?: boolean | 'hover';
classes?: { root?: string; pre?: string; code?: string };
class?: string;
}

let {
title,
source = null,
language = 'svelte',
includeCopyButton = true,
copyButton = true,
classes = {},
class: className
}: Props & HTMLAttributes<HTMLDivElement> = $props();
</script>

<div
class={cls(
'Code',
'relative bg-surface-200 dark:bg-surface-300 p-4 overflow-auto not-prose [tab-size:2]',
classes.root,
className
)}
>
{#if source}
<pre class={cls('whitespace-normal overflow-auto', classes.pre)}>
<code class={cls('text-sm', classes.code)}>
{#await highlighter}
<div>Loading...</div>
{:then h}
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html h.codeToHtml(source, {
lang: language,
themes: {
light: 'github-light-default',
dark: 'github-dark-default'
}
})}
{:catch error}
<div class="text-red-500">Error loading code highlighting: {error.message}</div>
{/await}

<div class={cls('rounded-sm overflow-hidden', classes.root, className)}>
{#if title}
<div class="text-sm font-bold text-surface-content/50 p-2 flex gap-2 items-end bg-surface-100">
{#if language === 'css'}
<Icon data={SimpleIconsCss} />
{:else if language === 'javascript'}
<Icon data={SimpleIconsJavascript} />
{:else if language === 'ts'}
<Icon data={SimpleIconsTypescript} />
{:else if language === 'json'}
<Icon data={SimpleIconsJson} />
{:else if language === 'sh' || language === 'bash'}
<Icon data={SimpleIconsTerminal} />
{:else if language === 'svelte'}
<Icon data={SimpleIconsSvelte} />
{:else if language === 'html'}
<Icon data={SimpleIconsHTML5} />
{:else}
<span class="red">Icon ERROR: {language}</span>
{/if}
{title}
</div>
{/if}
<div
class={cls(
'Code',
'relative bg-surface-200 dark:bg-surface-300 p-4 overflow-auto not-prose [tab-size:2]',
copyButton === 'hover' && 'group'
)}
>
{#if source}
<pre class={cls('whitespace-normal overflow-auto', classes.pre)}>
<code class={cls('text-sm', classes.code)}>
{#await highlighter}
<div>Loading...</div>
{:then h}
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html h.codeToHtml(source, {
lang: language,
themes: {
light: 'github-light-default',
dark: 'github-dark-default'
}
})}
{:catch error}
<div class="text-red-500">Error loading code highlighting: {error.message}</div>
{/await}

</code>
</pre>

{#if includeCopyButton}
<div class="absolute top-0 right-0 p-2 z-10">
<CopyButton
value={source ?? ''}
class="text-surface-content/70 hover:bg-surface-100/20 py-1 backdrop-blur-md"
size="sm"
/>
</div>
{#if copyButton !== false}
<div
class={cls(
'absolute top-0 right-0 p-2 z-10',
copyButton === 'hover' && 'opacity-0 group-hover:opacity-100 transition-opacity'
)}
>
<CopyButton
value={source ?? ''}
class="text-surface-content/70 hover:bg-surface-100/20 py-1 backdrop-blur-md"
size="sm"
/>
</div>
{/if}
{/if}
{/if}
</div>
</div>

<style>
Expand Down
6 changes: 3 additions & 3 deletions docs/src/lib/components/Example.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
<div
class={cls(
variant === 'default' && 'p-4 rounded bg-surface-200 shadow-lg',
canResize && 'outline resize-x overflow-hidden max-w-full'
canResize && 'resize-x overflow-hidden max-w-full'
)}
>
<!-- {#if page.params.example} -->
Expand All @@ -103,8 +103,8 @@
</div>

{#if showCode}
<div transition:slide class="border border-t-0">
<Code source={example.source} />
<div transition:slide class={cls('border border-t-0', showCode && 'rounded-b-sm')}>
<Code source={example.source} class="outline-none" />
</div>
{/if}

Expand Down
67 changes: 67 additions & 0 deletions docs/src/lib/components/Steps.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<script lang="ts">
import type { Snippet } from 'svelte';

let props = $props();

const stepSnippetMap = $derived(
Object.keys(props)
.filter(
(key) => key.startsWith('step') && typeof props[key as keyof typeof props] === 'function'
)
.reduce(
(map, key) => {
const isTitle = key.endsWith('Title');
const stepNum = parseInt(key.replace('step', '').replace('Title', '')) || 0;

if (!map[stepNum]) {
map[stepNum] = { title: undefined, content: undefined };
}

map[stepNum][isTitle ? 'title' : 'content'] = props[key as keyof typeof props] as Snippet;

return map;
},
{} as Record<number, { title?: Snippet; content?: Snippet }>
)
);
</script>

<div class="steps grid grid-cols-[50px_1fr] mt-6" style="counter-reset: section">
{#each Array(Object.keys(stepSnippetMap).length) as _, i}
{@render step(stepSnippetMap[i + 1].title, stepSnippetMap[i + 1].content)}
{/each}
</div>

{#snippet step(titleSnippet: Snippet | undefined, stepSnippet?: Snippet)}
<div class="left flex flex-col items-center">
<div class="circle relative bg-surface-content/20 outline shadow-md rounded-full size-6"></div>
<div class="line bg-surface-content/10 w-px flex-1"></div>
</div>
<div
class="right content ml-4 pb-2.5 [&_a]:text-primary [&_a]:font-semibold [&_a]:decoration-primary/50 [&_a:hover]:underline [&_a:hover]:underline-offset-2"
>
{#if titleSnippet}
<h2 class="text-lg font-bold">
{@render titleSnippet()}
</h2>
{/if}
{@render stepContent(stepSnippet)}
</div>
{/snippet}

{#snippet stepContent(stepSnippet?: Snippet)}
{@render stepSnippet?.()}
{/snippet}

<style>
.circle::before {
@apply absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2;
counter-increment: section;
content: counter(section);
}

/* override last one - target the second-to-last child (last left div) */
.left:nth-last-child(2) .circle::before {
content: '✔︎';
}
</style>
58 changes: 58 additions & 0 deletions docs/src/lib/components/Tabs.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import { Tabs, Tab } from 'svelte-ux';
import { cls } from '@layerstack/tailwind';

interface Props {
value?: number; // 0-indexed, starting tab index
keys: string[];
content?: Snippet<[number]>;
placement?: 'top' | 'left' | 'right' | 'bottom';
activeClass?: string;
classes?: {
root?: string;
tabs?: string;
tab?: string;
content?: string;
};
}

let {
value = $bindable(0),
keys,
content,
placement = 'top',
activeClass = 'bg-surface-100 border-b-surface-100' /* assuming title header is present */,
classes = {} as any
}: Props = $props();

const classDefaults = {
root: 'mt-2',
tabs: 'rounded-t',
tab: 'rounded-t',
content: 'p-4 border rounded-b rounded-tr'
};

const mergedClasses = {
root: cls(classDefaults.root, classes.root),
tabs: cls(classDefaults.tabs, classes.tabs),
tab: { root: cls(classDefaults.tab, classes.tab) },
content: cls(classDefaults.content, classes.content)
};
</script>

<Tabs {placement} classes={mergedClasses} bind:value>
{#each keys as key, v}
{@const isActive = value === v}
<Tab
class={cls(mergedClasses.tabs, isActive && activeClass)}
on:click={() => (value = v)}
selected={isActive}>{key}</Tab
>
{/each}
<svelte:fragment slot="content" let:value>
<div class="">
{@render content?.(value)}
</div>
</svelte:fragment>
</Tabs>
Loading
Loading