astryx-dracula 0.5.0 → 0.6.2
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/BRAND.md +3 -3
- package/README.md +7 -4
- package/USAGE.md +49 -49
- package/astryx-dracula.d.ts +2 -2
- package/astryx-dracula.js +25 -9
- package/astryx-dracula.variants.d.ts +2 -2
- package/astryx.integration.mjs +1 -1
- package/icons.mjs +21 -17
- package/icons.tsx +69 -62
- package/package.json +125 -120
- package/shared/auth-chrome-config.ts +6 -6
- package/shared/auth-chrome.tsx +143 -144
- package/shared/auth-copy.ts +14 -14
- package/shared/chaptered-doc-config.ts +48 -51
- package/shared/chaptered-doc.tsx +267 -289
- package/shared/chart-hues.ts +25 -10
- package/shared/chart-labels.tsx +31 -30
- package/shared/chart-legend.tsx +55 -56
- package/shared/chart-panel-style.ts +3 -3
- package/shared/data-bar.tsx +97 -102
- package/shared/format-money.ts +1 -1
- package/shared/gallery-image.ts +6 -6
- package/shared/login-demo.ts +1 -1
- package/shared/metric-card.tsx +101 -105
- package/shared/metric-delta.tsx +28 -27
- package/shared/order-desk.tsx +205 -216
- package/shared/revenue-chart.tsx +145 -165
- package/shared/scene-castle.tsx +206 -291
- package/shared/scene-frame.tsx +33 -32
- package/shared/scene-hues.ts +17 -17
- package/shared/scene-tile.tsx +88 -92
- package/shared/settings-data.ts +119 -125
- package/shared/settings-rows.tsx +122 -159
- package/shared/sparkline.tsx +75 -88
- package/shared/sso-icons.tsx +10 -22
- package/shared/stars.tsx +16 -25
- package/templates/ai-chat-landing.template.mjs +5 -4
- package/templates/ai-chat-landing.tsx +473 -460
- package/templates/ai-chat.template.mjs +5 -4
- package/templates/ai-chat.tsx +543 -612
- package/templates/blank.template.mjs +5 -4
- package/templates/blank.tsx +18 -14
- package/templates/centered-hero.template.mjs +5 -4
- package/templates/centered-hero.tsx +55 -68
- package/templates/classic-gallery.template.mjs +5 -4
- package/templates/classic-gallery.tsx +97 -104
- package/templates/contact-form.template.mjs +5 -4
- package/templates/contact-form.tsx +247 -282
- package/templates/dashboard-portfolio.template.mjs +5 -4
- package/templates/dashboard-portfolio.tsx +717 -548
- package/templates/dashboard.template.mjs +5 -4
- package/templates/dashboard.tsx +596 -567
- package/templates/detail-page.template.mjs +5 -4
- package/templates/detail-page.tsx +546 -603
- package/templates/documentation-design.template.mjs +5 -4
- package/templates/documentation-design.tsx +231 -243
- package/templates/documentation-technical.template.mjs +5 -4
- package/templates/documentation-technical.tsx +236 -239
- package/templates/documentation.template.mjs +5 -4
- package/templates/documentation.tsx +267 -279
- package/templates/editor.template.mjs +5 -4
- package/templates/editor.tsx +844 -877
- package/templates/file-explorer.template.mjs +5 -4
- package/templates/file-explorer.tsx +566 -590
- package/templates/form-two-column.template.mjs +5 -4
- package/templates/form-two-column.tsx +195 -210
- package/templates/gallery-hero.template.mjs +5 -4
- package/templates/gallery-hero.tsx +93 -114
- package/templates/ide.template.mjs +5 -4
- package/templates/ide.tsx +407 -453
- package/templates/incident-console.template.mjs +5 -4
- package/templates/incident-console.tsx +593 -575
- package/templates/kanban-board.template.mjs +5 -4
- package/templates/kanban-board.tsx +665 -683
- package/templates/library.template.mjs +5 -4
- package/templates/library.tsx +409 -430
- package/templates/login-card.template.mjs +5 -4
- package/templates/login-card.tsx +63 -72
- package/templates/login-split.template.mjs +5 -4
- package/templates/login-split.tsx +192 -193
- package/templates/login-sso.template.mjs +5 -4
- package/templates/login-sso.tsx +257 -258
- package/templates/login.template.mjs +5 -4
- package/templates/login.tsx +53 -49
- package/templates/messaging-shell.template.mjs +5 -4
- package/templates/messaging-shell.tsx +556 -574
- package/templates/mixed-gallery.template.mjs +5 -4
- package/templates/mixed-gallery.tsx +60 -58
- package/templates/payment-form.template.mjs +5 -4
- package/templates/payment-form.tsx +882 -991
- package/templates/product-detail.template.mjs +5 -4
- package/templates/product-detail.tsx +261 -272
- package/templates/product-gallery.template.mjs +5 -4
- package/templates/product-gallery.tsx +126 -137
- package/templates/product-tour.template.mjs +5 -4
- package/templates/product-tour.tsx +741 -773
- package/templates/settings-dialog.template.mjs +5 -4
- package/templates/settings-dialog.tsx +688 -717
- package/templates/settings-sidebar.template.mjs +5 -4
- package/templates/settings-sidebar.tsx +762 -807
- package/templates/settings.template.mjs +5 -4
- package/templates/settings.tsx +289 -304
- package/templates/shell-nav.template.mjs +5 -4
- package/templates/shell-nav.tsx +212 -219
- package/templates/shell-side-nav.template.mjs +5 -4
- package/templates/shell-side-nav.tsx +135 -157
- package/templates/shell-top-nav.template.mjs +5 -4
- package/templates/shell-top-nav.tsx +182 -203
- package/templates/side-gallery.template.mjs +5 -4
- package/templates/side-gallery.tsx +85 -90
- package/templates/table-grouped.template.mjs +5 -4
- package/templates/table-grouped.tsx +1000 -1053
- package/templates/table-page-chart.template.mjs +5 -4
- package/templates/table-page-chart.tsx +400 -398
- package/templates/table-page-heatmap-status.template.mjs +5 -4
- package/templates/table-page-heatmap-status.tsx +483 -512
- package/templates/table-page-shoe-store-heatmap.template.mjs +5 -4
- package/templates/table-page-shoe-store-heatmap.tsx +748 -746
- package/templates/table-page.template.mjs +5 -4
- package/templates/table-page.tsx +396 -405
- package/templates/table.template.mjs +5 -4
- package/templates/table.tsx +92 -93
- package/templates/tech-report.template.mjs +5 -4
- package/templates/tech-report.tsx +1316 -1304
- package/templates/theme-showcase.template.mjs +5 -4
- package/templates/theme-showcase.tsx +871 -951
- package/theme.css +35 -77
- package/theme.css.d.ts +9 -0
- package/tokens.css +373 -380
|
@@ -1,62 +1,62 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
-
// XLE (canonical structure
|
|
3
|
-
//
|
|
2
|
+
// XLE (canonical structure of the tree below; the CLI had no XLE validator
|
|
3
|
+
// after `astryx layout` was removed in 0.7.0, so this is a reading aid and a
|
|
4
|
+
// human drift check — update it with the file, never alone):
|
|
5
|
+
// L > (LP[w=68] > V[g=2] > Av + IB*4 + SI[fill] + IB"Settings") + (LP[w=260] > V > (H[g=2] > (SI[fill] > Hd"Messages"[level=1]) + IB"New message") + (V[g=0] > TI"Jump to…") + (SI[fill] > (UL[density=compact edge=flush !hasDividers] > (Tx"Channels"[t=label] + LI*4)) + (UL[density=compact edge=flush !hasDividers] > (Tx"Direct messages"[t=label] + LI*3)))) + (LC[p=0] > V > (H[g=3] > Ic + Hd"design-systems"[level=2] + (SI[fill] > Tx"Component APIs, tokens, and release coordination"[t=body]) + SD"12 online" + IB"Members") + D + (SI[fill] > ChL > (ChML[density=balanced] > ChSM"Tuesday, June 30" + ChSM"Sasha Ortiz joined #design-systems" + (ChM > ChBu) + (ChM > ChBu*2) + (ChM > ChBu*2) + (ChM > ChBu) + (ChM > ChBu)) + ChC"Message #design-systems" + EmptyState"No messages yet")) + (LP[w=340] > V > (H[g=2] > (SI[fill] > (H[g=2] > Tx"Thread"[weight=semibold] + Tx"#design-systems"[t=supporting])) + IB"Close thread") + D + (SI[fill] > ChML[density=compact] > (ChM > ChBu) + ChSM"2 replies" + (ChM > ChBu)*2) + (V[g=0] > ChC"Reply in thread…")))
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* Messaging Shell — Slack-style column frame for team messaging tools. Frame (desktop, left to right): (Frame/responsive/container: see XLE header above.)
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
+
import { Avatar, AvatarStatusDot } from "@astryxdesign/core/Avatar";
|
|
12
|
+
import { Badge } from "@astryxdesign/core/Badge";
|
|
11
13
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
} from
|
|
20
|
-
import {
|
|
14
|
+
ChatComposer,
|
|
15
|
+
ChatLayout,
|
|
16
|
+
ChatMessage,
|
|
17
|
+
ChatMessageBubble,
|
|
18
|
+
ChatMessageList,
|
|
19
|
+
ChatMessageMetadata,
|
|
20
|
+
ChatSystemMessage,
|
|
21
|
+
} from "@astryxdesign/core/Chat";
|
|
22
|
+
import { Divider } from "@astryxdesign/core/Divider";
|
|
23
|
+
import { EmptyState } from "@astryxdesign/core/EmptyState";
|
|
24
|
+
import { useMediaQuery } from "@astryxdesign/core/hooks";
|
|
25
|
+
import { Icon } from "@astryxdesign/core/Icon";
|
|
26
|
+
import { IconButton } from "@astryxdesign/core/IconButton";
|
|
21
27
|
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
} from
|
|
30
|
-
import {
|
|
31
|
-
import {StatusDot} from
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
35
|
-
import {TextInput} from '@astryxdesign/core/TextInput';
|
|
36
|
-
import {Timestamp} from '@astryxdesign/core/Timestamp';
|
|
37
|
-
import {EmptyState} from '@astryxdesign/core/EmptyState';
|
|
38
|
-
import {Icon} from '@astryxdesign/core/Icon';
|
|
39
|
-
import {IconButton} from '@astryxdesign/core/IconButton';
|
|
40
|
-
import {useMediaQuery} from '@astryxdesign/core/hooks';
|
|
41
|
-
|
|
28
|
+
HStack,
|
|
29
|
+
Layout,
|
|
30
|
+
LayoutContent,
|
|
31
|
+
LayoutPanel,
|
|
32
|
+
Stack,
|
|
33
|
+
StackItem,
|
|
34
|
+
VStack,
|
|
35
|
+
} from "@astryxdesign/core/Layout";
|
|
36
|
+
import { List, ListItem } from "@astryxdesign/core/List";
|
|
37
|
+
import { StatusDot } from "@astryxdesign/core/StatusDot";
|
|
38
|
+
import { Heading, Text } from "@astryxdesign/core/Text";
|
|
39
|
+
import { TextInput } from "@astryxdesign/core/TextInput";
|
|
40
|
+
import { Timestamp } from "@astryxdesign/core/Timestamp";
|
|
42
41
|
// ============= ICONS (verified lucide-react exports) =============
|
|
43
42
|
// Bell ← BellIcon, Bookmark ← BookmarkIcon, MessagesSquare ← ChatBubbleLeftRightIcon,
|
|
44
43
|
// Settings ← Cog6ToothIcon, Hash ← HashtagIcon, House ← HomeIcon, Inbox ← InboxIcon,
|
|
45
44
|
// Search ← MagnifyingGlassIcon, SquarePen ← PencilSquareIcon, Users ← UserGroupIcon,
|
|
46
45
|
// X ← XMarkIcon.
|
|
47
46
|
import {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
} from
|
|
47
|
+
Bell,
|
|
48
|
+
Bookmark,
|
|
49
|
+
Hash,
|
|
50
|
+
House,
|
|
51
|
+
Inbox,
|
|
52
|
+
MessagesSquare,
|
|
53
|
+
Search,
|
|
54
|
+
Settings,
|
|
55
|
+
SquarePen,
|
|
56
|
+
Users,
|
|
57
|
+
X,
|
|
58
|
+
} from "lucide-react";
|
|
59
|
+
import { type CSSProperties, type ReactNode, useState } from "react";
|
|
60
60
|
|
|
61
61
|
// ---------------------------------------------------------------------------
|
|
62
62
|
// Styles — semantic tokens only. Layout/Stack own their padding via props,
|
|
@@ -66,360 +66,350 @@ import {
|
|
|
66
66
|
|
|
67
67
|
// Fill the window: Layout height="fill" is height:100%, which only resolves
|
|
68
68
|
// against a definite height — the host's <html>/<body> don't set one.
|
|
69
|
-
const rootStyle: CSSProperties = {height:
|
|
69
|
+
const rootStyle: CSSProperties = { height: "100dvh", width: "100%" };
|
|
70
70
|
// Workspace rail: full height, centered, ring-safe vertical padding.
|
|
71
71
|
const railStyle: CSSProperties = {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
height: "100%",
|
|
73
|
+
alignItems: "center",
|
|
74
|
+
paddingTop: "var(--spacing-3)",
|
|
75
|
+
paddingBottom: "var(--spacing-3)",
|
|
76
76
|
};
|
|
77
77
|
// Sidebar frame: full height, scrolls internally.
|
|
78
|
-
const sidebarStyle: CSSProperties = {height:
|
|
78
|
+
const sidebarStyle: CSSProperties = { height: "100%", minHeight: 0 };
|
|
79
79
|
const sidebarHeaderStyle: CSSProperties = {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
alignItems: "center",
|
|
81
|
+
paddingInline: "var(--spacing-3)",
|
|
82
|
+
paddingBlock: "var(--spacing-3)",
|
|
83
83
|
};
|
|
84
84
|
const sidebarSearchStyle: CSSProperties = {
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
paddingInline: "var(--spacing-3)",
|
|
86
|
+
paddingBottom: "var(--spacing-2)",
|
|
87
87
|
};
|
|
88
88
|
// Block-start only: the inline (8px) and bottom (12px) edges already clear.
|
|
89
89
|
// INLINE is a content gutter matching sidebarHeader/sidebarSearch (12px);
|
|
90
90
|
// BLOCK-START is a 4px ring budget (offset 2 + thickness 2). Not redundant.
|
|
91
91
|
const sidebarScrollStyle: CSSProperties = {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
92
|
+
minHeight: 0,
|
|
93
|
+
overflowY: "auto",
|
|
94
|
+
paddingInline: "var(--spacing-3)",
|
|
95
|
+
paddingBlockStart: "var(--spacing-1)",
|
|
96
|
+
paddingBottom: "var(--spacing-3)",
|
|
97
97
|
};
|
|
98
98
|
// DM section gap: List is the row container, so the gap rides on the second
|
|
99
99
|
// List's own margin instead of a wrapper element.
|
|
100
|
-
const sectionGapStyle: CSSProperties = {marginTop:
|
|
100
|
+
const sectionGapStyle: CSSProperties = { marginTop: "var(--spacing-4)" };
|
|
101
101
|
// Stream column: full height, scrolls internally.
|
|
102
|
-
const streamColumnStyle: CSSProperties = {height:
|
|
102
|
+
const streamColumnStyle: CSSProperties = { height: "100%", minHeight: 0 };
|
|
103
103
|
const streamHeaderStyle: CSSProperties = {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
alignItems: "center",
|
|
105
|
+
paddingInline: "var(--spacing-4)",
|
|
106
|
+
paddingBlock: "var(--spacing-3)",
|
|
107
107
|
};
|
|
108
108
|
// Truncation floor: the topic StackItem must shrink before the header does.
|
|
109
|
-
const streamTopicStyle: CSSProperties = {minWidth: 0};
|
|
109
|
+
const streamTopicStyle: CSSProperties = { minWidth: 0 };
|
|
110
110
|
const chatAreaStyle: CSSProperties = {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
minHeight: 0,
|
|
112
|
+
display: "flex",
|
|
113
|
+
flexDirection: "column",
|
|
114
114
|
};
|
|
115
|
-
const chatFillStyle: CSSProperties = {flex: 1, minHeight: 0};
|
|
115
|
+
const chatFillStyle: CSSProperties = { flex: 1, minHeight: 0 };
|
|
116
116
|
// Thread column: full height, scrolls internally.
|
|
117
|
-
const threadColumnStyle: CSSProperties = {height:
|
|
117
|
+
const threadColumnStyle: CSSProperties = { height: "100%", minHeight: 0 };
|
|
118
118
|
const threadHeaderStyle: CSSProperties = {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
alignItems: "center",
|
|
120
|
+
paddingInline: "var(--spacing-3)",
|
|
121
|
+
paddingBlock: "var(--spacing-3)",
|
|
122
122
|
};
|
|
123
123
|
const threadScrollStyle: CSSProperties = {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
124
|
+
minHeight: 0,
|
|
125
|
+
overflowY: "auto",
|
|
126
|
+
paddingInline: "var(--spacing-3)",
|
|
127
|
+
paddingBlock: "var(--spacing-3)",
|
|
128
128
|
};
|
|
129
|
-
const threadComposerStyle: CSSProperties = {padding:
|
|
129
|
+
const threadComposerStyle: CSSProperties = { padding: "var(--spacing-3)" };
|
|
130
130
|
|
|
131
131
|
// ---------------------------------------------------------------------------
|
|
132
132
|
// Deterministic fixtures — fixed ISO timestamps, stable ordering.
|
|
133
133
|
// ---------------------------------------------------------------------------
|
|
134
134
|
|
|
135
|
-
type Presence =
|
|
135
|
+
type Presence = "online" | "busy" | "offline";
|
|
136
136
|
|
|
137
137
|
interface User {
|
|
138
|
-
|
|
138
|
+
name: string;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
const USERS: Record<string, User> = {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
142
|
+
mira: { name: "Mira Chen" },
|
|
143
|
+
devon: { name: "Devon Park" },
|
|
144
|
+
sasha: { name: "Sasha Ortiz" },
|
|
145
|
+
you: { name: "Riley Quinn" },
|
|
146
146
|
};
|
|
147
147
|
|
|
148
148
|
interface Channel {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
149
|
+
id: string;
|
|
150
|
+
name: string;
|
|
151
|
+
topic: string;
|
|
152
|
+
unread: number;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
const CHANNELS: Channel[] = [
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
156
|
+
{
|
|
157
|
+
id: "design-systems",
|
|
158
|
+
name: "design-systems",
|
|
159
|
+
topic: "Component APIs, tokens, and release coordination",
|
|
160
|
+
unread: 0,
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
id: "frontend-guild",
|
|
164
|
+
name: "frontend-guild",
|
|
165
|
+
topic: "Cross-team frontend practices",
|
|
166
|
+
unread: 4,
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
id: "releases",
|
|
170
|
+
name: "releases",
|
|
171
|
+
topic: "Release announcements and rollbacks",
|
|
172
|
+
unread: 12,
|
|
173
|
+
},
|
|
174
|
+
{ id: "random", name: "random", topic: "Everything else", unread: 0 },
|
|
175
175
|
];
|
|
176
176
|
|
|
177
177
|
interface DirectMessage {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
178
|
+
id: string;
|
|
179
|
+
userId: string;
|
|
180
|
+
presence: Presence;
|
|
181
|
+
unread: number;
|
|
182
182
|
}
|
|
183
183
|
|
|
184
184
|
const DIRECT_MESSAGES: DirectMessage[] = [
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
185
|
+
{ id: "dm-mira", userId: "mira", presence: "online", unread: 0 },
|
|
186
|
+
{ id: "dm-devon", userId: "devon", presence: "busy", unread: 2 },
|
|
187
|
+
{ id: "dm-sasha", userId: "sasha", presence: "offline", unread: 0 },
|
|
188
188
|
];
|
|
189
189
|
|
|
190
190
|
// AvatarStatusDot supports success | neutral | error (no warning): busy stays
|
|
191
191
|
// error (do-not-disturb), never warning. One config — variant and label travel
|
|
192
192
|
// together, so the dot and its accessible name can't disagree.
|
|
193
|
-
const PRESENCE: Record<
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
online: {variant: 'success', label: 'Online'},
|
|
198
|
-
busy: {variant: 'error', label: 'Busy'},
|
|
199
|
-
offline: {variant: 'neutral', label: 'Offline'},
|
|
193
|
+
const PRESENCE: Record<Presence, { variant: "success" | "error" | "neutral"; label: string }> = {
|
|
194
|
+
online: { variant: "success", label: "Online" },
|
|
195
|
+
busy: { variant: "error", label: "Busy" },
|
|
196
|
+
offline: { variant: "neutral", label: "Offline" },
|
|
200
197
|
};
|
|
201
198
|
|
|
202
199
|
/** One bubble's copy. Ids key the bubble; its position only sets the grouping. */
|
|
203
200
|
interface StreamBubble {
|
|
204
|
-
|
|
205
|
-
|
|
201
|
+
id: string;
|
|
202
|
+
text: string;
|
|
206
203
|
}
|
|
207
204
|
|
|
208
205
|
/** One message group: consecutive bubbles from the same sender. */
|
|
209
206
|
interface StreamMessage {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
207
|
+
id: string;
|
|
208
|
+
userId: string;
|
|
209
|
+
time: string;
|
|
210
|
+
bubbles: StreamBubble[];
|
|
214
211
|
}
|
|
215
212
|
|
|
216
213
|
const MESSAGES_BY_CHANNEL: Record<string, StreamMessage[]> = {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
214
|
+
"design-systems": [
|
|
215
|
+
{
|
|
216
|
+
id: "m1",
|
|
217
|
+
userId: "mira",
|
|
218
|
+
time: "2026-06-30T09:12:00",
|
|
219
|
+
bubbles: [
|
|
220
|
+
{
|
|
221
|
+
id: "m1-1",
|
|
222
|
+
text: "Morning! The Timestamp component now supports a `system_date` format, worth switching the audit log over.",
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
id: "m1-2",
|
|
226
|
+
text: "I put the migration notes in the wiki under Decisions.",
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
id: "m2",
|
|
232
|
+
userId: "devon",
|
|
233
|
+
time: "2026-06-30T09:15:00",
|
|
234
|
+
bubbles: [
|
|
235
|
+
{
|
|
236
|
+
id: "m2-1",
|
|
237
|
+
text: "Nice. Does that unblock the incident console timeline work?",
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
id: "m3",
|
|
243
|
+
userId: "you",
|
|
244
|
+
time: "2026-06-30T09:17:00",
|
|
245
|
+
bubbles: [
|
|
246
|
+
{
|
|
247
|
+
id: "m3-1",
|
|
248
|
+
text: "It does. I will pick that up after the template review.",
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
id: "m3-2",
|
|
252
|
+
text: "One question on the List density defaults, will start a thread.",
|
|
253
|
+
},
|
|
254
|
+
],
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
id: "m4",
|
|
258
|
+
userId: "sasha",
|
|
259
|
+
time: "2026-06-30T09:24:00",
|
|
260
|
+
bubbles: [
|
|
261
|
+
{
|
|
262
|
+
id: "m4-1",
|
|
263
|
+
text: "Heads up: the token sync job ran clean overnight, no drift between core and the theme packages.",
|
|
264
|
+
},
|
|
265
|
+
],
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
id: "m5",
|
|
269
|
+
userId: "mira",
|
|
270
|
+
time: "2026-06-30T09:26:00",
|
|
271
|
+
bubbles: [{ id: "m5-1", text: "Great, closing out the drift task then." }],
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
"frontend-guild": [
|
|
275
|
+
{
|
|
276
|
+
id: "g1",
|
|
277
|
+
userId: "devon",
|
|
278
|
+
time: "2026-06-30T08:40:00",
|
|
279
|
+
bubbles: [
|
|
280
|
+
{
|
|
281
|
+
id: "g1-1",
|
|
282
|
+
text: "Guild sync moved to Thursday this week to avoid the release freeze.",
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
id: "g2",
|
|
288
|
+
userId: "you",
|
|
289
|
+
time: "2026-06-30T08:44:00",
|
|
290
|
+
bubbles: [{ id: "g2-1", text: "Works for me, agenda doc is updated." }],
|
|
291
|
+
},
|
|
292
|
+
],
|
|
296
293
|
};
|
|
297
294
|
|
|
298
295
|
interface ThreadReply {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
296
|
+
id: string;
|
|
297
|
+
userId: string;
|
|
298
|
+
time: string;
|
|
299
|
+
text: string;
|
|
303
300
|
}
|
|
304
301
|
|
|
305
302
|
// One map: the root plus its replies. [0] is the opener, the rest are replies —
|
|
306
303
|
// one lookup for the divider count and one slice for the reply list.
|
|
307
304
|
const THREAD: ThreadReply[] = [
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
305
|
+
{
|
|
306
|
+
id: "t0",
|
|
307
|
+
userId: "you",
|
|
308
|
+
time: "2026-06-30T09:17:30",
|
|
309
|
+
text: "One question on the List density defaults: should channel sidebars use compact or balanced? The spec shows both.",
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
id: "t1",
|
|
313
|
+
userId: "mira",
|
|
314
|
+
time: "2026-06-30T09:19:00",
|
|
315
|
+
text: "Compact for navigation surfaces. Balanced is for content lists where descriptions carry weight.",
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
id: "t2",
|
|
319
|
+
userId: "devon",
|
|
320
|
+
time: "2026-06-30T09:21:00",
|
|
321
|
+
text: "Agreed, the sidebar rows here are a good reference implementation.",
|
|
322
|
+
},
|
|
326
323
|
];
|
|
327
324
|
|
|
328
325
|
const RAIL_ITEMS = [
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
326
|
+
{ id: "home", label: "Home", icon: House },
|
|
327
|
+
{ id: "dms", label: "Direct messages", icon: MessagesSquare },
|
|
328
|
+
{ id: "activity", label: "Activity", icon: Bell },
|
|
329
|
+
{ id: "saved", label: "Saved items", icon: Bookmark },
|
|
333
330
|
];
|
|
334
331
|
|
|
335
332
|
// One thread message. The root carries the divider above the replies, so the
|
|
336
|
-
function ThreadMessage({reply}: {reply: ThreadReply}) {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
</ChatMessageBubble>
|
|
350
|
-
</ChatMessage>
|
|
351
|
-
);
|
|
333
|
+
function ThreadMessage({ reply }: { reply: ThreadReply }) {
|
|
334
|
+
return (
|
|
335
|
+
<ChatMessage sender="assistant" avatar={<Avatar name={USERS[reply.userId].name} size="md" />}>
|
|
336
|
+
<ChatMessageBubble
|
|
337
|
+
name={USERS[reply.userId].name}
|
|
338
|
+
metadata={
|
|
339
|
+
<ChatMessageMetadata timestamp={<Timestamp value={reply.time} format="time" />} />
|
|
340
|
+
}
|
|
341
|
+
>
|
|
342
|
+
{reply.text}
|
|
343
|
+
</ChatMessageBubble>
|
|
344
|
+
</ChatMessage>
|
|
345
|
+
);
|
|
352
346
|
}
|
|
353
347
|
|
|
354
348
|
// One nav row factory for the sidebar lists. Search filtering stays at the
|
|
355
349
|
// call sites (visibleChannels/visibleDms); unread badges stay per-row. Only
|
|
356
350
|
// the ListItem shape is shared.
|
|
357
351
|
function NavRow({
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
352
|
+
label,
|
|
353
|
+
isSelected,
|
|
354
|
+
onSelect,
|
|
355
|
+
startContent,
|
|
356
|
+
unread,
|
|
363
357
|
}: {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
358
|
+
label: string;
|
|
359
|
+
isSelected: boolean;
|
|
360
|
+
onSelect: () => void;
|
|
361
|
+
startContent: ReactNode;
|
|
362
|
+
unread: number;
|
|
369
363
|
}) {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
) : undefined
|
|
380
|
-
}
|
|
381
|
-
/>
|
|
382
|
-
);
|
|
364
|
+
return (
|
|
365
|
+
<ListItem
|
|
366
|
+
label={label}
|
|
367
|
+
isSelected={isSelected}
|
|
368
|
+
onClick={onSelect}
|
|
369
|
+
startContent={startContent}
|
|
370
|
+
endContent={unread > 0 ? <Badge label={String(unread)} variant="neutral" /> : undefined}
|
|
371
|
+
/>
|
|
372
|
+
);
|
|
383
373
|
}
|
|
384
374
|
// ---------------------------------------------------------------------------
|
|
385
375
|
// Stream message group — avatar + name on the first bubble, timestamp on the
|
|
386
376
|
// last, `group` positions tighten corner radii between consecutive bubbles.
|
|
387
377
|
// ---------------------------------------------------------------------------
|
|
388
378
|
|
|
389
|
-
function StreamMessageGroup({message}: {message: StreamMessage}) {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
379
|
+
function StreamMessageGroup({ message }: { message: StreamMessage }) {
|
|
380
|
+
const isSelf = message.userId === "you";
|
|
381
|
+
const user = USERS[message.userId];
|
|
382
|
+
const lastIndex = message.bubbles.length - 1;
|
|
383
|
+
|
|
384
|
+
return (
|
|
385
|
+
<ChatMessage
|
|
386
|
+
sender={isSelf ? "user" : "assistant"}
|
|
387
|
+
avatar={isSelf ? undefined : <Avatar name={user.name} size="md" />}
|
|
388
|
+
>
|
|
389
|
+
{message.bubbles.map((bubble, index) => (
|
|
390
|
+
<ChatMessageBubble
|
|
391
|
+
key={bubble.id}
|
|
392
|
+
group={
|
|
393
|
+
message.bubbles.length === 1
|
|
394
|
+
? undefined
|
|
395
|
+
: index === 0
|
|
396
|
+
? "first"
|
|
397
|
+
: index === lastIndex
|
|
398
|
+
? "last"
|
|
399
|
+
: "middle"
|
|
400
|
+
}
|
|
401
|
+
name={!isSelf && index === 0 ? user.name : undefined}
|
|
402
|
+
metadata={
|
|
403
|
+
index === lastIndex ? (
|
|
404
|
+
<ChatMessageMetadata timestamp={<Timestamp value={message.time} format="time" />} />
|
|
405
|
+
) : undefined
|
|
406
|
+
}
|
|
407
|
+
>
|
|
408
|
+
{bubble.text}
|
|
409
|
+
</ChatMessageBubble>
|
|
410
|
+
))}
|
|
411
|
+
</ChatMessage>
|
|
412
|
+
);
|
|
423
413
|
}
|
|
424
414
|
|
|
425
415
|
// ---------------------------------------------------------------------------
|
|
@@ -427,272 +417,264 @@ function StreamMessageGroup({message}: {message: StreamMessage}) {
|
|
|
427
417
|
// ---------------------------------------------------------------------------
|
|
428
418
|
|
|
429
419
|
export default function MessagingShell() {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
420
|
+
const [selectedChannelId, setSelectedChannelId] = useState("design-systems");
|
|
421
|
+
const [selectedDmId, setSelectedDmId] = useState<string | null>(null);
|
|
422
|
+
const [isThreadOpen, setIsThreadOpen] = useState(true);
|
|
423
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
424
|
+
|
|
425
|
+
// Responsive contract (see file header).
|
|
426
|
+
const isThreadHidden = useMediaQuery("(max-width: 1024px)");
|
|
427
|
+
const isSidebarHidden = useMediaQuery("(max-width: 768px)");
|
|
428
|
+
|
|
429
|
+
const selectedChannel =
|
|
430
|
+
CHANNELS.find((channel) => channel.id === selectedChannelId) ?? CHANNELS[0];
|
|
431
|
+
const messages = MESSAGES_BY_CHANNEL[selectedChannel.id] ?? [];
|
|
432
|
+
|
|
433
|
+
const normalizedQuery = searchQuery.trim().toLowerCase();
|
|
434
|
+
const visibleChannels = CHANNELS.filter((channel) =>
|
|
435
|
+
channel.name.toLowerCase().includes(normalizedQuery),
|
|
436
|
+
);
|
|
437
|
+
const visibleDms = DIRECT_MESSAGES.filter((dm) =>
|
|
438
|
+
USERS[dm.userId].name.toLowerCase().includes(normalizedQuery),
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
const showThreadPanel = isThreadOpen && !isThreadHidden;
|
|
442
|
+
|
|
443
|
+
const workspaceRail = (
|
|
444
|
+
<VStack gap={2} style={railStyle}>
|
|
445
|
+
<Avatar name="Night watch" size="md" />
|
|
446
|
+
{RAIL_ITEMS.map((item) => (
|
|
447
|
+
<IconButton
|
|
448
|
+
key={item.id}
|
|
449
|
+
label={item.label}
|
|
450
|
+
tooltip={item.label}
|
|
451
|
+
icon={<Icon icon={item.icon} size="sm" color="inherit" />}
|
|
452
|
+
variant={item.id === "home" ? "secondary" : "ghost"}
|
|
453
|
+
onClick={() => {}}
|
|
454
|
+
/>
|
|
455
|
+
))}
|
|
456
|
+
<StackItem size="fill" />
|
|
457
|
+
<IconButton
|
|
458
|
+
label="Settings"
|
|
459
|
+
tooltip="Settings"
|
|
460
|
+
icon={<Icon icon={Settings} size="sm" color="inherit" />}
|
|
461
|
+
variant="ghost"
|
|
462
|
+
onClick={() => {}}
|
|
463
|
+
/>
|
|
464
|
+
</VStack>
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
const channelSidebar = (
|
|
468
|
+
<Stack direction="vertical" style={sidebarStyle}>
|
|
469
|
+
<HStack gap={2} style={sidebarHeaderStyle}>
|
|
470
|
+
<StackItem size="fill">
|
|
471
|
+
<Heading level={1}>Messages</Heading>
|
|
472
|
+
</StackItem>
|
|
473
|
+
<IconButton
|
|
474
|
+
label="New message"
|
|
475
|
+
tooltip="New message"
|
|
476
|
+
icon={<Icon icon={SquarePen} size="sm" color="inherit" />}
|
|
477
|
+
variant="ghost"
|
|
478
|
+
size="sm"
|
|
479
|
+
onClick={() => {}}
|
|
480
|
+
/>
|
|
481
|
+
</HStack>
|
|
482
|
+
<VStack gap={0} style={sidebarSearchStyle}>
|
|
483
|
+
<TextInput
|
|
484
|
+
label="Jump to"
|
|
485
|
+
isLabelHidden
|
|
486
|
+
size="sm"
|
|
487
|
+
placeholder="Jump to…"
|
|
488
|
+
startIcon={Search}
|
|
489
|
+
value={searchQuery}
|
|
490
|
+
onChange={setSearchQuery}
|
|
491
|
+
/>
|
|
492
|
+
</VStack>
|
|
493
|
+
<StackItem
|
|
494
|
+
size="fill"
|
|
495
|
+
style={sidebarScrollStyle}
|
|
496
|
+
role="region"
|
|
497
|
+
aria-label="Channels and direct messages"
|
|
498
|
+
tabIndex={0}
|
|
499
|
+
>
|
|
500
|
+
<List
|
|
501
|
+
density="compact"
|
|
502
|
+
edge="flush"
|
|
503
|
+
hasDividers={false}
|
|
504
|
+
header={
|
|
505
|
+
<Text type="label" size="sm" color="secondary">
|
|
506
|
+
Channels
|
|
507
|
+
</Text>
|
|
508
|
+
}
|
|
509
|
+
>
|
|
510
|
+
{visibleChannels.map((channel) => (
|
|
511
|
+
<NavRow
|
|
512
|
+
key={channel.id}
|
|
513
|
+
label={channel.name}
|
|
514
|
+
isSelected={selectedDmId === null && channel.id === selectedChannelId}
|
|
515
|
+
onSelect={() => {
|
|
516
|
+
setSelectedChannelId(channel.id);
|
|
517
|
+
setSelectedDmId(null);
|
|
518
|
+
}}
|
|
519
|
+
startContent={<Icon icon={Hash} size="sm" color="secondary" />}
|
|
520
|
+
unread={channel.unread}
|
|
521
|
+
/>
|
|
522
|
+
))}
|
|
523
|
+
</List>
|
|
524
|
+
{/* The section gap rides on this List's own margin: List is the row
|
|
534
525
|
container, so no wrapper element is needed. */}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
showThreadPanel ? (
|
|
690
|
-
<LayoutPanel width={340} padding={0}>
|
|
691
|
-
{threadPanel}
|
|
692
|
-
</LayoutPanel>
|
|
693
|
-
) : undefined
|
|
694
|
-
}
|
|
695
|
-
content={<LayoutContent padding={0}>{messageStream}</LayoutContent>}
|
|
696
|
-
/>
|
|
697
|
-
);
|
|
526
|
+
<List
|
|
527
|
+
density="compact"
|
|
528
|
+
edge="flush"
|
|
529
|
+
hasDividers={false}
|
|
530
|
+
style={sectionGapStyle}
|
|
531
|
+
header={
|
|
532
|
+
<Text type="label" size="sm" color="secondary">
|
|
533
|
+
Direct messages
|
|
534
|
+
</Text>
|
|
535
|
+
}
|
|
536
|
+
>
|
|
537
|
+
{visibleDms.map((dm) => (
|
|
538
|
+
<NavRow
|
|
539
|
+
key={dm.id}
|
|
540
|
+
label={USERS[dm.userId].name}
|
|
541
|
+
isSelected={selectedDmId === dm.id}
|
|
542
|
+
onSelect={() => setSelectedDmId(dm.id)}
|
|
543
|
+
startContent={
|
|
544
|
+
<Avatar
|
|
545
|
+
name={USERS[dm.userId].name}
|
|
546
|
+
size="sm"
|
|
547
|
+
status={
|
|
548
|
+
<AvatarStatusDot
|
|
549
|
+
variant={PRESENCE[dm.presence].variant}
|
|
550
|
+
label={PRESENCE[dm.presence].label}
|
|
551
|
+
/>
|
|
552
|
+
}
|
|
553
|
+
/>
|
|
554
|
+
}
|
|
555
|
+
unread={dm.unread}
|
|
556
|
+
/>
|
|
557
|
+
))}
|
|
558
|
+
</List>
|
|
559
|
+
</StackItem>
|
|
560
|
+
</Stack>
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
const messageStream = (
|
|
564
|
+
<Stack direction="vertical" style={streamColumnStyle}>
|
|
565
|
+
<HStack gap={3} style={streamHeaderStyle}>
|
|
566
|
+
<Icon icon={Hash} size="sm" color="secondary" />
|
|
567
|
+
<Heading level={2}>{selectedChannel.name}</Heading>
|
|
568
|
+
<StackItem size="fill" style={streamTopicStyle}>
|
|
569
|
+
<Text type="body" color="secondary" maxLines={1}>
|
|
570
|
+
{selectedChannel.topic}
|
|
571
|
+
</Text>
|
|
572
|
+
</StackItem>
|
|
573
|
+
<StatusDot variant="success" label="12 online" />
|
|
574
|
+
<IconButton
|
|
575
|
+
label="Members"
|
|
576
|
+
tooltip="Members"
|
|
577
|
+
icon={<Icon icon={Users} size="sm" color="inherit" />}
|
|
578
|
+
variant="ghost"
|
|
579
|
+
size="sm"
|
|
580
|
+
onClick={() => {}}
|
|
581
|
+
/>
|
|
582
|
+
</HStack>
|
|
583
|
+
<Divider />
|
|
584
|
+
<StackItem size="fill" style={chatAreaStyle}>
|
|
585
|
+
<ChatLayout
|
|
586
|
+
style={chatFillStyle}
|
|
587
|
+
composer={
|
|
588
|
+
<ChatComposer placeholder={`Message #${selectedChannel.name}`} onSubmit={() => {}} />
|
|
589
|
+
}
|
|
590
|
+
emptyState={
|
|
591
|
+
<EmptyState
|
|
592
|
+
icon={<Icon icon={Inbox} size="lg" color="secondary" />}
|
|
593
|
+
title="No messages yet"
|
|
594
|
+
description="Start the conversation. Messages posted here are visible to the whole channel."
|
|
595
|
+
/>
|
|
596
|
+
}
|
|
597
|
+
>
|
|
598
|
+
{messages.length > 0 ? (
|
|
599
|
+
<ChatMessageList density="balanced">
|
|
600
|
+
<ChatSystemMessage variant="divider">Tuesday, June 30</ChatSystemMessage>
|
|
601
|
+
<ChatSystemMessage>Sasha Ortiz joined #{selectedChannel.name}</ChatSystemMessage>
|
|
602
|
+
{messages.map((message) => (
|
|
603
|
+
<StreamMessageGroup key={message.id} message={message} />
|
|
604
|
+
))}
|
|
605
|
+
</ChatMessageList>
|
|
606
|
+
) : null}
|
|
607
|
+
</ChatLayout>
|
|
608
|
+
</StackItem>
|
|
609
|
+
</Stack>
|
|
610
|
+
);
|
|
611
|
+
|
|
612
|
+
const threadPanel = (
|
|
613
|
+
<Stack direction="vertical" style={threadColumnStyle}>
|
|
614
|
+
<HStack gap={2} style={threadHeaderStyle}>
|
|
615
|
+
<StackItem size="fill">
|
|
616
|
+
<HStack gap={2} style={{ alignItems: "baseline" }}>
|
|
617
|
+
<Text weight="semibold">Thread</Text>
|
|
618
|
+
<Text type="supporting" color="secondary">
|
|
619
|
+
#{selectedChannel.name}
|
|
620
|
+
</Text>
|
|
621
|
+
</HStack>
|
|
622
|
+
</StackItem>
|
|
623
|
+
<IconButton
|
|
624
|
+
label="Close thread"
|
|
625
|
+
tooltip="Close thread"
|
|
626
|
+
icon={<Icon icon={X} size="sm" color="inherit" />}
|
|
627
|
+
variant="ghost"
|
|
628
|
+
size="sm"
|
|
629
|
+
onClick={() => setIsThreadOpen(false)}
|
|
630
|
+
/>
|
|
631
|
+
</HStack>
|
|
632
|
+
<Divider />
|
|
633
|
+
<StackItem
|
|
634
|
+
size="fill"
|
|
635
|
+
style={threadScrollStyle}
|
|
636
|
+
role="region"
|
|
637
|
+
aria-label="Thread messages"
|
|
638
|
+
tabIndex={0}
|
|
639
|
+
>
|
|
640
|
+
<ChatMessageList density="compact">
|
|
641
|
+
<ThreadMessage reply={THREAD[0]} />
|
|
642
|
+
<ChatSystemMessage variant="divider">{THREAD.length - 1} replies</ChatSystemMessage>
|
|
643
|
+
{THREAD.slice(1).map((reply) => (
|
|
644
|
+
<ThreadMessage key={reply.id} reply={reply} />
|
|
645
|
+
))}
|
|
646
|
+
</ChatMessageList>
|
|
647
|
+
</StackItem>
|
|
648
|
+
<VStack gap={0} style={threadComposerStyle}>
|
|
649
|
+
<ChatComposer density="compact" placeholder="Reply in thread…" onSubmit={() => {}} />
|
|
650
|
+
</VStack>
|
|
651
|
+
</Stack>
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
return (
|
|
655
|
+
<Layout
|
|
656
|
+
height="fill"
|
|
657
|
+
style={rootStyle}
|
|
658
|
+
start={
|
|
659
|
+
<>
|
|
660
|
+
<LayoutPanel width={68} padding={0}>
|
|
661
|
+
{workspaceRail}
|
|
662
|
+
</LayoutPanel>
|
|
663
|
+
{!isSidebarHidden && (
|
|
664
|
+
<LayoutPanel width={260} padding={0}>
|
|
665
|
+
{channelSidebar}
|
|
666
|
+
</LayoutPanel>
|
|
667
|
+
)}
|
|
668
|
+
</>
|
|
669
|
+
}
|
|
670
|
+
end={
|
|
671
|
+
showThreadPanel ? (
|
|
672
|
+
<LayoutPanel width={340} padding={0}>
|
|
673
|
+
{threadPanel}
|
|
674
|
+
</LayoutPanel>
|
|
675
|
+
) : undefined
|
|
676
|
+
}
|
|
677
|
+
content={<LayoutContent padding={0}>{messageStream}</LayoutContent>}
|
|
678
|
+
/>
|
|
679
|
+
);
|
|
698
680
|
}
|