@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,166 @@
1
+ /* ============================================
2
+ Artifact — Claude-style side-panel viewer.
3
+
4
+ Canvas-surface panel that lives in the artifact
5
+ column of chat.html, or standalone in
6
+ preview/artifact.html. Header (provider chip +
7
+ segmented eye/code toggle + title/sub +
8
+ action cluster) over a scrollable body.
9
+
10
+ The panel rides `surface-container-high` in both
11
+ themes — same family as the chat shell's sidebar/
12
+ topbar. Surface-rooted means text tokens come
13
+ from `on-surface*` (POLICIES rule 7a). The
14
+ primitives flip in light theme via
15
+ _primitives.css, so a single set of rules covers
16
+ both modes. Mirrors the .bubble.bot pattern.
17
+ ============================================ */
18
+
19
+ .artifact {
20
+ background: var(--md-sys-color-surface-container-high);
21
+ border: 0;
22
+ border-left: 1px solid var(--md-sys-color-outline);
23
+ border-radius: 0;
24
+ display: flex;
25
+ flex-direction: column;
26
+ min-height: 0;
27
+ overflow: hidden;
28
+ color: var(--md-sys-color-on-surface);
29
+ }
30
+
31
+ /* ---------- Head ----------
32
+ Padding + min-height match preview/chat.html `.top` so the chat
33
+ topbar and artifact head render at identical heights when both
34
+ are visible. */
35
+ .artifact__head {
36
+ display: flex;
37
+ align-items: center;
38
+ gap: var(--s-3);
39
+ padding: var(--s-3) var(--s-5);
40
+ min-height: 56px;
41
+ box-sizing: border-box;
42
+ flex-shrink: 0;
43
+ /* Hairline seam so the header reads as a band over the body instead of
44
+ floating; outline-variant is the soft internal divider weight. */
45
+ border-bottom: 1px solid var(--md-sys-color-outline-variant);
46
+ }
47
+
48
+ .artifact__head-left {
49
+ display: inline-flex;
50
+ align-items: center;
51
+ gap: var(--s-2);
52
+ flex-shrink: 0;
53
+ }
54
+
55
+ .artifact__head-mid {
56
+ display: flex;
57
+ flex-direction: column;
58
+ gap: var(--s-1);
59
+ min-width: 0;
60
+ flex: 1;
61
+ }
62
+
63
+ .artifact__head-right {
64
+ display: inline-flex;
65
+ align-items: center;
66
+ gap: var(--s-1-5);
67
+ flex-shrink: 0;
68
+ }
69
+
70
+ .artifact__title {
71
+ font: 600 15px/1.2 var(--md-sys-typescale-body-large-font);
72
+ color: var(--md-sys-color-on-surface);
73
+ letter-spacing: -0.01em;
74
+ white-space: nowrap;
75
+ overflow: hidden;
76
+ text-overflow: ellipsis;
77
+ }
78
+
79
+ .artifact__sub {
80
+ font: 400 11px/1 var(--xm-typescale-mono-font);
81
+ color: var(--xm-color-on-surface-soft);
82
+ font-feature-settings: "tnum";
83
+ display: inline-flex;
84
+ align-items: center;
85
+ gap: var(--s-1-5);
86
+ flex-wrap: nowrap;
87
+ white-space: nowrap;
88
+ }
89
+ .artifact__sub > span { white-space: nowrap; }
90
+ /* Inline middot dividers come from the .ds-sep primitive (primitives/index.css);
91
+ no per-component override needed. */
92
+
93
+ /* ---------- View toggle (eye / code) ---------- */
94
+ .artifact__view-toggle {
95
+ display: inline-flex;
96
+ align-items: center;
97
+ padding: var(--s-0-5);
98
+ background: color-mix(in oklab, var(--md-sys-color-on-surface) 8%, transparent);
99
+ border-radius: 8px;
100
+ gap: var(--s-0-5);
101
+ }
102
+
103
+ /* ---------- Body ---------- */
104
+ .artifact__body {
105
+ flex: 1;
106
+ min-height: 0;
107
+ overflow-y: auto;
108
+ scrollbar-gutter: stable;
109
+ /* Horizontal inset matches .artifact__head (--s-5) so the title and the
110
+ first line of body copy share one left edge. */
111
+ padding: var(--s-5) var(--s-5) var(--s-6);
112
+ font:
113
+ var(--md-sys-typescale-body-large-weight)
114
+ var(--md-sys-typescale-body-large-size) /
115
+ var(--md-sys-typescale-body-large-line-height)
116
+ var(--md-sys-typescale-body-large-font);
117
+ color: var(--md-sys-color-on-surface);
118
+ }
119
+
120
+ .artifact__body :where(h1, h2, h3, h4) {
121
+ color: var(--md-sys-color-on-surface);
122
+ letter-spacing: -0.01em;
123
+ margin: var(--s-4-5) 0 var(--s-2);
124
+ }
125
+ .artifact__body h1 { font: 600 20px/1.25 var(--md-sys-typescale-body-large-font); }
126
+ .artifact__body h2 { font: 600 16px/1.3 var(--md-sys-typescale-body-large-font); }
127
+ .artifact__body h3 { font: 600 14px/1.3 var(--md-sys-typescale-body-large-font); }
128
+ .artifact__body :where(h1, h2, h3, h4):first-child { margin-top: 0; }
129
+
130
+ .artifact__body p { margin: 0 0 var(--s-3); }
131
+ .artifact__body ul,
132
+ .artifact__body ol { margin: 0 0 var(--s-3); padding-left: var(--s-5-5); }
133
+ .artifact__body li { margin: var(--s-1) 0; }
134
+ .artifact__body strong { color: var(--md-sys-color-on-surface); font-weight: 600; }
135
+ .artifact__body em { font-style: italic; color: var(--md-sys-color-on-surface); }
136
+ .artifact__body code {
137
+ font: 500 12.5px/1.4 var(--xm-typescale-mono-font);
138
+ padding: 0;
139
+ background: transparent;
140
+ color: var(--md-sys-color-on-surface);
141
+ }
142
+
143
+ .artifact__body pre {
144
+ font: 500 12.5px/1.55 var(--xm-typescale-mono-font);
145
+ color: var(--md-sys-color-on-surface);
146
+ background: color-mix(in oklab, var(--md-sys-color-on-surface) 6%, transparent);
147
+ border: 1px solid color-mix(in oklab, var(--md-sys-color-on-surface) 10%, transparent);
148
+ border-radius: var(--md-sys-shape-corner-extra-small);
149
+ padding: var(--s-3) var(--s-4);
150
+ margin: var(--s-3) 0;
151
+ white-space: pre-wrap;
152
+ overflow-x: auto;
153
+ font-feature-settings: "tnum";
154
+ }
155
+ .artifact__body pre code {
156
+ font: inherit;
157
+ color: inherit;
158
+ }
159
+
160
+ .artifact__body blockquote {
161
+ margin: var(--s-3) 0;
162
+ padding-left: var(--s-3);
163
+ border-left: 2px solid color-mix(in oklab, var(--md-sys-color-on-surface) 30%, transparent);
164
+ color: var(--xm-color-on-surface-soft);
165
+ font-style: italic;
166
+ }
@@ -0,0 +1,37 @@
1
+ import { LitElement } from "lit";
2
+ import type { TemplateResult } from "lit";
3
+ import "../button/index.js";
4
+ type ArtifactView = "preview" | "code";
5
+ declare class XmArtifact extends LitElement {
6
+ title: string;
7
+ sub: string[] | null;
8
+ subCsv: string;
9
+ view: ArtifactView;
10
+ showRefresh: boolean;
11
+ showClose: boolean;
12
+ private _bodies;
13
+ private _uid;
14
+ createRenderRoot(): HTMLElement | DocumentFragment;
15
+ connectedCallback(): void;
16
+ /**
17
+ * Imperatively set the preview/code body nodes from a host. Replaces any
18
+ * authored or previously-set bodies. Used by <xm-chat-shell> to hand the
19
+ * panel its slot content; the chat shell can't author them as direct
20
+ * children of the lit-rendered <xm-artifact> tag without the snapshot
21
+ * timing race that this method sidesteps.
22
+ */
23
+ setBodies(preview: ChildNode[], code: ChildNode[]): void;
24
+ private _setView;
25
+ private _onTabKeydown;
26
+ private _onRefresh;
27
+ private _onClose;
28
+ private _subItems;
29
+ render(): TemplateResult;
30
+ updated(): void;
31
+ }
32
+ declare global {
33
+ interface HTMLElementTagNameMap {
34
+ "xm-artifact": XmArtifact;
35
+ }
36
+ }
37
+ export {};
@@ -0,0 +1,294 @@
1
+ /*
2
+ artifact/index.ts — Lit port of components/artifact/index.jsx.
3
+
4
+ <xm-artifact> — Claude-style side-panel viewer.
5
+
6
+ Authoring:
7
+ <xm-artifact title="..." sub-csv="Document,MD" show-refresh show-close>
8
+ <div data-view="preview"> ...html body for preview... </div>
9
+ <div data-view="code"> ...html body for code... </div>
10
+ </xm-artifact>
11
+
12
+ The component shows whichever child matches the active `view`.
13
+ Children without a data-view attribute are treated as `preview`
14
+ (single-body case).
15
+
16
+ Properties:
17
+ title string
18
+ sub string[] — preferred (set via JS)
19
+ sub-csv string — comma-separated convenience
20
+ attribute for static HTML
21
+ view "preview" | "code" — controlled when host listens
22
+ for `view-change`; otherwise
23
+ local state from initial value
24
+ showRefresh boolean — render the refresh button
25
+ showClose boolean — render the close button
26
+
27
+ Events:
28
+ view-change detail: { view: "preview" | "code" }
29
+ refresh
30
+ close
31
+
32
+ Light DOM. components/artifact/index.css + components/button/index.css +
33
+ components/primitives/index.css must be loaded in the host page.
34
+ */
35
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
36
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
37
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
38
+ 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;
39
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
40
+ };
41
+ import { LitElement, html, svg, nothing } from "lit";
42
+ import { customElement, property, state } from "lit/decorators.js";
43
+ // Side-effect import: registers <xm-button> for use in the head toolbar.
44
+ import "../button/index.js";
45
+ const ICON_EYE = (size = 14) => svg `
46
+ <svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none"
47
+ stroke="currentColor" stroke-width="1.8" stroke-linecap="round"
48
+ stroke-linejoin="round" class="ds-icon">
49
+ <path d="M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12z" />
50
+ <circle cx="12" cy="12" r="3" />
51
+ </svg>
52
+ `;
53
+ const ICON_CODE = (size = 14) => svg `
54
+ <svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none"
55
+ stroke="currentColor" stroke-width="1.9" stroke-linecap="round"
56
+ stroke-linejoin="round" class="ds-icon">
57
+ <polyline points="16 18 22 12 16 6" />
58
+ <polyline points="8 6 2 12 8 18" />
59
+ </svg>
60
+ `;
61
+ const ICON_REFRESH = () => svg `
62
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none"
63
+ stroke="currentColor" stroke-width="1.8" stroke-linecap="round"
64
+ stroke-linejoin="round" class="ds-icon">
65
+ <path d="M21 12a9 9 0 1 1-3-6.7" />
66
+ <path d="M21 4v5h-5" />
67
+ </svg>
68
+ `;
69
+ const ICON_CLOSE = () => svg `
70
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none"
71
+ stroke="currentColor" stroke-width="2" stroke-linecap="round"
72
+ stroke-linejoin="round" class="ds-icon">
73
+ <path d="M6 6l12 12M18 6L6 18" />
74
+ </svg>
75
+ `;
76
+ let artifactInstance = 0;
77
+ let XmArtifact = class XmArtifact extends LitElement {
78
+ constructor() {
79
+ super(...arguments);
80
+ this.title = "";
81
+ this.sub = null;
82
+ this.subCsv = "";
83
+ this.view = "preview";
84
+ this.showRefresh = false;
85
+ this.showClose = false;
86
+ this._bodies = { preview: [], code: [] };
87
+ this._uid = `artifact-${++artifactInstance}`;
88
+ this._setView = (v) => {
89
+ if (this.view === v)
90
+ return;
91
+ // Always update internal state; consumers that prefer "controlled" mode
92
+ // can still listen for view-change to react.
93
+ this.view = v;
94
+ this.dispatchEvent(new CustomEvent("view-change", { detail: { view: v }, bubbles: true, composed: true }));
95
+ };
96
+ this._onTabKeydown = (e) => {
97
+ const k = e.key;
98
+ if (k !== "ArrowLeft" && k !== "ArrowRight" && k !== "Home" && k !== "End")
99
+ return;
100
+ e.preventDefault();
101
+ const next = k === "Home" ? "preview" :
102
+ k === "End" ? "code" :
103
+ this.view === "preview" ? "code" : "preview";
104
+ this._setView(next);
105
+ // Move focus to the newly-selected tab once render lands.
106
+ requestAnimationFrame(() => {
107
+ this.querySelector(`.artifact__view-btn[data-view="${next}"]`)?.focus();
108
+ });
109
+ };
110
+ this._onRefresh = () => {
111
+ this.dispatchEvent(new CustomEvent("refresh", { bubbles: true, composed: true }));
112
+ };
113
+ this._onClose = () => {
114
+ this.dispatchEvent(new CustomEvent("close", { bubbles: true, composed: true }));
115
+ };
116
+ }
117
+ createRenderRoot() {
118
+ return this;
119
+ }
120
+ connectedCallback() {
121
+ super.connectedCallback();
122
+ // Make the host transparent to the parent's grid/flex layout so the
123
+ // inner <section class="artifact"> behaves as the direct child. This
124
+ // matters for selectors like `.chat-shell__artifact-slot > .artifact`
125
+ // in components/chat/index.css.
126
+ if (!this.style.display)
127
+ this.style.display = "contents";
128
+ // Snapshot authored bodies by data-view; remove from DOM so lit's
129
+ // render() doesn't replace them mid-flight. We re-attach in updated().
130
+ const preview = [];
131
+ const code = [];
132
+ for (const child of Array.from(this.childNodes)) {
133
+ if (child.nodeType !== Node.ELEMENT_NODE) {
134
+ if (child.nodeType === Node.TEXT_NODE && !child.textContent?.trim()) {
135
+ // strip whitespace-only text — they'd otherwise bucket into preview
136
+ continue;
137
+ }
138
+ preview.push(child);
139
+ continue;
140
+ }
141
+ const v = child.getAttribute("data-view");
142
+ if (v === "code")
143
+ code.push(child);
144
+ else
145
+ preview.push(child);
146
+ }
147
+ for (const n of [...preview, ...code])
148
+ n.remove();
149
+ this._bodies = { preview, code };
150
+ }
151
+ /**
152
+ * Imperatively set the preview/code body nodes from a host. Replaces any
153
+ * authored or previously-set bodies. Used by <xm-chat-shell> to hand the
154
+ * panel its slot content; the chat shell can't author them as direct
155
+ * children of the lit-rendered <xm-artifact> tag without the snapshot
156
+ * timing race that this method sidesteps.
157
+ */
158
+ setBodies(preview, code) {
159
+ this._bodies = { preview: preview.slice(), code: code.slice() };
160
+ this.requestUpdate();
161
+ }
162
+ _subItems() {
163
+ if (Array.isArray(this.sub) && this.sub.length)
164
+ return this.sub;
165
+ if (this.subCsv)
166
+ return this.subCsv.split(",").map((s) => s.trim()).filter(Boolean);
167
+ return [];
168
+ }
169
+ render() {
170
+ const sub = this._subItems();
171
+ const v = this.view === "code" ? "code" : "preview";
172
+ const previewTabId = `${this._uid}-tab-preview`;
173
+ const codeTabId = `${this._uid}-tab-code`;
174
+ const bodyId = `${this._uid}-body`;
175
+ return html `
176
+ <section class="artifact" aria-label="${this.title || "Artifact"}">
177
+ <header class="artifact__head">
178
+ <div class="artifact__head-left">
179
+ <div
180
+ class="artifact__view-toggle"
181
+ role="tablist"
182
+ aria-label="Artifact view"
183
+ @keydown=${this._onTabKeydown}
184
+ >
185
+ <xm-button
186
+ id="${previewTabId}"
187
+ variant="ghost"
188
+ size="sm"
189
+ icon-only
190
+ ?selected=${v === "preview"}
191
+ role="tab"
192
+ data-view="preview"
193
+ aria-selected="${v === "preview"}"
194
+ aria-controls="${bodyId}"
195
+ tabindex="${v === "preview" ? 0 : -1}"
196
+ class="artifact__view-btn"
197
+ title="Preview"
198
+ @click=${() => this._setView("preview")}
199
+ >${ICON_EYE()}</xm-button>
200
+ <xm-button
201
+ id="${codeTabId}"
202
+ variant="ghost"
203
+ size="sm"
204
+ icon-only
205
+ ?selected=${v === "code"}
206
+ role="tab"
207
+ data-view="code"
208
+ aria-selected="${v === "code"}"
209
+ aria-controls="${bodyId}"
210
+ tabindex="${v === "code" ? 0 : -1}"
211
+ class="artifact__view-btn"
212
+ title="Source"
213
+ @click=${() => this._setView("code")}
214
+ >${ICON_CODE()}</xm-button>
215
+ </div>
216
+ </div>
217
+
218
+ <div class="artifact__head-mid">
219
+ ${this.title
220
+ ? html `<span class="artifact__title">${this.title}</span>`
221
+ : nothing}
222
+ ${sub.length
223
+ ? html `<span class="artifact__sub">
224
+ ${sub.map((s, i) => html `
225
+ ${i > 0 ? html `<span class="ds-sep">·</span>` : nothing}
226
+ <span>${s}</span>
227
+ `)}
228
+ </span>`
229
+ : nothing}
230
+ </div>
231
+
232
+ <div class="artifact__head-right">
233
+ ${this.showClose
234
+ ? html `<xm-button
235
+ variant="ghost"
236
+ size="sm"
237
+ icon-only
238
+ aria-label="Close artifact"
239
+ title="Close"
240
+ @click=${this._onClose}
241
+ >${ICON_CLOSE()}</xm-button>`
242
+ : nothing}
243
+ </div>
244
+ </header>
245
+
246
+ <div
247
+ id="${bodyId}"
248
+ role="tabpanel"
249
+ aria-labelledby="${v === "code" ? codeTabId : previewTabId}"
250
+ class="artifact__body scroll-canvas"
251
+ data-mount="body"
252
+ ></div>
253
+ </section>
254
+ `;
255
+ }
256
+ updated() {
257
+ const host = this.querySelector('[data-mount="body"]');
258
+ if (!host)
259
+ return;
260
+ const v = this.view === "code" ? "code" : "preview";
261
+ const wanted = this._bodies[v] || [];
262
+ // _bodies is the source of truth — the arrays still reference any
263
+ // currently-mounted nodes, since appendChild moves rather than copies.
264
+ // Just clear the host and re-mount the active view.
265
+ while (host.firstChild)
266
+ host.firstChild.remove();
267
+ for (const n of wanted)
268
+ host.appendChild(n);
269
+ }
270
+ };
271
+ __decorate([
272
+ property({ type: String })
273
+ ], XmArtifact.prototype, "title", void 0);
274
+ __decorate([
275
+ property({ attribute: false })
276
+ ], XmArtifact.prototype, "sub", void 0);
277
+ __decorate([
278
+ property({ type: String, attribute: "sub-csv" })
279
+ ], XmArtifact.prototype, "subCsv", void 0);
280
+ __decorate([
281
+ property({ type: String, reflect: true })
282
+ ], XmArtifact.prototype, "view", void 0);
283
+ __decorate([
284
+ property({ type: Boolean, attribute: "show-refresh" })
285
+ ], XmArtifact.prototype, "showRefresh", void 0);
286
+ __decorate([
287
+ property({ type: Boolean, attribute: "show-close" })
288
+ ], XmArtifact.prototype, "showClose", void 0);
289
+ __decorate([
290
+ state()
291
+ ], XmArtifact.prototype, "_bodies", void 0);
292
+ XmArtifact = __decorate([
293
+ customElement("xm-artifact")
294
+ ], XmArtifact);
@@ -0,0 +1,171 @@
1
+ /* ============================================
2
+ xm-autocomplete — filtering select for XmField (Story 2.4).
3
+
4
+ Same composition as xm-select: XmField (chrome) + xm-overlay (anchored
5
+ listbox). This file styles ONLY the bespoke parts — the text-input control
6
+ row + chevron, the option rows + match highlight, and the inline no-match
7
+ empty-state. Positioning / elevation of the panel surface is owned by
8
+ xm-overlay.
9
+
10
+ Surface & ink (AD-13): control + panel ride the inverse-surface card tier →
11
+ inverse-on-surface ink, on-surface-variant placeholder. The SELECTED option
12
+ tints coral primary-container + on-primary-container ink. The match highlight
13
+ carries coral ink AND an underline so the cue survives grayscale — color is
14
+ never the sole carrier (NFR-15). Coral = selection / match, never severity
15
+ (AD-11).
16
+
17
+ BEM block: `autocomplete`. Registered in scripts/check-bem.sh STRICT_BLOCKS.
18
+ ============================================ */
19
+
20
+ /* ---------- Control row — text input + chevron in the field wrapper ---------- */
21
+ .autocomplete__control {
22
+ display: flex;
23
+ align-items: center;
24
+ flex: 1;
25
+ min-width: 0;
26
+ width: 100%;
27
+ height: 100%;
28
+ }
29
+
30
+ /* The native <input> is already transparent / full-bleed via the base
31
+ `.field__control input` rule; here only the trailing chevron needs room. */
32
+ .autocomplete__input {
33
+ flex: 1;
34
+ min-width: 0;
35
+ }
36
+
37
+ .autocomplete__chevron {
38
+ display: inline-flex;
39
+ align-items: center;
40
+ flex-shrink: 0;
41
+ padding-right: var(--s-3);
42
+ color: var(--md-sys-color-inverse-on-surface);
43
+ transition: transform var(--md-sys-motion-duration-short3)
44
+ var(--md-sys-motion-easing-standard);
45
+ }
46
+ .autocomplete__chevron--open {
47
+ transform: rotate(180deg);
48
+ }
49
+
50
+ /* ---------- Listbox panel content (inside the overlay) ---------- */
51
+ .autocomplete__listbox {
52
+ list-style: none;
53
+ margin: 0;
54
+ padding: 0;
55
+ display: flex;
56
+ flex-direction: column;
57
+ gap: var(--s-0-5);
58
+ min-width: 220px;
59
+ max-height: 320px;
60
+ overflow-y: auto;
61
+ }
62
+
63
+ .autocomplete__option {
64
+ display: flex;
65
+ align-items: center;
66
+ gap: var(--s-2);
67
+ padding: var(--s-2) var(--s-3);
68
+ border-radius: var(--md-sys-shape-corner-small);
69
+ color: var(--md-sys-color-inverse-on-surface);
70
+ font:
71
+ var(--md-sys-typescale-body-medium-weight)
72
+ var(--md-sys-typescale-body-medium-size) /
73
+ var(--md-sys-typescale-body-medium-line-height)
74
+ var(--md-sys-typescale-body-medium-font);
75
+ cursor: pointer;
76
+ user-select: none;
77
+ }
78
+ .autocomplete__option-label {
79
+ flex: 1;
80
+ min-width: 0;
81
+ overflow: hidden;
82
+ white-space: nowrap;
83
+ text-overflow: ellipsis;
84
+ }
85
+ .autocomplete__option-check {
86
+ display: inline-flex;
87
+ align-items: center;
88
+ flex-shrink: 0;
89
+ color: var(--md-sys-color-primary);
90
+ }
91
+
92
+ /* Match highlight — coral ink + underline (dual cue, NFR-15). Background stays
93
+ transparent so it reads on both the panel ink and the selected coral tint. */
94
+ .autocomplete__mark {
95
+ background: transparent;
96
+ color: var(--md-sys-color-primary);
97
+ font-weight: 600;
98
+ text-decoration: underline;
99
+ text-underline-offset: 2px;
100
+ }
101
+
102
+ /* Active (keyboard / hover) — MD3 state layer over the panel ink. */
103
+ .autocomplete__option--active {
104
+ background: color-mix(
105
+ in oklab,
106
+ var(--md-sys-color-inverse-on-surface)
107
+ var(--md-sys-state-hover-state-layer-opacity),
108
+ transparent
109
+ );
110
+ }
111
+
112
+ /* Selected — coral primary-container tint + paired ink (AD-13). */
113
+ .autocomplete__option--selected {
114
+ background: var(--md-sys-color-primary-container);
115
+ color: var(--md-sys-color-on-primary-container);
116
+ }
117
+ .autocomplete__option--selected .autocomplete__option-check {
118
+ color: var(--md-sys-color-on-primary-container);
119
+ }
120
+ .autocomplete__option--selected .autocomplete__mark {
121
+ color: var(--md-sys-color-on-primary-container);
122
+ }
123
+ .autocomplete__option--selected.autocomplete__option--active {
124
+ background: color-mix(
125
+ in oklab,
126
+ var(--md-sys-color-on-primary-container)
127
+ var(--md-sys-state-hover-state-layer-opacity),
128
+ var(--md-sys-color-primary-container)
129
+ );
130
+ }
131
+
132
+ .autocomplete__option--disabled {
133
+ opacity: 0.4;
134
+ cursor: not-allowed;
135
+ }
136
+
137
+ /* ---------- No-match empty-state (inline; → xm-empty-state in Epic 4) ---------- */
138
+ .autocomplete__empty {
139
+ display: flex;
140
+ flex-direction: column;
141
+ align-items: center;
142
+ text-align: center;
143
+ gap: var(--s-1);
144
+ padding: var(--s-5) var(--s-4);
145
+ min-width: 220px;
146
+ }
147
+ .autocomplete__empty-icon {
148
+ display: inline-flex;
149
+ align-items: center;
150
+ justify-content: center;
151
+ margin-bottom: var(--s-1);
152
+ color: var(--xm-color-inverse-on-surface-muted);
153
+ }
154
+ .autocomplete__empty-title {
155
+ margin: 0;
156
+ color: var(--md-sys-color-inverse-on-surface);
157
+ font:
158
+ var(--md-sys-typescale-title-small-weight)
159
+ var(--md-sys-typescale-title-small-size) /
160
+ var(--md-sys-typescale-title-small-line-height)
161
+ var(--md-sys-typescale-title-small-font);
162
+ }
163
+ .autocomplete__empty-copy {
164
+ margin: 0;
165
+ color: var(--xm-color-inverse-on-surface-muted);
166
+ font:
167
+ var(--md-sys-typescale-body-small-weight)
168
+ var(--md-sys-typescale-body-small-size) /
169
+ var(--md-sys-typescale-body-small-line-height)
170
+ var(--md-sys-typescale-body-small-font);
171
+ }