Skip to content

Commit ce17875

Browse files
authored
Markdown components (#740)
* Improve Tabs/Tab component to sync state if `::tabs{key="foo"}` is used * Add :button markdown component * Rename remarkDirectives to remarkComponents * Add :example markdown component * Migrate getting started to new markdown components * Update lockfile * Fix scale page (array variabe access broke in markdown) * Update markdown icon loading to utilize unplugin-icons import/components instead of Icon component with runtime iconify loading * Fix typo * Fix Skeleton icon color * Improve framework tab organization * Fix alignment of source/stackblitz buttons * Remove unused `rehypeCodeBlockTitle` and rename `rehypeHandleCodeBlocks` to `rehypeCodeBlocks` * Add file type icons and improve title handling * Tweak styling of steps and paragraphs * Add copy code buttons to markdown code blocks * Adjust some padding * Add `showLineNumbers` styling and improve code block styling (handle within pre.svelte) * More styling refinements
1 parent 79cdf2f commit ce17875

File tree

21 files changed

+706
-503
lines changed

21 files changed

+706
-503
lines changed

docs/content-collections.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ const components = defineCollection({
4242
}
4343

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

4750
return {
4851
...doc,
@@ -133,7 +136,10 @@ const utils = defineCollection({
133136
}
134137

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

138144
return {
139145
...doc,

docs/mdsx.config.js

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,24 @@ import rehypePrettyCode from 'rehype-pretty-code';
77

88
import {
99
prettyCodeOptions,
10-
rehypeCodeBlockTitle,
11-
rehypeHandleCodeBlocks,
10+
rehypeCodeBlocks,
1211
remarkLiveCode,
13-
remarkDirectives
12+
remarkComponents
1413
} from './src/lib/markdown/config/index.js';
1514

1615
export const mdsxConfig = defineConfig({
1716
extensions: ['.md'],
1817
remarkPlugins: [
1918
remarkGfm,
2019
remarkMDC, // Parse MDC syntax (::component, :::component)
21-
remarkDirectives, // Transform MDC components to Svelte components
20+
remarkComponents, // Transform MDC components to Svelte components
2221
remarkLiveCode
2322
],
2423
rehypePlugins: [
2524
rehypeSlug,
2625
// rehypeComponentExample,
2726
[rehypePrettyCode, prettyCodeOptions],
28-
rehypeCodeBlockTitle,
29-
rehypeHandleCodeBlocks
27+
rehypeCodeBlocks
3028
],
3129
blueprints: {
3230
default: {

docs/src/app.css

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,9 @@ code:not(pre > code):not(.custom) {
7878

7979
/* Code block figure container */
8080
figure[data-rehype-pretty-code-figure] {
81-
@apply rounded-lg outline outline-surface-content/20 dark:outline-surface-content/10 overflow-hidden;
82-
83-
/* Title/filename display */
81+
/* Title/filename display - hidden, handled by pre.svelte component */
8482
& figcaption[data-rehype-pretty-code-title] {
85-
@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;
83+
@apply hidden;
8684
}
8785

8886
/* Pre element within figure */
@@ -110,6 +108,18 @@ pre {
110108
}
111109
}
112110

111+
/* Line numbers */
112+
code[data-line-numbers] {
113+
counter-reset: line;
114+
}
115+
116+
code[data-line-numbers] > [data-line]::before {
117+
counter-increment: line;
118+
content: counter(line);
119+
@apply inline-block w-4 mr-6 pr-2 text-right text-surface-content/40;
120+
@apply border-r border-surface-content/10;
121+
}
122+
113123
/* Custom scrollbar styling */
114124
* {
115125
scrollbar-width: thin;
@@ -140,18 +150,18 @@ pre {
140150

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

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

151161
/* Counter circle */
152162
&::before {
153163
content: counter(step);
154-
@apply absolute size-6 -start-[45px] bg-surface-100 rounded-full;
164+
@apply absolute size-6 -left-10 bg-surface-100 rounded-full;
155165
@apply font-semibold text-sm tabular-nums;
156166
@apply inline-flex items-center justify-center;
157167
@apply ring-1 ring-surface-content/20;
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<script lang="ts">
2+
import type { Snippet, ComponentProps } from 'svelte';
3+
import { cls } from '@layerstack/tailwind';
4+
import { Button } from 'svelte-ux';
5+
6+
interface Props extends ComponentProps<Button> {
7+
children: Snippet;
8+
}
9+
10+
const {
11+
label,
12+
icon,
13+
href,
14+
variant = 'fill-light',
15+
size = 'md',
16+
class: className,
17+
children,
18+
...restProps
19+
}: Props = $props();
20+
21+
const internal = $derived(href?.startsWith('/') || href?.startsWith('#'));
22+
const rel = $derived(!internal ? 'noopener noreferrer' : undefined);
23+
const target = $derived(!internal ? '_blank' : undefined);
24+
</script>
25+
26+
<Button
27+
{href}
28+
{target}
29+
{rel}
30+
{icon}
31+
{variant}
32+
{size}
33+
class={cls('button', className)}
34+
{...restProps}
35+
>
36+
{@render children?.()}
37+
{label}
38+
</Button>

docs/src/lib/markdown/components/Icon.svelte

Lines changed: 0 additions & 42 deletions
This file was deleted.

docs/src/lib/markdown/components/LiveCode.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
</script>
1111

1212
<div class="live-code">
13-
<div class="live-code-preview p-6 border border-t rounded-t-lg">
13+
<div class="live-code-preview p-6 border border-t border-surface-content/10 rounded-lg">
1414
{@render preview()}
1515
</div>
1616
{@render children()}

docs/src/lib/markdown/components/Note.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
class={cls(
2828
'border border-l-[6px] px-4 py-2 my-4 rounded-sm flex items-center gap-2 text-sm',
2929
'bg-(--color)/10 border-(--color)/50',
30+
'[*&>p]:my-2',
3031
className
3132
)}
3233
style:--color={color}

docs/src/lib/markdown/components/Tab.svelte

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,28 @@
77
interface Props extends HTMLAttributes<HTMLDivElement> {
88
children: Snippet;
99
label?: string;
10-
icon?: string | Component;
10+
icon?: Component;
1111
}
1212
1313
const { children, label, icon, class: className, ...restProps }: Props = $props();
1414
1515
const tabsContext = getContext<{
1616
activeTab: number;
1717
setActiveTab: (index: number) => void;
18-
registerTab: (label: string | undefined, icon: string | Component | undefined) => number;
18+
registerTab: (label: string | undefined, icon: Component | undefined) => number;
1919
}>('tabs');
2020
2121
// Register this tab and get its index
2222
// Use untrack to capture the initial values without creating a reactive dependency
23-
const tabIndex = tabsContext?.registerTab(untrack(() => label), untrack(() => icon)) ?? 0;
23+
const tabIndex =
24+
tabsContext?.registerTab(
25+
untrack(() => label),
26+
untrack(() => icon)
27+
) ?? 0;
2428
2529
const isActive = $derived(tabsContext?.activeTab === tabIndex);
2630
</script>
2731

28-
<div class={cls('tab-content', !isActive && 'hidden', className)} {...restProps}>
32+
<div class={cls('tab', !isActive && 'hidden', className)} {...restProps}>
2933
{@render children?.()}
3034
</div>
Lines changed: 58 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
<script lang="ts" module>
2+
// Shared state for synchronized tabs across instances
3+
class TabsState {
4+
activeLabel = $state<string | null>(null);
5+
}
6+
7+
const syncedTabs = new Map<string, TabsState>();
8+
9+
function getSyncedState(key: string) {
10+
if (!syncedTabs.has(key)) {
11+
syncedTabs.set(key, new TabsState());
12+
}
13+
return syncedTabs.get(key)!;
14+
}
15+
</script>
16+
117
<script lang="ts">
218
import type { Snippet, Component } from 'svelte';
319
import type { HTMLAttributes } from 'svelte/elements';
@@ -6,55 +22,56 @@
622
723
interface Props extends HTMLAttributes<HTMLDivElement> {
824
children: Snippet;
25+
key?: string;
926
}
1027
11-
const { children, class: className, ...restProps }: Props = $props();
28+
const { children, key, class: className, ...restProps }: Props = $props();
29+
30+
// Use synced state if key is provided, otherwise use local state
31+
let localActiveIndex = $state(0);
32+
let syncedState = $derived(key ? getSyncedState(key) : null);
1233
13-
let activeTab = $state(0);
14-
let tabs = $state<Array<{ label?: string; icon?: string | Component }>>([]);
34+
let tabs = $state<Array<{ label?: string; icon?: Component }>>([]);
1535
let tabCounter = 0;
1636
37+
// Compute active tab index based on synced label or local index
38+
let activeTab = $derived.by(() => {
39+
if (syncedState && syncedState.activeLabel !== null) {
40+
// Find the index of the tab with the matching label
41+
const index = tabs.findIndex((tab) => tab.label === syncedState.activeLabel);
42+
return index >= 0 ? index : 0;
43+
}
44+
return localActiveIndex;
45+
});
46+
47+
// Initialize synced state with first tab's label if not already set
48+
$effect(() => {
49+
if (syncedState && syncedState.activeLabel === null && tabs.length > 0) {
50+
syncedState.activeLabel = tabs[0].label ?? null;
51+
}
52+
});
53+
1754
// Provide context for Tab components to register themselves
1855
setContext('tabs', {
1956
get activeTab() {
2057
return activeTab;
2158
},
2259
setActiveTab: (index: number) => {
23-
activeTab = index;
60+
if (syncedState) {
61+
syncedState.activeLabel = tabs[index]?.label ?? null;
62+
} else {
63+
localActiveIndex = index;
64+
}
2465
},
25-
registerTab: (label: string | undefined, icon: string | Component | undefined) => {
66+
registerTab: (label: string | undefined, icon: Component | undefined) => {
2667
const index = tabCounter++;
2768
tabs = [...tabs, { label, icon }];
2869
return index;
2970
}
3071
});
31-
32-
// Convert i-collection-name format to collection:name format for Iconify
33-
function getIconifyName(name: string): string {
34-
// If already in collection:name format, use as-is
35-
if (name.includes(':')) return name;
36-
// Convert i-collection-name to collection:name
37-
const match = name.match(/^i-([^-]+)-(.+)$/);
38-
if (match) {
39-
const [, collection, iconName] = match;
40-
return `${collection}:${iconName.replace(/-/g, '-')}`;
41-
}
42-
return name;
43-
}
44-
45-
// Check if any tabs have string icons (need Iconify)
46-
const hasIconifyIcons = $derived(tabs.some((tab) => typeof tab.icon === 'string'));
4772
</script>
4873

49-
<svelte:head>
50-
<!-- Load Iconify web component if needed -->
51-
{#if hasIconifyIcons}
52-
<script src="https://code.iconify.design/iconify-icon/2.1.0/iconify-icon.min.js">
53-
</script>
54-
{/if}
55-
</svelte:head>
56-
57-
<div class={cls('tabs mt-4 flex flex-col', className)} {...restProps}>
74+
<div class={cls('tabs flex my-5 flex-col', className)} {...restProps}>
5875
<!-- Tabs -->
5976
<div class="flex gap-1 overflow-auto z-1 -mb-px">
6077
{#each tabs as tab, index}
@@ -67,25 +84,26 @@
6784
? 'bg-surface-100 text-surface-content border-b-surface-100'
6885
: 'bg-surface-200 text-surface-content/50 hover:text-surface-content'
6986
)}
70-
onclick={() => (activeTab = index)}
87+
onclick={() => {
88+
if (syncedState) {
89+
syncedState.activeLabel = tab.label ?? null;
90+
} else {
91+
localActiveIndex = index;
92+
}
93+
}}
7194
>
7295
{#if tab.icon}
73-
{#if typeof tab.icon === 'string'}
74-
<!-- Iconify web component -->
75-
<iconify-icon icon={getIconifyName(tab.icon)} class="size-4"></iconify-icon>
76-
{:else}
77-
<!-- Component import (dynamic by default in runes mode) -->
78-
{@const IconComponent = tab.icon}
79-
<IconComponent class="size-4" />
80-
{/if}
96+
<!-- unplugin-icons component -->
97+
{@const IconComponent = tab.icon}
98+
<IconComponent class="size-4 fill-surface-content" />
8199
{/if}
82100
{tab.label || `Tab ${index + 1}`}
83101
</button>
84102
{/each}
85103
</div>
86104

87105
<!-- Tab content -->
88-
<div class="border rounded-lg rounded-tl-none p-3 bg-surface-100">
106+
<div class="border rounded-lg rounded-tl-none px-3 py-1 bg-surface-100">
89107
{@render children?.()}
90108
</div>
91109
</div>

docs/src/lib/markdown/components/p.svelte

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,6 @@
55
let { class: className, children, ...restProps }: HTMLAttributes<HTMLParagraphElement> = $props();
66
</script>
77

8-
<p
9-
class={cls(
10-
'text-surface-content [main>&:not(:first-child)]:mt-6 ml-2 leading-relaxed',
11-
className
12-
)}
13-
{...restProps}
14-
>
8+
<p class={cls('text-surface-content first:mt-0 my-5 leading-relaxed', className)} {...restProps}>
159
{@render children?.()}
1610
</p>

0 commit comments

Comments
 (0)