@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.
- package/CLAUDE.md +734 -0
- package/README.md +219 -1
- package/llms.txt +97 -0
- package/package.json +54 -14
- package/src/actions/index.js +375 -0
- package/src/components/docs/CodeBlock.svelte +203 -0
- package/src/components/docs/DocSection.svelte +120 -0
- package/src/components/docs/PropsTable.svelte +136 -0
- package/src/components/docs/SplitPane.svelte +98 -0
- package/src/components/docs/index.js +14 -0
- package/src/components/feedback/Alert.svelte +234 -0
- package/src/components/feedback/EmptyState.svelte +179 -0
- package/src/components/feedback/ProgressBar.svelte +116 -0
- package/src/components/feedback/Skeleton.svelte +107 -0
- package/src/components/feedback/Spinner.svelte +77 -0
- package/src/components/feedback/Toast.svelte +261 -0
- package/src/components/forms/Checkbox.svelte +147 -0
- package/src/components/forms/Dropzone.svelte +248 -0
- package/src/components/forms/FileUpload.svelte +266 -0
- package/src/components/forms/IconInput.svelte +184 -0
- package/src/components/forms/Input.svelte +121 -0
- package/src/components/forms/NumberInput.svelte +225 -0
- package/src/components/forms/PinInput.svelte +169 -0
- package/src/components/forms/Radio.svelte +143 -0
- package/src/components/forms/RadioGroup.svelte +62 -0
- package/src/components/forms/RangeSlider.svelte +212 -0
- package/src/components/forms/SearchInput.svelte +175 -0
- package/src/components/forms/Select.svelte +324 -0
- package/src/components/forms/Switch.svelte +159 -0
- package/src/components/forms/Textarea.svelte +122 -0
- package/src/components/navigation/Accordion.svelte +65 -0
- package/src/components/navigation/AccordionItem.svelte +146 -0
- package/src/components/navigation/NavigationContainer.svelte +344 -0
- package/src/components/navigation/Sidebar.svelte +334 -0
- package/src/components/navigation/SidebarAccountGroup.svelte +495 -0
- package/src/components/navigation/SidebarAccountItem.svelte +492 -0
- package/src/components/navigation/SidebarGroup.svelte +230 -0
- package/src/components/navigation/SidebarGroupSwitcher.svelte +262 -0
- package/src/components/navigation/SidebarItem.svelte +210 -0
- package/src/components/navigation/SidebarNavigationItem.svelte +470 -0
- package/src/components/navigation/SidebarPopover.svelte +145 -0
- package/src/components/navigation/SidebarSearch.svelte +236 -0
- package/src/components/navigation/SidebarSection.svelte +158 -0
- package/src/components/navigation/SidebarToggle.svelte +86 -0
- package/src/components/navigation/Tabs.svelte +239 -0
- package/src/components/navigation/WorkspaceMenu.svelte +416 -0
- package/src/components/navigation/blocks/NavigationAccountGroup.svelte +396 -0
- package/src/components/navigation/blocks/NavigationCustomBlock.svelte +74 -0
- package/src/components/navigation/blocks/NavigationGroupSwitcher.svelte +277 -0
- package/src/components/navigation/blocks/NavigationSearch.svelte +300 -0
- package/src/components/navigation/blocks/NavigationSection.svelte +230 -0
- package/src/components/navigation/index.js +22 -0
- package/src/components/overlays/ConfirmDialog.svelte +272 -0
- package/src/components/overlays/Dropdown.svelte +153 -0
- package/src/components/overlays/DropdownDivider.svelte +23 -0
- package/src/components/overlays/DropdownItem.svelte +97 -0
- package/src/components/overlays/Modal.svelte +232 -0
- package/src/components/overlays/Popover.svelte +206 -0
- package/src/components/primitives/Avatar.svelte +132 -0
- package/src/components/primitives/Badge.svelte +118 -0
- package/src/components/primitives/Button.svelte +214 -0
- package/src/components/primitives/Card.svelte +104 -0
- package/src/components/primitives/Divider.svelte +105 -0
- package/src/components/primitives/LazyImage.svelte +104 -0
- package/src/components/primitives/Link.svelte +122 -0
- package/src/components/primitives/Stat.svelte +197 -0
- package/src/components/primitives/StatusBadge.svelte +122 -0
- package/src/index.js +183 -0
- package/src/tokens/colors.css +157 -0
- package/src/tokens/effects.css +128 -0
- package/src/tokens/index.css +81 -0
- package/src/tokens/spacing.css +49 -0
- package/src/tokens/typography.css +79 -0
- package/src/utils/cn.svelte.js +175 -0
- package/src/utils/highlighter.js +124 -0
- package/src/utils/index.js +22 -0
- package/src/utils/navigation.svelte.js +423 -0
- package/src/utils/reactive.svelte.js +328 -0
- package/src/utils/sidebar.svelte.js +211 -0
- package/jera.js +0 -135
- package/www/components/jera/Input/Input.svelte +0 -63
- 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, '&')
|
|
55
|
+
.replace(/</g, '<')
|
|
56
|
+
.replace(/>/g, '>');
|
|
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>
|