astryx-dracula 0.1.3 → 0.2.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 (94) hide show
  1. package/BRAND.md +1 -1
  2. package/README.md +2 -0
  3. package/USAGE.md +13 -0
  4. package/astryx-dracula.js +4 -4
  5. package/astryx.integration.mjs +3 -0
  6. package/package.json +4 -2
  7. package/templates/ai-chat-landing.template.mjs +6 -0
  8. package/templates/ai-chat-landing.tsx +476 -0
  9. package/templates/ai-chat.template.mjs +6 -0
  10. package/templates/ai-chat.tsx +762 -0
  11. package/templates/blank.template.mjs +6 -0
  12. package/templates/blank.tsx +16 -0
  13. package/templates/centered-hero.template.mjs +6 -0
  14. package/templates/centered-hero.tsx +188 -0
  15. package/templates/classic-gallery.template.mjs +6 -0
  16. package/templates/classic-gallery.tsx +181 -0
  17. package/templates/contact-form.template.mjs +6 -0
  18. package/templates/contact-form.tsx +292 -0
  19. package/templates/dashboard-portfolio.template.mjs +6 -0
  20. package/templates/dashboard-portfolio.tsx +800 -0
  21. package/templates/dashboard.template.mjs +6 -0
  22. package/templates/dashboard.tsx +774 -0
  23. package/templates/detail-page.template.mjs +6 -0
  24. package/templates/detail-page.tsx +695 -0
  25. package/templates/documentation-design.template.mjs +6 -0
  26. package/templates/documentation-design.tsx +751 -0
  27. package/templates/documentation-technical.template.mjs +6 -0
  28. package/templates/documentation-technical.tsx +292 -0
  29. package/templates/documentation.template.mjs +6 -0
  30. package/templates/documentation.tsx +347 -0
  31. package/templates/editor.template.mjs +6 -0
  32. package/templates/editor.tsx +959 -0
  33. package/templates/file-explorer.template.mjs +6 -0
  34. package/templates/file-explorer.tsx +577 -0
  35. package/templates/form-two-column.template.mjs +6 -0
  36. package/templates/form-two-column.tsx +314 -0
  37. package/templates/gallery-hero.template.mjs +6 -0
  38. package/templates/gallery-hero.tsx +287 -0
  39. package/templates/ide.template.mjs +6 -0
  40. package/templates/ide.tsx +533 -0
  41. package/templates/incident-console.template.mjs +6 -0
  42. package/templates/incident-console.tsx +584 -0
  43. package/templates/kanban-board.template.mjs +6 -0
  44. package/templates/kanban-board.tsx +746 -0
  45. package/templates/library.template.mjs +6 -0
  46. package/templates/library.tsx +489 -0
  47. package/templates/login-card.template.mjs +6 -0
  48. package/templates/login-card.tsx +200 -0
  49. package/templates/login-split.template.mjs +6 -0
  50. package/templates/login-split.tsx +318 -0
  51. package/templates/login-sso.template.mjs +6 -0
  52. package/templates/login-sso.tsx +277 -0
  53. package/templates/login.template.mjs +6 -0
  54. package/templates/login.tsx +98 -0
  55. package/templates/messaging-shell.template.mjs +6 -0
  56. package/templates/messaging-shell.tsx +675 -0
  57. package/templates/mixed-gallery.template.mjs +6 -0
  58. package/templates/mixed-gallery.tsx +190 -0
  59. package/templates/payment-form.template.mjs +6 -0
  60. package/templates/payment-form.tsx +990 -0
  61. package/templates/product-detail.template.mjs +6 -0
  62. package/templates/product-detail.tsx +366 -0
  63. package/templates/product-gallery.template.mjs +6 -0
  64. package/templates/product-gallery.tsx +173 -0
  65. package/templates/settings-dialog.template.mjs +6 -0
  66. package/templates/settings-dialog.tsx +920 -0
  67. package/templates/settings-sidebar.template.mjs +6 -0
  68. package/templates/settings-sidebar.tsx +952 -0
  69. package/templates/settings.template.mjs +6 -0
  70. package/templates/settings.tsx +235 -0
  71. package/templates/shell-nav.template.mjs +6 -0
  72. package/templates/shell-nav.tsx +306 -0
  73. package/templates/shell-side-nav.template.mjs +6 -0
  74. package/templates/shell-side-nav.tsx +238 -0
  75. package/templates/shell-top-nav.template.mjs +6 -0
  76. package/templates/shell-top-nav.tsx +243 -0
  77. package/templates/side-gallery.template.mjs +6 -0
  78. package/templates/side-gallery.tsx +179 -0
  79. package/templates/table-grouped.template.mjs +6 -0
  80. package/templates/table-grouped.tsx +1174 -0
  81. package/templates/table-page-chart.template.mjs +6 -0
  82. package/templates/table-page-chart.tsx +693 -0
  83. package/templates/table-page-heatmap-status.template.mjs +6 -0
  84. package/templates/table-page-heatmap-status.tsx +547 -0
  85. package/templates/table-page-shoe-store-heatmap.template.mjs +6 -0
  86. package/templates/table-page-shoe-store-heatmap.tsx +1038 -0
  87. package/templates/table-page.template.mjs +6 -0
  88. package/templates/table-page.tsx +445 -0
  89. package/templates/table.template.mjs +6 -0
  90. package/templates/table.tsx +84 -0
  91. package/templates/theme-showcase.template.mjs +6 -0
  92. package/templates/theme-showcase.tsx +1282 -0
  93. package/theme.css +4 -4
  94. package/tokens.css +4 -4
@@ -0,0 +1,675 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * Messaging Shell — Slack-style column frame for team messaging tools.
5
+ *
6
+ * Frame (desktop, left to right):
7
+ * workspace rail 68px | channel sidebar 260px | message stream (fill) | thread panel 340px
8
+ *
9
+ * Container policy: dense rows, zero Cards. Channels and DMs are List/ListItem
10
+ * rows, presence is AvatarStatusDot/StatusDot, unread counts are the only
11
+ * Badge usage. The stream and thread are built on the Chat component family.
12
+ *
13
+ * Responsive contract:
14
+ * >1024px — full four-column frame (rail | sidebar | stream | thread)
15
+ * <=1024px — thread panel hidden; stream takes the reclaimed width
16
+ * <=768px — sidebar also hidden; rail + stream keep full width
17
+ *
18
+ * Fixtures are deterministic: fixed ISO timestamps rendered via <Timestamp>,
19
+ * no Date.now()/Math.random() anywhere.
20
+ */
21
+
22
+ import {useState, type CSSProperties} from 'react';
23
+
24
+ import {
25
+ Layout,
26
+ LayoutContent,
27
+ LayoutPanel,
28
+ Stack,
29
+ StackItem,
30
+ HStack,
31
+ VStack,
32
+ } from '@astryxdesign/core/Layout';
33
+ import {Text, Heading} from '@astryxdesign/core/Text';
34
+ import {
35
+ ChatComposer,
36
+ ChatLayout,
37
+ ChatMessage,
38
+ ChatMessageBubble,
39
+ ChatMessageList,
40
+ ChatMessageMetadata,
41
+ ChatSystemMessage,
42
+ } from '@astryxdesign/core/Chat';
43
+ import {Avatar, AvatarStatusDot} from '@astryxdesign/core/Avatar';
44
+ import {StatusDot} from '@astryxdesign/core/StatusDot';
45
+ import {Badge} from '@astryxdesign/core/Badge';
46
+ import {List, ListItem} from '@astryxdesign/core/List';
47
+ import {Divider} from '@astryxdesign/core/Divider';
48
+ import {TextInput} from '@astryxdesign/core/TextInput';
49
+ import {Timestamp} from '@astryxdesign/core/Timestamp';
50
+ import {EmptyState} from '@astryxdesign/core/EmptyState';
51
+ import {Icon} from '@astryxdesign/core/Icon';
52
+ import {IconButton} from '@astryxdesign/core/IconButton';
53
+ import {useMediaQuery} from '@astryxdesign/core/hooks';
54
+
55
+ // ============= ICONS (verified lucide-react exports) =============
56
+ // Bell ← BellIcon, Bookmark ← BookmarkIcon, MessagesSquare ← ChatBubbleLeftRightIcon,
57
+ // Settings ← Cog6ToothIcon, Hash ← HashtagIcon, House ← HomeIcon, Inbox ← InboxIcon,
58
+ // Search ← MagnifyingGlassIcon, SquarePen ← PencilSquareIcon, Users ← UserGroupIcon,
59
+ // X ← XMarkIcon.
60
+ import {
61
+ Bell,
62
+ Bookmark,
63
+ MessagesSquare,
64
+ Settings,
65
+ Hash,
66
+ House,
67
+ Inbox,
68
+ Search,
69
+ SquarePen,
70
+ Users,
71
+ X,
72
+ } from 'lucide-react';
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // Styles — plain CSS properties with semantic tokens only.
76
+ // ---------------------------------------------------------------------------
77
+
78
+ const styles: Record<string, CSSProperties> = {
79
+ root: {
80
+ height: '100dvh',
81
+ width: '100%',
82
+ },
83
+ rail: {
84
+ height: '100%',
85
+ alignItems: 'center',
86
+ paddingTop: 'var(--spacing-3)',
87
+ paddingBottom: 'var(--spacing-3)',
88
+ },
89
+ railSpacer: {
90
+ flex: 1,
91
+ },
92
+ sidebar: {
93
+ height: '100%',
94
+ minHeight: 0,
95
+ },
96
+ sidebarHeader: {
97
+ alignItems: 'center',
98
+ paddingInline: 'var(--spacing-3)',
99
+ paddingBlock: 'var(--spacing-3)',
100
+ },
101
+ sidebarSearch: {
102
+ paddingInline: 'var(--spacing-3)',
103
+ paddingBottom: 'var(--spacing-2)',
104
+ },
105
+ sidebarScroll: {
106
+ minHeight: 0,
107
+ overflowY: 'auto',
108
+ paddingInline: 'var(--spacing-2)',
109
+ paddingBottom: 'var(--spacing-3)',
110
+ },
111
+ sectionGap: {
112
+ marginTop: 'var(--spacing-4)',
113
+ },
114
+ streamColumn: {
115
+ height: '100%',
116
+ minHeight: 0,
117
+ },
118
+ streamHeader: {
119
+ alignItems: 'center',
120
+ paddingInline: 'var(--spacing-4)',
121
+ paddingBlock: 'var(--spacing-3)',
122
+ },
123
+ streamTopic: {
124
+ minWidth: 0,
125
+ },
126
+ chatArea: {
127
+ minHeight: 0,
128
+ display: 'flex',
129
+ flexDirection: 'column',
130
+ },
131
+ chatFill: {
132
+ flex: 1,
133
+ minHeight: 0,
134
+ },
135
+ threadColumn: {
136
+ height: '100%',
137
+ minHeight: 0,
138
+ },
139
+ threadHeader: {
140
+ alignItems: 'center',
141
+ paddingInline: 'var(--spacing-3)',
142
+ paddingBlock: 'var(--spacing-2)',
143
+ },
144
+ threadScroll: {
145
+ minHeight: 0,
146
+ overflowY: 'auto',
147
+ paddingInline: 'var(--spacing-3)',
148
+ paddingBlock: 'var(--spacing-3)',
149
+ },
150
+ threadComposer: {
151
+ padding: 'var(--spacing-3)',
152
+ },
153
+ };
154
+
155
+ // ---------------------------------------------------------------------------
156
+ // Deterministic fixtures — fixed ISO timestamps, stable ordering.
157
+ // ---------------------------------------------------------------------------
158
+
159
+ type Presence = 'online' | 'busy' | 'offline';
160
+
161
+ interface User {
162
+ name: string;
163
+ }
164
+
165
+ const USERS: Record<string, User> = {
166
+ mira: {name: 'Mira Chen'},
167
+ devon: {name: 'Devon Park'},
168
+ sasha: {name: 'Sasha Ortiz'},
169
+ you: {name: 'Riley Quinn'},
170
+ };
171
+
172
+ interface Channel {
173
+ id: string;
174
+ name: string;
175
+ topic: string;
176
+ unread: number;
177
+ }
178
+
179
+ const CHANNELS: Channel[] = [
180
+ {
181
+ id: 'design-systems',
182
+ name: 'design-systems',
183
+ topic: 'Component APIs, tokens, and release coordination',
184
+ unread: 0,
185
+ },
186
+ {
187
+ id: 'frontend-guild',
188
+ name: 'frontend-guild',
189
+ topic: 'Cross-team frontend practices',
190
+ unread: 4,
191
+ },
192
+ {
193
+ id: 'releases',
194
+ name: 'releases',
195
+ topic: 'Release announcements and rollbacks',
196
+ unread: 12,
197
+ },
198
+ {id: 'random', name: 'random', topic: 'Everything else', unread: 0},
199
+ ];
200
+
201
+ interface DirectMessage {
202
+ id: string;
203
+ userId: string;
204
+ presence: Presence;
205
+ unread: number;
206
+ }
207
+
208
+ const DIRECT_MESSAGES: DirectMessage[] = [
209
+ {id: 'dm-mira', userId: 'mira', presence: 'online', unread: 0},
210
+ {id: 'dm-devon', userId: 'devon', presence: 'busy', unread: 2},
211
+ {id: 'dm-sasha', userId: 'sasha', presence: 'offline', unread: 0},
212
+ ];
213
+
214
+ // AvatarStatusDot supports success | neutral | error (no warning).
215
+ const PRESENCE_VARIANT: Record<Presence, 'success' | 'error' | 'neutral'> = {
216
+ online: 'success',
217
+ busy: 'error',
218
+ offline: 'neutral',
219
+ };
220
+
221
+ const PRESENCE_LABEL: Record<Presence, string> = {
222
+ online: 'Online',
223
+ busy: 'Busy',
224
+ offline: 'Offline',
225
+ };
226
+
227
+ /** One message group: consecutive bubbles from the same sender. */
228
+ interface StreamMessage {
229
+ id: string;
230
+ userId: string;
231
+ time: string;
232
+ bubbles: string[];
233
+ }
234
+
235
+ const MESSAGES_BY_CHANNEL: Record<string, StreamMessage[]> = {
236
+ 'design-systems': [
237
+ {
238
+ id: 'm1',
239
+ userId: 'mira',
240
+ time: '2026-06-30T09:12:00',
241
+ bubbles: [
242
+ 'Morning! The Timestamp component now supports a `system_date` format — worth switching the audit log over.',
243
+ 'I put the migration notes in the wiki under Decisions.',
244
+ ],
245
+ },
246
+ {
247
+ id: 'm2',
248
+ userId: 'devon',
249
+ time: '2026-06-30T09:15:00',
250
+ bubbles: ['Nice. Does that unblock the incident console timeline work?'],
251
+ },
252
+ {
253
+ id: 'm3',
254
+ userId: 'you',
255
+ time: '2026-06-30T09:17:00',
256
+ bubbles: [
257
+ 'It does — I will pick that up after the template review.',
258
+ 'One question on the List density defaults, will start a thread.',
259
+ ],
260
+ },
261
+ {
262
+ id: 'm4',
263
+ userId: 'sasha',
264
+ time: '2026-06-30T09:24:00',
265
+ bubbles: [
266
+ 'Heads up: the token sync job ran clean overnight, no drift between core and the theme packages.',
267
+ ],
268
+ },
269
+ {
270
+ id: 'm5',
271
+ userId: 'mira',
272
+ time: '2026-06-30T09:26:00',
273
+ bubbles: ['Great — closing out the drift task then.'],
274
+ },
275
+ ],
276
+ 'frontend-guild': [
277
+ {
278
+ id: 'g1',
279
+ userId: 'devon',
280
+ time: '2026-06-30T08:40:00',
281
+ bubbles: [
282
+ 'Guild sync moved to Thursday this week to avoid the release freeze.',
283
+ ],
284
+ },
285
+ {
286
+ id: 'g2',
287
+ userId: 'you',
288
+ time: '2026-06-30T08:44:00',
289
+ bubbles: ['Works for me — agenda doc is updated.'],
290
+ },
291
+ ],
292
+ };
293
+
294
+ interface ThreadReply {
295
+ id: string;
296
+ userId: string;
297
+ time: string;
298
+ text: string;
299
+ }
300
+
301
+ const THREAD_ROOT: ThreadReply = {
302
+ id: 't0',
303
+ userId: 'you',
304
+ time: '2026-06-30T09:17:30',
305
+ text: 'One question on the List density defaults — should channel sidebars use compact or balanced? The spec shows both.',
306
+ };
307
+
308
+ const THREAD_REPLIES: ThreadReply[] = [
309
+ {
310
+ id: 't1',
311
+ userId: 'mira',
312
+ time: '2026-06-30T09:19:00',
313
+ text: 'Compact for navigation surfaces. Balanced is for content lists where descriptions carry weight.',
314
+ },
315
+ {
316
+ id: 't2',
317
+ userId: 'devon',
318
+ time: '2026-06-30T09:21:00',
319
+ text: 'Agreed — the sidebar rows here are a good reference implementation.',
320
+ },
321
+ ];
322
+
323
+ const RAIL_ITEMS = [
324
+ {id: 'home', label: 'Home', icon: House},
325
+ {id: 'dms', label: 'Direct messages', icon: MessagesSquare},
326
+ {id: 'activity', label: 'Activity', icon: Bell},
327
+ {id: 'saved', label: 'Saved items', icon: Bookmark},
328
+ ];
329
+
330
+ // ---------------------------------------------------------------------------
331
+ // Stream message group — avatar + name on the first bubble, timestamp on the
332
+ // last, `group` positions tighten corner radii between consecutive bubbles.
333
+ // ---------------------------------------------------------------------------
334
+
335
+ function StreamMessageGroup({message}: {message: StreamMessage}) {
336
+ const isSelf = message.userId === 'you';
337
+ const user = USERS[message.userId];
338
+ const lastIndex = message.bubbles.length - 1;
339
+
340
+ return (
341
+ <ChatMessage
342
+ sender={isSelf ? 'user' : 'assistant'}
343
+ avatar={isSelf ? undefined : <Avatar name={user.name} size="md" />}>
344
+ {message.bubbles.map((text, index) => (
345
+ <ChatMessageBubble
346
+ key={`${message.id}-${index}`}
347
+ group={
348
+ message.bubbles.length === 1
349
+ ? undefined
350
+ : index === 0
351
+ ? 'first'
352
+ : index === lastIndex
353
+ ? 'last'
354
+ : 'middle'
355
+ }
356
+ name={!isSelf && index === 0 ? user.name : undefined}
357
+ metadata={
358
+ index === lastIndex ? (
359
+ <ChatMessageMetadata
360
+ timestamp={<Timestamp value={message.time} format="time" />}
361
+ />
362
+ ) : undefined
363
+ }>
364
+ {text}
365
+ </ChatMessageBubble>
366
+ ))}
367
+ </ChatMessage>
368
+ );
369
+ }
370
+
371
+ // ---------------------------------------------------------------------------
372
+ // Page
373
+ // ---------------------------------------------------------------------------
374
+
375
+ export default function MessagingShell() {
376
+ const [selectedChannelId, setSelectedChannelId] = useState('design-systems');
377
+ const [selectedDmId, setSelectedDmId] = useState<string | null>(null);
378
+ const [isThreadOpen, setIsThreadOpen] = useState(true);
379
+ const [searchQuery, setSearchQuery] = useState('');
380
+
381
+ // Responsive contract (see file header).
382
+ const isThreadHidden = useMediaQuery('(max-width: 1024px)');
383
+ const isSidebarHidden = useMediaQuery('(max-width: 768px)');
384
+
385
+ const selectedChannel =
386
+ CHANNELS.find(channel => channel.id === selectedChannelId) ?? CHANNELS[0];
387
+ const messages = MESSAGES_BY_CHANNEL[selectedChannel.id] ?? [];
388
+
389
+ const normalizedQuery = searchQuery.trim().toLowerCase();
390
+ const visibleChannels = CHANNELS.filter(channel =>
391
+ channel.name.toLowerCase().includes(normalizedQuery),
392
+ );
393
+ const visibleDms = DIRECT_MESSAGES.filter(dm =>
394
+ USERS[dm.userId].name.toLowerCase().includes(normalizedQuery),
395
+ );
396
+
397
+ const showThreadPanel = isThreadOpen && !isThreadHidden;
398
+
399
+ const workspaceRail = (
400
+ <VStack gap={2} style={styles.rail}>
401
+ <Avatar name="Night Watch" size="md" />
402
+ {RAIL_ITEMS.map(item => (
403
+ <IconButton
404
+ key={item.id}
405
+ label={item.label}
406
+ tooltip={item.label}
407
+ icon={<Icon icon={item.icon} size="sm" color="inherit" />}
408
+ variant={item.id === 'home' ? 'secondary' : 'ghost'}
409
+ onClick={() => {}}
410
+ />
411
+ ))}
412
+ <div style={styles.railSpacer} />
413
+ <IconButton
414
+ label="Settings"
415
+ tooltip="Settings"
416
+ icon={<Icon icon={Settings} size="sm" color="inherit" />}
417
+ variant="ghost"
418
+ onClick={() => {}}
419
+ />
420
+ <Avatar
421
+ name={USERS.you.name}
422
+ size="md"
423
+ status={<AvatarStatusDot variant="success" label="Online" />}
424
+ />
425
+ </VStack>
426
+ );
427
+
428
+ const channelSidebar = (
429
+ <Stack direction="vertical" style={styles.sidebar}>
430
+ <HStack gap={2} style={styles.sidebarHeader}>
431
+ <StackItem size="fill">
432
+ <Heading level={5}>Night Watch</Heading>
433
+ </StackItem>
434
+ <IconButton
435
+ label="New message"
436
+ tooltip="New message"
437
+ icon={<Icon icon={SquarePen} size="sm" color="inherit" />}
438
+ variant="ghost"
439
+ size="sm"
440
+ onClick={() => {}}
441
+ />
442
+ </HStack>
443
+ <div style={styles.sidebarSearch}>
444
+ <TextInput
445
+ label="Jump to"
446
+ isLabelHidden
447
+ size="sm"
448
+ placeholder="Jump to..."
449
+ startIcon={Search}
450
+ value={searchQuery}
451
+ onChange={setSearchQuery}
452
+ />
453
+ </div>
454
+ <StackItem size="fill" style={styles.sidebarScroll}>
455
+ <List
456
+ density="compact"
457
+ hasDividers={false}
458
+ header={
459
+ <Text type="label" size="sm" color="secondary">
460
+ Channels
461
+ </Text>
462
+ }>
463
+ {visibleChannels.map(channel => (
464
+ <ListItem
465
+ key={channel.id}
466
+ label={channel.name}
467
+ isSelected={
468
+ selectedDmId === null && channel.id === selectedChannelId
469
+ }
470
+ onClick={() => {
471
+ setSelectedChannelId(channel.id);
472
+ setSelectedDmId(null);
473
+ }}
474
+ startContent={<Icon icon={Hash} size="sm" color="secondary" />}
475
+ endContent={
476
+ channel.unread > 0 ? (
477
+ <Badge label={String(channel.unread)} variant="neutral" />
478
+ ) : undefined
479
+ }
480
+ />
481
+ ))}
482
+ </List>
483
+ <div style={styles.sectionGap}>
484
+ <List
485
+ density="compact"
486
+ hasDividers={false}
487
+ header={
488
+ <Text type="label" size="sm" color="secondary">
489
+ Direct messages
490
+ </Text>
491
+ }>
492
+ {visibleDms.map(dm => (
493
+ <ListItem
494
+ key={dm.id}
495
+ label={USERS[dm.userId].name}
496
+ isSelected={selectedDmId === dm.id}
497
+ onClick={() => setSelectedDmId(dm.id)}
498
+ startContent={
499
+ <Avatar
500
+ name={USERS[dm.userId].name}
501
+ size="sm"
502
+ status={
503
+ <AvatarStatusDot
504
+ variant={PRESENCE_VARIANT[dm.presence]}
505
+ label={PRESENCE_LABEL[dm.presence]}
506
+ />
507
+ }
508
+ />
509
+ }
510
+ endContent={
511
+ dm.unread > 0 ? (
512
+ <Badge label={String(dm.unread)} variant="neutral" />
513
+ ) : undefined
514
+ }
515
+ />
516
+ ))}
517
+ </List>
518
+ </div>
519
+ </StackItem>
520
+ </Stack>
521
+ );
522
+
523
+ const messageStream = (
524
+ <Stack direction="vertical" style={styles.streamColumn}>
525
+ <HStack gap={3} style={styles.streamHeader}>
526
+ <Icon icon={Hash} size="sm" color="secondary" />
527
+ <Heading level={5}>{selectedChannel.name}</Heading>
528
+ <StackItem size="fill" style={styles.streamTopic}>
529
+ <Text type="supporting" color="secondary" maxLines={1}>
530
+ {selectedChannel.topic}
531
+ </Text>
532
+ </StackItem>
533
+ <StatusDot variant="success" label="12 online" />
534
+ <IconButton
535
+ label="Members"
536
+ tooltip="Members"
537
+ icon={<Icon icon={Users} size="sm" color="inherit" />}
538
+ variant="ghost"
539
+ size="sm"
540
+ onClick={() => {}}
541
+ />
542
+ </HStack>
543
+ <Divider />
544
+ <StackItem size="fill" style={styles.chatArea}>
545
+ <div style={styles.chatFill}>
546
+ <ChatLayout
547
+ composer={
548
+ <ChatComposer
549
+ placeholder={`Message #${selectedChannel.name}`}
550
+ onSubmit={() => {}}
551
+ />
552
+ }
553
+ emptyState={
554
+ <EmptyState
555
+ icon={<Icon icon={Inbox} size="lg" />}
556
+ title="No messages yet"
557
+ description="Start the conversation — messages posted here are visible to the whole channel."
558
+ />
559
+ }>
560
+ {messages.length > 0 ? (
561
+ <ChatMessageList density="balanced">
562
+ <ChatSystemMessage variant="divider">
563
+ Tuesday, June 30
564
+ </ChatSystemMessage>
565
+ <ChatSystemMessage>
566
+ Sasha Ortiz joined #{selectedChannel.name}
567
+ </ChatSystemMessage>
568
+ {messages.map(message => (
569
+ <StreamMessageGroup key={message.id} message={message} />
570
+ ))}
571
+ </ChatMessageList>
572
+ ) : null}
573
+ </ChatLayout>
574
+ </div>
575
+ </StackItem>
576
+ </Stack>
577
+ );
578
+
579
+ const threadPanel = (
580
+ <Stack direction="vertical" style={styles.threadColumn}>
581
+ <HStack gap={2} style={styles.threadHeader}>
582
+ <StackItem size="fill">
583
+ <HStack gap={2} style={{alignItems: 'baseline'}}>
584
+ <Text weight="semibold">Thread</Text>
585
+ <Text type="supporting" color="secondary">
586
+ #{selectedChannel.name}
587
+ </Text>
588
+ </HStack>
589
+ </StackItem>
590
+ <IconButton
591
+ label="Close thread"
592
+ tooltip="Close thread"
593
+ icon={<Icon icon={X} size="sm" color="inherit" />}
594
+ variant="ghost"
595
+ size="sm"
596
+ onClick={() => setIsThreadOpen(false)}
597
+ />
598
+ </HStack>
599
+ <Divider />
600
+ <StackItem size="fill" style={styles.threadScroll}>
601
+ <ChatMessageList density="compact">
602
+ <ChatMessage
603
+ sender="assistant"
604
+ avatar={<Avatar name={USERS[THREAD_ROOT.userId].name} size="md" />}>
605
+ <ChatMessageBubble
606
+ name={USERS[THREAD_ROOT.userId].name}
607
+ metadata={
608
+ <ChatMessageMetadata
609
+ timestamp={
610
+ <Timestamp value={THREAD_ROOT.time} format="time" />
611
+ }
612
+ />
613
+ }>
614
+ {THREAD_ROOT.text}
615
+ </ChatMessageBubble>
616
+ </ChatMessage>
617
+ <ChatSystemMessage variant="divider">
618
+ {THREAD_REPLIES.length} replies
619
+ </ChatSystemMessage>
620
+ {THREAD_REPLIES.map(reply => (
621
+ <ChatMessage
622
+ key={reply.id}
623
+ sender="assistant"
624
+ avatar={<Avatar name={USERS[reply.userId].name} size="md" />}>
625
+ <ChatMessageBubble
626
+ name={USERS[reply.userId].name}
627
+ metadata={
628
+ <ChatMessageMetadata
629
+ timestamp={<Timestamp value={reply.time} format="time" />}
630
+ />
631
+ }>
632
+ {reply.text}
633
+ </ChatMessageBubble>
634
+ </ChatMessage>
635
+ ))}
636
+ </ChatMessageList>
637
+ </StackItem>
638
+ <div style={styles.threadComposer}>
639
+ <ChatComposer
640
+ density="compact"
641
+ placeholder="Reply in thread..."
642
+ onSubmit={() => {}}
643
+ />
644
+ </div>
645
+ </Stack>
646
+ );
647
+
648
+ return (
649
+ <div style={styles.root}>
650
+ <Layout
651
+ height="fill"
652
+ start={
653
+ <>
654
+ <LayoutPanel width={68} padding={0}>
655
+ {workspaceRail}
656
+ </LayoutPanel>
657
+ {!isSidebarHidden && (
658
+ <LayoutPanel width={260} padding={0}>
659
+ {channelSidebar}
660
+ </LayoutPanel>
661
+ )}
662
+ </>
663
+ }
664
+ end={
665
+ showThreadPanel ? (
666
+ <LayoutPanel width={340} padding={0}>
667
+ {threadPanel}
668
+ </LayoutPanel>
669
+ ) : undefined
670
+ }
671
+ content={<LayoutContent padding={0}>{messageStream}</LayoutContent>}
672
+ />
673
+ </div>
674
+ );
675
+ }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ type: 'page',
3
+ name: 'Mixed Gallery',
4
+ description: 'Masonry image gallery with varying tile sizes',
5
+ category: 'marketing',
6
+ };