@svadmin/ui 0.74.0 → 0.76.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.css +14 -0
- package/dist/app.theme.css +14 -0
- package/dist/components/AdminApp.svelte +5 -5
- package/dist/components/AppFooter.svelte +137 -0
- package/dist/components/AppFooter.svelte.d.ts +25 -0
- package/dist/components/CodeEditor.svelte +145 -0
- package/dist/components/CodeEditor.svelte.d.ts +26 -0
- package/dist/components/DatePicker.svelte +2 -2
- package/dist/components/DatePicker.svelte.d.ts +1 -1
- package/dist/components/DateRangePicker.svelte +2 -2
- package/dist/components/DateRangePicker.svelte.d.ts +1 -1
- package/dist/components/DateTimeInput.svelte +18 -18
- package/dist/components/DateTimeInput.svelte.d.ts +18 -18
- package/dist/components/DateTimePicker.svelte +2 -2
- package/dist/components/DateTimePicker.svelte.d.ts +1 -1
- package/dist/components/FieldRenderer.svelte +1 -1
- package/dist/components/HighlightText.svelte +46 -0
- package/dist/components/HighlightText.svelte.d.ts +13 -0
- package/dist/components/JsonEditor.svelte +182 -0
- package/dist/components/JsonEditor.svelte.d.ts +22 -0
- package/dist/components/LazyPage.svelte +3 -3
- package/dist/components/LazyPage.svelte.d.ts +2 -2
- package/dist/components/Marquee.svelte +104 -0
- package/dist/components/Marquee.svelte.d.ts +20 -0
- package/dist/components/MediaPlayer.svelte +104 -0
- package/dist/components/MediaPlayer.svelte.d.ts +21 -0
- package/dist/components/PasswordInput.svelte +3 -3
- package/dist/components/PasswordInput.svelte.d.ts +2 -1
- package/dist/components/PhoneInput.svelte +157 -0
- package/dist/components/PhoneInput.svelte.d.ts +26 -0
- package/dist/components/QRCode.svelte +88 -0
- package/dist/components/QRCode.svelte.d.ts +22 -0
- package/dist/components/ResizableGrid.svelte +252 -0
- package/dist/components/ResizableGrid.svelte.d.ts +34 -0
- package/dist/components/ResourceOperationsPage.svelte +5 -1
- package/dist/components/TimePicker.svelte +2 -2
- package/dist/components/TimePicker.svelte.d.ts +1 -1
- package/dist/components/TooltipButton.svelte +3 -4
- package/dist/components/TopProgressBar.svelte +90 -0
- package/dist/components/TopProgressBar.svelte.d.ts +15 -0
- package/dist/components/browser.svelte.d.ts +65 -0
- package/dist/components/browser.svelte.js +228 -0
- package/dist/components/charts/AreaChart.svelte +78 -0
- package/dist/components/charts/AreaChart.svelte.d.ts +17 -0
- package/dist/components/charts/ScatterChart.svelte +82 -0
- package/dist/components/charts/ScatterChart.svelte.d.ts +17 -0
- package/dist/components/charts/index.d.ts +2 -0
- package/dist/components/charts/index.js +2 -0
- package/dist/components/code-editor-presets.d.ts +14 -0
- package/dist/components/code-editor-presets.js +14 -0
- package/dist/components/fields/MarkdownField.svelte +14 -5
- package/dist/components/highlight.d.ts +16 -0
- package/dist/components/highlight.js +68 -0
- package/dist/components/media.svelte.d.ts +27 -0
- package/dist/components/media.svelte.js +60 -0
- package/dist/components/phone-dial-codes.d.ts +19 -0
- package/dist/components/phone-dial-codes.js +112 -0
- package/dist/components/ui/DesignSystemShowcase.svelte +1 -1
- package/dist/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +1 -2
- package/dist/editor-config.svelte.d.ts +4 -2
- package/dist/index.d.ts +19 -1
- package/dist/index.js +14 -1
- package/dist/styles/recipes.css +1262 -0
- package/dist/types.d.ts +4 -4
- package/package.json +64 -4
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../utils.js';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
/** 0–100 progress. Leave undefined for an idle/hidden bar. */
|
|
6
|
+
value?: number | null;
|
|
7
|
+
/** Renders an animated unknown-progress bar regardless of `value`. */
|
|
8
|
+
indeterminate?: boolean;
|
|
9
|
+
/** Bar thickness in pixels. */
|
|
10
|
+
height?: number;
|
|
11
|
+
/** Pins the bar to the top edge of the viewport. */
|
|
12
|
+
fixed?: boolean;
|
|
13
|
+
ariaLabel?: string;
|
|
14
|
+
class?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let {
|
|
18
|
+
value,
|
|
19
|
+
indeterminate = false,
|
|
20
|
+
height = 3,
|
|
21
|
+
fixed = true,
|
|
22
|
+
ariaLabel = 'Loading',
|
|
23
|
+
class: className = '',
|
|
24
|
+
}: Props = $props();
|
|
25
|
+
|
|
26
|
+
const visible = $derived(indeterminate || (value !== null && value !== undefined));
|
|
27
|
+
const clamped = $derived(Math.max(0, Math.min(100, value ?? 0)));
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
{#if visible}
|
|
31
|
+
<div
|
|
32
|
+
class={cn('svadmin-top-progress', className)}
|
|
33
|
+
data-slot="top-progress"
|
|
34
|
+
data-fixed={fixed ? 'true' : 'false'}
|
|
35
|
+
style={`--svadmin-top-progress-height: ${height}px;`}
|
|
36
|
+
role="progressbar"
|
|
37
|
+
aria-label={ariaLabel}
|
|
38
|
+
aria-valuemin="0"
|
|
39
|
+
aria-valuemax="100"
|
|
40
|
+
aria-valuenow={indeterminate ? undefined : clamped}
|
|
41
|
+
>
|
|
42
|
+
<span
|
|
43
|
+
class="svadmin-top-progress__bar"
|
|
44
|
+
data-indeterminate={indeterminate ? 'true' : 'false'}
|
|
45
|
+
style={indeterminate ? undefined : `transform: scaleX(${clamped / 100});`}
|
|
46
|
+
></span>
|
|
47
|
+
</div>
|
|
48
|
+
{/if}
|
|
49
|
+
|
|
50
|
+
<style>
|
|
51
|
+
.svadmin-top-progress {
|
|
52
|
+
position: relative;
|
|
53
|
+
width: 100%;
|
|
54
|
+
height: var(--svadmin-top-progress-height, 3px);
|
|
55
|
+
overflow: hidden;
|
|
56
|
+
background: var(--muted, color-mix(in oklch, var(--foreground, currentColor) 12%, transparent));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.svadmin-top-progress[data-fixed='true'] {
|
|
60
|
+
position: fixed;
|
|
61
|
+
inset-block-start: 0;
|
|
62
|
+
inset-inline: 0;
|
|
63
|
+
z-index: 9999;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.svadmin-top-progress__bar {
|
|
67
|
+
display: block;
|
|
68
|
+
width: 100%;
|
|
69
|
+
height: 100%;
|
|
70
|
+
transform-origin: left center;
|
|
71
|
+
background: var(--primary, currentColor);
|
|
72
|
+
transition: transform 240ms ease-out;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.svadmin-top-progress__bar[data-indeterminate='true'] {
|
|
76
|
+
width: 40%;
|
|
77
|
+
transform: translateX(-100%);
|
|
78
|
+
animation: svadmin-top-progress-indeterminate 1.1s ease-in-out infinite;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@keyframes svadmin-top-progress-indeterminate {
|
|
82
|
+
0% { transform: translateX(-100%); }
|
|
83
|
+
100% { transform: translateX(350%); }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@media (prefers-reduced-motion: reduce) {
|
|
87
|
+
.svadmin-top-progress__bar { transition: none; }
|
|
88
|
+
.svadmin-top-progress__bar[data-indeterminate='true'] { animation-duration: 2.2s; }
|
|
89
|
+
}
|
|
90
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
/** 0–100 progress. Leave undefined for an idle/hidden bar. */
|
|
3
|
+
value?: number | null;
|
|
4
|
+
/** Renders an animated unknown-progress bar regardless of `value`. */
|
|
5
|
+
indeterminate?: boolean;
|
|
6
|
+
/** Bar thickness in pixels. */
|
|
7
|
+
height?: number;
|
|
8
|
+
/** Pins the bar to the top edge of the viewport. */
|
|
9
|
+
fixed?: boolean;
|
|
10
|
+
ariaLabel?: string;
|
|
11
|
+
class?: string;
|
|
12
|
+
}
|
|
13
|
+
declare const TopProgressBar: import("svelte").Component<Props, {}, "">;
|
|
14
|
+
type TopProgressBar = ReturnType<typeof TopProgressBar>;
|
|
15
|
+
export default TopProgressBar;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Svelte-native browser hooks. Every hook must be called during component
|
|
3
|
+
* initialisation because each one registers a scoped effect; SSR renders the
|
|
4
|
+
* documented fallback and hydration corrects the value.
|
|
5
|
+
*/
|
|
6
|
+
export interface ClipboardState {
|
|
7
|
+
/** True for `timeout` ms after a successful copy. */
|
|
8
|
+
readonly copied: boolean;
|
|
9
|
+
/** Resolves to `true` when the text reached the clipboard. */
|
|
10
|
+
copy(text: string): Promise<boolean>;
|
|
11
|
+
}
|
|
12
|
+
export interface ClipboardOptions {
|
|
13
|
+
/** Feedback reset delay in ms. Defaults to 1600. */
|
|
14
|
+
timeout?: number;
|
|
15
|
+
}
|
|
16
|
+
export declare function useClipboard(options?: ClipboardOptions): ClipboardState;
|
|
17
|
+
export interface LocalStorageState<T> {
|
|
18
|
+
readonly value: T;
|
|
19
|
+
set(next: T): void;
|
|
20
|
+
remove(): void;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Reactive `localStorage` value with cross-tab synchronization. Serialization
|
|
24
|
+
* failures fall back to the initial value instead of throwing.
|
|
25
|
+
*/
|
|
26
|
+
export declare function useLocalStorage<T>(key: string, initial: T): LocalStorageState<T>;
|
|
27
|
+
export interface DebouncedValue<T> {
|
|
28
|
+
readonly value: T;
|
|
29
|
+
}
|
|
30
|
+
/** Trailing-edge debounce over a reactive getter. */
|
|
31
|
+
export declare function useDebouncedValue<T>(source: () => T, delay?: number): DebouncedValue<T>;
|
|
32
|
+
/**
|
|
33
|
+
* Runs `callback` on an interval. Return `null` (or a non-positive number) from
|
|
34
|
+
* `delay` to pause without unmounting the component.
|
|
35
|
+
*/
|
|
36
|
+
export declare function useInterval(callback: () => void, delay: () => number | null): void;
|
|
37
|
+
type EventTargetLike = EventTarget | (() => EventTarget | null) | null;
|
|
38
|
+
/** Adds a listener for the component lifetime, re-binding when `target` changes. */
|
|
39
|
+
export declare function useEventListener<E extends Event = Event>(target: EventTargetLike, type: string, handler: (event: E) => void, options?: AddEventListenerOptions | boolean): void;
|
|
40
|
+
/**
|
|
41
|
+
* Fires when a pointer press happens outside `target`. Uses `composedPath()` so
|
|
42
|
+
* shadow DOM boundaries behave the same as the plain `contains` fallback.
|
|
43
|
+
*/
|
|
44
|
+
export declare function useOnClickOutside(target: () => HTMLElement | null, handler: (event: Event) => void, enabled?: () => boolean): void;
|
|
45
|
+
export interface SwipeHandlers {
|
|
46
|
+
onSwipeLeft?: () => void;
|
|
47
|
+
onSwipeRight?: () => void;
|
|
48
|
+
onSwipeUp?: () => void;
|
|
49
|
+
onSwipeDown?: () => void;
|
|
50
|
+
/** Minimum travel in px before a swipe is recognised. Defaults to 30. */
|
|
51
|
+
threshold?: number;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Recognises single-finger/mouse swipes on a target using pointer events.
|
|
55
|
+
* For pinching, rotation, or multi-touch gestures, use the Svelte ecosystem
|
|
56
|
+
* `svelte-gestures` package instead.
|
|
57
|
+
*/
|
|
58
|
+
export declare function useSwipe(target: () => HTMLElement | null, handlers: SwipeHandlers): void;
|
|
59
|
+
export interface WindowSize {
|
|
60
|
+
readonly width: number;
|
|
61
|
+
readonly height: number;
|
|
62
|
+
}
|
|
63
|
+
/** Reactive viewport size; reports 0 on the server. */
|
|
64
|
+
export declare function useWindowSize(): WindowSize;
|
|
65
|
+
export {};
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { untrack } from 'svelte';
|
|
2
|
+
export function useClipboard(options = {}) {
|
|
3
|
+
const timeout = options.timeout ?? 1600;
|
|
4
|
+
let copied = $state(false);
|
|
5
|
+
let timer = null;
|
|
6
|
+
$effect(() => () => {
|
|
7
|
+
if (timer)
|
|
8
|
+
clearTimeout(timer);
|
|
9
|
+
});
|
|
10
|
+
async function copy(text) {
|
|
11
|
+
const clipboard = typeof navigator === 'undefined' ? undefined : navigator.clipboard;
|
|
12
|
+
if (!clipboard?.writeText)
|
|
13
|
+
return false;
|
|
14
|
+
try {
|
|
15
|
+
await clipboard.writeText(text);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
copied = true;
|
|
21
|
+
if (timer)
|
|
22
|
+
clearTimeout(timer);
|
|
23
|
+
timer = setTimeout(() => {
|
|
24
|
+
copied = false;
|
|
25
|
+
}, timeout);
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
get copied() {
|
|
30
|
+
return copied;
|
|
31
|
+
},
|
|
32
|
+
copy,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function readStorage(key, fallback) {
|
|
36
|
+
if (typeof localStorage === 'undefined')
|
|
37
|
+
return fallback;
|
|
38
|
+
try {
|
|
39
|
+
const raw = localStorage.getItem(key);
|
|
40
|
+
if (raw === null)
|
|
41
|
+
return fallback;
|
|
42
|
+
return JSON.parse(raw);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return fallback;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Reactive `localStorage` value with cross-tab synchronization. Serialization
|
|
50
|
+
* failures fall back to the initial value instead of throwing.
|
|
51
|
+
*/
|
|
52
|
+
export function useLocalStorage(key, initial) {
|
|
53
|
+
let value = $state(untrack(() => readStorage(key, initial)));
|
|
54
|
+
let skipNextPersist = false;
|
|
55
|
+
$effect(() => {
|
|
56
|
+
const serialized = JSON.stringify(value);
|
|
57
|
+
if (skipNextPersist) {
|
|
58
|
+
skipNextPersist = false;
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (typeof localStorage === 'undefined')
|
|
62
|
+
return;
|
|
63
|
+
try {
|
|
64
|
+
localStorage.setItem(key, serialized);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Storage may be full or blocked; keep the in-memory value.
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
useEventListener(typeof window === 'undefined' ? null : window, 'storage', (event) => {
|
|
71
|
+
if (event.key !== key || event.newValue === null)
|
|
72
|
+
return;
|
|
73
|
+
try {
|
|
74
|
+
value = JSON.parse(event.newValue);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// Ignore malformed external writes.
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
return {
|
|
81
|
+
get value() {
|
|
82
|
+
return value;
|
|
83
|
+
},
|
|
84
|
+
set(next) {
|
|
85
|
+
value = next;
|
|
86
|
+
},
|
|
87
|
+
remove() {
|
|
88
|
+
if (typeof localStorage !== 'undefined') {
|
|
89
|
+
try {
|
|
90
|
+
localStorage.removeItem(key);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Ignore storage access failures.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (value === initial)
|
|
97
|
+
return;
|
|
98
|
+
skipNextPersist = true;
|
|
99
|
+
value = initial;
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/** Trailing-edge debounce over a reactive getter. */
|
|
104
|
+
export function useDebouncedValue(source, delay = 200) {
|
|
105
|
+
let debounced = $state(untrack(source));
|
|
106
|
+
$effect(() => {
|
|
107
|
+
const next = source();
|
|
108
|
+
const timer = setTimeout(() => {
|
|
109
|
+
debounced = next;
|
|
110
|
+
}, Math.max(0, delay));
|
|
111
|
+
return () => clearTimeout(timer);
|
|
112
|
+
});
|
|
113
|
+
return {
|
|
114
|
+
get value() {
|
|
115
|
+
return debounced;
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Runs `callback` on an interval. Return `null` (or a non-positive number) from
|
|
121
|
+
* `delay` to pause without unmounting the component.
|
|
122
|
+
*/
|
|
123
|
+
export function useInterval(callback, delay) {
|
|
124
|
+
$effect(() => {
|
|
125
|
+
const ms = delay();
|
|
126
|
+
if (ms === null || !Number.isFinite(ms) || ms <= 0 || typeof window === 'undefined')
|
|
127
|
+
return;
|
|
128
|
+
const id = setInterval(callback, ms);
|
|
129
|
+
return () => clearInterval(id);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/** Adds a listener for the component lifetime, re-binding when `target` changes. */
|
|
133
|
+
export function useEventListener(target, type, handler, options) {
|
|
134
|
+
$effect(() => {
|
|
135
|
+
const resolved = typeof target === 'function' ? target() : target;
|
|
136
|
+
if (!resolved)
|
|
137
|
+
return;
|
|
138
|
+
const listener = handler;
|
|
139
|
+
resolved.addEventListener(type, listener, options);
|
|
140
|
+
return () => resolved.removeEventListener(type, listener, options);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Fires when a pointer press happens outside `target`. Uses `composedPath()` so
|
|
145
|
+
* shadow DOM boundaries behave the same as the plain `contains` fallback.
|
|
146
|
+
*/
|
|
147
|
+
export function useOnClickOutside(target, handler, enabled = () => true) {
|
|
148
|
+
useEventListener(() => (typeof document === 'undefined' ? null : document), 'pointerdown', (event) => {
|
|
149
|
+
if (!enabled())
|
|
150
|
+
return;
|
|
151
|
+
const node = target();
|
|
152
|
+
if (!node)
|
|
153
|
+
return;
|
|
154
|
+
const targetNode = event.target;
|
|
155
|
+
const path = typeof event.composedPath === 'function' ? event.composedPath() : [];
|
|
156
|
+
if (path.includes(node))
|
|
157
|
+
return;
|
|
158
|
+
if (targetNode instanceof Node && node.contains(targetNode))
|
|
159
|
+
return;
|
|
160
|
+
handler(event);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Recognises single-finger/mouse swipes on a target using pointer events.
|
|
165
|
+
* For pinching, rotation, or multi-touch gestures, use the Svelte ecosystem
|
|
166
|
+
* `svelte-gestures` package instead.
|
|
167
|
+
*/
|
|
168
|
+
export function useSwipe(target, handlers) {
|
|
169
|
+
const threshold = handlers.threshold ?? 30;
|
|
170
|
+
$effect(() => {
|
|
171
|
+
const node = target();
|
|
172
|
+
if (!node)
|
|
173
|
+
return;
|
|
174
|
+
let start = null;
|
|
175
|
+
const onDown = (event) => {
|
|
176
|
+
start = { x: event.clientX, y: event.clientY };
|
|
177
|
+
};
|
|
178
|
+
const onUp = (event) => {
|
|
179
|
+
if (!start)
|
|
180
|
+
return;
|
|
181
|
+
const dx = event.clientX - start.x;
|
|
182
|
+
const dy = event.clientY - start.y;
|
|
183
|
+
start = null;
|
|
184
|
+
if (Math.abs(dx) < threshold && Math.abs(dy) < threshold)
|
|
185
|
+
return;
|
|
186
|
+
if (Math.abs(dx) >= Math.abs(dy)) {
|
|
187
|
+
if (dx > 0)
|
|
188
|
+
handlers.onSwipeRight?.();
|
|
189
|
+
else
|
|
190
|
+
handlers.onSwipeLeft?.();
|
|
191
|
+
}
|
|
192
|
+
else if (dy > 0) {
|
|
193
|
+
handlers.onSwipeDown?.();
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
handlers.onSwipeUp?.();
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
const onCancel = () => {
|
|
200
|
+
start = null;
|
|
201
|
+
};
|
|
202
|
+
node.addEventListener('pointerdown', onDown);
|
|
203
|
+
node.addEventListener('pointerup', onUp);
|
|
204
|
+
node.addEventListener('pointercancel', onCancel);
|
|
205
|
+
return () => {
|
|
206
|
+
node.removeEventListener('pointerdown', onDown);
|
|
207
|
+
node.removeEventListener('pointerup', onUp);
|
|
208
|
+
node.removeEventListener('pointercancel', onCancel);
|
|
209
|
+
};
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
/** Reactive viewport size; reports 0 on the server. */
|
|
213
|
+
export function useWindowSize() {
|
|
214
|
+
let width = $state(typeof window === 'undefined' ? 0 : window.innerWidth);
|
|
215
|
+
let height = $state(typeof window === 'undefined' ? 0 : window.innerHeight);
|
|
216
|
+
useEventListener(() => (typeof window === 'undefined' ? null : window), 'resize', () => {
|
|
217
|
+
width = window.innerWidth;
|
|
218
|
+
height = window.innerHeight;
|
|
219
|
+
});
|
|
220
|
+
return {
|
|
221
|
+
get width() {
|
|
222
|
+
return width;
|
|
223
|
+
},
|
|
224
|
+
get height() {
|
|
225
|
+
return height;
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../../utils.js';
|
|
3
|
+
|
|
4
|
+
interface AreaPoint {
|
|
5
|
+
label: string;
|
|
6
|
+
value: number;
|
|
7
|
+
color?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
data: AreaPoint[];
|
|
12
|
+
height?: number;
|
|
13
|
+
/** Stroke width of the value line. */
|
|
14
|
+
strokeWidth?: number;
|
|
15
|
+
/** Fills the area under the line. Defaults to `true`. */
|
|
16
|
+
fill?: boolean;
|
|
17
|
+
class?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let {
|
|
21
|
+
data,
|
|
22
|
+
height = 200,
|
|
23
|
+
strokeWidth = 2,
|
|
24
|
+
fill = true,
|
|
25
|
+
class: className = '',
|
|
26
|
+
}: Props = $props();
|
|
27
|
+
|
|
28
|
+
const width = 480;
|
|
29
|
+
const padding = { top: 16, right: 16, bottom: 28, left: 36 };
|
|
30
|
+
|
|
31
|
+
const maxValue = $derived(Math.max(...data.map((point) => point.value), 1));
|
|
32
|
+
const innerWidth = $derived(width - padding.left - padding.right);
|
|
33
|
+
const innerHeight = $derived(height - padding.top - padding.bottom);
|
|
34
|
+
|
|
35
|
+
const coords = $derived.by(() =>
|
|
36
|
+
data.map((point, index) => {
|
|
37
|
+
const x = padding.left + (data.length <= 1 ? innerWidth / 2 : (index / (data.length - 1)) * innerWidth);
|
|
38
|
+
const y = padding.top + innerHeight - (point.value / maxValue) * innerHeight;
|
|
39
|
+
return { ...point, x, y };
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const linePath = $derived(coords.map((point) => `${point.x},${point.y}`).join(' '));
|
|
44
|
+
const areaPath = $derived(
|
|
45
|
+
coords.length
|
|
46
|
+
? `M ${coords[0]?.x ?? 0} ${padding.top + innerHeight} L ${linePath.replace(/ /gu, ' L ')} L ${coords.at(-1)?.x ?? 0} ${padding.top + innerHeight} Z`
|
|
47
|
+
: '',
|
|
48
|
+
);
|
|
49
|
+
const gridLines = $derived([0, 0.25, 0.5, 0.75, 1]);
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<div class={cn('svadmin-area-chart', className)} data-slot="area-chart">
|
|
53
|
+
<svg viewBox={`0 0 ${width} ${height}`} role="img" aria-label="Area chart" preserveAspectRatio="none">
|
|
54
|
+
{#each gridLines as ratio (ratio)}
|
|
55
|
+
{@const y = padding.top + innerHeight * ratio}
|
|
56
|
+
<line x1={padding.left} y1={y} x2={width - padding.right} y2={y} stroke="var(--border, currentColor)" stroke-width="1" opacity="0.6" />
|
|
57
|
+
<text x={padding.left - 6} y={y + 3} text-anchor="end" fill="var(--muted-foreground, currentColor)" font-size="10">{Math.round(maxValue * (1 - ratio))}</text>
|
|
58
|
+
{/each}
|
|
59
|
+
|
|
60
|
+
{#if fill && areaPath}
|
|
61
|
+
<path d={areaPath} fill="var(--primary, currentColor)" opacity="0.16" />
|
|
62
|
+
{/if}
|
|
63
|
+
|
|
64
|
+
{#if linePath}
|
|
65
|
+
<polyline points={linePath} fill="none" stroke="var(--primary, currentColor)" stroke-width={strokeWidth} stroke-linejoin="round" stroke-linecap="round" />
|
|
66
|
+
{/if}
|
|
67
|
+
|
|
68
|
+
{#each coords as point, index (index)}
|
|
69
|
+
<circle cx={point.x} cy={point.y} r="3" fill={point.color ?? 'var(--primary, currentColor)'} />
|
|
70
|
+
<text x={point.x} y={height - 8} text-anchor="middle" fill="var(--muted-foreground, currentColor)" font-size="10">{point.label}</text>
|
|
71
|
+
{/each}
|
|
72
|
+
</svg>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<style>
|
|
76
|
+
.svadmin-area-chart { width: 100%; color: var(--foreground, currentColor); }
|
|
77
|
+
.svadmin-area-chart svg { display: block; width: 100%; height: auto; }
|
|
78
|
+
</style>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
interface AreaPoint {
|
|
2
|
+
label: string;
|
|
3
|
+
value: number;
|
|
4
|
+
color?: string;
|
|
5
|
+
}
|
|
6
|
+
interface Props {
|
|
7
|
+
data: AreaPoint[];
|
|
8
|
+
height?: number;
|
|
9
|
+
/** Stroke width of the value line. */
|
|
10
|
+
strokeWidth?: number;
|
|
11
|
+
/** Fills the area under the line. Defaults to `true`. */
|
|
12
|
+
fill?: boolean;
|
|
13
|
+
class?: string;
|
|
14
|
+
}
|
|
15
|
+
declare const AreaChart: import("svelte").Component<Props, {}, "">;
|
|
16
|
+
type AreaChart = ReturnType<typeof AreaChart>;
|
|
17
|
+
export default AreaChart;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../../utils.js';
|
|
3
|
+
|
|
4
|
+
interface ScatterPoint {
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
label?: string;
|
|
8
|
+
color?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
data: ScatterPoint[];
|
|
13
|
+
height?: number;
|
|
14
|
+
pointRadius?: number;
|
|
15
|
+
xLabel?: string;
|
|
16
|
+
yLabel?: string;
|
|
17
|
+
class?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let {
|
|
21
|
+
data,
|
|
22
|
+
height = 220,
|
|
23
|
+
pointRadius = 4,
|
|
24
|
+
xLabel,
|
|
25
|
+
yLabel,
|
|
26
|
+
class: className = '',
|
|
27
|
+
}: Props = $props();
|
|
28
|
+
|
|
29
|
+
const width = 480;
|
|
30
|
+
const padding = { top: 16, right: 16, bottom: 32, left: 40 };
|
|
31
|
+
const innerWidth = $derived(width - padding.left - padding.right);
|
|
32
|
+
const innerHeight = $derived(height - padding.top - padding.bottom);
|
|
33
|
+
|
|
34
|
+
const bounds = $derived.by(() => {
|
|
35
|
+
const xs = data.map((point) => point.x);
|
|
36
|
+
const ys = data.map((point) => point.y);
|
|
37
|
+
const minX = Math.min(...xs, 0);
|
|
38
|
+
const maxX = Math.max(...xs, 1);
|
|
39
|
+
const minY = Math.min(...ys, 0);
|
|
40
|
+
const maxY = Math.max(...ys, 1);
|
|
41
|
+
return {
|
|
42
|
+
minX,
|
|
43
|
+
maxX: maxX === minX ? minX + 1 : maxX,
|
|
44
|
+
minY,
|
|
45
|
+
maxY: maxY === minY ? minY + 1 : maxY,
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
function scaleX(value: number): number {
|
|
50
|
+
return padding.left + ((value - bounds.minX) / (bounds.maxX - bounds.minX)) * innerWidth;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function scaleY(value: number): number {
|
|
54
|
+
return padding.top + innerHeight - ((value - bounds.minY) / (bounds.maxY - bounds.minY)) * innerHeight;
|
|
55
|
+
}
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<div class={cn('svadmin-scatter-chart', className)} data-slot="scatter-chart">
|
|
59
|
+
<svg
|
|
60
|
+
viewBox={`0 0 ${width} ${height}`}
|
|
61
|
+
role="img"
|
|
62
|
+
aria-label={[yLabel ?? 'Y', xLabel ?? 'X'].join(' by ')}
|
|
63
|
+
preserveAspectRatio="none"
|
|
64
|
+
>
|
|
65
|
+
<line x1={padding.left} y1={padding.top + innerHeight} x2={width - padding.right} y2={padding.top + innerHeight} stroke="var(--border, currentColor)" />
|
|
66
|
+
<line x1={padding.left} y1={padding.top} x2={padding.left} y2={padding.top + innerHeight} stroke="var(--border, currentColor)" />
|
|
67
|
+
|
|
68
|
+
{#if xLabel}<text x={width - padding.right} y={height - 6} text-anchor="end" fill="var(--muted-foreground, currentColor)" font-size="10">{xLabel}</text>{/if}
|
|
69
|
+
{#if yLabel}<text x={padding.left - 6} y={padding.top + 8} text-anchor="end" fill="var(--muted-foreground, currentColor)" font-size="10">{yLabel}</text>{/if}
|
|
70
|
+
|
|
71
|
+
{#each data as point, index (index)}
|
|
72
|
+
<circle cx={scaleX(point.x)} cy={scaleY(point.y)} r={pointRadius} fill={point.color ?? 'var(--primary, currentColor)'} opacity="0.85">
|
|
73
|
+
<title>{point.label ?? `${point.x}, ${point.y}`}</title>
|
|
74
|
+
</circle>
|
|
75
|
+
{/each}
|
|
76
|
+
</svg>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<style>
|
|
80
|
+
.svadmin-scatter-chart { width: 100%; color: var(--foreground, currentColor); }
|
|
81
|
+
.svadmin-scatter-chart svg { display: block; width: 100%; height: auto; }
|
|
82
|
+
</style>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
interface ScatterPoint {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
label?: string;
|
|
5
|
+
color?: string;
|
|
6
|
+
}
|
|
7
|
+
interface Props {
|
|
8
|
+
data: ScatterPoint[];
|
|
9
|
+
height?: number;
|
|
10
|
+
pointRadius?: number;
|
|
11
|
+
xLabel?: string;
|
|
12
|
+
yLabel?: string;
|
|
13
|
+
class?: string;
|
|
14
|
+
}
|
|
15
|
+
declare const ScatterChart: import("svelte").Component<Props, {}, "">;
|
|
16
|
+
type ScatterChart = ReturnType<typeof ScatterChart>;
|
|
17
|
+
export default ScatterChart;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export { default as AreaChart } from './AreaChart.svelte';
|
|
1
2
|
export { default as BarChart } from './BarChart.svelte';
|
|
2
3
|
export { default as LineChart } from './LineChart.svelte';
|
|
3
4
|
export { default as PieChart } from './PieChart.svelte';
|
|
5
|
+
export { default as ScatterChart } from './ScatterChart.svelte';
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export { default as AreaChart } from './AreaChart.svelte';
|
|
1
2
|
export { default as BarChart } from './BarChart.svelte';
|
|
2
3
|
export { default as LineChart } from './LineChart.svelte';
|
|
3
4
|
export { default as PieChart } from './PieChart.svelte';
|
|
5
|
+
export { default as ScatterChart } from './ScatterChart.svelte';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodeMirror language presets for `@svadmin/ui/code-editor`.
|
|
3
|
+
*
|
|
4
|
+
* Each language is an optional peer dependency. Import this module only if you
|
|
5
|
+
* need the presets, and install the languages you actually use:
|
|
6
|
+
*
|
|
7
|
+
* ```bash
|
|
8
|
+
* bun add @codemirror/lang-json
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
export { json } from '@codemirror/lang-json';
|
|
12
|
+
export { javascript, javascriptLanguage, typescriptLanguage, jsxLanguage, tsxLanguage, } from '@codemirror/lang-javascript';
|
|
13
|
+
export { sql } from '@codemirror/lang-sql';
|
|
14
|
+
export { markdown } from '@codemirror/lang-markdown';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodeMirror language presets for `@svadmin/ui/code-editor`.
|
|
3
|
+
*
|
|
4
|
+
* Each language is an optional peer dependency. Import this module only if you
|
|
5
|
+
* need the presets, and install the languages you actually use:
|
|
6
|
+
*
|
|
7
|
+
* ```bash
|
|
8
|
+
* bun add @codemirror/lang-json
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
export { json } from '@codemirror/lang-json';
|
|
12
|
+
export { javascript, javascriptLanguage, typescriptLanguage, jsxLanguage, tsxLanguage, } from '@codemirror/lang-javascript';
|
|
13
|
+
export { sql } from '@codemirror/lang-sql';
|
|
14
|
+
export { markdown } from '@codemirror/lang-markdown';
|
|
@@ -12,6 +12,15 @@
|
|
|
12
12
|
interface DomPurifyLike {
|
|
13
13
|
sanitize(source: string | Node, config?: Record<string, unknown>): string;
|
|
14
14
|
}
|
|
15
|
+
interface DomPurifyModule {
|
|
16
|
+
default?: DomPurifyLike;
|
|
17
|
+
sanitize?: DomPurifyLike["sanitize"];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isDomPurifyModule(value: unknown): value is DomPurifyModule {
|
|
21
|
+
return typeof value === 'object' && value !== null;
|
|
22
|
+
}
|
|
23
|
+
|
|
15
24
|
let DOMPurify: DomPurifyLike | null = $state(null);
|
|
16
25
|
|
|
17
26
|
onMount(() => {
|
|
@@ -19,12 +28,12 @@
|
|
|
19
28
|
import("isomorphic-dompurify")
|
|
20
29
|
.then((pkg) => {
|
|
21
30
|
if (!cancelled) {
|
|
22
|
-
|
|
31
|
+
if (!isDomPurifyModule(pkg)) return;
|
|
23
32
|
const loaded: DomPurifyLike | undefined =
|
|
24
|
-
|
|
25
|
-
?
|
|
26
|
-
: typeof
|
|
27
|
-
?
|
|
33
|
+
pkg.default && typeof pkg.default.sanitize === "function"
|
|
34
|
+
? pkg.default
|
|
35
|
+
: typeof pkg.sanitize === "function"
|
|
36
|
+
? { sanitize: pkg.sanitize }
|
|
28
37
|
: undefined;
|
|
29
38
|
if (loaded) {
|
|
30
39
|
DOMPurify = loaded;
|