polymorph-ui-components-mcp 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,14 @@
1
+ # Changelog All notable changes to this project will be documented in this file. The format is
2
+ based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to
3
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
+
5
+ ## [Unreleased](https://github.com/sinha-sahil/polymorph-ui-components/compare/0.2.0...HEAD)
6
+
7
+ - introducing modular chat components like bubble, message, headers, tools etc
8
+ - new resiable and draggable component to easily add support for resize and drag to any componet
9
+ - added extensive documentation for new available integrations
10
+
11
+ ## [0.2.0](https://github.com/sinha-sahil/polymorph-ui-components/compare/0.1.0...0.2.0) - 28 June 2026
12
+
13
+ ##
14
+ 0.1.0 - 27 June 2026
@@ -0,0 +1,211 @@
1
+ # Chat
2
+
3
+ A full chat surface that composes `ChatHeader`, `ChatMessageList`, `ChatSuggestions`, `ChatToolStatus`, and `ChatComposer` into one drop-in component. It is **fully controlled** — you pass `messages` and handle `onsend`; the component owns no transport, so it works with any backend. For a batteries-included experience, pair it with the decoupled **`ChatController`** (below), which manages message state, streaming, and an optional typewriter reveal while delegating the actual network call to a pluggable transport.
4
+
5
+ Markdown is intentionally not bundled: pass pre-sanitized HTML on a message's `html` field (e.g. the output of your own `marked` + `DOMPurify`) and it renders in the bubble.
6
+
7
+ ## Usage (controlled)
8
+
9
+ ```svelte
10
+ <script>
11
+ import { Chat } from 'polymorph-ui-components';
12
+
13
+ let messages = $state([]);
14
+ let value = $state('');
15
+
16
+ function onsend(text) {
17
+ messages.push({ id: crypto.randomUUID(), role: 'sender', content: text });
18
+ // ...call your API, append a responder message, stream into it...
19
+ }
20
+ </script>
21
+
22
+ <Chat {messages} bind:value title="Assistant" placeholder="Ask anything…" {onsend} />
23
+ ```
24
+
25
+ ## Usage (with the decoupled controller)
26
+
27
+ `ChatController` is a runes-based state holder. Its single dependency on your backend is the **`transport`** function — adapt SSE, WebSocket, or polling to the `ChatTransport` shape and the controller/UI never need to know the difference.
28
+
29
+ ```svelte
30
+ <script>
31
+ import { Chat, ChatController } from 'polymorph-ui-components';
32
+
33
+ const chat = new ChatController({
34
+ typewriter: true,
35
+ transport: async ({ message, history, sessionId, signal }, handlers) => {
36
+ const res = await fetch('/api/chat', {
37
+ method: 'POST',
38
+ body: JSON.stringify({ message }),
39
+ signal
40
+ });
41
+ // parse your stream and call the handlers:
42
+ handlers.onToolStatus?.({ label: 'Searching…' });
43
+ handlers.onText('Hello ');
44
+ handlers.onText('world');
45
+ handlers.onDone?.();
46
+ }
47
+ });
48
+
49
+ let value = $state('');
50
+ </script>
51
+
52
+ <Chat
53
+ messages={chat.messages}
54
+ bind:value
55
+ toolStatus={chat.toolStatus}
56
+ disabled={chat.isStreaming}
57
+ suggestions={['Track my order', 'Return policy?']}
58
+ onsend={(text) => chat.send(text)}
59
+ />
60
+ ```
61
+
62
+ ### `ChatController`
63
+
64
+ | Member | Type | Description |
65
+ | ------ | ---- | ----------- |
66
+ | `messages` | `ChatMessageData[]` | Reactive message list. |
67
+ | `isStreaming` | `boolean` | True while a response is in flight. |
68
+ | `toolStatus` | `ChatToolStatus \| null` | Current tool/typing status. |
69
+ | `send(text)` | `Promise<void>` | Append the sender's message and stream the reply. |
70
+ | `retry()` | `Promise<void>` | Drop the last reply and re-run the most recent sender message. |
71
+ | `stop()` | `void` | Abort the in-flight response. |
72
+ | `reset()` | `void` | Clear all messages and state. |
73
+
74
+ `ChatControllerOptions`: `transport` (required `ChatTransport`), `initialMessages?`, `typewriter?` (reveal text char-by-char), `generateId?` (defaults to `crypto.randomUUID()`).
75
+
76
+ `ChatTransport`: `(input: { message, history, sessionId, signal }, handlers: ChatStreamHandlers) => Promise<void>`. Handlers: `onText` (required), `onToolStatus?`, `onAttachment?`, `onError?`, `onDone?`. The controller threads sessions for you: whatever a transport reports via `onDone({ sessionId })` is passed back on the next request's `input.sessionId`.
77
+
78
+ **Roles.** Messages use the two-party primitive `role` — `sender` / `responder` (see `ChatMessage`). The controller emits those, and `partyOf(role)` resolves any role (including the `user`/`assistant`/`system` extensions) to its party. Map the primitive to your provider's roles inside the transport, where the API-specific terms belong: `history.map((m) => ({ role: partyOf(m.role) === 'sender' ? 'user' : 'assistant', content: m.content }))`.
79
+
80
+ ## Layout — fullscreen & floating
81
+
82
+ `Chat` is position- and size-agnostic: its root fills its container (`--chat-height` / `--chat-width` default to `100%`), with the message list on `flex: 1` and the composer pinned to the bottom. The only requirement is a **bounded-height parent** — the flexing list needs something to fill. Positioning (fixed, floating, modal) is the consumer's job; the component never assumes a layout context.
83
+
84
+ ### Fullscreen
85
+
86
+ Give it a viewport-sized box (use `100dvh` so the mobile URL bar doesn't clip the composer):
87
+
88
+ ```svelte
89
+ <div style="height: 100dvh; width: 100vw">
90
+ <Chat {messages} bind:value title="Assistant" {onsend} />
91
+ </div>
92
+ ```
93
+
94
+ Or set the variables directly: `--chat-height: 100dvh; --chat-width: 100vw`. On very wide screens, constrain the reading column (bubbles cap at `--chat-message-max-width`, default 82%) with e.g. `--chat-width: min(100%, 820px)` or a centered wrapper.
95
+
96
+ ### Floating widget
97
+
98
+ The component ships no launcher button or fixed positioning — compose those, and wire the header's `onclose` to the close/minimize action:
99
+
100
+ ```svelte
101
+ <script>
102
+ let open = $state(false);
103
+ </script>
104
+
105
+ {#if open}
106
+ <div class="chat-panel">
107
+ <Chat {messages} bind:value title="Assistant" {onsend} onclose={() => (open = false)} />
108
+ </div>
109
+ {/if}
110
+
111
+ <button class="chat-launcher" onclick={() => (open = !open)} aria-label="Chat">💬</button>
112
+
113
+ <style>
114
+ .chat-panel {
115
+ position: fixed;
116
+ right: 24px;
117
+ bottom: 88px;
118
+ width: 380px;
119
+ height: 600px;
120
+ max-height: calc(100dvh - 120px);
121
+ border-radius: 16px;
122
+ overflow: hidden;
123
+ box-shadow: 0 16px 48px rgba(0, 0, 0, 0.22);
124
+ z-index: 1000;
125
+ }
126
+ .chat-launcher {
127
+ position: fixed;
128
+ right: 24px;
129
+ bottom: 24px;
130
+ z-index: 1000;
131
+ width: 56px;
132
+ height: 56px;
133
+ border-radius: 50%;
134
+ }
135
+ </style>
136
+ ```
137
+
138
+ The fixed-height `.chat-panel` gives `Chat` its bounds; add a slide/scale transition on it for the open animation.
139
+
140
+ ## Props
141
+
142
+ | Prop | Type | Required | Default | Description |
143
+ | --------------- | ----------------------------- | -------- | ------- | --------------------------------------------------------------------------- |
144
+ | messages | `ChatMessageData[]` | Yes | `-` | The conversation to render. |
145
+ | value | `string` | No | `''` | Bindable. The composer draft text. |
146
+ | title | `string` | No | `''` | Header title. Header is hidden when there is no title/subtitle/image/close/avatar.|
147
+ | subtitle | `string` | No | `''` | Header subtitle. |
148
+ | image | `string` | No | `-` | Optional header brand image URL (rendered via `Img`). Off by default. |
149
+ | imageAlt | `string` | No | `''` | Alt text for the header image. |
150
+ | placeholder | `string` | No | `''` | Composer placeholder. |
151
+ | disabled | `boolean` | No | `false` | Disable the composer entirely. |
152
+ | streaming | `boolean` | No | `false` | A reply is streaming — the send button becomes a stop button. |
153
+ | recording | `boolean` | No | `false` | Visual active state for the composer voice button. |
154
+ | autoscroll | `boolean` | No | `true` | Auto-scroll to the latest message (only when already near the bottom). |
155
+ | toolStatus | `ChatToolStatus \| null` | No | `null` | Tool/typing status shown above the composer. |
156
+ | suggestions | `ChatSuggestion[]` | No | `[]` | Prompt chips shown when the conversation is empty. |
157
+ | attachments | `File[]` | No | `[]` | Bindable. Pending composer attachments. |
158
+ | accept | `string` | No | `''` | Accepted file types for the attach button. |
159
+ | multiple | `boolean` | No | `false` | Allow multiple files per attach pick. |
160
+ | allowCopy | `boolean` | No | `false` | Show copy buttons on assistant messages. |
161
+ | closeLabel | `string` | No | `'Close'`| Aria-label for the header close button. |
162
+ | showClose | `boolean` | No | `-` | Force the close button on/off (defaults to showing when `onclose` is set). |
163
+ | headerAvatar | `Snippet` | No | `-` | Brand/avatar mark in the header (takes precedence over `image`). |
164
+ | headerActions | `Snippet` | No | `-` | Extra inline header actions. |
165
+ | headerContent | `Snippet` | No | `-` | Extra content as a full-width second row in the header (toolbar, status…). |
166
+ | message | `Snippet<[ChatMessageData]>` | No | `-` | Custom per-message rendering. |
167
+ | empty | `Snippet` | No | `-` | Empty-state content. |
168
+ | composerLeading | `Snippet` | No | `-` | Content before the composer input. |
169
+ | sendIcon / stopIcon / voiceIcon / attachIcon | `Snippet` | No | `-` | Custom composer icons; each falls back to a built-in asset. |
170
+ | testId | `string` | No | `-` | `data-pw` on the root element. |
171
+ | classes | `string` | No | `-` | Class string on the root element. |
172
+
173
+ ## Events
174
+
175
+ | Event | Type | Description |
176
+ | ------------ | ------------------------------------- | --------------------------------------------------------------------- |
177
+ | onsend | `(value: string, attachments: File[]) => void` | Fires when a message is submitted from the composer. |
178
+ | onsuggestion | `(value: string, index: number) => void` | Fires when a suggestion chip is picked. Falls back to `onsend`. |
179
+ | onclose | `() => void` | Fires when the header close button is pressed. |
180
+ | onstop | `() => void` | Enables the stop button (e.g. `chat.stop`). |
181
+ | onvoice | `() => void` | Enables the composer voice button. |
182
+ | onattach | `(files: File[]) => void` | Enables the composer attach button. |
183
+ | onretry | `() => void` | Enables retry on the latest assistant message (e.g. `chat.retry`). |
184
+ | onfeedback | `(value: 'up' \| 'down', message: ChatMessageData) => void` | Enables 👍/👎 on assistant messages. |
185
+
186
+ ## CSS Variables
187
+
188
+ | Variable | Default | CSS Property | Description |
189
+ | ----------------------------- | ------------- | ---------------- | ------------------------------------ |
190
+ | `--chat-height` | `100%` | height | Height of the chat surface. |
191
+ | `--chat-width` | `100%` | width | Width of the chat surface. |
192
+ | `--chat-background` | `#ffffff` | background | Background of the chat surface. |
193
+ | `--chat-border` | `none` | border | Border of the chat surface. |
194
+ | `--chat-border-radius` | `0` | border-radius | Corner rounding of the chat surface. |
195
+ | `--chat-footer-gap` | `10px` | gap | Gap between footer rows. |
196
+ | `--chat-footer-padding` | `12px 1.5rem` | padding | Padding of the footer area. |
197
+ | `--chat-footer-background` | `transparent` | background | Footer background. |
198
+ | `--chat-footer-border-top` | `none` | border-top | Border above the footer. |
199
+ | `--chat-tool-status-justify` | `center` | justify-content | Alignment of the tool-status row. |
200
+
201
+ Child components (`ChatHeader`, `ChatMessageList`, `ChatComposer`, `ChatToolStatus`, `ChatSuggestions`, `ChatMessage`) are themed through their own CSS variables, which cascade into `Chat`.
202
+
203
+ ## Web Component
204
+
205
+ Tag: `<pui-chat>`
206
+
207
+ ```html
208
+ <pui-chat title="Assistant"></pui-chat>
209
+ ```
210
+
211
+ Set `.messages`, `.onsend`, and other object/array props via JavaScript.
@@ -0,0 +1,199 @@
1
+ # ChatBubble
2
+
3
+ A floating launcher button (FAB) pinned to a corner of the viewport that toggles a floating panel. Drop a `Chat` (or anything) inside via the `children` snippet — `ChatBubble` owns only the launcher, positioning, and open/close behavior, staying decoupled from what it contains. The launcher reuses the `Button` component and shows a chat icon when closed / a close icon when open (both overridable via snippet). Opening moves focus into the panel (`role="dialog"`), Escape closes it and returns focus to the launcher.
4
+
5
+ Set **`draggable`** to let the user grab the launcher and reposition the whole widget (a click still toggles — drag and click are distinguished by a small movement threshold, and the launcher is kept within the viewport). Two drag styles, via **`dragMode`**:
6
+
7
+ - **`'snap'` (default)** — anchored, mobile chat-head style: while dragging the bubble follows the pointer, and on release it snaps to the nearest left/right screen edge based on where you let go. The panel re-opens toward the screen center, and the resize edges follow suit.
8
+ - **`'free'`** — the bubble stays exactly where it's dropped.
9
+
10
+ The offset is the bindable `dragX`/`dragY`. Set **`resizable`** to let the user resize the panel from the edges nearest the screen center (via the `Resizable` component); the size is the bindable `panelWidth`/`panelHeight`. Both `draggable` and `resizable` are off by default.
11
+
12
+ **Adaptive placement.** However the launcher ends up positioned — by `position`, dragging, or snapping — the panel opens toward the side with the most room: it **drops down** when the bubble is in the top half of the viewport and opens upward when it's in the bottom half (and likewise left/right), so the panel never opens off-screen. The resize handles follow the chosen direction. The panel is also **capped to the available space** in that direction, so on small/short viewports it shrinks to fit (the conversation scrolls inside) rather than spilling past the screen edge.
13
+
14
+ ## Usage
15
+
16
+ ```svelte
17
+ <script>
18
+ import { ChatBubble, Chat } from 'polymorph-ui-components';
19
+
20
+ let messages = $state([]);
21
+ let value = $state('');
22
+ let open = $state(false);
23
+
24
+ function onsend(text) {
25
+ messages.push({ id: crypto.randomUUID(), role: 'user', content: text });
26
+ // …call your API, append an assistant reply…
27
+ }
28
+ </script>
29
+
30
+ <ChatBubble bind:open label="Open chat">
31
+ <Chat {messages} bind:value title="Assistant" {onsend} onclose={() => (open = false)} />
32
+ </ChatBubble>
33
+ ```
34
+
35
+ The panel is sized by `panelWidth`/`panelHeight`; the child fills it (`Chat`'s root defaults to `100%`). `ChatBubble` owns the fixed positioning and the open/close state, so the same `open` you bind here can also be driven from elsewhere in your app.
36
+
37
+ ## Usage — draggable, snap & resizable
38
+
39
+ Enable the chat-head behaviors. With the default `dragMode="snap"` the bubble docks to the nearest screen edge on release; pass `dragMode="free"` to leave it wherever it is dropped. `resizable` adds drag handles to the open panel.
40
+
41
+ ```svelte
42
+ <!-- snap to the nearest edge (default), resizable, position + size persisted -->
43
+ <ChatBubble
44
+ bind:open
45
+ draggable
46
+ resizable
47
+ bind:dragX
48
+ bind:dragY
49
+ bind:panelWidth
50
+ bind:panelHeight
51
+ position="bottom-right"
52
+ >
53
+ <Chat {messages} bind:value {onsend} onclose={() => (open = false)} />
54
+ </ChatBubble>
55
+
56
+ <!-- free-form drag: stays exactly where dropped -->
57
+ <ChatBubble bind:open draggable dragMode="free">
58
+ <Chat {messages} bind:value {onsend} onclose={() => (open = false)} />
59
+ </ChatBubble>
60
+ ```
61
+
62
+ Bind `dragX`/`dragY` (and `panelWidth`/`panelHeight`) to persist the user's placement and size across sessions. The resize handles are chosen automatically from where the bubble currently sits — e.g. a bottom-right bubble resizes from its top/left edges, and after snapping to the left the panel and its handles flip toward center.
63
+
64
+ ## Usage — decoupled controller
65
+
66
+ Pair with `ChatController` so the bubble is a complete, transport-agnostic chat widget — adapt SSE, WebSocket, or polling behind the `transport` and the UI never changes:
67
+
68
+ ```svelte
69
+ <script>
70
+ import { ChatBubble, Chat, ChatController } from 'polymorph-ui-components';
71
+
72
+ const chat = new ChatController({
73
+ typewriter: true,
74
+ transport: async ({ message, signal }, handlers) => {
75
+ const res = await fetch('/api/chat', {
76
+ method: 'POST',
77
+ body: JSON.stringify({ message }),
78
+ signal
79
+ });
80
+ // …parse your stream and call the handlers…
81
+ handlers.onText('Hello');
82
+ handlers.onDone?.();
83
+ }
84
+ });
85
+
86
+ let value = $state('');
87
+ let open = $state(false);
88
+ </script>
89
+
90
+ <ChatBubble bind:open draggable resizable label="Support">
91
+ <Chat
92
+ messages={chat.messages}
93
+ bind:value
94
+ title="Support"
95
+ subtitle="We reply in minutes"
96
+ streaming={chat.isStreaming}
97
+ onsend={(text) => chat.send(text)}
98
+ onstop={() => chat.stop()}
99
+ onclose={() => (open = false)}
100
+ />
101
+ </ChatBubble>
102
+ ```
103
+
104
+ ## Custom launcher icons
105
+
106
+ Both launcher icons are snippets; provide your own for closed (`icon`) and open (`openIcon`) states, or leave them for the built-in chat / close icons.
107
+
108
+ ```svelte
109
+ <ChatBubble bind:open>
110
+ {#snippet icon()}<MessageIcon />{/snippet}
111
+ {#snippet openIcon()}<ChevronDownIcon />{/snippet}
112
+ <Chat {messages} bind:value {onsend} onclose={() => (open = false)} />
113
+ </ChatBubble>
114
+ ```
115
+
116
+ ## Snippets
117
+
118
+ | Snippet | Description |
119
+ | -------- | ----------------------------------------------------------------------------------- |
120
+ | children | Panel content, typically a `Chat`. The panel sizes it to `panelWidth`/`panelHeight`.|
121
+ | icon | Launcher contents when closed. Falls back to a built-in chat icon. |
122
+ | openIcon | Launcher contents when open. Falls back to a built-in close icon. |
123
+
124
+ ## Props
125
+
126
+ | Prop | Type | Required | Default | Description |
127
+ | -------------- | -------------------------------------------------------- | -------- | --------------- | ----------------------------------------------------------- |
128
+ | open | `boolean` | No | `false` | Bindable. Whether the panel is open. |
129
+ | position | `'bottom-right'\|'bottom-left'\|'top-right'\|'top-left'` | No | `'bottom-right'`| Which corner the launcher pins to (panel opens toward center).|
130
+ | label | `string` | No | `'Open chat'` | Launcher aria-label (closed) and panel aria-label. |
131
+ | closeLabel | `string` | No | `'Close chat'` | Launcher aria-label when open. |
132
+ | icon | `Snippet` | No | `-` | Launcher icon when closed. Falls back to a built-in asset. |
133
+ | openIcon | `Snippet` | No | `-` | Launcher icon when open. Falls back to a built-in close icon.|
134
+ | children | `Snippet` | No | `-` | Panel content (e.g. a `Chat`). |
135
+ | draggable | `boolean` | No | `false` | Let the user drag the launcher to reposition the whole widget.|
136
+ | dragMode | `'snap' \| 'free'` | No | `'snap'` | `'snap'` anchors to the nearest left/right edge on release; `'free'` stays where dropped. |
137
+ | dragX | `number` | No | `0` | Bindable. Horizontal drag offset (px) from the anchored corner. |
138
+ | dragY | `number` | No | `0` | Bindable. Vertical drag offset (px) from the anchored corner. |
139
+ | resizable | `boolean` | No | `false` | Allow resizing the panel (via `Resizable`). |
140
+ | panelWidth | `number` | No | `380` | Bindable. Panel width in px. |
141
+ | panelHeight | `number` | No | `600` | Bindable. Panel height in px. |
142
+ | minPanelWidth | `number` | No | `280` | Minimum panel width when resizing. |
143
+ | minPanelHeight | `number` | No | `360` | Minimum panel height when resizing. |
144
+ | testId | `string` | No | `-` | `data-pw` on the root element. |
145
+ | classes | `string` | No | `-` | Class string on the root element. |
146
+
147
+ ## Events
148
+
149
+ | Event | Type | Description |
150
+ | -------- | -------------------------- | -------------------------------------------- |
151
+ | onopen | `() => void` | Fires when the panel opens. |
152
+ | onclose | `() => void` | Fires when the panel closes. |
153
+ | ontoggle | `(open: boolean) => void` | Fires on any open/close, with the new state. |
154
+
155
+ ## Accessibility
156
+
157
+ - The launcher is a real `Button` with an `aria-label` (`label` when closed, `closeLabel` when open) and `aria-expanded` reflecting the panel state.
158
+ - Opening moves focus into the panel (`role="dialog"`, `aria-label={label}`); Escape closes the panel and returns focus to the launcher.
159
+ - Dragging is a pointer enhancement layered on the clickable launcher — the widget is fully usable (open, close, converse) without ever dragging, and a drag never fires a toggle.
160
+ - The snap animation honors `prefers-reduced-motion: reduce` (the transition is dropped).
161
+
162
+ ## Internal Dependencies
163
+
164
+ Reuses `Button` (the launcher) and `Resizable` (panel resizing). Its content is whatever you pass — most commonly `Chat` (optionally driven by `ChatController`).
165
+
166
+ ## CSS Variables
167
+
168
+ | Variable | Default | CSS Property | Description |
169
+ | ------------------------------------- | --------------------------------------------- | ------------- | --------------------------------- |
170
+ | `--chat-bubble-z-index` | `1000` | z-index | Stacking order of the widget. |
171
+ | `--chat-bubble-offset-x` | `24px` | left/right | Horizontal distance from the edge.|
172
+ | `--chat-bubble-offset-y` | `24px` | top/bottom | Vertical distance from the edge. |
173
+ | `--chat-bubble-size` | `56px` | height/width | Launcher button size. |
174
+ | `--chat-bubble-padding` | `16px` | padding | Launcher icon padding. |
175
+ | `--chat-bubble-border-radius` | `50%` | border-radius | Launcher corner rounding. |
176
+ | `--chat-bubble-background-color` | `#18181b` | background | Launcher background. |
177
+ | `--chat-bubble-color` | `#ffffff` | color | Launcher icon color. |
178
+ | `--chat-bubble-hover-background-color`| `#27272a` | background | Launcher hover background. |
179
+ | `--chat-bubble-box-shadow` | `0 8px 24px rgba(0,0,0,0.25)` | box-shadow | Launcher shadow. |
180
+ | `--chat-bubble-snap-transition` | `transform 0.28s cubic-bezier(0.22,1,0.36,1)` | transition | Snap/reposition animation (disabled while dragging and under reduced-motion). |
181
+ | `--chat-bubble-panel-gap` | `16px` | bottom/top | Gap between launcher and panel. |
182
+ | `--chat-bubble-panel-max-width` | `calc(100vw - 32px)` | max-width | Panel max width. |
183
+ | `--chat-bubble-panel-max-height` | `calc(100dvh - 120px)` | max-height | Panel max height. |
184
+ | `--chat-bubble-panel-border-radius` | `16px` | border-radius | Panel corner rounding. |
185
+ | `--chat-bubble-panel-background` | `#ffffff` | background | Panel background. |
186
+ | `--chat-bubble-panel-box-shadow` | `0 16px 48px rgba(0,0,0,0.22)` | box-shadow | Panel shadow. |
187
+ | `--chat-bubble-resize-handle-color` | `transparent` | background | Resize handle fill (when `resizable`). |
188
+
189
+ Panel size is set via the bindable `panelWidth`/`panelHeight` props (not CSS variables), so it stays in sync when the panel is resized.
190
+
191
+ ## Web Component
192
+
193
+ Tag: `<pui-chat-bubble>`
194
+
195
+ ```html
196
+ <pui-chat-bubble label="Open chat" draggable resizable></pui-chat-bubble>
197
+ ```
198
+
199
+ Put panel content in the default slot, and set object props (`icon`, `openIcon`, `onopen`, …) via JavaScript. Boolean/number/string props map to attributes: `draggable`, `resizable`, `drag-mode`, `drag-x`, `drag-y`, `panel-width`, `panel-height`, `min-panel-width`, `min-panel-height`, `position`, `label`, `close-label`.
@@ -0,0 +1,96 @@
1
+ # ChatComposer
2
+
3
+ An auto-growing message input with a send button (the `Button` component). Enter submits and Shift+Enter inserts a newline (configurable via `submitOnEnter`); the send button is disabled until there is non-whitespace text or an attachment. The input clears on submit. All of the extra controls are **opt-in** — wiring a callback enables the matching button:
4
+
5
+ - `onattach` → a paperclip button that opens a file picker; picked files appear as removable chips (`Pill`) above the input and are bindable via `attachments`.
6
+ - `onvoice` → a mic button for voice input (`recording` toggles its active styling).
7
+ - `streaming` + `onstop` → the send button becomes a **stop** button while a reply streams.
8
+
9
+ Every icon falls back to a built-in asset and can be replaced with a snippet.
10
+
11
+ ## Usage
12
+
13
+ ```svelte
14
+ <script>
15
+ import { ChatComposer } from 'polymorph-ui-components';
16
+
17
+ let value = $state('');
18
+ </script>
19
+
20
+ <ChatComposer bind:value placeholder="Type a message…" onsubmit={(text) => console.log(text)} />
21
+ ```
22
+
23
+ ## Props
24
+
25
+ | Prop | Type | Required | Default | Description |
26
+ | ------------- | ----------- | -------- | ---------------- | ----------------------------------------------------------- |
27
+ | value | `string` | No | `''` | Bindable. The draft text. |
28
+ | placeholder | `string` | No | `''` | Input placeholder. |
29
+ | disabled | `boolean` | No | `false` | Disable input and buttons. |
30
+ | submitOnEnter | `boolean` | No | `true` | Submit on Enter (Shift+Enter inserts a newline). |
31
+ | maxLength | `number` | No | `0` | Character cap; `0` disables the limit. |
32
+ | streaming | `boolean` | No | `false` | When true, the send button becomes a stop button. |
33
+ | recording | `boolean` | No | `false` | Visual active state for the voice button. |
34
+ | attachments | `File[]` | No | `[]` | Bindable. Pending attachments, shown as removable chips. |
35
+ | accept | `string` | No | `''` | Accepted file types for the attach button. |
36
+ | multiple | `boolean` | No | `false` | Allow multiple files per pick. |
37
+ | sendLabel / stopLabel / voiceLabel / attachLabel | `string` | No | `…` | Aria-labels for the buttons. |
38
+ | sendIcon / stopIcon / voiceIcon / attachIcon | `Snippet` | No | `-` | Custom icons; each falls back to a built-in asset. |
39
+ | leading | `Snippet` | No | `-` | Content before the input. |
40
+ | testId | `string` | No | `-` | `data-pw` on the root element. |
41
+ | classes | `string` | No | `-` | Class string on the root element. |
42
+
43
+ ## Events
44
+
45
+ | Event | Type | Description |
46
+ | -------- | --------------------------- | --------------------------------------------------- |
47
+ | onsubmit | `(value: string, attachments: File[]) => void` | Fires on submit with the value and pending attachments. |
48
+ | oninput | `(value: string, event: Event) => void` | Fires on every input change. |
49
+ | onkeydown| `(event: KeyboardEvent) => void` | Fires on key down in the input. |
50
+ | onstop | `() => void` | Enables the stop button. Fires when stop is pressed. |
51
+ | onvoice | `() => void` | Enables the voice button. Fires when the mic is pressed. |
52
+ | onattach | `(files: File[]) => void` | Enables the attach button. Fires with newly picked files. |
53
+
54
+ ## CSS Variables
55
+
56
+ | Variable | Default | CSS Property | Description |
57
+ | ------------------------------------------- | ------------- | -------------- | ------------------------------------ |
58
+ | `--chat-composer-width` | `100%` | width | Width of the composer. |
59
+ | `--chat-composer-gap` | `8px` | gap | Gap between leading/input/send. |
60
+ | `--chat-composer-padding` | `8px` | padding | Outer padding. |
61
+ | `--chat-composer-background` | `#ffffff` | background | Composer background. |
62
+ | `--chat-composer-border` | `1px solid #e4e4e7` | border | Composer border. |
63
+ | `--chat-composer-border-radius` | `24px` | border-radius | Composer corner rounding. |
64
+ | `--chat-composer-box-shadow` | `none` | box-shadow | Composer shadow. |
65
+ | `--chat-composer-disabled-opacity` | `0.6` | opacity | Opacity when disabled. |
66
+ | `--chat-composer-font-family` | `inherit` | font-family | Input font family. |
67
+ | `--chat-composer-font-size` | `0.9375rem` | font-size | Input font size. |
68
+ | `--chat-composer-line-height` | `1.5` | line-height | Input line height. |
69
+ | `--chat-composer-color` | `#18181b` | color | Input text color. |
70
+ | `--chat-composer-placeholder-color` | `#a1a1aa` | color | Placeholder color. |
71
+ | `--chat-composer-input-padding` | `6px 4px` | padding | Input padding. |
72
+ | `--chat-composer-max-height` | `160px` | max-height | Max input height before scrolling. |
73
+ | `--chat-composer-send-size` | `40px` | height/width | Send button size. |
74
+ | `--chat-composer-send-padding` | `8px` | padding | Send button padding. |
75
+ | `--chat-composer-send-border-radius` | `50%` | border-radius | Send button corner rounding. |
76
+ | `--chat-composer-send-background-color` | `#18181b` | background | Send button background. |
77
+ | `--chat-composer-send-color` | `#ffffff` | color | Send icon color. |
78
+ | `--chat-composer-send-hover-background-color`| `#27272a` | background | Send button hover background. |
79
+ | `--chat-composer-stack-gap` | `8px` | gap | Gap between attachment chips and the input row. |
80
+ | `--chat-composer-attachments-gap` | `6px` | gap | Gap between attachment chips. |
81
+ | `--chat-composer-action-size` | `36px` | height/width | Size of the attach/voice buttons. |
82
+ | `--chat-composer-action-padding` | `8px` | padding | Padding of the attach/voice buttons. |
83
+ | `--chat-composer-action-color` | `#52525b` | color | Icon color of the attach/voice buttons. |
84
+ | `--chat-composer-action-hover-background-color` | `#f4f4f5` | background | Attach/voice hover background. |
85
+ | `--chat-composer-voice-recording-background-color` | `#fee2e2` | background | Voice button background while recording. |
86
+ | `--chat-composer-voice-recording-color` | `#dc2626` | color | Voice icon color while recording. |
87
+ | `--chat-composer-stop-background-color` | `#18181b` | background | Stop button background. |
88
+ | `--chat-composer-stop-color` | `#ffffff` | color | Stop icon color. |
89
+
90
+ ## Web Component
91
+
92
+ Tag: `<pui-chat-composer>`
93
+
94
+ ```html
95
+ <pui-chat-composer placeholder="Type a message…"></pui-chat-composer>
96
+ ```
@@ -0,0 +1,74 @@
1
+ # ChatHeader
2
+
3
+ A header bar for a chat surface: an optional avatar/brand mark, a title and subtitle, optional inline `actions`, and a close button (the `Button` component) that appears when `onclose` is provided. For arbitrary extra content (a toolbar, status line, tabs, model selector), pass a `children` snippet — it renders as a full-width second row below the main bar. The close icon falls back to a built-in asset and can be overridden with a snippet.
4
+
5
+ ## Usage
6
+
7
+ ```svelte
8
+ <script>
9
+ import { ChatHeader } from 'polymorph-ui-components';
10
+ </script>
11
+
12
+ <ChatHeader title="Shopping Assistant" subtitle="Online" onclose={() => {}} />
13
+ ```
14
+
15
+ ## Props
16
+
17
+ | Prop | Type | Required | Default | Description |
18
+ | --------- | ----------- | -------- | --------- | ----------------------------------------------------------- |
19
+ | title | `string` | No | `''` | Title text. Hidden when empty. |
20
+ | subtitle | `string` | No | `''` | Subtitle text. Hidden when empty. |
21
+ | image | `string` | No | `-` | Optional brand image URL (rendered via `Img`). Off by default. |
22
+ | imageAlt | `string` | No | `''` | Alt text for the image. |
23
+ | avatar | `Snippet` | No | `-` | Brand/avatar mark left of the title (takes precedence over `image`). |
24
+ | actions | `Snippet` | No | `-` | Extra actions right of the title (before close). |
25
+ | closeIcon | `Snippet` | No | `-` | Custom close icon. Falls back to the built-in asset. |
26
+ | closeLabel| `string` | No | `'Close'` | Aria-label for the close button. |
27
+ | showClose | `boolean` | No | `-` | Force the close button on/off (defaults to showing when `onclose` is set). |
28
+ | children | `Snippet` | No | `-` | Extra content rendered as a full-width second row below the main bar. |
29
+ | testId | `string` | No | `-` | `data-pw` on the root element. |
30
+ | classes | `string` | No | `-` | Class string on the root element. |
31
+
32
+ ## Events
33
+
34
+ | Event | Type | Description |
35
+ | ------- | -------------- | --------------------------------------- |
36
+ | onclose | `() => void` | Fires when the close button is pressed. |
37
+
38
+ ## CSS Variables
39
+
40
+ | Variable | Default | CSS Property | Description |
41
+ | ------------------------------------------ | ---------------- | ---------------- | --------------------------------- |
42
+ | `--chat-header-width` | `100%` | width | Header width. |
43
+ | `--chat-header-padding` | `0.75rem 1.5rem` | padding | Header padding. |
44
+ | `--chat-header-gap` | `12px` | gap | Gap between brand and trailing. |
45
+ | `--chat-header-background` | `transparent` | background | Header background. |
46
+ | `--chat-header-border-bottom` | `none` | border-bottom | Header bottom border. |
47
+ | `--chat-header-brand-gap` | `10px` | gap | Gap between avatar and titles. |
48
+ | `--chat-header-extra-gap` | `8px` | gap | Gap between the main bar and the `children` row. |
49
+ | `--chat-header-image-size` | `28px` | height/width | Size of the header image. |
50
+ | `--chat-header-image-border-radius` | `50%` | border-radius | Header image corner rounding. |
51
+ | `--chat-header-image-object-fit` | `cover` | object-fit | Header image object-fit. |
52
+ | `--chat-header-title-font-size` | `0.95rem` | font-size | Title font size. |
53
+ | `--chat-header-title-font-weight` | `600` | font-weight | Title weight. |
54
+ | `--chat-header-title-color` | `#18181b` | color | Title color. |
55
+ | `--chat-header-subtitle-font-size` | `0.7rem` | font-size | Subtitle font size. |
56
+ | `--chat-header-subtitle-font-weight` | `500` | font-weight | Subtitle weight. |
57
+ | `--chat-header-subtitle-color` | `#71717a` | color | Subtitle color. |
58
+ | `--chat-header-subtitle-text-transform` | `none` | text-transform | Subtitle text transform. |
59
+ | `--chat-header-subtitle-letter-spacing` | `normal` | letter-spacing | Subtitle letter spacing. |
60
+ | `--chat-header-trailing-gap` | `8px` | gap | Gap between actions and close. |
61
+ | `--chat-header-close-size` | `36px` | height/width | Close button size. |
62
+ | `--chat-header-close-padding` | `8px` | padding | Close button padding. |
63
+ | `--chat-header-close-border-radius` | `50%` | border-radius | Close button corner rounding. |
64
+ | `--chat-header-close-background-color` | `transparent` | background | Close button background. |
65
+ | `--chat-header-close-color` | `#52525b` | color | Close icon color. |
66
+ | `--chat-header-close-hover-background-color`| `#f4f4f5` | background | Close button hover background. |
67
+
68
+ ## Web Component
69
+
70
+ Tag: `<pui-chat-header>`
71
+
72
+ ```html
73
+ <pui-chat-header title="Assistant" subtitle="Online"></pui-chat-header>
74
+ ```