ui-svelte 0.1.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/LICENSE +21 -0
- package/README.md +118 -0
- package/dist/charts/ArcChart.svelte +320 -0
- package/dist/charts/ArcChart.svelte.d.ts +26 -0
- package/dist/charts/AreaChart.svelte +495 -0
- package/dist/charts/AreaChart.svelte.d.ts +32 -0
- package/dist/charts/BarChart.svelte +504 -0
- package/dist/charts/BarChart.svelte.d.ts +38 -0
- package/dist/charts/Candlestick.svelte +527 -0
- package/dist/charts/Candlestick.svelte.d.ts +38 -0
- package/dist/charts/LineChart.svelte +365 -0
- package/dist/charts/LineChart.svelte.d.ts +36 -0
- package/dist/charts/PieChart.svelte +311 -0
- package/dist/charts/PieChart.svelte.d.ts +28 -0
- package/dist/charts/css/arc-chart.css +237 -0
- package/dist/charts/css/area-chart.css +289 -0
- package/dist/charts/css/bar-chart.css +167 -0
- package/dist/charts/css/candlestick.css +197 -0
- package/dist/charts/css/line-chart.css +202 -0
- package/dist/charts/css/pie-chart.css +199 -0
- package/dist/control/Audio.svelte +212 -0
- package/dist/control/Audio.svelte.d.ts +8 -0
- package/dist/control/Button.svelte +116 -0
- package/dist/control/Button.svelte.d.ts +22 -0
- package/dist/control/IconButton.svelte +104 -0
- package/dist/control/IconButton.svelte.d.ts +17 -0
- package/dist/control/Record.svelte +430 -0
- package/dist/control/Record.svelte.d.ts +11 -0
- package/dist/control/ToggleTheme.svelte +21 -0
- package/dist/control/ToggleTheme.svelte.d.ts +8 -0
- package/dist/control/Video.svelte +222 -0
- package/dist/control/Video.svelte.d.ts +10 -0
- package/dist/control/css/btn.css +206 -0
- package/dist/control/css/media.css +78 -0
- package/dist/control/css/video.css +58 -0
- package/dist/css/animations.css +27 -0
- package/dist/css/base.css +192 -0
- package/dist/css/utilities.css +136 -0
- package/dist/display/Accordion.svelte +98 -0
- package/dist/display/Accordion.svelte.d.ts +20 -0
- package/dist/display/Alert.svelte +65 -0
- package/dist/display/Alert.svelte.d.ts +15 -0
- package/dist/display/Avatar.svelte +80 -0
- package/dist/display/Avatar.svelte.d.ts +13 -0
- package/dist/display/Badge.svelte +46 -0
- package/dist/display/Badge.svelte.d.ts +11 -0
- package/dist/display/Card.svelte +94 -0
- package/dist/display/Card.svelte.d.ts +21 -0
- package/dist/display/Carousel.svelte +359 -0
- package/dist/display/Carousel.svelte.d.ts +25 -0
- package/dist/display/ChatBox.svelte +249 -0
- package/dist/display/ChatBox.svelte.d.ts +18 -0
- package/dist/display/Chip.svelte +67 -0
- package/dist/display/Chip.svelte.d.ts +17 -0
- package/dist/display/Code.svelte +56 -0
- package/dist/display/Code.svelte.d.ts +9 -0
- package/dist/display/Collapsible.svelte +71 -0
- package/dist/display/Collapsible.svelte.d.ts +15 -0
- package/dist/display/Divider.svelte +32 -0
- package/dist/display/Divider.svelte.d.ts +10 -0
- package/dist/display/Empty.svelte +462 -0
- package/dist/display/Empty.svelte.d.ts +11 -0
- package/dist/display/Icon.svelte +20 -0
- package/dist/display/Icon.svelte.d.ts +11 -0
- package/dist/display/Item.svelte +119 -0
- package/dist/display/Item.svelte.d.ts +24 -0
- package/dist/display/Loading.svelte +8 -0
- package/dist/display/Loading.svelte.d.ts +26 -0
- package/dist/display/Marquee.svelte +164 -0
- package/dist/display/Marquee.svelte.d.ts +21 -0
- package/dist/display/Section.svelte +63 -0
- package/dist/display/Section.svelte.d.ts +16 -0
- package/dist/display/Table.svelte +407 -0
- package/dist/display/Table.svelte.d.ts +32 -0
- package/dist/display/TypeWriter.svelte +23 -0
- package/dist/display/TypeWriter.svelte.d.ts +11 -0
- package/dist/display/User.svelte +0 -0
- package/dist/display/User.svelte.d.ts +26 -0
- package/dist/display/css/accordion.css +98 -0
- package/dist/display/css/alert.css +51 -0
- package/dist/display/css/avatar.css +158 -0
- package/dist/display/css/badge.css +47 -0
- package/dist/display/css/card.css +231 -0
- package/dist/display/css/carousel.css +156 -0
- package/dist/display/css/chat-box.css +188 -0
- package/dist/display/css/chip.css +91 -0
- package/dist/display/css/code.css +19 -0
- package/dist/display/css/collapsible.css +86 -0
- package/dist/display/css/divider.css +54 -0
- package/dist/display/css/empty.css +8 -0
- package/dist/display/css/item.css +149 -0
- package/dist/display/css/listbox.css +24 -0
- package/dist/display/css/marquee.css +138 -0
- package/dist/display/css/section.css +85 -0
- package/dist/display/css/table.css +361 -0
- package/dist/form/Checkbox.svelte +45 -0
- package/dist/form/Checkbox.svelte.d.ts +13 -0
- package/dist/form/ComboBox.svelte +448 -0
- package/dist/form/ComboBox.svelte.d.ts +29 -0
- package/dist/form/CsvField.svelte +389 -0
- package/dist/form/CsvField.svelte.d.ts +21 -0
- package/dist/form/DateField.svelte +292 -0
- package/dist/form/DateField.svelte.d.ts +18 -0
- package/dist/form/Dropzone.svelte +196 -0
- package/dist/form/Dropzone.svelte.d.ts +30 -0
- package/dist/form/ImageCropper.svelte +254 -0
- package/dist/form/ImageCropper.svelte.d.ts +14 -0
- package/dist/form/PasswordField.svelte +170 -0
- package/dist/form/PasswordField.svelte.d.ts +28 -0
- package/dist/form/PhoneField.svelte +485 -0
- package/dist/form/PhoneField.svelte.d.ts +25 -0
- package/dist/form/PinField.svelte +139 -0
- package/dist/form/PinField.svelte.d.ts +17 -0
- package/dist/form/RadioGroup.svelte +70 -0
- package/dist/form/RadioGroup.svelte.d.ts +19 -0
- package/dist/form/Select.svelte +350 -0
- package/dist/form/Select.svelte.d.ts +26 -0
- package/dist/form/Slider.svelte +60 -0
- package/dist/form/Slider.svelte.d.ts +15 -0
- package/dist/form/TextField.svelte +154 -0
- package/dist/form/TextField.svelte.d.ts +31 -0
- package/dist/form/Textarea.svelte +137 -0
- package/dist/form/Textarea.svelte.d.ts +27 -0
- package/dist/form/Toggle.svelte +45 -0
- package/dist/form/Toggle.svelte.d.ts +13 -0
- package/dist/form/css/checkbox.css +46 -0
- package/dist/form/css/combo-box.css +69 -0
- package/dist/form/css/control.css +177 -0
- package/dist/form/css/csv-field.css +0 -0
- package/dist/form/css/date.css +56 -0
- package/dist/form/css/dropzone.css +133 -0
- package/dist/form/css/field.css +17 -0
- package/dist/form/css/image-cropper.css +155 -0
- package/dist/form/css/password.css +35 -0
- package/dist/form/css/radio-group.css +57 -0
- package/dist/form/css/select.css +18 -0
- package/dist/form/css/slider.css +80 -0
- package/dist/form/css/textarea.css +130 -0
- package/dist/form/css/toggle.css +27 -0
- package/dist/form/js/countries.d.ts +13 -0
- package/dist/form/js/countries.js +307 -0
- package/dist/form/js/phone-examples.d.ts +248 -0
- package/dist/form/js/phone-examples.js +247 -0
- package/dist/hooks/use-auth.svelte.d.ts +11 -0
- package/dist/hooks/use-auth.svelte.js +59 -0
- package/dist/hooks/use-chat.svelte.d.ts +40 -0
- package/dist/hooks/use-chat.svelte.js +265 -0
- package/dist/hooks/use-clipboard.svelte.d.ts +9 -0
- package/dist/hooks/use-clipboard.svelte.js +52 -0
- package/dist/hooks/use-fetch.svelte.d.ts +11 -0
- package/dist/hooks/use-fetch.svelte.js +38 -0
- package/dist/hooks/use-form.svelte.d.ts +31 -0
- package/dist/hooks/use-form.svelte.js +110 -0
- package/dist/hooks/use-localstorage.svelte.d.ts +3 -0
- package/dist/hooks/use-localstorage.svelte.js +26 -0
- package/dist/hooks/use-scroll.svelte.d.ts +6 -0
- package/dist/hooks/use-scroll.svelte.js +34 -0
- package/dist/hooks/use-search.svelte.d.ts +49 -0
- package/dist/hooks/use-search.svelte.js +229 -0
- package/dist/hooks/use-table.svelte.d.ts +85 -0
- package/dist/hooks/use-table.svelte.js +362 -0
- package/dist/hooks/use-websocket.svelte.d.ts +18 -0
- package/dist/hooks/use-websocket.svelte.js +79 -0
- package/dist/icons/index.d.ts +132 -0
- package/dist/icons/index.js +132 -0
- package/dist/index.css +115 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.js +76 -0
- package/dist/layout/AppBar.svelte +94 -0
- package/dist/layout/AppBar.svelte.d.ts +17 -0
- package/dist/layout/Footer.svelte +94 -0
- package/dist/layout/Footer.svelte.d.ts +17 -0
- package/dist/layout/FooterLinks.svelte +28 -0
- package/dist/layout/FooterLinks.svelte.d.ts +11 -0
- package/dist/layout/Provider.svelte +52 -0
- package/dist/layout/Provider.svelte.d.ts +10 -0
- package/dist/layout/Scaffold.svelte +46 -0
- package/dist/layout/Scaffold.svelte.d.ts +15 -0
- package/dist/layout/Sidebar.svelte +40 -0
- package/dist/layout/Sidebar.svelte.d.ts +13 -0
- package/dist/layout/css/app-bar.css +35 -0
- package/dist/layout/css/bottom-bar.css +12 -0
- package/dist/layout/css/footer-links.css +17 -0
- package/dist/layout/css/footer.css +35 -0
- package/dist/layout/css/scaffold.css +15 -0
- package/dist/layout/css/sidebar.css +17 -0
- package/dist/navigation/BottomNav.svelte +0 -0
- package/dist/navigation/BottomNav.svelte.d.ts +26 -0
- package/dist/navigation/NavMenu.svelte +254 -0
- package/dist/navigation/SideNav.svelte +249 -0
- package/dist/navigation/Tabs.svelte +79 -0
- package/dist/navigation/Tabs.svelte.d.ts +19 -0
- package/dist/navigation/css/bottom-nav.css +0 -0
- package/dist/navigation/css/nav-menu.css +168 -0
- package/dist/navigation/css/side-nav.css +244 -0
- package/dist/navigation/css/tabs.css +118 -0
- package/dist/overlay/AlertDialog.svelte +0 -0
- package/dist/overlay/AlertDialog.svelte.d.ts +26 -0
- package/dist/overlay/Command.svelte +0 -0
- package/dist/overlay/Command.svelte.d.ts +26 -0
- package/dist/overlay/Drawer.svelte +129 -0
- package/dist/overlay/Drawer.svelte.d.ts +20 -0
- package/dist/overlay/Dropdown.svelte +140 -0
- package/dist/overlay/Modal.svelte +102 -0
- package/dist/overlay/Modal.svelte.d.ts +19 -0
- package/dist/overlay/PopoverStack.svelte +0 -0
- package/dist/overlay/PopoverStack.svelte.d.ts +26 -0
- package/dist/overlay/Toast.svelte +83 -0
- package/dist/overlay/Toast.svelte.d.ts +9 -0
- package/dist/overlay/Tooltip.svelte +140 -0
- package/dist/overlay/Tooltip.svelte.d.ts +12 -0
- package/dist/overlay/css/drawer.css +75 -0
- package/dist/overlay/css/dropdown.css +24 -0
- package/dist/overlay/css/hovercard.css +11 -0
- package/dist/overlay/css/modal.css +51 -0
- package/dist/overlay/css/toast.css +80 -0
- package/dist/overlay/css/tooltip.css +89 -0
- package/dist/stores/i18n.svelte.d.ts +16 -0
- package/dist/stores/i18n.svelte.js +137 -0
- package/dist/stores/theme.svelte.d.ts +5 -0
- package/dist/stores/theme.svelte.js +55 -0
- package/dist/stores/toast.svelte.d.ts +19 -0
- package/dist/stores/toast.svelte.js +38 -0
- package/dist/types.d.ts +75 -0
- package/dist/types.js +1 -0
- package/dist/utils/charts.d.ts +27 -0
- package/dist/utils/charts.js +140 -0
- package/dist/utils/class-names.d.ts +1 -0
- package/dist/utils/class-names.js +3 -0
- package/dist/utils/click-outside.d.ts +3 -0
- package/dist/utils/click-outside.js +9 -0
- package/dist/utils/popover.d.ts +3 -0
- package/dist/utils/popover.js +17 -0
- package/dist/utils/ulid.d.ts +1 -0
- package/dist/utils/ulid.js +22 -0
- package/dist/utils/validate-schema.d.ts +2 -0
- package/dist/utils/validate-schema.js +97 -0
- package/package.json +69 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 SappsJs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# SappsUI
|
|
2
|
+
|
|
3
|
+
**A modern, accessible UI component library built for Svelte 5**
|
|
4
|
+
|
|
5
|
+
SappsUI is a comprehensive UI component library designed to leverage Svelte 5's latest features. Build beautiful, accessible interfaces with minimal configuration and maximum developer experience.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## ✨ Features
|
|
10
|
+
|
|
11
|
+
### 🎨 Modern Design System
|
|
12
|
+
|
|
13
|
+
Beautiful, pre-styled components with multiple variants and customization options. Choose from primary, secondary, soft, outlined, and ghost variants to match your design needs.
|
|
14
|
+
|
|
15
|
+
### 🎯 Built for Svelte 5
|
|
16
|
+
|
|
17
|
+
Fully optimized for Svelte 5's runes system with `$state`, `$derived`, and snippets. Experience the full power of Svelte's reactivity with a modern component API.
|
|
18
|
+
|
|
19
|
+
### 🔧 Highly Customizable
|
|
20
|
+
|
|
21
|
+
Every component accepts custom classes and styling props. Customize individual sections with dedicated class props for headers, bodies, footers, and more.
|
|
22
|
+
|
|
23
|
+
### 📱 Responsive Design
|
|
24
|
+
|
|
25
|
+
Mobile-first design approach with responsive sizing options (xs, sm, md, lg, xl). Components adapt seamlessly to different screen sizes and devices.
|
|
26
|
+
|
|
27
|
+
### ♿ Accessibility First
|
|
28
|
+
|
|
29
|
+
Built with accessibility in mind, following WCAG guidelines. Proper ARIA attributes, keyboard navigation, and semantic HTML ensure your apps are usable by everyone.
|
|
30
|
+
|
|
31
|
+
### 🎭 Rich Component Variants
|
|
32
|
+
|
|
33
|
+
Multiple style variants for every component including primary, secondary, soft, outlined, ghost, and semantic variants (success, info, warning, danger) for contextual feedback.
|
|
34
|
+
|
|
35
|
+
### 🖼️ Icon Integration
|
|
36
|
+
|
|
37
|
+
Seamless integration with Iconify for thousands of icon options. Add icons to buttons, text fields, and other components with a simple prop.
|
|
38
|
+
|
|
39
|
+
### 📝 Form Components
|
|
40
|
+
|
|
41
|
+
Comprehensive form component suite with built-in validation, error states, help text, and floating labels. Build complex forms with ease.
|
|
42
|
+
|
|
43
|
+
### 🎪 Interactive Elements
|
|
44
|
+
|
|
45
|
+
Rich interactive components including tabs, cards, selects, and more. Create dynamic user interfaces with minimal code.
|
|
46
|
+
|
|
47
|
+
### 🎨 Theming Support
|
|
48
|
+
|
|
49
|
+
Flexible theming system with support for custom color schemes. Adapt components to match your brand identity effortlessly.
|
|
50
|
+
|
|
51
|
+
### 📦 TypeScript Support
|
|
52
|
+
|
|
53
|
+
Full TypeScript support with comprehensive type definitions. Enjoy autocomplete, type checking, and compile-time error detection throughout your development process.
|
|
54
|
+
|
|
55
|
+
### 🚀 Lightweight & Fast
|
|
56
|
+
|
|
57
|
+
Optimized bundle size with tree-shaking support. Only import what you need for blazing-fast load times and minimal overhead.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 📦 Installation
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# or
|
|
65
|
+
bun add sappsui
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 🚀 Quick Start
|
|
71
|
+
|
|
72
|
+
```svelte
|
|
73
|
+
<script lang="ts">
|
|
74
|
+
import { Button, Card, TextField } from 'sappsui';
|
|
75
|
+
|
|
76
|
+
let name = $state('');
|
|
77
|
+
</script>
|
|
78
|
+
|
|
79
|
+
<Card>
|
|
80
|
+
<h2>Welcome to SappsUI</h2>
|
|
81
|
+
|
|
82
|
+
<TextField name="username" label="Your Name" placeholder="Enter your name..." bind:value={name} />
|
|
83
|
+
|
|
84
|
+
<Button label="Submit" variant="primary" onclick={() => console.log('Hello', name)} />
|
|
85
|
+
</Card>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## 📖 Documentation
|
|
91
|
+
|
|
92
|
+
For detailed documentation, examples, and interactive component previews, visit our [official documentation](https://ui.sappsdev.com).
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## 🎨 Component Library
|
|
97
|
+
|
|
98
|
+
SappsUI includes a comprehensive set of components for building modern web applications:
|
|
99
|
+
|
|
100
|
+
- **Buttons & Actions**: Button, IconButton
|
|
101
|
+
- **Forms**: TextField, Select, Checkbox
|
|
102
|
+
- **Layout**: Card, Divider, Section
|
|
103
|
+
- **Navigation**: Tabs
|
|
104
|
+
- **Data Display**: Avatar, Code
|
|
105
|
+
- **Feedback**: Status indicators, Error states
|
|
106
|
+
- **And more...**
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## 🌟 Support
|
|
111
|
+
|
|
112
|
+
If you find SappsUI helpful, please consider giving it a star on GitHub! Your support helps us continue improving the library.
|
|
113
|
+
|
|
114
|
+
For questions, issues, or feature requests, please visit our [GitHub Issues](https://github.com/sappsdev/sappsui_svelte/issues) page.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
**Built with ❤️ by the SappsDev Team**
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../utils/class-names.js';
|
|
3
|
+
import { onMount, untrack } from 'svelte';
|
|
4
|
+
|
|
5
|
+
type Color = 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'danger' | 'muted';
|
|
6
|
+
|
|
7
|
+
type ArcData = {
|
|
8
|
+
value: number;
|
|
9
|
+
max?: number;
|
|
10
|
+
color: Color;
|
|
11
|
+
label?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type Props = {
|
|
15
|
+
data?: ArcData[];
|
|
16
|
+
centerText?: string;
|
|
17
|
+
centerValue?: string | number;
|
|
18
|
+
thickness?: number;
|
|
19
|
+
gap?: number;
|
|
20
|
+
animated?: boolean;
|
|
21
|
+
animationDuration?: number;
|
|
22
|
+
loading?: boolean;
|
|
23
|
+
empty?: boolean;
|
|
24
|
+
emptyText?: string;
|
|
25
|
+
showLegend?: boolean;
|
|
26
|
+
showValues?: boolean;
|
|
27
|
+
class?: string;
|
|
28
|
+
chartClass?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
let {
|
|
32
|
+
data = [],
|
|
33
|
+
centerText = '',
|
|
34
|
+
centerValue = '',
|
|
35
|
+
thickness = 16,
|
|
36
|
+
gap = 8,
|
|
37
|
+
animated = true,
|
|
38
|
+
animationDuration = 1000,
|
|
39
|
+
loading = false,
|
|
40
|
+
empty = false,
|
|
41
|
+
emptyText = 'No data',
|
|
42
|
+
showLegend = true,
|
|
43
|
+
showValues = true,
|
|
44
|
+
class: className,
|
|
45
|
+
chartClass
|
|
46
|
+
}: Props = $props();
|
|
47
|
+
|
|
48
|
+
const colorClass = {
|
|
49
|
+
primary: 'is-primary',
|
|
50
|
+
secondary: 'is-secondary',
|
|
51
|
+
success: 'is-success',
|
|
52
|
+
info: 'is-info',
|
|
53
|
+
warning: 'is-warning',
|
|
54
|
+
danger: 'is-danger',
|
|
55
|
+
muted: 'is-muted'
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
let containerEl: HTMLDivElement | undefined = $state();
|
|
59
|
+
let displayPercentages = $state<number[]>(data.map(() => 0));
|
|
60
|
+
let animationFrameId: number | null = null;
|
|
61
|
+
|
|
62
|
+
let tooltipData = $state<(ArcData & { percentage: number }) | null>(null);
|
|
63
|
+
let tooltipPosition = $state<{ x: number; y: number }>({ x: 0, y: 0 });
|
|
64
|
+
let isTooltipActive = $state(false);
|
|
65
|
+
|
|
66
|
+
let containerSize = $state({ width: 0, height: 0 });
|
|
67
|
+
let width = $derived(containerSize.width);
|
|
68
|
+
let height = $derived(containerSize.height);
|
|
69
|
+
let size = $derived(Math.min(containerSize.width, containerSize.height));
|
|
70
|
+
let viewBoxSize = $derived(size);
|
|
71
|
+
let center = $derived(viewBoxSize / 2);
|
|
72
|
+
|
|
73
|
+
let responsiveThickness = $derived(() => {
|
|
74
|
+
const baseSize = 300;
|
|
75
|
+
const scale = size / baseSize;
|
|
76
|
+
return Math.max(8, Math.min(thickness * scale, thickness));
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
let responsiveGap = $derived(() => {
|
|
80
|
+
const baseSize = 300;
|
|
81
|
+
const scale = size / baseSize;
|
|
82
|
+
return Math.max(4, Math.min(gap * scale, gap));
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
let maxRadius = $derived(center - 20);
|
|
86
|
+
|
|
87
|
+
let responsiveLabelFontSize = $derived(() => {
|
|
88
|
+
const baseSize = 300;
|
|
89
|
+
const scale = size / baseSize;
|
|
90
|
+
return Math.max(10, Math.min(14 * scale, 14));
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
let responsiveValueFontSize = $derived(() => {
|
|
94
|
+
const baseSize = 300;
|
|
95
|
+
const scale = size / baseSize;
|
|
96
|
+
return Math.max(16, Math.min(32 * scale, 32));
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
let shouldShowText = $derived(size >= 120);
|
|
100
|
+
|
|
101
|
+
let computedCenterValue = $derived(centerValue || data.reduce((sum, arc) => sum + arc.value, 0));
|
|
102
|
+
|
|
103
|
+
let totalMax = $derived(() => {
|
|
104
|
+
if (data.length === 0) return 0;
|
|
105
|
+
if (data.length === 1 && data[0].max !== undefined) {
|
|
106
|
+
return data[0].max;
|
|
107
|
+
}
|
|
108
|
+
return data.reduce((sum, arc) => sum + (arc.max || arc.value), 0);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
onMount(() => {
|
|
112
|
+
const updateSize = () => {
|
|
113
|
+
if (containerEl) {
|
|
114
|
+
const rect = containerEl.getBoundingClientRect();
|
|
115
|
+
containerSize = { width: rect.width, height: rect.height };
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
updateSize();
|
|
120
|
+
const resizeObserver = new ResizeObserver(updateSize);
|
|
121
|
+
if (containerEl) {
|
|
122
|
+
resizeObserver.observe(containerEl);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return () => {
|
|
126
|
+
if (animationFrameId !== null) {
|
|
127
|
+
cancelAnimationFrame(animationFrameId);
|
|
128
|
+
}
|
|
129
|
+
resizeObserver.disconnect();
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
$effect(() => {
|
|
134
|
+
const total = totalMax();
|
|
135
|
+
const targetPercentages = data.map((arc) => {
|
|
136
|
+
const max = data.length === 1 && arc.max !== undefined ? arc.max : total;
|
|
137
|
+
return Math.min(Math.max(arc.value / max, 0), 1);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (animationFrameId !== null) {
|
|
141
|
+
cancelAnimationFrame(animationFrameId);
|
|
142
|
+
animationFrameId = null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (animated) {
|
|
146
|
+
const startTime = Date.now();
|
|
147
|
+
const startPercentages = untrack(() => [...displayPercentages]);
|
|
148
|
+
|
|
149
|
+
const animate = () => {
|
|
150
|
+
const now = Date.now();
|
|
151
|
+
const progress = Math.min((now - startTime) / animationDuration, 1);
|
|
152
|
+
const easeProgress = 1 - Math.pow(1 - progress, 3);
|
|
153
|
+
|
|
154
|
+
const newPercentages = startPercentages.map(
|
|
155
|
+
(start, i) => start + (targetPercentages[i] - start) * easeProgress
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
displayPercentages = newPercentages;
|
|
159
|
+
|
|
160
|
+
if (progress < 1) {
|
|
161
|
+
animationFrameId = requestAnimationFrame(animate);
|
|
162
|
+
} else {
|
|
163
|
+
animationFrameId = null;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
animationFrameId = requestAnimationFrame(animate);
|
|
168
|
+
} else {
|
|
169
|
+
displayPercentages = targetPercentages;
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
function handleArcHover(arc: ArcData, percentage: number, event: MouseEvent): void {
|
|
174
|
+
const target = event.target as SVGCircleElement;
|
|
175
|
+
const rect = target.getBoundingClientRect();
|
|
176
|
+
|
|
177
|
+
const max = data.length === 1 && arc.max !== undefined ? arc.max : totalMax();
|
|
178
|
+
|
|
179
|
+
tooltipData = { ...arc, max, percentage: percentage * 100 };
|
|
180
|
+
tooltipPosition = {
|
|
181
|
+
x: rect.left + rect.width / 2,
|
|
182
|
+
y: rect.top - 10
|
|
183
|
+
};
|
|
184
|
+
isTooltipActive = true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function handleArcLeave(): void {
|
|
188
|
+
isTooltipActive = false;
|
|
189
|
+
setTimeout(() => {
|
|
190
|
+
if (!isTooltipActive) {
|
|
191
|
+
tooltipData = null;
|
|
192
|
+
}
|
|
193
|
+
}, 100);
|
|
194
|
+
}
|
|
195
|
+
</script>
|
|
196
|
+
|
|
197
|
+
<div class={cn('arc-chart-container', className)}>
|
|
198
|
+
{#if loading}
|
|
199
|
+
<div class="arc-chart-loading">
|
|
200
|
+
<svg class="arc-chart-loading-spinner" viewBox="0 0 24 24">
|
|
201
|
+
<circle
|
|
202
|
+
cx="12"
|
|
203
|
+
cy="12"
|
|
204
|
+
r="10"
|
|
205
|
+
stroke="currentColor"
|
|
206
|
+
stroke-width="4"
|
|
207
|
+
fill="none"
|
|
208
|
+
opacity="0.25"
|
|
209
|
+
/>
|
|
210
|
+
<path
|
|
211
|
+
fill="currentColor"
|
|
212
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
213
|
+
/>
|
|
214
|
+
</svg>
|
|
215
|
+
</div>
|
|
216
|
+
{:else if empty}
|
|
217
|
+
<div class="arc-chart-empty">
|
|
218
|
+
<span>{emptyText}</span>
|
|
219
|
+
</div>
|
|
220
|
+
{:else}
|
|
221
|
+
<div bind:this={containerEl} class={cn('arc-chart', chartClass)}>
|
|
222
|
+
<svg class="arc-chart-svg" viewBox="0 0 {viewBoxSize} {viewBoxSize}">
|
|
223
|
+
{#each data as arc, i}
|
|
224
|
+
{@const currentThickness = responsiveThickness()}
|
|
225
|
+
{@const currentGap = responsiveGap()}
|
|
226
|
+
{@const radius = maxRadius - i * (currentThickness + currentGap)}
|
|
227
|
+
{@const percentage = displayPercentages[i]}
|
|
228
|
+
{@const circumference = 2 * Math.PI * radius}
|
|
229
|
+
{@const strokeDashoffset = circumference * (1 - percentage)}
|
|
230
|
+
|
|
231
|
+
<circle
|
|
232
|
+
cx={center}
|
|
233
|
+
cy={center}
|
|
234
|
+
r={radius}
|
|
235
|
+
class="arc-chart-background"
|
|
236
|
+
stroke-width={currentThickness}
|
|
237
|
+
/>
|
|
238
|
+
|
|
239
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
240
|
+
<circle
|
|
241
|
+
cx={center}
|
|
242
|
+
cy={center}
|
|
243
|
+
r={radius}
|
|
244
|
+
class="arc-chart-arc is-{arc.color}"
|
|
245
|
+
stroke-width={currentThickness}
|
|
246
|
+
style="
|
|
247
|
+
stroke-dasharray: {circumference};
|
|
248
|
+
stroke-dashoffset: {strokeDashoffset};
|
|
249
|
+
transform: rotate(-90deg);
|
|
250
|
+
transform-origin: center;
|
|
251
|
+
"
|
|
252
|
+
onmouseenter={(e) => handleArcHover(arc, percentage, e)}
|
|
253
|
+
onmouseleave={handleArcLeave}
|
|
254
|
+
/>
|
|
255
|
+
{/each}
|
|
256
|
+
|
|
257
|
+
{#if (centerText || computedCenterValue) && shouldShowText}
|
|
258
|
+
<text
|
|
259
|
+
x={center}
|
|
260
|
+
y={center - 10}
|
|
261
|
+
class="arc-chart-label"
|
|
262
|
+
text-anchor="middle"
|
|
263
|
+
dominant-baseline="middle"
|
|
264
|
+
style="font-size: {responsiveLabelFontSize()}px; font-weight: 500;"
|
|
265
|
+
>
|
|
266
|
+
{centerText}
|
|
267
|
+
</text>
|
|
268
|
+
<text
|
|
269
|
+
x={center}
|
|
270
|
+
y={center + 15}
|
|
271
|
+
class="arc-chart-value"
|
|
272
|
+
text-anchor="middle"
|
|
273
|
+
dominant-baseline="middle"
|
|
274
|
+
style="font-size: {responsiveValueFontSize()}px;"
|
|
275
|
+
>
|
|
276
|
+
{computedCenterValue}
|
|
277
|
+
</text>
|
|
278
|
+
{/if}
|
|
279
|
+
</svg>
|
|
280
|
+
</div>
|
|
281
|
+
|
|
282
|
+
{#if tooltipData && isTooltipActive}
|
|
283
|
+
<div
|
|
284
|
+
class="arc-chart-tooltip"
|
|
285
|
+
style="left: {tooltipPosition.x}px; top: {tooltipPosition.y}px;"
|
|
286
|
+
>
|
|
287
|
+
<div class="arc-chart-tooltip-content">
|
|
288
|
+
{#if tooltipData.label}
|
|
289
|
+
<div class="arc-chart-tooltip-title">{tooltipData.label}</div>
|
|
290
|
+
{/if}
|
|
291
|
+
<div class="arc-chart-tooltip-row">
|
|
292
|
+
<div class="arc-chart-tooltip-color is-{tooltipData.color}"></div>
|
|
293
|
+
<span class="arc-chart-tooltip-value">
|
|
294
|
+
{tooltipData.value} / {tooltipData.max} ({tooltipData.percentage.toFixed(1)}%)
|
|
295
|
+
</span>
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
{/if}
|
|
300
|
+
|
|
301
|
+
{#if showLegend}
|
|
302
|
+
<div class="arc-chart-legend">
|
|
303
|
+
{#each data as arc, i}
|
|
304
|
+
{@const percentage = displayPercentages[i]}
|
|
305
|
+
{@const max = data.length === 1 && arc.max !== undefined ? arc.max : totalMax()}
|
|
306
|
+
<div class="arc-chart-legend-item">
|
|
307
|
+
<div class={cn('arc-chart-legend-color', colorClass[arc.color])}></div>
|
|
308
|
+
<span>{arc.label || `Arc ${i + 1}`}</span>
|
|
309
|
+
{#if showValues}
|
|
310
|
+
<span class="arc-chart-legend-value">
|
|
311
|
+
({arc.value}{#if max}
|
|
312
|
+
/ {max}{/if})
|
|
313
|
+
</span>
|
|
314
|
+
{/if}
|
|
315
|
+
</div>
|
|
316
|
+
{/each}
|
|
317
|
+
</div>
|
|
318
|
+
{/if}
|
|
319
|
+
{/if}
|
|
320
|
+
</div>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
type Color = 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'danger' | 'muted';
|
|
2
|
+
type ArcData = {
|
|
3
|
+
value: number;
|
|
4
|
+
max?: number;
|
|
5
|
+
color: Color;
|
|
6
|
+
label?: string;
|
|
7
|
+
};
|
|
8
|
+
type Props = {
|
|
9
|
+
data?: ArcData[];
|
|
10
|
+
centerText?: string;
|
|
11
|
+
centerValue?: string | number;
|
|
12
|
+
thickness?: number;
|
|
13
|
+
gap?: number;
|
|
14
|
+
animated?: boolean;
|
|
15
|
+
animationDuration?: number;
|
|
16
|
+
loading?: boolean;
|
|
17
|
+
empty?: boolean;
|
|
18
|
+
emptyText?: string;
|
|
19
|
+
showLegend?: boolean;
|
|
20
|
+
showValues?: boolean;
|
|
21
|
+
class?: string;
|
|
22
|
+
chartClass?: string;
|
|
23
|
+
};
|
|
24
|
+
declare const ArcChart: import("svelte").Component<Props, {}, "">;
|
|
25
|
+
type ArcChart = ReturnType<typeof ArcChart>;
|
|
26
|
+
export default ArcChart;
|