@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,216 +0,0 @@
1
- import { Show, For } from "solid-js";
2
- import { Avatar } from "@proyecto-viviana/solid-spectrum";
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 conversation preview row, message bubbles, and a thread
6
- // list, on the S2 spacing/radius ramps in Silapse colors.
7
- import { style } from "../../style" with { type: "macro" };
8
-
9
- export interface Message {
10
- id: string;
11
- content: string;
12
- sender: "user" | "other";
13
- timestamp?: string;
14
- }
15
-
16
- export interface ConversationPreviewProps {
17
- user: {
18
- name: string;
19
- avatar?: string;
20
- online?: boolean;
21
- };
22
- lastMessage?: string;
23
- unreadCount?: number;
24
- timestamp?: string;
25
- onClick?: () => void;
26
- class?: string;
27
- }
28
-
29
- const preview = style({
30
- display: "flex",
31
- alignItems: "center",
32
- gap: 12,
33
- width: "full",
34
- padding: 12,
35
- borderRadius: "lg",
36
- textAlign: "start",
37
- cursor: "pointer",
38
- backgroundColor: "transparent",
39
- borderStyle: "none",
40
- });
41
-
42
- const previewBody = style({
43
- display: "flex",
44
- flexDirection: "column",
45
- gap: 2,
46
- flexGrow: 1,
47
- minWidth: 0,
48
- });
49
-
50
- const previewTop = style({
51
- display: "flex",
52
- alignItems: "center",
53
- justifyContent: "space-between",
54
- gap: 8,
55
- });
56
-
57
- const previewName = style({
58
- font: "ui",
59
- fontWeight: "bold",
60
- color: "[var(--color-primary-100)]",
61
- overflow: "hidden",
62
- textOverflow: "ellipsis",
63
- whiteSpace: "nowrap",
64
- });
65
-
66
- const previewTime = style({
67
- font: "detail-sm",
68
- // `--color-text-muted` renders ~1.7:1 light / ~2.4:1 dark on the `bg-200`
69
- // panel (fails AA both modes); the flipping `--color-text` clears it. Muting
70
- // is carried by the smaller `detail-sm` size, not a sub-AA color.
71
- color: "[var(--color-text)]",
72
- flexShrink: 0,
73
- });
74
-
75
- const previewMessage = style({
76
- font: "ui-sm",
77
- // `--color-text-secondary` renders 3.84:1 on the light `bg-200` panel (fails
78
- // the 4.5 small-text floor); the flipping `--color-text` clears both themes.
79
- color: "[var(--color-text)]",
80
- overflow: "hidden",
81
- textOverflow: "ellipsis",
82
- whiteSpace: "nowrap",
83
- });
84
-
85
- const unread = style({
86
- display: "flex",
87
- alignItems: "center",
88
- justifyContent: "center",
89
- flexShrink: 0,
90
- width: 20,
91
- height: 20,
92
- borderRadius: "full",
93
- font: "detail-sm",
94
- fontWeight: "bold",
95
- backgroundColor: "[var(--color-accent)]",
96
- // Text on the non-flipping pink `--color-accent` fill: the near-white
97
- // `--color-bg-400` gives only 2.74:1 in light mode (fail). `--color-grey-900`
98
- // (the darkest token) reaches ~5.2:1 light / ~6.1:1 dark — the Chip accent-fill
99
- // resolution.
100
- color: "[var(--color-grey-900)]",
101
- });
102
-
103
- export function ConversationPreview(props: ConversationPreviewProps) {
104
- return (
105
- <HeadlessButton
106
- type="button"
107
- class={`${preview} ${props.class ?? ""}`}
108
- onPress={() => props.onClick?.()}
109
- >
110
- <Avatar src={props.user.avatar} alt={props.user.name} online={props.user.online} size="md" />
111
- <div class={previewBody}>
112
- <div class={previewTop}>
113
- <span class={previewName}>{props.user.name}</span>
114
- <Show when={props.timestamp}>
115
- <span class={previewTime}>{props.timestamp}</span>
116
- </Show>
117
- </div>
118
- <Show when={props.lastMessage}>
119
- <p class={previewMessage}>{props.lastMessage}</p>
120
- </Show>
121
- </div>
122
- <Show when={props.unreadCount && props.unreadCount > 0}>
123
- <span class={unread}>{props.unreadCount}</span>
124
- </Show>
125
- </HeadlessButton>
126
- );
127
- }
128
-
129
- export interface ConversationBubbleProps {
130
- content: string;
131
- sender: "user" | "other";
132
- timestamp?: string;
133
- class?: string;
134
- }
135
-
136
- const bubbleRow = style<{ isUser: boolean }>({
137
- display: "flex",
138
- justifyContent: { default: "start", isUser: "end" },
139
- });
140
-
141
- const bubble = style<{ isUser: boolean }>({
142
- display: "flex",
143
- flexDirection: "column",
144
- gap: 2,
145
- maxWidth: "[70%]",
146
- paddingX: 16,
147
- paddingY: 8,
148
- borderRadius: "lg",
149
- backgroundColor: {
150
- default: "[var(--color-bg-300)]",
151
- isUser: "[var(--color-accent)]",
152
- },
153
- color: {
154
- // `other` bubble: `--color-primary-100` on the neutral `bg-300` fill (AA both
155
- // themes). `user` bubble text on the pink `--color-accent` fill: the near-white
156
- // `--color-bg-400` gives only 2.74:1 light (fail) → `--color-grey-900` (~5.2:1
157
- // light / ~6.1:1 dark), the Chip accent-fill resolution.
158
- default: "[var(--color-primary-100)]",
159
- isUser: "[var(--color-grey-900)]",
160
- },
161
- });
162
-
163
- const bubbleTime = style<{ isUser: boolean }>({
164
- font: "detail-sm",
165
- color: {
166
- // `other` timestamp on `bg-300`: `--color-text-muted` fails AA both modes
167
- // (~2.2:1 light / ~2.8:1 dark) → the flipping `--color-text`. `user` timestamp
168
- // on the `--color-accent` fill: `--color-bg-300` gives 2.42:1 light (fail) →
169
- // `--color-grey-900` (matching the bubble body).
170
- default: "[var(--color-text)]",
171
- isUser: "[var(--color-grey-900)]",
172
- },
173
- });
174
-
175
- export function ConversationBubble(props: ConversationBubbleProps) {
176
- const isUser = () => props.sender === "user";
177
-
178
- return (
179
- <div class={`${bubbleRow({ isUser: isUser() })} ${props.class ?? ""}`}>
180
- <div class={bubble({ isUser: isUser() })}>
181
- <p>{props.content}</p>
182
- <Show when={props.timestamp}>
183
- <span class={bubbleTime({ isUser: isUser() })}>{props.timestamp}</span>
184
- </Show>
185
- </div>
186
- </div>
187
- );
188
- }
189
-
190
- export interface ConversationProps {
191
- messages: Message[];
192
- class?: string;
193
- }
194
-
195
- const thread = style({
196
- display: "flex",
197
- flexDirection: "column",
198
- gap: 8,
199
- padding: 16,
200
- });
201
-
202
- export function Conversation(props: ConversationProps) {
203
- return (
204
- <div class={`${thread} ${props.class ?? ""}`}>
205
- <For each={props.messages}>
206
- {(message) => (
207
- <ConversationBubble
208
- content={message.content}
209
- sender={message.sender}
210
- timestamp={message.timestamp}
211
- />
212
- )}
213
- </For>
214
- </div>
215
- );
216
- }
@@ -1,253 +0,0 @@
1
- import type { JSX } from "solid-js";
2
- import { Show, For } from "solid-js";
3
- import { Avatar } from "@proyecto-viviana/solid-spectrum";
4
- import { Button as HeadlessButton } from "@proyecto-viviana/solidaria-components";
5
- // Styled via the S2 style() macro through viviana's own seam (../../style →
6
- // src/style.ts). An event summary card (S2 Card shape: preview + content +
7
- // footer) plus a compact list-item variant, on the S2 ramps in Silapse colors.
8
- import { style } from "../../style" with { type: "macro" };
9
-
10
- export interface EventCardProps {
11
- title: string;
12
- image?: string;
13
- date?: string;
14
- author?: string;
15
- authorAvatar?: string;
16
- attendees?: { avatar?: string; name: string }[];
17
- attendeeCount?: number;
18
- decorationImage?: string;
19
- /**
20
- * Actions to display below the event.
21
- * Use a function returning JSX for SSR compatibility: `actions={() => <Button>...</Button>}`
22
- */
23
- actions?: JSX.Element | (() => JSX.Element);
24
- class?: string;
25
- }
26
-
27
- const card = style({
28
- position: "relative",
29
- display: "flex",
30
- flexDirection: "column",
31
- overflow: "hidden",
32
- borderRadius: "xl",
33
- backgroundColor: "[var(--color-bg-200)]",
34
- boxShadow: "elevated",
35
- });
36
-
37
- const decoration = style({
38
- position: "absolute",
39
- top: 8,
40
- insetEnd: 8,
41
- width: 32,
42
- height: 32,
43
- objectFit: "contain",
44
- });
45
-
46
- const preview = style({
47
- width: "full",
48
- aspectRatio: "16/9",
49
- objectFit: "cover",
50
- });
51
-
52
- const body = style({
53
- display: "flex",
54
- flexDirection: "column",
55
- gap: 12,
56
- padding: 16,
57
- });
58
-
59
- const title = style({
60
- font: "heading",
61
- // WCAG AA fix: the title sits on the `--color-bg-200` card. `--color-accent`
62
- // (#df5c9a) is a fixed pink in BOTH themes, so it does not flip against a card
63
- // that is dark-grey in dark mode and light-blue in light mode — it read 4.48:1
64
- // dark / 1.89:1 light. `font: heading` renders 22px / weight 800, which is WCAG
65
- // large text via the BOLD path (≥18.66px && ≥700), NOT because it is ≥24px — so
66
- // the 3:1 large-text floor applies. `--color-accent-500` is the smallest ramp
67
- // step that flips with the theme (bright pink dark / deep magenta light) AND
68
- // clears 3:1 both ways (3.87:1 dark / 4.91:1 light), keeping the brand accent.
69
- color: "[var(--color-accent-500)]",
70
- });
71
-
72
- const meta = style({
73
- display: "flex",
74
- alignItems: "center",
75
- gap: 16,
76
- font: "ui-sm",
77
- // WCAG AA fix: the meta text is small (`ui-sm`), so the 4.5:1 normal-text
78
- // floor applies. `--color-text-secondary` read only 3.84:1 on the light
79
- // `--color-bg-200` card; `--color-text` flips and clears 4.5:1 both ways
80
- // (15.33:1 dark / 7.53:1 light). Secondary de-emphasis rides on the smaller
81
- // `ui-sm` size rather than a sub-AA color.
82
- color: "[var(--color-text)]",
83
- });
84
-
85
- const metaItem = style({
86
- display: "flex",
87
- alignItems: "center",
88
- gap: 4,
89
- });
90
-
91
- const metaIcon = style({
92
- // WCAG AA fix: the meta glyphs (@ / ⏱) are small (inherit `ui-sm`), so the
93
- // 4.5:1 floor applies. `--color-accent` fails on the card (4.48:1 dark /
94
- // 1.89:1 light) and no pink ramp step clears 4.5:1 on both the dark-grey and
95
- // light-blue card, so the glyphs take the flipping `--color-text` like the
96
- // meta text they prefix.
97
- color: "[var(--color-text)]",
98
- });
99
-
100
- const attendeesRow = style({
101
- display: "flex",
102
- alignItems: "center",
103
- gap: 8,
104
- });
105
-
106
- const avatars = style({
107
- display: "flex",
108
- alignItems: "center",
109
- gap: 4,
110
- });
111
-
112
- const more = style({
113
- font: "ui-sm",
114
- // WCAG AA fix: the "+N más" overflow count is small text on the same
115
- // `--color-bg-200` card as the meta, so it shares the same 3.84:1-light
116
- // failure and the same `--color-text` fix.
117
- color: "[var(--color-text)]",
118
- });
119
-
120
- const actionRow = style({
121
- display: "flex",
122
- gap: 8,
123
- });
124
-
125
- export function EventCard(props: EventCardProps) {
126
- const displayedAttendees = () => props.attendees?.slice(0, 3) ?? [];
127
- const remainingCount = () => {
128
- const total = props.attendeeCount ?? props.attendees?.length ?? 0;
129
- const displayed = displayedAttendees().length;
130
- return total - displayed;
131
- };
132
-
133
- return (
134
- <div class={`${card} ${props.class ?? ""}`}>
135
- <Show when={props.decorationImage}>
136
- <img class={decoration} src={props.decorationImage} alt="" />
137
- </Show>
138
-
139
- <Show when={props.image}>
140
- <img class={preview} src={props.image} alt={props.title} />
141
- </Show>
142
-
143
- <div class={body}>
144
- <h3 class={title}>{props.title}</h3>
145
-
146
- <Show when={props.date || props.author}>
147
- <div class={meta}>
148
- <Show when={props.author}>
149
- <div class={metaItem}>
150
- <span class={metaIcon}>@</span>
151
- <span>{props.author}</span>
152
- </div>
153
- </Show>
154
- <Show when={props.date}>
155
- <div class={metaItem}>
156
- <span class={metaIcon}>⏱</span>
157
- <span>{props.date}</span>
158
- </div>
159
- </Show>
160
- </div>
161
- </Show>
162
-
163
- <Show when={displayedAttendees().length > 0}>
164
- <div class={attendeesRow}>
165
- <div class={avatars}>
166
- <For each={displayedAttendees()}>
167
- {(attendee) => <Avatar src={attendee.avatar} alt={attendee.name} size="sm" />}
168
- </For>
169
- </div>
170
- <Show when={remainingCount() > 0}>
171
- <span class={more}>+{remainingCount()} más</span>
172
- </Show>
173
- </div>
174
- </Show>
175
-
176
- <Show when={props.actions}>
177
- <div class={actionRow}>
178
- {typeof props.actions === "function" ? props.actions() : props.actions}
179
- </div>
180
- </Show>
181
- </div>
182
- </div>
183
- );
184
- }
185
-
186
- export interface EventListItemProps {
187
- title: string;
188
- image?: string;
189
- subtitle?: string;
190
- onClick?: () => void;
191
- class?: string;
192
- }
193
-
194
- const listItem = style({
195
- display: "flex",
196
- alignItems: "center",
197
- gap: 12,
198
- width: "full",
199
- padding: 8,
200
- borderRadius: "lg",
201
- textAlign: "start",
202
- cursor: "pointer",
203
- backgroundColor: "transparent",
204
- borderStyle: "none",
205
- });
206
-
207
- const thumb = style({
208
- width: 48,
209
- height: 48,
210
- flexShrink: 0,
211
- borderRadius: "default",
212
- objectFit: "cover",
213
- });
214
-
215
- const listBody = style({
216
- display: "flex",
217
- flexDirection: "column",
218
- flexGrow: 1,
219
- minWidth: 0,
220
- });
221
-
222
- const listTitle = style({
223
- font: "ui",
224
- fontWeight: "medium",
225
- color: "[var(--color-primary-100)]",
226
- overflow: "hidden",
227
- textOverflow: "ellipsis",
228
- whiteSpace: "nowrap",
229
- });
230
-
231
- const listSubtitle = style({
232
- font: "ui-sm",
233
- color: "[var(--color-text-secondary)]",
234
- overflow: "hidden",
235
- textOverflow: "ellipsis",
236
- whiteSpace: "nowrap",
237
- });
238
-
239
- export function EventListItem(props: EventListItemProps) {
240
- return (
241
- <HeadlessButton class={`${listItem} ${props.class ?? ""}`} onPress={() => props.onClick?.()}>
242
- <Show when={props.image}>
243
- <img class={thumb} src={props.image} alt={props.title} />
244
- </Show>
245
- <div class={listBody}>
246
- <h4 class={listTitle}>{props.title}</h4>
247
- <Show when={props.subtitle}>
248
- <p class={listSubtitle}>{props.subtitle}</p>
249
- </Show>
250
- </div>
251
- </HeadlessButton>
252
- );
253
- }
@@ -1,64 +0,0 @@
1
- import type { JSX } from "solid-js";
2
- import { Logo, type LogoProps } from "../logo";
3
- // Styled via the S2 style() macro through viviana's own seam (../../style →
4
- // src/style.ts). A top app-bar: centered max-width row with a logo group and
5
- // a nav slot, on the S2 ramps in Silapse colors.
6
- import { style } from "../../style" with { type: "macro" };
7
-
8
- export interface HeaderProps {
9
- logoImage?: JSX.Element;
10
- /** Props to pass to the Logo component (firstWord, secondWord, size, inverted). Pass null to hide the text logo. */
11
- logoProps?: LogoProps | null;
12
- logo?: JSX.Element;
13
- children?: JSX.Element;
14
- class?: string;
15
- }
16
-
17
- const bar = style({
18
- display: "flex",
19
- alignItems: "center",
20
- justifyContent: "center",
21
- width: "full",
22
- backgroundColor: "[var(--color-header-bg)]",
23
- borderBottomWidth: 1,
24
- borderStyle: "solid",
25
- borderColor: "[var(--color-border)]",
26
- });
27
-
28
- const inner = style({
29
- display: "flex",
30
- alignItems: "center",
31
- justifyContent: "space-between",
32
- width: "full",
33
- maxWidth: 1152,
34
- paddingX: 24,
35
- paddingY: 12,
36
- });
37
-
38
- const logoGroup = style({
39
- display: "flex",
40
- alignItems: "center",
41
- gap: 12,
42
- });
43
-
44
- const nav = style({
45
- display: "flex",
46
- alignItems: "center",
47
- gap: 16,
48
- });
49
-
50
- export function Header(props: HeaderProps) {
51
- const showTextLogo = () => props.logo !== undefined || props.logoProps !== null;
52
-
53
- return (
54
- <header class={`${bar} ${props.class ?? ""}`}>
55
- <div class={inner}>
56
- <div class={logoGroup}>
57
- {props.logoImage}
58
- {showTextLogo() && (props.logo ?? <Logo size="lg" {...(props.logoProps ?? {})} />)}
59
- </div>
60
- <nav class={nav}>{props.children}</nav>
61
- </div>
62
- </header>
63
- );
64
- }
@@ -1,143 +0,0 @@
1
- import type { JSX } from "solid-js";
2
- import { Show, For } from "solid-js";
3
- // Styled via the S2 style() macro through viviana's own seam (../../style →
4
- // src/style.ts). A sidebar nav (sections with an accent rail + links) on the
5
- // S2 spacing/type ramps in Silapse colors.
6
- import { style } from "../../style" with { type: "macro" };
7
-
8
- export interface NavItemProps {
9
- title: string;
10
- children?: JSX.Element;
11
- class?: string;
12
- }
13
-
14
- const item = style({
15
- display: "flex",
16
- alignItems: "center",
17
- font: "heading-sm",
18
- color: "[var(--color-primary-200)]",
19
- });
20
-
21
- export function NavItem(props: NavItemProps) {
22
- return (
23
- <li class={`${item} ${props.class ?? ""}`}>
24
- <span>{props.title}</span>
25
- {props.children}
26
- </li>
27
- );
28
- }
29
-
30
- export interface NavLinkProps {
31
- href: string;
32
- children: JSX.Element;
33
- active?: boolean;
34
- class?: string;
35
- }
36
-
37
- const link = style<{ isActive: boolean }>({
38
- // A ≥24px hit target (WCAG 2.5.8): the link is a full-width flex row, not a
39
- // bare inline anchor (which rendered ~15px tall on the `ui` ramp).
40
- display: "flex",
41
- alignItems: "center",
42
- minHeight: 32,
43
- font: "ui",
44
- textDecoration: { default: "none", isActive: "underline" },
45
- fontWeight: { default: "normal", isActive: "medium" },
46
- // `color` MUST stay after `font`: the S2 style() `font` shorthand emits a
47
- // default text color that wins on source order otherwise. The resting link
48
- // uses the flipping `--color-text` (AA on the light `bg-200` panel); the active
49
- // link is set apart by its underline + medium weight + `--color-primary-300`.
50
- color: {
51
- default: "[var(--color-text)]",
52
- isActive: "[var(--color-primary-300)]",
53
- },
54
- });
55
-
56
- export function NavLink(props: NavLinkProps) {
57
- return (
58
- <a
59
- href={props.href}
60
- class={`${link({ isActive: props.active ?? false })} ${props.class ?? ""}`}
61
- >
62
- {props.children}
63
- </a>
64
- );
65
- }
66
-
67
- export interface NavSectionProps {
68
- title: string;
69
- links?: { href: string; label: string; active?: boolean }[];
70
- children?: JSX.Element;
71
- class?: string;
72
- }
73
-
74
- const sectionRow = style({
75
- display: "flex",
76
- });
77
-
78
- const accentRail = style({
79
- flexShrink: 0,
80
- width: 4,
81
- height: 20,
82
- backgroundColor: "[var(--color-accent-300)]",
83
- });
84
-
85
- const list = style({
86
- display: "flex",
87
- flexDirection: "column",
88
- flexGrow: 1,
89
- gap: 4,
90
- paddingStart: 16,
91
- });
92
-
93
- export function NavSection(props: NavSectionProps) {
94
- return (
95
- <div class={props.class ?? ""}>
96
- <NavItem title={props.title} />
97
- <div class={sectionRow}>
98
- <div class={accentRail} />
99
- <ul class={list}>
100
- <Show when={props.links}>
101
- <For each={props.links}>
102
- {(navLink) => (
103
- <li>
104
- <NavLink href={navLink.href} active={navLink.active}>
105
- {navLink.label}
106
- </NavLink>
107
- </li>
108
- )}
109
- </For>
110
- </Show>
111
- {props.children}
112
- </ul>
113
- </div>
114
- </div>
115
- );
116
- }
117
-
118
- export interface LateralNavProps {
119
- transparent?: boolean;
120
- children?: JSX.Element;
121
- class?: string;
122
- }
123
-
124
- const nav = style<{ isTransparent: boolean }>({
125
- width: 300,
126
- margin: 0,
127
- padding: 12,
128
- borderEndWidth: 1,
129
- borderStyle: "solid",
130
- borderColor: "[var(--color-primary-600)]",
131
- backgroundColor: {
132
- default: "[var(--color-bg-200)]",
133
- isTransparent: "transparent",
134
- },
135
- });
136
-
137
- export function LateralNav(props: LateralNavProps) {
138
- return (
139
- <div class={`${nav({ isTransparent: props.transparent ?? false })} ${props.class ?? ""}`}>
140
- {props.children}
141
- </div>
142
- );
143
- }