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
10 changes: 8 additions & 2 deletions docs/content-collections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ const components = defineCollection({
}

// Extract the first Example component's name from the markdown content
const usageExample = doc.content.match(/<Example\s+[^>]*name=["']([^"']+)["'][^>]*>/)?.[1];
// Support both <Example name="..."> and :example{name="..."} syntax
const usageExample =
doc.content.match(/<Example\s+[^>]*name=["']([^"']+)["'][^>]*>/)?.[1] ||
doc.content.match(/:example\{[^}]*name=["']([^"']+)["'][^}]*\}/)?.[1];

return {
...doc,
Expand Down Expand Up @@ -133,7 +136,10 @@ const utils = defineCollection({
}

// Extract the first Example component's name from the markdown content
const usageExample = doc.content.match(/<Example\s+[^>]*name=["']([^"']+)["'][^>]*>/)?.[1];
// Support both <Example name="..."> and :example{name="..."} syntax
const usageExample =
doc.content.match(/<Example\s+[^>]*name=["']([^"']+)["'][^>]*>/)?.[1] ||
doc.content.match(/:example\{[^}]*name=["']([^"']+)["'][^}]*\}/)?.[1];

return {
...doc,
Expand Down
10 changes: 4 additions & 6 deletions docs/mdsx.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,24 @@ import rehypePrettyCode from 'rehype-pretty-code';

import {
prettyCodeOptions,
rehypeCodeBlockTitle,
rehypeHandleCodeBlocks,
rehypeCodeBlocks,
remarkLiveCode,
remarkDirectives
remarkComponents
} from './src/lib/markdown/config/index.js';

export const mdsxConfig = defineConfig({
extensions: ['.md'],
remarkPlugins: [
remarkGfm,
remarkMDC, // Parse MDC syntax (::component, :::component)
remarkDirectives, // Transform MDC components to Svelte components
remarkComponents, // Transform MDC components to Svelte components
remarkLiveCode
],
rehypePlugins: [
rehypeSlug,
// rehypeComponentExample,
[rehypePrettyCode, prettyCodeOptions],
rehypeCodeBlockTitle,
rehypeHandleCodeBlocks
rehypeCodeBlocks
],
blueprints: {
default: {
Expand Down
24 changes: 17 additions & 7 deletions docs/src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,9 @@ code:not(pre > code):not(.custom) {

/* Code block figure container */
figure[data-rehype-pretty-code-figure] {
@apply rounded-lg outline outline-surface-content/20 dark:outline-surface-content/10 overflow-hidden;

/* Title/filename display */
/* Title/filename display - hidden, handled by pre.svelte component */
& figcaption[data-rehype-pretty-code-title] {
@apply text-sm font-mono font-medium leading-tight text-surface-content/50 border-b border-surface-content/20 dark:border-surface-content/10 px-4 py-2 bg-surface-100;
@apply hidden;
}

/* Pre element within figure */
Expand Down Expand Up @@ -110,6 +108,18 @@ pre {
}
}

/* Line numbers */
code[data-line-numbers] {
counter-reset: line;
}

code[data-line-numbers] > [data-line]::before {
counter-increment: line;
content: counter(line);
@apply inline-block w-4 mr-6 pr-2 text-right text-surface-content/40;
@apply border-r border-surface-content/10;
}

/* Custom scrollbar styling */
* {
scrollbar-width: thin;
Expand Down Expand Up @@ -140,18 +150,18 @@ pre {

/* Steps component styling - inspired by Docus */
.steps {
@apply ms-4 ps-8 border-l border-surface-content/10;
@apply ms-4 pl-7 border-l border-surface-content/10;
counter-reset: step;

/* Headings (h2, h3, h4) in steps */
& :is(h2, h3, h4) {
counter-increment: step;
@apply relative font-semibold text-base mb-2 mt-6 first:mt-0;
@apply relative font-semibold text-lg mb-2 mt-6 first:mt-0;

/* Counter circle */
&::before {
content: counter(step);
@apply absolute size-6 -start-[45px] bg-surface-100 rounded-full;
@apply absolute size-6 -left-10 bg-surface-100 rounded-full;
@apply font-semibold text-sm tabular-nums;
@apply inline-flex items-center justify-center;
@apply ring-1 ring-surface-content/20;
Expand Down
38 changes: 38 additions & 0 deletions docs/src/lib/markdown/components/Button.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<script lang="ts">
import type { Snippet, ComponentProps } from 'svelte';
import { cls } from '@layerstack/tailwind';
import { Button } from 'svelte-ux';

interface Props extends ComponentProps<Button> {
children: Snippet;
}

const {
label,
icon,
href,
variant = 'fill-light',
size = 'md',
class: className,
children,
...restProps
}: Props = $props();

const internal = $derived(href?.startsWith('/') || href?.startsWith('#'));
const rel = $derived(!internal ? 'noopener noreferrer' : undefined);
const target = $derived(!internal ? '_blank' : undefined);
</script>

<Button
{href}
{target}
{rel}
{icon}
{variant}
{size}
class={cls('button', className)}
{...restProps}
>
{@render children?.()}
{label}
</Button>
42 changes: 0 additions & 42 deletions docs/src/lib/markdown/components/Icon.svelte

This file was deleted.

2 changes: 1 addition & 1 deletion docs/src/lib/markdown/components/LiveCode.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
</script>

<div class="live-code">
<div class="live-code-preview p-6 border border-t rounded-t-lg">
<div class="live-code-preview p-6 border border-t border-surface-content/10 rounded-lg">
{@render preview()}
</div>
{@render children()}
Expand Down
1 change: 1 addition & 0 deletions docs/src/lib/markdown/components/Note.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
class={cls(
'border border-l-[6px] px-4 py-2 my-4 rounded-sm flex items-center gap-2 text-sm',
'bg-(--color)/10 border-(--color)/50',
'[*&>p]:my-2',
className
)}
style:--color={color}
Expand Down
12 changes: 8 additions & 4 deletions docs/src/lib/markdown/components/Tab.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,28 @@
interface Props extends HTMLAttributes<HTMLDivElement> {
children: Snippet;
label?: string;
icon?: string | Component;
icon?: Component;
}

const { children, label, icon, class: className, ...restProps }: Props = $props();

const tabsContext = getContext<{
activeTab: number;
setActiveTab: (index: number) => void;
registerTab: (label: string | undefined, icon: string | Component | undefined) => number;
registerTab: (label: string | undefined, icon: Component | undefined) => number;
}>('tabs');

// Register this tab and get its index
// Use untrack to capture the initial values without creating a reactive dependency
const tabIndex = tabsContext?.registerTab(untrack(() => label), untrack(() => icon)) ?? 0;
const tabIndex =
tabsContext?.registerTab(
untrack(() => label),
untrack(() => icon)
) ?? 0;

const isActive = $derived(tabsContext?.activeTab === tabIndex);
</script>

<div class={cls('tab-content', !isActive && 'hidden', className)} {...restProps}>
<div class={cls('tab', !isActive && 'hidden', className)} {...restProps}>
{@render children?.()}
</div>
98 changes: 58 additions & 40 deletions docs/src/lib/markdown/components/Tabs.svelte
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
<script lang="ts" module>
// Shared state for synchronized tabs across instances
class TabsState {
activeLabel = $state<string | null>(null);
}

const syncedTabs = new Map<string, TabsState>();

function getSyncedState(key: string) {
if (!syncedTabs.has(key)) {
syncedTabs.set(key, new TabsState());
}
return syncedTabs.get(key)!;
}
</script>

<script lang="ts">
import type { Snippet, Component } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
Expand All @@ -6,55 +22,56 @@

interface Props extends HTMLAttributes<HTMLDivElement> {
children: Snippet;
key?: string;
}

const { children, class: className, ...restProps }: Props = $props();
const { children, key, class: className, ...restProps }: Props = $props();

// Use synced state if key is provided, otherwise use local state
let localActiveIndex = $state(0);
let syncedState = $derived(key ? getSyncedState(key) : null);

let activeTab = $state(0);
let tabs = $state<Array<{ label?: string; icon?: string | Component }>>([]);
let tabs = $state<Array<{ label?: string; icon?: Component }>>([]);
let tabCounter = 0;

// Compute active tab index based on synced label or local index
let activeTab = $derived.by(() => {
if (syncedState && syncedState.activeLabel !== null) {
// Find the index of the tab with the matching label
const index = tabs.findIndex((tab) => tab.label === syncedState.activeLabel);
return index >= 0 ? index : 0;
}
return localActiveIndex;
});

// Initialize synced state with first tab's label if not already set
$effect(() => {
if (syncedState && syncedState.activeLabel === null && tabs.length > 0) {
syncedState.activeLabel = tabs[0].label ?? null;
}
});

// Provide context for Tab components to register themselves
setContext('tabs', {
get activeTab() {
return activeTab;
},
setActiveTab: (index: number) => {
activeTab = index;
if (syncedState) {
syncedState.activeLabel = tabs[index]?.label ?? null;
} else {
localActiveIndex = index;
}
},
registerTab: (label: string | undefined, icon: string | Component | undefined) => {
registerTab: (label: string | undefined, icon: Component | undefined) => {
const index = tabCounter++;
tabs = [...tabs, { label, icon }];
return index;
}
});

// Convert i-collection-name format to collection:name format for Iconify
function getIconifyName(name: string): string {
// If already in collection:name format, use as-is
if (name.includes(':')) return name;
// Convert i-collection-name to collection:name
const match = name.match(/^i-([^-]+)-(.+)$/);
if (match) {
const [, collection, iconName] = match;
return `${collection}:${iconName.replace(/-/g, '-')}`;
}
return name;
}

// Check if any tabs have string icons (need Iconify)
const hasIconifyIcons = $derived(tabs.some((tab) => typeof tab.icon === 'string'));
</script>

<svelte:head>
<!-- Load Iconify web component if needed -->
{#if hasIconifyIcons}
<script src="https://code.iconify.design/iconify-icon/2.1.0/iconify-icon.min.js">
</script>
{/if}
</svelte:head>

<div class={cls('tabs mt-4 flex flex-col', className)} {...restProps}>
<div class={cls('tabs flex my-5 flex-col', className)} {...restProps}>
<!-- Tabs -->
<div class="flex gap-1 overflow-auto z-1 -mb-px">
{#each tabs as tab, index}
Expand All @@ -67,25 +84,26 @@
? 'bg-surface-100 text-surface-content border-b-surface-100'
: 'bg-surface-200 text-surface-content/50 hover:text-surface-content'
)}
onclick={() => (activeTab = index)}
onclick={() => {
if (syncedState) {
syncedState.activeLabel = tab.label ?? null;
} else {
localActiveIndex = index;
}
}}
>
{#if tab.icon}
{#if typeof tab.icon === 'string'}
<!-- Iconify web component -->
<iconify-icon icon={getIconifyName(tab.icon)} class="size-4"></iconify-icon>
{:else}
<!-- Component import (dynamic by default in runes mode) -->
{@const IconComponent = tab.icon}
<IconComponent class="size-4" />
{/if}
<!-- unplugin-icons component -->
{@const IconComponent = tab.icon}
<IconComponent class="size-4 fill-surface-content" />
{/if}
{tab.label || `Tab ${index + 1}`}
</button>
{/each}
</div>

<!-- Tab content -->
<div class="border rounded-lg rounded-tl-none p-3 bg-surface-100">
<div class="border rounded-lg rounded-tl-none px-3 py-1 bg-surface-100">
{@render children?.()}
</div>
</div>
8 changes: 1 addition & 7 deletions docs/src/lib/markdown/components/p.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,6 @@
let { class: className, children, ...restProps }: HTMLAttributes<HTMLParagraphElement> = $props();
</script>

<p
class={cls(
'text-surface-content [main>&:not(:first-child)]:mt-6 ml-2 leading-relaxed',
className
)}
{...restProps}
>
<p class={cls('text-surface-content first:mt-0 my-5 leading-relaxed', className)} {...restProps}>
{@render children?.()}
</p>
Loading
Loading