@svadmin/ui 0.75.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.
Files changed (41) hide show
  1. package/dist/app.css +14 -0
  2. package/dist/app.theme.css +14 -0
  3. package/dist/components/AppFooter.svelte +137 -0
  4. package/dist/components/AppFooter.svelte.d.ts +25 -0
  5. package/dist/components/CodeEditor.svelte +145 -0
  6. package/dist/components/CodeEditor.svelte.d.ts +26 -0
  7. package/dist/components/HighlightText.svelte +46 -0
  8. package/dist/components/HighlightText.svelte.d.ts +13 -0
  9. package/dist/components/JsonEditor.svelte +182 -0
  10. package/dist/components/JsonEditor.svelte.d.ts +22 -0
  11. package/dist/components/Marquee.svelte +104 -0
  12. package/dist/components/Marquee.svelte.d.ts +20 -0
  13. package/dist/components/MediaPlayer.svelte +104 -0
  14. package/dist/components/MediaPlayer.svelte.d.ts +21 -0
  15. package/dist/components/PhoneInput.svelte +157 -0
  16. package/dist/components/PhoneInput.svelte.d.ts +26 -0
  17. package/dist/components/QRCode.svelte +88 -0
  18. package/dist/components/QRCode.svelte.d.ts +22 -0
  19. package/dist/components/ResizableGrid.svelte +252 -0
  20. package/dist/components/ResizableGrid.svelte.d.ts +34 -0
  21. package/dist/components/TopProgressBar.svelte +90 -0
  22. package/dist/components/TopProgressBar.svelte.d.ts +15 -0
  23. package/dist/components/browser.svelte.d.ts +65 -0
  24. package/dist/components/browser.svelte.js +228 -0
  25. package/dist/components/charts/AreaChart.svelte +78 -0
  26. package/dist/components/charts/AreaChart.svelte.d.ts +17 -0
  27. package/dist/components/charts/ScatterChart.svelte +82 -0
  28. package/dist/components/charts/ScatterChart.svelte.d.ts +17 -0
  29. package/dist/components/charts/index.d.ts +2 -0
  30. package/dist/components/charts/index.js +2 -0
  31. package/dist/components/code-editor-presets.d.ts +14 -0
  32. package/dist/components/code-editor-presets.js +14 -0
  33. package/dist/components/highlight.d.ts +16 -0
  34. package/dist/components/highlight.js +68 -0
  35. package/dist/components/media.svelte.d.ts +27 -0
  36. package/dist/components/media.svelte.js +60 -0
  37. package/dist/components/phone-dial-codes.d.ts +19 -0
  38. package/dist/components/phone-dial-codes.js +112 -0
  39. package/dist/index.d.ts +19 -1
  40. package/dist/index.js +14 -1
  41. package/package.json +64 -4
@@ -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';
@@ -0,0 +1,16 @@
1
+ export interface HighlightSegment {
2
+ text: string;
3
+ match: boolean;
4
+ }
5
+ export interface SplitHighlightsOptions {
6
+ caseSensitive?: boolean;
7
+ /** Requires the match to be surrounded by non-word characters. */
8
+ wholeWord?: boolean;
9
+ }
10
+ export declare function escapeRegExp(value: string): string;
11
+ /**
12
+ * Splits `text` into ordered segments, flagging the ones that match any query
13
+ * term. Overlapping matches are merged so the same characters are highlighted
14
+ * once. Never returns HTML, so hosts can render segments safely.
15
+ */
16
+ export declare function splitHighlights(text: string, query: string | readonly string[] | undefined, options?: SplitHighlightsOptions): HighlightSegment[];
@@ -0,0 +1,68 @@
1
+ const REGEXP_SPECIALS = /[.*+?^${}()|[\]\\]/gu;
2
+ export function escapeRegExp(value) {
3
+ return value.replace(REGEXP_SPECIALS, '\\$&');
4
+ }
5
+ function normalizeTerms(query) {
6
+ if (query === undefined || query === null)
7
+ return [];
8
+ const list = typeof query === 'string' ? [query] : [...query];
9
+ return list
10
+ .map((term) => term.trim())
11
+ .filter((term) => term.length > 0);
12
+ }
13
+ /**
14
+ * Splits `text` into ordered segments, flagging the ones that match any query
15
+ * term. Overlapping matches are merged so the same characters are highlighted
16
+ * once. Never returns HTML, so hosts can render segments safely.
17
+ */
18
+ export function splitHighlights(text, query, options = {}) {
19
+ if (text.length === 0)
20
+ return [];
21
+ const terms = normalizeTerms(query);
22
+ if (terms.length === 0)
23
+ return [{ text, match: false }];
24
+ const flags = options.caseSensitive ? 'gu' : 'giu';
25
+ const wordBoundary = options.wholeWord ? '\\b' : '';
26
+ const pattern = terms
27
+ .sort((a, b) => b.length - a.length)
28
+ .map((term) => `${wordBoundary}${escapeRegExp(term)}${wordBoundary}`)
29
+ .join('|');
30
+ let regex;
31
+ try {
32
+ regex = new RegExp(`(${pattern})`, flags);
33
+ }
34
+ catch {
35
+ return [{ text, match: false }];
36
+ }
37
+ const matches = [];
38
+ for (const match of text.matchAll(regex)) {
39
+ const start = match.index;
40
+ const value = match[0];
41
+ if (start === undefined || value.length === 0)
42
+ continue;
43
+ matches.push({ start, end: start + value.length });
44
+ }
45
+ if (matches.length === 0)
46
+ return [{ text, match: false }];
47
+ const merged = [];
48
+ for (const current of matches) {
49
+ const previous = merged.at(-1);
50
+ if (previous && current.start <= previous.end) {
51
+ previous.end = Math.max(previous.end, current.end);
52
+ }
53
+ else {
54
+ merged.push({ ...current });
55
+ }
56
+ }
57
+ const segments = [];
58
+ let cursor = 0;
59
+ for (const { start, end } of merged) {
60
+ if (start > cursor)
61
+ segments.push({ text: text.slice(cursor, start), match: false });
62
+ segments.push({ text: text.slice(start, end), match: true });
63
+ cursor = end;
64
+ }
65
+ if (cursor < text.length)
66
+ segments.push({ text: text.slice(cursor), match: false });
67
+ return segments;
68
+ }
@@ -0,0 +1,27 @@
1
+ export interface MediaQueryState {
2
+ readonly matches: boolean;
3
+ }
4
+ export interface DeviceDetectState {
5
+ readonly isMobile: boolean;
6
+ readonly isTablet: boolean;
7
+ readonly isDesktop: boolean;
8
+ readonly isTouch: boolean;
9
+ readonly prefersReducedMotion: boolean;
10
+ readonly prefersDark: boolean;
11
+ }
12
+ export declare const MOBILE_QUERY = "(max-width: 640px)";
13
+ export declare const TABLET_QUERY = "(min-width: 641px) and (max-width: 1024px)";
14
+ export declare const TOUCH_QUERY = "(hover: none), (pointer: coarse)";
15
+ export declare const REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
16
+ export declare const DARK_QUERY = "(prefers-color-scheme: dark)";
17
+ /**
18
+ * Reactive `window.matchMedia`. Must be called during component initialisation
19
+ * because it registers a scoped effect. SSR renders the fallback (no match)
20
+ * and hydration corrects the value.
21
+ */
22
+ export declare function useMediaQuery(query: string, fallback?: boolean): MediaQueryState;
23
+ /**
24
+ * Convenience device/preference detection built on `useMediaQuery`. Falls back
25
+ * to the desktop, motion-enabled profile during SSR.
26
+ */
27
+ export declare function useDeviceDetect(): DeviceDetectState;
@@ -0,0 +1,60 @@
1
+ export const MOBILE_QUERY = '(max-width: 640px)';
2
+ export const TABLET_QUERY = '(min-width: 641px) and (max-width: 1024px)';
3
+ export const TOUCH_QUERY = '(hover: none), (pointer: coarse)';
4
+ export const REDUCED_MOTION_QUERY = '(prefers-reduced-motion: reduce)';
5
+ export const DARK_QUERY = '(prefers-color-scheme: dark)';
6
+ /**
7
+ * Reactive `window.matchMedia`. Must be called during component initialisation
8
+ * because it registers a scoped effect. SSR renders the fallback (no match)
9
+ * and hydration corrects the value.
10
+ */
11
+ export function useMediaQuery(query, fallback = false) {
12
+ let matches = $state(fallback);
13
+ $effect(() => {
14
+ if (typeof window === 'undefined' || typeof window.matchMedia !== 'function')
15
+ return;
16
+ const mediaQuery = window.matchMedia(query);
17
+ matches = mediaQuery.matches;
18
+ const onChange = (event) => {
19
+ matches = event.matches;
20
+ };
21
+ mediaQuery.addEventListener('change', onChange);
22
+ return () => mediaQuery.removeEventListener('change', onChange);
23
+ });
24
+ return {
25
+ get matches() {
26
+ return matches;
27
+ },
28
+ };
29
+ }
30
+ /**
31
+ * Convenience device/preference detection built on `useMediaQuery`. Falls back
32
+ * to the desktop, motion-enabled profile during SSR.
33
+ */
34
+ export function useDeviceDetect() {
35
+ const mobile = useMediaQuery(MOBILE_QUERY);
36
+ const tablet = useMediaQuery(TABLET_QUERY);
37
+ const touch = useMediaQuery(TOUCH_QUERY);
38
+ const reducedMotion = useMediaQuery(REDUCED_MOTION_QUERY);
39
+ const dark = useMediaQuery(DARK_QUERY);
40
+ return {
41
+ get isMobile() {
42
+ return mobile.matches;
43
+ },
44
+ get isTablet() {
45
+ return tablet.matches;
46
+ },
47
+ get isDesktop() {
48
+ return !mobile.matches && !tablet.matches;
49
+ },
50
+ get isTouch() {
51
+ return touch.matches;
52
+ },
53
+ get prefersReducedMotion() {
54
+ return reducedMotion.matches;
55
+ },
56
+ get prefersDark() {
57
+ return dark.matches;
58
+ },
59
+ };
60
+ }
@@ -0,0 +1,19 @@
1
+ export interface PhoneDialCode {
2
+ /** ISO 3166-1 alpha-2 country code. */
3
+ country: string;
4
+ /** International dialling prefix without the leading `+`. */
5
+ dial: string;
6
+ name: string;
7
+ }
8
+ /** Curated dialling codes covering common enterprise locales; hosts can pass their own list. */
9
+ export declare const PHONE_DIAL_CODES: readonly PhoneDialCode[];
10
+ export declare function findDialCodeByCountry(country: string, codes?: readonly PhoneDialCode[]): PhoneDialCode | undefined;
11
+ /** Builds an E.164 string (`+<dial><national>`), or an empty string when there is no number. */
12
+ export declare function toE164(country: string, nationalNumber: string, codes?: readonly PhoneDialCode[]): string;
13
+ /** Longest-prefix match so `380` wins over `1` for shared dial prefixes. */
14
+ export declare function splitE164(value: string, fallbackCountry?: string, codes?: readonly PhoneDialCode[]): {
15
+ country: string;
16
+ nationalNumber: string;
17
+ };
18
+ /** Formats a national number in loose groups of 3–4 digits for readability. */
19
+ export declare function formatNationalNumber(value: string): string;
@@ -0,0 +1,112 @@
1
+ /** Curated dialling codes covering common enterprise locales; hosts can pass their own list. */
2
+ export const PHONE_DIAL_CODES = [
3
+ { country: 'AR', dial: '54', name: 'Argentina' },
4
+ { country: 'AU', dial: '61', name: 'Australia' },
5
+ { country: 'AT', dial: '43', name: 'Austria' },
6
+ { country: 'BH', dial: '973', name: 'Bahrain' },
7
+ { country: 'BD', dial: '880', name: 'Bangladesh' },
8
+ { country: 'BE', dial: '32', name: 'Belgium' },
9
+ { country: 'BR', dial: '55', name: 'Brazil' },
10
+ { country: 'BG', dial: '359', name: 'Bulgaria' },
11
+ { country: 'CA', dial: '1', name: 'Canada' },
12
+ { country: 'CL', dial: '56', name: 'Chile' },
13
+ { country: 'CN', dial: '86', name: 'China' },
14
+ { country: 'CO', dial: '57', name: 'Colombia' },
15
+ { country: 'HR', dial: '385', name: 'Croatia' },
16
+ { country: 'CZ', dial: '420', name: 'Czechia' },
17
+ { country: 'DK', dial: '45', name: 'Denmark' },
18
+ { country: 'EG', dial: '20', name: 'Egypt' },
19
+ { country: 'EE', dial: '372', name: 'Estonia' },
20
+ { country: 'FI', dial: '358', name: 'Finland' },
21
+ { country: 'FR', dial: '33', name: 'France' },
22
+ { country: 'DE', dial: '49', name: 'Germany' },
23
+ { country: 'GR', dial: '30', name: 'Greece' },
24
+ { country: 'HK', dial: '852', name: 'Hong Kong' },
25
+ { country: 'HU', dial: '36', name: 'Hungary' },
26
+ { country: 'IS', dial: '354', name: 'Iceland' },
27
+ { country: 'IN', dial: '91', name: 'India' },
28
+ { country: 'ID', dial: '62', name: 'Indonesia' },
29
+ { country: 'IE', dial: '353', name: 'Ireland' },
30
+ { country: 'IL', dial: '972', name: 'Israel' },
31
+ { country: 'IT', dial: '39', name: 'Italy' },
32
+ { country: 'JP', dial: '81', name: 'Japan' },
33
+ { country: 'JO', dial: '962', name: 'Jordan' },
34
+ { country: 'KZ', dial: '7', name: 'Kazakhstan' },
35
+ { country: 'KE', dial: '254', name: 'Kenya' },
36
+ { country: 'KW', dial: '965', name: 'Kuwait' },
37
+ { country: 'LV', dial: '371', name: 'Latvia' },
38
+ { country: 'LT', dial: '370', name: 'Lithuania' },
39
+ { country: 'LU', dial: '352', name: 'Luxembourg' },
40
+ { country: 'MY', dial: '60', name: 'Malaysia' },
41
+ { country: 'MT', dial: '356', name: 'Malta' },
42
+ { country: 'MX', dial: '52', name: 'Mexico' },
43
+ { country: 'MA', dial: '212', name: 'Morocco' },
44
+ { country: 'NL', dial: '31', name: 'Netherlands' },
45
+ { country: 'NZ', dial: '64', name: 'New Zealand' },
46
+ { country: 'NG', dial: '234', name: 'Nigeria' },
47
+ { country: 'NO', dial: '47', name: 'Norway' },
48
+ { country: 'OM', dial: '968', name: 'Oman' },
49
+ { country: 'PK', dial: '92', name: 'Pakistan' },
50
+ { country: 'PE', dial: '51', name: 'Peru' },
51
+ { country: 'PH', dial: '63', name: 'Philippines' },
52
+ { country: 'PL', dial: '48', name: 'Poland' },
53
+ { country: 'PT', dial: '351', name: 'Portugal' },
54
+ { country: 'QA', dial: '974', name: 'Qatar' },
55
+ { country: 'RO', dial: '40', name: 'Romania' },
56
+ { country: 'RU', dial: '7', name: 'Russia' },
57
+ { country: 'SA', dial: '966', name: 'Saudi Arabia' },
58
+ { country: 'RS', dial: '381', name: 'Serbia' },
59
+ { country: 'SG', dial: '65', name: 'Singapore' },
60
+ { country: 'SK', dial: '421', name: 'Slovakia' },
61
+ { country: 'SI', dial: '386', name: 'Slovenia' },
62
+ { country: 'ZA', dial: '27', name: 'South Africa' },
63
+ { country: 'KR', dial: '82', name: 'South Korea' },
64
+ { country: 'ES', dial: '34', name: 'Spain' },
65
+ { country: 'LK', dial: '94', name: 'Sri Lanka' },
66
+ { country: 'SE', dial: '46', name: 'Sweden' },
67
+ { country: 'CH', dial: '41', name: 'Switzerland' },
68
+ { country: 'TW', dial: '886', name: 'Taiwan' },
69
+ { country: 'TH', dial: '66', name: 'Thailand' },
70
+ { country: 'TR', dial: '90', name: 'Türkiye' },
71
+ { country: 'UA', dial: '380', name: 'Ukraine' },
72
+ { country: 'AE', dial: '971', name: 'United Arab Emirates' },
73
+ { country: 'GB', dial: '44', name: 'United Kingdom' },
74
+ { country: 'US', dial: '1', name: 'United States' },
75
+ { country: 'UY', dial: '598', name: 'Uruguay' },
76
+ { country: 'UZ', dial: '998', name: 'Uzbekistan' },
77
+ { country: 'VE', dial: '58', name: 'Venezuela' },
78
+ { country: 'VN', dial: '84', name: 'Vietnam' },
79
+ ];
80
+ export function findDialCodeByCountry(country, codes = PHONE_DIAL_CODES) {
81
+ const target = country.trim().toUpperCase();
82
+ return codes.find((entry) => entry.country === target);
83
+ }
84
+ const digitsOnly = (value) => value.replace(/\D+/gu, '');
85
+ /** Builds an E.164 string (`+<dial><national>`), or an empty string when there is no number. */
86
+ export function toE164(country, nationalNumber, codes = PHONE_DIAL_CODES) {
87
+ const national = digitsOnly(nationalNumber);
88
+ if (national.length === 0)
89
+ return '';
90
+ const entry = findDialCodeByCountry(country, codes);
91
+ if (!entry)
92
+ return `+${national}`;
93
+ return `+${entry.dial}${national.replace(/^0+/u, '')}`;
94
+ }
95
+ /** Longest-prefix match so `380` wins over `1` for shared dial prefixes. */
96
+ export function splitE164(value, fallbackCountry = 'CN', codes = PHONE_DIAL_CODES) {
97
+ const digits = digitsOnly(value);
98
+ if (digits.length === 0)
99
+ return { country: fallbackCountry, nationalNumber: '' };
100
+ const byDialLength = [...codes].sort((a, b) => b.dial.length - a.dial.length);
101
+ for (const entry of byDialLength) {
102
+ if (digits.startsWith(entry.dial)) {
103
+ return { country: entry.country, nationalNumber: digits.slice(entry.dial.length) };
104
+ }
105
+ }
106
+ return { country: fallbackCountry, nationalNumber: digits };
107
+ }
108
+ /** Formats a national number in loose groups of 3–4 digits for readability. */
109
+ export function formatNationalNumber(value) {
110
+ const digits = digitsOnly(value);
111
+ return digits.replace(/(\d{3,4})(?=\d)/gu, '$1 ').trim();
112
+ }
package/dist/index.d.ts CHANGED
@@ -196,7 +196,7 @@ export { default as RetryTaskButton } from "./components/RetryTaskButton.svelte"
196
196
  export { default as CancelTaskButton } from "./components/CancelTaskButton.svelte";
197
197
  export { default as DraggableGrid } from "./components/DraggableGrid.svelte";
198
198
  export type { GridModule } from "./types.js";
199
- export { BarChart, LineChart, PieChart } from './components/charts/index.js';
199
+ export { BarChart, LineChart, PieChart, AreaChart, ScatterChart } from './components/charts/index.js';
200
200
  export { default as NumberField } from './components/fields/NumberField.svelte';
201
201
  export { default as DateField } from './components/fields/DateField.svelte';
202
202
  export { default as EmailField } from './components/fields/EmailField.svelte';
@@ -341,3 +341,21 @@ export { default as MediaThumbnail } from './components/content/MediaThumbnail.s
341
341
  export type { MediaThumbnailSize, MediaThumbnailFit } from './components/content/MediaThumbnail.svelte';
342
342
  export { default as FileBrowser } from './components/content/FileBrowser.svelte';
343
343
  export type { FileBrowserData, FileBrowserInit, FileBrowserMenuOptions, FileBrowserProps } from './components/content/FileBrowser.svelte';
344
+ export { default as HighlightText } from './components/HighlightText.svelte';
345
+ export { splitHighlights, escapeRegExp } from './components/highlight.js';
346
+ export type { HighlightSegment, SplitHighlightsOptions } from './components/highlight.js';
347
+ export { default as Marquee } from './components/Marquee.svelte';
348
+ export { default as MediaPlayer } from './components/MediaPlayer.svelte';
349
+ export { default as TopProgressBar } from './components/TopProgressBar.svelte';
350
+ export { default as PhoneInput } from './components/PhoneInput.svelte';
351
+ export { PHONE_DIAL_CODES, findDialCodeByCountry, toE164, splitE164, formatNationalNumber, } from './components/phone-dial-codes.js';
352
+ export type { PhoneDialCode } from './components/phone-dial-codes.js';
353
+ export { useMediaQuery, useDeviceDetect, MOBILE_QUERY, TABLET_QUERY, TOUCH_QUERY, REDUCED_MOTION_QUERY, DARK_QUERY, } from './components/media.svelte.js';
354
+ export type { MediaQueryState, DeviceDetectState } from './components/media.svelte.js';
355
+ export { useClipboard, useLocalStorage, useDebouncedValue, useInterval, useEventListener, useOnClickOutside, useWindowSize, } from './components/browser.svelte.js';
356
+ export type { ClipboardState, ClipboardOptions, LocalStorageState, DebouncedValue, WindowSize, SwipeHandlers, } from './components/browser.svelte.js';
357
+ export { useSwipe } from './components/browser.svelte.js';
358
+ export { default as AppFooter } from './components/AppFooter.svelte';
359
+ export type { FooterLink, FooterColumn } from './components/AppFooter.svelte';
360
+ export { default as ResizableGrid } from './components/ResizableGrid.svelte';
361
+ export type { GridItem } from './components/ResizableGrid.svelte';
package/dist/index.js CHANGED
@@ -153,7 +153,7 @@ export { default as RetryTaskButton } from "./components/RetryTaskButton.svelte"
153
153
  export { default as CancelTaskButton } from "./components/CancelTaskButton.svelte";
154
154
  export { default as DraggableGrid } from "./components/DraggableGrid.svelte";
155
155
  // Dashboard Charts (zero-dependency SVG)
156
- export { BarChart, LineChart, PieChart } from './components/charts/index.js';
156
+ export { BarChart, LineChart, PieChart, AreaChart, ScatterChart } from './components/charts/index.js';
157
157
  // Field display components
158
158
  export { default as NumberField } from './components/fields/NumberField.svelte';
159
159
  export { default as DateField } from './components/fields/DateField.svelte';
@@ -280,3 +280,16 @@ export { default as FilterDropdown } from './components/FilterDropdown.svelte';
280
280
  export { default as BatchActionBar } from './components/BatchActionBar.svelte';
281
281
  export { default as MediaThumbnail } from './components/content/MediaThumbnail.svelte';
282
282
  export { default as FileBrowser } from './components/content/FileBrowser.svelte';
283
+ // Ecosystem utilities: highlighting, marquee, media, progress, phone, responsive
284
+ export { default as HighlightText } from './components/HighlightText.svelte';
285
+ export { splitHighlights, escapeRegExp } from './components/highlight.js';
286
+ export { default as Marquee } from './components/Marquee.svelte';
287
+ export { default as MediaPlayer } from './components/MediaPlayer.svelte';
288
+ export { default as TopProgressBar } from './components/TopProgressBar.svelte';
289
+ export { default as PhoneInput } from './components/PhoneInput.svelte';
290
+ export { PHONE_DIAL_CODES, findDialCodeByCountry, toE164, splitE164, formatNationalNumber, } from './components/phone-dial-codes.js';
291
+ export { useMediaQuery, useDeviceDetect, MOBILE_QUERY, TABLET_QUERY, TOUCH_QUERY, REDUCED_MOTION_QUERY, DARK_QUERY, } from './components/media.svelte.js';
292
+ export { useClipboard, useLocalStorage, useDebouncedValue, useInterval, useEventListener, useOnClickOutside, useWindowSize, } from './components/browser.svelte.js';
293
+ export { useSwipe } from './components/browser.svelte.js';
294
+ export { default as AppFooter } from './components/AppFooter.svelte';
295
+ export { default as ResizableGrid } from './components/ResizableGrid.svelte';