@rimelight/ui 0.0.30 → 0.0.31

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 (152) hide show
  1. package/package.json +1 -1
  2. package/src/components/astro/RLAAlert.astro +60 -0
  3. package/src/components/astro/RLAAlertDialog.astro +86 -0
  4. package/src/components/astro/RLAAspectRatio.astro +23 -0
  5. package/src/components/astro/RLAAvatar.astro +56 -0
  6. package/src/components/astro/RLAAvatarGroup.astro +19 -0
  7. package/src/components/astro/RLABadge.astro +31 -0
  8. package/src/components/astro/RLABreadcrumb.astro +44 -0
  9. package/src/components/astro/RLACard.astro +74 -0
  10. package/src/components/astro/RLACarousel.astro +71 -0
  11. package/src/components/astro/RLAChart.astro +215 -0
  12. package/src/components/astro/RLACheckbox.astro +51 -0
  13. package/src/components/astro/RLACollapsible.astro +38 -0
  14. package/src/components/astro/RLACombobox.astro +131 -0
  15. package/src/components/astro/RLADialog.astro +95 -0
  16. package/src/components/astro/RLADrawer.astro +74 -0
  17. package/src/components/astro/RLADropdownMenu.astro +82 -0
  18. package/src/components/astro/RLADropzone.astro +134 -0
  19. package/src/components/astro/RLAField.astro +43 -0
  20. package/src/components/{Head.astro → astro/RLAHead.astro} +181 -199
  21. package/src/components/astro/RLAHoverCard.astro +31 -0
  22. package/src/components/astro/RLAInput.astro +60 -0
  23. package/src/components/astro/RLAInputGroup.astro +35 -0
  24. package/src/components/astro/RLAKbd.astro +22 -0
  25. package/src/components/astro/RLALabel.astro +23 -0
  26. package/src/components/astro/RLALocaleSelector.astro +110 -0
  27. package/src/components/astro/RLAMarquee.astro +32 -0
  28. package/src/components/astro/RLANativeSelect.astro +51 -0
  29. package/src/components/astro/RLANavigationMenu.astro +136 -0
  30. package/src/components/astro/RLAPagination.astro +94 -0
  31. package/src/components/astro/RLAPopover.astro +31 -0
  32. package/src/components/astro/RLAProgress.astro +43 -0
  33. package/src/components/astro/RLAResizable.astro +177 -0
  34. package/src/components/astro/RLAScrollArea.astro +157 -0
  35. package/src/components/astro/RLASelect.astro +109 -0
  36. package/src/components/astro/RLASeparator.astro +27 -0
  37. package/src/components/astro/RLASheet.astro +72 -0
  38. package/src/components/astro/RLASidebar.astro +135 -0
  39. package/src/components/astro/RLASkeleton.astro +20 -0
  40. package/src/components/astro/RLASlider.astro +53 -0
  41. package/src/components/astro/RLASpinner.astro +25 -0
  42. package/src/components/astro/RLAStickySurface.astro +27 -0
  43. package/src/components/astro/RLASwitch.astro +49 -0
  44. package/src/components/astro/RLATable.astro +60 -0
  45. package/src/components/astro/RLATabs.astro +92 -0
  46. package/src/components/astro/RLATextarea.astro +42 -0
  47. package/src/components/astro/RLAThemeSelector.astro +73 -0
  48. package/src/components/astro/RLAToast.astro +107 -0
  49. package/src/components/astro/RLAToggle.astro +38 -0
  50. package/src/components/astro/RLAToggleGroup.astro +30 -0
  51. package/src/components/astro/RLATooltip.astro +31 -0
  52. package/src/integrations/ui.ts +1 -0
  53. package/src/themes/alert.theme.ts +51 -0
  54. package/src/themes/alertDialog.theme.ts +33 -0
  55. package/src/themes/aspectRatio.theme.ts +11 -0
  56. package/src/themes/avatar.theme.ts +28 -0
  57. package/src/themes/avatarGroup.theme.ts +10 -0
  58. package/src/themes/badge.theme.ts +61 -0
  59. package/src/themes/breadcrumb.theme.ts +15 -0
  60. package/src/themes/card.theme.ts +34 -0
  61. package/src/themes/carousel.theme.ts +32 -0
  62. package/src/themes/chart.theme.ts +21 -0
  63. package/src/themes/checkbox.theme.ts +42 -0
  64. package/src/themes/collapsible.theme.ts +22 -0
  65. package/src/themes/combobox.theme.ts +28 -0
  66. package/src/themes/dialog.theme.ts +37 -0
  67. package/src/themes/drawer.theme.ts +44 -0
  68. package/src/themes/dropdownMenu.theme.ts +27 -0
  69. package/src/themes/dropzone.theme.ts +24 -0
  70. package/src/themes/field.theme.ts +24 -0
  71. package/src/themes/hoverCard.theme.ts +27 -0
  72. package/src/themes/input.theme.ts +36 -0
  73. package/src/themes/inputGroup.theme.ts +23 -0
  74. package/src/themes/kbd.theme.ts +24 -0
  75. package/src/themes/label.theme.ts +20 -0
  76. package/src/themes/localeSelector.theme.ts +50 -0
  77. package/src/themes/marquee.theme.ts +27 -0
  78. package/src/themes/nativeSelect.theme.ts +24 -0
  79. package/src/themes/navigationMenu.theme.ts +19 -0
  80. package/src/themes/pagination.theme.ts +26 -0
  81. package/src/themes/popover.theme.ts +28 -0
  82. package/src/themes/progress.theme.ts +26 -0
  83. package/src/themes/resizable.theme.ts +22 -0
  84. package/src/themes/scrollArea.theme.ts +24 -0
  85. package/src/themes/section.theme.ts +1 -2
  86. package/src/themes/select.theme.ts +27 -0
  87. package/src/themes/separator.theme.ts +19 -0
  88. package/src/themes/sheet.theme.ts +43 -0
  89. package/src/themes/sidebar.theme.ts +33 -0
  90. package/src/themes/skeleton.theme.ts +20 -0
  91. package/src/themes/slider.theme.ts +28 -0
  92. package/src/themes/spinner.theme.ts +30 -0
  93. package/src/themes/stickySurface.theme.ts +10 -0
  94. package/src/themes/switch.theme.ts +59 -0
  95. package/src/themes/table.theme.ts +36 -0
  96. package/src/themes/tabs.theme.ts +44 -0
  97. package/src/themes/textarea.theme.ts +26 -0
  98. package/src/themes/themeSelector.theme.ts +16 -0
  99. package/src/themes/toast.theme.ts +25 -0
  100. package/src/themes/toggle.theme.ts +42 -0
  101. package/src/themes/toggleGroup.theme.ts +21 -0
  102. package/src/themes/tooltip.theme.ts +27 -0
  103. package/src/types/components/alert-dialog.ts +35 -0
  104. package/src/types/components/alert.ts +31 -0
  105. package/src/types/components/aspect-ratio.ts +15 -0
  106. package/src/types/components/avatar.ts +47 -0
  107. package/src/types/components/badge.ts +27 -0
  108. package/src/types/components/breadcrumb.ts +24 -0
  109. package/src/types/components/card.ts +31 -0
  110. package/src/types/components/carousel.ts +25 -0
  111. package/src/types/components/chart.ts +38 -0
  112. package/src/types/components/checkbox.ts +35 -0
  113. package/src/types/components/collapsible.ts +31 -0
  114. package/src/types/components/combobox.ts +43 -0
  115. package/src/types/components/dialog.ts +39 -0
  116. package/src/types/components/drawer.ts +31 -0
  117. package/src/types/components/dropdown-menu.ts +42 -0
  118. package/src/types/components/dropzone.ts +39 -0
  119. package/src/types/components/field.ts +31 -0
  120. package/src/types/components/hover-card.ts +19 -0
  121. package/src/types/components/index.ts +47 -0
  122. package/src/types/components/input-group.ts +23 -0
  123. package/src/types/components/input.ts +59 -0
  124. package/src/types/components/kbd.ts +15 -0
  125. package/src/types/components/label.ts +19 -0
  126. package/src/types/components/localeSelector.ts +33 -0
  127. package/src/types/components/marquee.ts +23 -0
  128. package/src/types/components/native-select.ts +39 -0
  129. package/src/types/components/navigation-menu.ts +27 -0
  130. package/src/types/components/pagination.ts +23 -0
  131. package/src/types/components/popover.ts +23 -0
  132. package/src/types/components/progress.ts +27 -0
  133. package/src/types/components/resizable.ts +19 -0
  134. package/src/types/components/scroll-area.ts +23 -0
  135. package/src/types/components/select.ts +43 -0
  136. package/src/types/components/separator.ts +19 -0
  137. package/src/types/components/sheet.ts +31 -0
  138. package/src/types/components/sidebar.ts +36 -0
  139. package/src/types/components/skeleton.ts +15 -0
  140. package/src/types/components/slider.ts +35 -0
  141. package/src/types/components/spinner.ts +19 -0
  142. package/src/types/components/sticky-surface.ts +19 -0
  143. package/src/types/components/switch.ts +35 -0
  144. package/src/types/components/table.ts +31 -0
  145. package/src/types/components/tabs.ts +43 -0
  146. package/src/types/components/textarea.ts +51 -0
  147. package/src/types/components/themeSelector.ts +15 -0
  148. package/src/types/components/toast.ts +35 -0
  149. package/src/types/components/toggle-group.ts +23 -0
  150. package/src/types/components/toggle.ts +27 -0
  151. package/src/types/components/tooltip.ts +23 -0
  152. package/src/components/ThemeToggle.astro +0 -57
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rimelight/ui",
3
- "version": "0.0.30",
3
+ "version": "0.0.31",
4
4
  "private": false,
5
5
  "description": "Rimelight Entertainment UI Library.",
6
6
  "keywords": [
@@ -0,0 +1,60 @@
1
+ ---
2
+ import { scv, cx } from "css-variants"
3
+ import { useUi, resolveClasses } from "../../utils"
4
+ import type { AlertProps } from "../../types"
5
+ import alertThemeDefault, { createAlertTheme } from "../../themes/alert.theme"
6
+ import { getUIConfig } from "virtual:rimelight-ui"
7
+
8
+ const uiConfig = getUIConfig()
9
+ const alertTheme = uiConfig.colors ? createAlertTheme(uiConfig.colors) : alertThemeDefault
10
+ const alert = scv(alertTheme)
11
+
12
+ const {
13
+ variant = "default",
14
+ color = "primary",
15
+ title,
16
+ description,
17
+ icon,
18
+ ui: uiProp,
19
+ class: className,
20
+ ...rest
21
+ } = Astro.props as AlertProps
22
+
23
+ const isIcon = !!(icon || Astro.slots.has("icon"))
24
+
25
+ const classes = resolveClasses(alert, {
26
+ variant,
27
+ color,
28
+ hasIcon: isIcon,
29
+ }, useUi("alert", uiProp), className)
30
+ ---
31
+ <div
32
+ class={classes.root}
33
+ data-slot="root"
34
+ role="alert"
35
+ {...rest}
36
+ >
37
+ <slot name="icon">
38
+ {icon && (
39
+ <span class={classes.icon} data-slot="icon">
40
+ <span class={cx(icon)} aria-hidden="true" />
41
+ </span>
42
+ )}
43
+ </slot>
44
+
45
+ <div class={classes.content} data-slot="content">
46
+ {title && (
47
+ <h5 class={classes.title} data-slot="title">
48
+ {title}
49
+ </h5>
50
+ )}
51
+ <slot name="title" />
52
+
53
+ {description && (
54
+ <div class={classes.description} data-slot="description">
55
+ {description}
56
+ </div>
57
+ )}
58
+ <slot />
59
+ </div>
60
+ </div>
@@ -0,0 +1,86 @@
1
+ ---
2
+ import { scv } from "css-variants"
3
+ import { useUi, resolveClasses } from "../../utils"
4
+ import type { AlertDialogProps } from "../../types"
5
+ import alertDialogTheme from "../../themes/alertDialog.theme"
6
+ import RLAButton from "./RLAButton.astro"
7
+
8
+ const alertDialog = scv(alertDialogTheme)
9
+
10
+ const {
11
+ variant = "default",
12
+ title,
13
+ description,
14
+ actionLabel = "Confirm",
15
+ cancelLabel = "Cancel",
16
+ triggerLabel,
17
+ ui: uiProp,
18
+ class: className,
19
+ ...rest
20
+ } = Astro.props as AlertDialogProps
21
+
22
+ const classes = resolveClasses(alertDialog, {
23
+ variant
24
+ }, useUi("alertDialog", uiProp), className)
25
+
26
+ const dialogId = `rla-alert-dialog-${Math.random().toString(36).substring(2, 9)}`
27
+ ---
28
+ <div class={classes.root} data-slot="alert-dialog-container">
29
+ {triggerLabel ? (
30
+ <RLAButton
31
+ class={classes.trigger}
32
+ data-slot="trigger"
33
+ type="button"
34
+ onclick={`document.getElementById('${dialogId}').showModal()`}
35
+ >
36
+ {triggerLabel}
37
+ </RLAButton>
38
+ ) : (
39
+ <span onclick={`document.getElementById('${dialogId}').showModal()`}>
40
+ <slot name="trigger" />
41
+ </span>
42
+ )}
43
+
44
+ <dialog
45
+ id={dialogId}
46
+ class={classes.content}
47
+ data-slot="content"
48
+ data-lock-scroll="true"
49
+ data-close-on-escape="false"
50
+ data-close-on-click-outside="false"
51
+ {...rest}
52
+ >
53
+ <div class={classes.header} data-slot="header">
54
+ {title && <h3 class={classes.title} data-slot="title">{title}</h3>}
55
+ <slot name="title" />
56
+
57
+ {description && <p class={classes.description} data-slot="description">{description}</p>}
58
+ <slot name="description" />
59
+ </div>
60
+
61
+ <slot />
62
+
63
+ <div class={classes.footer} data-slot="footer">
64
+ <slot name="footer">
65
+ <RLAButton
66
+ class={classes.cancelButton}
67
+ data-slot="cancel"
68
+ variant="outline"
69
+ type="button"
70
+ onclick={`document.getElementById('${dialogId}').close()`}
71
+ >
72
+ {cancelLabel}
73
+ </RLAButton>
74
+ <RLAButton
75
+ class={classes.actionButton}
76
+ data-slot="action"
77
+ color={variant === "destructive" ? "error" : "primary"}
78
+ type="button"
79
+ onclick={`document.getElementById('${dialogId}').close()`}
80
+ >
81
+ {actionLabel}
82
+ </RLAButton>
83
+ </slot>
84
+ </div>
85
+ </dialog>
86
+ </div>
@@ -0,0 +1,23 @@
1
+ ---
2
+ import { scv } from "css-variants"
3
+ import { useUi, resolveClasses } from "../../utils"
4
+ import type { AspectRatioProps } from "../../types"
5
+ import aspectRatioTheme from "../../themes/aspectRatio.theme"
6
+
7
+ const aspectRatio = scv(aspectRatioTheme)
8
+
9
+ const {
10
+ ratio = 1,
11
+ ui: uiProp,
12
+ class: className,
13
+ ...rest
14
+ } = Astro.props as AspectRatioProps
15
+
16
+ const classes = resolveClasses(aspectRatio, {}, useUi("aspectRatio", uiProp), className)
17
+ const paddingBottom = `${(1 / ratio) * 100}%`
18
+ ---
19
+ <div class={classes.root} data-slot="root" style={{ paddingBottom }} {...rest}>
20
+ <div class={classes.content} data-slot="content">
21
+ <slot />
22
+ </div>
23
+ </div>
@@ -0,0 +1,56 @@
1
+ ---
2
+ import { scv, cx } from "css-variants"
3
+ import { useUi, resolveClasses } from "../../utils"
4
+ import type { AvatarProps } from "../../types"
5
+ import avatarThemeDefault from "../../themes/avatar.theme"
6
+
7
+ const avatar = scv(avatarThemeDefault)
8
+
9
+ const {
10
+ src,
11
+ alt = "",
12
+ fallback,
13
+ size = "md",
14
+ badge = false,
15
+ badgeColor = "success",
16
+ ui: uiProp,
17
+ class: className,
18
+ ...rest
19
+ } = Astro.props as AvatarProps
20
+
21
+ const classes = resolveClasses(avatar, {
22
+ size,
23
+ hasBadge: !!badge
24
+ }, useUi("avatar", uiProp), className)
25
+ ---
26
+ <span
27
+ class={classes.root}
28
+ data-slot="root"
29
+ {...rest}
30
+ >
31
+ <slot>
32
+ {src ? (
33
+ <img
34
+ src={src}
35
+ alt={alt}
36
+ class={classes.image}
37
+ data-slot="image"
38
+ />
39
+ ) : (
40
+ <span class={classes.fallback} data-slot="fallback">
41
+ <slot name="fallback">
42
+ {fallback}
43
+ </slot>
44
+ </span>
45
+ )}
46
+ </slot>
47
+
48
+ <slot name="badge">
49
+ {badge && (
50
+ <span
51
+ class={cx(classes.badge, badgeColor && `bg-${badgeColor}-500`)}
52
+ data-slot="badge"
53
+ />
54
+ )}
55
+ </slot>
56
+ </span>
@@ -0,0 +1,19 @@
1
+ ---
2
+ import { scv } from "css-variants"
3
+ import { useUi, resolveClasses } from "../../utils"
4
+ import type { AvatarGroupProps } from "../../types"
5
+ import avatarGroupTheme from "../../themes/avatarGroup.theme"
6
+
7
+ const avatarGroup = scv(avatarGroupTheme)
8
+
9
+ const {
10
+ ui: uiProp,
11
+ class: className,
12
+ ...rest
13
+ } = Astro.props as AvatarGroupProps
14
+
15
+ const classes = resolveClasses(avatarGroup, {}, useUi("avatarGroup", uiProp), className)
16
+ ---
17
+ <div class={classes.root} data-slot="avatar-group" {...rest}>
18
+ <slot />
19
+ </div>
@@ -0,0 +1,31 @@
1
+ ---
2
+ import { scv } from "css-variants"
3
+ import { useUi, resolveClasses } from "../../utils"
4
+ import type { BadgeProps } from "../../types"
5
+ import badgeThemeDefault, { createBadgeTheme } from "../../themes/badge.theme"
6
+ import { getUIConfig } from "virtual:rimelight-ui"
7
+
8
+ const uiConfig = getUIConfig()
9
+ const badgeTheme = uiConfig.colors ? createBadgeTheme(uiConfig.colors) : badgeThemeDefault
10
+ const badge = scv(badgeTheme)
11
+
12
+ const {
13
+ variant = "solid",
14
+ color = "primary",
15
+ size = "md",
16
+ shape = "pill",
17
+ ui: uiProp,
18
+ class: className,
19
+ ...rest
20
+ } = Astro.props as BadgeProps
21
+
22
+ const classes = resolveClasses(badge, {
23
+ variant,
24
+ color,
25
+ size,
26
+ shape
27
+ }, useUi("badge", uiProp), className)
28
+ ---
29
+ <span class={classes.root} data-slot="root" {...rest}>
30
+ <slot />
31
+ </span>
@@ -0,0 +1,44 @@
1
+ ---
2
+ import { scv } from "css-variants"
3
+ import { useUi, resolveClasses } from "../../utils"
4
+ import type { BreadcrumbProps } from "../../types"
5
+ import breadcrumbTheme from "../../themes/breadcrumb.theme"
6
+
7
+ const breadcrumb = scv(breadcrumbTheme)
8
+
9
+ const {
10
+ items,
11
+ separatorIcon = "i-lucide-chevron-right",
12
+ ui: uiProp,
13
+ class: className,
14
+ ...rest
15
+ } = Astro.props as BreadcrumbProps
16
+
17
+ const classes = resolveClasses(breadcrumb, {}, useUi("breadcrumb", uiProp), className)
18
+ ---
19
+ <nav class={classes.root} data-slot="breadcrumb-root" {...rest}>
20
+ <ol class={classes.list} data-slot="list">
21
+ {items ? (
22
+ items.map((item, idx) => (
23
+ <li class={classes.item} data-slot="item">
24
+ {item.href ? (
25
+ <a href={item.href} class={classes.link} data-slot="link">
26
+ {item.label}
27
+ </a>
28
+ ) : (
29
+ <span class={classes.page} data-slot="page">
30
+ {item.label}
31
+ </span>
32
+ )}
33
+ {idx < items.length - 1 && (
34
+ <span class={classes.separator} data-slot="separator" aria-hidden="true">
35
+ <span class={separatorIcon} />
36
+ </span>
37
+ )}
38
+ </li>
39
+ ))
40
+ ) : (
41
+ <slot />
42
+ )}
43
+ </ol>
44
+ </nav>
@@ -0,0 +1,74 @@
1
+ ---
2
+ import { scv } from "css-variants"
3
+ import { useUi, resolveClasses } from "../../utils"
4
+ import type { CardProps } from "../../types"
5
+ import cardTheme from "../../themes/card.theme"
6
+
7
+ const card = scv(cardTheme)
8
+
9
+ const {
10
+ title,
11
+ description,
12
+ mediaSrc,
13
+ href,
14
+ overlay = false,
15
+ ui: uiProp,
16
+ class: className,
17
+ ...rest
18
+ } = Astro.props as CardProps
19
+
20
+ const classes = resolveClasses(card, {
21
+ interactive: !!href,
22
+ overlay
23
+ }, useUi("card", uiProp), className)
24
+
25
+ const Tag = href ? "a" : "div"
26
+ ---
27
+ <Tag
28
+ class={classes.root}
29
+ data-slot="card-root"
30
+ {...(Tag === "a" ? { href } : {})}
31
+ {...rest}
32
+ >
33
+ <slot name="media">
34
+ {mediaSrc && (
35
+ <div class={classes.media} data-slot="media">
36
+ <img src={mediaSrc} alt={title || ""} class="h-full w-full object-cover" />
37
+ </div>
38
+ )}
39
+ </slot>
40
+
41
+ {overlay ? (
42
+ <div class={classes.overlay} data-slot="overlay">
43
+ {title && <h3 class={classes.title} data-slot="title">{title}</h3>}
44
+ {description && <p class={classes.description} data-slot="description">{description}</p>}
45
+ <slot />
46
+ </div>
47
+ ) : (
48
+ <>
49
+ <slot name="header">
50
+ {(title || description || Astro.slots.has("title") || Astro.slots.has("description")) && (
51
+ <div class={classes.header} data-slot="header">
52
+ {title && <h3 class={classes.title} data-slot="title">{title}</h3>}
53
+ <slot name="title" />
54
+
55
+ {description && <p class={classes.description} data-slot="description">{description}</p>}
56
+ <slot name="description" />
57
+ </div>
58
+ )}
59
+ </slot>
60
+
61
+ {Astro.slots.has("default") && (
62
+ <div class={classes.content} data-slot="content">
63
+ <slot />
64
+ </div>
65
+ )}
66
+
67
+ {Astro.slots.has("footer") && (
68
+ <div class={classes.footer} data-slot="footer">
69
+ <slot name="footer" />
70
+ </div>
71
+ )}
72
+ </>
73
+ )}
74
+ </Tag>
@@ -0,0 +1,71 @@
1
+ ---
2
+ import { scv } from "css-variants"
3
+ import { useUi, resolveClasses } from "../../utils"
4
+ import type { CarouselProps } from "../../types"
5
+ import carouselTheme from "../../themes/carousel.theme"
6
+
7
+ const carousel = scv(carouselTheme)
8
+
9
+ const {
10
+ slides,
11
+ autoplay = false,
12
+ ui: uiProp,
13
+ class: className,
14
+ ...rest
15
+ } = Astro.props as CarouselProps
16
+
17
+ const classes = resolveClasses(carousel, {}, useUi("carousel", uiProp), className)
18
+ const carouselId = `rla-carousel-${Math.random().toString(36).substring(2, 9)}`
19
+ ---
20
+ <div id={carouselId} class={classes.root} data-slot="carousel-root" data-autoplay={autoplay ? "true" : "false"} {...rest}>
21
+ <div class={classes.viewport} data-slot="viewport">
22
+ <div class={classes.container} data-slot="container">
23
+ {slides ? (
24
+ slides.map((slide) => (
25
+ <div class={classes.slide} data-slot="slide">
26
+ {slide.imageSrc && <img src={slide.imageSrc} alt={slide.title || ""} class="w-full aspect-video object-cover rounded-lg" />}
27
+ {(slide.title || slide.description) && (
28
+ <div class="absolute bottom-4 left-4 right-4 p-4 rounded-lg bg-black/60 backdrop-blur-sm text-white">
29
+ {slide.title && <h4 class="text-sm font-semibold">{slide.title}</h4>}
30
+ {slide.description && <p class="text-xs opacity-95 mt-1">{slide.description}</p>}
31
+ </div>
32
+ )}
33
+ </div>
34
+ ))
35
+ ) : (
36
+ <slot />
37
+ )}
38
+ </div>
39
+ </div>
40
+
41
+ <button class={classes.buttonPrev} data-slot="prev-button" type="button" aria-label="Previous slide">
42
+ <span class="i-lucide-chevron-left size-5" aria-hidden="true" />
43
+ </button>
44
+ <button class={classes.buttonNext} data-slot="next-button" type="button" aria-label="Next slide">
45
+ <span class="i-lucide-chevron-right size-5" aria-hidden="true" />
46
+ </button>
47
+ </div>
48
+
49
+ <script>
50
+ import EmblaCarousel from "embla-carousel"
51
+
52
+ document.querySelectorAll('[data-slot="carousel-root"]').forEach((carouselEl) => {
53
+ const viewport = carouselEl.querySelector('[data-slot="viewport"]') as HTMLElement
54
+ const prevBtn = carouselEl.querySelector('[data-slot="prev-button"]') as HTMLButtonElement
55
+ const nextBtn = carouselEl.querySelector('[data-slot="next-button"]') as HTMLButtonElement
56
+
57
+ if (!viewport) return
58
+
59
+ const embla = EmblaCarousel(viewport, { loop: true })
60
+
61
+ if (prevBtn) prevBtn.addEventListener("click", () => embla.scrollPrev())
62
+ if (nextBtn) nextBtn.addEventListener("click", () => embla.scrollNext())
63
+
64
+ const autoplay = carouselEl.getAttribute("data-autoplay") === "true"
65
+ if (autoplay) {
66
+ setInterval(() => {
67
+ if (embla) embla.scrollNext()
68
+ }, 4000)
69
+ }
70
+ })
71
+ </script>
@@ -0,0 +1,215 @@
1
+ ---
2
+ import { scv } from "css-variants"
3
+ import { useUi, resolveClasses } from "../../utils"
4
+ import type { ChartProps, ChartDataPoint } from "../../types"
5
+ import chartThemeDefault, { createChartTheme } from "../../themes/chart.theme"
6
+
7
+ const chartTheme = chartThemeDefault
8
+ const chart = scv(chartTheme)
9
+
10
+ const {
11
+ type = "bar",
12
+ data = [],
13
+ title,
14
+ height = 280,
15
+ color = "primary",
16
+ ui: uiProp,
17
+ class: className,
18
+ ...rest
19
+ } = Astro.props as ChartProps
20
+
21
+ const classes = resolveClasses(chart, {}, useUi("chart", uiProp), className)
22
+
23
+ // Compute derived values on the server so we don't do heavy JS on the client
24
+ const max = data.length > 0 ? Math.max(...data.map(d => d.value)) : 1
25
+ const total = data.reduce((sum, d) => sum + d.value, 0)
26
+
27
+ // Palette for chart segments/bars when no per-item color given
28
+ const palette = [
29
+ "hsl(221 83% 53%)", // blue
30
+ "hsl(262 83% 58%)", // violet
31
+ "hsl(142 76% 36%)", // green
32
+ "hsl(32 95% 44%)", // amber
33
+ "hsl(0 84% 60%)", // red
34
+ "hsl(187 85% 43%)", // cyan
35
+ ]
36
+
37
+ const getColor = (item: ChartDataPoint, index: number) =>
38
+ item.color ?? palette[index % palette.length]
39
+
40
+ // Build pie/donut SVG path segments
41
+ const PIE_R = 80
42
+ const PIE_CX = 100
43
+ const PIE_CY = 100
44
+ const DONUT_INNER_R = 45
45
+
46
+ const polarToCartesian = (cx: number, cy: number, r: number, angleDeg: number) => {
47
+ const rad = (angleDeg - 90) * (Math.PI / 180)
48
+ return { x: cx + r * Math.cos(rad), y: cy + r * Math.sin(rad) }
49
+ }
50
+
51
+ const describeArc = (cx: number, cy: number, r: number, start: number, end: number) => {
52
+ const s = polarToCartesian(cx, cy, r, start)
53
+ const e = polarToCartesian(cx, cy, r, end)
54
+ const largeArc = end - start > 180 ? 1 : 0
55
+ return `M ${s.x} ${s.y} A ${r} ${r} 0 ${largeArc} 1 ${e.x} ${e.y}`
56
+ }
57
+
58
+ let pieCursor = 0
59
+ const pieSlices = data.map((item, i) => {
60
+ const angle = (item.value / (total || 1)) * 360
61
+ const start = pieCursor
62
+ const end = pieCursor + angle
63
+ pieCursor = end
64
+ return { item, i, start, end }
65
+ })
66
+
67
+ // Build line chart SVG points
68
+ const LINE_W = 400
69
+ const LINE_H = 160
70
+ const LINE_PAD = 32
71
+ const linePoints = data.map((d, i) => {
72
+ const x = data.length < 2
73
+ ? LINE_PAD
74
+ : LINE_PAD + (i / (data.length - 1)) * (LINE_W - LINE_PAD * 2)
75
+ const y = LINE_H - LINE_PAD - ((d.value / (max || 1)) * (LINE_H - LINE_PAD * 2))
76
+ return { x, y, d, i }
77
+ })
78
+ const linePath = linePoints.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x} ${p.y}`).join(" ")
79
+ const areaPath = linePoints.length > 0
80
+ ? linePath +
81
+ ` L ${linePoints[linePoints.length - 1]!.x} ${LINE_H - LINE_PAD}` +
82
+ ` L ${linePoints[0]!.x} ${LINE_H - LINE_PAD} Z`
83
+ : ""
84
+ ---
85
+
86
+ <div class={classes.root} data-slot="root" style={`min-height:${height}px`} {...rest}>
87
+ {title && <div class={classes.title} data-slot="title">{title}</div>}
88
+
89
+ {/* Bar Chart */}
90
+ {type === "bar" && (
91
+ <div class={classes.container} data-slot="container" style={`height:${height - 60}px;gap:6px;align-items:flex-end`}>
92
+ {data.map((item, i) => {
93
+ const pct = max > 0 ? (item.value / max) * 100 : 0
94
+ return (
95
+ <div class="flex flex-col items-center gap-1 flex-1 group">
96
+ <span class="text-[10px] text-muted-foreground font-mono opacity-0 group-hover:opacity-100 transition-opacity">
97
+ {item.value}
98
+ </span>
99
+ <div
100
+ class={classes.bar}
101
+ style={`height:${pct}%;background:${getColor(item, i)};min-height:4px;width:100%;`}
102
+ title={`${item.label}: ${item.value}`}
103
+ />
104
+ <span class={classes.axis}>{item.label}</span>
105
+ </div>
106
+ )
107
+ })}
108
+ </div>
109
+ )}
110
+
111
+ {/* Line / Area Chart */}
112
+ {(type === "line" || type === "area") && (
113
+ <div class="relative w-full" style={`height:${height - 60}px`}>
114
+ <svg
115
+ class={classes.svg}
116
+ viewBox={`0 0 ${LINE_W} ${LINE_H}`}
117
+ preserveAspectRatio="none"
118
+ aria-label={title ?? "Line chart"}
119
+ >
120
+ {/* Grid lines */}
121
+ {[0, 0.25, 0.5, 0.75, 1].map(t => {
122
+ const y = LINE_PAD + t * (LINE_H - LINE_PAD * 2)
123
+ return <line x1={LINE_PAD} y1={y} x2={LINE_W - LINE_PAD} y2={y} stroke="currentColor" stroke-opacity="0.08" />
124
+ })}
125
+
126
+ {/* Area fill */}
127
+ {type === "area" && (
128
+ <path d={areaPath} fill={palette[0]!} fill-opacity="0.15" />
129
+ )}
130
+
131
+ {/* Line */}
132
+ <path d={linePath} class={classes.line} stroke={palette[0]!} stroke-width="2.5" />
133
+
134
+ {/* Dots */}
135
+ {linePoints.map(p => (
136
+ <circle
137
+ cx={p.x}
138
+ cy={p.y}
139
+ r="4"
140
+ fill={getColor(p.d, p.i)}
141
+ stroke="white"
142
+ stroke-width="1.5"
143
+ >
144
+ <title>{p.d.label}: {p.d.value}</title>
145
+ </circle>
146
+ ))}
147
+
148
+ {/* X axis labels */}
149
+ {linePoints.map(p => (
150
+ <text x={p.x} y={LINE_H - 4} text-anchor="middle" class={classes.axis}>{p.d.label}</text>
151
+ ))}
152
+ </svg>
153
+ </div>
154
+ )}
155
+
156
+ {/* Pie / Donut Chart */}
157
+ {(type === "pie" || type === "donut") && (
158
+ <div class="flex items-center gap-6 justify-center" style={`height:${height - 60}px`}>
159
+ <svg
160
+ viewBox="0 0 200 200"
161
+ class="h-full w-auto max-h-48"
162
+ aria-label={title ?? "Pie chart"}
163
+ >
164
+ {pieSlices.map(({ item, i, start, end }) => {
165
+ const isFullCircle = Math.abs(end - start - 360) < 0.01
166
+ const d = isFullCircle
167
+ ? `M ${PIE_CX} ${PIE_CY - PIE_R} A ${PIE_R} ${PIE_R} 0 1 1 ${PIE_CX - 0.001} ${PIE_CY - PIE_R} Z`
168
+ : describeArc(PIE_CX, PIE_CY, PIE_R, start, end) +
169
+ ` L ${PIE_CX} ${PIE_CY} Z`
170
+ return (
171
+ <path
172
+ d={d}
173
+ fill={getColor(item, i)}
174
+ class="transition-opacity hover:opacity-85 cursor-pointer"
175
+ >
176
+ <title>{item.label}: {item.value}</title>
177
+ </path>
178
+ )
179
+ })}
180
+ {type === "donut" && (
181
+ <circle cx={PIE_CX} cy={PIE_CY} r={DONUT_INNER_R} fill="var(--color-card, white)" />
182
+ )}
183
+ </svg>
184
+
185
+ {/* Legend */}
186
+ <div class="flex flex-col gap-1.5">
187
+ {data.map((item, i) => (
188
+ <div class="flex items-center gap-2 text-xs text-muted-foreground">
189
+ <span
190
+ class="size-3 rounded-sm shrink-0"
191
+ style={`background:${getColor(item, i)}`}
192
+ />
193
+ <span>{item.label}</span>
194
+ <span class="ml-1 font-mono text-foreground">{item.value}</span>
195
+ </div>
196
+ ))}
197
+ </div>
198
+ </div>
199
+ )}
200
+
201
+ {/* Custom slot for fully custom chart content */}
202
+ <slot />
203
+
204
+ {/* Legend row for bar/line */}
205
+ {(type === "bar" || type === "line" || type === "area") && data.length > 0 && (
206
+ <div class={classes.legend} data-slot="legend">
207
+ {data.map((item, i) => (
208
+ <div class="flex items-center gap-1.5">
209
+ <span class="size-2.5 rounded-full shrink-0" style={`background:${getColor(item, i)}`} />
210
+ <span>{item.label}</span>
211
+ </div>
212
+ ))}
213
+ </div>
214
+ )}
215
+ </div>