@xmesh/system-design 0.0.1

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 (175) hide show
  1. package/README.md +472 -0
  2. package/assets/brand-lockup-dark.svg +9 -0
  3. package/assets/brand-lockup-light.svg +9 -0
  4. package/assets/brand-mark.svg +9 -0
  5. package/colors_and_type.css +11 -0
  6. package/dist/lit/components/alert/index.css +201 -0
  7. package/dist/lit/components/alert/index.d.ts +25 -0
  8. package/dist/lit/components/alert/index.js +191 -0
  9. package/dist/lit/components/app-bar/index.css +80 -0
  10. package/dist/lit/components/app-bar/index.d.ts +19 -0
  11. package/dist/lit/components/app-bar/index.js +120 -0
  12. package/dist/lit/components/artifact/index.css +166 -0
  13. package/dist/lit/components/artifact/index.d.ts +37 -0
  14. package/dist/lit/components/artifact/index.js +294 -0
  15. package/dist/lit/components/autocomplete/index.css +171 -0
  16. package/dist/lit/components/autocomplete/index.d.ts +47 -0
  17. package/dist/lit/components/autocomplete/index.js +404 -0
  18. package/dist/lit/components/avatar/index.css +62 -0
  19. package/dist/lit/components/avatar/index.d.ts +19 -0
  20. package/dist/lit/components/avatar/index.js +112 -0
  21. package/dist/lit/components/avatar-group/index.css +60 -0
  22. package/dist/lit/components/avatar-group/index.d.ts +19 -0
  23. package/dist/lit/components/avatar-group/index.js +97 -0
  24. package/dist/lit/components/badge/index.css +72 -0
  25. package/dist/lit/components/badge/index.d.ts +18 -0
  26. package/dist/lit/components/badge/index.js +115 -0
  27. package/dist/lit/components/brand-mark/index.css +109 -0
  28. package/dist/lit/components/brand-mark/index.d.ts +24 -0
  29. package/dist/lit/components/brand-mark/index.js +116 -0
  30. package/dist/lit/components/breadcrumbs/index.css +91 -0
  31. package/dist/lit/components/breadcrumbs/index.d.ts +19 -0
  32. package/dist/lit/components/breadcrumbs/index.js +104 -0
  33. package/dist/lit/components/bubble/index.css +182 -0
  34. package/dist/lit/components/bubble/index.d.ts +72 -0
  35. package/dist/lit/components/bubble/index.js +617 -0
  36. package/dist/lit/components/button/index.css +342 -0
  37. package/dist/lit/components/button/index.d.ts +32 -0
  38. package/dist/lit/components/button/index.js +202 -0
  39. package/dist/lit/components/card/index.css +99 -0
  40. package/dist/lit/components/card/index.d.ts +20 -0
  41. package/dist/lit/components/card/index.js +133 -0
  42. package/dist/lit/components/chat/index.css +292 -0
  43. package/dist/lit/components/chat/index.d.ts +74 -0
  44. package/dist/lit/components/chat/index.js +589 -0
  45. package/dist/lit/components/checkbox/index.css +126 -0
  46. package/dist/lit/components/checkbox/index.d.ts +21 -0
  47. package/dist/lit/components/checkbox/index.js +138 -0
  48. package/dist/lit/components/chip/index.css +145 -0
  49. package/dist/lit/components/chip/index.d.ts +30 -0
  50. package/dist/lit/components/chip/index.js +230 -0
  51. package/dist/lit/components/chip-group/index.css +19 -0
  52. package/dist/lit/components/chip-group/index.d.ts +24 -0
  53. package/dist/lit/components/chip-group/index.js +171 -0
  54. package/dist/lit/components/code/index.css +42 -0
  55. package/dist/lit/components/code/index.d.ts +12 -0
  56. package/dist/lit/components/code/index.js +68 -0
  57. package/dist/lit/components/composer/index.css +548 -0
  58. package/dist/lit/components/composer/index.d.ts +67 -0
  59. package/dist/lit/components/composer/index.js +713 -0
  60. package/dist/lit/components/data-table/index.css +166 -0
  61. package/dist/lit/components/data-table/index.d.ts +55 -0
  62. package/dist/lit/components/data-table/index.js +390 -0
  63. package/dist/lit/components/dialog/index.css +124 -0
  64. package/dist/lit/components/dialog/index.d.ts +24 -0
  65. package/dist/lit/components/dialog/index.js +199 -0
  66. package/dist/lit/components/divider/index.css +27 -0
  67. package/dist/lit/components/divider/index.d.ts +13 -0
  68. package/dist/lit/components/divider/index.js +67 -0
  69. package/dist/lit/components/empty-state/index.css +69 -0
  70. package/dist/lit/components/empty-state/index.d.ts +21 -0
  71. package/dist/lit/components/empty-state/index.js +123 -0
  72. package/dist/lit/components/expansion-panel/index.css +120 -0
  73. package/dist/lit/components/expansion-panel/index.d.ts +22 -0
  74. package/dist/lit/components/expansion-panel/index.js +174 -0
  75. package/dist/lit/components/field/index.css +223 -0
  76. package/dist/lit/components/field/index.d.ts +106 -0
  77. package/dist/lit/components/field/index.js +388 -0
  78. package/dist/lit/components/file-input/index.css +257 -0
  79. package/dist/lit/components/file-input/index.d.ts +30 -0
  80. package/dist/lit/components/file-input/index.js +298 -0
  81. package/dist/lit/components/form/index.css +29 -0
  82. package/dist/lit/components/form/index.d.ts +38 -0
  83. package/dist/lit/components/form/index.js +192 -0
  84. package/dist/lit/components/grid/index.css +53 -0
  85. package/dist/lit/components/grid/index.d.ts +14 -0
  86. package/dist/lit/components/grid/index.js +82 -0
  87. package/dist/lit/components/kbd/index.css +35 -0
  88. package/dist/lit/components/kbd/index.d.ts +11 -0
  89. package/dist/lit/components/kbd/index.js +43 -0
  90. package/dist/lit/components/list/index.css +15 -0
  91. package/dist/lit/components/list/index.d.ts +28 -0
  92. package/dist/lit/components/list/index.js +188 -0
  93. package/dist/lit/components/list-item/index.css +119 -0
  94. package/dist/lit/components/list-item/index.d.ts +20 -0
  95. package/dist/lit/components/list-item/index.js +127 -0
  96. package/dist/lit/components/menu/index.css +94 -0
  97. package/dist/lit/components/menu/index.d.ts +47 -0
  98. package/dist/lit/components/menu/index.js +386 -0
  99. package/dist/lit/components/navigation-drawer/index.css +114 -0
  100. package/dist/lit/components/navigation-drawer/index.d.ts +29 -0
  101. package/dist/lit/components/navigation-drawer/index.js +218 -0
  102. package/dist/lit/components/overlay/index.css +171 -0
  103. package/dist/lit/components/overlay/index.d.ts +65 -0
  104. package/dist/lit/components/overlay/index.js +566 -0
  105. package/dist/lit/components/pagination/index.css +102 -0
  106. package/dist/lit/components/pagination/index.d.ts +22 -0
  107. package/dist/lit/components/pagination/index.js +184 -0
  108. package/dist/lit/components/primitives/index.css +504 -0
  109. package/dist/lit/components/primitives/index.d.ts +25 -0
  110. package/dist/lit/components/primitives/index.js +283 -0
  111. package/dist/lit/components/progress/index.css +143 -0
  112. package/dist/lit/components/progress/index.d.ts +23 -0
  113. package/dist/lit/components/progress/index.js +180 -0
  114. package/dist/lit/components/radio-group/index.css +178 -0
  115. package/dist/lit/components/radio-group/index.d.ts +35 -0
  116. package/dist/lit/components/radio-group/index.js +292 -0
  117. package/dist/lit/components/select/index.css +151 -0
  118. package/dist/lit/components/select/index.d.ts +50 -0
  119. package/dist/lit/components/select/index.js +390 -0
  120. package/dist/lit/components/sidebar-item/index.css +133 -0
  121. package/dist/lit/components/sidebar-item/index.d.ts +20 -0
  122. package/dist/lit/components/sidebar-item/index.js +105 -0
  123. package/dist/lit/components/skeleton/index.css +81 -0
  124. package/dist/lit/components/skeleton/index.d.ts +19 -0
  125. package/dist/lit/components/skeleton/index.js +119 -0
  126. package/dist/lit/components/slider/index.css +171 -0
  127. package/dist/lit/components/slider/index.d.ts +36 -0
  128. package/dist/lit/components/slider/index.js +302 -0
  129. package/dist/lit/components/snackbar/index.css +279 -0
  130. package/dist/lit/components/snackbar/index.d.ts +33 -0
  131. package/dist/lit/components/snackbar/index.js +195 -0
  132. package/dist/lit/components/stack/index.css +41 -0
  133. package/dist/lit/components/stack/index.d.ts +20 -0
  134. package/dist/lit/components/stack/index.js +103 -0
  135. package/dist/lit/components/switch/index.css +126 -0
  136. package/dist/lit/components/switch/index.d.ts +17 -0
  137. package/dist/lit/components/switch/index.js +116 -0
  138. package/dist/lit/components/table/index.css +85 -0
  139. package/dist/lit/components/table/index.d.ts +25 -0
  140. package/dist/lit/components/table/index.js +139 -0
  141. package/dist/lit/components/tabs/index.css +116 -0
  142. package/dist/lit/components/tabs/index.d.ts +49 -0
  143. package/dist/lit/components/tabs/index.js +320 -0
  144. package/dist/lit/components/text-field/index.css +90 -0
  145. package/dist/lit/components/text-field/index.d.ts +17 -0
  146. package/dist/lit/components/text-field/index.js +101 -0
  147. package/dist/lit/components/textarea/index.css +55 -0
  148. package/dist/lit/components/textarea/index.d.ts +26 -0
  149. package/dist/lit/components/textarea/index.js +124 -0
  150. package/dist/lit/components/tooltip/index.css +37 -0
  151. package/dist/lit/components/tooltip/index.d.ts +31 -0
  152. package/dist/lit/components/tooltip/index.js +196 -0
  153. package/dist/lit/components/validation/index.css +386 -0
  154. package/dist/lit/components/validation/index.d.ts +45 -0
  155. package/dist/lit/components/validation/index.js +318 -0
  156. package/dist/lit/index.d.ts +50 -0
  157. package/dist/lit/index.js +59 -0
  158. package/package.json +81 -0
  159. package/styles/README.md +346 -0
  160. package/styles/_elevation.css +24 -0
  161. package/styles/_fonts.css +6 -0
  162. package/styles/_layout.css +37 -0
  163. package/styles/_primitives.css +154 -0
  164. package/styles/_scroll.css +75 -0
  165. package/styles/_semantic.css +146 -0
  166. package/styles/_space.css +61 -0
  167. package/styles/_type.css +139 -0
  168. package/styles/_xmesh-extensions.css +232 -0
  169. package/styles/index.css +44 -0
  170. package/styles/md3/_color.css +102 -0
  171. package/styles/md3/_elevation.css +26 -0
  172. package/styles/md3/_motion.css +35 -0
  173. package/styles/md3/_shape.css +22 -0
  174. package/styles/md3/_state.css +22 -0
  175. package/styles/md3/_type.css +111 -0
@@ -0,0 +1,589 @@
1
+ /*
2
+ chat/index.ts — Lit port of components/chat/index.jsx.
3
+
4
+ <xm-chat-shell> — full-viewport chat layout (sidebar + top + thread +
5
+ composer + optional artifact panel).
6
+
7
+ Properties (all set via JS — they're nested objects):
8
+ conversations Array<{ group: string, items: [{ id, title, active? }] }>
9
+ thread Array<{ from: 'user'|'bot', text?, html?, ts,
10
+ atts?, artifact? }>
11
+ `html` is a trusted HTML string (rendered via
12
+ unsafeHTML); `text` is plain text.
13
+ processing boolean — forwarded to the inner <xm-composer>
14
+ thinkingLabel string — forwarded to the inner <xm-composer>
15
+ topTitle string override (default "New Conversation")
16
+
17
+ Slots (host-level):
18
+ artifact-body rendered inside <xm-artifact> as the preview view
19
+ when an artifact is open
20
+ artifact-code rendered inside <xm-artifact> as the code view; the
21
+ panel's segmented eye/code toggle swaps between this
22
+ and artifact-body. Optional — omit for preview-only.
23
+ empty replaces the thread + composer area when set
24
+ (host owns its own composer in that case)
25
+
26
+ Events (all bubble, composed):
27
+ send detail: { text, attachments } — proxy from xm-composer
28
+ cancel
29
+ artifact-open detail: { artifact } — when a bubble's artifact chip
30
+ is clicked. The shell also handles this internally
31
+ (opens the artifact slot).
32
+ artifact-close
33
+
34
+ Light DOM. Loads the entire palette of dependent stylesheets at the
35
+ preview level. Composes the previously-ported xm-* elements.
36
+ */
37
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
38
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
39
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
40
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
41
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
42
+ };
43
+ import { LitElement, html, svg, nothing } from "lit";
44
+ import { customElement, property, state } from "lit/decorators.js";
45
+ // Pull in dependencies (side-effect imports; each defines its own custom
46
+ // elements). The preview HTML can also `<script type="module">` them
47
+ // individually if it wants finer control over load order.
48
+ import "../primitives/index.js";
49
+ import "../brand-mark/index.js";
50
+ import "../button/index.js";
51
+ import "../sidebar-item/index.js";
52
+ import "../bubble/index.js";
53
+ import "../composer/index.js";
54
+ import "../artifact/index.js";
55
+ /* ---------- Inline icon SVG fragments (used in topbar / sidebar chrome) */
56
+ const I_PANEL = (size = 16) => svg `
57
+ <svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none"
58
+ stroke="currentColor" stroke-width="1.8" stroke-linecap="round"
59
+ stroke-linejoin="round" class="ds-icon">
60
+ <rect x="3" y="4" width="18" height="16" rx="2" />
61
+ <path d="M9 4v16" />
62
+ </svg>
63
+ `;
64
+ const I_PLUS = (size = 14) => svg `
65
+ <svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none"
66
+ stroke="currentColor" stroke-width="2.2" stroke-linecap="round"
67
+ stroke-linejoin="round" class="ds-icon">
68
+ <path d="M12 5v14M5 12h14" />
69
+ </svg>
70
+ `;
71
+ const I_SEARCH = (size = 14) => svg `
72
+ <svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none"
73
+ stroke="currentColor" stroke-width="2" stroke-linecap="round"
74
+ stroke-linejoin="round" class="ds-icon">
75
+ <circle cx="11" cy="11" r="7" />
76
+ <path d="M21 21l-4.3-4.3" />
77
+ </svg>
78
+ `;
79
+ const I_TRACE = (size = 17) => svg `
80
+ <svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none"
81
+ stroke="currentColor" stroke-width="1.6" stroke-linecap="round"
82
+ stroke-linejoin="round" class="ds-icon">
83
+ <path d="M8 6V4a4 4 0 0 1 8 0v2" />
84
+ <rect x="6" y="6" width="12" height="13" rx="6" />
85
+ <path d="M12 11v8" />
86
+ <path d="M3 11h3" /><path d="M18 11h3" />
87
+ <path d="M3 18h3" /><path d="M18 18h3" />
88
+ <path d="M3 14h3" /><path d="M18 14h3" />
89
+ </svg>
90
+ `;
91
+ const I_SETTINGS = (size = 17) => svg `
92
+ <svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none"
93
+ stroke="currentColor" stroke-width="1.8" stroke-linecap="round"
94
+ stroke-linejoin="round" class="ds-icon">
95
+ <circle cx="12" cy="12" r="3" />
96
+ <path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3h.1a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8v.1a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z" />
97
+ </svg>
98
+ `;
99
+ const I_REGEN = (size = 16) => svg `
100
+ <svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none"
101
+ stroke="currentColor" stroke-width="1.6" stroke-linecap="round"
102
+ stroke-linejoin="round" class="ds-icon">
103
+ <path d="M3 12a9 9 0 1 0 3-6.7" />
104
+ <path d="M3 4v5h5" />
105
+ </svg>
106
+ `;
107
+ const I_COPY = (size = 16) => svg `
108
+ <svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none"
109
+ stroke="currentColor" stroke-width="1.6" stroke-linecap="round"
110
+ stroke-linejoin="round" class="ds-icon">
111
+ <rect x="9" y="9" width="11" height="11" rx="2" />
112
+ <path d="M5 15V5a2 2 0 0 1 2-2h10" />
113
+ </svg>
114
+ `;
115
+ const I_EDIT = (size = 16) => svg `
116
+ <svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none"
117
+ stroke="currentColor" stroke-width="1.6" stroke-linecap="round"
118
+ stroke-linejoin="round" class="ds-icon">
119
+ <path d="M12 20h9" />
120
+ <path d="M16.5 3.5a2.121 2.121 0 1 1 3 3L7 19l-4 1 1-4 12.5-12.5z" />
121
+ </svg>
122
+ `;
123
+ const I_CHECK = (size = 16) => svg `
124
+ <svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none"
125
+ stroke="currentColor" stroke-width="1.8" stroke-linecap="round"
126
+ stroke-linejoin="round" class="ds-icon">
127
+ <polyline points="20 6 9 17 4 12" />
128
+ </svg>
129
+ `;
130
+ // Best-effort clipboard write. Async API first; falls back to a hidden
131
+ // textarea + execCommand for headless / non-secure-context environments
132
+ // (notably the screenshot harness).
133
+ const writeClipboard = async (text) => {
134
+ try {
135
+ if (navigator.clipboard?.writeText) {
136
+ await navigator.clipboard.writeText(text);
137
+ return true;
138
+ }
139
+ }
140
+ catch (_) { /* fall through to legacy path */ }
141
+ try {
142
+ const ta = document.createElement("textarea");
143
+ ta.value = text;
144
+ ta.setAttribute("readonly", "");
145
+ ta.style.position = "fixed";
146
+ ta.style.opacity = "0";
147
+ document.body.appendChild(ta);
148
+ ta.select();
149
+ const ok = document.execCommand("copy");
150
+ document.body.removeChild(ta);
151
+ return ok;
152
+ }
153
+ catch (_) {
154
+ return false;
155
+ }
156
+ };
157
+ // Strip HTML to plain text by parsing into a detached div. Used for
158
+ // thread messages whose body is authored as `html` — pasting raw HTML
159
+ // is rarely what the user wants.
160
+ const htmlToText = (html) => {
161
+ const div = document.createElement("div");
162
+ div.innerHTML = html;
163
+ return (div.textContent || "").trim();
164
+ };
165
+ let XmChatShell = class XmChatShell extends LitElement {
166
+ constructor() {
167
+ super(...arguments);
168
+ this.conversations = [];
169
+ this.thread = [];
170
+ this.processing = false;
171
+ this.thinkingLabel = "";
172
+ this.topTitle = "New Conversation";
173
+ /** Pin the shell to the viewport (height: 100vh). Default is height: 100%
174
+ so the shell sizes to its container — embedded hosts get correct
175
+ sizing for free. Full-bleed previews opt in via the `full-viewport`
176
+ attribute. */
177
+ this.fullViewport = false;
178
+ this._collapsed = false;
179
+ this._activeId = null;
180
+ this._openArtifact = null;
181
+ this._hasEmpty = false;
182
+ this._searchValue = "";
183
+ this._copiedIdx = null;
184
+ this._copiedTimer = null;
185
+ this._artifactBodyNodes = [];
186
+ this._artifactCodeNodes = [];
187
+ this._emptyNodes = [];
188
+ this._toggleCollapsed = () => { this._collapsed = !this._collapsed; };
189
+ this._selectConv = (id) => { this._activeId = id; };
190
+ this._onSearchInput = (e) => {
191
+ this._searchValue = e.target.value;
192
+ };
193
+ this._onSearchClear = () => { this._searchValue = ""; };
194
+ this._onArtifactOpen = (e) => {
195
+ const detail = e.detail;
196
+ this._openArtifact = detail?.artifact ?? null;
197
+ this.dispatchEvent(new CustomEvent("artifact-open", {
198
+ detail: { artifact: this._openArtifact },
199
+ bubbles: true, composed: true,
200
+ }));
201
+ };
202
+ this._onArtifactClose = () => {
203
+ this._openArtifact = null;
204
+ this.dispatchEvent(new CustomEvent("artifact-close", {
205
+ bubbles: true, composed: true,
206
+ }));
207
+ };
208
+ this._onComposerSend = (e) => {
209
+ const detail = e.detail;
210
+ this.dispatchEvent(new CustomEvent("send", {
211
+ detail, bubbles: true, composed: true,
212
+ }));
213
+ };
214
+ this._onComposerCancel = () => {
215
+ this.dispatchEvent(new CustomEvent("cancel", {
216
+ bubbles: true, composed: true,
217
+ }));
218
+ };
219
+ this._onCopyMessage = async (e, idx) => {
220
+ // Stop the bubble-actions delegated listener (added by xm-bubble-actions
221
+ // for the static-preview path) from also firing.
222
+ e.stopPropagation();
223
+ const m = (this.thread || [])[idx];
224
+ if (!m)
225
+ return;
226
+ const text = m.text ? m.text : (m.html ? htmlToText(m.html) : "");
227
+ if (!text)
228
+ return;
229
+ const ok = await writeClipboard(text);
230
+ if (!ok)
231
+ return;
232
+ this._copiedIdx = idx;
233
+ if (this._copiedTimer != null)
234
+ window.clearTimeout(this._copiedTimer);
235
+ this._copiedTimer = window.setTimeout(() => {
236
+ this._copiedIdx = null;
237
+ this._copiedTimer = null;
238
+ }, 1200);
239
+ };
240
+ }
241
+ createRenderRoot() {
242
+ return this;
243
+ }
244
+ connectedCallback() {
245
+ super.connectedCallback();
246
+ // Snapshot any authored slot="artifact-body" / slot="artifact-code" /
247
+ // slot="empty" content so lit's render doesn't replace them. We mount
248
+ // them imperatively in updated().
249
+ const ab = [];
250
+ const ac = [];
251
+ const em = [];
252
+ for (const child of Array.from(this.childNodes)) {
253
+ if (child.nodeType !== Node.ELEMENT_NODE) {
254
+ continue;
255
+ }
256
+ const slot = child.getAttribute("slot");
257
+ if (slot === "artifact-body")
258
+ ab.push(child);
259
+ else if (slot === "artifact-code")
260
+ ac.push(child);
261
+ else if (slot === "empty")
262
+ em.push(child);
263
+ }
264
+ for (const n of [...ab, ...ac, ...em])
265
+ n.remove();
266
+ this._artifactBodyNodes = ab;
267
+ this._artifactCodeNodes = ac;
268
+ this._emptyNodes = em;
269
+ this._hasEmpty = em.length > 0;
270
+ // Initialize active conversation from .active flag
271
+ if (Array.isArray(this.conversations)) {
272
+ for (const g of this.conversations) {
273
+ const match = g.items?.find((i) => i.active);
274
+ if (match) {
275
+ this._activeId = match.id;
276
+ break;
277
+ }
278
+ }
279
+ }
280
+ }
281
+ willUpdate(changed) {
282
+ // Re-init active id if conversations were re-set after upgrade.
283
+ if (changed.has("conversations") && this._activeId == null && Array.isArray(this.conversations)) {
284
+ for (const g of this.conversations) {
285
+ const match = g.items?.find((i) => i.active);
286
+ if (match) {
287
+ this._activeId = match.id;
288
+ break;
289
+ }
290
+ }
291
+ }
292
+ }
293
+ updated() {
294
+ // Pin thread to bottom on render (mirrors useEffect on [thread, processing]).
295
+ const threadEl = this.querySelector(".chat-shell__thread");
296
+ if (threadEl)
297
+ threadEl.scrollTop = threadEl.scrollHeight;
298
+ // Mount empty content
299
+ const emptyMount = this.querySelector('[data-mount="empty"]');
300
+ if (emptyMount && this._emptyNodes.length && emptyMount.childNodes.length === 0) {
301
+ for (const n of this._emptyNodes)
302
+ emptyMount.appendChild(n);
303
+ }
304
+ // Hand the artifact panel its preview/code body nodes via the public
305
+ // setBodies() API. This replaces any self-authored bodies and triggers
306
+ // a re-render so the active view mounts. We re-call on every chat-shell
307
+ // update so a freshly-mounted <xm-artifact> (after open/close/reopen)
308
+ // always lands with the right content.
309
+ if (this._openArtifact) {
310
+ const art = this.querySelector("xm-artifact");
311
+ if (art?.setBodies && (this._artifactBodyNodes.length || this._artifactCodeNodes.length)) {
312
+ art.setBodies(this._artifactBodyNodes, this._artifactCodeNodes);
313
+ }
314
+ }
315
+ }
316
+ /* ---------- sidebar render ---------- */
317
+ _renderSidebar() {
318
+ const collapsed = this._collapsed;
319
+ const groups = this.conversations || [];
320
+ return html `
321
+ <aside class="chat-sidebar" aria-label="Conversations">
322
+ ${collapsed
323
+ ? html `<xm-button
324
+ class="chat-sidebar__top"
325
+ variant="ghost"
326
+ icon-only
327
+ aria-label="Expand sidebar"
328
+ title="Expand sidebar"
329
+ @click=${this._toggleCollapsed}
330
+ >${I_PANEL()}</xm-button>`
331
+ : html `<xm-button
332
+ class="chat-sidebar__top"
333
+ variant="ghost"
334
+ full-width
335
+ aria-label="Collapse sidebar"
336
+ title="Collapse sidebar"
337
+ @click=${this._toggleCollapsed}
338
+ >
339
+ <xm-brand-mark size="sm" show-wordmark></xm-brand-mark>
340
+ <span slot="icon-right">${I_PANEL()}</span>
341
+ </xm-button>`}
342
+
343
+ ${collapsed
344
+ ? html `<xm-button
345
+ class="chat-sidebar__new"
346
+ variant="ghost"
347
+ icon-only
348
+ aria-label="New chat"
349
+ title="New chat"
350
+ >${I_PLUS()}</xm-button>`
351
+ : html `<xm-button
352
+ class="chat-sidebar__new"
353
+ variant="ghost"
354
+ full-width
355
+ aria-label="New chat"
356
+ >
357
+ <span slot="icon-left">${I_PLUS()}</span>
358
+ New chat
359
+ <span slot="icon-right" class="ds-kbd">⌘N</span>
360
+ </xm-button>`}
361
+
362
+ ${!collapsed
363
+ ? html `
364
+ <div class="chat-sidebar__search chat-sidebar__search--full">
365
+ <span class="ds-search ds-search--on-canvas">
366
+ <span class="ds-search__icon" aria-hidden="true">${I_SEARCH(13)}</span>
367
+ <input
368
+ type="text"
369
+ class="ds-search__input"
370
+ .value=${this._searchValue}
371
+ placeholder="Search conversations"
372
+ spellcheck="false"
373
+ @input=${this._onSearchInput}
374
+ />
375
+ ${this._searchValue
376
+ ? html `<button
377
+ type="button"
378
+ class="ds-search__clear"
379
+ aria-label="Clear search"
380
+ title="Clear search"
381
+ @click=${this._onSearchClear}
382
+ >${svg `
383
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none"
384
+ stroke="currentColor" stroke-width="2" stroke-linecap="round"
385
+ stroke-linejoin="round" class="ds-icon">
386
+ <path d="M6 6l12 12M18 6L6 18" />
387
+ </svg>`}</button>`
388
+ : nothing}
389
+ </span>
390
+ </div>
391
+ `
392
+ : html `
393
+ <xm-button
394
+ variant="ghost"
395
+ icon-only
396
+ class="chat-sidebar__search--icon-only"
397
+ aria-label="Search conversations"
398
+ title="Search conversations"
399
+ >${I_SEARCH(14)}</xm-button>
400
+ `}
401
+
402
+ <nav class="chat-sidebar__list scroll-canvas" aria-label="Recent chats">
403
+ ${collapsed
404
+ ? this._renderCollapsedList(groups)
405
+ : this._renderExpandedList(groups)}
406
+ </nav>
407
+ </aside>
408
+ `;
409
+ }
410
+ _renderCollapsedList(groups) {
411
+ const all = groups.flatMap((g) => g.items || []);
412
+ return all.map((c, i) => html `
413
+ <xm-sidebar-item
414
+ collapsed
415
+ ?active=${this._activeId === c.id}
416
+ title="${c.title}"
417
+ @click=${() => this._selectConv(c.id)}
418
+ ></xm-sidebar-item>
419
+ ${i === 2 ? html `<div class="chat-sidebar__divider" aria-hidden="true"></div>` : nothing}
420
+ `);
421
+ }
422
+ _renderExpandedList(groups) {
423
+ return groups.map((group) => html `
424
+ <div class="sidebar-item sidebar-item--row sidebar-item--eyebrow">${group.group}</div>
425
+ ${(group.items || []).map((c) => html `
426
+ <xm-sidebar-item
427
+ layout="stacked"
428
+ ?active=${this._activeId === c.id}
429
+ title="${c.title}"
430
+ @click=${() => this._selectConv(c.id)}
431
+ ></xm-sidebar-item>
432
+ `)}
433
+ `);
434
+ }
435
+ /* ---------- topbar render ---------- */
436
+ _renderTopBar() {
437
+ const showSubtitle = !this._hasEmpty;
438
+ return html `
439
+ <header class="chat-shell__top">
440
+ <div class="chat-shell__top-left">
441
+ <span class="chat-shell__top-title">${this.topTitle || "New Conversation"}</span>
442
+ ${showSubtitle
443
+ ? html `
444
+ <span class="chat-shell__top-sub">
445
+ <span>${(this.thread || []).length} messages</span>
446
+ <span class="ds-sep">·</span>
447
+ <span>updated now</span>
448
+ </span>
449
+ `
450
+ : nothing}
451
+ </div>
452
+ <div class="chat-shell__top-right">
453
+ <xm-button variant="ghost" icon-only aria-label="Open debug panel" title="Open debug panel">${I_TRACE()}</xm-button>
454
+ <xm-button variant="ghost" icon-only aria-label="Conversation settings" title="Conversation settings">${I_SETTINGS()}</xm-button>
455
+ </div>
456
+ </header>
457
+ `;
458
+ }
459
+ /* ---------- bubble row render ----------
460
+ Bubbles receive their body via `.text=` or `.bodyHtml=` properties.
461
+ This avoids the lit-marker-vs-snapshot race that would happen if
462
+ we put text content between <xm-bubble>...</xm-bubble> in the
463
+ parent template — the bubble's own template controls its body
464
+ so lit's diffing stays consistent across re-renders. */
465
+ _renderBubbleRow(m, idx) {
466
+ const isUser = m.from === "user";
467
+ const variant = isUser ? "user" : "bot";
468
+ const copied = this._copiedIdx === idx;
469
+ return html `
470
+ <div class="chat-shell__row chat-shell__row--${variant}">
471
+ <xm-bubble-group variant="${variant}">
472
+ <xm-bubble
473
+ variant="${variant}"
474
+ .attachments=${m.atts || []}
475
+ .artifact=${m.artifact || null}
476
+ ?download=${!!m.artifact}
477
+ .text=${m.html ? "" : (m.text || "")}
478
+ .bodyHtml=${m.html || ""}
479
+ @artifact-open=${this._onArtifactOpen}
480
+ ></xm-bubble>
481
+ <xm-bubble-actions ts="${m.ts || ""}">
482
+ <xm-button variant="ghost-canvas" icon-only size="sm" aria-label="Regenerate">${I_REGEN()}</xm-button>
483
+ ${!isUser
484
+ ? html `<xm-button variant="ghost-canvas" icon-only size="sm" aria-label="View trace">${I_TRACE(16)}</xm-button>`
485
+ : html `<xm-button variant="ghost-canvas" icon-only size="sm" aria-label="Edit message">${I_EDIT()}</xm-button>`}
486
+ <xm-button
487
+ variant="ghost-canvas"
488
+ icon-only
489
+ size="sm"
490
+ aria-label="${copied ? "Copied" : "Copy message"}"
491
+ title="${copied ? "Copied" : "Copy message"}"
492
+ @click=${(e) => this._onCopyMessage(e, idx)}
493
+ >${copied ? I_CHECK() : I_COPY()}</xm-button>
494
+ </xm-bubble-actions>
495
+ </xm-bubble-group>
496
+ </div>
497
+ `;
498
+ }
499
+ /* ---------- shell render ---------- */
500
+ render() {
501
+ const cls = [
502
+ "chat-shell",
503
+ this.fullViewport ? "chat-shell--full-viewport" : "",
504
+ this._collapsed ? "chat-shell--collapsed" : "",
505
+ this._openArtifact ? "chat-shell--has-artifact" : "",
506
+ ].filter(Boolean).join(" ");
507
+ const showEmpty = this._hasEmpty;
508
+ const thread = this.thread || [];
509
+ return html `
510
+ <div class="${cls}">
511
+ ${this._renderSidebar()}
512
+ ${this._renderTopBar()}
513
+ <div class="chat-shell__center">
514
+ ${showEmpty
515
+ ? html `<div data-mount="empty" style="display:contents"></div>`
516
+ : html `
517
+ <div class="chat-shell__thread scroll-canvas">
518
+ <div class="chat-shell__thread-inner">
519
+ ${thread.map((m, i) => this._renderBubbleRow(m, i))}
520
+ </div>
521
+ </div>
522
+ <div class="chat-shell__composer">
523
+ <xm-composer
524
+ ?processing=${this.processing}
525
+ thinking-label="${this.thinkingLabel}"
526
+ show-attachments
527
+ .initialFocus=${false}
528
+ @send=${this._onComposerSend}
529
+ @cancel=${this._onComposerCancel}
530
+ ></xm-composer>
531
+ </div>
532
+ `}
533
+ </div>
534
+ ${this._openArtifact
535
+ ? html `
536
+ <aside class="chat-shell__artifact-slot" aria-label="Artifact viewer">
537
+ <xm-artifact
538
+ title="${this._openArtifact.title || ""}"
539
+ .sub=${this._openArtifact.sub || []}
540
+ show-refresh
541
+ show-close
542
+ @close=${this._onArtifactClose}
543
+ ></xm-artifact>
544
+ </aside>
545
+ `
546
+ : nothing}
547
+ </div>
548
+ `;
549
+ }
550
+ };
551
+ __decorate([
552
+ property({ attribute: false })
553
+ ], XmChatShell.prototype, "conversations", void 0);
554
+ __decorate([
555
+ property({ attribute: false })
556
+ ], XmChatShell.prototype, "thread", void 0);
557
+ __decorate([
558
+ property({ type: Boolean })
559
+ ], XmChatShell.prototype, "processing", void 0);
560
+ __decorate([
561
+ property({ type: String, attribute: "thinking-label" })
562
+ ], XmChatShell.prototype, "thinkingLabel", void 0);
563
+ __decorate([
564
+ property({ type: String, attribute: "top-title" })
565
+ ], XmChatShell.prototype, "topTitle", void 0);
566
+ __decorate([
567
+ property({ type: Boolean, attribute: "full-viewport" })
568
+ ], XmChatShell.prototype, "fullViewport", void 0);
569
+ __decorate([
570
+ state()
571
+ ], XmChatShell.prototype, "_collapsed", void 0);
572
+ __decorate([
573
+ state()
574
+ ], XmChatShell.prototype, "_activeId", void 0);
575
+ __decorate([
576
+ state()
577
+ ], XmChatShell.prototype, "_openArtifact", void 0);
578
+ __decorate([
579
+ state()
580
+ ], XmChatShell.prototype, "_hasEmpty", void 0);
581
+ __decorate([
582
+ state()
583
+ ], XmChatShell.prototype, "_searchValue", void 0);
584
+ __decorate([
585
+ state()
586
+ ], XmChatShell.prototype, "_copiedIdx", void 0);
587
+ XmChatShell = __decorate([
588
+ customElement("xm-chat-shell")
589
+ ], XmChatShell);