Skip to content

Commit 549aa67

Browse files
committed
feat(web): widen the reading column and soften the composer card
The chat column was 760px and the composer card read as a small box with a fixed-height input. Widen `--read-max` to 928px, which the message list and the composer dock both derive from, so one value moves both. The card takes a 24px radius from the new `--r-xl` token, a translucent blurred surface, and a border that strengthens on hover and focus-within. The input grows with its content up to 384px and scrolls past that. Toolbar controls become 30px circles, with a divider after the attachment button and a 20px send glyph in 5px of padding. The send button keeps the theme accent rather than an inverted fill: the Pythinker theme defines that colour as its brand periwinkle. Behaviour is unchanged; this is geometry and colour only.
1 parent f97b801 commit 549aa67

6 files changed

Lines changed: 145 additions & 39 deletions

File tree

.changeset/web-composer-shell.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@pymodel/pythinker-code": patch
3+
---
4+
5+
Widen the chat reading column to 928px and restyle the composer card: a 24px radius, a translucent blurred surface, a border that strengthens on hover and focus, and an input that grows to 384px before it scrolls. The toolbar controls are 30px circles with a divider after the attachment button.

apps/pythinker-web/src/components/ChatDock.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -332,8 +332,9 @@ defineExpose({ loadForEdit });
332332
333333
/* Use a different surface so the card radius reads against the page, especially in dark mode; --sh only exists on the modern and pythinker themes, hence the fallback. */
334334
.chat-dock :deep(.composer-card) {
335-
background: var(--panel);
336-
box-shadow: var(--sh, 0 6px 18px rgba(0, 0, 0, 0.28));
335+
background: color-mix(in srgb, var(--panel) 78%, transparent);
336+
backdrop-filter: blur(16px);
337+
box-shadow: var(--sh, 0 6px 18px color-mix(in srgb, var(--ink) 28%, transparent));
337338
}
338339
339340
.chat-dock.align-center { margin-left: auto; margin-right: auto; }

apps/pythinker-web/src/components/Composer.vue

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1078,6 +1078,7 @@ function selectModel(modelId: string): void {
10781078
>
10791079
<svg class="attach-icon" viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><path d="M8 3v10M3 8h10"/></svg>
10801080
</button>
1081+
<span v-if="hasUpload" class="toolbar-divider" aria-hidden="true" />
10811082

10821083
<!-- Permission pill — click to open dropdown -->
10831084
<span
@@ -1318,12 +1319,18 @@ function selectModel(modelId: string): void {
13181319
.composer-card {
13191320
position: relative;
13201321
border: 1px solid var(--line);
1321-
border-radius: 16px;
1322-
background: var(--bg);
1323-
box-shadow: 0 1px 4px rgba(0,0,0,0.04);
1322+
border-radius: var(--r-xl);
1323+
background: color-mix(in srgb, var(--bg) 82%, transparent);
1324+
backdrop-filter: blur(16px);
1325+
box-shadow: 0 1px 4px color-mix(in srgb, var(--ink) 4%, transparent);
13241326
transition: border-color 0.15s, box-shadow 0.15s;
13251327
}
13261328
1329+
.composer-card:hover,
1330+
.composer-card:focus-within {
1331+
border-color: color-mix(in srgb, var(--line) 55%, var(--ink));
1332+
}
1333+
13271334
13281335
13291336
/* Attachment strip */
@@ -1535,10 +1542,11 @@ function selectModel(modelId: string): void {
15351542
font-family: var(--mono);
15361543
font-size: var(--ui-font-size);
15371544
background: transparent;
1538-
height: 56px;
1545+
height: auto;
15391546
min-height: 56px;
1540-
max-height: 56px;
1547+
max-height: 384px;
15411548
overflow-y: auto;
1549+
field-sizing: content;
15421550
line-height: 1.5;
15431551
margin-bottom: 6px;
15441552
}
@@ -1570,12 +1578,13 @@ function selectModel(modelId: string): void {
15701578
/* Send button — circular icon (morphs into the abort square while running) */
15711579
.send {
15721580
width: 30px;
1581+
min-width: 30px;
15731582
height: 30px;
15741583
border-radius: 50%;
15751584
background: var(--blue);
15761585
color: var(--bg); /* on-accent text — readable in dark + mono-dark */
15771586
border: none;
1578-
padding: 0;
1587+
padding: 5px;
15791588
display: flex;
15801589
align-items: center;
15811590
justify-content: center;
@@ -1595,6 +1604,8 @@ function selectModel(modelId: string): void {
15951604
15961605
.send svg {
15971606
flex: none;
1607+
width: 20px;
1608+
height: 20px;
15981609
}
15991610
16001611
.send-icon {
@@ -1621,9 +1632,9 @@ function selectModel(modelId: string): void {
16211632
align-items: center;
16221633
justify-content: space-between;
16231634
padding: 6px 10px 4px;
1624-
background: color-mix(in srgb, var(--panel2), black 1.5%);
1635+
background: color-mix(in srgb, var(--panel2) 98.5%, var(--ink) 1.5%);
16251636
position: relative;
1626-
border-radius: 0 0 var(--r-md) var(--r-md);
1637+
border-radius: 0 0 var(--r-xl) var(--r-xl);
16271638
}
16281639
16291640
.toolbar-left,
@@ -1635,14 +1646,16 @@ function selectModel(modelId: string): void {
16351646
overflow: hidden;
16361647
}
16371648
1638-
/* Attach button (pill style, matches permission/plan) */
1649+
/* Attach button */
16391650
.attach-btn {
16401651
display: inline-flex;
16411652
align-items: center;
16421653
justify-content: center;
1643-
gap: 4px;
1644-
padding: 2px 7px;
1645-
border-radius: 6px;
1654+
width: 30px;
1655+
min-width: 30px;
1656+
height: 30px;
1657+
padding: 0;
1658+
border-radius: 50%;
16461659
font-size: var(--ui-font-size);
16471660
color: var(--muted);
16481661
cursor: pointer;
@@ -1663,6 +1676,14 @@ function selectModel(modelId: string): void {
16631676
background: var(--soft);
16641677
}
16651678
1679+
.toolbar-divider {
1680+
width: 1px;
1681+
height: 16px;
1682+
flex: none;
1683+
margin: 0 4px;
1684+
background: var(--line);
1685+
}
1686+
16661687
/* Permission pill */
16671688
.perm-pill {
16681689
display: inline-flex;
@@ -2125,19 +2146,19 @@ function selectModel(modelId: string): void {
21252146
var(--dock-inline-left, max(12px, env(safe-area-inset-left)));
21262147
}
21272148
.composer-card {
2128-
border-radius: 14px;
2149+
border-radius: var(--r-xl);
21292150
max-width: 100%;
21302151
}
21312152
.input-row {
21322153
gap: 6px;
21332154
min-width: 0;
21342155
}
2135-
/* Send → 36px round (hide the SVG arrow, show only the ::after glyph) */
2156+
/* Send → 30px round (hide the SVG arrow, show only the ::after glyph) */
21362157
.send {
2137-
width: 36px;
2138-
height: 36px;
2139-
min-width: 36px;
2140-
padding: 0;
2158+
width: 30px;
2159+
height: 30px;
2160+
min-width: 30px;
2161+
padding: 5px;
21412162
border-radius: 50%;
21422163
font-size: 0;
21432164
align-self: flex-end;
@@ -2149,7 +2170,7 @@ function selectModel(modelId: string): void {
21492170
.send::after {
21502171
content: "";
21512172
/* Fixed icon glyph size — not part of the UI font scale. */
2152-
font-size: 17px;
2173+
font-size: 20px;
21532174
line-height: 1;
21542175
color: var(--bg);
21552176
}
@@ -2177,14 +2198,13 @@ function selectModel(modelId: string): void {
21772198
max-width: calc(100vw - 24px);
21782199
}
21792200
2180-
/* Bump mobile font sizes +2px and pin input at 16px to prevent iOS zoom.
2181-
Single-line-friendly height: 56px desktop default → 44px touch target. */
2201+
/* Bump mobile font sizes +2px and pin input at 16px to prevent iOS zoom. */
21822202
.ph {
21832203
/* Pinned at 16px to prevent iOS auto-zoom on focus (not part of UI font scale). */
21842204
font-size: 16px;
2185-
height: 44px;
2205+
height: auto;
21862206
min-height: 44px;
2187-
max-height: 44px;
2207+
max-height: 384px;
21882208
}
21892209
.model-pill,
21902210
.attach-btn {

apps/pythinker-web/src/components/ConversationPane.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1047,7 +1047,7 @@ defineExpose({ loadComposerForEdit });
10471047

10481048
<style scoped>
10491049
.con {
1050-
--read-max: 760px;
1050+
--read-max: 928px;
10511051
display: flex;
10521052
flex-direction: column;
10531053
min-width: 0;

apps/pythinker-web/src/style.css

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,15 @@
4141
--err: var(--dsw-alias-state-error-secondary);
4242
/* Subtle hover wash for icon buttons (toast close, etc.). */
4343
--hover: var(--dsw-alias-interactive-bg-hover);
44-
/* Radius scale (Modern). A regular 8 / 12 / 16 progression: list items + pill
45-
controls use --r-sm, the input box + cards use --r-md, dialogs use --r-lg.
44+
/* Radius scale (Modern). A regular 8 / 12 / 16 / 24 progression: list items + pill
45+
controls use --r-sm, input boxes use --r-md, dialogs use --r-lg, and the
46+
composer card uses --r-xl.
4647
The Terminal theme keeps its own sharper 3–4px corners. */
4748
--r-xs: 6px;
4849
--r-sm: 8px;
4950
--r-md: 12px;
5051
--r-lg: 16px;
52+
--r-xl: 24px;
5153
--ui-font-size: 14px;
5254
--ui-font-size-sm: calc(var(--ui-font-size) - 1px);
5355
--ui-font-size-xs: calc(var(--ui-font-size) - 2px);
@@ -387,9 +389,10 @@ html[data-color-scheme="dark"][data-theme="modern"] {
387389
to the global sheet where they apply reliably. The chat surface is white, so
388390
the composer card reads as a rounded card via a soft shadow + hairline border. */
389391
:is(html[data-theme="modern"], html[data-theme="pythinker"]) .composer-card {
390-
border-radius: var(--r-md);
392+
border-radius: var(--r-xl);
391393
border: 1px solid var(--line);
392-
background: var(--bg);
394+
background: color-mix(in srgb, var(--bg) 82%, transparent);
395+
backdrop-filter: blur(16px);
393396
box-shadow: var(--shc);
394397
}
395398

@@ -408,12 +411,12 @@ html[data-color-scheme="dark"][data-theme="modern"] {
408411
width: 30px;
409412
min-width: 30px;
410413
height: 30px;
411-
padding: 0;
414+
padding: 5px;
412415
border-radius: 50%;
413416
display: flex;
414417
align-items: center;
415418
justify-content: center;
416-
box-shadow: 0 1px 2px rgba(20, 23, 28, 0.1);
419+
box-shadow: 0 1px 2px color-mix(in srgb, var(--ink) 10%, transparent);
417420
align-self: flex-end;
418421
}
419422
:is(html[data-theme="modern"], html[data-theme="pythinker"]) .send:hover { background: var(--blue2); }
@@ -427,10 +430,11 @@ html[data-color-scheme="dark"][data-theme="modern"] {
427430
background: color-mix(in srgb, var(--panel), black 1.5%);
428431
}
429432
:is(html[data-theme="modern"], html[data-theme="pythinker"]) .attach-btn {
430-
width: 26px;
431-
height: 26px;
433+
width: 30px;
434+
min-width: 30px;
435+
height: 30px;
432436
padding: 0;
433-
border-radius: var(--r-sm);
437+
border-radius: 50%;
434438
}
435439
:is(html[data-theme="modern"], html[data-theme="pythinker"]) .attach-btn:hover {
436440
background: var(--soft);
@@ -855,8 +859,8 @@ html[data-color-scheme="dark"][data-theme="pythinker"] {
855859
/* Composer: the one card that keeps a shadow (effect.shadow.inputDefault),
856860
with the chat-input radius documented in web-best-practices §8. */
857861
html[data-theme="pythinker"] .composer-card {
858-
border-radius: 20px;
859-
box-shadow: 0 5px 16px -4px rgba(0, 0, 0, 0.07);
862+
border-radius: var(--r-xl);
863+
box-shadow: 0 5px 16px -4px color-mix(in srgb, var(--ink) 7%, transparent);
860864
}
861865

862866
/* Cards stay flat: no hover lift, no shadow (Quiet Utility / card.md). */

apps/pythinker-web/test/composer.test.ts

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import { readFileSync } from 'node:fs';
2-
import { fileURLToPath } from 'node:url';
2+
import { join } from 'node:path';
33
import { flushPromises, mount } from '@vue/test-utils';
44
import { createI18n } from 'vue-i18n';
55
import { afterEach, describe, expect, it, vi } from 'vitest';
66
import Composer from '../src/components/Composer.vue';
77
import type { AppModel } from '../src/api/types';
88

9-
const sourcePath = (path: string) => fileURLToPath(new URL(path, import.meta.url));
9+
const sourcePath = (path: string) => join(import.meta.dirname, path);
1010
const composerSource = readFileSync(sourcePath('../src/components/Composer.vue'), 'utf8');
11+
const conversationPaneSource = readFileSync(sourcePath('../src/components/ConversationPane.vue'), 'utf8');
12+
const styleSource = readFileSync(sourcePath('../src/style.css'), 'utf8');
1113

1214
function mountComposer(props: Record<string, unknown> = {}) {
1315
const i18n = createI18n({
@@ -82,6 +84,80 @@ afterEach(() => {
8284
});
8385

8486
describe('Composer styling', () => {
87+
it('uses the shared xl radius token for the composer card', () => {
88+
const composerCardRule = composerSource.match(/(?:^|\n)\.composer-card\s*\{([^}]*)\}/)?.[1] ?? '';
89+
expect(composerCardRule.trim()).not.toBe('');
90+
expect(composerCardRule).toMatch(/border-radius:\s*var\(--r-xl\);/);
91+
92+
const modernCardRule = styleSource.match(/:is\(html\[data-theme="modern"\], html\[data-theme="pythinker"\]\) \.composer-card\s*\{([^}]*)\}/)?.[1] ?? '';
93+
expect(modernCardRule).toMatch(/border-radius:\s*var\(--r-xl\);/);
94+
95+
const pythinkerCardRule = styleSource.match(/html\[data-theme="pythinker"\] \.composer-card\s*\{([^}]*)\}/)?.[1] ?? '';
96+
expect(pythinkerCardRule).toMatch(/border-radius:\s*var\(--r-xl\);/);
97+
});
98+
99+
it('defines the xl radius token in the shared style tokens', () => {
100+
expect(styleSource).toMatch(/--r-xl:\s*24px;/);
101+
});
102+
103+
it('uses a translucent card surface with a backdrop blur', () => {
104+
const composerCardRule = composerSource.match(/(?:^|\n)\.composer-card\s*\{([^}]*)\}/)?.[1] ?? '';
105+
expect(composerCardRule).toMatch(/background:\s*color-mix\([^;]+transparent\);/);
106+
expect(composerCardRule).toMatch(/backdrop-filter:\s*blur\([^;]+\);/);
107+
});
108+
109+
it('strengthens the card border on hover and focus within', () => {
110+
const interactionRule = composerSource.match(/(?:^|\n)\.composer-card:hover,\s*\.composer-card:focus-within\s*\{([^}]*)\}/)?.[1] ?? '';
111+
expect(interactionRule.trim()).not.toBe('');
112+
expect(interactionRule).toMatch(/border-color:\s*[^;]+;/);
113+
});
114+
115+
it('caps the input at 384px and scrolls its content', () => {
116+
const inputRule = composerSource.match(/(?:^|\n)\.ph\s*\{([^}]*)\}/)?.[1] ?? '';
117+
expect(inputRule).toMatch(/max-height:\s*384px;/);
118+
expect(inputRule).toMatch(/overflow-y:\s*auto;/);
119+
expect(inputRule).toMatch(/field-sizing:\s*content;/);
120+
});
121+
122+
it('uses circular attachment controls and a divider after the plus button', () => {
123+
const attachRule = composerSource.match(/(?:^|\n)\.attach-btn\s*\{([^}]*)\}/)?.[1] ?? '';
124+
expect(attachRule).toMatch(/width:\s*30px;/);
125+
expect(attachRule).toMatch(/height:\s*30px;/);
126+
expect(attachRule).toMatch(/border-radius:\s*50%;/);
127+
128+
const themedAttachRule = styleSource.match(/:is\(html\[data-theme="modern"\], html\[data-theme="pythinker"\]\) \.attach-btn\s*\{([^}]*)\}/)?.[1] ?? '';
129+
expect(themedAttachRule).toMatch(/width:\s*30px;/);
130+
expect(themedAttachRule).toMatch(/height:\s*30px;/);
131+
expect(themedAttachRule).toMatch(/border-radius:\s*50%;/);
132+
133+
const dividerRule = composerSource.match(/(?:^|\n)\.toolbar-divider\s*\{([^}]*)\}/)?.[1] ?? '';
134+
expect(dividerRule).toMatch(/width:\s*1px;/);
135+
expect(dividerRule).toMatch(/height:\s*16px;/);
136+
expect(composerSource).toMatch(/class="attach-btn"[\s\S]*class="toolbar-divider"/);
137+
});
138+
139+
it('pads the send button around a 20px glyph and keeps the accent fill', () => {
140+
const sendRule = composerSource.match(/(?:^|\n)\.send\s*\{([^}]*)\}/)?.[1] ?? '';
141+
expect(sendRule).toMatch(/padding:\s*5px;/);
142+
// The send button carries the theme accent, not a monochrome fill: --blue is
143+
// the Pythinker brand colour and each theme redefines it.
144+
expect(sendRule).toMatch(/background:\s*var\(--blue\);/);
145+
expect(sendRule).toMatch(/color:\s*var\(--bg\);/);
146+
147+
const sendIconRule = composerSource.match(/(?:^|\n)\.send svg\s*\{([^}]*)\}/)?.[1] ?? '';
148+
expect(sendIconRule).toMatch(/width:\s*20px;/);
149+
expect(sendIconRule).toMatch(/height:\s*20px;/);
150+
151+
const themedSendRule = styleSource.match(/:is\(html\[data-theme="modern"\], html\[data-theme="pythinker"\]\) \.send\s*\{([^}]*)\}/)?.[1] ?? '';
152+
expect(themedSendRule).toMatch(/padding:\s*5px;/);
153+
expect(themedSendRule).not.toMatch(/background:\s*var\(--ink\);/);
154+
});
155+
156+
it('widens the shared reading column to 928px', () => {
157+
const conRule = conversationPaneSource.match(/(?:^|\n)\.con\s*\{([^}]*)\}/)?.[1] ?? '';
158+
expect(conRule).toMatch(/--read-max:\s*928px;/);
159+
});
160+
85161
it('places the composer border flush against the message list', () => {
86162
const composerRule = composerSource.match(/(?:^|\n)\.composer\s*\{([^}]*)\}/)?.[1] ?? '';
87163
expect(composerRule.trim()).not.toBe('');

0 commit comments

Comments
 (0)