@rimelight/ui 0.0.38 → 0.0.39

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.38",
3
+ "version": "0.0.39",
4
4
  "private": false,
5
5
  "description": "Rimelight Entertainment UI Library.",
6
6
  "keywords": [
@@ -54,11 +54,11 @@
54
54
  "@astrojs/solid-js": "7.0.1",
55
55
  "@astrojs/ts-plugin": "1.10.10",
56
56
  "@astrojs/vue": "7.0.1",
57
- "astro": "7.1.0",
57
+ "astro": "7.1.3",
58
58
  "solid-js": "1.9.14",
59
59
  "typescript": "6.0.3",
60
60
  "unocss": "66.7.5",
61
- "vue": "3.5.39"
61
+ "vue": "3.5.40"
62
62
  },
63
63
  "peerDependencies": {
64
64
  "@astrojs/solid-js": ">=7.0.1",
@@ -4,7 +4,14 @@ export const createIconTheme = () =>
4
4
  defineTheme({
5
5
  slots: ["root"],
6
6
  base: {
7
- root: ["inline-block", "align-middle"]
7
+ root: [
8
+ "shrink-0",
9
+ "inline-flex",
10
+ "items-center",
11
+ "justify-center",
12
+ "align-middle",
13
+ "leading-none"
14
+ ]
8
15
  },
9
16
  variants: {
10
17
  size: {
@@ -1,8 +1,6 @@
1
1
  ---
2
- import { getImage } from "astro:assets"
2
+ import { Image, Picture, getImage } from "astro:assets"
3
3
  import type { ImageMetadata } from "astro"
4
- import Image from "astro/components/Image.astro"
5
- import Picture from "astro/components/Picture.astro"
6
4
  import RLAIcon from "../icon/RLAIcon.astro"
7
5
  import { scv } from "css-variants"
8
6
  import { useUi } from "../../utils/useUi"
@@ -42,6 +42,7 @@ const classes = resolveClasses(pageSection, {
42
42
  }, useUi("pageSection", uiProp), className);
43
43
 
44
44
  const hasDefaultSlot = Astro.slots.has("default");
45
+ const hasContentSlot = Astro.slots.has("content");
45
46
 
46
47
  const Tag = as;
47
48
  const TitleTag = variant === "hero" ? "h1" : "h2";
@@ -152,7 +153,14 @@ const TitleTag = variant === "hero" ? "h1" : "h2";
152
153
  ) : orientation === "horizontal" ? (
153
154
  <div class="hidden lg:block" />
154
155
  ) : null}
156
+
157
+ {hasContentSlot && (
158
+ <div class="col-span-full w-full">
159
+ <slot name="content" />
160
+ </div>
161
+ )}
155
162
  </div>
156
163
 
157
164
  <slot name="bottom" />
158
165
  </Tag>
166
+
@@ -1,72 +1,72 @@
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 { PostProps } from "./post"
6
- import postTheme from "./post.theme"
7
- import type { ImageMetadata } from "astro"
8
- import Image from "astro/components/Image.astro"
9
- import RLABadge from "../badge/RLABadge.astro"
10
-
11
-
12
- const post = scv(postTheme)
13
-
14
- const {
15
- title,
16
- description,
17
- date,
18
- image,
19
- to,
20
- badge,
21
- badgeColor = "primary",
22
- variant = "soft",
23
- orientation = "vertical",
24
- ui: uiProp,
25
- class: className,
26
- ...rest
27
- } = Astro.props as PostProps
28
-
29
- const classes = resolveClasses(post, { variant, orientation }, useUi("post", uiProp), className);
30
-
31
- const Tag = to ? "a" : "div";
32
- const formattedDate = date ? (date instanceof Date ? date.toLocaleDateString() : date) : null;
33
- ---
34
- <Tag class={classes.root} data-slot="root" {...(to ? { href: to } : {})} {...rest}>
35
- <slot name="image" {...{ ui: classes }}>
36
- {image && (
37
- <div class={classes.imageWrapper} data-slot="image-wrapper">
38
- {typeof image === 'string' ? (
39
- <img src={image} alt={title} class={classes.image} data-slot="image" />
40
- ) : (
41
- <Image src={image as ImageMetadata} alt={title || ""} class={classes.image} data-slot="image" />
42
- )}
43
- </div>
44
- )}
45
- </slot>
46
-
47
- <div class={classes.content} data-slot="content">
48
- <slot name="title" {...{ ui: classes }}>
49
- {title && <h3 class={classes.title} data-slot="title">{title}</h3>}
50
- </slot>
51
-
52
- <slot name="description" {...{ ui: classes }}>
53
- {description && <p class={classes.description} data-slot="description">{description}</p>}
54
- </slot>
55
-
56
- <slot name="badge" {...{ ui: classes }}>
57
- {badge && <RLABadge color={badgeColor as any} size="sm" shape="pill" class={classes.badge} data-slot="badge">{badge}</RLABadge>}
58
- </slot>
59
-
60
- <slot name="date" {...{ ui: classes }}>
61
- {formattedDate && <time class={classes.date} data-slot="date">{formattedDate}</time>}
62
- </slot>
63
-
64
- <slot />
65
- </div>
66
-
67
- {Astro.slots.has("footer") && (
68
- <div class={classes.footer} data-slot="footer">
69
- <slot name="footer" {...{ ui: classes }} />
70
- </div>
71
- )}
4
+ import { resolveClasses } from "../../utils/resolveClasses"
5
+ import type { PostProps } from "./post"
6
+ import postTheme from "./post.theme"
7
+ import { Image } from "astro:assets"
8
+ import type { ImageMetadata } from "astro"
9
+ import RLABadge from "../badge/RLABadge.astro"
10
+
11
+
12
+ const post = scv(postTheme)
13
+
14
+ const {
15
+ title,
16
+ description,
17
+ date,
18
+ image,
19
+ to,
20
+ badge,
21
+ badgeColor = "primary",
22
+ variant = "soft",
23
+ orientation = "vertical",
24
+ ui: uiProp,
25
+ class: className,
26
+ ...rest
27
+ } = Astro.props as PostProps
28
+
29
+ const classes = resolveClasses(post, { variant, orientation }, useUi("post", uiProp), className);
30
+
31
+ const Tag = to ? "a" : "div";
32
+ const formattedDate = date ? (date instanceof Date ? date.toLocaleDateString() : date) : null;
33
+ ---
34
+ <Tag class={classes.root} data-slot="root" {...(to ? { href: to } : {})} {...rest}>
35
+ <slot name="image" {...{ ui: classes }}>
36
+ {image && (
37
+ <div class={classes.imageWrapper} data-slot="image-wrapper">
38
+ {typeof image === 'string' ? (
39
+ <img src={image} alt={title} class={classes.image} data-slot="image" />
40
+ ) : (
41
+ <Image src={image as ImageMetadata} alt={title || ""} class={classes.image} data-slot="image" />
42
+ )}
43
+ </div>
44
+ )}
45
+ </slot>
46
+
47
+ <div class={classes.content} data-slot="content">
48
+ <slot name="title" {...{ ui: classes }}>
49
+ {title && <h3 class={classes.title} data-slot="title">{title}</h3>}
50
+ </slot>
51
+
52
+ <slot name="description" {...{ ui: classes }}>
53
+ {description && <p class={classes.description} data-slot="description">{description}</p>}
54
+ </slot>
55
+
56
+ <slot name="badge" {...{ ui: classes }}>
57
+ {badge && <RLABadge color={badgeColor as any} size="sm" shape="pill" class={classes.badge} data-slot="badge">{badge}</RLABadge>}
58
+ </slot>
59
+
60
+ <slot name="date" {...{ ui: classes }}>
61
+ {formattedDate && <time class={classes.date} data-slot="date">{formattedDate}</time>}
62
+ </slot>
63
+
64
+ <slot />
65
+ </div>
66
+
67
+ {Astro.slots.has("footer") && (
68
+ <div class={classes.footer} data-slot="footer">
69
+ <slot name="footer" {...{ ui: classes }} />
70
+ </div>
71
+ )}
72
72
  </Tag>
@@ -2,71 +2,96 @@
2
2
  import { scv } from "css-variants"
3
3
  import { useUi } from "../../utils/useUi"
4
4
  import { resolveClasses } from "../../utils/resolveClasses"
5
- import type { TabsProps } from "./tabs"
5
+ import type { TabsProps, TabItem } from "./tabs"
6
6
  import tabsTheme from "./tabs.theme"
7
-
7
+ import RLABadge from "../badge/RLABadge.astro"
8
8
 
9
9
  const tabs = scv(tabsTheme)
10
10
 
11
11
  const {
12
12
  items = [],
13
13
  value = "",
14
- variant = "solid",
14
+ defaultValue = "",
15
+ variant = "pill",
15
16
  orientation = "horizontal",
16
17
  size = "md",
17
18
  color = "primary",
19
+ content = true,
20
+ unmountOnHide = true,
21
+ valueKey = "value",
22
+ labelKey = "label",
18
23
  ui: uiProp,
19
24
  class: className,
20
25
  ...rest
21
26
  } = Astro.props as TabsProps
22
27
 
28
+ // Normalize variant names for backward compatibility (solid -> pill, pills -> pill, underline -> underline, link -> underline)
29
+ const normalizedVariant = (variant === "solid" || variant === "pills") ? "pill" : (variant === "link" ? "underline" : variant);
30
+
23
31
  const classes = resolveClasses(tabs, {
24
- variant,
32
+ variant: normalizedVariant,
25
33
  orientation,
26
34
  size
27
35
  }, useUi("tabs", uiProp), className);
28
36
 
29
- const initialValue = value || (items.length > 0 ? items[0]?.value : "")
37
+ const initialValue = value || defaultValue || (items.length > 0 ? (items[0][valueKey] ?? items[0].value ?? "0") : "")
30
38
  ---
31
- <rla-tabs {...rest}>
39
+ <rla-tabs {...rest} data-orientation={orientation} data-variant={normalizedVariant}>
32
40
  <div class={classes.root} data-slot="root" data-tabs-container data-value={initialValue}>
33
41
  <div class={classes.list} data-slot="list" role="tablist">
34
- {items.map((item) => {
35
- const active = item.value === initialValue
42
+ <div class={classes.indicator} data-slot="indicator" style="display: none;"></div>
43
+ {items.map((item, index) => {
44
+ const itemVal = item[valueKey] ?? item.value ?? String(index)
45
+ const itemLabel = item[labelKey] ?? item.label
46
+ const active = itemVal === initialValue
36
47
  return (
37
48
  <button
38
49
  type="button"
39
50
  class={classes.trigger}
40
51
  data-slot="trigger"
41
52
  role="tab"
42
- data-value={item.value}
53
+ data-value={itemVal}
43
54
  data-state={active ? "active" : "inactive"}
44
55
  disabled={item.disabled}
45
56
  >
46
- {item.icon && <span class={item.icon + " mr-2"} />}
47
- {item.label}
57
+ {item.icon && <span class={item.icon + " " + classes.leadingIcon} data-slot="leadingIcon" />}
58
+ {itemLabel && <span class={classes.label} data-slot="label">{itemLabel}</span>}
59
+ {item.badge !== undefined && item.badge !== null && (
60
+ <RLABadge
61
+ size="sm"
62
+ variant="subtle"
63
+ color="neutral"
64
+ class={classes.trailingBadge}
65
+ data-slot="trailingBadge"
66
+ >
67
+ {typeof item.badge === "object" ? item.badge.label : item.badge}
68
+ </RLABadge>
69
+ )}
48
70
  </button>
49
71
  )
50
72
  })}
51
73
  </div>
52
- <div class="flex-1">
53
- <slot>
54
- {items.map(item => {
55
- const active = item.value === initialValue
56
- return (
57
- <div
58
- class={classes.content}
59
- data-slot="content"
60
- role="tabpanel"
61
- data-value={item.value}
62
- hidden={active ? undefined : "until-found"}
63
- >
64
- {item.content}
65
- </div>
66
- )
67
- })}
68
- </slot>
69
- </div>
74
+ {content && (
75
+ <div class="flex-1">
76
+ <slot>
77
+ {items.map((item, index) => {
78
+ const itemVal = item[valueKey] ?? item.value ?? String(index)
79
+ const active = itemVal === initialValue
80
+ return (
81
+ <div
82
+ class={classes.content}
83
+ data-slot="content"
84
+ role="tabpanel"
85
+ data-value={itemVal}
86
+ hidden={active ? undefined : "until-found"}
87
+ >
88
+ {item.content}
89
+ </div>
90
+ )
91
+ })}
92
+ </slot>
93
+ </div>
94
+ )}
70
95
  </div>
71
96
  </rla-tabs>
72
97
 
@@ -74,19 +99,59 @@ const initialValue = value || (items.length > 0 ? items[0]?.value : "")
74
99
  if (typeof HTMLElement !== 'undefined') {
75
100
  class RLATabs extends HTMLElement {
76
101
  private tabsCleanup: (() => void) | null = null;
102
+ private resizeObserver: ResizeObserver | null = null;
77
103
 
78
104
  connectedCallback() {
79
105
  this.style.display = 'contents';
80
106
  const container = this.querySelector("[data-tabs-container]") as HTMLElement | null;
81
107
  if (!container) return;
82
108
 
109
+ const list = container.querySelector("[data-slot=list]") as HTMLElement | null;
110
+ const indicator = container.querySelector("[data-slot=indicator]") as HTMLElement | null;
111
+ const orientation = this.dataset.orientation || "horizontal";
112
+ const isVertical = orientation === "vertical";
113
+
114
+ const updateIndicator = (activeTrigger: HTMLElement | null) => {
115
+ if (!indicator || !list || !activeTrigger) {
116
+ if (indicator) indicator.style.display = 'none';
117
+ return;
118
+ }
119
+
120
+ const listRect = list.getBoundingClientRect();
121
+ const triggerRect = activeTrigger.getBoundingClientRect();
122
+
123
+ if (triggerRect.width === 0 && triggerRect.height === 0) {
124
+ indicator.style.display = 'none';
125
+ return;
126
+ }
127
+
128
+ indicator.style.display = 'block';
129
+
130
+ if (isVertical) {
131
+ const top = triggerRect.top - listRect.top + list.scrollTop;
132
+ const height = triggerRect.height;
133
+ indicator.style.transform = `translateY(${top}px)`;
134
+ indicator.style.height = `${height}px`;
135
+ } else {
136
+ const left = triggerRect.left - listRect.left + list.scrollLeft;
137
+ const width = triggerRect.width;
138
+ indicator.style.transform = `translateX(${left}px)`;
139
+ indicator.style.width = `${width}px`;
140
+ }
141
+ };
142
+
83
143
  const containerValue = container.dataset.value;
84
144
  const triggers = container.querySelectorAll("[role=tab]");
85
145
  const panels = container.querySelectorAll("[role=tabpanel]");
86
146
 
147
+ let activeTrigger: HTMLElement | null = null;
148
+
87
149
  triggers.forEach(trigger => {
88
- const val = (trigger as HTMLButtonElement).dataset.value;
89
- trigger.setAttribute("data-state", val === containerValue ? "active" : "inactive");
150
+ const el = trigger as HTMLElement;
151
+ const val = el.dataset.value;
152
+ const isActive = val === containerValue;
153
+ el.setAttribute("data-state", isActive ? "active" : "inactive");
154
+ if (isActive) activeTrigger = el;
90
155
  });
91
156
 
92
157
  panels.forEach(panel => {
@@ -98,6 +163,17 @@ const initialValue = value || (items.length > 0 ? items[0]?.value : "")
98
163
  }
99
164
  });
100
165
 
166
+ // Initialize indicator position
167
+ requestAnimationFrame(() => updateIndicator(activeTrigger));
168
+
169
+ if (window.ResizeObserver && list) {
170
+ this.resizeObserver = new ResizeObserver(() => {
171
+ const currentActive = list.querySelector("[role=tab][data-state=active]") as HTMLElement | null;
172
+ updateIndicator(currentActive);
173
+ });
174
+ this.resizeObserver.observe(list);
175
+ }
176
+
101
177
  const clickCleanups: (() => void)[] = [];
102
178
  const beforeMatchCleanups: (() => void)[] = [];
103
179
 
@@ -117,11 +193,14 @@ const initialValue = value || (items.length > 0 ? items[0]?.value : "")
117
193
  triggers.forEach(triggerNode => {
118
194
  const trigger = triggerNode as HTMLButtonElement;
119
195
  const onClick = () => {
196
+ if (trigger.disabled) return;
120
197
  const targetValue = trigger.dataset.value;
121
198
 
122
199
  triggers.forEach(tNode => {
123
200
  const t = tNode as HTMLButtonElement;
124
- t.setAttribute("data-state", t.dataset.value === targetValue ? "active" : "inactive");
201
+ const isTarget = t.dataset.value === targetValue;
202
+ t.setAttribute("data-state", isTarget ? "active" : "inactive");
203
+ if (isTarget) updateIndicator(t);
125
204
  });
126
205
 
127
206
  panels.forEach(panel => {
@@ -139,6 +218,10 @@ const initialValue = value || (items.length > 0 ? items[0]?.value : "")
139
218
  this.tabsCleanup = () => {
140
219
  clickCleanups.forEach(fn => fn());
141
220
  beforeMatchCleanups.forEach(fn => fn());
221
+ if (this.resizeObserver) {
222
+ this.resizeObserver.disconnect();
223
+ this.resizeObserver = null;
224
+ }
142
225
  };
143
226
  }
144
227
 
@@ -155,3 +238,4 @@ const initialValue = value || (items.length > 0 ? items[0]?.value : "")
155
238
  }
156
239
  }
157
240
  </script>
241
+
@@ -2,51 +2,79 @@ import { defineTheme } from "../../utils/defineTheme"
2
2
 
3
3
  export const createTabsTheme = () =>
4
4
  defineTheme({
5
- slots: ["root", "list", "trigger", "content"],
5
+ slots: ["root", "list", "indicator", "trigger", "leadingIcon", "leadingAvatar", "label", "trailingBadge", "content"],
6
6
  base: {
7
- root: "w-full flex flex-col gap-4",
8
- list: "inline-flex items-center justify-start rounded-md bg-muted p-1 text-muted-foreground",
7
+ root: "flex gap-2 w-full",
8
+ 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",
9
10
  trigger:
10
- "inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:text-foreground cursor-pointer",
11
- content:
12
- "mt-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
11
+ "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
+ leadingIcon: "shrink-0",
13
+ leadingAvatar: "shrink-0",
14
+ label: "truncate",
15
+ trailingBadge: "shrink-0",
16
+ content: "w-full focus-visible:outline-none"
13
17
  },
14
18
  variants: {
15
19
  variant: {
16
- solid: {
17
- list: "bg-muted p-1 rounded-lg",
18
- trigger:
19
- "data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm"
20
+ pill: {
21
+ list: "bg-muted/60 dark:bg-muted/40 rounded-lg p-1",
22
+ trigger: "grow rounded-md data-[state=active]:text-foreground z-10",
23
+ indicator: "bg-background rounded-md shadow-xs top-1 bottom-1 h-[calc(100%-8px)]"
20
24
  },
21
25
  underline: {
22
- list: "bg-transparent border-b border-border rounded-none p-0 gap-4",
23
- trigger:
24
- "rounded-none border-b-2 border-transparent px-1 pb-3 pt-2 data-[state=active]:border-primary data-[state=active]:text-foreground hover:border-muted-foreground/30"
26
+ list: "bg-transparent border-b border-border p-0 gap-2 -mb-px",
27
+ trigger: "rounded-md data-[state=active]:text-primary pb-2 pt-1 px-3 z-10",
28
+ indicator: "bg-primary rounded-full -bottom-px h-0.5 left-0"
25
29
  },
26
- pills: {
27
- list: "bg-transparent p-0 gap-2",
28
- trigger:
29
- "rounded-full px-4 py-1.5 data-[state=active]:bg-primary data-[state=active]:text-white"
30
+ link: {
31
+ list: "bg-transparent border-b border-border p-0 gap-2 -mb-px",
32
+ trigger: "rounded-md data-[state=active]:text-primary pb-2 pt-1 px-3 z-10",
33
+ indicator: "bg-primary rounded-full -bottom-px h-0.5 left-0"
30
34
  }
31
35
  },
32
36
  orientation: {
33
- horizontal: { root: "flex-col", list: "w-full" },
37
+ horizontal: {
38
+ root: "flex-col",
39
+ list: "w-full",
40
+ indicator: "",
41
+ trigger: "justify-center"
42
+ },
34
43
  vertical: {
35
44
  root: "flex-row gap-6",
36
- list: "flex-col items-stretch h-full w-48 border-r border-border rounded-none bg-transparent"
45
+ list: "flex-col items-stretch w-48 border-r border-border p-0",
46
+ trigger: "justify-start text-left"
37
47
  }
38
48
  },
39
49
  size: {
40
- sm: { trigger: "text-xs px-2.5 py-1" },
41
- md: { trigger: "text-sm px-3.5 py-1.5" },
42
- lg: { trigger: "text-base px-4.5 py-2" }
50
+ xs: {
51
+ trigger: "px-2 py-1 text-xs gap-1",
52
+ leadingIcon: "size-3.5"
53
+ },
54
+ sm: {
55
+ trigger: "px-2.5 py-1.5 text-xs gap-1.5",
56
+ leadingIcon: "size-4"
57
+ },
58
+ md: {
59
+ trigger: "px-3 py-1.5 text-sm gap-1.5",
60
+ leadingIcon: "size-4"
61
+ },
62
+ lg: {
63
+ trigger: "px-3.5 py-2 text-sm gap-2",
64
+ leadingIcon: "size-5"
65
+ },
66
+ xl: {
67
+ trigger: "px-4 py-2.5 text-base gap-2",
68
+ leadingIcon: "size-5"
69
+ }
43
70
  }
44
71
  },
45
72
  defaultVariants: {
46
- variant: "solid",
73
+ variant: "pill",
47
74
  orientation: "horizontal",
48
75
  size: "md"
49
76
  }
50
77
  })
51
78
 
52
79
  export default createTabsTheme()
80
+
@@ -1,37 +1,71 @@
1
1
  import type { ThemeColor } from "../../types/component"
2
+
2
3
  export interface TabItem {
3
- label: string
4
- value: string
4
+ label?: string
5
+ value?: string
5
6
  icon?: string
7
+ badge?: string | number | { label?: string | number; color?: string; variant?: string }
6
8
  content?: string
7
9
  disabled?: boolean
10
+ slot?: string
11
+ class?: any
12
+ ui?: any
13
+ [key: string]: any
8
14
  }
9
15
 
10
- export interface TabsProps {
16
+ export interface TabsProps<T extends TabItem = TabItem> {
11
17
  /**
12
18
  * The tab items.
13
19
  */
14
- items?: TabItem[]
20
+ items?: T[]
15
21
  /**
16
22
  * Active tab value.
17
23
  */
18
24
  value?: string
25
+ /**
26
+ * Default active tab value when uncontrolled.
27
+ */
28
+ defaultValue?: string
19
29
  /**
20
30
  * Tab variant.
31
+ * @defaultValue 'pill'
21
32
  */
22
- variant?: "solid" | "underline" | "pills"
33
+ variant?: "pill" | "underline" | "link" | "solid" | "pills"
23
34
  /**
24
35
  * Layout orientation.
36
+ * @defaultValue 'horizontal'
25
37
  */
26
38
  orientation?: "horizontal" | "vertical"
27
39
  /**
28
40
  * Size scale.
41
+ * @defaultValue 'md'
29
42
  */
30
- size?: "sm" | "md" | "lg"
43
+ size?: "xs" | "sm" | "md" | "lg" | "xl"
31
44
  /**
32
45
  * Color theme.
33
46
  */
34
47
  color?: ThemeColor
48
+ /**
49
+ * Whether to render content panels automatically. Set to false for tab bar only.
50
+ * @defaultValue true
51
+ */
52
+ content?: boolean
53
+ /**
54
+ * Unmount hidden tab panels from DOM. Defaults to true.
55
+ * If false, panel is hidden via CSS/attribute.
56
+ * @defaultValue true
57
+ */
58
+ unmountOnHide?: boolean
59
+ /**
60
+ * Property key for value on item object.
61
+ * @defaultValue 'value'
62
+ */
63
+ valueKey?: string
64
+ /**
65
+ * Property key for label on item object.
66
+ * @defaultValue 'label'
67
+ */
68
+ labelKey?: string
35
69
  /**
36
70
  * Slot-specific CSS class overrides.
37
71
  */
@@ -42,3 +76,4 @@ export interface TabsProps {
42
76
  class?: any
43
77
  [key: string]: unknown
44
78
  }
79
+
@@ -1,21 +1,195 @@
1
- ---
2
- import { scv } from "css-variants"
1
+ ---
2
+ import { scv, cx } from "css-variants"
3
3
  import { useUi } from "../../utils/useUi"
4
- import { resolveClasses } from "../../utils/resolveClasses"
5
- import type { TimelineProps } from "./timeline"
6
- import timelineTheme from "./timeline.theme"
7
-
8
-
9
- const timeline = scv(timelineTheme)
10
-
11
- const {
12
- ui: uiProp,
13
- class: className,
14
- ...rest
15
- } = Astro.props as TimelineProps
16
-
17
- const classes = resolveClasses(timeline, {}, useUi("timeline", uiProp), className);
18
- ---
19
- <div class={classes.root} data-slot="root" {...rest}>
20
- <slot />
21
- </div>
4
+ import { resolveClasses } from "../../utils/resolveClasses"
5
+ import type { TimelineProps, TimelineItem } from "./timeline"
6
+ import { resolveTheme } from "../../utils/resolveTheme"
7
+ import timelineThemeDefault, { createTimelineTheme } from "./timeline.theme"
8
+ const timelineTheme = resolveTheme(timelineThemeDefault, createTimelineTheme)
9
+ import RLAAvatar from "../avatar/RLAAvatar.astro"
10
+ import RLAIcon from "../icon/RLAIcon.astro"
11
+
12
+ const timeline = scv(timelineTheme)
13
+
14
+ const {
15
+ as: Component = "div",
16
+ items,
17
+ size = "md",
18
+ color = "primary",
19
+ orientation = "vertical",
20
+ valueKey = "value",
21
+ defaultValue,
22
+ modelValue,
23
+ reverse = false,
24
+ ui: uiProp,
25
+ class: className,
26
+ ...rest
27
+ } = Astro.props as TimelineProps
28
+
29
+ const classes = resolveClasses(
30
+ timeline,
31
+ {
32
+ orientation,
33
+ size,
34
+ color,
35
+ reverse
36
+ },
37
+ useUi("timeline", uiProp),
38
+ className
39
+ )
40
+
41
+ const activeVal = modelValue !== undefined ? modelValue : defaultValue
42
+
43
+ function getItemValue(item: TimelineItem, index: number) {
44
+ return item[valueKey] !== undefined ? item[valueKey] : index
45
+ }
46
+
47
+ function getItemState(item: TimelineItem, index: number): "active" | "completed" | undefined {
48
+ if (activeVal === undefined) return undefined
49
+
50
+ let currentIndex = -1
51
+ if (items && items.length > 0) {
52
+ if (typeof activeVal === "string") {
53
+ currentIndex = items.findIndex((i) => getItemValue(i, items.indexOf(i)) === activeVal)
54
+ } else if (typeof activeVal === "number") {
55
+ if (reverse) {
56
+ currentIndex = items.length - 1 - activeVal
57
+ } else {
58
+ currentIndex = activeVal
59
+ }
60
+ }
61
+ }
62
+
63
+ if (currentIndex === -1) return undefined
64
+ if (index === currentIndex) return "active"
65
+
66
+ if (reverse) {
67
+ return index > currentIndex ? "completed" : undefined
68
+ } else {
69
+ return index < currentIndex ? "completed" : undefined
70
+ }
71
+ }
72
+
73
+ function getSlotName(item: TimelineItem, slotType: string): string {
74
+ return item.slot ? `${item.slot}-${slotType}` : slotType
75
+ }
76
+ ---
77
+
78
+ <Component
79
+ data-orientation={orientation}
80
+ data-slot="root"
81
+ class={classes.root}
82
+ {...rest}
83
+ >
84
+ {items && items.length > 0 ? (
85
+ items.map((item, index) => {
86
+ const state = getItemState(item, index)
87
+ const isLast = index === items.length - 1
88
+
89
+ const itemIndicatorSlot = getSlotName(item, "indicator")
90
+ const itemWrapperSlot = getSlotName(item, "wrapper")
91
+ const itemDateSlot = getSlotName(item, "date")
92
+ const itemTitleSlot = getSlotName(item, "title")
93
+ const itemDescriptionSlot = getSlotName(item, "description")
94
+
95
+ const hasCustomIndicator = Astro.slots.has(itemIndicatorSlot) || (item.slot && Astro.slots.has("indicator"))
96
+ const hasCustomWrapper = Astro.slots.has(itemWrapperSlot) || (item.slot && Astro.slots.has("wrapper"))
97
+ const hasCustomDate = Astro.slots.has(itemDateSlot) || (item.slot && Astro.slots.has("date"))
98
+ const hasCustomTitle = Astro.slots.has(itemTitleSlot) || (item.slot && Astro.slots.has("title"))
99
+ const hasCustomDescription = Astro.slots.has(itemDescriptionSlot) || (item.slot && Astro.slots.has("description"))
100
+
101
+ return (
102
+ <div
103
+ data-slot="item"
104
+ class={cx(classes.item, item.ui?.item, item.class)}
105
+ data-state={state}
106
+ >
107
+ <div
108
+ data-slot="container"
109
+ class={cx(classes.container, item.ui?.container)}
110
+ >
111
+ <RLAAvatar
112
+ size={size as any}
113
+ src={item.avatar?.src}
114
+ alt={item.avatar?.alt}
115
+ {...(typeof item.avatar === "object" ? item.avatar : {})}
116
+ data-slot="indicator"
117
+ class={cx(classes.indicator, item.ui?.indicator)}
118
+ ui={{ icon: "text-highlighted group-data-[state=completed]:text-inverted group-data-[state=active]:text-inverted", fallback: "text-highlighted group-data-[state=completed]:text-inverted group-data-[state=active]:text-inverted" }}
119
+ >
120
+ {hasCustomIndicator ? (
121
+ <slot name={itemIndicatorSlot} item={item}>
122
+ <slot name="indicator" item={item} />
123
+ </slot>
124
+ ) : item.icon ? (
125
+ <RLAIcon name={item.icon} class="text-inherit" />
126
+ ) : null}
127
+ </RLAAvatar>
128
+
129
+ {!isLast && (
130
+ <div
131
+ data-slot="separator"
132
+ class={cx(classes.separator, item.ui?.separator)}
133
+ data-orientation={orientation}
134
+ />
135
+ )}
136
+ </div>
137
+
138
+ <div
139
+ data-slot="wrapper"
140
+ class={cx(classes.wrapper, item.ui?.wrapper)}
141
+ >
142
+ {hasCustomWrapper ? (
143
+ <slot name={itemWrapperSlot} item={item}>
144
+ <slot name="wrapper" item={item} />
145
+ </slot>
146
+ ) : (
147
+ <>
148
+ {(item.date || hasCustomDate) && (
149
+ <div
150
+ data-slot="date"
151
+ class={cx(classes.date, item.ui?.date)}
152
+ >
153
+ <slot name={itemDateSlot} item={item}>
154
+ <slot name="date" item={item}>
155
+ {item.date}
156
+ </slot>
157
+ </slot>
158
+ </div>
159
+ )}
160
+
161
+ {(item.title || hasCustomTitle) && (
162
+ <div
163
+ data-slot="title"
164
+ class={cx(classes.title, item.ui?.title)}
165
+ >
166
+ <slot name={itemTitleSlot} item={item}>
167
+ <slot name="title" item={item}>
168
+ {item.title}
169
+ </slot>
170
+ </slot>
171
+ </div>
172
+ )}
173
+
174
+ {(item.description || hasCustomDescription) && (
175
+ <div
176
+ data-slot="description"
177
+ class={cx(classes.description, item.ui?.description)}
178
+ >
179
+ <slot name={itemDescriptionSlot} item={item}>
180
+ <slot name="description" item={item}>
181
+ {item.description}
182
+ </slot>
183
+ </slot>
184
+ </div>
185
+ )}
186
+ </>
187
+ )}
188
+ </div>
189
+ </div>
190
+ )
191
+ })
192
+ ) : (
193
+ <slot />
194
+ )}
195
+ </Component>
@@ -2,17 +2,315 @@ import { defineTheme } from "../../utils/defineTheme"
2
2
 
3
3
  export const createTimelineTheme = () =>
4
4
  defineTheme({
5
- slots: ["root", "node", "dot", "dotInner", "header", "content"],
5
+ slots: [
6
+ "root",
7
+ "item",
8
+ "container",
9
+ "indicator",
10
+ "separator",
11
+ "wrapper",
12
+ "date",
13
+ "title",
14
+ "description"
15
+ ],
6
16
  base: {
7
- root: "relative flex flex-col pl-6 border-l-2 border-gray-800 ml-2 py-2",
8
- node: "relative timeline-node",
9
- dot: "absolute -left-[33px] top-[2px] flex h-4 w-4 items-center justify-center rounded-full bg-gray-950 ring-2 ring-gray-800",
10
- dotInner: "h-2 w-2 rounded-full bg-primary-500",
11
- header: "flex items-center justify-between gap-4 mb-3",
12
- content: "space-y-2 text-sm text-gray-400"
17
+ root: "flex gap-1.5",
18
+ item: "group relative flex flex-1 gap-3",
19
+ container: "relative flex items-center gap-1.5",
20
+ indicator:
21
+ "group-data-[state=completed]:text-inverted group-data-[state=active]:text-inverted text-highlighted",
22
+ separator: "flex-1 rounded-full bg-elevated",
23
+ wrapper: "w-full",
24
+ date: "text-dimmed text-xs/5",
25
+ title: "font-medium text-highlighted text-sm",
26
+ description: "text-muted text-wrap text-sm"
13
27
  },
14
- variants: {},
15
- defaultVariants: {}
28
+ variants: {
29
+ orientation: {
30
+ horizontal: {
31
+ root: "flex-row w-full",
32
+ item: "flex-col",
33
+ separator: "h-0.5"
34
+ },
35
+ vertical: {
36
+ root: "flex-col",
37
+ container: "flex-col",
38
+ separator: "w-0.5"
39
+ }
40
+ },
41
+ color: {
42
+ primary: {
43
+ indicator: "group-data-[state=completed]:bg-primary group-data-[state=active]:bg-primary"
44
+ },
45
+ secondary: {
46
+ indicator:
47
+ "group-data-[state=completed]:bg-secondary group-data-[state=active]:bg-secondary"
48
+ },
49
+ success: {
50
+ indicator: "group-data-[state=completed]:bg-success group-data-[state=active]:bg-success"
51
+ },
52
+ info: {
53
+ indicator: "group-data-[state=completed]:bg-info group-data-[state=active]:bg-info"
54
+ },
55
+ warning: {
56
+ indicator: "group-data-[state=completed]:bg-warning group-data-[state=active]:bg-warning"
57
+ },
58
+ error: {
59
+ indicator: "group-data-[state=completed]:bg-error group-data-[state=active]:bg-error"
60
+ },
61
+ neutral: {
62
+ indicator:
63
+ "group-data-[state=completed]:bg-inverted group-data-[state=active]:bg-inverted"
64
+ }
65
+ },
66
+ size: {
67
+ "3xs": {},
68
+ "2xs": {},
69
+ "xs": {},
70
+ "sm": {},
71
+ "md": {},
72
+ "lg": {},
73
+ "xl": {},
74
+ "2xl": {},
75
+ "3xl": {}
76
+ },
77
+ reverse: {
78
+ true: {},
79
+ false: {}
80
+ }
81
+ },
82
+ compoundVariants: [
83
+ {
84
+ color: "primary",
85
+ reverse: false,
86
+ class: {
87
+ separator: "group-data-[state=completed]:bg-primary"
88
+ }
89
+ },
90
+ {
91
+ color: "secondary",
92
+ reverse: false,
93
+ class: {
94
+ separator: "group-data-[state=completed]:bg-secondary"
95
+ }
96
+ },
97
+ {
98
+ color: "success",
99
+ reverse: false,
100
+ class: {
101
+ separator: "group-data-[state=completed]:bg-success"
102
+ }
103
+ },
104
+ {
105
+ color: "info",
106
+ reverse: false,
107
+ class: {
108
+ separator: "group-data-[state=completed]:bg-info"
109
+ }
110
+ },
111
+ {
112
+ color: "warning",
113
+ reverse: false,
114
+ class: {
115
+ separator: "group-data-[state=completed]:bg-warning"
116
+ }
117
+ },
118
+ {
119
+ color: "error",
120
+ reverse: false,
121
+ class: {
122
+ separator: "group-data-[state=completed]:bg-error"
123
+ }
124
+ },
125
+ {
126
+ color: "primary",
127
+ reverse: true,
128
+ class: {
129
+ separator: "group-data-[state=active]:bg-primary group-data-[state=completed]:bg-primary"
130
+ }
131
+ },
132
+ {
133
+ color: "secondary",
134
+ reverse: true,
135
+ class: {
136
+ separator:
137
+ "group-data-[state=active]:bg-secondary group-data-[state=completed]:bg-secondary"
138
+ }
139
+ },
140
+ {
141
+ color: "success",
142
+ reverse: true,
143
+ class: {
144
+ separator: "group-data-[state=active]:bg-success group-data-[state=completed]:bg-success"
145
+ }
146
+ },
147
+ {
148
+ color: "info",
149
+ reverse: true,
150
+ class: {
151
+ separator: "group-data-[state=active]:bg-info group-data-[state=completed]:bg-info"
152
+ }
153
+ },
154
+ {
155
+ color: "warning",
156
+ reverse: true,
157
+ class: {
158
+ separator: "group-data-[state=active]:bg-warning group-data-[state=completed]:bg-warning"
159
+ }
160
+ },
161
+ {
162
+ color: "error",
163
+ reverse: true,
164
+ class: {
165
+ separator: "group-data-[state=active]:bg-error group-data-[state=completed]:bg-error"
166
+ }
167
+ },
168
+ {
169
+ color: "neutral",
170
+ reverse: false,
171
+ class: {
172
+ separator: "group-data-[state=completed]:bg-inverted"
173
+ }
174
+ },
175
+ {
176
+ color: "neutral",
177
+ reverse: true,
178
+ class: {
179
+ separator:
180
+ "group-data-[state=active]:bg-inverted group-data-[state=completed]:bg-inverted"
181
+ }
182
+ },
183
+ {
184
+ orientation: "horizontal",
185
+ size: "3xs",
186
+ class: {
187
+ wrapper: "pe-4.5"
188
+ }
189
+ },
190
+ {
191
+ orientation: "horizontal",
192
+ size: "2xs",
193
+ class: {
194
+ wrapper: "pe-5"
195
+ }
196
+ },
197
+ {
198
+ orientation: "horizontal",
199
+ size: "xs",
200
+ class: {
201
+ wrapper: "pe-5.5"
202
+ }
203
+ },
204
+ {
205
+ orientation: "horizontal",
206
+ size: "sm",
207
+ class: {
208
+ wrapper: "pe-6"
209
+ }
210
+ },
211
+ {
212
+ orientation: "horizontal",
213
+ size: "md",
214
+ class: {
215
+ wrapper: "pe-6.5"
216
+ }
217
+ },
218
+ {
219
+ orientation: "horizontal",
220
+ size: "lg",
221
+ class: {
222
+ wrapper: "pe-7"
223
+ }
224
+ },
225
+ {
226
+ orientation: "horizontal",
227
+ size: "xl",
228
+ class: {
229
+ wrapper: "pe-7.5"
230
+ }
231
+ },
232
+ {
233
+ orientation: "horizontal",
234
+ size: "2xl",
235
+ class: {
236
+ wrapper: "pe-8"
237
+ }
238
+ },
239
+ {
240
+ orientation: "horizontal",
241
+ size: "3xl",
242
+ class: {
243
+ wrapper: "pe-8.5"
244
+ }
245
+ },
246
+ {
247
+ orientation: "vertical",
248
+ size: "3xs",
249
+ class: {
250
+ wrapper: "-mt-0.5 pb-4.5"
251
+ }
252
+ },
253
+ {
254
+ orientation: "vertical",
255
+ size: "2xs",
256
+ class: {
257
+ wrapper: "pb-5"
258
+ }
259
+ },
260
+ {
261
+ orientation: "vertical",
262
+ size: "xs",
263
+ class: {
264
+ wrapper: "mt-0.5 pb-5.5"
265
+ }
266
+ },
267
+ {
268
+ orientation: "vertical",
269
+ size: "sm",
270
+ class: {
271
+ wrapper: "mt-1 pb-6"
272
+ }
273
+ },
274
+ {
275
+ orientation: "vertical",
276
+ size: "md",
277
+ class: {
278
+ wrapper: "mt-1.5 pb-6.5"
279
+ }
280
+ },
281
+ {
282
+ orientation: "vertical",
283
+ size: "lg",
284
+ class: {
285
+ wrapper: "mt-2 pb-7"
286
+ }
287
+ },
288
+ {
289
+ orientation: "vertical",
290
+ size: "xl",
291
+ class: {
292
+ wrapper: "mt-2.5 pb-7.5"
293
+ }
294
+ },
295
+ {
296
+ orientation: "vertical",
297
+ size: "2xl",
298
+ class: {
299
+ wrapper: "mt-3 pb-8"
300
+ }
301
+ },
302
+ {
303
+ orientation: "vertical",
304
+ size: "3xl",
305
+ class: {
306
+ wrapper: "mt-3.5 pb-8.5"
307
+ }
308
+ }
309
+ ],
310
+ defaultVariants: {
311
+ size: "md",
312
+ color: "primary"
313
+ }
16
314
  })
17
315
 
18
316
  export default createTimelineTheme()
@@ -1,11 +1,61 @@
1
- export interface TimelineProps {
1
+ export interface TimelineItem {
2
+ date?: string
3
+ title?: string
4
+ description?: string
5
+ icon?: string
6
+ avatar?: any
7
+ value?: string | number
8
+ slot?: string
9
+ class?: any
10
+ ui?: {
11
+ item?: any
12
+ container?: any
13
+ indicator?: any
14
+ separator?: any
15
+ wrapper?: any
16
+ date?: any
17
+ title?: any
18
+ description?: any
19
+ [key: string]: any
20
+ }
21
+ [key: string]: any
22
+ }
23
+
24
+ export interface TimelineProps<T extends TimelineItem = TimelineItem> {
25
+ /**
26
+ * The element or component this component should render as.
27
+ */
28
+ as?: any
29
+ items?: T[]
30
+ size?: "3xs" | "2xs" | "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl"
31
+ color?: "primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral"
2
32
  /**
3
- * Slot-specific CSS class overrides.
33
+ * The orientation of the Timeline.
34
+ *
35
+ * @default "vertical"
4
36
  */
5
- ui?: any
37
+ orientation?: "horizontal" | "vertical"
6
38
  /**
7
- * Additional CSS classes to apply to the root element.
39
+ * The key used to get the value from the item.
40
+ *
41
+ * @default "value"
8
42
  */
43
+ valueKey?: string
44
+ defaultValue?: string | number
45
+ modelValue?: string | number
46
+ reverse?: boolean
47
+ ui?: {
48
+ root?: any
49
+ item?: any
50
+ container?: any
51
+ indicator?: any
52
+ separator?: any
53
+ wrapper?: any
54
+ date?: any
55
+ title?: any
56
+ description?: any
57
+ [key: string]: any
58
+ }
9
59
  class?: any
10
60
  [key: string]: unknown
11
61
  }
@@ -4,7 +4,7 @@ import { useUi } from "../../utils/useUi"
4
4
  import { resolveClasses } from "../../utils/resolveClasses"
5
5
  import type { VideoProps } from "./video"
6
6
  import videoTheme from "./video.theme"
7
- import Image from "astro/components/Image.astro"
7
+ import { Image } from "astro:assets"
8
8
  import RLAIcon from "../icon/RLAIcon.astro"
9
9
 
10
10
  const PLACEHOLDER_THUMBNAIL = "/assets/placeholder.webp";