@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,65 @@
1
+ /* Storyboard */
2
+ .rf-storyboard {
3
+ margin: 1.5rem 0;
4
+ }
5
+ .rf-storyboard__panels {
6
+ display: grid;
7
+ grid-template-columns: repeat(var(--sb-columns, 3), 1fr);
8
+ gap: 1rem;
9
+ }
10
+ /* Clean style */
11
+ .rf-storyboard--clean .rf-storyboard-panel {
12
+ border: 1px solid var(--rf-color-border);
13
+ border-radius: var(--rf-radius-md);
14
+ overflow: hidden;
15
+ }
16
+ .rf-storyboard--clean .rf-storyboard-panel__body {
17
+ padding: 0.75rem;
18
+ }
19
+ /* Comic style */
20
+ .rf-storyboard--comic .rf-storyboard-panel {
21
+ border: 3px solid var(--rf-color-text);
22
+ border-radius: 0.25rem;
23
+ overflow: hidden;
24
+ transform: rotate(-0.5deg);
25
+ }
26
+ .rf-storyboard--comic .rf-storyboard-panel:nth-child(even) {
27
+ transform: rotate(0.5deg);
28
+ }
29
+ .rf-storyboard--comic .rf-storyboard-panel__body {
30
+ padding: 0.5rem;
31
+ }
32
+ .rf-storyboard--comic .rf-storyboard-panel__body p {
33
+ font-family: 'Comic Sans MS', 'Chalkboard SE', cursive;
34
+ font-size: 0.9375rem;
35
+ text-align: center;
36
+ margin: 0.5rem 0 0;
37
+ }
38
+ /* Polaroid style */
39
+ .rf-storyboard--polaroid .rf-storyboard-panel {
40
+ background: white;
41
+ padding: 0.75rem 0.75rem 2.5rem;
42
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
43
+ border-radius: 2px;
44
+ }
45
+ .rf-storyboard--polaroid .rf-storyboard-panel__body p {
46
+ font-size: 0.8125rem;
47
+ text-align: center;
48
+ color: var(--rf-color-muted);
49
+ margin: 0.5rem 0 0;
50
+ }
51
+ /* Shared image styles */
52
+ .rf-storyboard-panel img {
53
+ width: 100%;
54
+ height: auto;
55
+ display: block;
56
+ }
57
+ .rf-storyboard-panel__body span[property],
58
+ .rf-storyboard-panel__body meta { display: none; }
59
+ .rf-storyboard-panel__body p:last-child { margin-bottom: 0; }
60
+ @media (max-width: 768px) {
61
+ .rf-storyboard__panels { grid-template-columns: repeat(2, 1fr) !important; }
62
+ }
63
+ @media (max-width: 480px) {
64
+ .rf-storyboard__panels { grid-template-columns: 1fr !important; }
65
+ }
@@ -0,0 +1,50 @@
1
+ /* Tabs */
2
+ .rf-tabs {
3
+ border: 1px solid var(--rf-color-border);
4
+ border-radius: var(--rf-radius-md);
5
+ overflow: hidden;
6
+ margin: 1.5rem 0;
7
+ background: var(--rf-color-bg);
8
+ }
9
+ .rf-tabs__bar {
10
+ display: flex;
11
+ background: var(--rf-color-surface);
12
+ border-bottom: 1px solid var(--rf-color-border);
13
+ overflow-x: auto;
14
+ overflow-y: hidden;
15
+ -webkit-overflow-scrolling: touch;
16
+ }
17
+ .rf-tabs__button {
18
+ flex: 0 0 auto;
19
+ padding: 0.625rem 1.25rem;
20
+ font-size: 0.85rem;
21
+ font-weight: 500;
22
+ color: var(--rf-color-muted);
23
+ background: none;
24
+ border: none;
25
+ border-bottom: 2px solid transparent;
26
+ cursor: pointer;
27
+ transition: color 200ms ease, border-color 200ms ease, background-color 200ms ease;
28
+ white-space: nowrap;
29
+ margin-bottom: -1px;
30
+ font-family: inherit;
31
+ }
32
+ .rf-tabs__button:hover {
33
+ color: var(--rf-color-text);
34
+ background: var(--rf-color-surface-hover);
35
+ }
36
+ .rf-tabs__button--active {
37
+ color: var(--rf-color-primary);
38
+ border-bottom-color: var(--rf-color-primary);
39
+ font-weight: 600;
40
+ background: var(--rf-color-bg);
41
+ }
42
+ .rf-tabs__panel {
43
+ padding: 1.25rem 1.5rem;
44
+ }
45
+ .rf-tabs__panel pre {
46
+ margin: 0;
47
+ }
48
+ .rf-tabs__panel p:last-child {
49
+ margin-bottom: 0;
50
+ }
@@ -0,0 +1,59 @@
1
+ /* Testimonial */
2
+ .rf-testimonial {
3
+ border: 1px solid var(--rf-color-border);
4
+ border-radius: var(--rf-radius-lg);
5
+ padding: 1.75rem 2rem;
6
+ margin: 1.5rem 0;
7
+ background: var(--rf-color-surface);
8
+ }
9
+ .rf-testimonial__rating {
10
+ display: flex;
11
+ gap: 0.125rem;
12
+ margin-bottom: 0.75rem;
13
+ }
14
+ .rf-testimonial__star {
15
+ font-size: 1.1rem;
16
+ color: var(--rf-color-border);
17
+ line-height: 1;
18
+ }
19
+ .rf-testimonial__star--filled {
20
+ color: var(--rf-color-warning);
21
+ }
22
+ .rf-testimonial blockquote,
23
+ .rf-testimonial .rf-blockquote {
24
+ border: none;
25
+ padding: 0;
26
+ margin: 0 0 1rem;
27
+ font-size: 1.05rem;
28
+ font-style: italic;
29
+ line-height: 1.7;
30
+ color: var(--rf-color-text);
31
+ background: none;
32
+ border-radius: 0;
33
+ }
34
+ .rf-testimonial .rf-blockquote__quote-mark {
35
+ display: none;
36
+ }
37
+ .rf-testimonial blockquote p {
38
+ margin: 0;
39
+ }
40
+ .rf-testimonial span[property="authorName"] {
41
+ display: block;
42
+ font-weight: 700;
43
+ font-style: normal;
44
+ font-size: 0.925rem;
45
+ color: var(--rf-color-text);
46
+ }
47
+ .rf-testimonial span[property="authorRole"] {
48
+ display: block;
49
+ margin-top: 0.125rem;
50
+ font-size: 0.85rem;
51
+ color: var(--rf-color-muted);
52
+ }
53
+ .rf-testimonial img {
54
+ width: 48px;
55
+ height: 48px;
56
+ border-radius: 50%;
57
+ object-fit: cover;
58
+ margin-top: 0.75rem;
59
+ }
@@ -0,0 +1,67 @@
1
+ /* Timeline */
2
+ .rf-timeline {
3
+ margin: 2rem 0;
4
+ }
5
+ .rf-timeline ol {
6
+ list-style: none;
7
+ padding: 0;
8
+ margin: 0;
9
+ position: relative;
10
+ }
11
+ .rf-timeline--vertical ol {
12
+ padding-left: 2rem;
13
+ border-left: 2px solid var(--rf-color-border);
14
+ }
15
+ .rf-timeline__entries {
16
+ display: contents;
17
+ }
18
+ .rf-timeline-entry {
19
+ position: relative;
20
+ padding: 0 0 2rem;
21
+ }
22
+ .rf-timeline-entry:last-child {
23
+ padding-bottom: 0;
24
+ }
25
+ .rf-timeline-entry::before {
26
+ content: '';
27
+ position: absolute;
28
+ left: -2.5625rem;
29
+ top: 0.35rem;
30
+ width: 12px;
31
+ height: 12px;
32
+ border-radius: 50%;
33
+ background: var(--rf-color-primary);
34
+ border: 2px solid var(--rf-color-bg);
35
+ box-shadow: 0 0 0 2px var(--rf-color-primary);
36
+ }
37
+ .rf-timeline-entry__body time {
38
+ display: block;
39
+ font-size: 0.8rem;
40
+ font-weight: 600;
41
+ color: var(--rf-color-primary);
42
+ letter-spacing: 0.02em;
43
+ margin-bottom: 0.25rem;
44
+ }
45
+ .rf-timeline-entry__body span {
46
+ display: block;
47
+ font-size: 1.05rem;
48
+ font-weight: 700;
49
+ color: var(--rf-color-text);
50
+ margin-bottom: 0.5rem;
51
+ }
52
+ .rf-timeline-entry__body {
53
+ font-size: 0.925rem;
54
+ line-height: 1.65;
55
+ color: var(--rf-color-muted);
56
+ }
57
+ .rf-timeline-entry__body p:last-child {
58
+ margin-bottom: 0;
59
+ }
60
+ .rf-timeline--horizontal ol {
61
+ display: flex;
62
+ gap: 2rem;
63
+ overflow-x: auto;
64
+ padding: 2rem 0 1rem;
65
+ border-left: none;
66
+ border-top: 2px solid var(--rf-color-border);
67
+ }
@@ -0,0 +1,43 @@
1
+ /* Table of Contents */
2
+ .rf-toc {
3
+ padding: 1.25rem 1.5rem;
4
+ border-left: 3px solid var(--rf-color-primary);
5
+ background: var(--rf-color-surface);
6
+ border-radius: 0 var(--rf-radius-md) var(--rf-radius-md) 0;
7
+ margin: 1.5rem 0;
8
+ }
9
+ .rf-toc__list {
10
+ font-weight: 650;
11
+ margin-bottom: 0.75rem;
12
+ font-size: 0.7rem;
13
+ text-transform: uppercase;
14
+ letter-spacing: 0.06em;
15
+ color: var(--rf-color-muted);
16
+ }
17
+ .rf-toc ul,
18
+ .rf-toc ol {
19
+ list-style: none;
20
+ padding-left: 0;
21
+ margin: 0;
22
+ }
23
+ .rf-toc li {
24
+ margin: 0.125rem 0;
25
+ }
26
+ .rf-toc li[data-level="3"] {
27
+ padding-left: 1rem;
28
+ }
29
+ .rf-toc li[data-level="4"] {
30
+ padding-left: 2rem;
31
+ }
32
+ .rf-toc a {
33
+ color: var(--rf-color-text);
34
+ text-decoration: none;
35
+ font-size: 0.85rem;
36
+ display: inline-block;
37
+ padding: 0.2rem 0;
38
+ transition: color 150ms ease;
39
+ }
40
+ .rf-toc a:hover {
41
+ color: var(--rf-color-primary);
42
+ text-decoration: none;
43
+ }
@@ -0,0 +1,26 @@
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 isGroup = $derived(tag.attributes.typeof === 'Accordion');
8
+ </script>
9
+
10
+ {#if isGroup}
11
+ <div class="rf-accordion">
12
+ {@render children()}
13
+ </div>
14
+ {:else}
15
+ {@const name = tag.children
16
+ .find((c) => c?.name === 'span' && c?.attributes?.property === 'name')
17
+ ?.children?.[0] ?? ''}
18
+ <details class="rf-accordion-item">
19
+ <summary class="rf-accordion-item__header">
20
+ <span class="rf-accordion-item__title">{name}</span>
21
+ </summary>
22
+ <div class="rf-accordion-item__body">
23
+ {@render children()}
24
+ </div>
25
+ </details>
26
+ {/if}
@@ -0,0 +1,50 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { SerializedTag, RendererNode } from '@refrakt-md/svelte';
4
+ import { Renderer } from '@refrakt-md/svelte';
5
+
6
+ let { tag, children }: { tag: SerializedTag; children: Snippet } = $props();
7
+
8
+ function isTag(c: RendererNode): c is SerializedTag {
9
+ return typeof c === 'object' && c !== null && 'name' in c;
10
+ }
11
+
12
+ const isGroup = tag.attributes.typeof === 'Bento';
13
+
14
+ // For Bento container
15
+ const metas = isGroup ? tag.children.filter((c: any) => c?.name === 'meta') : [];
16
+ const gap = isGroup ? metas[0]?.attributes?.content || '1rem' : '1rem';
17
+ const columns = isGroup ? parseInt(metas[1]?.attributes?.content || '4', 10) : 4;
18
+
19
+ // Find the grid container in tag.children
20
+ const gridEl = isGroup
21
+ ? tag.children.find((c): c is SerializedTag => isTag(c) && c.attributes?.['data-name'] === 'grid')
22
+ : undefined;
23
+
24
+ // For BentoCell
25
+ const cellName = !isGroup
26
+ ? tag.children.find((c: any) => c?.name === 'span' && c?.attributes?.property === 'name')?.children?.[0] || ''
27
+ : '';
28
+ const cellSize = !isGroup
29
+ ? tag.attributes['data-size'] || 'small'
30
+ : 'small';
31
+ </script>
32
+
33
+ {#if isGroup}
34
+ <section class="rf-bento">
35
+ <div class="rf-bento__grid" style:grid-template-columns="repeat({columns}, 1fr)" style:gap={gap}>
36
+ {#if gridEl}
37
+ <Renderer node={gridEl.children} />
38
+ {/if}
39
+ </div>
40
+ </section>
41
+ {:else}
42
+ <div class="rf-bento-cell rf-bento-cell--{cellSize}">
43
+ {#if cellName}
44
+ <h3 class="rf-bento-cell__title">{cellName}</h3>
45
+ {/if}
46
+ <div class="rf-bento-cell__body">
47
+ {@render children()}
48
+ </div>
49
+ </div>
50
+ {/if}
@@ -0,0 +1,121 @@
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 chartType = tag.children.find((c: any) => c?.name === 'meta' && c?.attributes?.property === 'type')?.attributes?.content || 'bar';
8
+ const title = tag.children.find((c: any) => c?.name === 'meta' && c?.attributes?.property === 'title')?.attributes?.content || '';
9
+ const stacked = tag.children.find((c: any) => c?.name === 'meta' && c?.attributes?.property === 'stacked')?.attributes?.content === 'true';
10
+
11
+ const dataJson = tag.children.find((c: any) => c?.name === 'meta' && c?.attributes?.['data-name'] === 'data')?.attributes?.content || '{}';
12
+ let chartData: { headers: string[]; rows: string[][] } = { headers: [], rows: [] };
13
+ try {
14
+ chartData = JSON.parse(dataJson);
15
+ } catch {}
16
+
17
+ const colors = [
18
+ 'var(--rf-color-info)',
19
+ 'var(--rf-color-success)',
20
+ 'var(--rf-color-warning)',
21
+ 'var(--rf-color-danger)',
22
+ '#7c3aed',
23
+ '#0891b2',
24
+ ];
25
+
26
+ const svgWidth = 600;
27
+ const svgHeight = 300;
28
+ const padding = { top: 30, right: 20, bottom: 40, left: 50 };
29
+ const chartWidth = svgWidth - padding.left - padding.right;
30
+ const chartHeight = svgHeight - padding.top - padding.bottom;
31
+
32
+ const labels = chartData.rows.map(r => r[0] || '');
33
+ const series = chartData.headers.slice(1);
34
+ const values = chartData.rows.map(r => r.slice(1).map(v => parseFloat(v) || 0));
35
+
36
+ const allValues = values.flat();
37
+ const maxValue = Math.max(...allValues, 1);
38
+
39
+ const barGroupWidth = chartWidth / Math.max(labels.length, 1);
40
+ const barWidth = barGroupWidth / Math.max(series.length + 1, 2);
41
+ </script>
42
+
43
+ <figure class="rf-chart" typeof="Chart">
44
+ {#if title}
45
+ <figcaption class="rf-chart__title">{title}</figcaption>
46
+ {/if}
47
+ <div class="rf-chart__container">
48
+ <svg viewBox="0 0 {svgWidth} {svgHeight}" class="rf-chart__svg">
49
+ <line
50
+ x1={padding.left} y1={padding.top}
51
+ x2={padding.left} y2={svgHeight - padding.bottom}
52
+ stroke="var(--rf-color-border)" stroke-width="1"
53
+ />
54
+ <line
55
+ x1={padding.left} y1={svgHeight - padding.bottom}
56
+ x2={svgWidth - padding.right} y2={svgHeight - padding.bottom}
57
+ stroke="var(--rf-color-border)" stroke-width="1"
58
+ />
59
+
60
+ {#if chartType === 'bar'}
61
+ {#each labels as label, i}
62
+ {#each series as _, si}
63
+ <rect
64
+ x={padding.left + i * barGroupWidth + si * barWidth + barWidth * 0.25}
65
+ y={padding.top + chartHeight - (values[i][si] / maxValue) * chartHeight}
66
+ width={barWidth * 0.75}
67
+ height={(values[i][si] / maxValue) * chartHeight}
68
+ style="fill: {colors[si % colors.length]}"
69
+ rx="2"
70
+ />
71
+ {/each}
72
+ <text
73
+ x={padding.left + i * barGroupWidth + barGroupWidth / 2}
74
+ y={svgHeight - padding.bottom + 20}
75
+ text-anchor="middle"
76
+ font-size="12"
77
+ fill="var(--rf-color-muted)"
78
+ >{label}</text>
79
+ {/each}
80
+ {:else if chartType === 'line'}
81
+ {#each series as _, si}
82
+ <polyline
83
+ points={labels.map((_, i) =>
84
+ `${padding.left + i * barGroupWidth + barGroupWidth / 2},${padding.top + chartHeight - (values[i][si] / maxValue) * chartHeight}`
85
+ ).join(' ')}
86
+ fill="none"
87
+ style="stroke: {colors[si % colors.length]}"
88
+ stroke-width="2"
89
+ />
90
+ {#each labels as _, i}
91
+ <circle
92
+ cx={padding.left + i * barGroupWidth + barGroupWidth / 2}
93
+ cy={padding.top + chartHeight - (values[i][si] / maxValue) * chartHeight}
94
+ r="4"
95
+ style="fill: {colors[si % colors.length]}"
96
+ />
97
+ {/each}
98
+ {/each}
99
+ {#each labels as label, i}
100
+ <text
101
+ x={padding.left + i * barGroupWidth + barGroupWidth / 2}
102
+ y={svgHeight - padding.bottom + 20}
103
+ text-anchor="middle"
104
+ font-size="12"
105
+ fill="var(--rf-color-muted)"
106
+ >{label}</text>
107
+ {/each}
108
+ {/if}
109
+ </svg>
110
+ </div>
111
+ {#if series.length > 1}
112
+ <div class="rf-chart__legend">
113
+ {#each series as name, i}
114
+ <span class="rf-chart__legend-item">
115
+ <span class="rf-chart__legend-color" style="background: {colors[i % colors.length]};"></span>
116
+ {name}
117
+ </span>
118
+ {/each}
119
+ </div>
120
+ {/if}
121
+ </figure>
@@ -0,0 +1,88 @@
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
+ function isTag(n: RendererNode): n is SerializedTag {
9
+ return n !== null && typeof n === 'object' && !Array.isArray(n) && (n as any).$$mdtype === 'Tag';
10
+ }
11
+
12
+ function getTextContent(node: RendererNode): string {
13
+ if (typeof node === 'string') return node;
14
+ if (typeof node === 'number') return String(node);
15
+ if (isTag(node)) return node.children.map(getTextContent).join('');
16
+ if (Array.isArray(node)) return node.map(getTextContent).join('');
17
+ return '';
18
+ }
19
+
20
+ const title = $derived.by(() => {
21
+ for (const child of tag.children) {
22
+ if (isTag(child) && child.attributes?.property === 'title') {
23
+ return getTextContent(child);
24
+ }
25
+ }
26
+ return '';
27
+ });
28
+
29
+ const { tabs, panels } = $derived.by(() => {
30
+ const tabs: { name: string }[] = [];
31
+ const panels: { children: RendererNode[] }[] = [];
32
+ for (const child of tag.children) {
33
+ if (!isTag(child) || child.name !== 'ul') continue;
34
+ for (const item of child.children) {
35
+ if (!isTag(item)) continue;
36
+ if (item.attributes?.typeof === 'Tab') {
37
+ const nameNode = item.children.find(
38
+ (c): c is SerializedTag => isTag(c) && c.attributes?.property === 'name'
39
+ );
40
+ tabs.push({ name: (nameNode ? getTextContent(nameNode) : getTextContent(item)).trim() });
41
+ } else if (item.attributes?.typeof === 'TabPanel') {
42
+ panels.push({ children: item.children });
43
+ }
44
+ }
45
+ }
46
+ return { tabs, panels };
47
+ });
48
+
49
+ let activeIndex = $state(0);
50
+ </script>
51
+
52
+ {#if tabs.length > 0}
53
+ <div class="rf-codegroup">
54
+ <div class="rf-codegroup__topbar">
55
+ <span class="rf-codegroup__dot"></span>
56
+ <span class="rf-codegroup__dot"></span>
57
+ <span class="rf-codegroup__dot"></span>
58
+ {#if title}
59
+ <span class="rf-codegroup__title">{title}</span>
60
+ {/if}
61
+ </div>
62
+ {#if tabs.length > 1}
63
+ <div class="rf-codegroup__tabs" role="tablist">
64
+ {#each tabs as tab, i}
65
+ <button
66
+ class="rf-codegroup__tab {i === activeIndex ? 'rf-codegroup__tab--active' : ''}"
67
+ role="tab"
68
+ aria-selected={i === activeIndex}
69
+ onclick={() => activeIndex = i}
70
+ >
71
+ {tab.name}
72
+ </button>
73
+ {/each}
74
+ </div>
75
+ {/if}
76
+ <div class="rf-codegroup__content">
77
+ {#each panels as panel, i}
78
+ {#if i === activeIndex}
79
+ <div class="rf-codegroup__panel" role="tabpanel">
80
+ <Renderer node={panel.children} />
81
+ </div>
82
+ {/if}
83
+ {/each}
84
+ </div>
85
+ </div>
86
+ {:else}
87
+ {@render children()}
88
+ {/if}