@proyecto-viviana/ui 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/dist/styles.css +0 -3
  2. package/dist/viviana-tokens.css +240 -171
  3. package/package.json +3 -57
  4. package/src/viviana-tokens.css +240 -171
  5. package/dist/CalendarCard.d.ts +0 -2
  6. package/dist/CalendarCard.d.ts.map +0 -1
  7. package/dist/CalendarCard.js +0 -96
  8. package/dist/CalendarCard.js.map +0 -1
  9. package/dist/CalendarCard.jsx +0 -50
  10. package/dist/CalendarCard.jsx.map +0 -1
  11. package/dist/Chip.d.ts +0 -2
  12. package/dist/Chip.d.ts.map +0 -1
  13. package/dist/Chip.js +0 -2
  14. package/dist/Chip.jsx +0 -2
  15. package/dist/Conversation.d.ts +0 -2
  16. package/dist/Conversation.d.ts.map +0 -1
  17. package/dist/Conversation.js +0 -176
  18. package/dist/Conversation.js.map +0 -1
  19. package/dist/Conversation.jsx +0 -106
  20. package/dist/Conversation.jsx.map +0 -1
  21. package/dist/EventCard.d.ts +0 -2
  22. package/dist/EventCard.d.ts.map +0 -1
  23. package/dist/EventCard.js +0 -171
  24. package/dist/EventCard.js.map +0 -1
  25. package/dist/EventCard.jsx +0 -94
  26. package/dist/EventCard.jsx.map +0 -1
  27. package/dist/Logo.d.ts +0 -2
  28. package/dist/Logo.d.ts.map +0 -1
  29. package/dist/Logo.js +0 -116
  30. package/dist/Logo.js.map +0 -1
  31. package/dist/Logo.jsx +0 -101
  32. package/dist/Logo.jsx.map +0 -1
  33. package/dist/PageLayout.d.ts +0 -2
  34. package/dist/PageLayout.d.ts.map +0 -1
  35. package/dist/PageLayout.js +0 -31
  36. package/dist/PageLayout.js.map +0 -1
  37. package/dist/PageLayout.jsx +0 -23
  38. package/dist/PageLayout.jsx.map +0 -1
  39. package/dist/ProfileCard.d.ts +0 -2
  40. package/dist/ProfileCard.d.ts.map +0 -1
  41. package/dist/ProfileCard.js +0 -77
  42. package/dist/ProfileCard.js.map +0 -1
  43. package/dist/ProfileCard.jsx +0 -52
  44. package/dist/ProfileCard.jsx.map +0 -1
  45. package/dist/ProjectCard.d.ts +0 -2
  46. package/dist/ProjectCard.d.ts.map +0 -1
  47. package/dist/ProjectCard.js +0 -162
  48. package/dist/ProjectCard.js.map +0 -1
  49. package/dist/ProjectCard.jsx +0 -131
  50. package/dist/ProjectCard.jsx.map +0 -1
  51. package/dist/TimelineItem.d.ts +0 -2
  52. package/dist/TimelineItem.d.ts.map +0 -1
  53. package/dist/TimelineItem.js +0 -137
  54. package/dist/TimelineItem.js.map +0 -1
  55. package/dist/TimelineItem.jsx +0 -70
  56. package/dist/TimelineItem.jsx.map +0 -1
  57. package/dist/chip2.js +0 -82
  58. package/dist/chip2.js.map +0 -1
  59. package/dist/chip2.jsx +0 -68
  60. package/dist/chip2.jsx.map +0 -1
  61. package/dist/custom/calendar-card/index.d.ts +0 -12
  62. package/dist/custom/calendar-card/index.d.ts.map +0 -1
  63. package/dist/custom/chip/index.d.ts +0 -16
  64. package/dist/custom/chip/index.d.ts.map +0 -1
  65. package/dist/custom/conversation/index.d.ts +0 -32
  66. package/dist/custom/conversation/index.d.ts.map +0 -1
  67. package/dist/custom/event-card/index.d.ts +0 -30
  68. package/dist/custom/event-card/index.d.ts.map +0 -1
  69. package/dist/custom/header/index.d.ts +0 -12
  70. package/dist/custom/header/index.d.ts.map +0 -1
  71. package/dist/custom/lateral-nav/index.d.ts +0 -32
  72. package/dist/custom/lateral-nav/index.d.ts.map +0 -1
  73. package/dist/custom/logo/index.d.ts +0 -11
  74. package/dist/custom/logo/index.d.ts.map +0 -1
  75. package/dist/custom/nav-header/index.d.ts +0 -13
  76. package/dist/custom/nav-header/index.d.ts.map +0 -1
  77. package/dist/custom/page-layout/index.d.ts +0 -7
  78. package/dist/custom/page-layout/index.d.ts.map +0 -1
  79. package/dist/custom/profile-card/index.d.ts +0 -16
  80. package/dist/custom/profile-card/index.d.ts.map +0 -1
  81. package/dist/custom/project-card/index.d.ts +0 -13
  82. package/dist/custom/project-card/index.d.ts.map +0 -1
  83. package/dist/custom/timeline-item/index.d.ts +0 -28
  84. package/dist/custom/timeline-item/index.d.ts.map +0 -1
  85. package/src/CalendarCard.ts +0 -1
  86. package/src/Chip.ts +0 -1
  87. package/src/Conversation.ts +0 -1
  88. package/src/EventCard.ts +0 -1
  89. package/src/Logo.ts +0 -1
  90. package/src/PageLayout.ts +0 -1
  91. package/src/ProfileCard.ts +0 -1
  92. package/src/ProjectCard.ts +0 -1
  93. package/src/TimelineItem.ts +0 -1
  94. package/src/custom/calendar-card/index.tsx +0 -124
  95. package/src/custom/chip/index.tsx +0 -82
  96. package/src/custom/conversation/index.tsx +0 -216
  97. package/src/custom/event-card/index.tsx +0 -253
  98. package/src/custom/header/index.tsx +0 -64
  99. package/src/custom/lateral-nav/index.tsx +0 -143
  100. package/src/custom/logo/index.tsx +0 -60
  101. package/src/custom/nav-header/index.tsx +0 -106
  102. package/src/custom/page-layout/index.tsx +0 -33
  103. package/src/custom/profile-card/index.tsx +0 -123
  104. package/src/custom/project-card/index.tsx +0 -76
  105. package/src/custom/timeline-item/index.tsx +0 -148
@@ -1,60 +0,0 @@
1
- import type { JSX } from "solid-js";
2
- // Styled via the S2 style() macro through viviana's own seam (../../style →
3
- // src/style.ts). A clean two-word wordmark: the S2 title ramp sizes it; Silapse
4
- // colors paint it (primary + accent). `inverted` swaps which word takes accent.
5
- import { style } from "../../style" with { type: "macro" };
6
-
7
- export type LogoSize = "sm" | "md" | "lg" | "xl";
8
-
9
- export interface LogoProps {
10
- firstWord?: string;
11
- secondWord?: string;
12
- size?: LogoSize;
13
- inverted?: boolean;
14
- class?: string;
15
- }
16
-
17
- const wordmark = style<{ size: LogoSize }>({
18
- display: "inline-flex",
19
- alignItems: "baseline",
20
- columnGap: 8,
21
- whiteSpace: "nowrap",
22
- font: {
23
- size: {
24
- sm: "title-sm",
25
- md: "title",
26
- lg: "title-xl",
27
- xl: "title-3xl",
28
- },
29
- },
30
- fontWeight: "black",
31
- });
32
-
33
- const word = style<{ tone: "primary" | "accent" }>({
34
- color: {
35
- tone: {
36
- primary: "[var(--color-primary-100)]",
37
- // `--color-accent` (#df5c9a) is the SAME non-flipping pink in both themes;
38
- // on the light `--color-bg-200` panel the wordmark reads only 1.89:1 (fails
39
- // even the 3:1 large-text floor the `black`-weight `title-xl` earns). No
40
- // single non-flipping shade clears a near-white AND a near-black panel, so
41
- // the accent word takes the *flipping* `--color-accent-500` (dark #d84a8f /
42
- // light #8a1e4a) → 3.86:1 dark / 4.91:1 light — clearing the large-text
43
- // floor in both themes while keeping the two-tone wordmark identity.
44
- accent: "[var(--color-accent-500)]",
45
- },
46
- },
47
- });
48
-
49
- export function Logo(props: LogoProps): JSX.Element {
50
- const firstWord = () => props.firstWord ?? "Proyecto";
51
- const secondWord = () => props.secondWord ?? "Viviana";
52
- const inverted = () => props.inverted ?? false;
53
-
54
- return (
55
- <span class={`${wordmark({ size: props.size ?? "md" })} ${props.class ?? ""}`}>
56
- <span class={word({ tone: inverted() ? "accent" : "primary" })}>{firstWord()}</span>
57
- <span class={word({ tone: inverted() ? "primary" : "accent" })}>{secondWord()}</span>
58
- </span>
59
- );
60
- }
@@ -1,106 +0,0 @@
1
- import type { JSX } from "solid-js";
2
- import { Show } from "solid-js";
3
- import { Button as HeadlessButton } from "@proyecto-viviana/solidaria-components";
4
- // Styled via the S2 style() macro through viviana's own seam (../../style →
5
- // src/style.ts). A nav bar with an accent bottom rule, a logo/wordmark slot,
6
- // and trailing actions, on the S2 ramps in Silapse colors.
7
- import { style } from "../../style" with { type: "macro" };
8
-
9
- export interface NavHeaderProps {
10
- logo?: string;
11
- logoAlt?: string;
12
- logoText?: string;
13
- children?: JSX.Element;
14
- menuIcon?: JSX.Element;
15
- onMenuClick?: () => void;
16
- menuAriaLabel?: string;
17
- class?: string;
18
- }
19
-
20
- const bar = style({
21
- display: "flex",
22
- alignItems: "center",
23
- height: 70,
24
- backgroundColor: "[var(--color-bg-400)]",
25
- borderBottomWidth: 4,
26
- borderStyle: "solid",
27
- borderColor: "[var(--color-accent)]",
28
- });
29
-
30
- const lead = style({
31
- display: "flex",
32
- alignItems: "center",
33
- paddingStart: 32,
34
- });
35
-
36
- const logoText = style({
37
- display: "flex",
38
- alignItems: "center",
39
- font: "title-xl",
40
- fontWeight: "normal",
41
- // WCAG AA fix: the wordmark sits on the `--color-bg-400` bar. `--color-primary-700`
42
- // is a non-flipping brand fill (light blue in light mode, dark blue in dark mode) —
43
- // the SAME direction as the bar, so it read only 2.10:1 light / 2.92:1 dark. The
44
- // rendered `title-xl` measures under 24px, so WCAG scores it as normal text (4.5:1
45
- // floor, not the 3:1 large-text exception). `--color-primary-500` is the smallest
46
- // ramp step that flips with the theme (dark-on-light / light-on-dark) AND clears
47
- // 4.5:1, lifting the wordmark to 5.37:1 light / 7.00:1 dark while keeping a prominent
48
- // brand blue.
49
- color: "[var(--color-primary-500)]",
50
- });
51
-
52
- const logoImage = style({
53
- height: 42,
54
- width: "auto",
55
- });
56
-
57
- const trail = style({
58
- display: "flex",
59
- alignItems: "center",
60
- justifyContent: "end",
61
- flexGrow: 1,
62
- gap: 12,
63
- paddingEnd: 32,
64
- });
65
-
66
- const menuButton = style({
67
- display: "flex",
68
- alignItems: "center",
69
- justifyContent: "center",
70
- cursor: "pointer",
71
- backgroundColor: "transparent",
72
- borderStyle: "none",
73
- color: "[var(--color-primary-200)]",
74
- });
75
-
76
- export function NavHeader(props: NavHeaderProps) {
77
- return (
78
- <nav class={`${bar} ${props.class ?? ""}`}>
79
- <div class={lead}>
80
- <Show
81
- when={props.logo}
82
- fallback={
83
- <Show when={props.logoText}>
84
- <span class={logoText}>{props.logoText}</span>
85
- </Show>
86
- }
87
- >
88
- <img class={logoImage} src={props.logo} alt={props.logoAlt ?? "Logo"} />
89
- </Show>
90
- </div>
91
-
92
- <div class={trail}>
93
- {props.children}
94
- <Show when={props.menuIcon}>
95
- <HeadlessButton
96
- class={menuButton}
97
- onPress={() => props.onMenuClick?.()}
98
- aria-label={props.menuAriaLabel ?? "Open menu"}
99
- >
100
- {props.menuIcon}
101
- </HeadlessButton>
102
- </Show>
103
- </div>
104
- </nav>
105
- );
106
- }
@@ -1,33 +0,0 @@
1
- import { type JSX, splitProps } from "solid-js";
2
- // Styled via the S2 style() macro through viviana's own seam (../../style →
3
- // src/style.ts). A full-height page shell painted in Silapse surface/text
4
- // colors; `withHeader` reserves top space so a fixed header doesn't overlap.
5
- import { style } from "../../style" with { type: "macro" };
6
-
7
- export interface PageLayoutProps extends JSX.HTMLAttributes<HTMLDivElement> {
8
- children: JSX.Element;
9
- withHeader?: boolean;
10
- }
11
-
12
- const page = style<{ isWithHeader: boolean }>({
13
- display: "flex",
14
- flexDirection: "column",
15
- minHeight: "screen",
16
- width: "full",
17
- backgroundColor: "[var(--color-background)]",
18
- color: "[var(--color-text)]",
19
- paddingTop: { isWithHeader: 64 },
20
- });
21
-
22
- export function PageLayout(props: PageLayoutProps) {
23
- const [local, rest] = splitProps(props, ["class", "withHeader"]);
24
-
25
- return (
26
- <div
27
- class={`${page({ isWithHeader: local.withHeader ?? false })} ${local.class ?? ""}`}
28
- {...rest}
29
- >
30
- {props.children}
31
- </div>
32
- );
33
- }
@@ -1,123 +0,0 @@
1
- import type { JSX } from "solid-js";
2
- import { Show } from "solid-js";
3
- import { Avatar } from "@proyecto-viviana/solid-spectrum";
4
- // Styled via the S2 style() macro through viviana's own seam (../../style →
5
- // src/style.ts). Mirrors S2's UserCard shape (avatar + title/description +
6
- // footer actions) on the S2 spacing/radius/elevation ramps, in Silapse colors.
7
- import { style } from "../../style" with { type: "macro" };
8
-
9
- export interface ProfileCardProps {
10
- username: string;
11
- avatar?: string;
12
- bio?: string;
13
- followers?: number;
14
- following?: number;
15
- /**
16
- * Actions to display below the profile.
17
- * Use a function returning JSX for SSR compatibility: `actions={() => <Button>...</Button>}`
18
- */
19
- actions?: JSX.Element | (() => JSX.Element);
20
- class?: string;
21
- }
22
-
23
- const card = style({
24
- display: "flex",
25
- flexDirection: "column",
26
- gap: 16,
27
- padding: 16,
28
- borderRadius: "lg",
29
- backgroundColor: "[var(--color-bg-200)]",
30
- boxShadow: "elevated",
31
- });
32
-
33
- const head = style({
34
- display: "flex",
35
- alignItems: "start",
36
- gap: 16,
37
- });
38
-
39
- const info = style({
40
- display: "flex",
41
- flexDirection: "column",
42
- gap: 4,
43
- flexGrow: 1,
44
- minWidth: 0,
45
- });
46
-
47
- const name = style({
48
- font: "heading-sm",
49
- color: "[var(--color-primary-100)]",
50
- overflow: "hidden",
51
- textOverflow: "ellipsis",
52
- whiteSpace: "nowrap",
53
- });
54
-
55
- const bio = style({
56
- font: "ui-sm",
57
- // WCAG AA fix: the bio is small text (`ui-sm`) on the `--color-bg-200` card, so
58
- // the 4.5:1 floor applies. `--color-text-secondary` read only 3.84:1 on the
59
- // light-blue card (5.86:1 dark); `--color-text` flips and clears 4.5:1 both
60
- // ways (15.33:1 dark / 7.53:1 light). The bio stays visually secondary to the
61
- // `heading-sm` name through its smaller size, not a sub-AA color.
62
- color: "[var(--color-text)]",
63
- });
64
-
65
- const stats = style({
66
- display: "flex",
67
- gap: 16,
68
- font: "ui-sm",
69
- // WCAG AA fix: the stat connector words ("seguidores" / "siguiendo") share the
70
- // bio's 3.84:1-light failure on the `--color-bg-200` card and take the same
71
- // flipping `--color-text`. (The stat VALUES are `--color-primary-100`, which
72
- // already clears AA — 13.47:1 dark / 10.37:1 light.)
73
- color: "[var(--color-text)]",
74
- });
75
-
76
- const statValue = style({
77
- fontWeight: "bold",
78
- color: "[var(--color-primary-100)]",
79
- });
80
-
81
- const actionRow = style({
82
- display: "flex",
83
- gap: 8,
84
- });
85
-
86
- export function ProfileCard(props: ProfileCardProps) {
87
- const formatNumber = (num: number) => {
88
- if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`;
89
- if (num >= 1000) return `${(num / 1000).toFixed(1)}K`;
90
- return num.toString();
91
- };
92
-
93
- return (
94
- <div class={`${card} ${props.class ?? ""}`}>
95
- <div class={head}>
96
- <Avatar src={props.avatar} alt={props.username} size="lg" />
97
- <div class={info}>
98
- <h3 class={name}>{props.username}</h3>
99
- <Show when={props.bio}>
100
- <p class={bio}>{props.bio}</p>
101
- </Show>
102
- <div class={stats}>
103
- <Show when={props.followers !== undefined}>
104
- <span>
105
- <span class={statValue}>{formatNumber(props.followers!)}</span> seguidores
106
- </span>
107
- </Show>
108
- <Show when={props.following !== undefined}>
109
- <span>
110
- <span class={statValue}>{formatNumber(props.following!)}</span> siguiendo
111
- </span>
112
- </Show>
113
- </div>
114
- </div>
115
- </div>
116
- <Show when={props.actions}>
117
- <div class={actionRow}>
118
- {typeof props.actions === "function" ? props.actions() : props.actions}
119
- </div>
120
- </Show>
121
- </div>
122
- );
123
- }
@@ -1,76 +0,0 @@
1
- import type { JSX } from "solid-js";
2
- // Styled via the S2 style() macro through viviana's own seam (../../style →
3
- // src/style.ts). A square logo/preview tile with a caption, sized by the S2
4
- // spacing + type ramps and painted in Silapse colors. `inactive` dims it;
5
- // `href` turns the whole card into a link.
6
- import { style } from "../../style" with { type: "macro" };
7
-
8
- export type ProjectCardSize = "sm" | "md" | "lg";
9
-
10
- export interface ProjectCardProps {
11
- name: string;
12
- imageSrc: string;
13
- imageAlt?: string;
14
- href?: string;
15
- size?: ProjectCardSize;
16
- inactive?: boolean;
17
- class?: string;
18
- }
19
-
20
- const card = style<{ size: ProjectCardSize; isInactive: boolean }>({
21
- display: "flex",
22
- flexDirection: "column",
23
- alignItems: "center",
24
- gap: 8,
25
- width: "full",
26
- borderRadius: "lg",
27
- borderWidth: 1,
28
- borderStyle: "solid",
29
- borderColor: "[var(--color-border)]",
30
- backgroundColor: "[var(--color-bg-200)]",
31
- color: "[var(--color-primary-100)]",
32
- textDecoration: "none",
33
- padding: { size: { sm: 12, md: 16, lg: 20 } },
34
- opacity: { default: 1, isInactive: 0.5 },
35
- });
36
-
37
- const image = style({
38
- width: "full",
39
- aspectRatio: "square",
40
- objectFit: "contain",
41
- borderRadius: "sm",
42
- });
43
-
44
- const caption = style<{ size: ProjectCardSize }>({
45
- width: "full",
46
- textAlign: "center",
47
- overflow: "hidden",
48
- textOverflow: "ellipsis",
49
- whiteSpace: "nowrap",
50
- font: { size: { sm: "ui-sm", md: "ui", lg: "ui-lg" } },
51
- color: "[var(--color-primary-200)]",
52
- });
53
-
54
- export function ProjectCard(props: ProjectCardProps): JSX.Element {
55
- const size = () => props.size ?? "sm";
56
- const inactive = () => props.inactive ?? false;
57
-
58
- const cardContent = () => (
59
- <>
60
- <img class={image} src={props.imageSrc} alt={props.imageAlt ?? props.name} />
61
- <span class={caption({ size: size() })}>{props.name}</span>
62
- </>
63
- );
64
-
65
- const classes = () => `${card({ size: size(), isInactive: inactive() })} ${props.class ?? ""}`;
66
-
67
- if (props.href) {
68
- return (
69
- <a href={props.href} target="_blank" rel="noopener noreferrer" class={classes()}>
70
- {cardContent()}
71
- </a>
72
- );
73
- }
74
-
75
- return <div class={classes()}>{cardContent()}</div>;
76
- }
@@ -1,148 +0,0 @@
1
- import type { JSX } from "solid-js";
2
- import { Show } from "solid-js";
3
- import { Avatar } from "@proyecto-viviana/solid-spectrum";
4
- // Styled via the S2 style() macro through viviana's own seam (../../style →
5
- // src/style.ts). A social timeline event card (two avatars + icon + message)
6
- // on the S2 spacing/radius ramps in Silapse colors.
7
- import { style } from "../../style" with { type: "macro" };
8
-
9
- export type TimelineEventType = "follow" | "like" | "comment" | "event" | "custom";
10
-
11
- export interface TimelineItemProps {
12
- type?: TimelineEventType;
13
- /**
14
- * Icon to display between the two avatars.
15
- * Use a function returning JSX for SSR compatibility: `icon={() => <MyIcon />}`
16
- * Or pass a simple string for text-based icons: `icon="👋"`
17
- */
18
- icon?: string | (() => JSX.Element);
19
- leftUser?: {
20
- name: string;
21
- avatar?: string;
22
- };
23
- rightUser?: {
24
- name: string;
25
- avatar?: string;
26
- };
27
- /**
28
- * Custom message content.
29
- * Use a function returning JSX for SSR compatibility: `message={() => <span>...</span>}`
30
- * Or pass a simple string.
31
- */
32
- message?: string | (() => JSX.Element);
33
- class?: string;
34
- }
35
-
36
- const nameEmph = style({
37
- fontWeight: "bold",
38
- // Emphasis via weight, not color: `--color-accent` (#df5c9a) fails AA on the
39
- // `--color-bg-200` card (~1.9:1 light, ~4.48:1 dark) and no accent shade clears
40
- // 4.5:1 in BOTH themes. The flipping `--color-text` does (7.53 light / 15.33
41
- // dark); the `bold` weight sets the names apart from the message body.
42
- color: "[var(--color-text)]",
43
- });
44
-
45
- const eventMessages: Record<TimelineEventType, (left: string, right: string) => JSX.Element> = {
46
- follow: (left, right) => (
47
- <>
48
- <span class={nameEmph}>{left}</span>
49
- {" ha empezado a seguir a "}
50
- <span class={nameEmph}>{right}</span>
51
- </>
52
- ),
53
- like: (left, right) => (
54
- <>
55
- <span class={nameEmph}>{left}</span>
56
- {" le ha dado like a "}
57
- <span class={nameEmph}>{right}</span>
58
- </>
59
- ),
60
- comment: (left, right) => (
61
- <>
62
- <span class={nameEmph}>{left}</span>
63
- {" ha comentado en "}
64
- <span class={nameEmph}>{right}</span>
65
- </>
66
- ),
67
- event: (left, right) => (
68
- <>
69
- <span class={nameEmph}>{left}</span>
70
- {" asistirá al evento de "}
71
- <span class={nameEmph}>{right}</span>
72
- </>
73
- ),
74
- custom: () => null,
75
- };
76
-
77
- const card = style({
78
- display: "inline-flex",
79
- flexDirection: "column",
80
- gap: 20,
81
- padding: 20,
82
- borderRadius: "xl",
83
- borderWidth: 1,
84
- borderStyle: "solid",
85
- borderColor: "[var(--color-primary-700)]",
86
- backgroundColor: "[var(--color-bg-200)]",
87
- });
88
-
89
- const avatarsRow = style({
90
- display: "flex",
91
- alignItems: "center",
92
- justifyContent: "center",
93
- gap: 12,
94
- });
95
-
96
- const messageRow = style({
97
- display: "flex",
98
- alignItems: "center",
99
- justifyContent: "center",
100
- textAlign: "center",
101
- font: "ui",
102
- // `--color-text-secondary` renders 3.84:1 on the light `--color-bg-200` card
103
- // (fails the 4.5:1 small-text floor); the flipping `--color-text` clears it in
104
- // both themes. `color` stays after `font` (the S2 macro font-shorthand default
105
- // color would otherwise win on source order).
106
- color: "[var(--color-text)]",
107
- });
108
-
109
- export function TimelineItem(props: TimelineItemProps) {
110
- const type = () => props.type ?? "follow";
111
- const leftName = () => props.leftUser?.name ?? "";
112
- const rightName = () => props.rightUser?.name ?? "";
113
-
114
- const renderIcon = () => {
115
- const icon = props.icon;
116
- if (!icon) return null;
117
- if (typeof icon === "string") return icon;
118
- return icon();
119
- };
120
-
121
- const renderMessage = () => {
122
- const message = props.message;
123
- if (!message) return null;
124
- if (typeof message === "string") return message;
125
- return message();
126
- };
127
-
128
- return (
129
- <div class={`${card} ${props.class ?? ""}`}>
130
- <div class={avatarsRow}>
131
- <Show when={props.leftUser}>
132
- <Avatar src={props.leftUser!.avatar} alt={props.leftUser!.name} />
133
- </Show>
134
- <Show when={props.icon}>{renderIcon()}</Show>
135
- <Show when={props.rightUser}>
136
- <Avatar src={props.rightUser!.avatar} alt={props.rightUser!.name} />
137
- </Show>
138
- </div>
139
- <div class={messageRow}>
140
- <span>
141
- <Show when={props.message} fallback={eventMessages[type()](leftName(), rightName())}>
142
- {renderMessage()}
143
- </Show>
144
- </span>
145
- </div>
146
- </div>
147
- );
148
- }