@refrakt-md/svelte 0.5.1 → 0.7.0
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/package.json +4 -3
- package/src/Renderer.svelte +1 -1
- package/src/ThemeShell.svelte +41 -9
- package/src/behaviors.ts +15 -0
- package/src/elements/Pre.svelte +77 -0
- package/src/elements/Table.svelte +40 -0
- package/src/elements.ts +9 -0
- package/src/index.ts +4 -0
- package/src/registry.ts +22 -0
- package/src/theme.ts +13 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@refrakt-md/svelte",
|
|
3
3
|
"description": "Svelte renderer for refrakt.md content",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.7.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -27,8 +27,9 @@
|
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@markdoc/markdoc": "0.4.0",
|
|
30
|
-
"@refrakt-md/behaviors": "0.
|
|
31
|
-
"@refrakt-md/
|
|
30
|
+
"@refrakt-md/behaviors": "0.7.0",
|
|
31
|
+
"@refrakt-md/transform": "0.7.0",
|
|
32
|
+
"@refrakt-md/types": "0.7.0"
|
|
32
33
|
},
|
|
33
34
|
"peerDependencies": {
|
|
34
35
|
"svelte": "^5.0.0"
|
package/src/Renderer.svelte
CHANGED
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
{:else}
|
|
80
80
|
<svelte:element this={node.name} {...htmlAttrs(node.attributes)}>
|
|
81
81
|
{#each node.children as child}
|
|
82
|
-
{#if node.attributes?.['data-codeblock'] && typeof child === 'string'}
|
|
82
|
+
{#if (node.attributes?.['data-codeblock'] || node.attributes?.['data-raw-html']) && typeof child === 'string'}
|
|
83
83
|
{@html child}
|
|
84
84
|
{:else}
|
|
85
85
|
<Renderer node={child} overrides={merged} />
|
package/src/ThemeShell.svelte
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { SvelteTheme } from './theme.js';
|
|
3
|
+
import { isLayoutConfig } from './theme.js';
|
|
3
4
|
import { setRegistry, setElementOverrides } from './context.js';
|
|
4
5
|
import { setContext, tick } from 'svelte';
|
|
5
6
|
import { matchRouteRule } from './route-rules.js';
|
|
6
|
-
import { initRuneBehaviors } from '@refrakt-md/behaviors';
|
|
7
|
+
import { initRuneBehaviors, initLayoutBehaviors, registerElements, RfContext } from '@refrakt-md/behaviors';
|
|
8
|
+
import { layoutTransform } from '@refrakt-md/transform';
|
|
9
|
+
import Renderer from './Renderer.svelte';
|
|
7
10
|
|
|
8
11
|
interface OgMeta {
|
|
9
12
|
title?: string;
|
|
@@ -33,9 +36,12 @@
|
|
|
33
36
|
author?: string;
|
|
34
37
|
tags?: string[];
|
|
35
38
|
image?: string;
|
|
39
|
+
version?: string;
|
|
40
|
+
versionGroup?: string;
|
|
36
41
|
}>;
|
|
37
42
|
url: string;
|
|
38
43
|
seo?: PageSeo;
|
|
44
|
+
headings?: Array<{ level: number; text: string; id: string }>;
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
let { theme, page }: { theme: SvelteTheme; page: PageData } = $props();
|
|
@@ -47,25 +53,47 @@
|
|
|
47
53
|
if (theme.elements) setElementOverrides(theme.elements);
|
|
48
54
|
// svelte-ignore state_referenced_locally
|
|
49
55
|
setContext('pages', page.pages);
|
|
56
|
+
// svelte-ignore state_referenced_locally
|
|
57
|
+
setContext('currentUrl', page.url);
|
|
58
|
+
|
|
59
|
+
// Populate RfContext for framework-neutral web components (Nav, Sandbox, etc.)
|
|
60
|
+
// svelte-ignore state_referenced_locally
|
|
61
|
+
RfContext.pages = page.pages;
|
|
62
|
+
// svelte-ignore state_referenced_locally
|
|
63
|
+
RfContext.currentUrl = page.url;
|
|
64
|
+
registerElements();
|
|
50
65
|
|
|
51
66
|
// Pick layout via route rules (reactive so layout updates on client-side navigation)
|
|
52
|
-
const layoutName = $derived(matchRouteRule(page.url, theme.manifest.routeRules));
|
|
53
|
-
const
|
|
67
|
+
const layoutName = $derived(matchRouteRule(page.url, theme.manifest.routeRules ?? []));
|
|
68
|
+
const layoutEntry = $derived(theme.layouts[layoutName] ?? theme.layouts['default']);
|
|
69
|
+
|
|
70
|
+
// Keep RfContext in sync for web components BEFORE DOM updates.
|
|
71
|
+
// $effect.pre runs before {#key page.url} recreates the DOM, so
|
|
72
|
+
// RfNav.connectedCallback sees the new URL when it fires.
|
|
73
|
+
$effect.pre(() => {
|
|
74
|
+
RfContext.pages = page.pages;
|
|
75
|
+
RfContext.currentUrl = page.url;
|
|
76
|
+
});
|
|
54
77
|
|
|
55
|
-
// Initialize rune behaviors after render, re-run on navigation.
|
|
78
|
+
// Initialize rune + layout behaviors after render, re-run on navigation.
|
|
56
79
|
// The {#key page.url} block in the template ensures full DOM recreation on
|
|
57
80
|
// navigation, so behaviors always run on fresh DOM and old behavior-modified
|
|
58
81
|
// elements are simply discarded (no cleanup/restore conflicts with Svelte).
|
|
59
82
|
$effect(() => {
|
|
60
83
|
void page.url; // re-run when page changes
|
|
61
|
-
let
|
|
84
|
+
let cleanupRunes: (() => void) | undefined;
|
|
85
|
+
let cleanupLayout: (() => void) | undefined;
|
|
62
86
|
let active = true;
|
|
63
87
|
tick().then(() => {
|
|
64
|
-
if (active)
|
|
88
|
+
if (active) {
|
|
89
|
+
cleanupRunes = initRuneBehaviors();
|
|
90
|
+
cleanupLayout = initLayoutBehaviors();
|
|
91
|
+
}
|
|
65
92
|
});
|
|
66
93
|
return () => {
|
|
67
94
|
active = false;
|
|
68
|
-
|
|
95
|
+
cleanupRunes?.();
|
|
96
|
+
cleanupLayout?.();
|
|
69
97
|
};
|
|
70
98
|
});
|
|
71
99
|
</script>
|
|
@@ -101,8 +129,11 @@
|
|
|
101
129
|
</svelte:head>
|
|
102
130
|
|
|
103
131
|
{#key page.url}
|
|
104
|
-
{#if
|
|
105
|
-
|
|
132
|
+
{#if isLayoutConfig(layoutEntry)}
|
|
133
|
+
{@const tree = layoutTransform(layoutEntry, page, 'rf')}
|
|
134
|
+
<Renderer node={tree} />
|
|
135
|
+
{:else if layoutEntry}
|
|
136
|
+
<svelte:component this={layoutEntry}
|
|
106
137
|
title={page.title}
|
|
107
138
|
description={page.description}
|
|
108
139
|
frontmatter={page.frontmatter}
|
|
@@ -110,6 +141,7 @@
|
|
|
110
141
|
renderable={page.renderable}
|
|
111
142
|
pages={page.pages}
|
|
112
143
|
url={page.url}
|
|
144
|
+
headings={page.headings}
|
|
113
145
|
/>
|
|
114
146
|
{/if}
|
|
115
147
|
{/key}
|
package/src/behaviors.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { initRuneBehaviors } from '@refrakt-md/behaviors';
|
|
2
|
+
|
|
3
|
+
/** Svelte action that initializes rune behaviors on mounted content. */
|
|
4
|
+
export function behaviors(node: HTMLElement) {
|
|
5
|
+
let cleanup = initRuneBehaviors(node);
|
|
6
|
+
return {
|
|
7
|
+
update() {
|
|
8
|
+
cleanup();
|
|
9
|
+
cleanup = initRuneBehaviors(node);
|
|
10
|
+
},
|
|
11
|
+
destroy() {
|
|
12
|
+
cleanup();
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { SerializedTag } from '../types.js';
|
|
3
|
+
import type { Snippet } from 'svelte';
|
|
4
|
+
|
|
5
|
+
let { tag, children }: { tag: SerializedTag; children: Snippet } = $props();
|
|
6
|
+
|
|
7
|
+
const isCodeBlock = 'data-language' in (tag.attributes || {});
|
|
8
|
+
|
|
9
|
+
let preEl: HTMLPreElement;
|
|
10
|
+
let copied = $state(false);
|
|
11
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
12
|
+
|
|
13
|
+
function copy() {
|
|
14
|
+
const text = preEl.textContent ?? '';
|
|
15
|
+
navigator.clipboard.writeText(text);
|
|
16
|
+
copied = true;
|
|
17
|
+
clearTimeout(timer);
|
|
18
|
+
timer = setTimeout(() => copied = false, 2000);
|
|
19
|
+
}
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
{#if isCodeBlock}
|
|
23
|
+
<div class="rf-codeblock">
|
|
24
|
+
<pre bind:this={preEl} {...tag.attributes}>{@render children()}</pre>
|
|
25
|
+
<button class="rf-codeblock__copy" class:rf-codeblock__copy--copied={copied} onclick={copy} aria-label="Copy code">
|
|
26
|
+
{#if copied}
|
|
27
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
28
|
+
<polyline points="20 6 9 17 4 12" />
|
|
29
|
+
</svg>
|
|
30
|
+
{:else}
|
|
31
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
32
|
+
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
|
|
33
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
|
34
|
+
</svg>
|
|
35
|
+
{/if}
|
|
36
|
+
</button>
|
|
37
|
+
</div>
|
|
38
|
+
{:else}
|
|
39
|
+
<pre {...tag.attributes}>{@render children()}</pre>
|
|
40
|
+
{/if}
|
|
41
|
+
|
|
42
|
+
<style>
|
|
43
|
+
.rf-codeblock {
|
|
44
|
+
position: relative;
|
|
45
|
+
}
|
|
46
|
+
.rf-codeblock :global(pre) {
|
|
47
|
+
margin: 0;
|
|
48
|
+
}
|
|
49
|
+
.rf-codeblock__copy {
|
|
50
|
+
position: absolute;
|
|
51
|
+
top: 0.5rem;
|
|
52
|
+
right: 0.5rem;
|
|
53
|
+
display: flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
justify-content: center;
|
|
56
|
+
width: 2rem;
|
|
57
|
+
height: 2rem;
|
|
58
|
+
border: none;
|
|
59
|
+
border-radius: var(--rf-radius-sm);
|
|
60
|
+
background: transparent;
|
|
61
|
+
color: var(--rf-color-code-text, #e2e8f0);
|
|
62
|
+
cursor: pointer;
|
|
63
|
+
opacity: 0;
|
|
64
|
+
transition: opacity 150ms ease, background-color 150ms ease;
|
|
65
|
+
}
|
|
66
|
+
.rf-codeblock:hover .rf-codeblock__copy {
|
|
67
|
+
opacity: 0.6;
|
|
68
|
+
}
|
|
69
|
+
.rf-codeblock__copy:hover {
|
|
70
|
+
opacity: 1 !important;
|
|
71
|
+
background: rgba(255, 255, 255, 0.1);
|
|
72
|
+
}
|
|
73
|
+
.rf-codeblock__copy--copied {
|
|
74
|
+
opacity: 1 !important;
|
|
75
|
+
color: var(--rf-color-success, #4ade80);
|
|
76
|
+
}
|
|
77
|
+
</style>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { SerializedTag } from '../types.js';
|
|
3
|
+
import type { Snippet } from 'svelte';
|
|
4
|
+
|
|
5
|
+
let { tag, children }: { tag: SerializedTag; children: Snippet } = $props();
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<div class="table-wrapper">
|
|
9
|
+
<table {...tag.attributes}>
|
|
10
|
+
{@render children()}
|
|
11
|
+
</table>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<style>
|
|
15
|
+
.table-wrapper {
|
|
16
|
+
overflow-x: auto;
|
|
17
|
+
-webkit-overflow-scrolling: touch;
|
|
18
|
+
margin: 1.5rem 0;
|
|
19
|
+
border: 1px solid var(--rf-color-border);
|
|
20
|
+
border-radius: var(--rf-radius-md);
|
|
21
|
+
}
|
|
22
|
+
.table-wrapper :global(table) {
|
|
23
|
+
margin: 0;
|
|
24
|
+
border-collapse: collapse;
|
|
25
|
+
width: 100%;
|
|
26
|
+
min-width: 100%;
|
|
27
|
+
}
|
|
28
|
+
.table-wrapper :global(th) {
|
|
29
|
+
background: var(--rf-color-surface);
|
|
30
|
+
}
|
|
31
|
+
.table-wrapper :global(th:first-child) {
|
|
32
|
+
border-top-left-radius: var(--rf-radius-md);
|
|
33
|
+
}
|
|
34
|
+
.table-wrapper :global(th:last-child) {
|
|
35
|
+
border-top-right-radius: var(--rf-radius-md);
|
|
36
|
+
}
|
|
37
|
+
.table-wrapper :global(tr:last-child td) {
|
|
38
|
+
border-bottom: none;
|
|
39
|
+
}
|
|
40
|
+
</style>
|
package/src/elements.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ElementOverrides } from './context.js';
|
|
2
|
+
import Table from './elements/Table.svelte';
|
|
3
|
+
import Pre from './elements/Pre.svelte';
|
|
4
|
+
|
|
5
|
+
/** Base element overrides — functional enhancements for HTML elements */
|
|
6
|
+
export const elements: ElementOverrides = {
|
|
7
|
+
'table': Table,
|
|
8
|
+
'pre': Pre,
|
|
9
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -5,4 +5,8 @@ export { serialize, serializeTree } from './serialize.js';
|
|
|
5
5
|
export { setRegistry, getComponent, setElementOverrides, getElementOverrides } from './context.js';
|
|
6
6
|
export type { ComponentRegistry, ElementOverrides } from './context.js';
|
|
7
7
|
export type { SvelteTheme } from './theme.js';
|
|
8
|
+
export { isLayoutConfig } from './theme.js';
|
|
8
9
|
export { matchRouteRule } from './route-rules.js';
|
|
10
|
+
export { registry } from './registry.js';
|
|
11
|
+
export { elements } from './elements.js';
|
|
12
|
+
export { behaviors } from './behaviors.js';
|
package/src/registry.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ComponentRegistry } from './context.js';
|
|
2
|
+
|
|
3
|
+
/** Maps typeof attribute values to base theme Svelte components.
|
|
4
|
+
*
|
|
5
|
+
* All runes are now rendered purely through the identity transform engine:
|
|
6
|
+
*
|
|
7
|
+
* - Interactive runes requiring client-side lifecycle (Diagram, Map, Nav, Sandbox)
|
|
8
|
+
* are framework-neutral web components in @refrakt-md/behaviors, initialized
|
|
9
|
+
* via custom elements. Their postTransform hooks produce custom element tags.
|
|
10
|
+
*
|
|
11
|
+
* - Data rendering runes (Chart, Comparison, Embed, Testimonial, DesignContext)
|
|
12
|
+
* use postTransform hooks to generate their complete HTML structure.
|
|
13
|
+
*
|
|
14
|
+
* - Behavior-driven runes (tabs, accordion, datatable, form, reveal, preview, details)
|
|
15
|
+
* use BEM classes from the identity transform + @refrakt-md/behaviors.
|
|
16
|
+
*
|
|
17
|
+
* - Layout runes (grid, bento, storyboard, pricing) are fully handled by
|
|
18
|
+
* identity transform + CSS attribute selectors / custom properties.
|
|
19
|
+
*
|
|
20
|
+
* The registry is empty but preserved for user-defined component overrides.
|
|
21
|
+
*/
|
|
22
|
+
export const registry: ComponentRegistry = {};
|
package/src/theme.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ThemeManifest } from '@refrakt-md/types';
|
|
2
|
+
import type { LayoutConfig } from '@refrakt-md/transform';
|
|
2
3
|
import type { Component } from 'svelte';
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -7,10 +8,20 @@ import type { Component } from 'svelte';
|
|
|
7
8
|
*/
|
|
8
9
|
export interface SvelteTheme {
|
|
9
10
|
manifest: ThemeManifest;
|
|
10
|
-
/** Layout name → Svelte component */
|
|
11
|
-
layouts: Record<string, Component<any
|
|
11
|
+
/** Layout name → declarative LayoutConfig or Svelte component (legacy) */
|
|
12
|
+
layouts: Record<string, Component<any> | LayoutConfig>;
|
|
12
13
|
/** typeof name → Svelte component (the component registry) */
|
|
13
14
|
components: Record<string, Component<any>>;
|
|
14
15
|
/** HTML element name → Svelte component (element-level overrides) */
|
|
15
16
|
elements?: Record<string, Component<any>>;
|
|
16
17
|
}
|
|
18
|
+
|
|
19
|
+
/** Runtime check: LayoutConfig has a `block` string + `slots` object */
|
|
20
|
+
export function isLayoutConfig(value: unknown): value is LayoutConfig {
|
|
21
|
+
return (
|
|
22
|
+
value !== null &&
|
|
23
|
+
typeof value === 'object' &&
|
|
24
|
+
typeof (value as any).block === 'string' &&
|
|
25
|
+
typeof (value as any).slots === 'object'
|
|
26
|
+
);
|
|
27
|
+
}
|