@miozu/jera 0.0.2 → 0.4.2

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 (82) hide show
  1. package/CLAUDE.md +734 -0
  2. package/README.md +219 -1
  3. package/llms.txt +97 -0
  4. package/package.json +54 -14
  5. package/src/actions/index.js +375 -0
  6. package/src/components/docs/CodeBlock.svelte +203 -0
  7. package/src/components/docs/DocSection.svelte +120 -0
  8. package/src/components/docs/PropsTable.svelte +136 -0
  9. package/src/components/docs/SplitPane.svelte +98 -0
  10. package/src/components/docs/index.js +14 -0
  11. package/src/components/feedback/Alert.svelte +234 -0
  12. package/src/components/feedback/EmptyState.svelte +179 -0
  13. package/src/components/feedback/ProgressBar.svelte +116 -0
  14. package/src/components/feedback/Skeleton.svelte +107 -0
  15. package/src/components/feedback/Spinner.svelte +77 -0
  16. package/src/components/feedback/Toast.svelte +261 -0
  17. package/src/components/forms/Checkbox.svelte +147 -0
  18. package/src/components/forms/Dropzone.svelte +248 -0
  19. package/src/components/forms/FileUpload.svelte +266 -0
  20. package/src/components/forms/IconInput.svelte +184 -0
  21. package/src/components/forms/Input.svelte +121 -0
  22. package/src/components/forms/NumberInput.svelte +225 -0
  23. package/src/components/forms/PinInput.svelte +169 -0
  24. package/src/components/forms/Radio.svelte +143 -0
  25. package/src/components/forms/RadioGroup.svelte +62 -0
  26. package/src/components/forms/RangeSlider.svelte +212 -0
  27. package/src/components/forms/SearchInput.svelte +175 -0
  28. package/src/components/forms/Select.svelte +324 -0
  29. package/src/components/forms/Switch.svelte +159 -0
  30. package/src/components/forms/Textarea.svelte +122 -0
  31. package/src/components/navigation/Accordion.svelte +65 -0
  32. package/src/components/navigation/AccordionItem.svelte +146 -0
  33. package/src/components/navigation/NavigationContainer.svelte +344 -0
  34. package/src/components/navigation/Sidebar.svelte +334 -0
  35. package/src/components/navigation/SidebarAccountGroup.svelte +495 -0
  36. package/src/components/navigation/SidebarAccountItem.svelte +492 -0
  37. package/src/components/navigation/SidebarGroup.svelte +230 -0
  38. package/src/components/navigation/SidebarGroupSwitcher.svelte +262 -0
  39. package/src/components/navigation/SidebarItem.svelte +210 -0
  40. package/src/components/navigation/SidebarNavigationItem.svelte +470 -0
  41. package/src/components/navigation/SidebarPopover.svelte +145 -0
  42. package/src/components/navigation/SidebarSearch.svelte +236 -0
  43. package/src/components/navigation/SidebarSection.svelte +158 -0
  44. package/src/components/navigation/SidebarToggle.svelte +86 -0
  45. package/src/components/navigation/Tabs.svelte +239 -0
  46. package/src/components/navigation/WorkspaceMenu.svelte +416 -0
  47. package/src/components/navigation/blocks/NavigationAccountGroup.svelte +396 -0
  48. package/src/components/navigation/blocks/NavigationCustomBlock.svelte +74 -0
  49. package/src/components/navigation/blocks/NavigationGroupSwitcher.svelte +277 -0
  50. package/src/components/navigation/blocks/NavigationSearch.svelte +300 -0
  51. package/src/components/navigation/blocks/NavigationSection.svelte +230 -0
  52. package/src/components/navigation/index.js +22 -0
  53. package/src/components/overlays/ConfirmDialog.svelte +272 -0
  54. package/src/components/overlays/Dropdown.svelte +153 -0
  55. package/src/components/overlays/DropdownDivider.svelte +23 -0
  56. package/src/components/overlays/DropdownItem.svelte +97 -0
  57. package/src/components/overlays/Modal.svelte +232 -0
  58. package/src/components/overlays/Popover.svelte +206 -0
  59. package/src/components/primitives/Avatar.svelte +132 -0
  60. package/src/components/primitives/Badge.svelte +118 -0
  61. package/src/components/primitives/Button.svelte +214 -0
  62. package/src/components/primitives/Card.svelte +104 -0
  63. package/src/components/primitives/Divider.svelte +105 -0
  64. package/src/components/primitives/LazyImage.svelte +104 -0
  65. package/src/components/primitives/Link.svelte +122 -0
  66. package/src/components/primitives/Stat.svelte +197 -0
  67. package/src/components/primitives/StatusBadge.svelte +122 -0
  68. package/src/index.js +183 -0
  69. package/src/tokens/colors.css +157 -0
  70. package/src/tokens/effects.css +128 -0
  71. package/src/tokens/index.css +81 -0
  72. package/src/tokens/spacing.css +49 -0
  73. package/src/tokens/typography.css +79 -0
  74. package/src/utils/cn.svelte.js +175 -0
  75. package/src/utils/highlighter.js +124 -0
  76. package/src/utils/index.js +22 -0
  77. package/src/utils/navigation.svelte.js +423 -0
  78. package/src/utils/reactive.svelte.js +328 -0
  79. package/src/utils/sidebar.svelte.js +211 -0
  80. package/jera.js +0 -135
  81. package/www/components/jera/Input/Input.svelte +0 -63
  82. package/www/components/jera/Input/index.js +0 -1
@@ -0,0 +1,375 @@
1
+ /**
2
+ * Svelte Actions Library
3
+ *
4
+ * Reusable element behaviors that can be composed onto any element.
5
+ * Actions are the Svelte way to add imperative logic to elements.
6
+ *
7
+ * @example
8
+ * <div use:clickOutside={handleClose}>...</div>
9
+ * <input use:focus />
10
+ * <button use:longPress={{ duration: 500, onLongPress: handler }}>...</button>
11
+ */
12
+
13
+ /**
14
+ * Click Outside Action
15
+ *
16
+ * Fires callback when clicking outside the element.
17
+ * Useful for dropdowns, modals, popovers.
18
+ *
19
+ * @param {HTMLElement} node
20
+ * @param {() => void} callback
21
+ * @returns {{ destroy: () => void, update: (cb: () => void) => void }}
22
+ *
23
+ * @example
24
+ * <div use:clickOutside={() => isOpen = false}>Dropdown</div>
25
+ */
26
+ export function clickOutside(node, callback) {
27
+ let handler = callback;
28
+
29
+ function handleClick(event) {
30
+ if (!node.contains(event.target) && !event.defaultPrevented) {
31
+ handler?.();
32
+ }
33
+ }
34
+
35
+ // Use mousedown for better UX (fires before focus changes)
36
+ document.addEventListener('mousedown', handleClick, true);
37
+
38
+ return {
39
+ update(newCallback) {
40
+ handler = newCallback;
41
+ },
42
+ destroy() {
43
+ document.removeEventListener('mousedown', handleClick, true);
44
+ }
45
+ };
46
+ }
47
+
48
+ /**
49
+ * Focus Trap Action
50
+ *
51
+ * Traps focus within an element. Essential for accessible modals.
52
+ *
53
+ * @param {HTMLElement} node
54
+ * @param {{ enabled?: boolean, initialFocus?: string }} [options]
55
+ * @returns {{ destroy: () => void, update: (opts: { enabled?: boolean }) => void }}
56
+ *
57
+ * @example
58
+ * <dialog use:focusTrap={{ enabled: isOpen }}>...</dialog>
59
+ */
60
+ export function focusTrap(node, options = {}) {
61
+ let { enabled = true, initialFocus } = options;
62
+
63
+ const focusableSelectors = [
64
+ 'a[href]',
65
+ 'button:not([disabled])',
66
+ 'input:not([disabled])',
67
+ 'select:not([disabled])',
68
+ 'textarea:not([disabled])',
69
+ '[tabindex]:not([tabindex="-1"])'
70
+ ].join(',');
71
+
72
+ function getFocusable() {
73
+ return Array.from(node.querySelectorAll(focusableSelectors));
74
+ }
75
+
76
+ function handleKeydown(event) {
77
+ if (!enabled || event.key !== 'Tab') return;
78
+
79
+ const focusable = getFocusable();
80
+ if (focusable.length === 0) return;
81
+
82
+ const first = focusable[0];
83
+ const last = focusable[focusable.length - 1];
84
+
85
+ if (event.shiftKey && document.activeElement === first) {
86
+ event.preventDefault();
87
+ last.focus();
88
+ } else if (!event.shiftKey && document.activeElement === last) {
89
+ event.preventDefault();
90
+ first.focus();
91
+ }
92
+ }
93
+
94
+ function setInitialFocus() {
95
+ if (!enabled) return;
96
+
97
+ const target = initialFocus
98
+ ? node.querySelector(initialFocus)
99
+ : getFocusable()[0];
100
+
101
+ target?.focus();
102
+ }
103
+
104
+ node.addEventListener('keydown', handleKeydown);
105
+ // Delay initial focus to allow transitions
106
+ requestAnimationFrame(setInitialFocus);
107
+
108
+ return {
109
+ update(newOptions) {
110
+ enabled = newOptions.enabled ?? true;
111
+ initialFocus = newOptions.initialFocus;
112
+ if (enabled) setInitialFocus();
113
+ },
114
+ destroy() {
115
+ node.removeEventListener('keydown', handleKeydown);
116
+ }
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Auto-focus Action
122
+ *
123
+ * Focuses element on mount.
124
+ *
125
+ * @param {HTMLElement} node
126
+ * @param {{ delay?: number, select?: boolean }} [options]
127
+ *
128
+ * @example
129
+ * <input use:autoFocus />
130
+ * <input use:autoFocus={{ delay: 100, select: true }} />
131
+ */
132
+ export function autoFocus(node, options = {}) {
133
+ const { delay = 0, select = false } = options;
134
+
135
+ const timeout = setTimeout(() => {
136
+ node.focus();
137
+ if (select && 'select' in node) {
138
+ node.select();
139
+ }
140
+ }, delay);
141
+
142
+ return {
143
+ destroy() {
144
+ clearTimeout(timeout);
145
+ }
146
+ };
147
+ }
148
+
149
+ /**
150
+ * Long Press Action
151
+ *
152
+ * Detects long press/hold gesture.
153
+ *
154
+ * @param {HTMLElement} node
155
+ * @param {{ duration?: number, onLongPress: () => void }} options
156
+ *
157
+ * @example
158
+ * <button use:longPress={{ duration: 500, onLongPress: handleLongPress }}>
159
+ * Hold me
160
+ * </button>
161
+ */
162
+ export function longPress(node, options) {
163
+ let { duration = 500, onLongPress } = options;
164
+ let timeout = null;
165
+
166
+ function handleStart() {
167
+ timeout = setTimeout(() => {
168
+ onLongPress?.();
169
+ }, duration);
170
+ }
171
+
172
+ function handleEnd() {
173
+ if (timeout) {
174
+ clearTimeout(timeout);
175
+ timeout = null;
176
+ }
177
+ }
178
+
179
+ node.addEventListener('mousedown', handleStart);
180
+ node.addEventListener('touchstart', handleStart, { passive: true });
181
+ node.addEventListener('mouseup', handleEnd);
182
+ node.addEventListener('mouseleave', handleEnd);
183
+ node.addEventListener('touchend', handleEnd);
184
+ node.addEventListener('touchcancel', handleEnd);
185
+
186
+ return {
187
+ update(newOptions) {
188
+ duration = newOptions.duration ?? 500;
189
+ onLongPress = newOptions.onLongPress;
190
+ },
191
+ destroy() {
192
+ handleEnd();
193
+ node.removeEventListener('mousedown', handleStart);
194
+ node.removeEventListener('touchstart', handleStart);
195
+ node.removeEventListener('mouseup', handleEnd);
196
+ node.removeEventListener('mouseleave', handleEnd);
197
+ node.removeEventListener('touchend', handleEnd);
198
+ node.removeEventListener('touchcancel', handleEnd);
199
+ }
200
+ };
201
+ }
202
+
203
+ /**
204
+ * Escape Key Action
205
+ *
206
+ * Fires callback when Escape is pressed while element or children have focus.
207
+ *
208
+ * @param {HTMLElement} node
209
+ * @param {() => void} callback
210
+ *
211
+ * @example
212
+ * <dialog use:escapeKey={handleClose}>...</dialog>
213
+ */
214
+ export function escapeKey(node, callback) {
215
+ let handler = callback;
216
+
217
+ function handleKeydown(event) {
218
+ if (event.key === 'Escape') {
219
+ event.preventDefault();
220
+ handler?.();
221
+ }
222
+ }
223
+
224
+ // Listen on document for global escape handling
225
+ document.addEventListener('keydown', handleKeydown);
226
+
227
+ return {
228
+ update(newCallback) {
229
+ handler = newCallback;
230
+ },
231
+ destroy() {
232
+ document.removeEventListener('keydown', handleKeydown);
233
+ }
234
+ };
235
+ }
236
+
237
+ /**
238
+ * Portal Action
239
+ *
240
+ * Moves element to a different location in the DOM.
241
+ * Useful for modals, toasts, tooltips.
242
+ *
243
+ * @param {HTMLElement} node
244
+ * @param {string | HTMLElement} [target] - CSS selector or element (default: document.body)
245
+ *
246
+ * @example
247
+ * <div use:portal>Portaled to body</div>
248
+ * <div use:portal="#modal-root">Portaled to #modal-root</div>
249
+ */
250
+ export function portal(node, target = 'body') {
251
+ let targetEl;
252
+
253
+ function update(newTarget) {
254
+ targetEl = typeof newTarget === 'string'
255
+ ? document.querySelector(newTarget)
256
+ : newTarget;
257
+
258
+ if (targetEl) {
259
+ targetEl.appendChild(node);
260
+ }
261
+ }
262
+
263
+ update(target);
264
+
265
+ return {
266
+ update,
267
+ destroy() {
268
+ node.remove();
269
+ }
270
+ };
271
+ }
272
+
273
+ /**
274
+ * Intersection Observer Action
275
+ *
276
+ * Observes element visibility in viewport.
277
+ *
278
+ * @param {HTMLElement} node
279
+ * @param {{ onIntersect: (entry: IntersectionObserverEntry) => void, options?: IntersectionObserverInit }} config
280
+ *
281
+ * @example
282
+ * <div use:intersect={{ onIntersect: (e) => isVisible = e.isIntersecting }}>
283
+ * Lazy loaded content
284
+ * </div>
285
+ */
286
+ export function intersect(node, config) {
287
+ let { onIntersect, options = {} } = config;
288
+
289
+ const observer = new IntersectionObserver((entries) => {
290
+ entries.forEach(entry => onIntersect?.(entry));
291
+ }, options);
292
+
293
+ observer.observe(node);
294
+
295
+ return {
296
+ update(newConfig) {
297
+ onIntersect = newConfig.onIntersect;
298
+ },
299
+ destroy() {
300
+ observer.disconnect();
301
+ }
302
+ };
303
+ }
304
+
305
+ /**
306
+ * Resize Observer Action
307
+ *
308
+ * Observes element size changes.
309
+ *
310
+ * @param {HTMLElement} node
311
+ * @param {(entry: ResizeObserverEntry) => void} callback
312
+ *
313
+ * @example
314
+ * <div use:resize={(entry) => width = entry.contentRect.width}>
315
+ * Responsive content
316
+ * </div>
317
+ */
318
+ export function resize(node, callback) {
319
+ let handler = callback;
320
+
321
+ const observer = new ResizeObserver((entries) => {
322
+ entries.forEach(entry => handler?.(entry));
323
+ });
324
+
325
+ observer.observe(node);
326
+
327
+ return {
328
+ update(newCallback) {
329
+ handler = newCallback;
330
+ },
331
+ destroy() {
332
+ observer.disconnect();
333
+ }
334
+ };
335
+ }
336
+
337
+ /**
338
+ * Copy to Clipboard Action
339
+ *
340
+ * Copies element's text content or provided value on click.
341
+ *
342
+ * @param {HTMLElement} node
343
+ * @param {{ value?: string, onCopy?: () => void, onError?: (err: Error) => void }} [options]
344
+ *
345
+ * @example
346
+ * <button use:copy={{ value: 'Text to copy', onCopy: () => showToast('Copied!') }}>
347
+ * Copy
348
+ * </button>
349
+ */
350
+ export function copy(node, options = {}) {
351
+ let { value, onCopy, onError } = options;
352
+
353
+ async function handleClick() {
354
+ try {
355
+ const text = value ?? node.textContent ?? '';
356
+ await navigator.clipboard.writeText(text);
357
+ onCopy?.();
358
+ } catch (err) {
359
+ onError?.(err);
360
+ }
361
+ }
362
+
363
+ node.addEventListener('click', handleClick);
364
+
365
+ return {
366
+ update(newOptions) {
367
+ value = newOptions.value;
368
+ onCopy = newOptions.onCopy;
369
+ onError = newOptions.onError;
370
+ },
371
+ destroy() {
372
+ node.removeEventListener('click', handleClick);
373
+ }
374
+ };
375
+ }
@@ -0,0 +1,203 @@
1
+ <!--
2
+ @component CodeBlock
3
+
4
+ Syntax-highlighted code block with copy-to-clipboard functionality.
5
+ Uses Shiki for highlighting via CSS variables for theme compatibility.
6
+
7
+ @example
8
+ ```svelte
9
+ <CodeBlock
10
+ code={`const greeting = "Hello, World!";`}
11
+ lang="javascript"
12
+ filename="example.js"
13
+ />
14
+ ```
15
+ -->
16
+ <script>
17
+ import {getHighlighter} from '../../utils/highlighter.js';
18
+ import {Copy, Check} from '@lucide/svelte';
19
+
20
+ let {
21
+ /** The code to display */
22
+ code = '',
23
+ /** Language for syntax highlighting */
24
+ lang = 'javascript',
25
+ /** Optional filename to display in header */
26
+ filename = '',
27
+ /** Show line numbers */
28
+ showLineNumbers = false,
29
+ /** Additional CSS classes */
30
+ class: className = ''
31
+ } = $props();
32
+
33
+ let copied = $state(false);
34
+ let highlightedCode = $state('');
35
+
36
+ $effect(() => {
37
+ highlightCodeAsync();
38
+ });
39
+
40
+ async function highlightCodeAsync() {
41
+ try {
42
+ const highlighter = await getHighlighter();
43
+ highlightedCode = highlighter.codeToHtml(code.trim(), {
44
+ lang: lang || 'text',
45
+ theme: 'css-variables'
46
+ });
47
+ } catch (e) {
48
+ highlightedCode = `<pre><code>${escapeHtml(code)}</code></pre>`;
49
+ }
50
+ }
51
+
52
+ function escapeHtml(str) {
53
+ return str
54
+ .replace(/&/g, '&amp;')
55
+ .replace(/</g, '&lt;')
56
+ .replace(/>/g, '&gt;');
57
+ }
58
+
59
+ async function copyCode() {
60
+ try {
61
+ await navigator.clipboard.writeText(code.trim());
62
+ copied = true;
63
+ setTimeout(() => copied = false, 2000);
64
+ } catch (e) {
65
+ console.error('Failed to copy:', e);
66
+ }
67
+ }
68
+ </script>
69
+
70
+ <div class="jera-code-block {className}">
71
+ {#if filename}
72
+ <div class="code-header">
73
+ <span class="code-filename">{filename}</span>
74
+ <span class="code-lang">{lang}</span>
75
+ </div>
76
+ {/if}
77
+
78
+ <div class="code-container">
79
+ <button
80
+ class="copy-btn"
81
+ onclick={copyCode}
82
+ aria-label={copied ? 'Copied!' : 'Copy code'}
83
+ >
84
+ {#if copied}
85
+ <Check size={16} />
86
+ {:else}
87
+ <Copy size={16} />
88
+ {/if}
89
+ </button>
90
+
91
+ <div class="code-content" class:with-lines={showLineNumbers}>
92
+ {@html highlightedCode}
93
+ </div>
94
+ </div>
95
+ </div>
96
+
97
+ <style>
98
+ .jera-code-block {
99
+ position: relative;
100
+ border-radius: var(--radius-md, 0.5rem);
101
+ overflow: hidden;
102
+ background: var(--color-base1, var(--base1, #1a1f26));
103
+ border: 1px solid var(--color-border, var(--base2, #2d3748));
104
+ }
105
+
106
+ .code-header {
107
+ display: flex;
108
+ justify-content: space-between;
109
+ align-items: center;
110
+ padding: 0.5rem 1rem;
111
+ background: var(--color-base0, var(--base0, #0f1419));
112
+ border-bottom: 1px solid var(--color-border, var(--base2, #2d3748));
113
+ font-size: 0.75rem;
114
+ }
115
+
116
+ .code-filename {
117
+ color: var(--color-base5, var(--base5, #e2e8f0));
118
+ font-family: var(--font-mono, monospace);
119
+ }
120
+
121
+ .code-lang {
122
+ color: var(--color-base4, var(--base4, #a0aec0));
123
+ text-transform: uppercase;
124
+ font-size: 0.625rem;
125
+ letter-spacing: 0.05em;
126
+ }
127
+
128
+ .code-container {
129
+ position: relative;
130
+ }
131
+
132
+ .copy-btn {
133
+ position: absolute;
134
+ top: 0.75rem;
135
+ right: 0.75rem;
136
+ z-index: 10;
137
+ padding: 0.5rem;
138
+ border: none;
139
+ border-radius: var(--radius-sm, 0.25rem);
140
+ background: var(--color-base2, var(--base2, #242a33));
141
+ color: var(--color-base4, var(--base4, #a0aec0));
142
+ cursor: pointer;
143
+ opacity: 0;
144
+ transition: opacity 0.15s, background 0.15s, color 0.15s;
145
+ }
146
+
147
+ .jera-code-block:hover .copy-btn {
148
+ opacity: 1;
149
+ }
150
+
151
+ .copy-btn:hover {
152
+ background: var(--color-base3, var(--base3, #4a5568));
153
+ color: var(--color-base6, var(--base6, #f7fafc));
154
+ }
155
+
156
+ .code-content {
157
+ overflow-x: auto;
158
+ }
159
+
160
+ .code-content :global(pre) {
161
+ margin: 0;
162
+ padding: 1rem;
163
+ background: transparent !important;
164
+ font-family: var(--font-mono, monospace);
165
+ font-size: 0.875rem;
166
+ line-height: 1.6;
167
+ }
168
+
169
+ .code-content :global(code) {
170
+ font-family: inherit;
171
+ }
172
+
173
+ /* Shiki CSS variables theme - compatible with both jera and selify themes */
174
+ .code-content {
175
+ --shiki-color-text: var(--color-base5, var(--base5, #e2e8f0));
176
+ --shiki-color-background: transparent;
177
+ --shiki-token-constant: var(--blue, #83d2fc);
178
+ --shiki-token-string: var(--green, #6dd672);
179
+ --shiki-token-comment: var(--base3, #565e78);
180
+ --shiki-token-keyword: var(--magenta, #c974e6);
181
+ --shiki-token-parameter: var(--peach, #ff9982);
182
+ --shiki-token-function: var(--blue, #83d2fc);
183
+ --shiki-token-string-expression: var(--green, #6dd672);
184
+ --shiki-token-punctuation: var(--color-base4, var(--base4, #a0aec0));
185
+ --shiki-token-link: var(--blue, #83d2fc);
186
+ }
187
+
188
+ /* Line numbers */
189
+ .with-lines :global(pre) {
190
+ counter-reset: line;
191
+ }
192
+
193
+ .with-lines :global(.line::before) {
194
+ counter-increment: line;
195
+ content: counter(line);
196
+ display: inline-block;
197
+ width: 2rem;
198
+ margin-right: 1rem;
199
+ text-align: right;
200
+ color: var(--color-base3, var(--base3, #4a5568));
201
+ user-select: none;
202
+ }
203
+ </style>
@@ -0,0 +1,120 @@
1
+ <!--
2
+ @component DocSection
3
+
4
+ A documentation section with optional anchor link, title, description,
5
+ and content area. Useful for organizing documentation into scannable sections.
6
+
7
+ @example
8
+ ```svelte
9
+ <DocSection id="installation" title="Installation" description="How to install the package">
10
+ <CodeBlock code="npm install @miozu/jera" lang="bash" />
11
+ </DocSection>
12
+ ```
13
+ -->
14
+ <script>
15
+ import {Link2} from '@lucide/svelte';
16
+
17
+ let {
18
+ /** Unique ID for anchor links */
19
+ id = '',
20
+ /** Section title */
21
+ title = '',
22
+ /** Optional description below title */
23
+ description = '',
24
+ /** Heading level (2-6) */
25
+ level = 2,
26
+ /** Show anchor link on hover */
27
+ showAnchor = true,
28
+ /** Children content */
29
+ children,
30
+ /** Additional CSS classes */
31
+ class: className = ''
32
+ } = $props();
33
+
34
+ const Tag = `h${level}`;
35
+ </script>
36
+
37
+ <section class="jera-doc-section {className}" {id}>
38
+ {#if title}
39
+ <div class="section-header">
40
+ <svelte:element this={Tag} class="section-title">
41
+ {title}
42
+ {#if showAnchor && id}
43
+ <a href="#{id}" class="anchor-link" aria-label="Link to {title}">
44
+ <Link2 size={16} />
45
+ </a>
46
+ {/if}
47
+ </svelte:element>
48
+ {#if description}
49
+ <p class="section-description">{description}</p>
50
+ {/if}
51
+ </div>
52
+ {/if}
53
+
54
+ <div class="section-content">
55
+ {@render children?.()}
56
+ </div>
57
+ </section>
58
+
59
+ <style>
60
+ .jera-doc-section {
61
+ margin-bottom: 2rem;
62
+ scroll-margin-top: 2rem;
63
+ }
64
+
65
+ .section-header {
66
+ margin-bottom: 1rem;
67
+ }
68
+
69
+ .section-title {
70
+ display: flex;
71
+ align-items: center;
72
+ gap: 0.5rem;
73
+ margin: 0 0 0.5rem 0;
74
+ font-weight: 600;
75
+ color: var(--color-base6, var(--base6, #f3f4f7));
76
+ }
77
+
78
+ h2.section-title {
79
+ font-size: 1.5rem;
80
+ padding-bottom: 0.5rem;
81
+ border-bottom: 1px solid var(--color-border, var(--base2, #2d3748));
82
+ }
83
+
84
+ h3.section-title {
85
+ font-size: 1.25rem;
86
+ }
87
+
88
+ h4.section-title {
89
+ font-size: 1.125rem;
90
+ }
91
+
92
+ h5.section-title,
93
+ h6.section-title {
94
+ font-size: 1rem;
95
+ }
96
+
97
+ .anchor-link {
98
+ opacity: 0;
99
+ color: var(--color-base4, var(--base4, #a0aec0));
100
+ transition: opacity 0.15s, color 0.15s;
101
+ }
102
+
103
+ .section-title:hover .anchor-link {
104
+ opacity: 1;
105
+ }
106
+
107
+ .anchor-link:hover {
108
+ color: var(--color-primary, var(--magenta, #c974e6));
109
+ }
110
+
111
+ .section-description {
112
+ margin: 0;
113
+ color: var(--color-base4, var(--base4, #a0aec0));
114
+ line-height: 1.6;
115
+ }
116
+
117
+ .section-content {
118
+ /* Content inherits full width */
119
+ }
120
+ </style>