@refrakt-md/lumina 0.4.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.
Files changed (98) hide show
  1. package/contracts/structures.json +5 -0
  2. package/dist/config.d.ts +4 -0
  3. package/dist/config.d.ts.map +1 -0
  4. package/dist/config.js +238 -0
  5. package/dist/config.js.map +1 -0
  6. package/dist/lib/engine.d.ts +13 -0
  7. package/dist/lib/engine.d.ts.map +1 -0
  8. package/dist/lib/engine.js +218 -0
  9. package/dist/lib/engine.js.map +1 -0
  10. package/dist/lib/helpers.d.ts +14 -0
  11. package/dist/lib/helpers.d.ts.map +1 -0
  12. package/dist/lib/helpers.js +26 -0
  13. package/dist/lib/helpers.js.map +1 -0
  14. package/dist/lib/types.d.ts +74 -0
  15. package/dist/lib/types.d.ts.map +1 -0
  16. package/dist/lib/types.js +2 -0
  17. package/dist/lib/types.js.map +1 -0
  18. package/dist/transform.d.ts +5 -0
  19. package/dist/transform.d.ts.map +1 -0
  20. package/dist/transform.js +7 -0
  21. package/dist/transform.js.map +1 -0
  22. package/index.css +57 -0
  23. package/manifest.json +13 -0
  24. package/package.json +62 -0
  25. package/styles/elements/blockquote.css +22 -0
  26. package/styles/elements/table.css +26 -0
  27. package/styles/global.css +138 -0
  28. package/styles/layouts/default.css +45 -0
  29. package/styles/layouts/docs.css +94 -0
  30. package/styles/runes/accordion.css +55 -0
  31. package/styles/runes/annotate.css +87 -0
  32. package/styles/runes/api.css +77 -0
  33. package/styles/runes/bento.css +48 -0
  34. package/styles/runes/breadcrumb.css +46 -0
  35. package/styles/runes/cast.css +56 -0
  36. package/styles/runes/changelog.css +53 -0
  37. package/styles/runes/chart.css +33 -0
  38. package/styles/runes/codegroup.css +72 -0
  39. package/styles/runes/compare.css +41 -0
  40. package/styles/runes/comparison.css +211 -0
  41. package/styles/runes/conversation.css +54 -0
  42. package/styles/runes/cta.css +87 -0
  43. package/styles/runes/datatable.css +112 -0
  44. package/styles/runes/details.css +44 -0
  45. package/styles/runes/diagram.css +27 -0
  46. package/styles/runes/diff.css +74 -0
  47. package/styles/runes/embed.css +32 -0
  48. package/styles/runes/event.css +57 -0
  49. package/styles/runes/feature.css +67 -0
  50. package/styles/runes/figure.css +24 -0
  51. package/styles/runes/form.css +184 -0
  52. package/styles/runes/grid.css +13 -0
  53. package/styles/runes/hero.css +102 -0
  54. package/styles/runes/hint.css +97 -0
  55. package/styles/runes/howto.css +37 -0
  56. package/styles/runes/nav.css +50 -0
  57. package/styles/runes/organization.css +26 -0
  58. package/styles/runes/page-section.css +20 -0
  59. package/styles/runes/pricing.css +109 -0
  60. package/styles/runes/recipe.css +47 -0
  61. package/styles/runes/reveal.css +55 -0
  62. package/styles/runes/sidenote.css +28 -0
  63. package/styles/runes/steps.css +59 -0
  64. package/styles/runes/storyboard.css +65 -0
  65. package/styles/runes/tabs.css +50 -0
  66. package/styles/runes/testimonial.css +59 -0
  67. package/styles/runes/timeline.css +67 -0
  68. package/styles/runes/toc.css +43 -0
  69. package/sveltekit/components/Accordion.svelte +26 -0
  70. package/sveltekit/components/Bento.svelte +50 -0
  71. package/sveltekit/components/Chart.svelte +121 -0
  72. package/sveltekit/components/CodeGroup.svelte +88 -0
  73. package/sveltekit/components/Comparison.svelte +209 -0
  74. package/sveltekit/components/DataTable.svelte +154 -0
  75. package/sveltekit/components/Details.svelte +23 -0
  76. package/sveltekit/components/Diagram.svelte +45 -0
  77. package/sveltekit/components/Embed.svelte +36 -0
  78. package/sveltekit/components/Form.svelte +194 -0
  79. package/sveltekit/components/Grid.svelte +42 -0
  80. package/sveltekit/components/Nav.svelte +62 -0
  81. package/sveltekit/components/Pricing.svelte +20 -0
  82. package/sveltekit/components/Reveal.svelte +62 -0
  83. package/sveltekit/components/Storyboard.svelte +41 -0
  84. package/sveltekit/components/Tabs.svelte +75 -0
  85. package/sveltekit/components/Testimonial.svelte +26 -0
  86. package/sveltekit/elements/Blockquote.svelte +37 -0
  87. package/sveltekit/elements/Pre.svelte +77 -0
  88. package/sveltekit/elements/Table.svelte +40 -0
  89. package/sveltekit/elements.ts +11 -0
  90. package/sveltekit/index.ts +32 -0
  91. package/sveltekit/layouts/BlogLayout.svelte +382 -0
  92. package/sveltekit/layouts/DefaultLayout.svelte +70 -0
  93. package/sveltekit/layouts/DocsLayout.svelte +133 -0
  94. package/sveltekit/manifest.json +51 -0
  95. package/sveltekit/registry.ts +59 -0
  96. package/sveltekit/tokens.css +71 -0
  97. package/tokens/base.css +78 -0
  98. package/tokens/dark.css +102 -0
@@ -0,0 +1,209 @@
1
+ <script lang="ts">
2
+ import type { SerializedTag, RendererNode } from '@refrakt-md/svelte';
3
+ import { Renderer } from '@refrakt-md/svelte';
4
+ import type { Snippet } from 'svelte';
5
+
6
+ let { tag, children }: { tag: SerializedTag; children: Snippet } = $props();
7
+
8
+ const typeName = tag.attributes.typeof;
9
+
10
+ function isTag(n: RendererNode): n is SerializedTag {
11
+ return n !== null && typeof n === 'object' && !Array.isArray(n) && (n as any).$$mdtype === 'Tag';
12
+ }
13
+
14
+ function meta(node: SerializedTag, prop: string): string {
15
+ const child = node.children.find(
16
+ (c): c is SerializedTag => isTag(c) && c.name === 'meta' && c.attributes?.property === prop
17
+ );
18
+ return child?.attributes?.content ?? '';
19
+ }
20
+
21
+ function propText(node: SerializedTag, prop: string): string {
22
+ const child = node.children.find(
23
+ (c): c is SerializedTag => isTag(c) && c.attributes?.property === prop
24
+ );
25
+ if (!child) return '';
26
+ return child.children.filter((c): c is string => typeof c === 'string').join('');
27
+ }
28
+
29
+ function findByType(node: SerializedTag, type: string): SerializedTag[] {
30
+ const results: SerializedTag[] = [];
31
+ function walk(children: RendererNode[]) {
32
+ for (const c of children) {
33
+ if (isTag(c)) {
34
+ if (c.attributes?.typeof === type) {
35
+ results.push(c);
36
+ } else {
37
+ walk(c.children);
38
+ }
39
+ } else if (Array.isArray(c)) {
40
+ walk(c);
41
+ }
42
+ }
43
+ }
44
+ walk(node.children);
45
+ return results;
46
+ }
47
+
48
+ function findRef(node: SerializedTag, name: string): SerializedTag | undefined {
49
+ for (const c of node.children) {
50
+ if (isTag(c) && c.attributes?.['data-name'] === name) return c;
51
+ }
52
+ return undefined;
53
+ }
54
+
55
+ const isComparison = typeName === 'Comparison';
56
+ const layout = isComparison ? (meta(tag, 'layout') || 'table') : '';
57
+ const verdict = isComparison ? meta(tag, 'verdict') : '';
58
+ const highlightedName = isComparison ? meta(tag, 'highlighted') : '';
59
+ const labelsPosition = isComparison ? (meta(tag, 'labels') || 'left') : '';
60
+ const rowLabelsJson = isComparison ? meta(tag, 'rowLabels') : '[]';
61
+ const rowLabels: string[] = isComparison ? (() => { try { return JSON.parse(rowLabelsJson); } catch { return []; } })() : [];
62
+ const columns = isComparison ? findByType(tag, 'ComparisonColumn') : [];
63
+
64
+ const titleTag = isComparison ? tag.children.find(
65
+ (c): c is SerializedTag => isTag(c) && /^h[1-6]$/.test(c.name)
66
+ ) : undefined;
67
+ const titleText = titleTag ? titleTag.children.filter((c): c is string => typeof c === 'string').join('') : '';
68
+
69
+ interface ColumnData {
70
+ name: string;
71
+ highlighted: boolean;
72
+ rows: SerializedTag[];
73
+ }
74
+
75
+ const columnData: ColumnData[] = isComparison ? columns.map(col => ({
76
+ name: propText(col, 'name'),
77
+ highlighted: meta(col, 'highlighted') === 'true',
78
+ rows: findByType(col, 'ComparisonRow'),
79
+ })) : [];
80
+
81
+ const isColumn = typeName === 'ComparisonColumn';
82
+ const colName = isColumn ? propText(tag, 'name') : '';
83
+ const colHighlighted = isColumn ? meta(tag, 'highlighted') === 'true' : false;
84
+
85
+ const isRow = typeName === 'ComparisonRow';
86
+ const rowLabel = isRow ? propText(tag, 'label') : '';
87
+ const rowType = isRow ? meta(tag, 'rowType') : '';
88
+ const rowBody = isRow ? findRef(tag, 'body') : undefined;
89
+ </script>
90
+
91
+ {#if isComparison}
92
+ <section class="rf-comparison rf-comparison--{layout}">
93
+ {#if titleText}
94
+ <h2 class="rf-comparison__title">{titleText}</h2>
95
+ {/if}
96
+
97
+ {#if layout === 'cards'}
98
+ <div class="rf-comparison__cards">
99
+ {#each columnData as col}
100
+ <div class="rf-comparison-card {col.highlighted ? 'rf-comparison-card--highlighted' : ''}">
101
+ {#if col.highlighted}
102
+ <div class="rf-comparison-card__badge">Recommended</div>
103
+ {/if}
104
+ <h3 class="rf-comparison-card__name">{col.name}</h3>
105
+ <ul class="rf-comparison-card__rows">
106
+ {#each col.rows as row}
107
+ {@const rType = meta(row, 'rowType')}
108
+ {@const rLabel = propText(row, 'label')}
109
+ {@const rBody = findRef(row, 'body')}
110
+ {#if rType !== 'empty'}
111
+ <li class="rf-comparison-card__row {rType === 'negative' ? 'rf-comparison-card__row--negative' : ''} {rType === 'callout' ? 'rf-comparison-card__row--callout' : ''}">
112
+ {#if rType === 'check'}
113
+ <span class="rf-comparison__row-icon rf-comparison__row-icon--check" aria-label="Supported">&#10003;</span>
114
+ {#if rLabel}<strong>{rLabel}</strong>{/if}
115
+ {#if rBody}<Renderer node={rBody.children} />{/if}
116
+ {:else if rType === 'cross'}
117
+ <span class="rf-comparison__row-icon rf-comparison__row-icon--cross" aria-label="Not supported">&#10007;</span>
118
+ {#if rLabel}<strong>{rLabel}</strong>{/if}
119
+ {#if rBody}<Renderer node={rBody.children} />{/if}
120
+ {:else if rType === 'negative'}
121
+ {#if rLabel}<strong>{rLabel}</strong>{/if}
122
+ {#if rBody}<span class="rf-comparison__negative"><Renderer node={rBody.children} /></span>{/if}
123
+ {:else if rType === 'callout'}
124
+ <div class="rf-comparison__callout-badge">
125
+ {#if rBody}<Renderer node={rBody.children} />{/if}
126
+ </div>
127
+ {:else}
128
+ {#if rLabel}<strong>{rLabel}</strong>{/if}
129
+ {#if rBody}<Renderer node={rBody.children} />{/if}
130
+ {/if}
131
+ </li>
132
+ {/if}
133
+ {/each}
134
+ </ul>
135
+ </div>
136
+ {/each}
137
+ </div>
138
+ {:else}
139
+ <div class="rf-comparison__table-wrapper">
140
+ <table class="rf-comparison__table">
141
+ <thead>
142
+ <tr>
143
+ {#if labelsPosition !== 'hidden'}
144
+ <th class="rf-comparison__label-col"></th>
145
+ {/if}
146
+ {#each columnData as col}
147
+ <th class="{col.highlighted ? 'rf-comparison__col-header--highlighted' : ''}">
148
+ {col.name}
149
+ {#if col.highlighted}
150
+ <span class="rf-comparison__recommended-badge">Recommended</span>
151
+ {/if}
152
+ </th>
153
+ {/each}
154
+ </tr>
155
+ </thead>
156
+ <tbody>
157
+ {#each rowLabels as label, i}
158
+ <tr>
159
+ {#if labelsPosition !== 'hidden'}
160
+ <th class="rf-comparison__row-label" scope="row">{label}</th>
161
+ {/if}
162
+ {#each columnData as col}
163
+ {@const row = col.rows[i]}
164
+ {@const rType = row ? meta(row, 'rowType') : 'empty'}
165
+ {@const rBody = row ? findRef(row, 'body') : undefined}
166
+ <td class="rf-comparison__cell {col.highlighted ? 'rf-comparison__cell--highlighted' : ''} {rType === 'empty' ? 'rf-comparison__cell--empty' : ''}">
167
+ {#if rType === 'check'}
168
+ <span class="rf-comparison__row-icon rf-comparison__row-icon--check" aria-label="Supported">&#10003;</span>
169
+ {:else if rType === 'cross'}
170
+ <span class="rf-comparison__row-icon rf-comparison__row-icon--cross" aria-label="Not supported">&#10007;</span>
171
+ {:else if rType === 'negative'}
172
+ {#if rBody}
173
+ <span class="rf-comparison__negative"><Renderer node={rBody.children} /></span>
174
+ {/if}
175
+ {:else if rType === 'empty'}
176
+ <span class="rf-comparison__cell--empty" aria-label="Not applicable">&mdash;</span>
177
+ {:else if rType === 'callout'}
178
+ {#if rBody}
179
+ <span class="rf-comparison__callout-badge"><Renderer node={rBody.children} /></span>
180
+ {/if}
181
+ {:else}
182
+ {#if rBody}
183
+ <Renderer node={rBody.children} />
184
+ {/if}
185
+ {/if}
186
+ </td>
187
+ {/each}
188
+ </tr>
189
+ {/each}
190
+ </tbody>
191
+ </table>
192
+ </div>
193
+ {/if}
194
+
195
+ {#if verdict}
196
+ <p class="rf-comparison__verdict">{verdict}</p>
197
+ {/if}
198
+ </section>
199
+ {:else if isColumn}
200
+ <div class="rf-comparison-column {colHighlighted ? 'rf-comparison-column--highlighted' : ''}">
201
+ <h3>{colName}</h3>
202
+ {@render children()}
203
+ </div>
204
+ {:else if isRow}
205
+ <div class="rf-comparison-row rf-comparison-row--{rowType}">
206
+ {#if rowLabel}<strong>{rowLabel}</strong>{/if}
207
+ {@render children()}
208
+ </div>
209
+ {/if}
@@ -0,0 +1,154 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import type { Snippet } from 'svelte';
4
+ import type { SerializedTag } from '@refrakt-md/svelte';
5
+
6
+ let { tag, children }: { tag: SerializedTag; children: Snippet } = $props();
7
+
8
+ const sortable = (tag.children.find((c: any) => c?.name === 'meta' && c?.attributes?.property === 'sortable')?.attributes?.content || '').split(',').map((s: string) => s.trim()).filter(Boolean);
9
+ const searchable = tag.children.find((c: any) => c?.name === 'meta' && c?.attributes?.property === 'searchable')?.attributes?.content === 'true';
10
+ const pageSize = parseInt(tag.children.find((c: any) => c?.name === 'meta' && c?.attributes?.property === 'pageSize')?.attributes?.content || '0', 10);
11
+ const defaultSort = tag.children.find((c: any) => c?.name === 'meta' && c?.attributes?.property === 'defaultSort')?.attributes?.content || '';
12
+
13
+ let searchQuery = $state('');
14
+ let sortColumn = $state(defaultSort);
15
+ let sortDirection = $state<'asc' | 'desc'>('asc');
16
+ let currentPage = $state(0);
17
+
18
+ let contentEl: HTMLDivElement;
19
+ let headers: string[] = [];
20
+ let allRows: { el: HTMLTableRowElement; cells: string[] }[] = [];
21
+ let totalFiltered = $state(0);
22
+
23
+ function toggleSort(column: string) {
24
+ if (!sortable.includes(column)) return;
25
+ if (sortColumn === column) {
26
+ sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
27
+ } else {
28
+ sortColumn = column;
29
+ sortDirection = 'asc';
30
+ }
31
+ currentPage = 0;
32
+ }
33
+
34
+ onMount(() => {
35
+ const table = contentEl?.querySelector('table');
36
+ if (!table) return;
37
+
38
+ const thEls = table.querySelectorAll('th');
39
+ headers = Array.from(thEls).map(th => th.textContent?.trim() || '');
40
+
41
+ thEls.forEach((th) => {
42
+ const name = th.textContent?.trim() || '';
43
+ if (sortable.includes(name)) {
44
+ th.style.cursor = 'pointer';
45
+ th.style.userSelect = 'none';
46
+ th.addEventListener('click', () => toggleSort(name));
47
+ }
48
+ });
49
+
50
+ const bodyRows = table.querySelectorAll('tbody tr');
51
+ const rows = bodyRows.length > 0 ? bodyRows : table.querySelectorAll('tr:not(:first-child)');
52
+ allRows = Array.from(rows).map(tr => ({
53
+ el: tr as HTMLTableRowElement,
54
+ cells: Array.from(tr.querySelectorAll('td')).map(td => td.textContent?.trim() || ''),
55
+ }));
56
+
57
+ totalFiltered = allRows.length;
58
+ });
59
+
60
+ $effect(() => {
61
+ if (!allRows.length) return;
62
+
63
+ let filtered = [...allRows];
64
+
65
+ if (searchQuery) {
66
+ const q = searchQuery.toLowerCase();
67
+ filtered = filtered.filter(r => r.cells.some(c => c.toLowerCase().includes(q)));
68
+ }
69
+
70
+ if (sortColumn) {
71
+ const idx = headers.indexOf(sortColumn);
72
+ if (idx >= 0) {
73
+ filtered.sort((a, b) => {
74
+ const cmp = a.cells[idx].localeCompare(b.cells[idx], undefined, { numeric: true });
75
+ return sortDirection === 'asc' ? cmp : -cmp;
76
+ });
77
+ }
78
+ }
79
+
80
+ totalFiltered = filtered.length;
81
+
82
+ const visible = pageSize > 0
83
+ ? filtered.slice(currentPage * pageSize, (currentPage + 1) * pageSize)
84
+ : filtered;
85
+
86
+ const tbody = contentEl?.querySelector('tbody') || contentEl?.querySelector('table');
87
+ if (!tbody) return;
88
+
89
+ allRows.forEach(r => r.el.style.display = 'none');
90
+ visible.forEach(r => {
91
+ r.el.style.display = '';
92
+ tbody.appendChild(r.el);
93
+ });
94
+
95
+ const thEls = contentEl?.querySelectorAll('th');
96
+ thEls?.forEach(th => {
97
+ const name = th.textContent?.replace(/[▲▼]/g, '').trim() || '';
98
+ const indicator = th.querySelector('.sort-indicator');
99
+ if (sortable.includes(name)) {
100
+ if (sortColumn === name) {
101
+ if (indicator) {
102
+ indicator.textContent = sortDirection === 'asc' ? ' ▲' : ' ▼';
103
+ } else {
104
+ const span = document.createElement('span');
105
+ span.className = 'sort-indicator';
106
+ span.textContent = sortDirection === 'asc' ? ' ▲' : ' ▼';
107
+ th.appendChild(span);
108
+ }
109
+ } else {
110
+ if (indicator) indicator.remove();
111
+ }
112
+ }
113
+ });
114
+ });
115
+
116
+ const totalPages = $derived(pageSize > 0 ? Math.ceil(totalFiltered / pageSize) : 1);
117
+ </script>
118
+
119
+ <div class="rf-datatable" typeof="DataTable">
120
+ {#if searchable}
121
+ <div class="rf-datatable__toolbar">
122
+ <input
123
+ type="search"
124
+ placeholder="Filter rows..."
125
+ bind:value={searchQuery}
126
+ class="rf-datatable__input"
127
+ />
128
+ </div>
129
+ {/if}
130
+ <div class="rf-datatable__content" bind:this={contentEl}>
131
+ {@render children()}
132
+ </div>
133
+ {#if pageSize > 0 && totalPages > 1}
134
+ <div class="rf-datatable__pagination">
135
+ <button
136
+ class="rf-datatable__page-btn"
137
+ disabled={currentPage === 0}
138
+ onclick={() => currentPage--}
139
+ >
140
+ &larr; Prev
141
+ </button>
142
+ <span class="rf-datatable__page-info">
143
+ {currentPage + 1} / {totalPages}
144
+ </span>
145
+ <button
146
+ class="rf-datatable__page-btn"
147
+ disabled={currentPage >= totalPages - 1}
148
+ onclick={() => currentPage++}
149
+ >
150
+ Next &rarr;
151
+ </button>
152
+ </div>
153
+ {/if}
154
+ </div>
@@ -0,0 +1,23 @@
1
+ <script lang="ts">
2
+ import type { SerializedTag } from '@refrakt-md/svelte';
3
+ import type { Snippet } from 'svelte';
4
+
5
+ let { tag, children }: { tag: SerializedTag; children: Snippet } = $props();
6
+
7
+ const summary = $derived(tag.children
8
+ .find((c: any) => c?.name === 'span' && c?.attributes?.property === 'summary')
9
+ ?.children?.[0] ?? 'Details');
10
+
11
+ const isOpen = $derived(tag.children
12
+ .find((c: any) => c?.name === 'meta' && c?.attributes?.property === 'open')
13
+ ?.attributes?.content ?? false);
14
+ </script>
15
+
16
+ <details class="rf-details" open={isOpen || undefined}>
17
+ <summary class="rf-details__summary">
18
+ <span>{summary}</span>
19
+ </summary>
20
+ <div class="rf-details__body">
21
+ {@render children()}
22
+ </div>
23
+ </details>
@@ -0,0 +1,45 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import type { Snippet } from 'svelte';
4
+ import type { SerializedTag } from '@refrakt-md/svelte';
5
+
6
+ let { tag, children }: { tag: SerializedTag; children: Snippet } = $props();
7
+
8
+ const language = tag.children.find((c: any) => c?.name === 'meta' && c?.attributes?.property === 'language')?.attributes?.content || 'mermaid';
9
+ const title = tag.children.find((c: any) => c?.name === 'meta' && c?.attributes?.property === 'title')?.attributes?.content || '';
10
+ const source = tag.children.find((c: any) => c?.name === 'meta' && c?.attributes?.['data-name'] === 'source')?.attributes?.content || '';
11
+
12
+ let container: HTMLDivElement;
13
+ let rendered = $state(false);
14
+
15
+ onMount(async () => {
16
+ if (language === 'mermaid' && source) {
17
+ try {
18
+ const cdn = 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
19
+ const mermaid = (await import(/* @vite-ignore */ cdn)).default;
20
+ mermaid.initialize({ startOnLoad: false, theme: 'default' });
21
+ const { svg } = await mermaid.render('mermaid-' + Math.random().toString(36).slice(2), source);
22
+ container.innerHTML = svg;
23
+ rendered = true;
24
+ } catch (e) {
25
+ container.textContent = source;
26
+ }
27
+ } else if (language === 'ascii' && source) {
28
+ container.textContent = source;
29
+ container.style.fontFamily = 'var(--rf-font-mono)';
30
+ container.style.whiteSpace = 'pre';
31
+ rendered = true;
32
+ }
33
+ });
34
+ </script>
35
+
36
+ <figure class="rf-diagram" typeof="Diagram">
37
+ {#if title}
38
+ <figcaption class="rf-diagram__title">{title}</figcaption>
39
+ {/if}
40
+ <div class="rf-diagram__container" bind:this={container}>
41
+ {#if !rendered}
42
+ <pre class="rf-diagram__source"><code>{source}</code></pre>
43
+ {/if}
44
+ </div>
45
+ </figure>
@@ -0,0 +1,36 @@
1
+ <script lang="ts">
2
+ import type { SerializedTag } from '@refrakt-md/svelte';
3
+ import type { Snippet } from 'svelte';
4
+
5
+ let { tag, children }: { tag: SerializedTag; children: Snippet } = $props();
6
+
7
+ const getMeta = (prop: string) => tag.children
8
+ .find((c: any) => c?.name === 'meta' && c?.attributes?.property === prop)
9
+ ?.attributes?.content;
10
+
11
+ const embedUrl = getMeta('embedUrl') || getMeta('url') || '';
12
+ const title = getMeta('title') || 'Embedded content';
13
+ const aspect = getMeta('aspect') || '16:9';
14
+ const provider = getMeta('provider') || '';
15
+
16
+ const [w, h] = aspect.split(':').map(Number);
17
+ const paddingPercent = h && w ? (h / w) * 100 : 56.25;
18
+ </script>
19
+
20
+ <figure class="rf-embed" data-provider={provider || undefined}>
21
+ {#if embedUrl}
22
+ <div class="rf-embed__wrapper" style="padding-bottom: {paddingPercent}%">
23
+ <iframe
24
+ src={embedUrl}
25
+ {title}
26
+ frameborder="0"
27
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
28
+ allowfullscreen
29
+ loading="lazy"
30
+ ></iframe>
31
+ </div>
32
+ {/if}
33
+ <div class="rf-embed__fallback">
34
+ {@render children()}
35
+ </div>
36
+ </figure>
@@ -0,0 +1,194 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { SerializedTag } from '@refrakt-md/svelte';
4
+
5
+ let { tag, children }: { tag: SerializedTag; children: Snippet } = $props();
6
+
7
+ const isForm = tag.attributes.typeof === 'Form';
8
+
9
+ // Form-level properties
10
+ const action = isForm
11
+ ? tag.children.find((c: any) => c?.name === 'meta' && c?.attributes?.property === 'action')?.attributes?.content || ''
12
+ : '';
13
+ const method = isForm
14
+ ? tag.children.find((c: any) => c?.name === 'meta' && c?.attributes?.property === 'method')?.attributes?.content || 'POST'
15
+ : 'POST';
16
+ const success = isForm
17
+ ? tag.children.find((c: any) => c?.name === 'meta' && c?.attributes?.property === 'success')?.attributes?.content || ''
18
+ : '';
19
+ const errorMsg = isForm
20
+ ? tag.children.find((c: any) => c?.name === 'meta' && c?.attributes?.property === 'error')?.attributes?.content || ''
21
+ : '';
22
+ const formStyle = isForm
23
+ ? tag.attributes['data-style'] || 'stacked'
24
+ : 'stacked';
25
+ const honeypot = isForm
26
+ ? tag.children.find((c: any) => c?.name === 'meta' && c?.attributes?.property === 'honeypot')?.attributes?.content !== 'false'
27
+ : true;
28
+
29
+ // FormField properties
30
+ const fieldName = !isForm
31
+ ? tag.children.find((c: any) => c?.name === 'span' && c?.attributes?.property === 'name')?.children?.[0] || ''
32
+ : '';
33
+ const fieldType = !isForm
34
+ ? tag.attributes['data-field-type'] || 'text'
35
+ : '';
36
+ const required = !isForm
37
+ ? tag.children.find((c: any) => c?.name === 'meta' && c?.attributes?.property === 'required')?.attributes?.content === 'true'
38
+ : false;
39
+ const placeholder = !isForm
40
+ ? tag.children.find((c: any) => c?.name === 'meta' && c?.attributes?.property === 'placeholder')?.attributes?.content || ''
41
+ : '';
42
+ const options = !isForm
43
+ ? (tag.children.find((c: any) => c?.name === 'meta' && c?.attributes?.property === 'options')?.attributes?.content || '')
44
+ .split(',')
45
+ .map((o: string) => o.trim())
46
+ .filter(Boolean)
47
+ : [];
48
+
49
+ const fieldId = !isForm ? `field-${fieldName.toLowerCase().replace(/\s+/g, '-')}` : '';
50
+
51
+ // Form submission state
52
+ let status = $state<'idle' | 'submitting' | 'success' | 'error'>('idle');
53
+ let statusMessage = $state('');
54
+
55
+ async function handleSubmit(e: SubmitEvent) {
56
+ e.preventDefault();
57
+ if (!action) return;
58
+
59
+ status = 'submitting';
60
+ const formData = new FormData(e.target as HTMLFormElement);
61
+
62
+ try {
63
+ const response = await fetch(action, {
64
+ method: method,
65
+ body: formData,
66
+ headers: { 'Accept': 'application/json' },
67
+ });
68
+
69
+ if (response.ok) {
70
+ status = 'success';
71
+ statusMessage = success || 'Form submitted successfully.';
72
+ (e.target as HTMLFormElement).reset();
73
+ } else {
74
+ status = 'error';
75
+ statusMessage = errorMsg || 'Something went wrong. Please try again.';
76
+ }
77
+ } catch {
78
+ status = 'error';
79
+ statusMessage = errorMsg || 'Something went wrong. Please try again.';
80
+ }
81
+ }
82
+ </script>
83
+
84
+ {#if isForm}
85
+ <form
86
+ class="rf-form {formStyle !== 'stacked' ? `rf-form--${formStyle}` : ''}"
87
+ action={action}
88
+ method={method}
89
+ onsubmit={handleSubmit}
90
+ >
91
+ {#if honeypot}
92
+ <div class="rf-form__hp" aria-hidden="true">
93
+ <input type="text" name="_gotcha" autocomplete="off" tabindex={-1} />
94
+ </div>
95
+ {/if}
96
+ {@render children()}
97
+ {#if status === 'submitting'}
98
+ <div class="rf-form__status rf-form__status--submitting" role="status">Submitting...</div>
99
+ {:else if status === 'success'}
100
+ <div class="rf-form__status rf-form__status--success" role="alert">{statusMessage}</div>
101
+ {:else if status === 'error'}
102
+ <div class="rf-form__status rf-form__status--error" role="alert">{statusMessage}</div>
103
+ {/if}
104
+ </form>
105
+ {:else if fieldType === 'group'}
106
+ <fieldset class="rf-form-fieldset">
107
+ <legend>{fieldName}</legend>
108
+ {@render children()}
109
+ </fieldset>
110
+ {:else if fieldType === 'submit'}
111
+ <button type="submit" class="rf-form__submit">{fieldName}</button>
112
+ {:else if fieldType === 'separator'}
113
+ <hr class="rf-form__separator" />
114
+ {:else if fieldType === 'help'}
115
+ <p class="rf-form__help">{fieldName}</p>
116
+ {:else if fieldType === 'description'}
117
+ <p class="rf-form__text">{fieldName}</p>
118
+ {:else if fieldType === 'textarea'}
119
+ <div class="rf-form-field">
120
+ <label for={fieldId}>
121
+ {fieldName}
122
+ {#if required}<span class="rf-form-field__required" aria-hidden="true">*</span>{/if}
123
+ </label>
124
+ <textarea
125
+ id={fieldId}
126
+ name={fieldId}
127
+ placeholder={placeholder}
128
+ required={required}
129
+ rows={4}
130
+ ></textarea>
131
+ </div>
132
+ {:else if fieldType === 'select'}
133
+ <div class="rf-form-field">
134
+ <label for={fieldId}>
135
+ {fieldName}
136
+ {#if required}<span class="rf-form-field__required" aria-hidden="true">*</span>{/if}
137
+ </label>
138
+ <select id={fieldId} name={fieldId} required={required}>
139
+ <option value="" disabled selected>Select an option</option>
140
+ {#each options as option}
141
+ <option value={option}>{option}</option>
142
+ {/each}
143
+ </select>
144
+ </div>
145
+ {:else if fieldType === 'radio'}
146
+ <fieldset class="rf-form-field rf-form-choice-group">
147
+ <legend>
148
+ {fieldName}
149
+ {#if required}<span class="rf-form-field__required" aria-hidden="true">*</span>{/if}
150
+ </legend>
151
+ {#each options as option, i}
152
+ <label class="rf-form-choice">
153
+ <input
154
+ type="radio"
155
+ name={fieldId}
156
+ value={option}
157
+ required={required && i === 0}
158
+ />
159
+ <span>{option}</span>
160
+ </label>
161
+ {/each}
162
+ </fieldset>
163
+ {:else if fieldType === 'checkbox'}
164
+ <fieldset class="rf-form-field rf-form-choice-group">
165
+ <legend>
166
+ {fieldName}
167
+ {#if required}<span class="rf-form-field__required" aria-hidden="true">*</span>{/if}
168
+ </legend>
169
+ {#each options as option}
170
+ <label class="rf-form-choice">
171
+ <input
172
+ type="checkbox"
173
+ name={fieldId}
174
+ value={option}
175
+ />
176
+ <span>{option}</span>
177
+ </label>
178
+ {/each}
179
+ </fieldset>
180
+ {:else}
181
+ <div class="rf-form-field">
182
+ <label for={fieldId}>
183
+ {fieldName}
184
+ {#if required}<span class="rf-form-field__required" aria-hidden="true">*</span>{/if}
185
+ </label>
186
+ <input
187
+ type={fieldType}
188
+ id={fieldId}
189
+ name={fieldId}
190
+ placeholder={placeholder}
191
+ required={required}
192
+ />
193
+ </div>
194
+ {/if}