@frontera-sdk/chat-ui 1.50.40
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.
- package/LICENSE +202 -0
- package/README.md +348 -0
- package/dist/af-DK_CjRn6-OPBHDTNZ.js +197 -0
- package/dist/agent-picker.d.ts +38 -0
- package/dist/agent-picker.js +8 -0
- package/dist/as-BPtSu6VN-O4ZZEAJG.js +11 -0
- package/dist/be-CrQ0smca-5HIE5K27.js +11 -0
- package/dist/bg-CrQqGwni-3IRVPZQ7.js +11 -0
- package/dist/bn-NCLZcWHi-XOZJFNIB.js +11 -0
- package/dist/ca-SUf6Y2Om-V6F2EBCA.js +38 -0
- package/dist/capture-NO5ZIGP6.js +1652 -0
- package/dist/chat.css +3555 -0
- package/dist/chat.d.ts +161 -0
- package/dist/chat.js +60498 -0
- package/dist/chunk-2KEBRV3W.js +191 -0
- package/dist/chunk-6BPHEBCF.js +639 -0
- package/dist/chunk-6HYPVNVA.js +25 -0
- package/dist/chunk-7ADM3CKD.js +10 -0
- package/dist/chunk-AFVU5VT6.js +5601 -0
- package/dist/chunk-BEJ3HQG4.js +93 -0
- package/dist/chunk-CCVPZVFK.js +28 -0
- package/dist/chunk-DGDHKMDB.js +10 -0
- package/dist/chunk-FXKXDR5K.js +2166 -0
- package/dist/chunk-GTQH7MPF.js +129 -0
- package/dist/chunk-I7SXWUAN.js +1258 -0
- package/dist/chunk-IQRPGFLI.js +234 -0
- package/dist/chunk-IXDM5R55.js +23 -0
- package/dist/chunk-JD6ETTXC.js +178 -0
- package/dist/chunk-OP4MLJDA.js +215 -0
- package/dist/chunk-PQZ2CS26.js +2315 -0
- package/dist/chunk-PR4QN5HX.js +38 -0
- package/dist/chunk-PUIVKDV5.js +271 -0
- package/dist/chunk-QSSFBZAS.js +6914 -0
- package/dist/chunk-RAKKA3WU.js +5896 -0
- package/dist/chunk-RB2Y4NYC.js +681 -0
- package/dist/chunk-VDZH5Z3K.js +72 -0
- package/dist/chunk-WMN6BPPS.js +49 -0
- package/dist/chunk-WO6DJLFE.js +10857 -0
- package/dist/chunk-X5E2DJVO.js +47634 -0
- package/dist/chunk-XJFTVKLW.js +2687 -0
- package/dist/chunk-ZS6PXIZP.js +187 -0
- package/dist/client.d.ts +89 -0
- package/dist/client.js +20 -0
- package/dist/composer.d.ts +23 -0
- package/dist/composer.js +89 -0
- package/dist/cop-C3sk0IUo-CIZA2H43.js +11 -0
- package/dist/credential.d.ts +75 -0
- package/dist/credential.js +3 -0
- package/dist/cs-CmAteokq-HAKEQPB4.js +17 -0
- package/dist/cu-CBUlrW6Y-QTZ6ZLA4.js +32 -0
- package/dist/cy-BBPa6z2x-MX6DGDNW.js +11 -0
- package/dist/da-CCDXfXtG-5YEOHMG3.js +11 -0
- package/dist/de-1901-CWoAOigE-63QFSF2A.js +11 -0
- package/dist/de-1996-BL6awyxG-R2PHKMAB.js +11 -0
- package/dist/de-ch-1901-hVThVHVb-JSDB53OZ.js +11 -0
- package/dist/el-monoton-D3aKqTz2-2V4OOFBB.js +11 -0
- package/dist/el-polyton-ogVWcTzh-3U7NGQMT.js +11 -0
- package/dist/en-gb-B7Al27b_-UKCBQFSU.js +21 -0
- package/dist/es-DcaaHPU_-NOBXSOZK.js +11 -0
- package/dist/esm-MHF25KDL.js +3 -0
- package/dist/et-B_spaleu-5UAYIW62.js +11 -0
- package/dist/eu-Ctv_kq9Z-V5DREOPN.js +11 -0
- package/dist/fi-RW5VB8Yp-YSXBK7LW.js +11 -0
- package/dist/fr-BC2onEz9-7KH5T76G.js +11 -0
- package/dist/frame.d.ts +59 -0
- package/dist/frame.js +111 -0
- package/dist/fur-AC12wySk-4PD2Y72H.js +11 -0
- package/dist/ga-Cn6LkudM-RRAAJT3N.js +58 -0
- package/dist/gl-DZGc-K_G-KYSXXTCY.js +11 -0
- package/dist/grc-DOlGgO8T-2HXBZVJ5.js +11 -0
- package/dist/gu-Bjl0l-cf-4CFSVZHZ.js +11 -0
- package/dist/header.d.ts +78 -0
- package/dist/header.js +18 -0
- package/dist/hi-DsAaTPCC-KMCFBWK7.js +11 -0
- package/dist/hr-DT1SBBes-X2TIX6EI.js +11 -0
- package/dist/hsb-DC_ITc1e-PXLERA7R.js +46 -0
- package/dist/hu-DVk7Y_ka-PJPVU54B.js +11 -0
- package/dist/hy-e995ysyO-MZBRFX52.js +11 -0
- package/dist/ia-Cr4TJH0c-D3O2FHGP.js +22 -0
- package/dist/id-DifDyWg9-MZ2BGHWI.js +58 -0
- package/dist/is-BT8UVRze-CZZCKEFI.js +11 -0
- package/dist/it-Bt_Meacx-T4Y5JYV6.js +11 -0
- package/dist/ka-jyoLzyDE-ES6EXZLW.js +11 -0
- package/dist/kmr-Cfwhwuff-TSW2R2KS.js +11 -0
- package/dist/kn-C6Lov-DP-B4BL47CE.js +11 -0
- package/dist/la-DuvHjUWw-6K7TFU6X.js +11 -0
- package/dist/la-x-classic-yTyAOQEt-YWTKMM2L.js +11 -0
- package/dist/la-x-liturgic-CXeQb4FY-B772L5E7.js +11 -0
- package/dist/lib-3ZKX25RJ.js +4511 -0
- package/dist/lt-BBe8DlDF-4YINVOZR.js +11 -0
- package/dist/lv-Q2BBHnzp-TMVQ3MX2.js +11 -0
- package/dist/message.d.ts +64 -0
- package/dist/message.js +18 -0
- package/dist/ml-2xLVYkVY-NGZLAPIN.js +11 -0
- package/dist/mn-cyrl-lHM4n0oC-DGBYYUT3.js +11 -0
- package/dist/mn-cyrl-x-lmc-3aetruI8-QHJABMOO.js +11 -0
- package/dist/mr-CjWJPVmH-X5UHV65O.js +11 -0
- package/dist/mul-ethi-D_CMSMEq-H7UMQSOQ.js +11 -0
- package/dist/nb-DI8u59ch-J4CAVI4Y.js +122 -0
- package/dist/new-session-Dw7JOgzu.d.ts +141 -0
- package/dist/new-session.d.ts +4 -0
- package/dist/new-session.js +10 -0
- package/dist/nl-BHQJ6Gvn-7BH55RA7.js +52 -0
- package/dist/nn-ntZvarUK-KKNCHHJ4.js +122 -0
- package/dist/no-B8ItS0F3-3ZXIRJ6U.js +120 -0
- package/dist/oauth-callback.d.ts +35 -0
- package/dist/oauth-callback.js +18 -0
- package/dist/oc-P_i3-xLA-XZM6E5YZ.js +11 -0
- package/dist/or-50rxpoq4-5VELAFMJ.js +11 -0
- package/dist/pa-C54UG8eN-WEJPMZ6Z.js +11 -0
- package/dist/parts.d.ts +49 -0
- package/dist/parts.js +106 -0
- package/dist/pdf-cover-thumbnail-inner-4N2Q5AAG.js +36 -0
- package/dist/pdf-deck-view-inner-FZVLAEQR.js +575 -0
- package/dist/pdf-document-view-inner-6DF54JWK.js +585 -0
- package/dist/pdf-viewer-inner-GPES3KG2.js +256 -0
- package/dist/pending-cards.d.ts +26 -0
- package/dist/pending-cards.js +9 -0
- package/dist/pi-B9cgjyG1-GD5ASDDU.js +11 -0
- package/dist/pl-9fg-ESVt-UMX7MCJM.js +32 -0
- package/dist/platform.css +4013 -0
- package/dist/pms-BRGfm5gS-SSE3AUW5.js +11 -0
- package/dist/pt-Cum2_NUC-W35JYVR4.js +11 -0
- package/dist/rm-C0MIK9N4-IWL4YTNT.js +11 -0
- package/dist/ro-0fmdH0HA-46WUTOYH.js +11 -0
- package/dist/ru-v4l-ayaq-BUGK6XCN.js +196 -0
- package/dist/sa-CKY9c_Vq-4KIJOI5Y.js +11 -0
- package/dist/sh-cyrl-BpCRFOmq-DSLQ4UY2.js +128 -0
- package/dist/sh-latn-sRy4Tuaj-UNQOR63V.js +128 -0
- package/dist/sk-_iOeDNrQ-NCTQKH25.js +17 -0
- package/dist/sl-DpiklF-D-6GG5TDVN.js +11 -0
- package/dist/sr-cyrl-Dpzrbd0W-4Q5226YJ.js +142 -0
- package/dist/sv-CFm5y56M-VTWNPZ7I.js +11 -0
- package/dist/ta-BcghgTbD-NRT35DW6.js +11 -0
- package/dist/te-B5Spl2Jk-5VE6CLN4.js +11 -0
- package/dist/th-DWX5iPSo-N5GMOQOP.js +11 -0
- package/dist/theme.d.ts +49 -0
- package/dist/theme.js +3 -0
- package/dist/tk-BMbKB10q-HGWWY5L3.js +11 -0
- package/dist/tr-CIHIuzqc-6ISBNYLV.js +11 -0
- package/dist/types-BDqLaBfH.d.ts +60 -0
- package/dist/uk-Dir3oxLs-YQ3QZRI4.js +11 -0
- package/dist/univer-workbook-BSSUE5TG.css +3555 -0
- package/dist/univer-workbook-N5IIN7CH.js +290849 -0
- package/dist/vega-chart-block-NQADYNWQ.js +2857 -0
- package/dist/zh-latn-pinyin-BdYb-Ey_-RB542AHF.js +11 -0
- package/package.json +137 -0
- package/src/styles.css +996 -0
package/src/styles.css
ADDED
|
@@ -0,0 +1,996 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frontera chat UI.
|
|
3
|
+
*
|
|
4
|
+
* Every colour and metric is a custom property, so a host application themes
|
|
5
|
+
* the surface without overriding rules. Class names are prefixed `fr-` because
|
|
6
|
+
* this stylesheet lands in someone else's page and must not collide with it.
|
|
7
|
+
*
|
|
8
|
+
* Import once:
|
|
9
|
+
* import '@frontera-sdk/chat-ui/styles.css'
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
The outer surface: conversation on the left, artifact pane on the right.
|
|
14
|
+
|
|
15
|
+
Themed here rather than on `.fr-chat` because the pane is a sibling of the
|
|
16
|
+
conversation, not a child of it — tokens set one level down would not reach
|
|
17
|
+
it, and the pane would render in the host page's palette.
|
|
18
|
+
*/
|
|
19
|
+
.fr-shell {
|
|
20
|
+
position: relative;
|
|
21
|
+
display: flex;
|
|
22
|
+
min-height: 0;
|
|
23
|
+
height: 100%;
|
|
24
|
+
isolation: isolate;
|
|
25
|
+
/*
|
|
26
|
+
Tokens live HERE, not on `.fr-chat`. The pane is a SIBLING of the
|
|
27
|
+
conversation — tokens declared one level down never reach it, and every
|
|
28
|
+
`var(--fr-*)` inside the pane resolved to nothing: a transparent (black on
|
|
29
|
+
a dark host) pane with an unresolved border. See `.fr-chat` for what each
|
|
30
|
+
alias means and why the fallbacks are load-bearing.
|
|
31
|
+
*/
|
|
32
|
+
--fr-bg: var(--surface-chat, hsl(0 0% 100%));
|
|
33
|
+
--fr-surface: var(--surface, hsl(0 0% 98%));
|
|
34
|
+
--fr-bubble: var(--surface-raised-active, hsl(0 0% 94%));
|
|
35
|
+
--fr-border: var(--border, hsl(240 100% 6% / 0.08));
|
|
36
|
+
--fr-text: hsl(var(--foreground, 47 13% 14%));
|
|
37
|
+
--fr-muted: var(--muted-foreground, hsl(240 4% 46%));
|
|
38
|
+
--fr-accent: var(--primary, hsl(221 83% 53%));
|
|
39
|
+
--fr-accent-text: var(--primary-foreground, hsl(0 0% 100%));
|
|
40
|
+
--fr-danger: var(--destructive, hsl(0 72% 45%));
|
|
41
|
+
--fr-code: var(--inline-code, hsl(9 62% 45%));
|
|
42
|
+
--fr-code-bg: var(--inline-code-bg, hsl(0 0% 95.5%));
|
|
43
|
+
--fr-radius: 1rem;
|
|
44
|
+
--fr-font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
45
|
+
/* The answer is 15px; the prompt one step under, at 14px — see .fr-bubble. */
|
|
46
|
+
--fr-font-size: 15px;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/*
|
|
50
|
+
BOTH panels ease `flex-grow`, which is the platform's push: one gives up
|
|
51
|
+
exactly the width the other takes (`PANE_ANIM_CLASS` in
|
|
52
|
+
chat-content-chrome.tsx). Suspended while the handle is being dragged — an
|
|
53
|
+
eased width would leave the panel lagging behind the cursor.
|
|
54
|
+
*/
|
|
55
|
+
.fr-panel-anim {
|
|
56
|
+
transition: flex-grow 150ms ease-out;
|
|
57
|
+
}
|
|
58
|
+
.fr-split:has([data-resize-handle-state="drag"]) .fr-panel-anim,
|
|
59
|
+
/* Expand/restore snaps — `.fr-snap` is set for exactly one frame while the
|
|
60
|
+
fullscreen layout commits, matching the platform's mode switch. */
|
|
61
|
+
.fr-split.fr-snap .fr-panel-anim {
|
|
62
|
+
transition: none;
|
|
63
|
+
}
|
|
64
|
+
@media (prefers-reduced-motion: reduce) {
|
|
65
|
+
.fr-panel-anim { transition: none; }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* The panel group fills the shell; the library sets the panels' widths. */
|
|
69
|
+
.fr-split {
|
|
70
|
+
flex: 1;
|
|
71
|
+
min-width: 0;
|
|
72
|
+
height: 100%;
|
|
73
|
+
/* The platform's group carries `overflow-hidden`; without it anything the
|
|
74
|
+
panels fail to contain widens the page itself. */
|
|
75
|
+
overflow: hidden;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* The pane holds a fixed column; the conversation takes whatever is left and
|
|
79
|
+
is allowed to shrink — `min-width: 0`, without which a long unbroken line
|
|
80
|
+
in a reply pushes the pane off the edge instead of wrapping. */
|
|
81
|
+
/* Width is the panel library's; this only paints the pane and its seam. The
|
|
82
|
+
border lives on the pane rather than the handle so a collapsed pane leaves
|
|
83
|
+
no line behind. */
|
|
84
|
+
.fr-side-pane {
|
|
85
|
+
min-height: 0;
|
|
86
|
+
/* min-width matters since the pane's content stays MOUNTED while shut: a
|
|
87
|
+
flex item's default `min-width: auto` refuses to shrink below the
|
|
88
|
+
content's min-content width, so the collapsed 0% panel held the viewer's
|
|
89
|
+
width and pushed the whole page into a horizontal scroll. */
|
|
90
|
+
min-width: 0;
|
|
91
|
+
overflow: hidden;
|
|
92
|
+
background: var(--fr-bg);
|
|
93
|
+
border-left: 1px solid transparent;
|
|
94
|
+
transition: border-color 150ms ease-out;
|
|
95
|
+
}
|
|
96
|
+
.fr-side-pane:not([data-panel-size="0.0"]) {
|
|
97
|
+
border-left-color: var(--fr-border);
|
|
98
|
+
}
|
|
99
|
+
/* Fullscreen: the conversation is collapsed, so the pane's leading edge is the
|
|
100
|
+
surface's own — a seam there reads as a rendering artifact. `~` because the
|
|
101
|
+
resize handle sits between the two panels. */
|
|
102
|
+
.fr-split > [data-panel-size="0.0"] ~ .fr-side-pane {
|
|
103
|
+
border-left-color: transparent;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/*
|
|
107
|
+
On a phone the pane is a SHEET, not a column.
|
|
108
|
+
|
|
109
|
+
The platform drops its split layout entirely under 768px and surfaces the
|
|
110
|
+
same content as a full-screen overlay — the canvas, the computer panel, an
|
|
111
|
+
artifact, the sources. This package kept the split at every width, so a
|
|
112
|
+
360px screen was asked to hold a readable conversation AND a document side
|
|
113
|
+
by side: the thread squeezed to its `minSize`, the pane to a strip, and the
|
|
114
|
+
summary and files the platform shows inline were effectively unreachable.
|
|
115
|
+
|
|
116
|
+
Done here rather than by branching the tree, deliberately. The panel group
|
|
117
|
+
owns the open/closed state, the mount/unmount of the viewer and the restore
|
|
118
|
+
of a conversation's tabs; a second layout would have to reproduce all three
|
|
119
|
+
and then drift from it. Taking the pane out of flow changes where it PAINTS
|
|
120
|
+
and leaves every one of those behaviours where it already works.
|
|
121
|
+
|
|
122
|
+
`!important` because `react-resizable-panels` writes `flex` inline on each
|
|
123
|
+
panel, and inline styles outrank a stylesheet. The overrides are scoped to
|
|
124
|
+
this breakpoint and to this package's own classes.
|
|
125
|
+
*/
|
|
126
|
+
@media (max-width: 767px) {
|
|
127
|
+
.fr-shell .fr-side-pane:not([data-panel-size="0.0"]) {
|
|
128
|
+
position: fixed;
|
|
129
|
+
inset: 0;
|
|
130
|
+
z-index: 60;
|
|
131
|
+
/* Above the composer's own stacking, and clear of the home indicator. */
|
|
132
|
+
padding-bottom: env(safe-area-inset-bottom);
|
|
133
|
+
flex: none !important;
|
|
134
|
+
width: 100% !important;
|
|
135
|
+
border-left: none;
|
|
136
|
+
}
|
|
137
|
+
/*
|
|
138
|
+
A collapsed pane is `0%` wide in the flow and invisible. Fixed, it would
|
|
139
|
+
be a full-screen transparent layer over the conversation swallowing every
|
|
140
|
+
tap — so the closed state has to leave the page rather than shrink.
|
|
141
|
+
*/
|
|
142
|
+
.fr-shell .fr-side-pane[data-panel-size="0.0"] {
|
|
143
|
+
display: none;
|
|
144
|
+
}
|
|
145
|
+
/* The conversation takes the whole width once the pane no longer occupies
|
|
146
|
+
any of it — otherwise it keeps the percentage the group assigned and
|
|
147
|
+
leaves a blank column beside the sheet. */
|
|
148
|
+
.fr-shell .fr-split > [data-panel]:first-child {
|
|
149
|
+
flex: 1 1 0% !important;
|
|
150
|
+
}
|
|
151
|
+
/* Nothing to drag between a thread and an overlay. */
|
|
152
|
+
.fr-shell .fr-split > [data-panel-resize-handle-id] {
|
|
153
|
+
display: none;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* The pane's content chain: `RightPanel` is `h-full` + its own ScrollArea, so
|
|
158
|
+
this must actually BE the pane's height or the panel renders collapsed —
|
|
159
|
+
which looked like a bordered box floating over a void. */
|
|
160
|
+
.fr-side-pane-surface {
|
|
161
|
+
height: 100%;
|
|
162
|
+
min-height: 0;
|
|
163
|
+
background: var(--fr-bg);
|
|
164
|
+
/* The pane is OUTSIDE `.fr-chat`, so without these it renders in the host
|
|
165
|
+
page's font and colour — which is what made the Outputs list look like a
|
|
166
|
+
different product from the conversation beside it. */
|
|
167
|
+
font-family: var(--fr-font);
|
|
168
|
+
color: var(--fr-text);
|
|
169
|
+
line-height: 1.5;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* The positioning context for the floating composer. */
|
|
173
|
+
.fr-chat {
|
|
174
|
+
position: relative;
|
|
175
|
+
min-width: 0;
|
|
176
|
+
/*
|
|
177
|
+
DERIVED from the platform's tokens, not a second copy of them.
|
|
178
|
+
|
|
179
|
+
These used to be literal colours, with a `prefers-color-scheme` block of
|
|
180
|
+
their own — a whole parallel palette beside the one `platform.css` defines.
|
|
181
|
+
Two palettes meant two dark switches, and they disagreed: the shell turned
|
|
182
|
+
dark with the OS while the copied components waited for a `dark` class, so
|
|
183
|
+
white cards sat on a black background and the surface looked half-themed.
|
|
184
|
+
|
|
185
|
+
One source of truth now. Each `--fr-*` is an alias, so a host overriding
|
|
186
|
+
either layer moves both, and dark happens once.
|
|
187
|
+
|
|
188
|
+
Aliased to the RAW tokens (`--background`), not Tailwind's `--color-*`.
|
|
189
|
+
That is not a style preference — it is the only thing that works. Tailwind
|
|
190
|
+
declares `--color-background: hsl(var(--background))` on `:root`, where it
|
|
191
|
+
is substituted once; descendants inherit the already-computed colour. So a
|
|
192
|
+
`dark` class further down the tree changes `--background` and the `--color-*`
|
|
193
|
+
alias never notices. The raw tokens ARE redeclared inside `.dark`, which is
|
|
194
|
+
what makes them follow.
|
|
195
|
+
|
|
196
|
+
Two shapes, deliberately preserved: some tokens hold bare channels for
|
|
197
|
+
`hsl()` to wrap, others a finished colour. Wrapping a finished colour, or
|
|
198
|
+
failing to wrap bare channels, yields an invalid value and the property
|
|
199
|
+
silently drops.
|
|
200
|
+
|
|
201
|
+
The fallbacks are load-bearing: `styles.css` can be imported WITHOUT
|
|
202
|
+
`platform.css` by a consumer composing their own layout from `./message`
|
|
203
|
+
and `./composer`, and an unresolved custom property paints nothing at all.
|
|
204
|
+
*/
|
|
205
|
+
/*
|
|
206
|
+
`--surface-chat`, not `--background`.
|
|
207
|
+
|
|
208
|
+
The platform paints its whole chat container `bg-surface-chat`
|
|
209
|
+
(`chat-view.tsx`), and several components lean on that: the reasoning row's
|
|
210
|
+
icon sits in a `bg-surface-chat` pill that is invisible BECAUSE it matches
|
|
211
|
+
what is behind it. Using `--background` here left those pills a shade off
|
|
212
|
+
the thread, so a spacer read as a badge around the icon.
|
|
213
|
+
*/
|
|
214
|
+
/* Declared on `.fr-shell` too — the pane is a sibling of `.fr-chat` and
|
|
215
|
+
needs the same tokens. Kept here as well so a consumer composing their own
|
|
216
|
+
layout from `./message` inside a bare `.fr-chat` still gets them. */
|
|
217
|
+
--fr-bg: var(--surface-chat, hsl(0 0% 100%));
|
|
218
|
+
--fr-surface: var(--surface, hsl(0 0% 98%));
|
|
219
|
+
--fr-bubble: var(--surface-raised-active, hsl(0 0% 94%));
|
|
220
|
+
--fr-border: var(--border, hsl(240 100% 6% / 0.08));
|
|
221
|
+
--fr-text: hsl(var(--foreground, 47 13% 14%));
|
|
222
|
+
--fr-muted: var(--muted-foreground, hsl(240 4% 46%));
|
|
223
|
+
--fr-accent: var(--primary, hsl(221 83% 53%));
|
|
224
|
+
--fr-accent-text: var(--primary-foreground, hsl(0 0% 100%));
|
|
225
|
+
--fr-danger: var(--destructive, hsl(0 72% 45%));
|
|
226
|
+
--fr-code: var(--inline-code, hsl(9 62% 45%));
|
|
227
|
+
--fr-code-bg: var(--inline-code-bg, hsl(0 0% 95.5%));
|
|
228
|
+
--fr-radius: 1rem;
|
|
229
|
+
--fr-font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
230
|
+
--fr-font-size: 15px;
|
|
231
|
+
|
|
232
|
+
display: flex;
|
|
233
|
+
flex-direction: column;
|
|
234
|
+
min-height: 0;
|
|
235
|
+
height: 100%;
|
|
236
|
+
/*
|
|
237
|
+
The platform's `@container/chatpane` context. The vendored components
|
|
238
|
+
ship rules against this container — the summary card's gutter, the
|
|
239
|
+
composer's narrow-width folds — and every one of them was inert here
|
|
240
|
+
because nothing declared the container. Declared on `.fr-chat` because
|
|
241
|
+
that IS the pane the platform measures: it narrows when the side pane
|
|
242
|
+
opens, which is exactly what those rules respond to.
|
|
243
|
+
*/
|
|
244
|
+
container: chatpane / inline-size;
|
|
245
|
+
/* The summary card is `absolute inset-y-0 right-0`, so it needs a
|
|
246
|
+
positioning context. Without one it escapes to the nearest positioned
|
|
247
|
+
ancestor — the host's page — and lands somewhere nobody chose. */
|
|
248
|
+
position: relative;
|
|
249
|
+
/* The platform's inner column carries `overflow-x-hidden`: a wide code
|
|
250
|
+
block or table scrolls INSIDE its message, never the conversation. */
|
|
251
|
+
overflow-x: hidden;
|
|
252
|
+
background: var(--fr-bg);
|
|
253
|
+
color: var(--fr-text);
|
|
254
|
+
font-family: var(--fr-font);
|
|
255
|
+
font-size: var(--fr-font-size);
|
|
256
|
+
line-height: 1.6;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/*
|
|
260
|
+
Dark mode lives in `platform.css`, keyed off the `dark` class that
|
|
261
|
+
`<FronteraChat>` puts on this element — the same mechanism the platform uses,
|
|
262
|
+
so every `dark:` utility inside the copied components switches with it.
|
|
263
|
+
|
|
264
|
+
There is deliberately no `prefers-color-scheme` block here any more. It was a
|
|
265
|
+
SECOND switch on a second palette, and the two could not agree; the component
|
|
266
|
+
reads the OS itself and sets the class, which keeps one decision in one place
|
|
267
|
+
and lets a host override it with the `dark` prop.
|
|
268
|
+
*/
|
|
269
|
+
|
|
270
|
+
.fr-chat *,
|
|
271
|
+
.fr-chat *::before,
|
|
272
|
+
.fr-chat *::after {
|
|
273
|
+
box-sizing: border-box;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/*
|
|
277
|
+
The composer on an empty conversation.
|
|
278
|
+
|
|
279
|
+
Sits directly under the greeting instead of at the bottom edge. `max-width`
|
|
280
|
+
matches the thread column so the input is the same width it will be once the
|
|
281
|
+
conversation starts — otherwise sending the first message visibly resizes the
|
|
282
|
+
control someone just typed into.
|
|
283
|
+
|
|
284
|
+
The dock's own positioning (absolute, bottom, scrollbar inset) belongs to the
|
|
285
|
+
dock; here the input is ordinary flow content.
|
|
286
|
+
*/
|
|
287
|
+
.fr-empty-composer {
|
|
288
|
+
width: 100%;
|
|
289
|
+
max-width: 49rem;
|
|
290
|
+
margin: 20px auto 0;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/* ── thread ─────────────────────────────────────────────────────────── */
|
|
294
|
+
|
|
295
|
+
/*
|
|
296
|
+
The platform's TWO-LAYER thread, reproduced exactly:
|
|
297
|
+
|
|
298
|
+
`.fr-thread` is the VIEWPORT — full width, owns the scroll, carries the
|
|
299
|
+
summary-card gutter (`pr-[320px]` lives on the platform's viewport too) and
|
|
300
|
+
nothing else.
|
|
301
|
+
|
|
302
|
+
`.fr-thread-column` is the CONTENT COLUMN — `max-w-view mx-auto`, the
|
|
303
|
+
platform's `gap-6 px-3 pt-6 pb-24 md:gap-2 md:px-4 md:pt-16` plus the
|
|
304
|
+
composer-tracked bottom padding. Putting the paddings on the viewport
|
|
305
|
+
instead produced an asymmetric box (`padding: 64px 320px 166px 0`) that
|
|
306
|
+
matched the platform's numbers but not its geometry.
|
|
307
|
+
*/
|
|
308
|
+
.fr-thread {
|
|
309
|
+
flex: 1;
|
|
310
|
+
min-height: 0;
|
|
311
|
+
overflow-y: auto;
|
|
312
|
+
transition: padding-right 150ms ease-out;
|
|
313
|
+
/* The platform viewport's `overscroll-contain`: reaching the end of the
|
|
314
|
+
thread must not start scrolling the host page. */
|
|
315
|
+
overscroll-behavior: contain;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.fr-thread-column {
|
|
319
|
+
display: flex;
|
|
320
|
+
flex-direction: column;
|
|
321
|
+
width: 100%;
|
|
322
|
+
/* 49rem — `max-w-view` / `--container-3xl`, centred like `mx-auto`. */
|
|
323
|
+
max-width: 49rem;
|
|
324
|
+
margin-inline: auto;
|
|
325
|
+
/* So the empty state's `margin: auto` can centre vertically. */
|
|
326
|
+
min-height: 100%;
|
|
327
|
+
/* The platform column's `gap-6 md:gap-2`. */
|
|
328
|
+
gap: 24px;
|
|
329
|
+
/*
|
|
330
|
+
Top clears the floating header (`md:pt-16`). Bottom is the composer's
|
|
331
|
+
measured height plus THREAD_TAIL_GAP_PX (64px) — the fallback covers the
|
|
332
|
+
first frame, before the observer has measured.
|
|
333
|
+
*/
|
|
334
|
+
padding: 64px 12px calc(var(--fr-composer-h, 96px) + 64px);
|
|
335
|
+
}
|
|
336
|
+
@media (min-width: 768px) {
|
|
337
|
+
.fr-thread-column {
|
|
338
|
+
gap: 8px;
|
|
339
|
+
padding-inline: 16px;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/* Burst spacing: consecutive bubbles from the person pull together, the
|
|
344
|
+
platform's `-mt-5 md:-mt-1` on the message wrapper — net ~4px against the
|
|
345
|
+
thread gap at either width. */
|
|
346
|
+
.fr-turn-user + .fr-turn-user {
|
|
347
|
+
margin-top: -20px;
|
|
348
|
+
}
|
|
349
|
+
@media (min-width: 768px) {
|
|
350
|
+
.fr-turn-user + .fr-turn-user {
|
|
351
|
+
margin-top: -4px;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.fr-empty {
|
|
356
|
+
margin: auto;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/*
|
|
360
|
+
Room for the floating summary card — the platform's
|
|
361
|
+
`md:@max-[1520px]/chatpane:pr-[320px]` on both the thread and the composer.
|
|
362
|
+
On a wide pane the card overlays empty gutter and costs nothing; under
|
|
363
|
+
1520px the message column and the composer both step aside, or the card
|
|
364
|
+
sits on top of the conversation it is summarising.
|
|
365
|
+
*/
|
|
366
|
+
@container chatpane (max-width: 1520px) and (min-width: 768px) {
|
|
367
|
+
.fr-thread[data-reserve-gutter],
|
|
368
|
+
.fr-composer-dock[data-reserve-gutter] {
|
|
369
|
+
padding-right: 320px;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/* One turn — a plain block inside the column, like the platform's
|
|
374
|
+
`.thread-message` wrapper. Width, inset and centring all belong to
|
|
375
|
+
`.fr-thread-column` now; a turn only stacks its own parts. */
|
|
376
|
+
.fr-turn {
|
|
377
|
+
display: flex;
|
|
378
|
+
flex-direction: column;
|
|
379
|
+
gap: 6px;
|
|
380
|
+
width: 100%;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/* The user's turn is a bubble; the agent's is not. An agent reply can carry
|
|
384
|
+
headings, code and tables, and boxing that reads as cramped.
|
|
385
|
+
The turn keeps the column's full width and aligns its BUBBLE right —
|
|
386
|
+
`align-self: flex-end` would instead throw the whole column to the edge of
|
|
387
|
+
the screen, away from the reply. */
|
|
388
|
+
.fr-turn-user {
|
|
389
|
+
align-items: flex-end;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.fr-bubble {
|
|
393
|
+
background: var(--fr-bubble);
|
|
394
|
+
color: var(--fr-text);
|
|
395
|
+
/* 12px / 8px and a full 1rem radius — the platform's own bubble metrics. */
|
|
396
|
+
padding: 8px 12px;
|
|
397
|
+
border-radius: var(--fr-radius);
|
|
398
|
+
/* One step under the answer: a prompt is something you wrote and glance back
|
|
399
|
+
at, not the thing you came back to read. */
|
|
400
|
+
font-size: 0.933em;
|
|
401
|
+
max-width: 100%;
|
|
402
|
+
overflow-wrap: anywhere;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
.fr-author {
|
|
406
|
+
display: flex;
|
|
407
|
+
align-items: center;
|
|
408
|
+
gap: 6px;
|
|
409
|
+
font-size: 12px;
|
|
410
|
+
font-weight: 600;
|
|
411
|
+
color: var(--fr-muted);
|
|
412
|
+
letter-spacing: 0.01em;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/*
|
|
416
|
+
The composer, floating over the foot of the scroll area.
|
|
417
|
+
|
|
418
|
+
Absolute so the thread's scrollbar spans the pane's full height and reaches
|
|
419
|
+
the true bottom, the way the platform's does — a composer in the flow cuts
|
|
420
|
+
the scroll region short and leaves the thumb ending above it.
|
|
421
|
+
*/
|
|
422
|
+
/*
|
|
423
|
+
Content fading out under the composer.
|
|
424
|
+
|
|
425
|
+
A hard cut at the composer's top edge reads as content being clipped; the
|
|
426
|
+
gradient says it continues beneath. Matches the platform: 4rem tall, from the
|
|
427
|
+
chat surface colour to transparent.
|
|
428
|
+
*/
|
|
429
|
+
.fr-scroll-fade {
|
|
430
|
+
position: absolute;
|
|
431
|
+
left: 0;
|
|
432
|
+
/* ABOVE the composer, not behind it — the platform moves its fade with the
|
|
433
|
+
composer's measured height for exactly this reason. Anchored at 0 it sat
|
|
434
|
+
entirely under the opaque dock and did nothing, so content met the
|
|
435
|
+
composer at a hard edge. */
|
|
436
|
+
bottom: var(--fr-composer-h, 96px);
|
|
437
|
+
z-index: 10;
|
|
438
|
+
height: 4rem;
|
|
439
|
+
pointer-events: none;
|
|
440
|
+
background: linear-gradient(to top, var(--fr-bg), transparent);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/* Scroll-to-bottom, the platform's own affordance: a raised circle floating
|
|
444
|
+
just above the composer, centred on the column (nudged by half the
|
|
445
|
+
scrollbar). Shown only once the reader has scrolled away from the tail. */
|
|
446
|
+
.fr-scroll-down {
|
|
447
|
+
position: absolute;
|
|
448
|
+
bottom: calc(var(--fr-composer-h, 96px) + 16px);
|
|
449
|
+
transform: translateX(-50%);
|
|
450
|
+
z-index: 30;
|
|
451
|
+
display: inline-flex;
|
|
452
|
+
align-items: center;
|
|
453
|
+
justify-content: center;
|
|
454
|
+
width: 36px;
|
|
455
|
+
height: 36px;
|
|
456
|
+
border-radius: 999px;
|
|
457
|
+
border: 1px solid var(--fr-border);
|
|
458
|
+
background: var(--surface-raised, var(--fr-surface));
|
|
459
|
+
color: var(--fr-text);
|
|
460
|
+
cursor: pointer;
|
|
461
|
+
box-shadow: 0 2px 8px rgb(0 0 0 / 0.08);
|
|
462
|
+
}
|
|
463
|
+
.fr-scroll-down:hover {
|
|
464
|
+
background: var(--surface-raised-hover, var(--fr-bubble));
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
.fr-composer-dock {
|
|
468
|
+
position: absolute;
|
|
469
|
+
left: 0;
|
|
470
|
+
bottom: 0;
|
|
471
|
+
z-index: 20;
|
|
472
|
+
transition: padding-right 150ms ease-out;
|
|
473
|
+
/*
|
|
474
|
+
OPAQUE, and that is the point: the composer floats over the scroll area, so
|
|
475
|
+
without a background the conversation scrolls visibly through it.
|
|
476
|
+
*/
|
|
477
|
+
background: var(--fr-bg);
|
|
478
|
+
/*
|
|
479
|
+
Horizontal only. The composer carries its own bottom padding —
|
|
480
|
+
`pb-4 max-md:pb-[max(1rem,env(safe-area-inset-bottom))]` — which also
|
|
481
|
+
clears the home indicator on a phone. Adding any here double-pads it and
|
|
482
|
+
breaks the safe-area handling.
|
|
483
|
+
*/
|
|
484
|
+
padding: 0 20px;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/* ── header ─────────────────────────────────────────────────────────── */
|
|
488
|
+
|
|
489
|
+
/*
|
|
490
|
+
Sits above the thread, full width, and stays put while the conversation
|
|
491
|
+
scrolls under it — the title and the summary control are about the whole
|
|
492
|
+
conversation, not the part currently in view.
|
|
493
|
+
*/
|
|
494
|
+
/*
|
|
495
|
+
A transparent overlay floating above the conversation, exactly the
|
|
496
|
+
platform's (`session-view.tsx`): `absolute inset-x-0 top-0 z-20 h-10` with
|
|
497
|
+
`transition-colors duration-200`. It only turns solid — background, blur and
|
|
498
|
+
a bottom border — when the centred message column would slide under the
|
|
499
|
+
title or the actions (see the clash measurement in `header.tsx`); on a wide
|
|
500
|
+
view the column clears both sides and the header stays invisible.
|
|
501
|
+
*/
|
|
502
|
+
.fr-header {
|
|
503
|
+
position: absolute;
|
|
504
|
+
inset-inline: 0;
|
|
505
|
+
top: 0;
|
|
506
|
+
z-index: 20;
|
|
507
|
+
height: 40px;
|
|
508
|
+
display: flex;
|
|
509
|
+
align-items: center;
|
|
510
|
+
gap: 2px;
|
|
511
|
+
padding: 0 12px;
|
|
512
|
+
border-bottom: 1px solid transparent;
|
|
513
|
+
background: transparent;
|
|
514
|
+
transition: background-color 200ms, border-color 200ms;
|
|
515
|
+
}
|
|
516
|
+
.fr-header[data-solid] {
|
|
517
|
+
border-bottom-color: var(--border-secondary, var(--fr-border));
|
|
518
|
+
background: color-mix(in srgb, var(--fr-bg) 95%, transparent);
|
|
519
|
+
backdrop-filter: blur(4px);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/*
|
|
523
|
+
The phone's bar: solid, in the flow, and tall enough to tap.
|
|
524
|
+
|
|
525
|
+
The floating header works on a desktop because the thread has gutters for it
|
|
526
|
+
to float over. At 360px it sat directly on the first reply, and the pinned
|
|
527
|
+
pane toggle sat on the conversation. The platform's `MobileChatHeader` is
|
|
528
|
+
`sticky`, `h-13` and opaque for exactly that reason; this matches it.
|
|
529
|
+
*/
|
|
530
|
+
.fr-header-mobile {
|
|
531
|
+
position: sticky;
|
|
532
|
+
inset-inline: auto;
|
|
533
|
+
height: auto;
|
|
534
|
+
min-height: 52px;
|
|
535
|
+
/* Under the notch on a fullscreen phone browser, not behind it. */
|
|
536
|
+
padding-top: env(safe-area-inset-top);
|
|
537
|
+
padding-inline: 12px;
|
|
538
|
+
gap: 8px;
|
|
539
|
+
}
|
|
540
|
+
/*
|
|
541
|
+
The host's session control, sized like the ones opposite it.
|
|
542
|
+
|
|
543
|
+
The slot takes whatever the host passes — a bare `<button>`, their own
|
|
544
|
+
icon-button component, an anchor — so the 40px tap target lives on the
|
|
545
|
+
wrapper rather than depending on how they styled the thing inside.
|
|
546
|
+
*/
|
|
547
|
+
.fr-header-mobile-lead {
|
|
548
|
+
display: inline-flex;
|
|
549
|
+
flex-shrink: 0;
|
|
550
|
+
align-items: center;
|
|
551
|
+
justify-content: center;
|
|
552
|
+
min-width: 40px;
|
|
553
|
+
min-height: 40px;
|
|
554
|
+
color: var(--fr-muted);
|
|
555
|
+
}
|
|
556
|
+
.fr-header-mobile-lead > * {
|
|
557
|
+
/* A host button brings its own background; inside this bar it should read
|
|
558
|
+
as part of the row, the way the platform's ghost hamburger does. */
|
|
559
|
+
background: none;
|
|
560
|
+
border: none;
|
|
561
|
+
color: inherit;
|
|
562
|
+
cursor: pointer;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/* The title takes the row; the controls keep their size at its expense. */
|
|
566
|
+
.fr-header-mobile .fr-header-title {
|
|
567
|
+
flex: 1;
|
|
568
|
+
}
|
|
569
|
+
/*
|
|
570
|
+
The Files control, squared off rather than a pill.
|
|
571
|
+
|
|
572
|
+
The platform marks it pressed while its sheet is up — the same "the surface I
|
|
573
|
+
opened is open" mark the summary toggle wears — because the sheet docks below
|
|
574
|
+
this bar, so the button that opened it stays on screen. Left at rest, the bar
|
|
575
|
+
reads as if nothing were showing and the sheet as if it arrived on its own.
|
|
576
|
+
*/
|
|
577
|
+
.fr-header-files {
|
|
578
|
+
display: inline-flex;
|
|
579
|
+
align-items: center;
|
|
580
|
+
justify-content: center;
|
|
581
|
+
/* 40px: a tap target smaller than a fingertip is a miss. */
|
|
582
|
+
width: 40px;
|
|
583
|
+
height: 40px;
|
|
584
|
+
border: none;
|
|
585
|
+
border-radius: calc(var(--fr-radius, 8px) - 2px);
|
|
586
|
+
background: transparent;
|
|
587
|
+
color: var(--fr-muted);
|
|
588
|
+
cursor: pointer;
|
|
589
|
+
}
|
|
590
|
+
.fr-header-files:hover {
|
|
591
|
+
color: var(--fr-text);
|
|
592
|
+
}
|
|
593
|
+
/* The summary shares the Files control's box but stays a pill, matching the
|
|
594
|
+
platform's round ghost button beside its squared-off Files mark. */
|
|
595
|
+
.fr-header-summary-button {
|
|
596
|
+
border-radius: 999px;
|
|
597
|
+
}
|
|
598
|
+
.fr-header-files[data-open] {
|
|
599
|
+
background: var(--surface-raised-active, color-mix(in srgb, var(--fr-text) 10%, transparent));
|
|
600
|
+
color: var(--fr-text);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
.fr-header-mobile-actions {
|
|
604
|
+
display: flex;
|
|
605
|
+
flex-shrink: 0;
|
|
606
|
+
align-items: center;
|
|
607
|
+
gap: 2px;
|
|
608
|
+
}
|
|
609
|
+
/* No pinned overlay at this width, so the header keeps its own inset — the
|
|
610
|
+
desktop rules above reserve 44px for a control that is now in the row. */
|
|
611
|
+
@media (max-width: 767px) {
|
|
612
|
+
.fr-shell:has(.fr-header-actions) .fr-header {
|
|
613
|
+
padding-right: 12px;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/* The platform's always-on soften at the top of the thread:
|
|
618
|
+
`h-12 bg-gradient-to-b from-surface-chat/50 to-transparent z-10`. */
|
|
619
|
+
.fr-top-fade {
|
|
620
|
+
position: absolute;
|
|
621
|
+
top: 0;
|
|
622
|
+
left: 0;
|
|
623
|
+
right: 0;
|
|
624
|
+
z-index: 10;
|
|
625
|
+
height: 3rem;
|
|
626
|
+
pointer-events: none;
|
|
627
|
+
background: linear-gradient(
|
|
628
|
+
to bottom,
|
|
629
|
+
color-mix(in srgb, var(--fr-bg) 50%, transparent),
|
|
630
|
+
transparent
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/* Avatar + title hug each other so the group's right edge can be measured
|
|
635
|
+
against the message column for clash detection (see header.tsx). */
|
|
636
|
+
.fr-header-title-group {
|
|
637
|
+
display: flex;
|
|
638
|
+
min-width: 0;
|
|
639
|
+
align-items: center;
|
|
640
|
+
gap: 8px;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/* The pinned actions overlay (`.fr-header-actions`) occupies the header's
|
|
644
|
+
right edge; the in-header summary control stops short of it — but only
|
|
645
|
+
while the pane is SHUT. Open, the pinned toggle sits over the pane, the
|
|
646
|
+
chat header gets its full width back, and the summary control returns
|
|
647
|
+
flush to the column's edge exactly as the platform's does. */
|
|
648
|
+
.fr-shell:has(.fr-header-actions) .fr-header {
|
|
649
|
+
padding-right: 44px;
|
|
650
|
+
}
|
|
651
|
+
.fr-shell:has(.fr-side-pane:not([data-panel-size="0.0"])) .fr-header {
|
|
652
|
+
padding-right: 12px;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
.fr-header-title {
|
|
656
|
+
font-size: 14px;
|
|
657
|
+
font-weight: 550;
|
|
658
|
+
color: var(--fr-text);
|
|
659
|
+
overflow: hidden;
|
|
660
|
+
text-overflow: ellipsis;
|
|
661
|
+
white-space: nowrap;
|
|
662
|
+
min-width: 0;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/* The summary, at the trailing edge of the CONVERSATION column — it
|
|
666
|
+
summarises the session, so it travels with it. */
|
|
667
|
+
.fr-header-summary {
|
|
668
|
+
margin-left: auto;
|
|
669
|
+
flex-shrink: 0;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/*
|
|
673
|
+
The pane's own toggle, pinned across the whole surface.
|
|
674
|
+
|
|
675
|
+
`inset-inline: 0` with `justify-content: flex-end` puts these at the right
|
|
676
|
+
edge of everything on screen — the conversation when the pane is shut, the
|
|
677
|
+
pane when it is open. The controls therefore never move; the layout animates
|
|
678
|
+
underneath them, which is what makes the handover look seamless instead of
|
|
679
|
+
the buttons sliding across.
|
|
680
|
+
|
|
681
|
+
The row height and inset match the header's, so it lands exactly where a
|
|
682
|
+
trailing header button would have sat.
|
|
683
|
+
*/
|
|
684
|
+
.fr-header-actions {
|
|
685
|
+
position: absolute;
|
|
686
|
+
inset-inline: 0;
|
|
687
|
+
top: 0;
|
|
688
|
+
z-index: 30;
|
|
689
|
+
display: flex;
|
|
690
|
+
align-items: center;
|
|
691
|
+
justify-content: flex-end;
|
|
692
|
+
gap: 2px;
|
|
693
|
+
height: 40px;
|
|
694
|
+
padding-inline: 12px;
|
|
695
|
+
/* Click-through: the conversation and the pane's tab strip sit underneath
|
|
696
|
+
this row and must stay reachable. Only the buttons take the pointer. */
|
|
697
|
+
pointer-events: none;
|
|
698
|
+
}
|
|
699
|
+
.fr-header-actions > * {
|
|
700
|
+
pointer-events: auto;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
.fr-body {
|
|
704
|
+
display: flex;
|
|
705
|
+
flex-direction: column;
|
|
706
|
+
gap: 10px;
|
|
707
|
+
overflow-wrap: anywhere;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
.fr-text {
|
|
711
|
+
margin: 0;
|
|
712
|
+
white-space: pre-wrap;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/* ── markdown ───────────────────────────────────────────────────────── */
|
|
716
|
+
|
|
717
|
+
.fr-markdown > *:first-child { margin-top: 0; }
|
|
718
|
+
.fr-markdown > *:last-child { margin-bottom: 0; }
|
|
719
|
+
.fr-markdown p { margin: 0 0 0.75em; }
|
|
720
|
+
.fr-markdown h1, .fr-markdown h2, .fr-markdown h3 {
|
|
721
|
+
margin: 1.2em 0 0.5em;
|
|
722
|
+
line-height: 1.3;
|
|
723
|
+
font-weight: 650;
|
|
724
|
+
}
|
|
725
|
+
.fr-markdown h1 { font-size: 1.4em; }
|
|
726
|
+
.fr-markdown h2 { font-size: 1.2em; }
|
|
727
|
+
.fr-markdown h3 { font-size: 1.05em; }
|
|
728
|
+
.fr-markdown ul, .fr-markdown ol { margin: 0 0 0.75em; padding-left: 1.4em; }
|
|
729
|
+
.fr-markdown li { margin: 0.2em 0; }
|
|
730
|
+
.fr-markdown a { color: var(--fr-accent); }
|
|
731
|
+
.fr-markdown code {
|
|
732
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
733
|
+
font-size: 0.9em;
|
|
734
|
+
/* A warm code accent on a fill one step off the surface, so the chip still
|
|
735
|
+
reads inside a raised bubble. */
|
|
736
|
+
color: var(--fr-code);
|
|
737
|
+
background: var(--fr-code-bg);
|
|
738
|
+
border: 1px solid var(--fr-border);
|
|
739
|
+
border-radius: 5px;
|
|
740
|
+
padding: 0.1em 0.35em;
|
|
741
|
+
}
|
|
742
|
+
.fr-markdown pre {
|
|
743
|
+
background: var(--fr-surface);
|
|
744
|
+
border: 1px solid var(--fr-border);
|
|
745
|
+
border-radius: 10px;
|
|
746
|
+
padding: 12px 14px;
|
|
747
|
+
overflow-x: auto;
|
|
748
|
+
margin: 0 0 0.75em;
|
|
749
|
+
}
|
|
750
|
+
.fr-markdown pre code { background: none; border: 0; padding: 0; color: inherit; }
|
|
751
|
+
.fr-markdown blockquote {
|
|
752
|
+
margin: 0 0 0.75em;
|
|
753
|
+
padding-left: 12px;
|
|
754
|
+
border-left: 3px solid var(--fr-border);
|
|
755
|
+
color: var(--fr-muted);
|
|
756
|
+
}
|
|
757
|
+
/* Wide tables scroll inside the message rather than stretching the layout. */
|
|
758
|
+
.fr-markdown table { display: block; overflow-x: auto; border-collapse: collapse; }
|
|
759
|
+
.fr-markdown th, .fr-markdown td {
|
|
760
|
+
border: 1px solid var(--fr-border);
|
|
761
|
+
padding: 6px 10px;
|
|
762
|
+
text-align: left;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/* ── reasoning ──────────────────────────────────────────────────────── */
|
|
766
|
+
|
|
767
|
+
.fr-reasoning {
|
|
768
|
+
border: 1px solid var(--fr-border);
|
|
769
|
+
border-radius: 10px;
|
|
770
|
+
padding: 8px 12px;
|
|
771
|
+
color: var(--fr-muted);
|
|
772
|
+
font-size: 0.93em;
|
|
773
|
+
}
|
|
774
|
+
.fr-reasoning summary { cursor: pointer; user-select: none; }
|
|
775
|
+
.fr-reasoning-body { margin-top: 8px; white-space: pre-wrap; }
|
|
776
|
+
|
|
777
|
+
/* ── tool activity ──────────────────────────────────────────────────── */
|
|
778
|
+
|
|
779
|
+
.fr-tool {
|
|
780
|
+
display: inline-flex;
|
|
781
|
+
flex-direction: column;
|
|
782
|
+
gap: 6px;
|
|
783
|
+
align-self: flex-start;
|
|
784
|
+
border: 1px solid var(--fr-border);
|
|
785
|
+
border-radius: 999px;
|
|
786
|
+
padding: 5px 12px;
|
|
787
|
+
font-size: 0.88em;
|
|
788
|
+
color: var(--fr-muted);
|
|
789
|
+
background: var(--fr-surface);
|
|
790
|
+
}
|
|
791
|
+
.fr-tool.is-running { border-color: var(--fr-accent); color: var(--fr-accent); }
|
|
792
|
+
.fr-tool.is-failed { border-color: var(--fr-danger); color: var(--fr-danger); }
|
|
793
|
+
.fr-tool-head { display: flex; align-items: center; gap: 7px; }
|
|
794
|
+
.fr-tool-name { font-weight: 500; }
|
|
795
|
+
.fr-tool-error { color: var(--fr-danger); font-size: 0.95em; }
|
|
796
|
+
|
|
797
|
+
/* ── attachments and artifacts ──────────────────────────────────────── */
|
|
798
|
+
|
|
799
|
+
.fr-image { max-width: 100%; border-radius: 10px; }
|
|
800
|
+
|
|
801
|
+
.fr-file,
|
|
802
|
+
.fr-artifact {
|
|
803
|
+
display: inline-flex;
|
|
804
|
+
align-items: center;
|
|
805
|
+
gap: 8px;
|
|
806
|
+
align-self: flex-start;
|
|
807
|
+
border: 1px solid var(--fr-border);
|
|
808
|
+
border-radius: 10px;
|
|
809
|
+
background: var(--fr-surface);
|
|
810
|
+
color: inherit;
|
|
811
|
+
font: inherit;
|
|
812
|
+
padding: 8px 12px;
|
|
813
|
+
text-decoration: none;
|
|
814
|
+
cursor: pointer;
|
|
815
|
+
}
|
|
816
|
+
.fr-artifact:disabled { cursor: default; }
|
|
817
|
+
.fr-file:hover,
|
|
818
|
+
.fr-artifact:not(:disabled):hover { border-color: var(--fr-accent); }
|
|
819
|
+
|
|
820
|
+
/* ── typing ─────────────────────────────────────────────────────────── */
|
|
821
|
+
|
|
822
|
+
.fr-typing { display: flex; gap: 4px; padding: 6px 0; }
|
|
823
|
+
.fr-typing span {
|
|
824
|
+
width: 6px;
|
|
825
|
+
height: 6px;
|
|
826
|
+
border-radius: 50%;
|
|
827
|
+
background: var(--fr-muted);
|
|
828
|
+
animation: fr-bounce 1.2s infinite ease-in-out;
|
|
829
|
+
}
|
|
830
|
+
.fr-typing span:nth-child(2) { animation-delay: 0.15s; }
|
|
831
|
+
.fr-typing span:nth-child(3) { animation-delay: 0.3s; }
|
|
832
|
+
|
|
833
|
+
@keyframes fr-bounce {
|
|
834
|
+
0%, 60%, 100% { opacity: 0.3; transform: translateY(0); }
|
|
835
|
+
30% { opacity: 1; transform: translateY(-3px); }
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
.fr-icon { width: 15px; height: 15px; flex: none; }
|
|
839
|
+
.fr-spin { animation: fr-rotate 0.9s linear infinite; }
|
|
840
|
+
|
|
841
|
+
@keyframes fr-rotate { to { transform: rotate(360deg); } }
|
|
842
|
+
|
|
843
|
+
/* Respect a stated preference for less motion; the states remain legible
|
|
844
|
+
without it. */
|
|
845
|
+
@media (prefers-reduced-motion: reduce) {
|
|
846
|
+
.fr-typing span, .fr-spin { animation: none; }
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/* ── errors ─────────────────────────────────────────────────────────── */
|
|
850
|
+
|
|
851
|
+
.fr-error {
|
|
852
|
+
border: 1px solid var(--fr-danger);
|
|
853
|
+
border-radius: 10px;
|
|
854
|
+
padding: 10px 12px;
|
|
855
|
+
color: var(--fr-danger);
|
|
856
|
+
font-size: 0.93em;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/* ── composer ───────────────────────────────────────────────────────── */
|
|
860
|
+
|
|
861
|
+
/* The composer sits under the conversation, in the same column — an input
|
|
862
|
+
spanning a wide screen edge to edge does not read as part of the thread
|
|
863
|
+
above it. */
|
|
864
|
+
.fr-composer {
|
|
865
|
+
display: flex;
|
|
866
|
+
align-items: flex-end;
|
|
867
|
+
gap: 8px;
|
|
868
|
+
width: 100%;
|
|
869
|
+
max-width: 46rem;
|
|
870
|
+
margin: 0 auto;
|
|
871
|
+
padding: 12px 20px 16px;
|
|
872
|
+
background: var(--fr-bg);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
.fr-composer-input {
|
|
876
|
+
flex: 1;
|
|
877
|
+
resize: none;
|
|
878
|
+
/* Thin, and only when the text actually overflows: a full scrollbar with
|
|
879
|
+
arrows inside a one-line field reads as a broken control. */
|
|
880
|
+
scrollbar-width: thin;
|
|
881
|
+
font: inherit;
|
|
882
|
+
color: inherit;
|
|
883
|
+
background: var(--fr-surface);
|
|
884
|
+
border: 1px solid var(--fr-border);
|
|
885
|
+
border-radius: var(--fr-radius);
|
|
886
|
+
padding: 10px 14px;
|
|
887
|
+
max-height: 200px;
|
|
888
|
+
overflow-y: auto;
|
|
889
|
+
}
|
|
890
|
+
.fr-composer-input:focus {
|
|
891
|
+
outline: none;
|
|
892
|
+
border-color: var(--fr-accent);
|
|
893
|
+
}
|
|
894
|
+
.fr-composer-input::placeholder { color: var(--fr-muted); }
|
|
895
|
+
|
|
896
|
+
.fr-btn {
|
|
897
|
+
display: inline-flex;
|
|
898
|
+
align-items: center;
|
|
899
|
+
justify-content: center;
|
|
900
|
+
width: 38px;
|
|
901
|
+
height: 38px;
|
|
902
|
+
border-radius: 10px;
|
|
903
|
+
border: 1px solid transparent;
|
|
904
|
+
background: var(--fr-accent);
|
|
905
|
+
color: var(--fr-accent-text);
|
|
906
|
+
cursor: pointer;
|
|
907
|
+
flex: none;
|
|
908
|
+
}
|
|
909
|
+
.fr-btn:disabled { opacity: 0.45; cursor: not-allowed; }
|
|
910
|
+
.fr-btn-stop { background: var(--fr-surface); color: var(--fr-danger); border-color: var(--fr-border); }
|
|
911
|
+
.fr-btn:focus-visible { outline: 2px solid var(--fr-accent); outline-offset: 2px; }
|
|
912
|
+
|
|
913
|
+
/* A delegated task is work happening on the reader's behalf, not a mechanism
|
|
914
|
+
they invoked — given a little more room than a plain tool chip. */
|
|
915
|
+
.fr-tool.is-delegation {
|
|
916
|
+
border-radius: 10px;
|
|
917
|
+
padding: 7px 12px;
|
|
918
|
+
}
|
|
919
|
+
.fr-tool-task {
|
|
920
|
+
color: var(--fr-text);
|
|
921
|
+
opacity: 0.85;
|
|
922
|
+
}
|
|
923
|
+
.fr-tool-head { flex-wrap: wrap; }
|
|
924
|
+
|
|
925
|
+
/* ── new session ────────────────────────────────────────────────────── */
|
|
926
|
+
|
|
927
|
+
/* The empty state is centred in the thread, not pinned to the top: before a
|
|
928
|
+
conversation exists there is no reading order to respect, and a greeting at
|
|
929
|
+
the top of an empty column reads as a page that failed to load. */
|
|
930
|
+
.fr-empty {
|
|
931
|
+
/* Centred in the column, which already carries the width and inset. */
|
|
932
|
+
margin: auto;
|
|
933
|
+
width: 100%;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
.fr-new-session {
|
|
937
|
+
display: flex;
|
|
938
|
+
flex-direction: column;
|
|
939
|
+
gap: 18px;
|
|
940
|
+
align-items: center;
|
|
941
|
+
text-align: center;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
.fr-new-session-title {
|
|
945
|
+
margin: 0;
|
|
946
|
+
font-size: 1.55em;
|
|
947
|
+
font-weight: 650;
|
|
948
|
+
line-height: 1.25;
|
|
949
|
+
color: var(--fr-text);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/* Two columns, because four cards in a row become unreadably narrow and a
|
|
953
|
+
single column pushes the composer off a short screen. */
|
|
954
|
+
.fr-shortcuts {
|
|
955
|
+
display: grid;
|
|
956
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
957
|
+
gap: 10px;
|
|
958
|
+
width: 100%;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
.fr-shortcut {
|
|
962
|
+
display: flex;
|
|
963
|
+
flex-direction: column;
|
|
964
|
+
gap: 3px;
|
|
965
|
+
align-items: flex-start;
|
|
966
|
+
text-align: left;
|
|
967
|
+
padding: 12px 14px;
|
|
968
|
+
border-radius: var(--fr-radius);
|
|
969
|
+
border: 1px solid var(--fr-border);
|
|
970
|
+
background: var(--fr-surface);
|
|
971
|
+
color: var(--fr-text);
|
|
972
|
+
font: inherit;
|
|
973
|
+
cursor: pointer;
|
|
974
|
+
}
|
|
975
|
+
.fr-shortcut:hover { border-color: var(--fr-accent); }
|
|
976
|
+
.fr-shortcut:focus-visible { outline: 2px solid var(--fr-accent); outline-offset: 2px; }
|
|
977
|
+
|
|
978
|
+
.fr-shortcut-label { font-weight: 550; }
|
|
979
|
+
.fr-shortcut-desc { color: var(--fr-muted); font-size: 0.9em; }
|
|
980
|
+
|
|
981
|
+
@media (max-width: 640px) {
|
|
982
|
+
.fr-shortcuts { grid-template-columns: 1fr; }
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/* ── pending decisions ───────────────────────────────────────────────── */
|
|
986
|
+
|
|
987
|
+
/*
|
|
988
|
+
Nothing here any more, on purpose.
|
|
989
|
+
|
|
990
|
+
The approval and connect rail is styled with the platform's own Tailwind
|
|
991
|
+
classes rather than `fr-*` rules, because it sits directly on the copied
|
|
992
|
+
composer: a hand-written near-match reads as a bug in the product, not as a
|
|
993
|
+
difference in ours. Tailwind scans this package's `src`, so those classes
|
|
994
|
+
compile into `dist/platform.css` alongside the vendored components'.
|
|
995
|
+
*/
|
|
996
|
+
|