@rimelight/ui 0.0.39 → 0.0.41

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rimelight/ui",
3
- "version": "0.0.39",
3
+ "version": "0.0.41",
4
4
  "private": false,
5
5
  "description": "Rimelight Entertainment UI Library.",
6
6
  "keywords": [
@@ -28,6 +28,7 @@ const {
28
28
  square = false,
29
29
  rounded = false,
30
30
  block = false,
31
+ justify = "center",
31
32
  active = false,
32
33
  theme,
33
34
  activeColor,
@@ -60,6 +61,7 @@ const classes = resolveClasses(button, {
60
61
  color: (active && activeColor ? activeColor : color) as "neutral",
61
62
  size,
62
63
  block,
64
+ justify,
63
65
  square: square || (!label && !Astro.slots.has("default")),
64
66
  rounded,
65
67
  leading: isLeading,
@@ -11,7 +11,7 @@ export const createButtonTheme = (colors: readonly string[] = colorConfig.colors
11
11
  slots: ["root", "label", "leadingIcon", "leadingAvatar", "leadingAvatarSize", "trailingIcon"],
12
12
  base: {
13
13
  root: [
14
- "group inline-flex items-center justify-center font-medium ws-nowrap no-underline disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75",
14
+ "group inline-flex items-center font-medium ws-nowrap no-underline disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75",
15
15
  "transition-colors outline-none",
16
16
  "aria-invalid:border-error-500 aria-invalid:ring-error-500/20"
17
17
  ],
@@ -63,8 +63,16 @@ export const createButtonTheme = (colors: readonly string[] = colorConfig.colors
63
63
  trailingIcon: "size-7"
64
64
  }
65
65
  },
66
+ justify: {
67
+ start: { root: "justify-start" },
68
+ center: { root: "justify-center" },
69
+ end: { root: "justify-end" },
70
+ between: { root: "justify-between" },
71
+ around: { root: "justify-around" },
72
+ evenly: { root: "justify-evenly" }
73
+ },
66
74
  block: {
67
- true: { root: "w-full justify-center", trailingIcon: "ms-auto" }
75
+ true: { root: "w-full", trailingIcon: "ms-auto" }
68
76
  },
69
77
  square: {
70
78
  true: {},
@@ -218,7 +226,8 @@ export const createButtonTheme = (colors: readonly string[] = colorConfig.colors
218
226
  defaultVariants: {
219
227
  variant: "solid",
220
228
  color: "primary",
221
- size: "md"
229
+ size: "md",
230
+ justify: "center"
222
231
  }
223
232
  })
224
233
  }
@@ -46,6 +46,10 @@ export interface ButtonProps extends LinkProps {
46
46
  * Makes the button stretch to full width
47
47
  */
48
48
  block?: boolean
49
+ /**
50
+ * Horizontal alignment of content inside the button flex container
51
+ */
52
+ justify?: "start" | "center" | "end" | "between" | "around" | "evenly"
49
53
  /**
50
54
  * Applies active state styling
51
55
  */
@@ -21,7 +21,7 @@ export const createCheckboxTheme = (colors: readonly string[] = colorConfig.colo
21
21
  color,
22
22
  {
23
23
  root: "rl-color-" + color,
24
- box: `peer-checked:bg-[var(--rl-bg)] peer-checked:border-[var(--rl-border)] focus-within:ring-[var(--rl-ring)]/50`
24
+ box: `peer-checked:bg-[var(--rl-bg)] peer-checked:border-[var(--rl-border)] focus-within:ring-[var(--rl-ring)]/50 peer-checked:[&_[data-slot=indicator]]:scale-100`
25
25
  }
26
26
  ])
27
27
  ),
@@ -8,9 +8,9 @@ export const createHeaderTheme = () =>
8
8
  content: "w-full",
9
9
  container:
10
10
  "flex flex-row items-center justify-between gap-4xs h-16 md:h-18 px-xs sm:px-md md:px-xl lg:px-xl",
11
- left: "flex-1 min-w-0 flex items-center justify-start truncate",
11
+ left: "flex-1 min-w-0 flex items-center justify-start",
12
12
  center: "flex-none flex items-center justify-center mx-4",
13
- right: "flex-1 min-w-0 flex items-center justify-end truncate"
13
+ right: "flex-1 min-w-0 flex items-center justify-end"
14
14
  },
15
15
  variants: {
16
16
  contain: {
@@ -14,7 +14,7 @@ export const createNavigationMenuTheme = () =>
14
14
  "absolute left-0 top-full z-50 mt-1.5 w-56 rounded-md border bg-popover p-1 text-popover-foreground shadow-md origin-top-left",
15
15
  viewportWrapper: "absolute left-0 top-full flex justify-center w-full",
16
16
  viewport:
17
- "relative mt-1.5 h-auto w-full origin-[top_center] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90"
17
+ "relative mt-1.5 h-auto w-full origin-top overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90"
18
18
  },
19
19
  variants: {
20
20
  theme: {
@@ -1,130 +1,173 @@
1
- ---
2
- import { scv } from "css-variants"
1
+ ---
2
+ import { scv } from "css-variants"
3
3
  import { useUi } from "../../utils/useUi"
4
- import { resolveClasses } from "../../utils/resolveClasses"
5
- import type { PopoverProps } from "./popover"
4
+ import { resolveClasses } from "../../utils/resolveClasses"
5
+ import type { PopoverProps } from "./popover"
6
6
  import { resolveTheme } from "../../utils/resolveTheme"
7
7
  import popoverThemeDefault, { createPopoverTheme } from "./popover.theme"
8
- const popoverTheme = resolveTheme(popoverThemeDefault, createPopoverTheme)
9
-
10
-
11
- const popover = scv(popoverTheme)
12
-
13
- const {
14
- triggerLabel,
15
- placement = "bottom",
16
- ui: uiProp,
17
- class: className,
18
- ...rest
19
- } = Astro.props as PopoverProps
20
-
21
- const classes = resolveClasses(popover, {
22
- placement
23
- }, useUi("popover", uiProp), className)
24
- ---
25
- <div class={classes.root} data-slot="popover-container" data-placement={placement} {...rest}>
26
- <div class={`${classes.trigger} popover-trigger`} data-slot="trigger">
27
- <slot name="trigger">
28
- {triggerLabel && <button type="button" class="cursor-pointer">{triggerLabel}</button>}
29
- </slot>
30
- </div>
31
-
32
- <div class={`${classes.content} popover-content hidden`} data-slot="content">
33
- <slot />
34
- </div>
35
- </div>
36
-
37
- <script>
38
- function initPopovers() {
39
- document.querySelectorAll('[data-slot="popover-container"]').forEach(container => {
40
- const el = container as any;
41
- if (el.dataset && el.dataset.popoverInitialized) return;
42
- if (el.dataset) el.dataset.popoverInitialized = "true";
43
-
44
- const trigger = container.querySelector('.popover-trigger');
45
- const content = container.querySelector('.popover-content') as HTMLElement;
46
-
47
- if (!trigger || !content) return;
48
-
49
- trigger.addEventListener('click', (e) => {
50
- e.stopPropagation();
51
- const isOpen = !content.classList.contains('hidden');
52
-
53
- document.querySelectorAll('.popover-content').forEach(otherContent => {
54
- const other = otherContent as HTMLElement;
55
- if (other !== content && !other.classList.contains('hidden')) {
56
- other.classList.add('hidden');
57
- other.style.left = '';
58
- other.style.right = '';
59
- other.style.transform = '';
60
- other.dispatchEvent(new CustomEvent('popover-toggle', { detail: { open: false } }));
61
- }
62
- });
63
-
64
- if (isOpen) {
65
- content.classList.add('hidden');
66
- content.style.left = '';
67
- content.style.right = '';
68
- content.style.transform = '';
69
- content.dispatchEvent(new CustomEvent('popover-toggle', { detail: { open: false } }));
70
- } else {
71
- content.classList.remove('hidden');
72
-
73
- // Reset styles to measure original computed bounds
74
- content.style.left = '';
75
- content.style.right = '';
76
- content.style.transform = '';
77
-
78
- const rect = content.getBoundingClientRect();
79
- const viewportWidth = window.innerWidth;
80
- const placement = container.getAttribute('data-placement') || 'bottom';
81
-
82
- let shift = 0;
83
- const safetyMargin = 24;
84
- if (rect.right > viewportWidth - safetyMargin) {
85
- shift = (viewportWidth - safetyMargin) - rect.right;
86
- } else if (rect.left < safetyMargin) {
87
- shift = safetyMargin - rect.left;
88
- }
89
-
90
- if (shift !== 0) {
91
- const isCentered = (placement === 'top' || placement === 'bottom') &&
92
- !content.classList.contains('right-0') &&
93
- !content.classList.contains('left-0');
94
- if (isCentered) {
95
- content.style.transform = `translateX(calc(-50% + ${shift}px))`;
96
- } else {
97
- content.style.transform = `translateX(${shift}px)`;
98
- }
99
- }
100
-
101
- content.dispatchEvent(new CustomEvent('popover-toggle', { detail: { open: true } }));
102
- }
103
- });
104
-
105
- content.addEventListener('click', (e) => {
106
- e.stopPropagation();
107
- });
108
- });
109
- }
110
-
111
- const win = window as any;
112
- if (!win.hasGlobalPopoverListener) {
113
- win.hasGlobalPopoverListener = true;
114
- document.addEventListener('click', () => {
115
- document.querySelectorAll('.popover-content').forEach(content => {
116
- const c = content as HTMLElement;
117
- if (!c.classList.contains('hidden')) {
118
- c.classList.add('hidden');
119
- c.style.left = '';
120
- c.style.right = '';
121
- c.style.transform = '';
122
- c.dispatchEvent(new CustomEvent('popover-toggle', { detail: { open: false } }));
123
- }
124
- });
125
- });
126
- }
127
-
128
- document.addEventListener('astro:page-load', initPopovers);
129
- initPopovers();
130
- </script>
8
+
9
+ const popoverTheme = resolveTheme(popoverThemeDefault, createPopoverTheme)
10
+ const popover = scv(popoverTheme)
11
+
12
+ const {
13
+ triggerLabel,
14
+ placement = "bottom",
15
+ mode = "both",
16
+ ui: uiProp,
17
+ class: className,
18
+ ...rest
19
+ } = Astro.props as PopoverProps
20
+
21
+ const classes = resolveClasses(popover, {
22
+ placement
23
+ }, useUi("popover", uiProp), className)
24
+ ---
25
+ <div class={`group/popover ${classes.root}`} data-slot="popover-container" data-placement={placement} data-mode={mode} {...rest}>
26
+ <div class={`${classes.trigger} popover-trigger`} data-slot="trigger">
27
+ <slot name="trigger">
28
+ {triggerLabel && <button type="button" class="cursor-pointer">{triggerLabel}</button>}
29
+ </slot>
30
+ </div>
31
+
32
+ <div class={`${classes.content} popover-content hidden`} data-slot="content">
33
+ <slot />
34
+ </div>
35
+ </div>
36
+
37
+ <script>
38
+ function openPopover(container: Element, content: HTMLElement) {
39
+ document.querySelectorAll('.popover-content').forEach(other => {
40
+ if (other !== content && !other.classList.contains('hidden')) {
41
+ other.classList.add('hidden');
42
+ (other as HTMLElement).style.transform = '';
43
+ other.dispatchEvent(new CustomEvent('popover-toggle', { detail: { open: false } }));
44
+ }
45
+ });
46
+
47
+ content.classList.remove('hidden');
48
+ content.style.transform = '';
49
+
50
+ const rect = content.getBoundingClientRect();
51
+ const viewportWidth = window.innerWidth;
52
+ let shift = 0;
53
+ const safetyMargin = 16;
54
+ if (rect.right > viewportWidth - safetyMargin) {
55
+ shift = (viewportWidth - safetyMargin) - rect.right;
56
+ } else if (rect.left < safetyMargin) {
57
+ shift = safetyMargin - rect.left;
58
+ }
59
+
60
+ const placement = container.getAttribute('data-placement') || 'bottom';
61
+ const isCentered = (placement === 'top' || placement === 'bottom') &&
62
+ !content.classList.contains('right-0') &&
63
+ !content.classList.contains('left-0');
64
+
65
+ if (shift !== 0) {
66
+ if (isCentered) {
67
+ content.style.transform = `translateX(calc(-50% + ${shift}px))`;
68
+ } else {
69
+ content.style.transform = `translateX(${shift}px)`;
70
+ }
71
+ }
72
+
73
+ content.dispatchEvent(new CustomEvent('popover-toggle', { detail: { open: true } }));
74
+ }
75
+
76
+ function closePopover(content: HTMLElement) {
77
+ if (!content.classList.contains('hidden')) {
78
+ content.classList.add('hidden');
79
+ content.style.transform = '';
80
+ content.dispatchEvent(new CustomEvent('popover-toggle', { detail: { open: false } }));
81
+ }
82
+ }
83
+
84
+ function setupPopoverListeners() {
85
+ const win = window as any;
86
+ if (win.hasGlobalPopoverListeners) return;
87
+ win.hasGlobalPopoverListeners = true;
88
+
89
+ let hoverTimeout: any = null;
90
+
91
+ document.addEventListener('mouseover', (e) => {
92
+ const target = e.target as HTMLElement | null;
93
+ if (!target) return;
94
+
95
+ const container = target.closest('[data-slot="popover-container"]') as HTMLElement | null;
96
+ if (container) {
97
+ const mode = container.getAttribute('data-mode') || 'both';
98
+ if (mode === 'hover' || mode === 'both') {
99
+ if (hoverTimeout) clearTimeout(hoverTimeout);
100
+ const content = container.querySelector('.popover-content') as HTMLElement | null;
101
+ if (content && content.classList.contains('hidden')) {
102
+ openPopover(container, content);
103
+ }
104
+ }
105
+ }
106
+ });
107
+
108
+ document.addEventListener('mouseout', (e) => {
109
+ const target = e.target as HTMLElement | null;
110
+ if (!target) return;
111
+
112
+ const container = target.closest('[data-slot="popover-container"]') as HTMLElement | null;
113
+ if (container) {
114
+ const mode = container.getAttribute('data-mode') || 'both';
115
+ if (mode === 'hover' || mode === 'both') {
116
+ const related = e.relatedTarget as HTMLElement | null;
117
+ if (related && container.contains(related)) return;
118
+
119
+ const content = container.querySelector('.popover-content') as HTMLElement | null;
120
+ if (content && !content.classList.contains('hidden')) {
121
+ hoverTimeout = setTimeout(() => {
122
+ closePopover(content);
123
+ }, 150);
124
+ }
125
+ }
126
+ }
127
+ });
128
+
129
+ document.addEventListener('click', (e) => {
130
+ const target = e.target as HTMLElement | null;
131
+ if (!target) return;
132
+
133
+ const trigger = target.closest('.popover-trigger') as HTMLElement | null;
134
+ if (trigger) {
135
+ e.stopPropagation();
136
+ const container = trigger.closest('[data-slot="popover-container"]');
137
+ if (!container) return;
138
+
139
+ const content = container.querySelector('.popover-content') as HTMLElement | null;
140
+ if (!content) return;
141
+
142
+ const isOpen = !content.classList.contains('hidden');
143
+ if (isOpen) {
144
+ closePopover(content);
145
+ } else {
146
+ openPopover(container, content);
147
+ }
148
+ return;
149
+ }
150
+
151
+ if (target.closest('.popover-content')) {
152
+ const isActionLink = target.closest('a') || target.closest('button[data-close-popover]');
153
+ if (isActionLink) {
154
+ document.querySelectorAll('.popover-content').forEach(other => {
155
+ closePopover(other as HTMLElement);
156
+ });
157
+ }
158
+ return;
159
+ }
160
+
161
+ document.querySelectorAll('.popover-content').forEach(other => {
162
+ closePopover(other as HTMLElement);
163
+ });
164
+ });
165
+ }
166
+
167
+ if (document.readyState === 'loading') {
168
+ document.addEventListener('DOMContentLoaded', setupPopoverListeners);
169
+ } else {
170
+ setupPopoverListeners();
171
+ }
172
+ document.addEventListener('astro:page-load', setupPopoverListeners);
173
+ </script>
@@ -8,13 +8,15 @@ export const createPopoverTheme = (_colors: readonly string[] = colorConfig.colo
8
8
  root: "relative inline-block",
9
9
  trigger: "",
10
10
  content: [
11
- "absolute z-50 w-72 rounded-xl border border-border bg-background p-4 text-foreground shadow-lg outline-none"
11
+ "absolute z-50 w-72 rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-4 text-neutral-900 dark:text-neutral-100 shadow-xl outline-none"
12
12
  ]
13
13
  },
14
14
  variants: {
15
15
  placement: {
16
16
  top: { content: "bottom-full left-1/2 -translate-x-1/2 mb-2" },
17
17
  bottom: { content: "top-full left-1/2 -translate-x-1/2 mt-2" },
18
+ "bottom-end": { content: "top-full right-0 left-auto mt-2" },
19
+ "bottom-start": { content: "top-full left-0 right-auto mt-2" },
18
20
  left: { content: "right-full top-1/2 -translate-y-1/2 mr-2" },
19
21
  right: { content: "left-full top-1/2 -translate-y-1/2 ml-2" }
20
22
  }
@@ -10,7 +10,11 @@ export interface PopoverProps {
10
10
  /**
11
11
  * The placement of the popover relative to trigger.
12
12
  */
13
- placement?: "top" | "bottom" | "left" | "right"
13
+ placement?: "top" | "bottom" | "bottom-end" | "bottom-start" | "left" | "right"
14
+ /**
15
+ * Trigger mode: 'click', 'hover', or 'both'.
16
+ */
17
+ mode?: "click" | "hover" | "both"
14
18
  /**
15
19
  * Slot-specific CSS class overrides.
16
20
  */
@@ -35,7 +35,7 @@ export const createTableOfContentsTheme = () =>
35
35
  trailingIcon:
36
36
  "size-5 transform transition-transform duration-200 shrink-0 group-data-[state=open]:rotate-180 lg:hidden",
37
37
  content:
38
- "relative data-[state=open]:animate-[collapsible-down_200ms_ease-out] data-[state=closed]:animate-[collapsible-up_200ms_ease-out] overflow-hidden focus:outline-none",
38
+ "relative data-[state=open]:animate-fade-in data-[state=closed]:animate-fade-out overflow-hidden focus:outline-none",
39
39
  list: "min-w-0",
40
40
  listWithChildren: "ms-3",
41
41
  item: "min-w-0",
@@ -2,11 +2,22 @@ import { defineTheme } from "../../utils/defineTheme"
2
2
 
3
3
  export const createTabsTheme = () =>
4
4
  defineTheme({
5
- slots: ["root", "list", "indicator", "trigger", "leadingIcon", "leadingAvatar", "label", "trailingBadge", "content"],
5
+ slots: [
6
+ "root",
7
+ "list",
8
+ "indicator",
9
+ "trigger",
10
+ "leadingIcon",
11
+ "leadingAvatar",
12
+ "label",
13
+ "trailingBadge",
14
+ "content"
15
+ ],
6
16
  base: {
7
17
  root: "flex gap-2 w-full",
8
18
  list: "relative flex p-1 group items-center min-w-0",
9
- indicator: "absolute transition-[translate,width,height,top,left] duration-200 ease-out pointer-events-none",
19
+ indicator:
20
+ "absolute transition-[translate,width,height,top,left] duration-200 ease-out pointer-events-none",
10
21
  trigger:
11
22
  "group relative inline-flex items-center justify-center min-w-0 whitespace-nowrap font-medium transition-colors cursor-pointer disabled:cursor-not-allowed disabled:opacity-75 focus-visible:outline-none data-[state=inactive]:text-muted-foreground hover:data-[state=inactive]:text-foreground",
12
23
  leadingIcon: "shrink-0",
@@ -77,4 +88,3 @@ export const createTabsTheme = () =>
77
88
  })
78
89
 
79
90
  export default createTabsTheme()
80
-
@@ -28,17 +28,20 @@ export interface TabsProps<T extends TabItem = TabItem> {
28
28
  defaultValue?: string
29
29
  /**
30
30
  * Tab variant.
31
- * @defaultValue 'pill'
31
+ *
32
+ * @default "pill"
32
33
  */
33
34
  variant?: "pill" | "underline" | "link" | "solid" | "pills"
34
35
  /**
35
36
  * Layout orientation.
36
- * @defaultValue 'horizontal'
37
+ *
38
+ * @default "horizontal"
37
39
  */
38
40
  orientation?: "horizontal" | "vertical"
39
41
  /**
40
42
  * Size scale.
41
- * @defaultValue 'md'
43
+ *
44
+ * @default "md"
42
45
  */
43
46
  size?: "xs" | "sm" | "md" | "lg" | "xl"
44
47
  /**
@@ -47,23 +50,27 @@ export interface TabsProps<T extends TabItem = TabItem> {
47
50
  color?: ThemeColor
48
51
  /**
49
52
  * Whether to render content panels automatically. Set to false for tab bar only.
50
- * @defaultValue true
53
+ *
54
+ * @default true
51
55
  */
52
56
  content?: boolean
53
57
  /**
54
- * Unmount hidden tab panels from DOM. Defaults to true.
55
- * If false, panel is hidden via CSS/attribute.
56
- * @defaultValue true
58
+ * Unmount hidden tab panels from DOM. Defaults to true. If false, panel is hidden via
59
+ * CSS/attribute.
60
+ *
61
+ * @default true
57
62
  */
58
63
  unmountOnHide?: boolean
59
64
  /**
60
65
  * Property key for value on item object.
61
- * @defaultValue 'value'
66
+ *
67
+ * @default "value"
62
68
  */
63
69
  valueKey?: string
64
70
  /**
65
71
  * Property key for label on item object.
66
- * @defaultValue 'label'
72
+ *
73
+ * @default "label"
67
74
  */
68
75
  labelKey?: string
69
76
  /**
@@ -76,4 +83,3 @@ export interface TabsProps<T extends TabItem = TabItem> {
76
83
  class?: any
77
84
  [key: string]: unknown
78
85
  }
79
-
@@ -0,0 +1,88 @@
1
+ ---
2
+ ---
3
+
4
+ <div id="rla-toaster-container" class="fixed bottom-5 right-5 z-[200] flex flex-col gap-2 max-w-sm w-full pointer-events-none"></div>
5
+
6
+ <script>
7
+ import { toasts, removeToast, addToast } from "../../stores/toast"
8
+
9
+ function checkPendingToasts() {
10
+ const stored = sessionStorage.getItem("pending_toast")
11
+ if (stored) {
12
+ sessionStorage.removeItem("pending_toast")
13
+ try {
14
+ const parsed = JSON.parse(stored)
15
+ if (parsed && parsed.title) {
16
+ addToast(parsed)
17
+ }
18
+ } catch (err) {}
19
+ }
20
+
21
+ const url = new URL(window.location.href)
22
+ let toastTitle = url.searchParams.get("toast_title")
23
+ let toastDesc = url.searchParams.get("toast_desc") ?? undefined
24
+ let toastVariant = (url.searchParams.get("toast_variant") as any) || "success"
25
+
26
+ if (!toastTitle) {
27
+ if (url.searchParams.has("signed_in")) {
28
+ toastTitle = "Signed in successfully"
29
+ toastDesc = "Welcome back!"
30
+ } else if (url.searchParams.has("new")) {
31
+ toastTitle = "Account created successfully"
32
+ toastDesc = "Please sign in to continue."
33
+ }
34
+ }
35
+
36
+ if (toastTitle) {
37
+ url.searchParams.delete("toast_title")
38
+ url.searchParams.delete("toast_desc")
39
+ url.searchParams.delete("toast_variant")
40
+ url.searchParams.delete("signed_in")
41
+ url.searchParams.delete("new")
42
+ const cleanUrl = url.pathname + (url.search ? url.search : "")
43
+ window.history.replaceState({}, "", cleanUrl)
44
+
45
+ const payload: any = {
46
+ title: toastTitle,
47
+ variant: toastVariant
48
+ }
49
+ if (toastDesc) payload.description = toastDesc
50
+
51
+ addToast(payload)
52
+ }
53
+ }
54
+
55
+ checkPendingToasts()
56
+ document.addEventListener("astro:page-load", checkPendingToasts)
57
+
58
+ const container = document.getElementById("rla-toaster-container")
59
+
60
+ toasts.subscribe((list) => {
61
+ if (!container) return
62
+ container.innerHTML = ""
63
+ list.forEach((t) => {
64
+ const el = document.createElement("div")
65
+ const isError = t.variant === "error"
66
+ const bgClass = isError
67
+ ? "bg-error-900 text-error-100 border-error-700"
68
+ : "bg-neutral-900 text-white border-neutral-800 dark:bg-neutral-800"
69
+ const iconSvg = isError
70
+ ? `<svg class="size-4 shrink-0 text-error-400 mt-0.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6M9 9l6 6"/></svg>`
71
+ : `<svg class="size-4 shrink-0 text-success-400 mt-0.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>`
72
+
73
+ el.className = `pointer-events-auto flex items-start gap-3 p-4 rounded-lg border shadow-xl transition-all duration-300 ${bgClass}`
74
+ el.innerHTML = `
75
+ ${iconSvg}
76
+ <div class="flex flex-col gap-0.5 flex-1 min-w-0">
77
+ <div class="font-semibold text-sm">${t.title}</div>
78
+ ${t.description ? `<div class="text-xs opacity-90">${t.description}</div>` : ""}
79
+ </div>
80
+ <button type="button" class="text-neutral-400 hover:text-white p-0.5 rounded focus:outline-none" aria-label="Close">
81
+ <svg class="size-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6 6 18M6 6l12 12"/></svg>
82
+ </button>
83
+ `
84
+ el.querySelector("button")?.addEventListener("click", () => removeToast(t.id))
85
+ container.appendChild(el)
86
+ })
87
+ })
88
+ </script>
@@ -34,7 +34,10 @@ export const DEFAULT_ICONS = {
34
34
  sun: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false" width="100%" height="100%"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/></svg>`,
35
35
  uploadCloud: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false" width="100%" height="100%"><path d="M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242M12 12v9M9 15l3-3 3 3"/></svg>`,
36
36
  user: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false" width="100%" height="100%"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>`,
37
- star: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false" width="100%" height="100%"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>`
37
+ star: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false" width="100%" height="100%"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>`,
38
+ lock: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false" width="100%" height="100%"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>`,
39
+ eye: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false" width="100%" height="100%"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0z"/><circle cx="12" cy="12" r="3"/></svg>`,
40
+ eyeOff: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false" width="100%" height="100%"><path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"/><path d="M14.084 14.158a3 3 0 0 1-4.242-4.242"/><path d="M17.479 17.499A10.75 10.75 0 0 1 2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.225-4.22"/><line x1="2" x2="22" y1="2" y2="22"/></svg>`
38
41
  } as const
39
42
 
40
43
  export type RimelightIcons = Partial<Record<keyof typeof DEFAULT_ICONS, string>> &
@@ -202,14 +202,14 @@ export default definePreset((options?: RimelightPresetOptions) => {
202
202
  }
203
203
  ],
204
204
  [
205
- /^(text)-(default|inverted|muted|dimmed|toned|highlighted)(?:\/(\d+))?$/,
205
+ /^(text)-(default|muted|dimmed|toned|highlighted|elevated|accented|inverted)(?:\/(\d+))?$/,
206
206
  ([, , name, opacity]) =>
207
207
  opacity
208
208
  ? { color: `color-mix(in srgb, var(--text-color-${name}) ${opacity}%, transparent)` }
209
209
  : { color: `var(--text-color-${name})` }
210
210
  ],
211
211
  [
212
- /^(bg)-(default|inverted|muted|elevated|accented)(?:\/(\d+))?$/,
212
+ /^(bg)-(default|muted|elevated|accented|dimmed|toned|highlighted)(?:\/(\d+))?$/,
213
213
  ([, , name, opacity]) =>
214
214
  opacity
215
215
  ? {
@@ -218,7 +218,20 @@ export default definePreset((options?: RimelightPresetOptions) => {
218
218
  : { "background-color": `var(--background-color-${name})` }
219
219
  ],
220
220
  [
221
- /^(ring)-(default|inverted|muted|accented)(?:\/(\d+))?$/,
221
+ /^bg-inverted(?:\/(\d+))?$/,
222
+ ([, opacity]) =>
223
+ opacity
224
+ ? {
225
+ "background-color": `color-mix(in srgb, var(--background-color-inverted) ${opacity}%, transparent)`,
226
+ "color": "var(--text-color-inverted)"
227
+ }
228
+ : {
229
+ "background-color": "var(--background-color-inverted)",
230
+ "color": "var(--text-color-inverted)"
231
+ }
232
+ ],
233
+ [
234
+ /^(ring)-(default|muted|elevated|accented|dimmed|toned|highlighted|inverted)(?:\/(\d+))?$/,
222
235
  ([, , name, opacity]) =>
223
236
  opacity
224
237
  ? {
@@ -227,7 +240,7 @@ export default definePreset((options?: RimelightPresetOptions) => {
227
240
  : { "--un-ring-color": `var(--ring-color-${name})` }
228
241
  ],
229
242
  [
230
- /^(border)-(default|inverted|muted|accented)(?:\/(\d+))?$/,
243
+ /^(border)-(default|muted|elevated|accented|dimmed|toned|highlighted|inverted)(?:\/(\d+))?$/,
231
244
  ([, , name, opacity]) =>
232
245
  opacity
233
246
  ? {
@@ -345,10 +358,10 @@ export default definePreset((options?: RimelightPresetOptions) => {
345
358
  animation: {
346
359
  keyframes: {
347
360
  "accordion-down":
348
- "{ from { height: 0; } to { height: var(--starwind-accordion-content-height); } }",
361
+ "from { height: 0; } to { height: var(--starwind-accordion-content-height); }",
349
362
  "accordion-up":
350
- "{ from { height: var(--starwind-accordion-content-height); } to { height: 0; } }",
351
- "marquee": "{ from { transform: translateX(0); } to { transform: translateX(-100%); } }"
363
+ "from { height: var(--starwind-accordion-content-height); } to { height: 0; }",
364
+ "marquee": "from { transform: translateX(0); } to { transform: translateX(-100%); }"
352
365
  },
353
366
  durations: {
354
367
  "accordion-down": "0.2s",
@@ -460,24 +473,41 @@ export default definePreset((options?: RimelightPresetOptions) => {
460
473
 
461
474
  /* Text / background / border / ring semantic tokens using responsive neutral variables */
462
475
  --text-color-default: var(--color-neutral-700);
463
- --text-color-muted: var(--color-neutral-500);
464
- --text-color-dimmed: var(--color-neutral-400);
465
- --text-color-toned: var(--color-neutral-600);
476
+ --text-color-muted: var(--color-neutral-600);
477
+ --text-color-dimmed: var(--color-neutral-500);
478
+ --text-color-toned: var(--color-neutral-800);
466
479
  --text-color-highlighted: var(--color-neutral-950);
467
- --text-color-inverted: light-dark(#ffffff, var(--color-neutral-50));
480
+ --text-color-elevated: var(--color-neutral-800);
481
+ --text-color-accented: var(--color-neutral-900);
482
+ --text-color-inverted: var(--color-neutral-50);
483
+
468
484
  --background-color-default: light-dark(#ffffff, var(--color-neutral-50));
469
485
  --background-color-muted: var(--color-neutral-100);
470
486
  --background-color-elevated: var(--color-neutral-200);
471
487
  --background-color-accented: var(--color-neutral-300);
488
+ --background-color-dimmed: var(--color-neutral-400);
489
+ --background-color-toned: var(--color-neutral-800);
490
+ --background-color-highlighted: var(--color-neutral-900);
472
491
  --background-color-inverted: var(--color-neutral-950);
492
+
473
493
  --border-color-default: var(--color-neutral-200);
474
494
  --border-color-muted: var(--color-neutral-100);
495
+ --border-color-elevated: var(--color-neutral-200);
475
496
  --border-color-accented: var(--color-neutral-300);
497
+ --border-color-dimmed: var(--color-neutral-400);
498
+ --border-color-toned: var(--color-neutral-800);
499
+ --border-color-highlighted: var(--color-neutral-900);
476
500
  --border-color-inverted: var(--color-neutral-950);
501
+
477
502
  --ring-color-default: var(--color-neutral-200);
478
503
  --ring-color-muted: var(--color-neutral-100);
504
+ --ring-color-elevated: var(--color-neutral-200);
479
505
  --ring-color-accented: var(--color-neutral-300);
506
+ --ring-color-dimmed: var(--color-neutral-400);
507
+ --ring-color-toned: var(--color-neutral-800);
508
+ --ring-color-highlighted: var(--color-neutral-900);
480
509
  --ring-color-inverted: var(--color-neutral-950);
510
+
481
511
  --divide-color-default: var(--color-neutral-200);
482
512
  --divide-color-inverted: var(--color-neutral-950);
483
513
 
@@ -0,0 +1,24 @@
1
+ import { atom } from "nanostores"
2
+
3
+ export interface ToastItem {
4
+ id: string
5
+ title: string
6
+ description?: string
7
+ variant?: "success" | "error" | "warning" | "info" | "default"
8
+ }
9
+
10
+ export const toasts = atom<ToastItem[]>([])
11
+
12
+ export function addToast(toast: Omit<ToastItem, "id">) {
13
+ const id = Math.random().toString(36).slice(2, 9)
14
+ const item: ToastItem = { id, ...toast }
15
+ toasts.set([...toasts.get(), item])
16
+
17
+ setTimeout(() => {
18
+ removeToast(id)
19
+ }, 4500)
20
+ }
21
+
22
+ export function removeToast(id: string) {
23
+ toasts.set(toasts.get().filter((t) => t.id !== id))
24
+ }