@adia-ai/web-modules 0.3.3 → 0.3.5

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 (88) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/chat/chat-composer/chat-composer.a2ui.json +94 -0
  3. package/chat/chat-composer/chat-composer.examples.html +28 -0
  4. package/chat/chat-composer/chat-composer.html +43 -0
  5. package/chat/chat-composer/chat-composer.js +107 -0
  6. package/chat/chat-composer/chat-composer.test.js +112 -0
  7. package/chat/chat-composer/chat-composer.yaml +91 -0
  8. package/chat/chat-empty/chat-empty.a2ui.json +68 -0
  9. package/chat/chat-empty/chat-empty.examples.html +34 -0
  10. package/chat/chat-empty/chat-empty.html +42 -0
  11. package/chat/chat-empty/chat-empty.yaml +58 -0
  12. package/chat/chat-header/chat-header.a2ui.json +77 -0
  13. package/chat/chat-header/chat-header.examples.html +30 -0
  14. package/chat/chat-header/chat-header.html +42 -0
  15. package/chat/chat-header/chat-header.yaml +68 -0
  16. package/chat/chat-shell/chat-shell.css +1 -0
  17. package/chat/chat-shell/chat-shell.examples.html +126 -0
  18. package/chat/chat-shell/chat-shell.html +42 -0
  19. package/chat/chat-shell/chat-shell.js +35 -7
  20. package/chat/chat-shell/css/chat-shell.bespoke.css +196 -0
  21. package/chat/chat-sidebar/chat-sidebar.a2ui.json +136 -0
  22. package/chat/chat-sidebar/chat-sidebar.examples.html +36 -0
  23. package/chat/chat-sidebar/chat-sidebar.html +43 -0
  24. package/chat/chat-sidebar/chat-sidebar.js +227 -0
  25. package/chat/chat-sidebar/chat-sidebar.test.js +110 -0
  26. package/chat/chat-sidebar/chat-sidebar.yaml +140 -0
  27. package/chat/chat-status/chat-status.a2ui.json +63 -0
  28. package/chat/chat-status/chat-status.examples.html +29 -0
  29. package/chat/chat-status/chat-status.html +42 -0
  30. package/chat/chat-status/chat-status.yaml +52 -0
  31. package/chat/chat-thread/chat-thread.a2ui.json +91 -0
  32. package/chat/chat-thread/chat-thread.examples.html +36 -0
  33. package/chat/chat-thread/chat-thread.html +43 -0
  34. package/chat/chat-thread/chat-thread.js +106 -0
  35. package/chat/chat-thread/chat-thread.test.js +82 -0
  36. package/chat/chat-thread/chat-thread.yaml +89 -0
  37. package/chat/index.js +3 -0
  38. package/editor/editor-shell/editor-shell.examples.html +71 -0
  39. package/editor/editor-shell/editor-shell.html +42 -0
  40. package/package.json +1 -1
  41. package/shell/admin-command/admin-command.a2ui.json +102 -0
  42. package/shell/admin-command/admin-command.examples.html +83 -0
  43. package/shell/admin-command/admin-command.html +42 -0
  44. package/shell/admin-command/admin-command.js +161 -0
  45. package/shell/admin-command/admin-command.test.js +115 -0
  46. package/shell/admin-command/admin-command.yaml +102 -0
  47. package/shell/admin-content/admin-content.a2ui.json +73 -0
  48. package/shell/admin-content/admin-content.examples.html +33 -0
  49. package/shell/admin-content/admin-content.html +42 -0
  50. package/shell/admin-content/admin-content.yaml +63 -0
  51. package/shell/admin-page/admin-page.a2ui.json +74 -0
  52. package/shell/admin-page/admin-page.examples.html +37 -0
  53. package/shell/admin-page/admin-page.html +42 -0
  54. package/shell/admin-page/admin-page.yaml +61 -0
  55. package/shell/admin-page-body/admin-page-body.a2ui.json +62 -0
  56. package/shell/admin-page-body/admin-page-body.examples.html +34 -0
  57. package/shell/admin-page-body/admin-page-body.html +42 -0
  58. package/shell/admin-page-body/admin-page-body.yaml +49 -0
  59. package/shell/admin-page-header/admin-page-header.a2ui.json +62 -0
  60. package/shell/admin-page-header/admin-page-header.examples.html +34 -0
  61. package/shell/admin-page-header/admin-page-header.html +42 -0
  62. package/shell/admin-page-header/admin-page-header.yaml +47 -0
  63. package/shell/admin-scroll/admin-scroll.a2ui.json +62 -0
  64. package/shell/admin-scroll/admin-scroll.examples.html +31 -0
  65. package/shell/admin-scroll/admin-scroll.html +42 -0
  66. package/shell/admin-scroll/admin-scroll.yaml +51 -0
  67. package/shell/admin-shell/admin-shell.a2ui.json +0 -10
  68. package/shell/admin-shell/admin-shell.css +1 -0
  69. package/shell/admin-shell/admin-shell.examples.html +61 -5
  70. package/shell/admin-shell/admin-shell.js +165 -121
  71. package/shell/admin-shell/admin-shell.yaml +6 -6
  72. package/shell/admin-shell/css/admin-shell.bespoke.css +198 -0
  73. package/shell/admin-shell/css/admin-shell.tokens.css +10 -0
  74. package/shell/admin-sidebar/admin-sidebar.a2ui.json +138 -0
  75. package/shell/admin-sidebar/admin-sidebar.examples.html +76 -0
  76. package/shell/admin-sidebar/admin-sidebar.html +47 -0
  77. package/shell/admin-sidebar/admin-sidebar.js +227 -0
  78. package/shell/admin-sidebar/admin-sidebar.test.js +123 -0
  79. package/shell/admin-sidebar/admin-sidebar.yaml +140 -0
  80. package/shell/admin-statusbar/admin-statusbar.a2ui.json +81 -0
  81. package/shell/admin-statusbar/admin-statusbar.examples.html +29 -0
  82. package/shell/admin-statusbar/admin-statusbar.html +42 -0
  83. package/shell/admin-statusbar/admin-statusbar.yaml +68 -0
  84. package/shell/admin-topbar/admin-topbar.a2ui.json +83 -0
  85. package/shell/admin-topbar/admin-topbar.examples.html +31 -0
  86. package/shell/admin-topbar/admin-topbar.html +42 -0
  87. package/shell/admin-topbar/admin-topbar.yaml +75 -0
  88. package/shell/index.js +2 -0
@@ -0,0 +1,196 @@
1
+ /* ═══════════════════════════════════════════════════════════════
2
+ chat-shell — Bespoke shell-tier children (Phase 2 of ADR-0023)
3
+
4
+ The chat-* CSS-only structural children are styled by SHARING the
5
+ same CSS rules as their legacy raw-HTML counterparts. This file
6
+ re-applies the existing selectors (header / section / footer) to
7
+ the new bespoke tags via display + structural mappings.
8
+
9
+ Mirrors the admin-shell.bespoke.css pattern. Keeps the chat
10
+ cluster's CSS modifications isolated in one bridge file so Phase 3
11
+ (legacy shape removal) is a single-file delete.
12
+ ═══════════════════════════════════════════════════════════════ */
13
+
14
+ /* ── chat-header — top chrome bar ── */
15
+ chat-shell > chat-header,
16
+ chat-sidebar > chat-header[slot="header"],
17
+ chat-sidebar > chat-header:first-child {
18
+ display: flex;
19
+ align-items: center;
20
+ gap: var(--chat-header-gap, var(--a-space-2));
21
+ padding: 0 var(--chat-header-px, var(--a-space-3));
22
+ height: var(--chat-header-height, var(--a-size-lg));
23
+ font-size: var(--chat-header-font, var(--a-ui-size));
24
+ border-bottom: var(--chat-border, 1px solid var(--a-border-subtle));
25
+ flex-shrink: 0;
26
+ background: var(--chat-bg, var(--a-bg));
27
+ }
28
+
29
+ /* Slot vocabulary inside chat-header */
30
+ chat-header > [slot="name"] {
31
+ font-weight: var(--a-weight-medium, 500);
32
+ color: var(--a-fg);
33
+ }
34
+
35
+ chat-header > [slot="status"] {
36
+ margin-inline-start: var(--a-space-2);
37
+ }
38
+
39
+ chat-header > [slot="action"]:first-of-type {
40
+ margin-inline-start: auto;
41
+ }
42
+
43
+ chat-header > [slot="action-leading"] {
44
+ margin-inline-end: var(--a-space-2);
45
+ }
46
+
47
+ /* ── chat-status — inline status indicator ── */
48
+ chat-status {
49
+ display: inline-flex;
50
+ align-items: center;
51
+ gap: var(--a-space-1);
52
+ font-size: var(--a-ui-sm);
53
+ color: var(--a-fg-muted);
54
+ }
55
+
56
+ /* ── chat-thread — message scroll surface ── */
57
+ chat-shell > chat-thread {
58
+ flex: 1;
59
+ min-height: 0;
60
+ overflow-y: auto;
61
+ overscroll-behavior: contain;
62
+ display: flex;
63
+ flex-direction: column;
64
+ gap: var(--chat-message-gap, var(--a-space-3));
65
+ padding: var(--chat-thread-py, var(--a-space-4)) var(--chat-thread-px, var(--a-space-4));
66
+ background: var(--chat-thread-bg, var(--a-bg));
67
+ }
68
+
69
+ chat-thread[streaming] {
70
+ /* Optional: subtle indicator while streaming. Default: no visual change. */
71
+ }
72
+
73
+ /* ── chat-empty — empty state, hidden when messages present ── */
74
+ chat-thread:not([empty]) > chat-empty {
75
+ display: none;
76
+ }
77
+
78
+ chat-thread[empty] > chat-empty {
79
+ display: flex;
80
+ flex: 1;
81
+ align-items: center;
82
+ justify-content: center;
83
+ padding: var(--a-space-6);
84
+ }
85
+
86
+ /* ── chat-composer — input wrapper ── */
87
+ chat-shell > chat-composer {
88
+ display: flex;
89
+ align-items: stretch;
90
+ gap: var(--a-space-2);
91
+ padding: var(--chat-composer-py, var(--a-space-3)) var(--chat-composer-px, var(--a-space-3));
92
+ border-top: var(--chat-border, 1px solid var(--a-border-subtle));
93
+ background: var(--chat-bg, var(--a-bg));
94
+ flex-shrink: 0;
95
+ }
96
+
97
+ chat-composer > [slot="leading"],
98
+ chat-composer > [slot="attach"] {
99
+ flex-shrink: 0;
100
+ display: inline-flex;
101
+ align-items: center;
102
+ gap: var(--a-space-1);
103
+ }
104
+
105
+ chat-composer > [slot="trailing"] {
106
+ flex-shrink: 0;
107
+ display: inline-flex;
108
+ align-items: center;
109
+ gap: var(--a-space-1);
110
+ }
111
+
112
+ /* The primary input child takes remaining space */
113
+ chat-composer > :is(chat-input-ui, input-ui, textarea-ui) {
114
+ flex: 1;
115
+ min-width: 0;
116
+ }
117
+
118
+ chat-composer[disabled] {
119
+ opacity: 0.6;
120
+ pointer-events: none;
121
+ }
122
+
123
+ /* ── chat-sidebar — bridges to admin-sidebar pattern ──
124
+ Uses the same geometry + container-query + resize handle rules as
125
+ admin-sidebar. The cluster-namespaced tag exists for discoverability;
126
+ the styling logic is identical because the concern is identical
127
+ (resizable side rail). */
128
+
129
+ :is(chat-sidebar[slot="leading"], chat-sidebar[slot="trailing"]) {
130
+ display: flex;
131
+ flex-direction: column;
132
+ flex-shrink: 0;
133
+ min-width: var(--chat-sidebar-min-width, 48px);
134
+ max-width: var(--chat-sidebar-max-width, 480px);
135
+ min-height: 0;
136
+ font-size: var(--chat-sidebar-font, var(--a-ui-size));
137
+ position: relative;
138
+ container-type: inline-size;
139
+ container-name: sidebar;
140
+ transition: width var(--chat-duration, var(--a-duration, 200ms)) var(--chat-easing, var(--a-easing, ease));
141
+ background: var(--chat-bg, var(--a-bg));
142
+ }
143
+
144
+ chat-sidebar[slot="leading"] {
145
+ width: var(--chat-sidebar-width-leading, 240px);
146
+ border-right: var(--chat-border, 1px solid var(--a-border-subtle));
147
+ }
148
+
149
+ chat-sidebar[slot="trailing"] {
150
+ width: var(--chat-sidebar-width-trailing, 320px);
151
+ border-left: var(--chat-border, 1px solid var(--a-border-subtle));
152
+ }
153
+
154
+ /* Resize handle */
155
+ :is(chat-sidebar[slot="leading"], chat-sidebar[slot="trailing"]) > [data-resize] {
156
+ position: absolute;
157
+ top: 0;
158
+ bottom: 0;
159
+ width: 6px;
160
+ cursor: col-resize;
161
+ z-index: 1;
162
+ user-select: none;
163
+ }
164
+
165
+ chat-sidebar[slot="leading"] > [data-resize] { right: -3px; }
166
+ chat-sidebar[slot="trailing"] > [data-resize] { left: -3px; }
167
+
168
+ :is(chat-sidebar[slot="leading"], chat-sidebar[slot="trailing"]) > [data-resize]:hover {
169
+ background: var(--a-accent);
170
+ opacity: 0.5;
171
+ }
172
+
173
+ chat-sidebar[resizing] {
174
+ transition: none;
175
+ }
176
+
177
+ chat-sidebar[resizing] > [data-resize] {
178
+ background: var(--a-accent);
179
+ opacity: 0.8;
180
+ }
181
+
182
+ /* ── chat-shell layout — when bespoke children are used ──
183
+ Adjusts flex direction so chat-sidebar can sit alongside the main
184
+ chat surface. Without this, chat-shell's existing layout.css
185
+ assumes a vertical stack (header / messages / composer). */
186
+
187
+ chat-shell:has(> chat-sidebar) {
188
+ display: flex;
189
+ flex-direction: row;
190
+ }
191
+
192
+ /* When sidebar is present, the main chat content (everything that
193
+ isn't a sidebar) needs to be a flex column itself */
194
+ chat-shell:has(> chat-sidebar) > :not(chat-sidebar) {
195
+ display: contents;
196
+ }
@@ -0,0 +1,136 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://adiaui.dev/a2ui/v0_9/components/ChatSidebar.json",
4
+ "title": "ChatSidebar",
5
+ "description": "Module-tier chat-cluster sidebar — owns resize, snap-to-collapsed,\npersistence, and the [collapsed] reflected attribute. Sits inside\n<chat-shell> as slot=\"leading\" (conversation history rail) or\nslot=\"trailing\" (artifacts/citations panel). Mirrors the admin-sidebar\nshape — same 4 concerns, cluster-namespaced for the chat family.\n\nThis is the chat-cluster equivalent of <admin-sidebar> per ADR-0023.\n<chat-shell> doesn't currently have a legacy sidebar shape (chat is\ntypically a single-pane experience), so <chat-sidebar> is purely\nforward-looking — for chat apps that want conversation-history or\ninspector rails.\n",
6
+ "type": "object",
7
+ "allOf": [
8
+ {
9
+ "$ref": "common_types.json#/$defs/ComponentCommon"
10
+ },
11
+ {
12
+ "$ref": "common_types.json#/$defs/CatalogComponentCommon"
13
+ }
14
+ ],
15
+ "properties": {
16
+ "collapsed": {
17
+ "description": "Reflected — set when the sidebar's measured width is at or below\n96px. Consumers query this to style \"collapsed mode\" without\nduplicating threshold math.\n",
18
+ "type": "boolean",
19
+ "default": false
20
+ },
21
+ "collapsible": {
22
+ "description": "Opts in to programmatic collapse — toggle button wiring + the\n.toggle() / .collapse() / .expand() public methods.\n",
23
+ "type": "boolean",
24
+ "default": false
25
+ },
26
+ "component": {
27
+ "const": "ChatSidebar"
28
+ },
29
+ "min-width": {
30
+ "description": "Optional override for the snap-floor width. Defaults to reading\nCSS min-width via getComputedStyle.\n",
31
+ "type": "string",
32
+ "default": ""
33
+ },
34
+ "name": {
35
+ "description": "Identifier for localStorage namespacing. Defaults to slot value\n(\"leading\" or \"trailing\"). Override when running multiple sidebars\nwith the same slot.\n",
36
+ "type": "string",
37
+ "default": ""
38
+ },
39
+ "resizable": {
40
+ "description": "Opts in to drag-to-resize behavior. Author supplies a child\n[data-resize] element as the drag handle.\n",
41
+ "type": "boolean",
42
+ "default": false
43
+ },
44
+ "resizing": {
45
+ "description": "Reflected — set during an active pointer-drag on the resize handle.\nUseful for suppressing transitions while dragging.\n",
46
+ "type": "boolean",
47
+ "default": false
48
+ }
49
+ },
50
+ "required": [
51
+ "component"
52
+ ],
53
+ "unevaluatedProperties": false,
54
+ "x-adiaui": {
55
+ "anti_patterns": [],
56
+ "category": "layout",
57
+ "events": {
58
+ "sidebar-resize": {
59
+ "description": "Bubbles when an active pointer-drag releases.",
60
+ "detail": {
61
+ "name": "string",
62
+ "width": "number"
63
+ }
64
+ },
65
+ "sidebar-toggle": {
66
+ "description": "Bubbles when sidebar collapses or expands.",
67
+ "detail": {
68
+ "expanded": "boolean",
69
+ "name": "string"
70
+ }
71
+ }
72
+ },
73
+ "examples": [],
74
+ "keywords": [
75
+ "chat-sidebar",
76
+ "chat-sidebar",
77
+ "conversation-history",
78
+ "chat-rail",
79
+ "sidebar",
80
+ "leading",
81
+ "trailing",
82
+ "inspector"
83
+ ],
84
+ "name": "ChatSidebar",
85
+ "related": [
86
+ "ChatShell",
87
+ "ChatComposer",
88
+ "ChatHeader",
89
+ "List",
90
+ "Nav"
91
+ ],
92
+ "slots": {
93
+ "default": {
94
+ "description": "Default content — typically a <nav-ui> for navigation, or a list / tree for the trailing inspector pattern."
95
+ },
96
+ "footer": {
97
+ "description": "Bottom chrome bar (user select, status indicator, etc.)."
98
+ },
99
+ "header": {
100
+ "description": "Top chrome bar (workspace select, breadcrumb, etc.)."
101
+ }
102
+ },
103
+ "states": [
104
+ {
105
+ "description": "Default, expanded.",
106
+ "name": "idle"
107
+ },
108
+ {
109
+ "description": "Sidebar width is at or below the snap threshold; CSS container queries flip child layout to icon-only mode.",
110
+ "attribute": "collapsed",
111
+ "name": "collapsed"
112
+ },
113
+ {
114
+ "description": "Active pointer-drag in progress.",
115
+ "attribute": "resizing",
116
+ "name": "resizing"
117
+ }
118
+ ],
119
+ "synonyms": {
120
+ "collapsed": [
121
+ "minimized",
122
+ "narrow",
123
+ "icon-only"
124
+ ],
125
+ "sidebar": [
126
+ "aside",
127
+ "rail",
128
+ "panel"
129
+ ]
130
+ },
131
+ "tag": "chat-sidebar",
132
+ "tokens": {},
133
+ "traits": [],
134
+ "version": 1
135
+ }
136
+ }
@@ -0,0 +1,36 @@
1
+ <header>
2
+ <div>
3
+ <h1>Chat Sidebar</h1>
4
+ <div data-actions>
5
+ <tag-ui size="sm">chat-sidebar</tag-ui>
6
+ <tag-ui size="sm" variant="ghost">JS-bearing</tag-ui>
7
+ </div>
8
+ </div>
9
+ <p>Module-tier chat-cluster sidebar — mirrors admin-sidebar (resize + collapse + persist), cluster-namespaced for the chat family.</p>
10
+ </header>
11
+
12
+ <section data-section>
13
+ <h2 variant="section">Role</h2>
14
+ <p>Mirrors <code>&lt;admin-sidebar&gt;</code> — resize drag, snap-to-collapsed, localStorage persistence, ResizeObserver. Cluster-namespaced for the chat family. JS-bearing.</p>
15
+ </section>
16
+
17
+ <section data-section>
18
+ <h2 variant="section">Composition</h2>
19
+ <p>Typical placement inside <code>&lt;chat-shell&gt;</code>:</p>
20
+ <code-ui language="html">&lt;chat-shell&gt;
21
+ &lt;chat-sidebar slot="leading" resizable collapsible&gt;
22
+ &lt;chat-header slot="header"&gt;
23
+ &lt;span slot="name"&gt;Conversations&lt;/span&gt;
24
+ &lt;/chat-header&gt;
25
+ &lt;list-ui&gt;…conversation history…&lt;/list-ui&gt;
26
+ &lt;div data-resize&gt;&lt;/div&gt;
27
+ &lt;/chat-sidebar&gt;
28
+ &lt;chat-thread&gt;…&lt;/chat-thread&gt;
29
+ &lt;chat-composer&gt;…&lt;/chat-composer&gt;
30
+ &lt;/chat-shell&gt;</code-ui>
31
+ </section>
32
+
33
+ <section data-section>
34
+ <h2 variant="section">Family</h2>
35
+ <p>Per <a href="../../../../.brain/adrs/0023-bespoke-shell-tier-children.md">ADR-0023</a>, the chat cluster's bespoke family — <code>&lt;chat-shell&gt;</code> (host), <code>&lt;chat-thread&gt;</code>, <code>&lt;chat-composer&gt;</code>, <code>&lt;chat-sidebar&gt;</code> (JS-bearing) + <code>&lt;chat-header&gt;</code>, <code>&lt;chat-status&gt;</code>, <code>&lt;chat-empty&gt;</code> (CSS-only). Mirrors the admin cluster's pattern.</p>
36
+ </section>
@@ -0,0 +1,43 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-theme="auto">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Chat Sidebar — AdiaUI</title>
7
+
8
+ <link rel="stylesheet" href="../../../web-components/styles/resets.css">
9
+ <link rel="stylesheet" href="../../../web-components/styles/tokens.css">
10
+ <link rel="stylesheet" href="../chat-shell/chat-shell.css">
11
+ <link rel="stylesheet" href="../../../web-components/components/code/code.css">
12
+ <link rel="stylesheet" href="../../../web-components/components/tag/tag.css">
13
+
14
+ <script type="module" src="../chat-shell/chat-shell.js"></script>
15
+ <script type="module" src="./chat-sidebar.js"></script>
16
+ <script type="module" src="../../../web-components/components/code/code.js"></script>
17
+ <script type="module" src="../../../web-components/components/tag/tag.js"></script>
18
+
19
+ <style>
20
+ :where(html, body) { margin: 0; min-height: 100vh; background: var(--a-bg); color: var(--a-fg); font-family: var(--a-font); }
21
+ main { max-width: 960px; margin-inline: auto; padding: var(--a-space-6) var(--a-space-5); }
22
+ </style>
23
+ </head>
24
+ <body>
25
+
26
+ <main id="demo-root">
27
+ <p>Loading examples…</p>
28
+ </main>
29
+
30
+ <script type="module">
31
+ const root = document.getElementById('demo-root');
32
+ try {
33
+ const res = await fetch('./chat-sidebar.examples.html');
34
+ if (!res.ok) throw new Error(`fetch failed (${res.status})`);
35
+ root.innerHTML = await res.text();
36
+ } catch (err) {
37
+ root.innerHTML = `<p style="color:var(--a-danger-strong);">Failed to load chat-sidebar.examples.html — ${err.message}</p>`;
38
+ console.error('[chat-sidebar.html]', err);
39
+ }
40
+ </script>
41
+
42
+ </body>
43
+ </html>
@@ -0,0 +1,227 @@
1
+ /**
2
+ * <chat-sidebar slot="leading|trailing" resizable collapsible>
3
+ * <chat-header slot="header">…</chat-header> (or any chrome bar)
4
+ * …default content (nav, list, etc.)…
5
+ * <chat-status slot="footer">…</chat-status>
6
+ * </chat-sidebar>
7
+ *
8
+ * Module-tier chat-cluster sidebar — owns resize, snap-to-collapsed, persistence,
9
+ * and the [collapsed] reflected attribute. Sits inside <chat-shell>
10
+ * but doesn't reach into it; the shell coordinates without orchestrating
11
+ * child internals.
12
+ *
13
+ * Reflected attributes (the consumer-queryable state):
14
+ * [collapsed] — set when sidebar width is at or below SNAP_THRESHOLD
15
+ * [resizing] — set during an active pointer-drag
16
+ *
17
+ * Author-supplied attributes (read once at connect, never overwritten):
18
+ * [slot="leading"|"trailing"] — required, drives drag direction +
19
+ * localStorage namespacing
20
+ * [resizable] — opts in to drag handle wiring
21
+ * [collapsible] — opts in to programmatic collapse
22
+ * (toggle button + window.toggle())
23
+ * [name="<id>"] — optional override for the localStorage
24
+ * key. Defaults to slot value.
25
+ * [min-width="48px"] — optional override for the snap-floor
26
+ * width (otherwise reads CSS min-width)
27
+ *
28
+ * Events:
29
+ * sidebar-toggle — bubbles. detail: { name, expanded }
30
+ * sidebar-resize — bubbles. detail: { name, width }
31
+ *
32
+ * The drag handle is conventional: a child `[data-resize]` element.
33
+ * Authors may provide a custom one, or omit (no resize affordance).
34
+ *
35
+ * Backwards compat: <chat-shell> still recognizes the legacy
36
+ * `<aside data-sidebar="leading">` shape via :is() selector. New
37
+ * code should prefer <chat-sidebar>.
38
+ */
39
+
40
+ import { UIElement } from '../../../web-components/core/element.js';
41
+
42
+ const SNAP_THRESHOLD = 96;
43
+ const SNAP_MIN_USABLE = 160;
44
+
45
+ class ChatSidebar extends UIElement {
46
+ static properties = {
47
+ collapsed: { type: Boolean, default: false, reflect: true },
48
+ resizing: { type: Boolean, default: false, reflect: true },
49
+ resizable: { type: Boolean, default: false, reflect: true },
50
+ collapsible: { type: Boolean, default: false, reflect: true },
51
+ name: { type: String, default: '', reflect: true },
52
+ minWidth: { type: String, default: '', reflect: true, attribute: 'min-width' },
53
+ };
54
+
55
+ static template = () => null;
56
+
57
+ // The width the sidebar had before being collapsed — used for restore.
58
+ // Map keyed by sidebar name allows multiple sidebars on one host.
59
+ #previousExpandedWidth = '';
60
+ #resizeCleanups = [];
61
+ #childRO = null;
62
+
63
+ connected() {
64
+ this.#restoreFromStorage();
65
+ if (this.resizable) this.#setupResizeHandle();
66
+ this.#setupChildResizeObserver();
67
+ this.#syncCollapsedFromWidth();
68
+ }
69
+
70
+ disconnected() {
71
+ for (const cleanup of this.#resizeCleanups) cleanup();
72
+ this.#resizeCleanups = [];
73
+ this.#childRO?.disconnect();
74
+ this.#childRO = null;
75
+ }
76
+
77
+ // ── Public API (callable from <chat-shell> or external code) ──
78
+
79
+ /**
80
+ * Toggle collapsed state. Collapses if expanded, restores if collapsed.
81
+ * Returns the new collapsed value.
82
+ */
83
+ toggle() {
84
+ if (this.collapsed) {
85
+ this.expand();
86
+ } else {
87
+ this.collapse();
88
+ }
89
+ return this.collapsed;
90
+ }
91
+
92
+ /** Collapse to the snap-floor width. Persists to localStorage. */
93
+ collapse() {
94
+ if (this.collapsed) return;
95
+ // Remember current expanded width before collapsing
96
+ const currentWidth = this.style.width || getComputedStyle(this).width;
97
+ if (parseFloat(currentWidth) > SNAP_THRESHOLD) {
98
+ this.#previousExpandedWidth = currentWidth;
99
+ }
100
+ const minW = this.minWidth || getComputedStyle(this).minWidth;
101
+ this.style.width = minW;
102
+ this.#persist(minW);
103
+ this.collapsed = true;
104
+ this.#dispatchToggle(false);
105
+ }
106
+
107
+ /** Restore to the previous expanded width (or default if none). */
108
+ expand() {
109
+ if (!this.collapsed) return;
110
+ const restoreWidth = this.#previousExpandedWidth || '';
111
+ this.style.width = restoreWidth;
112
+ this.#persist(restoreWidth);
113
+ this.collapsed = false;
114
+ this.#dispatchToggle(true);
115
+ }
116
+
117
+ // ── Persistence ──
118
+
119
+ #storageKey() {
120
+ const id = this.name || this.getAttribute('slot') || 'default';
121
+ return `adia-chat-sidebar-${id}`;
122
+ }
123
+
124
+ #persist(width) {
125
+ try { localStorage.setItem(this.#storageKey(), width); } catch {}
126
+ }
127
+
128
+ #restoreFromStorage() {
129
+ try {
130
+ const saved = localStorage.getItem(this.#storageKey());
131
+ if (!saved) return;
132
+ this.style.width = saved;
133
+ // Only treat as "previous expanded" if actually expanded
134
+ const w = parseFloat(saved);
135
+ if (!isNaN(w) && w > SNAP_THRESHOLD) {
136
+ this.#previousExpandedWidth = saved;
137
+ }
138
+ } catch {}
139
+ }
140
+
141
+ // ── Reflect [collapsed] from current measured width ──
142
+
143
+ #syncCollapsedFromWidth() {
144
+ const w = this.getBoundingClientRect().width;
145
+ this.collapsed = w <= SNAP_THRESHOLD;
146
+ }
147
+
148
+ // ── Resize drag handle ──
149
+
150
+ #setupResizeHandle() {
151
+ const handle = this.querySelector(':scope > [data-resize]');
152
+ if (!handle) return;
153
+
154
+ const slot = this.getAttribute('slot');
155
+ const isLeading = slot === 'leading';
156
+
157
+ const onPointerDown = (e) => {
158
+ e.preventDefault();
159
+ handle.setPointerCapture(e.pointerId);
160
+ const startX = e.clientX;
161
+ const startW = this.getBoundingClientRect().width;
162
+ this.resizing = true;
163
+ document.documentElement.style.cursor = 'col-resize';
164
+
165
+ const onMove = (e) => {
166
+ const dx = e.clientX - startX;
167
+ const max = parseInt(getComputedStyle(this).getPropertyValue('max-width')) || 480;
168
+ const w = Math.max(48, Math.min(max, startW + (isLeading ? dx : -dx)));
169
+ this.style.width = `${w}px`;
170
+ };
171
+
172
+ const onUp = () => {
173
+ this.resizing = false;
174
+ document.documentElement.style.cursor = '';
175
+ handle.removeEventListener('pointermove', onMove);
176
+ handle.removeEventListener('pointerup', onUp);
177
+
178
+ // Snap logic
179
+ const w = this.getBoundingClientRect().width;
180
+ if (w <= SNAP_THRESHOLD) {
181
+ this.style.width = this.minWidth || getComputedStyle(this).minWidth;
182
+ } else if (w < SNAP_MIN_USABLE) {
183
+ this.style.width = `${SNAP_MIN_USABLE}px`;
184
+ }
185
+
186
+ this.#persist(this.style.width);
187
+ this.#syncCollapsedFromWidth();
188
+ this.dispatchEvent(new CustomEvent('sidebar-resize', {
189
+ bubbles: true,
190
+ detail: { name: this.name || slot, width: this.getBoundingClientRect().width },
191
+ }));
192
+ };
193
+
194
+ handle.addEventListener('pointermove', onMove);
195
+ handle.addEventListener('pointerup', onUp);
196
+ };
197
+
198
+ handle.addEventListener('pointerdown', onPointerDown);
199
+ this.#resizeCleanups.push(() => handle.removeEventListener('pointerdown', onPointerDown));
200
+ }
201
+
202
+ // ── Child ResizeObserver — flips select-ui placement in narrow mode ──
203
+
204
+ #setupChildResizeObserver() {
205
+ this.#childRO = new ResizeObserver((entries) => {
206
+ for (const entry of entries) {
207
+ const narrow = entry.contentBoxSize[0].inlineSize <= SNAP_THRESHOLD;
208
+ for (const sel of this.querySelectorAll('select-ui')) {
209
+ sel.setAttribute('placement', narrow ? 'right' : 'bottom-start');
210
+ }
211
+ }
212
+ });
213
+ this.#childRO.observe(this);
214
+ }
215
+
216
+ // ── Internal — dispatch the toggle event in a consistent shape ──
217
+
218
+ #dispatchToggle(expanded) {
219
+ this.dispatchEvent(new CustomEvent('sidebar-toggle', {
220
+ bubbles: true,
221
+ detail: { name: this.name || this.getAttribute('slot') || 'default', expanded },
222
+ }));
223
+ }
224
+ }
225
+
226
+ customElements.define('chat-sidebar', ChatSidebar);
227
+ export { ChatSidebar };