@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,713 @@
1
+ /*
2
+ composer/index.ts — Lit port of components/composer/index.jsx.
3
+
4
+ <xm-composer> — canonical chat composer.
5
+
6
+ Public properties (mirror the React props):
7
+ initialValue prefill text
8
+ initialFocus focus textarea on first render (default true)
9
+ processing render thinking pill + stop button
10
+ thinkingLabel text for the thinking pill
11
+ (default "Routing query through Planning Path…")
12
+ maxChars char counter cap (default 5000)
13
+ counterAt threshold to show counter (0..1) (default 0.8)
14
+ placeholder textarea placeholder
15
+ (default "Describe what you want to create…")
16
+ showAttachments enable attach button + drop/paste + chips/cards/tray
17
+ value controlled-mode mirror property
18
+ (set via JS to seed text from a host)
19
+ attachments controlled-mode array — same idea for files
20
+
21
+ Events (all bubble, composed):
22
+ send detail: { text, attachments: [...] }
23
+ cancel
24
+ value-change detail: { value }
25
+ attachments-change detail: { attachments }
26
+
27
+ Light DOM. Uses components/composer/index.css + components/button/index.css.
28
+ */
29
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
30
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
31
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
32
+ 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;
33
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
34
+ };
35
+ import { LitElement, html, svg, nothing } from "lit";
36
+ import { customElement, property, state } from "lit/decorators.js";
37
+ // Side-effect import: registers <xm-button> for use in the toolbar.
38
+ import "../button/index.js";
39
+ /* ---------- attachment helpers (verbatim from JSX) ---------- */
40
+ const STRIP_LIMIT = 10;
41
+ const HARD_CAP = 50;
42
+ const PER_FILE_MAX = 25 * 1024 * 1024;
43
+ const TEXTY_EXTS = new Set([
44
+ "TXT", "MD", "JSON", "YAML", "YML", "CSV", "LOG", "XML", "HTML", "CSS", "JS",
45
+ "TS", "JSX", "TSX", "PY", "GO", "RS", "SH", "SQL", "TOML", "INI", "ENV"
46
+ ]);
47
+ const fmtSize = (b) => {
48
+ if (b < 1024)
49
+ return b + " B";
50
+ if (b < 1024 * 1024)
51
+ return (b / 1024).toFixed(b < 10240 ? 1 : 0) + " KB";
52
+ return (b / (1024 * 1024)).toFixed(1) + " MB";
53
+ };
54
+ let _nextAttId = 1;
55
+ const nextAttId = () => _nextAttId++;
56
+ /* ---------- inline icon SVG fragments ---------- */
57
+ const I_PAPERCLIP = (size = 14) => svg `
58
+ <svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none"
59
+ stroke="currentColor" stroke-width="1.8" stroke-linecap="round"
60
+ stroke-linejoin="round" class="ds-icon">
61
+ <path d="M21.4 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 17.93 8.83l-8.06 8.06a2 2 0 0 1-2.83-2.83l7.07-7.07" />
62
+ </svg>
63
+ `;
64
+ const I_SEND = (size = 14) => svg `
65
+ <svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none"
66
+ stroke="currentColor" stroke-width="2" stroke-linecap="round"
67
+ stroke-linejoin="round" class="ds-icon">
68
+ <path d="M7 17L17 7" />
69
+ <path d="M9 7h8v8" />
70
+ </svg>
71
+ `;
72
+ const I_PLAY = (size = 14) => svg `
73
+ <svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none"
74
+ stroke="currentColor" stroke-width="1.8" stroke-linecap="round"
75
+ stroke-linejoin="round" class="ds-icon">
76
+ <path d="M5 4l14 8-14 8V4z" />
77
+ </svg>
78
+ `;
79
+ const I_FILE = (size = 14) => svg `
80
+ <svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none"
81
+ stroke="currentColor" stroke-width="1.8" stroke-linecap="round"
82
+ stroke-linejoin="round" class="ds-icon">
83
+ <path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z" />
84
+ <path d="M14 3v5h5" />
85
+ </svg>
86
+ `;
87
+ const I_X = (size = 10) => svg `
88
+ <svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none"
89
+ stroke="currentColor" stroke-width="2.6" stroke-linecap="round"
90
+ stroke-linejoin="round" class="ds-icon">
91
+ <path d="M6 6l12 12M18 6L6 18" />
92
+ </svg>
93
+ `;
94
+ const I_WARN = (size = 14) => svg `
95
+ <svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none"
96
+ stroke="currentColor" stroke-width="2" stroke-linecap="round"
97
+ stroke-linejoin="round" class="ds-icon">
98
+ <path d="M12 9v4" />
99
+ <path d="M12 17h.01" />
100
+ <path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
101
+ </svg>
102
+ `;
103
+ const I_CARET = (size = 12) => svg `
104
+ <svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none"
105
+ stroke="currentColor" stroke-width="2" stroke-linecap="round"
106
+ stroke-linejoin="round" class="ds-icon">
107
+ <polyline points="6 9 12 15 18 9" />
108
+ </svg>
109
+ `;
110
+ let XmComposer = class XmComposer extends LitElement {
111
+ constructor() {
112
+ super(...arguments);
113
+ this.initialValue = "";
114
+ this.initialFocus = true;
115
+ this.processing = false;
116
+ this.thinkingLabel = "";
117
+ this.maxChars = 5000;
118
+ this.counterAt = 0.8;
119
+ this.placeholder = "Describe what you want to create…";
120
+ this.showAttachments = false;
121
+ this.value = null;
122
+ this.attachments = null;
123
+ this._value = "";
124
+ this._atts = [];
125
+ this._warning = null;
126
+ this._trayOpen = false;
127
+ this._dragging = false;
128
+ this._dragDepth = 0;
129
+ this._warnTimer = null;
130
+ this._didInitialFocus = false;
131
+ this._lastValueProp = null;
132
+ this._lastAttsProp = null;
133
+ this._clearAtts = () => {
134
+ this._atts = [];
135
+ this._trayOpen = false;
136
+ this._warning = null;
137
+ };
138
+ this._send = () => {
139
+ const t = this._value.trim();
140
+ const hasAtts = this._atts.length > 0;
141
+ const overLimit = this._value.length > this.maxChars;
142
+ if (!t && !hasAtts)
143
+ return;
144
+ if (overLimit)
145
+ return;
146
+ this.dispatchEvent(new CustomEvent("send", {
147
+ detail: { text: t || "(attachment only)", attachments: this._atts.slice() },
148
+ bubbles: true, composed: true,
149
+ }));
150
+ this._value = "";
151
+ this._atts = [];
152
+ this._trayOpen = false;
153
+ };
154
+ this._cancel = () => {
155
+ this.dispatchEvent(new CustomEvent("cancel", { bubbles: true, composed: true }));
156
+ };
157
+ this._onInput = (e) => {
158
+ this._value = e.target.value;
159
+ };
160
+ this._onKeyDown = (e) => {
161
+ if (e.key === "Enter" && !e.shiftKey && !this.processing) {
162
+ e.preventDefault();
163
+ this._send();
164
+ }
165
+ };
166
+ this._onPaste = (e) => {
167
+ if (!this.showAttachments)
168
+ return;
169
+ const items = e.clipboardData?.items;
170
+ if (!items)
171
+ return;
172
+ const files = [];
173
+ for (const it of items) {
174
+ if (it.kind === "file") {
175
+ const f = it.getAsFile();
176
+ if (f)
177
+ files.push(f);
178
+ }
179
+ }
180
+ if (files.length) {
181
+ e.preventDefault();
182
+ this._addFiles(files);
183
+ }
184
+ };
185
+ this._onDragEnter = (e) => {
186
+ if (!this.showAttachments)
187
+ return;
188
+ if (!e.dataTransfer?.types?.includes("Files"))
189
+ return;
190
+ e.preventDefault();
191
+ this._dragDepth++;
192
+ this._dragging = true;
193
+ };
194
+ this._onDragOver = (e) => {
195
+ if (!this.showAttachments)
196
+ return;
197
+ if (e.dataTransfer?.types?.includes("Files"))
198
+ e.preventDefault();
199
+ };
200
+ this._onDragLeave = () => {
201
+ if (!this.showAttachments)
202
+ return;
203
+ this._dragDepth = Math.max(0, this._dragDepth - 1);
204
+ if (this._dragDepth === 0)
205
+ this._dragging = false;
206
+ };
207
+ this._onDrop = (e) => {
208
+ if (!this.showAttachments)
209
+ return;
210
+ if (!e.dataTransfer?.files?.length)
211
+ return;
212
+ e.preventDefault();
213
+ this._dragDepth = 0;
214
+ this._dragging = false;
215
+ this._addFiles(e.dataTransfer.files);
216
+ };
217
+ this._onBoxClick = (e) => {
218
+ const target = e.target;
219
+ if (target instanceof Element && target.closest("textarea, button, xm-button, a, input, label, [role='button']"))
220
+ return;
221
+ const ta = this.querySelector(".composer__field");
222
+ if (!ta)
223
+ return;
224
+ ta.focus();
225
+ const len = ta.value.length;
226
+ try {
227
+ ta.setSelectionRange(len, len);
228
+ }
229
+ catch (_) { }
230
+ };
231
+ this._onPickFile = () => {
232
+ this.querySelector(".composer__file-input")?.click();
233
+ };
234
+ this._onFileInput = (e) => {
235
+ const input = e.target;
236
+ if (input.files?.length)
237
+ this._addFiles(input.files);
238
+ input.value = "";
239
+ };
240
+ }
241
+ createRenderRoot() {
242
+ return this;
243
+ }
244
+ connectedCallback() {
245
+ super.connectedCallback();
246
+ // Make the host transparent to flex/grid layout so the inner
247
+ // .composer-shell behaves as the direct child of whatever container
248
+ // the host sits in. Matters for selectors like
249
+ // `.chat-shell__composer > .composer-shell` in components/chat/index.css.
250
+ if (!this.style.display)
251
+ this.style.display = "contents";
252
+ if (this.value != null) {
253
+ this._value = this.value;
254
+ this._lastValueProp = this.value;
255
+ }
256
+ else if (this.initialValue) {
257
+ this._value = this.initialValue;
258
+ }
259
+ if (Array.isArray(this.attachments)) {
260
+ this._atts = this.attachments.slice();
261
+ this._lastAttsProp = this.attachments;
262
+ }
263
+ }
264
+ willUpdate(changed) {
265
+ // Sync from controlled props when they change identity (mirrors the
266
+ // React useEffect on controlledValue / controlledAttachments).
267
+ if (changed.has("value")) {
268
+ if (this.value != null && this.value !== this._lastValueProp) {
269
+ this._value = this.value;
270
+ this._lastValueProp = this.value;
271
+ }
272
+ }
273
+ if (changed.has("attachments")) {
274
+ if (Array.isArray(this.attachments) && this.attachments !== this._lastAttsProp) {
275
+ this._atts = this.attachments.slice();
276
+ this._lastAttsProp = this.attachments;
277
+ }
278
+ }
279
+ }
280
+ updated(changed) {
281
+ // Autoresize textarea — match JSX useLayoutEffect (max 240px).
282
+ const ta = this.querySelector(".composer__field");
283
+ if (ta) {
284
+ ta.style.height = "auto";
285
+ if (ta.scrollHeight > 0) {
286
+ ta.style.height = Math.min(ta.scrollHeight, 240) + "px";
287
+ }
288
+ else {
289
+ ta.style.removeProperty("height");
290
+ }
291
+ }
292
+ // Initial focus once on first render.
293
+ if (!this._didInitialFocus && this.initialFocus && ta) {
294
+ ta.focus();
295
+ const len = ta.value.length;
296
+ try {
297
+ ta.setSelectionRange(len, len);
298
+ }
299
+ catch (_) { }
300
+ this._didInitialFocus = true;
301
+ }
302
+ // Notify listeners on value changes from internal updates.
303
+ if (changed.has("_value")) {
304
+ this.dispatchEvent(new CustomEvent("value-change", {
305
+ detail: { value: this._value },
306
+ bubbles: true, composed: true,
307
+ }));
308
+ }
309
+ if (changed.has("_atts")) {
310
+ this.dispatchEvent(new CustomEvent("attachments-change", {
311
+ detail: { attachments: this._atts },
312
+ bubbles: true, composed: true,
313
+ }));
314
+ }
315
+ }
316
+ /* ---------- attachment handlers ---------- */
317
+ _flashWarning(msg) {
318
+ if (this._warnTimer)
319
+ clearTimeout(this._warnTimer);
320
+ if (!msg) {
321
+ this._warning = null;
322
+ return;
323
+ }
324
+ this._warning = { msg };
325
+ this._warnTimer = setTimeout(() => { this._warning = null; }, 5000);
326
+ }
327
+ _addFiles(fileList) {
328
+ const incoming = Array.from(fileList);
329
+ const current = this._atts;
330
+ const slots = Math.max(0, HARD_CAP - current.length);
331
+ const accepted = [];
332
+ let dupes = 0, oversized = 0, overCap = 0;
333
+ for (const f of incoming) {
334
+ if (current.some((a) => a.name === f.name && a.size === f.size)) {
335
+ dupes++;
336
+ continue;
337
+ }
338
+ if (f.size > PER_FILE_MAX) {
339
+ oversized++;
340
+ continue;
341
+ }
342
+ if (accepted.length >= slots) {
343
+ overCap++;
344
+ continue;
345
+ }
346
+ accepted.push(f);
347
+ }
348
+ const next = [...current];
349
+ const newlyAdded = [];
350
+ for (const f of accepted) {
351
+ const id = nextAttId();
352
+ const ext = (f.name.split(".").pop() || "").toUpperCase();
353
+ const isImage = f.type.startsWith("image/");
354
+ const att = {
355
+ id, name: f.name, kind: isImage ? "image" : "file",
356
+ size: f.size, ext,
357
+ };
358
+ next.push(att);
359
+ newlyAdded.push({ att, file: f, isImage, ext });
360
+ }
361
+ this._atts = next;
362
+ // Async readers for previews / line counts — same logic as JSX.
363
+ for (const { att, file, isImage, ext } of newlyAdded) {
364
+ if (isImage) {
365
+ const reader = new FileReader();
366
+ reader.onload = (e) => {
367
+ const result = e.target?.result;
368
+ if (typeof result !== "string")
369
+ return;
370
+ this._atts = this._atts.map((a) => a.id === att.id ? { ...a, dataUrl: result } : a);
371
+ };
372
+ reader.readAsDataURL(file);
373
+ }
374
+ else if (TEXTY_EXTS.has(ext) && file.size <= 2 * 1024 * 1024) {
375
+ const reader = new FileReader();
376
+ reader.onload = (e) => {
377
+ const t = (e.target?.result ?? "");
378
+ let count = 0;
379
+ for (let i = 0; i < t.length; i++)
380
+ if (t.charCodeAt(i) === 10)
381
+ count++;
382
+ if (t.length && t.charCodeAt(t.length - 1) !== 10)
383
+ count++;
384
+ this._atts = this._atts.map((a) => a.id === att.id ? { ...a, lines: count } : a);
385
+ };
386
+ reader.readAsText(file);
387
+ }
388
+ else if (ext === "PDF") {
389
+ // Synchronous estimate for the placeholder; React mutated the att
390
+ // object directly. We patch through a state update for parity.
391
+ this._atts = this._atts.map((a) => a.id === att.id
392
+ ? { ...a, pages: Math.max(1, Math.round(file.size / (60 * 1024))) }
393
+ : a);
394
+ }
395
+ }
396
+ const parts = [];
397
+ if (overCap)
398
+ parts.push(`${overCap} skipped (${HARD_CAP}-file cap reached)`);
399
+ if (oversized)
400
+ parts.push(`${oversized} too large (>${fmtSize(PER_FILE_MAX)} each)`);
401
+ if (dupes)
402
+ parts.push(`${dupes} duplicate${dupes > 1 ? "s" : ""} ignored`);
403
+ this._flashWarning(parts.length ? parts.join(" · ") : null);
404
+ }
405
+ _removeAtt(id) {
406
+ const next = this._atts.filter((a) => a.id !== id);
407
+ if (next.length <= STRIP_LIMIT)
408
+ this._trayOpen = false;
409
+ this._atts = next;
410
+ }
411
+ /* ---------- render fragments ---------- */
412
+ _renderAttCard(att) {
413
+ if (att.kind === "image" && att.dataUrl) {
414
+ const meta = att.dims ? `${att.dims} · ${fmtSize(att.size)}` : fmtSize(att.size);
415
+ return html `
416
+ <div class="att-card att-card--media" title="${att.name}">
417
+ <span
418
+ class="att-card__media"
419
+ style="background-image:url(${att.dataUrl})"
420
+ role="img"
421
+ aria-label="${att.name}"
422
+ ></span>
423
+ <span class="att-card__ext">${att.ext || "IMG"}</span>
424
+ <span class="att-card__meta">${meta}</span>
425
+ <button class="att-card__x" aria-label="remove attachment"
426
+ @click=${() => this._removeAtt(att.id)}>${I_X()}</button>
427
+ </div>
428
+ `;
429
+ }
430
+ const primary = att.lines != null ? `${att.lines.toLocaleString()} lines` :
431
+ att.pages != null ? `${att.pages} page${att.pages === 1 ? "" : "s"}` : null;
432
+ const secondary = fmtSize(att.size);
433
+ return html `
434
+ <div class="att-card att-card--text" title="${att.name}">
435
+ <span class="att-card__name">${att.name}</span>
436
+ ${primary
437
+ ? html `
438
+ <span class="att-card__sub">
439
+ <span class="att-card__sub-primary">${primary}</span>
440
+ <span class="att-card__sub-secondary">${secondary}</span>
441
+ </span>
442
+ `
443
+ : html `<span class="att-card__sub">${secondary}</span>`}
444
+ <span class="att-card__ext">${att.ext || "FILE"}</span>
445
+ <button class="att-card__x" aria-label="remove attachment"
446
+ @click=${() => this._removeAtt(att.id)}>${I_X()}</button>
447
+ </div>
448
+ `;
449
+ }
450
+ _renderAttRow(att) {
451
+ const ext = att.kind === "image" ? (att.ext || "PNG") : (att.ext || "FILE");
452
+ const meta = att.lines != null ? `${att.lines.toLocaleString()} lines` :
453
+ att.pages != null ? `${att.pages} page${att.pages === 1 ? "" : "s"}` :
454
+ att.dims ? att.dims : null;
455
+ return html `
456
+ <div class="att__row">
457
+ ${att.kind === "image" && att.dataUrl
458
+ ? html `<span class="att__row-thumb"
459
+ style="background-image:url(${att.dataUrl})"></span>`
460
+ : html `<span class="att__row-icon">${I_FILE()}</span>`}
461
+ <span class="att__row-name" title="${att.name}">${att.name}</span>
462
+ ${meta
463
+ ? html `
464
+ <span class="att__row-meta">
465
+ <span class="att__row-meta-primary">${meta}</span>
466
+ <span class="att__row-meta-secondary">${ext} · ${fmtSize(att.size)}</span>
467
+ </span>
468
+ `
469
+ : html `<span class="att__row-size">${ext} · ${fmtSize(att.size)}</span>`}
470
+ <button class="att__row-x" aria-label="remove attachment"
471
+ @click=${() => this._removeAtt(att.id)}>${I_X(9)}</button>
472
+ </div>
473
+ `;
474
+ }
475
+ _renderAttachments() {
476
+ const atts = this._atts;
477
+ const warning = this._warning;
478
+ if (atts.length === 0 && !warning)
479
+ return nothing;
480
+ const n = atts.length;
481
+ const sumSize = atts.reduce((s, a) => s + (a.size || 0), 0);
482
+ return html `
483
+ <div class="att">
484
+ ${warning
485
+ ? html `<div class="att__warn" role="status">${I_WARN()}<span>${warning.msg}</span></div>`
486
+ : nothing}
487
+ ${n > 0
488
+ ? html `
489
+ <div class="att__header">
490
+ <span class="att__header-left">
491
+ <span class="att__pip">${n}</span>
492
+ ${n === 1 ? " attachment" : " attachments"} · ${fmtSize(sumSize)}
493
+ </span>
494
+ <span class="att__header-right">
495
+ <button class="att__link" @click=${this._clearAtts}>Clear all</button>
496
+ </span>
497
+ </div>
498
+ ${n <= STRIP_LIMIT
499
+ ? html `<div class="att__grid">${atts.map((a) => this._renderAttCard(a))}</div>`
500
+ : html `
501
+ <button
502
+ type="button"
503
+ class="att__bundle"
504
+ aria-expanded="${this._trayOpen}"
505
+ @click=${() => { this._trayOpen = !this._trayOpen; }}
506
+ >
507
+ <span class="att__bundle-stack" aria-hidden="true">
508
+ <span></span><span></span>
509
+ <span>${I_FILE()}</span>
510
+ </span>
511
+ <span class="att__bundle-meta">
512
+ <span class="att__bundle-name">${n} files bundled</span>
513
+ <span class="att__bundle-sub">${fmtSize(sumSize)} · ${this._trayOpen ? "Hide list" : "View list"}</span>
514
+ </span>
515
+ <span class="att__bundle-caret">${I_CARET()}</span>
516
+ </button>
517
+ ${this._trayOpen
518
+ ? html `<div class="att__tray" role="list">
519
+ ${atts.map((a) => this._renderAttRow(a))}
520
+ </div>`
521
+ : nothing}
522
+ `}
523
+ `
524
+ : nothing}
525
+ </div>
526
+ `;
527
+ }
528
+ _renderHint() {
529
+ if (this.processing) {
530
+ return html `
531
+ <span class="composer__hint">
532
+ Click <span class="composer__stop-icon"
533
+ style="vertical-align:-1px;margin:0 2px"
534
+ aria-hidden="true"></span> to cancel current request
535
+ </span>
536
+ `;
537
+ }
538
+ const n = this._atts.length;
539
+ if (n >= HARD_CAP) {
540
+ return html `<span class="composer__hint">${HARD_CAP}/${HARD_CAP} attached · cap reached</span>`;
541
+ }
542
+ if (n > STRIP_LIMIT) {
543
+ return html `<span class="composer__hint">${n} attached · expand list to manage</span>`;
544
+ }
545
+ if (n > 0) {
546
+ return html `<span class="composer__hint">${n} attached · paste with <kbd>⌘V</kbd></span>`;
547
+ }
548
+ return html `<span class="composer__hint">Enter to send · <kbd>Shift</kbd>+<kbd>Enter</kbd> for new line</span>`;
549
+ }
550
+ render() {
551
+ const text = this._value;
552
+ const hasText = text.trim().length > 0;
553
+ const hasAtts = this._atts.length > 0;
554
+ const charCount = text.length;
555
+ const overLimit = charCount > this.maxChars;
556
+ const showCounter = charCount >= this.maxChars * this.counterAt;
557
+ const canSend = (hasText || hasAtts) && !overLimit && !this.processing;
558
+ return html `
559
+ <div class="composer-shell">
560
+ <div
561
+ class="composer ${this._dragging ? "composer--dragging" : ""}"
562
+ @click=${this._onBoxClick}
563
+ @dragenter=${this._onDragEnter}
564
+ @dragover=${this._onDragOver}
565
+ @dragleave=${this._onDragLeave}
566
+ @drop=${this._onDrop}
567
+ >
568
+ ${this.showAttachments ? this._renderAttachments() : nothing}
569
+
570
+ ${this.processing
571
+ ? html `
572
+ <div class="composer__thinking" role="status" aria-live="polite">
573
+ <span class="composer__thinking-dots" aria-hidden="true">
574
+ <span class="composer__thinking-dot"></span>
575
+ <span class="composer__thinking-dot"></span>
576
+ <span class="composer__thinking-dot"></span>
577
+ </span>
578
+ <span class="composer__thinking-label">${this.thinkingLabel || "Routing query through Planning Path…"}</span>
579
+ </div>
580
+ `
581
+ : nothing}
582
+
583
+ <textarea
584
+ class="composer__field scroll-canvas"
585
+ name="message"
586
+ rows="1"
587
+ .value=${text}
588
+ placeholder="${this.placeholder}"
589
+ aria-label="Message"
590
+ @input=${this._onInput}
591
+ @keydown=${this._onKeyDown}
592
+ @paste=${this._onPaste}
593
+ ></textarea>
594
+
595
+ <div class="composer__toolbar">
596
+ <div class="composer__toolbar-left">
597
+ ${this.showAttachments
598
+ ? html `
599
+ <xm-button
600
+ variant="ghost"
601
+ size="sm"
602
+ icon-only
603
+ aria-label="attach"
604
+ title="Attach file"
605
+ @click=${this._onPickFile}
606
+ >${I_PAPERCLIP()}</xm-button>
607
+ <input
608
+ type="file"
609
+ multiple
610
+ hidden
611
+ class="composer__file-input"
612
+ @change=${this._onFileInput}
613
+ />
614
+ `
615
+ : nothing}
616
+ </div>
617
+ <div class="composer__toolbar-right">
618
+ ${showCounter && !this.processing
619
+ ? html `
620
+ <span class="composer__counter ${overLimit ? "composer__counter--over" : "composer__counter--warn"}">
621
+ ${charCount.toLocaleString()} / ${this.maxChars.toLocaleString()}
622
+ </span>
623
+ `
624
+ : nothing}
625
+
626
+ ${!this.processing
627
+ ? html `
628
+ <xm-button
629
+ variant="primary"
630
+ size="sm"
631
+ aria-label="send"
632
+ ?disabled=${!canSend}
633
+ title="${overLimit ? "Message too long" : canSend ? "Send (Enter)" : "Type a message to send"}"
634
+ @click=${this._send}
635
+ >
636
+ <span slot="icon-left">${I_SEND()}</span>
637
+ Send
638
+ </xm-button>
639
+ `
640
+ : html `
641
+ <xm-button
642
+ variant="ghost-canvas"
643
+ size="sm"
644
+ icon-only
645
+ disabled
646
+ aria-label="sending"
647
+ title="Generating response…"
648
+ >${I_PLAY()}</xm-button>
649
+ <xm-button
650
+ variant="ghost-canvas"
651
+ size="sm"
652
+ icon-only
653
+ aria-label="stop"
654
+ title="Cancel current request"
655
+ @click=${this._cancel}
656
+ ><span class="composer__stop-icon"></span></xm-button>
657
+ `}
658
+ </div>
659
+ </div>
660
+ </div>
661
+ <div class="composer-shell__bottom">${this._renderHint()}</div>
662
+ </div>
663
+ `;
664
+ }
665
+ };
666
+ __decorate([
667
+ property({ type: String, attribute: "initial-value" })
668
+ ], XmComposer.prototype, "initialValue", void 0);
669
+ __decorate([
670
+ property({ type: Boolean, attribute: "initial-focus" })
671
+ ], XmComposer.prototype, "initialFocus", void 0);
672
+ __decorate([
673
+ property({ type: Boolean })
674
+ ], XmComposer.prototype, "processing", void 0);
675
+ __decorate([
676
+ property({ type: String, attribute: "thinking-label" })
677
+ ], XmComposer.prototype, "thinkingLabel", void 0);
678
+ __decorate([
679
+ property({ type: Number, attribute: "max-chars" })
680
+ ], XmComposer.prototype, "maxChars", void 0);
681
+ __decorate([
682
+ property({ type: Number, attribute: "counter-at" })
683
+ ], XmComposer.prototype, "counterAt", void 0);
684
+ __decorate([
685
+ property({ type: String })
686
+ ], XmComposer.prototype, "placeholder", void 0);
687
+ __decorate([
688
+ property({ type: Boolean, attribute: "show-attachments" })
689
+ ], XmComposer.prototype, "showAttachments", void 0);
690
+ __decorate([
691
+ property({ attribute: false })
692
+ ], XmComposer.prototype, "value", void 0);
693
+ __decorate([
694
+ property({ attribute: false })
695
+ ], XmComposer.prototype, "attachments", void 0);
696
+ __decorate([
697
+ state()
698
+ ], XmComposer.prototype, "_value", void 0);
699
+ __decorate([
700
+ state()
701
+ ], XmComposer.prototype, "_atts", void 0);
702
+ __decorate([
703
+ state()
704
+ ], XmComposer.prototype, "_warning", void 0);
705
+ __decorate([
706
+ state()
707
+ ], XmComposer.prototype, "_trayOpen", void 0);
708
+ __decorate([
709
+ state()
710
+ ], XmComposer.prototype, "_dragging", void 0);
711
+ XmComposer = __decorate([
712
+ customElement("xm-composer")
713
+ ], XmComposer);