@gemboss/ui 0.2.0 → 0.3.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.
- package/README.md +75 -5
- package/catalog/index.html +140 -14
- package/gemboss-ui.css +784 -11
- package/package.json +6 -1
- package/src/assistant/dock.ts +83 -5
- package/src/assistant/host.tsx +113 -17
- package/src/assistant/index.ts +1 -1
- package/src/assistant/provider.tsx +75 -8
- package/src/assistant.css +90 -1
- package/src/chat-composer.css +110 -0
- package/src/chat.css +427 -0
- package/src/gemboss-field.css +4 -0
- package/src/icons.tsx +1 -0
- package/src/index.ts +1 -0
- package/src/kit-plain.css +110 -0
- package/src/kit-plain.js +179 -0
- package/src/menu.tsx +64 -0
- package/src/shell.css +8 -4
- package/src/ui.css +27 -4
package/gemboss-ui.css
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* GemBoss admin design system — GENERATED — do not edit; regenerate from gemboss:
|
|
3
3
|
* node apps/admin/scripts/export-ui-css.mjs --out <path> (npm run export:ui-css)
|
|
4
|
-
* gemboss commit:
|
|
5
|
-
* sources sha256:
|
|
4
|
+
* gemboss commit: 7d7968ae7238c5f4e7643d54dbac372f32ef5775
|
|
5
|
+
* sources sha256: 2807a6d2a8b27730eaa31cbe6195b633cbba9467b0ab62b9244a7f814bb8d205
|
|
6
6
|
* contents: 1 allow-listed rule from apps/admin/src/app/globals.css + packages/ui/src/gemboss-tokens.css
|
|
7
7
|
* (verbatim) + packages/ui/src/ui.css (verbatim) + packages/ui/src/gemboss-field.css (verbatim)
|
|
8
8
|
* + 180 rules from packages/ui/src/shell.css (the shared shell)
|
|
@@ -337,7 +337,7 @@
|
|
|
337
337
|
background-color: var(--gemboss-field-bg-hover);
|
|
338
338
|
}
|
|
339
339
|
.ui-select:focus { outline: none; }
|
|
340
|
-
.ui-select:focus-visible {
|
|
340
|
+
.ui-select:focus-visible:not(:disabled) {
|
|
341
341
|
outline: 2px solid #005bd3; outline-offset: 1px;
|
|
342
342
|
border-color: var(--gemboss-field-line-active);
|
|
343
343
|
}
|
|
@@ -483,12 +483,35 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
|
|
|
483
483
|
consumer belongs in that consumer's chunk (see SettingsDialog.module.css). */
|
|
484
484
|
.ui-modal--lg { position: relative; }
|
|
485
485
|
|
|
486
|
+
/* The kit's multi-line field joins the ONE field family (01/10/2026, Chris: "form control đồng bộ"):
|
|
487
|
+
same input-border token, hover and focus ring as `gemboss-field` and `.ui-select`. Before, it drew a
|
|
488
|
+
lighter --ui-border box at 14px with no focus ring, so on GemFactory's compose form a textarea sat
|
|
489
|
+
beside selects of a darker border and text fields of a third look (measured on prod). */
|
|
486
490
|
.ui-textarea {
|
|
487
491
|
width: 100%; min-height: 96px; resize: vertical; box-sizing: border-box;
|
|
488
|
-
background: var(--
|
|
489
|
-
|
|
492
|
+
background: var(--gemboss-field-bg);
|
|
493
|
+
border: 1px solid var(--gemboss-field-line); border-radius: var(--ui-radius-md);
|
|
494
|
+
padding: 12px; color: var(--gemboss-field-ink); font-size: var(--ui-fs-body); line-height: 1.5; font-family: inherit;
|
|
495
|
+
}
|
|
496
|
+
.ui-textarea::placeholder { color: var(--gemboss-field-placeholder); }
|
|
497
|
+
.ui-textarea:hover:not(:disabled) {
|
|
498
|
+
border-color: var(--gemboss-field-line-hover);
|
|
499
|
+
background-color: var(--gemboss-field-bg-hover);
|
|
500
|
+
}
|
|
501
|
+
.ui-textarea:focus { outline: none; }
|
|
502
|
+
.ui-textarea:focus-visible:not(:disabled) {
|
|
503
|
+
outline: 2px solid #005bd3; outline-offset: 1px;
|
|
504
|
+
border-color: var(--gemboss-field-line-active);
|
|
505
|
+
}
|
|
506
|
+
.ui-textarea:disabled { opacity: .5; cursor: not-allowed; }
|
|
507
|
+
/* On a PHONE-WIDTH screen every field of the family reads 16px (iOS zooms the page on tap below that)
|
|
508
|
+
and a select is a 44px target — by WIDTH, as `gemboss-field` does, because a desktop browser at 320px
|
|
509
|
+
still reports pointer: fine. Measured on GemFactory at 390px before this: select 13px, textarea 14px. */
|
|
510
|
+
@media (max-width: 47.9975em) {
|
|
511
|
+
.ui-select.ui-select, .ui-textarea { font-size: 16px; }
|
|
512
|
+
/* Height only for the full select: `--slim` is a density decision (table rows), as the coarse block keeps it. */
|
|
513
|
+
.ui-select:not(.ui-select--slim) { min-height: 44px; }
|
|
490
514
|
}
|
|
491
|
-
.ui-textarea:focus { border-color: var(--ui-accent); }
|
|
492
515
|
|
|
493
516
|
/* ── Segmented control ────────────────────────────────────────────────────────
|
|
494
517
|
Grouped toggle: icon squares (.ui-seg button) or text tabs (.is-text). The
|
|
@@ -768,6 +791,10 @@ body.ui-resizing::after { content: ""; position: fixed; inset: 0; z-index: 9999;
|
|
|
768
791
|
border: 1px solid var(--gemboss-field-line);
|
|
769
792
|
border-radius: 8px;
|
|
770
793
|
}
|
|
794
|
+
/* WebKit draws `input[type=search]` as `-webkit-appearance: searchfield`: its own padding and rounding, so in Safari
|
|
795
|
+
(macOS and iOS, the platform the 16px rule exists for) a search box did not match the fields beside it. Same box for
|
|
796
|
+
every text type; the clear button stays. (GemFactory #98 review, 01/10/2026.) */
|
|
797
|
+
input[type="search"]:is(.gemboss-field, .gemboss-field__bare) { -webkit-appearance: none; appearance: none; }
|
|
771
798
|
.gemboss-field::placeholder { color: var(--gemboss-field-placeholder); }
|
|
772
799
|
/* A NARROW field — the inspector's T/R/B/L boxes and its number spinners are 40-68px wide, and the
|
|
773
800
|
shared 12px side padding left 38px of content box for a three-digit value (measured in Chromium:
|
|
@@ -882,7 +909,8 @@ textarea.gemboss-field { height: auto; padding: 6px 12px; resize: vertical; }
|
|
|
882
909
|
z-index: 320;
|
|
883
910
|
overflow: hidden;
|
|
884
911
|
}
|
|
885
|
-
.gemboss-account-menu-item
|
|
912
|
+
.gemboss-account-menu-item,
|
|
913
|
+
.ui-menu__item {
|
|
886
914
|
display: block;
|
|
887
915
|
width: 100%;
|
|
888
916
|
min-height: 44px;
|
|
@@ -897,15 +925,18 @@ textarea.gemboss-field { height: auto; padding: 6px 12px; resize: vertical; }
|
|
|
897
925
|
cursor: pointer;
|
|
898
926
|
transition: background var(--gemboss-transition-fast) ease, color var(--gemboss-transition-fast) ease;
|
|
899
927
|
}
|
|
900
|
-
.gemboss-account-menu-item:hover
|
|
928
|
+
.gemboss-account-menu-item:hover,
|
|
929
|
+
.ui-menu__item:hover {
|
|
901
930
|
background: var(--gemboss-hover);
|
|
902
931
|
}
|
|
903
|
-
.gemboss-account-menu-item:focus-visible
|
|
932
|
+
.gemboss-account-menu-item:focus-visible,
|
|
933
|
+
.ui-menu__item:focus-visible {
|
|
904
934
|
outline: 2px solid var(--gemboss-primary);
|
|
905
935
|
outline-offset: -2px;
|
|
906
936
|
background: var(--gemboss-hover);
|
|
907
937
|
}
|
|
908
|
-
.gemboss-account-menu-item.danger
|
|
938
|
+
.gemboss-account-menu-item.danger,
|
|
939
|
+
.ui-menu__item--danger {
|
|
909
940
|
color: var(--gemboss-danger-strong);
|
|
910
941
|
}
|
|
911
942
|
.gemboss-account-chip {
|
|
@@ -2709,6 +2740,7 @@ body.gemboss-assistant-unread .gnav-item.gnav-assistant::after {
|
|
|
2709
2740
|
degrading to instant is the right failure for chrome nobody asked to watch. */
|
|
2710
2741
|
@starting-style {
|
|
2711
2742
|
.gb-dock-dock, .gb-dock-mini { transform: translateY(10px); opacity: 0; }
|
|
2743
|
+
.gb-dock-side { transform: translateX(12px); opacity: 0; }
|
|
2712
2744
|
}
|
|
2713
2745
|
/* Nothing animates while a pointer is setting the numbers — see `dragging` in the host. */
|
|
2714
2746
|
.gb-dock-dragging, .gb-dock-dragging * { transition: none !important; }
|
|
@@ -2747,6 +2779,66 @@ body.gemboss-assistant-unread .gnav-item.gnav-assistant::after {
|
|
|
2747
2779
|
background: var(--gemboss-app-bg);
|
|
2748
2780
|
}
|
|
2749
2781
|
|
|
2782
|
+
/* ── Side: a full-height panel pinned to the right edge (01/10/2026) ─────────────────────────────
|
|
2783
|
+
The third working shape, beside the popup and full screen: a long conversation next to the screen
|
|
2784
|
+
it is about. Opt-in per surface (`<AssistantHost allowSide>`), because unlike the popup it TAKES
|
|
2785
|
+
ROOM — the page beside it moves over (rule below), so nothing under it is hidden. That is why the
|
|
2786
|
+
surface is solid rather than frosted: nothing sits behind it to show through.
|
|
2787
|
+
Above `.gemboss-route-shell` (5), so the panel stays put while routes change — a sidebar that
|
|
2788
|
+
vanishes on every click is a popup with extra steps. BELOW the topbar (300): the account menu
|
|
2789
|
+
opens down out of the topbar's right end, exactly where this panel starts, and at the popup's 320
|
|
2790
|
+
the menu fell behind the panel (review). 290 still clears the desktop invite (260/280). The host
|
|
2791
|
+
is rendered inside `main` (AppShell's slot), which works because `main` makes no stacking context
|
|
2792
|
+
or containing block; a surface that gives its `main` a transform or z-index must mount it outside. */
|
|
2793
|
+
.gb-dock-side {
|
|
2794
|
+
z-index: 290;
|
|
2795
|
+
top: 56px;
|
|
2796
|
+
right: 0;
|
|
2797
|
+
bottom: 0;
|
|
2798
|
+
width: var(--gb-dock-side-w, 400px);
|
|
2799
|
+
background: var(--gemboss-surface);
|
|
2800
|
+
-webkit-backdrop-filter: none;
|
|
2801
|
+
backdrop-filter: none;
|
|
2802
|
+
border: 0;
|
|
2803
|
+
border-left: 1px solid var(--gemboss-divider);
|
|
2804
|
+
border-radius: 0;
|
|
2805
|
+
box-shadow: none;
|
|
2806
|
+
overflow: visible; /* the seam's touch band reaches 14px past the left edge */
|
|
2807
|
+
}
|
|
2808
|
+
.gb-dock-side .gb-dock-body, .gb-dock-side .gb-dock-pane { overflow: hidden; }
|
|
2809
|
+
/* The header does not drag a panel that is pinned. */
|
|
2810
|
+
.gb-dock-side .gb-dock-header { cursor: default; touch-action: auto; }
|
|
2811
|
+
/* The word for what the dot already says stands down here, as it does in a popup under 420px: the panel has
|
|
2812
|
+
five controls in its header, and the title needs the room (measured: "Gem assi…" / "Re…" at 400). Visually
|
|
2813
|
+
only — the dot is aria-hidden, so the word is the one place a screen reader hears "Working" (review #3622). */
|
|
2814
|
+
.gb-dock-side .gb-dock-status {
|
|
2815
|
+
flex: 0 0 auto; position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
|
|
2816
|
+
overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
|
|
2817
|
+
}
|
|
2818
|
+
.gb-dock-side .gb-dock-header:active { cursor: default; }
|
|
2819
|
+
/* The seam: the panel's left edge, the fleet's one `.ui-resizer` (ui.css draws the grip). A 5px strip
|
|
2820
|
+
centred on the border, ABOVE the page beside it. */
|
|
2821
|
+
/* `.gb-dock-side >` is not decoration: `.ui-resizer { position: relative }` (ui.css) has the weight of one
|
|
2822
|
+
class too, and which file a bundler loads last would decide whether the seam is a 5px strip or a 0px
|
|
2823
|
+
line nobody can grab. */
|
|
2824
|
+
.gb-dock-side > .gb-dock-sidegrip {
|
|
2825
|
+
position: absolute;
|
|
2826
|
+
top: 0;
|
|
2827
|
+
bottom: 0;
|
|
2828
|
+
left: -3px;
|
|
2829
|
+
width: 5px;
|
|
2830
|
+
z-index: 3;
|
|
2831
|
+
}
|
|
2832
|
+
/* The page makes room. `margin-right`, not padding: main's padding is set by four rules in shell.css
|
|
2833
|
+
and a fifth here would fight them. The provider puts the class and the width on <body>; the default
|
|
2834
|
+
`bodyClassPrefix` is `gemboss-assistant` — a surface with another prefix writes its own copy of this
|
|
2835
|
+
one rule. No transition: the seam is set from a pointer, and a page that trails the finger is the
|
|
2836
|
+
bug `.gb-dock` already avoids. */
|
|
2837
|
+
@media (min-width: 1100px) {
|
|
2838
|
+
body.gemboss-assistant-side .gemboss-platform-main { margin-right: var(--gb-assistant-side-w, 400px); }
|
|
2839
|
+
}
|
|
2840
|
+
|
|
2841
|
+
|
|
2750
2842
|
/* ── Full: the one control full screen needs — the way back to the popup ──────────────────────
|
|
2751
2843
|
Floated over the chat's own header, whose right half is empty, rather than given a bar of its
|
|
2752
2844
|
own: a whole strip of chrome for one button would push the conversation down on every screen. */
|
|
@@ -2896,7 +2988,10 @@ body.gemboss-assistant-unread .gnav-item.gnav-assistant::after {
|
|
|
2896
2988
|
white-space: nowrap;
|
|
2897
2989
|
}
|
|
2898
2990
|
.gb-dock-header:active { cursor: grabbing; }
|
|
2899
|
-
|
|
2991
|
+
/* `margin-left: auto`: the status word was the only thing that stretched, so wherever it stands down (a popup under
|
|
2992
|
+
420px, the side panel) the controls followed the title instead of the right edge — 350px adrift in a 720px panel
|
|
2993
|
+
(review #3622). With the word showing, it still takes the free space first and nothing moves. */
|
|
2994
|
+
.gb-dock-actions { display: flex; align-items: center; gap: 2px; cursor: default; margin-left: auto; }
|
|
2900
2995
|
/* One ACTION, then the three WINDOW controls — ordered by how much each changes the window: bigger,
|
|
2901
2996
|
smaller, gone. The gap is what says they are two different kinds of button; without it four icons
|
|
2902
2997
|
in a row read as one undifferentiated strip, which is what "các icon đang không đồng đều" was
|
|
@@ -3039,4 +3134,682 @@ body.gemboss-assistant-unread .gnav-item.gnav-assistant::after {
|
|
|
3039
3134
|
bottom: 0;
|
|
3040
3135
|
border-radius: 0;
|
|
3041
3136
|
}
|
|
3137
|
+
/* No room beside a phone screen: the side panel takes the screen, the way full does, and there is
|
|
3138
|
+
nothing to drag. The page is not pushed (the rule above stops at 761). */
|
|
3139
|
+
.gb-dock-side {
|
|
3140
|
+
left: 0;
|
|
3141
|
+
right: 0;
|
|
3142
|
+
width: auto;
|
|
3143
|
+
border-left: 0;
|
|
3144
|
+
border-radius: 0; /* the phone .gb-dock rule above rounds the top; this panel is the screen */
|
|
3145
|
+
}
|
|
3146
|
+
.gb-dock-sidegrip { display: none; }
|
|
3147
|
+
/* …so the button that pins it there would only say "full screen" a second time. */
|
|
3148
|
+
.gb-dock-pinside { display: none; }
|
|
3149
|
+
}
|
|
3150
|
+
|
|
3151
|
+
/* 1100 = rail 240 + the panel's narrowest 380 + the page's narrowest 480 (SIDE_MIN_WIDTH, SIDE_PAGE_MIN in
|
|
3152
|
+
assistant/dock.ts). Below it the two do not fit side by side — MEASURED at 768: pushed, the page kept
|
|
3153
|
+
120px and its table ran under the panel. So between the phone and 1100 the panel covers the content
|
|
3154
|
+
area the way full screen does, nothing is pushed, there is no seam to drag, and the popup does not
|
|
3155
|
+
offer to pin.
|
|
3156
|
+
AT THE END OF THE FILE on purpose: `.gb-dock-icon { display: grid }` comes later than the side
|
|
3157
|
+
section and has the same weight, so placed there this block lost and the pin still showed at 768
|
|
3158
|
+
(measured). */
|
|
3159
|
+
@media (min-width: 761px) and (max-width: 1099.98px) {
|
|
3160
|
+
.gb-dock-side { left: 240px; width: auto; }
|
|
3161
|
+
.gb-dock-side .gb-dock-sidegrip, .gb-dock-pinside { display: none; }
|
|
3162
|
+
}
|
|
3163
|
+
|
|
3164
|
+
/* ═══ @gemboss/ui src/kit-plain.css ═══ */
|
|
3165
|
+
/* kit-plain.css — kit primitives that only the NON-React surfaces use (GemFactory, gemcare, gempress write the
|
|
3166
|
+
markup by hand). They ship inside gemboss-ui.css (scripts/build-css.mjs `own`), but the admin does NOT import
|
|
3167
|
+
this file: `ui.css` is loaded by the admin's root layout, so every rule there is paid on every admin screen,
|
|
3168
|
+
and adding these to it pushed the admin shell 0.2 KB over its first-load budget (CI of #3591, 01/10/2026).
|
|
3169
|
+
The admin has Polaris for menus, checkboxes and description lists. */
|
|
3170
|
+
|
|
3171
|
+
/* ── Icon button · Menu · Key-value list ──────────────────────────────────────
|
|
3172
|
+
Added 01/10/2026 for GemFactory's item dialog, which had seven actions in one header row; the
|
|
3173
|
+
secondary five now sit behind one "⋯" button. Plain CSS, same contract as the tabs and tables in ui.css.
|
|
3174
|
+
|
|
3175
|
+
Icon button: `<button class="ui-btn ui-btn--icon" aria-label="…"><svg…></button>` — square, the
|
|
3176
|
+
label is REQUIRED (there is no text for a screen reader to read otherwise).
|
|
3177
|
+
|
|
3178
|
+
Menu: a `<details>` so it opens with no script. `<details class="ui-menu ui-menu--end">
|
|
3179
|
+
<summary class="ui-btn ui-btn--icon" aria-label="More actions">…</summary><div class="ui-menu__list">
|
|
3180
|
+
<button class="ui-menu__item">Copy #12<span class="ui-menu__hint">for Claude Code</span></button>
|
|
3181
|
+
<hr class="ui-menu__sep"><button class="ui-menu__item ui-menu__item--danger">Delete…</button></div>
|
|
3182
|
+
</details>`. `--end` aligns the list to the right edge of its button. Closing on an outside click,
|
|
3183
|
+
on an item, and on Escape is the page's job (a few lines; GemFactory's ui-kit.mjs has them).
|
|
3184
|
+
Items are 44px under EVERY pointer, not only coarse: they touch each other (0px apart), and §9.1
|
|
3185
|
+
only allows a gap under 8px between targets that are already 44px.
|
|
3186
|
+
|
|
3187
|
+
Key-value list: `<dl class="ui-kv"><dt>Owner</dt><dd>Lily</dd>…</dl>` — label column sized to
|
|
3188
|
+
its longest label, value column takes the rest and wraps long keys and URLs. */
|
|
3189
|
+
.ui-btn--icon { flex: none; width: var(--ui-control-h); padding: 0; }
|
|
3190
|
+
.ui-btn--icon.ui-btn--sm { width: 28px; }
|
|
3191
|
+
.ui-btn--icon svg { width: 18px; height: 18px; }
|
|
3192
|
+
/* Menu: the SURFACE is the shell's `gemboss-popover-surface` and the rows are the account menu's row (shell.css
|
|
3193
|
+
declares `.gemboss-account-menu-item, .ui-menu__item` together), so a kit menu and the account menu on one page
|
|
3194
|
+
look the same and a fix to one reaches the other (§9.3). Markup: `<div class="ui-menu__list gemboss-popover-surface">`.
|
|
3195
|
+
Here: only placement, the hint, the separator and the disabled state. */
|
|
3196
|
+
.ui-menu { position: relative; display: inline-block; }
|
|
3197
|
+
.ui-menu > summary { list-style: none; cursor: pointer; }
|
|
3198
|
+
.ui-menu > summary::-webkit-details-marker { display: none; }
|
|
3199
|
+
.ui-menu__list { position: absolute; z-index: 20; top: calc(100% + 4px); left: 0; display: grid; min-width: 220px; padding: var(--ui-space-1); box-sizing: border-box; }
|
|
3200
|
+
.ui-menu--end .ui-menu__list { left: auto; right: 0; }
|
|
3201
|
+
.ui-menu__item { display: flex; align-items: center; gap: var(--ui-space-2); white-space: nowrap; border-radius: var(--ui-radius-sm); }
|
|
3202
|
+
.ui-menu__item:is([disabled], [aria-disabled="true"]) { opacity: .5; cursor: not-allowed; pointer-events: none; }
|
|
3203
|
+
.ui-menu__hint { margin-left: auto; padding-left: var(--ui-space-4); color: var(--ui-text-muted); font-size: var(--ui-fs-caption); }
|
|
3204
|
+
.ui-menu__sep { height: 1px; margin: var(--ui-space-1) 0; border: 0; background: var(--ui-border); }
|
|
3205
|
+
/* Checkbox: the LABEL is the target; disabled turns both the label and the box to not-allowed. Focus ring in the
|
|
3206
|
+
kit's own token (no --p-* here: this file is appended after the export's Polaris resolver runs). */
|
|
3207
|
+
.ui-check { display: inline-flex; align-items: center; gap: var(--ui-space-2); min-height: 44px; cursor: pointer; font-size: var(--ui-fs-body); color: var(--ui-text); }
|
|
3208
|
+
.ui-check > input { flex: none; width: 18px; height: 18px; margin: 0; accent-color: var(--ui-accent); cursor: inherit; }
|
|
3209
|
+
.ui-check > input:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: 2px; }
|
|
3210
|
+
.ui-check:has(> input:disabled) { opacity: .5; cursor: not-allowed; }
|
|
3211
|
+
.ui-label { display: block; margin: 0 0 var(--ui-space-1); font-size: var(--ui-fs-caption); font-weight: 600; color: var(--ui-text-muted); }
|
|
3212
|
+
.ui-kv { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: var(--ui-space-2) var(--ui-space-4); margin: 0; font-size: var(--ui-fs-body); }
|
|
3213
|
+
.ui-kv > dt { color: var(--ui-text-muted); }
|
|
3214
|
+
.ui-kv > dd { margin: 0; min-width: 0; overflow-wrap: anywhere; color: var(--ui-text); }
|
|
3215
|
+
|
|
3216
|
+
/* Under a thumb the icon-only button is 44 WIDE as well (the kit's coarse block already raises every .ui-btn to 44 tall). */
|
|
3217
|
+
@media (pointer: coarse) { .ui-btn--icon:not(.ui-btn--sm) { min-width: 44px; } }
|
|
3218
|
+
/* …and at phone WIDTH, as gemboss-field does: a desktop browser at 320px still reports pointer: fine. */
|
|
3219
|
+
@media (max-width: 47.9975em) { .ui-btn--icon:not(.ui-btn--sm) { min-width: 44px; min-height: 44px; } }
|
|
3220
|
+
|
|
3221
|
+
/* ── Link · Media card (02/10/2026) ──────────────────────────────────────────
|
|
3222
|
+
GemFactory, gemcare and gempress left text links in the browser's default blue-and-purple, and each drew its own
|
|
3223
|
+
thumbnail card. The admin uses Polaris' Link and has no media card, so both live here, not in ui.css.
|
|
3224
|
+
|
|
3225
|
+
Link: `<a class="ui-link" href>` for a link inside text. Iris (--gemboss-accent-strong, 6.2:1 on white), underlined
|
|
3226
|
+
so it is not told by colour alone (WCAG 1.4.1); visited looks the same (these are tools, not articles). Markdown or
|
|
3227
|
+
other generated HTML you cannot add classes to: wrap it in `.ui-prose` and its bare `<a>` get the same look.
|
|
3228
|
+
|
|
3229
|
+
Media card: `<article class="ui-card ui-card--media"><a|button class="ui-card__media"><img alt=""></…>
|
|
3230
|
+
<div class="ui-card__body">…</div><div class="ui-card__foot">…</div></article>`. The picture is 16:10 and
|
|
3231
|
+
cropped to fill; the card has no padding of its own, the body and foot do. */
|
|
3232
|
+
.ui-link,
|
|
3233
|
+
.ui-prose a:not([class]) {
|
|
3234
|
+
color: var(--gemboss-accent-strong, #3a44de);
|
|
3235
|
+
text-decoration: underline;
|
|
3236
|
+
text-decoration-thickness: from-font;
|
|
3237
|
+
text-underline-offset: 2px;
|
|
3238
|
+
}
|
|
3239
|
+
.ui-link:visited,
|
|
3240
|
+
.ui-prose a:not([class]):visited { color: var(--gemboss-accent-strong, #3a44de); }
|
|
3241
|
+
.ui-link:hover,
|
|
3242
|
+
.ui-prose a:not([class]):hover { color: var(--gemboss-accent, #5661f5); text-decoration-thickness: 2px; }
|
|
3243
|
+
.ui-link:focus-visible,
|
|
3244
|
+
.ui-prose a:not([class]):focus-visible { outline: 2px solid var(--ui-focus, #005bd3); outline-offset: 2px; border-radius: 2px; }
|
|
3245
|
+
|
|
3246
|
+
.ui-card--media { padding: 0; overflow: hidden; display: flex; flex-direction: column; }
|
|
3247
|
+
.ui-card__media {
|
|
3248
|
+
display: block; width: 100%; aspect-ratio: 16 / 10; margin: 0; padding: 0; border: 0;
|
|
3249
|
+
border-bottom: 1px solid var(--ui-border); background: var(--ui-surface-2); overflow: hidden; color: inherit;
|
|
3250
|
+
}
|
|
3251
|
+
.ui-card__media > img { display: block; width: 100%; height: 100%; object-fit: cover; }
|
|
3252
|
+
button.ui-card__media, a.ui-card__media { cursor: pointer; }
|
|
3253
|
+
.ui-card__media:focus-visible { outline: 2px solid var(--ui-focus, #005bd3); outline-offset: -2px; }
|
|
3254
|
+
/* justify-items: start: a tag or a button in the body keeps its own width instead of stretching across the card. */
|
|
3255
|
+
.ui-card__body { display: grid; justify-items: start; gap: var(--ui-space-2, 8px); padding: var(--ui-space-3, 12px) var(--ui-space-4, 16px); min-width: 0; }
|
|
3256
|
+
.ui-card__foot {
|
|
3257
|
+
display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--ui-space-2, 8px);
|
|
3258
|
+
margin-top: auto; padding: 0 var(--ui-space-4, 16px) var(--ui-space-3, 12px);
|
|
3259
|
+
color: var(--ui-text-muted); font-size: var(--ui-fs-caption, 13px);
|
|
3260
|
+
}
|
|
3261
|
+
|
|
3262
|
+
/* Popup body and foot: `<div class="ui-modal__body">…</div><div class="ui-modal__foot"><button class="ui-btn">…</button></div>`.
|
|
3263
|
+
The admin's Modal.tsx pads its own children, so ui.css never styled these; every non-React surface padded them by
|
|
3264
|
+
hand (GemFactory: 16/20 and a hairline above the actions). kit-plain.js's openModal writes exactly this shape. */
|
|
3265
|
+
.ui-modal__body { padding: 16px 20px; }
|
|
3266
|
+
.ui-modal__foot {
|
|
3267
|
+
display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end;
|
|
3268
|
+
padding: 12px 20px; border-top: 1px solid var(--ui-border);
|
|
3269
|
+
}
|
|
3270
|
+
/* A popup taller than the screen: the body scrolls, the head and foot stay, so the actions are always reachable
|
|
3271
|
+
(320×568 with a long form put Huỷ/Xoá below the fold, unreachable under the locked page; review #3633). */
|
|
3272
|
+
.ui-overlay > .ui-modal { display: flex; flex-direction: column; max-height: calc(100dvh - 40px); }
|
|
3273
|
+
.ui-modal > .ui-modal__head, .ui-modal > .ui-modal__foot { flex: none; }
|
|
3274
|
+
.ui-modal > .ui-modal__body { flex: 1 1 auto; min-height: 0; overflow: auto; }
|
|
3275
|
+
|
|
3276
|
+
/* ═══ @gemboss/ui src/chat.css ═══ */
|
|
3277
|
+
/*
|
|
3278
|
+
* The chat BODY — what an answer looks like, wherever a GemBoss surface puts a chat (01/10/2026).
|
|
3279
|
+
*
|
|
3280
|
+
* Chris: every surface's AI assistant (gemcare, content-radar, partner-radar, GemCommunity) must be built from the
|
|
3281
|
+
* GemBoss UI framework. Until today only the FRAME was shared (assistant.css: popup / bar / full screen; shell.css:
|
|
3282
|
+
* the thread rail); the body lived in the admin's gemboss-shell.css and the exporter left it out on purpose, so every
|
|
3283
|
+
* surface drew its own bubbles, markdown and composer. These are those rules, moved here VERBATIM (same selectors,
|
|
3284
|
+
* same values, same order) — the admin @imports this file at the top of its shell, so it is still the one owner.
|
|
3285
|
+
*
|
|
3286
|
+
* What is here: the header (`.gemboss-ec-head/title/sub`), the message list (`.gemboss-ec-scroll`), messages and
|
|
3287
|
+
* bubbles (`.gemboss-ec-msg.user|.assistant`, `.gemboss-ec-bubble`), the working indicator and the streaming cursor
|
|
3288
|
+
* (`.gemboss-ec-pending*`, `.gemboss-ec-cursor`), markdown (`.gemboss-md-*`, `.gemboss-ec-md-link`), step narration
|
|
3289
|
+
* (`.gemboss-narr*`), starters and option chips (`.gemboss-ec-starter(s)`, `.gemboss-ec-chip(s)`), and the composer
|
|
3290
|
+
* (`.gemboss-ec-input` with its textarea and submit button).
|
|
3291
|
+
* What is NOT: the panel's geometry (`.gemboss-ec` width, editor columns, tabs), attachments, CTA / action cards,
|
|
3292
|
+
* the engine picker and work mode — those are still the admin's, and a surface brings its own. ONE exception, on
|
|
3293
|
+
* purpose: the chat panel's touch / mouse TARGET blocks (`@media (pointer: coarse|fine)` below) name every control
|
|
3294
|
+
* of the panel, the admin-only ones (tabs, upload, engine picker, work mode, attach, stop) included. They are one
|
|
3295
|
+
* rule per pointer kind; split in two they wrote their declarations twice and the admin's shell budget had no room.
|
|
3296
|
+
* Those selectors match nothing on a surface without those controls. (`.gemboss-ec-workmode`: the click-area
|
|
3297
|
+
* `::before` is here, its label-sizing `::after` stays the admin's.)
|
|
3298
|
+
*
|
|
3299
|
+
* Markup: see the catalog's "Thân chat" section. Behaviour (streaming, stop, keyboard) belongs to the surface.
|
|
3300
|
+
*/
|
|
3301
|
+
|
|
3302
|
+
.gemboss-ec-head { padding: 16px 16px; border-bottom: 1px solid var(--gemboss-divider); display: flex; flex-direction: column; gap: 2px; }
|
|
3303
|
+
.gemboss-ec-title { font-family: var(--gemboss-serif); font-size: 15px; font-weight: 600; }
|
|
3304
|
+
.gemboss-ec-sub { font-size: 12px; color: var(--gemboss-text-muted); }
|
|
3305
|
+
.gemboss-ec-scroll { flex: 1; min-height: 0; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 16px; scrollbar-gutter: stable; }
|
|
3306
|
+
/* Visible thin scrollbar on the chat message list — the default macOS overlay bar is
|
|
3307
|
+
hidden until you actively scroll, so the list reads as "can't scroll" (esp. on the
|
|
3308
|
+
dedicated Assistant page, a fixed-height chat where the page itself doesn't scroll).
|
|
3309
|
+
A persistent thin bar makes the scroll affordance obvious; matches the composer input. */
|
|
3310
|
+
.gemboss-ec-scroll { scrollbar-width: thin; scrollbar-color: rgba(31,30,29,0.22) transparent; }
|
|
3311
|
+
.gemboss-ec-scroll::-webkit-scrollbar { width: 9px; }
|
|
3312
|
+
.gemboss-ec-scroll::-webkit-scrollbar-track { background: transparent; }
|
|
3313
|
+
.gemboss-ec-scroll::-webkit-scrollbar-thumb { background: rgba(31,30,29,0.2); border-radius: 6px; border: 2px solid transparent; background-clip: content-box; }
|
|
3314
|
+
.gemboss-ec-msg { display: flex; flex-direction: column; gap: 8px; max-width: 100%; }
|
|
3315
|
+
.gemboss-ec-msg.user { align-items: flex-end; }
|
|
3316
|
+
.gemboss-ec-msg.assistant { align-items: flex-start; }
|
|
3317
|
+
.gemboss-ec-bubble {
|
|
3318
|
+
/* Cap reading measure: a chat reply stays at a readable line length, not the bubble's full 90%. */
|
|
3319
|
+
font-size: var(--gemboss-fs-body); line-height: 1.5; padding: 12px 12px; border-radius: 16px; max-width: min(90%, 68ch);
|
|
3320
|
+
white-space: pre-wrap; word-break: break-word;
|
|
3321
|
+
}
|
|
3322
|
+
.gemboss-ec-msg.user .gemboss-ec-bubble { background: var(--gemboss-primary); color: #fff; border-bottom-right-radius: 4px; }
|
|
3323
|
+
.gemboss-ec-msg.assistant .gemboss-ec-bubble { background: var(--gemboss-surface); color: var(--gemboss-text); border: 1px solid var(--gemboss-divider); border-bottom-left-radius: 4px; }
|
|
3324
|
+
.gemboss-ec-input button[type="submit"] svg { width: 18px; height: 18px; fill: currentColor; }
|
|
3325
|
+
/* Live "working" heartbeat — the Claude-Code-style running asterisk shown before the first token.
|
|
3326
|
+
Slim, muted, single line so the chat stays clean; the motion (pulsing ✳, crossfading verb, live
|
|
3327
|
+
counter) is what makes the wait feel fast instead of frozen. */
|
|
3328
|
+
.gemboss-ec-pending { display: inline-flex; align-items: center; gap: 8px; padding: 6px 2px; font-size: var(--gemboss-fs-caption); color: var(--gemboss-text-muted); }
|
|
3329
|
+
.gemboss-ec-pending-star { color: var(--gemboss-primary); font-size: 12px; line-height: 1; animation: gemboss-star-pulse 1.1s ease-in-out infinite; }
|
|
3330
|
+
.gemboss-ec-pending-verb { color: var(--gemboss-text); animation: gemboss-verb-in 0.4s ease-out; }
|
|
3331
|
+
.gemboss-ec-pending-meta { font-variant-numeric: tabular-nums; opacity: 0.7; font-size: var(--gemboss-fs-caption); }
|
|
3332
|
+
@keyframes gemboss-star-pulse { 0%,100% { transform: scale(0.85); opacity: 0.55; } 50% { transform: scale(1.15); opacity: 1; } }
|
|
3333
|
+
@keyframes gemboss-verb-in { from { opacity: 0; transform: translateY(2px); } to { opacity: 1; transform: none; } }
|
|
3334
|
+
/* Typewriter cursor — a thin blinking bar trailing the answer while tokens stream in. */
|
|
3335
|
+
.gemboss-ec-cursor { display: inline-block; width: 2px; height: 1.05em; margin-left: 2px; vertical-align: -2px; background: var(--gemboss-primary); border-radius: 2px; animation: gemboss-cursor-blink 1s steps(1) infinite; }
|
|
3336
|
+
@keyframes gemboss-cursor-blink { 0%,45% { opacity: 1; } 50%,95% { opacity: 0.15; } 100% { opacity: 1; } }
|
|
3337
|
+
@media (prefers-reduced-motion: reduce) {
|
|
3338
|
+
.gemboss-ec-pending-star, .gemboss-ec-cursor { animation: none; }
|
|
3339
|
+
}
|
|
3340
|
+
/* Markdown link inside the answer bubble — a friendly inline link, never a raw path. */
|
|
3341
|
+
.gemboss-ec-md-link { color: var(--gemboss-primary-dark); font-weight: 600; text-decoration: none; border-bottom: 1px solid color-mix(in srgb, var(--gemboss-primary) 35%, transparent); cursor: pointer; }
|
|
3342
|
+
.gemboss-ec-md-link:hover { border-bottom-color: var(--gemboss-primary); }
|
|
3343
|
+
/* Suggestion starters = compact wrapping CHIPS (pills), not stacked full-width boxes —
|
|
3344
|
+
reads as a chat's quick-reply chips and stops one prompt eating a whole row. */
|
|
3345
|
+
.gemboss-ec-starters { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 4px; }
|
|
3346
|
+
.gemboss-ec-starter {
|
|
3347
|
+
text-align: left; font-size: 13px; padding: 6px 12px; border-radius: var(--gemboss-radius-full); cursor: pointer;
|
|
3348
|
+
background: var(--gemboss-surface); border: 1px solid var(--gemboss-divider); color: var(--gemboss-text);
|
|
3349
|
+
white-space: nowrap; max-width: 100%;
|
|
3350
|
+
}
|
|
3351
|
+
.gemboss-ec-starter:hover { border-color: var(--gemboss-primary); background: rgba(86, 97, 245, 0.06); }
|
|
3352
|
+
/* (removed) dead legacy rail styles — `.icon-nav-assistant` + the hover-flyout submenu
|
|
3353
|
+
(`.icon-nav-flyout*`). The current nav (IconNav.tsx) is a persistent 240px labeled
|
|
3354
|
+
sidebar using `.gnav-*` classes with route-driven sub-nav; there is no 72px icon rail
|
|
3355
|
+
and no hover flyout, so these selectors matched nothing. */
|
|
3356
|
+
|
|
3357
|
+
/* Composer = ONE bordered rounded shell (ChatGPT/Claude-style): the textarea sits borderless
|
|
3358
|
+
inside it and the attach (📎) + send (↑) buttons float within the same box — cleaner than the
|
|
3359
|
+
old three-separate-boxes row. The shell is inset from the panel edges (margin) and lifts its
|
|
3360
|
+
border to primary on focus-within, so the whole composer reads as one focused field. */
|
|
3361
|
+
/* TWO ROWS when there are chips (ChatComposer): the textarea on its own full-width row, the toolbar
|
|
3362
|
+
under it. The toolbar is where chips of any length live, and when they shared a row with the text
|
|
3363
|
+
box the send button was the thing that gave way — measured 22/09/2026 at 320px, it wrapped to a
|
|
3364
|
+
THIRD line. With no chips at all, ONE row — see `:has` below.
|
|
3365
|
+
8px inside, not 6: at 6 the text sat 13px from the border and the box read as cramped. What the
|
|
3366
|
+
toolbar SAYS at each width is ChatComposer's job (data-density), measured rather than guessed. */
|
|
3367
|
+
.gemboss-ec-input {
|
|
3368
|
+
display: flex; flex-wrap: wrap; align-items: center; gap: 4px;
|
|
3369
|
+
margin: 12px; padding: 8px;
|
|
3370
|
+
border: 1px solid var(--gemboss-divider); border-radius: 16px;
|
|
3371
|
+
background: var(--gemboss-surface);
|
|
3372
|
+
transition: border-color var(--gemboss-transition-fast) ease, box-shadow var(--gemboss-transition-fast) ease;
|
|
3373
|
+
}
|
|
3374
|
+
.gemboss-ec-input:focus-within {
|
|
3375
|
+
border-color: var(--gemboss-primary);
|
|
3376
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--gemboss-primary) 12%, transparent);
|
|
3377
|
+
}
|
|
3378
|
+
.gemboss-ec-input textarea {
|
|
3379
|
+
flex: 1 0 100%; min-width: 0; width: 100%; box-sizing: border-box; resize: none; border-radius: 0; background: transparent;
|
|
3380
|
+
/* The CHIP's box model — a transparent 1px border and 8px of padding — so the first letter typed
|
|
3381
|
+
lines up with the first letter of "Claude Code" below it. It was 6px with no border: the text
|
|
3382
|
+
started 3px left of the chips, measured on every host at every width, and the box read as two
|
|
3383
|
+
columns. (Box model rather than nine pixels of padding: nine is off the spacing scale the CI guard holds.) */
|
|
3384
|
+
border: 1px solid transparent;
|
|
3385
|
+
padding: 6px 8px; font-size: var(--gemboss-fs-body); font-family: inherit;
|
|
3386
|
+
/* Starts at one line; GemChat's auto-grow JS expands it up to ~3 lines (max-height),
|
|
3387
|
+
then scrolls inside (ChatGPT/Gemini-style). overflow-y is hidden until the JS flips
|
|
3388
|
+
it to auto past the cap, so a 1–2 line message never shows a (rounding) scrollbar. */
|
|
3389
|
+
min-height: 0; max-height: 84px; line-height: 1.5; overflow-y: hidden;
|
|
3390
|
+
scrollbar-width: thin; scrollbar-color: rgba(31,30,29,0.22) transparent; /* thin, not the OS bar */
|
|
3391
|
+
}
|
|
3392
|
+
.gemboss-ec-input textarea::-webkit-scrollbar { width: 7px; }
|
|
3393
|
+
.gemboss-ec-input textarea::-webkit-scrollbar-track { background: transparent; }
|
|
3394
|
+
.gemboss-ec-input textarea::-webkit-scrollbar-thumb { background: rgba(31,30,29,0.2); border-radius: 4px; border: 2px solid transparent; background-clip: content-box; }
|
|
3395
|
+
.gemboss-ec-input textarea:focus { outline: none; }
|
|
3396
|
+
/* Send button — a filled primary circle floating at the shell's bottom-right. inline-flex centers
|
|
3397
|
+
the ↑ SVG (the old rule had no flex-centering, so the icon sat high).
|
|
3398
|
+
|
|
3399
|
+
SCOPED TO THE SUBMIT BUTTON, and that word is load-bearing. This rule used to read
|
|
3400
|
+
`.gemboss-ec-input button`, which reached EVERY button in the composer — including whatever a host
|
|
3401
|
+
passes in through `composerLeading`, a slot that exists precisely so hosts can put their own
|
|
3402
|
+
controls here. The 📎 attach button already had to fight it back with a class of its own (see the
|
|
3403
|
+
comment above `.gemboss-ec-attach-btn`), which was the first sign that this selector owns more than
|
|
3404
|
+
it should. The second was the engine picker: a Polaris Button, rendered into the slot, came out as a
|
|
3405
|
+
34px black circle with its label ("Claude Code · Opus") clipped to three characters. Nothing was
|
|
3406
|
+
wrong with the picker — this rule was painting it.
|
|
3407
|
+
|
|
3408
|
+
§9.3: one visual owner per component. The composer owns its own send button and nothing that is
|
|
3409
|
+
handed to it. Guarded by composer-owns-only-its-own-buttons.test.ts.
|
|
3410
|
+
|
|
3411
|
+
44px, not 34: it is a CONTROL (§9.1, Apple HIG / WCAG 2.5.5 AAA). This branch had already raised
|
|
3412
|
+
it — but on the OLD unscoped selector, the very rule main had just narrowed. The two changes were
|
|
3413
|
+
fixing different faults in one declaration, so both are kept: main's scope, this branch's size. */
|
|
3414
|
+
.gemboss-ec-input button[type="submit"] {
|
|
3415
|
+
flex: 0 0 auto; width: 44px; height: 44px; min-width: 44px; min-height: 44px; border-radius: 50%; border: 0; cursor: pointer;
|
|
3416
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
3417
|
+
background: var(--gemboss-primary); color: #fff; font-size: 16px; font-weight: 700;
|
|
3418
|
+
transition: background var(--gemboss-transition-fast) ease, filter var(--gemboss-transition-fast) ease;
|
|
3419
|
+
}
|
|
3420
|
+
.gemboss-ec-input button[type="submit"]:hover:not(:disabled) { background: var(--gemboss-primary-dark); }
|
|
3421
|
+
.gemboss-ec-input button[type="submit"]:active:not(:disabled) { background: var(--gemboss-primary-dark); filter: brightness(0.94); }
|
|
3422
|
+
.gemboss-ec-input button[type="submit"]:focus-visible { outline: 2px solid var(--gemboss-primary); outline-offset: 2px; }
|
|
3423
|
+
/* Disabled (empty input): a clear neutral grey circle with a muted arrow — reads as
|
|
3424
|
+
"inactive" rather than a washed-out faint-primary blob. */
|
|
3425
|
+
.gemboss-ec-input button[type="submit"]:disabled { background: var(--gemboss-divider); color: var(--gemboss-text-muted); opacity: 1; cursor: default; }
|
|
3426
|
+
/* ── Narration timeline ── */
|
|
3427
|
+
.gemboss-narr { list-style: none; margin: 0; padding: 8px 0 0; display: flex; flex-direction: column; gap: 8px; }
|
|
3428
|
+
.gemboss-narr-row { display: flex; align-items: center; gap: 8px; font-size: var(--gemboss-fs-caption); color: var(--gemboss-text-muted); }
|
|
3429
|
+
.gemboss-narr-dot { width: 16px; height: 16px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; font-size: 10px; flex: 0 0 auto; }
|
|
3430
|
+
.gemboss-narr-row.ok .gemboss-narr-dot { background: var(--gemboss-active-bg); color: var(--gemboss-active-fg); }
|
|
3431
|
+
.gemboss-narr-row.failed .gemboss-narr-dot { background: #fde8e6; color: var(--gemboss-danger); }
|
|
3432
|
+
.gemboss-narr-row.running .gemboss-narr-dot { background: transparent; }
|
|
3433
|
+
/* THE ENGINE'S MARK, in the dot's slot. No pill behind it: the ✓/✕ dots earn their background by
|
|
3434
|
+
being a glyph in a circle, and a logo in a coloured circle reads as a badge the engine awarded
|
|
3435
|
+
itself. It takes the ROW's colour, so a failed turn's mark goes red with everything else — a mark
|
|
3436
|
+
that stayed brand-coloured over a failure would be the one cheerful thing on a broken row. */
|
|
3437
|
+
.gemboss-narr-dot-engine { background: transparent !important; color: inherit; }
|
|
3438
|
+
.gemboss-narr-row.ok .gemboss-narr-dot-engine { color: var(--gemboss-text); }
|
|
3439
|
+
.gemboss-narr-row.failed .gemboss-narr-dot-engine { color: var(--gemboss-danger); }
|
|
3440
|
+
.gemboss-narr-row.ok .gemboss-narr-text, .gemboss-narr-row.failed .gemboss-narr-text { color: var(--gemboss-text); }
|
|
3441
|
+
/* A message the seller typed INTO the running turn. It reads as THEIR line, not as a step the agent
|
|
3442
|
+
took: full-strength text (the timeline is muted by default) against the page's own text colour, and
|
|
3443
|
+
a `❯` prompt caret instead of a status dot. It must be legible at a glance while the agent is still
|
|
3444
|
+
writing above it — a seller scanning a long timeline needs to find the moment they interrupted. */
|
|
3445
|
+
.gemboss-narr-row.steer .gemboss-narr-dot { background: transparent; color: var(--gemboss-text-muted); }
|
|
3446
|
+
/* `anywhere`, not `normal`: a seller correcting a turn pastes URLs and product handles, and one
|
|
3447
|
+
unbroken 60-character token in a 320px chat column is a row that pushes the panel sideways. Measured
|
|
3448
|
+
2026-09-23 — the default `overflow-wrap: normal` on this row does not break such a token. */
|
|
3449
|
+
.gemboss-narr-steer { color: var(--gemboss-text); font-weight: 500; overflow-wrap: anywhere; }
|
|
3450
|
+
.gemboss-narr-spin { width: 11px; height: 11px; border: 2px solid var(--gemboss-divider-soft); border-top-color: var(--gemboss-primary); border-radius: 50%; animation: gemboss-spin 0.7s linear infinite; }
|
|
3451
|
+
@keyframes gemboss-spin { to { transform: rotate(360deg); } }
|
|
3452
|
+
/* Live-reasoning ("thinking") row: muted + italic while streaming, collapsible once settled. */
|
|
3453
|
+
.gemboss-narr-row.thinking { align-items: flex-start; }
|
|
3454
|
+
.gemboss-narr-row.thinking .gemboss-narr-dot { color: var(--gemboss-text-muted); background: transparent; }
|
|
3455
|
+
.gemboss-narr-thinking-live { font-style: italic; color: var(--gemboss-text-muted); white-space: pre-wrap; opacity: 0.9; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
|
|
3456
|
+
.gemboss-narr-thinking { font-size: var(--gemboss-fs-caption); }
|
|
3457
|
+
.gemboss-narr-thinking > summary { cursor: pointer; color: var(--gemboss-text-muted); list-style: none; user-select: none; }
|
|
3458
|
+
.gemboss-narr-thinking > summary::-webkit-details-marker { display: none; }
|
|
3459
|
+
.gemboss-narr-thinking > summary::before { content: "▸ "; }
|
|
3460
|
+
.gemboss-narr-thinking[open] > summary::before { content: "▾ "; }
|
|
3461
|
+
.gemboss-narr-thinking-body { margin-top: 6px; padding: 8px 8px; border-left: 2px solid var(--gemboss-divider-soft); color: var(--gemboss-text-muted); font-style: italic; white-space: pre-wrap; line-height: 1.5; }
|
|
3462
|
+
/* TOOL TRANSCRIPT rows — the Claude-Code shape: a monospace call line, its return value folded
|
|
3463
|
+
behind a disclosure. Muted on purpose: the transcript is evidence you can audit, not the answer. */
|
|
3464
|
+
.gemboss-narr-row.trace code { font-family: var(--gemboss-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); font-size: var(--gemboss-fs-caption); word-break: break-word; }
|
|
3465
|
+
.gemboss-narr-trace-call { color: var(--gemboss-text-muted); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
|
3466
|
+
.gemboss-narr-trace { font-size: var(--gemboss-fs-caption); min-width: 0; }
|
|
3467
|
+
.gemboss-narr-trace > summary { cursor: pointer; color: var(--gemboss-text-muted); list-style: none; user-select: none;
|
|
3468
|
+
/* A tool call with real arguments is long and the chat panel is narrow: a full payload wrapped to five
|
|
3469
|
+
lines buries the rows around it. Clamp while closed; opening the row shows the call in full. */
|
|
3470
|
+
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
|
3471
|
+
.gemboss-narr-trace[open] > summary { display: block; overflow: visible; }
|
|
3472
|
+
.gemboss-narr-trace > summary::-webkit-details-marker { display: none; }
|
|
3473
|
+
/* The fold is now the ONLY way to see a payload or a finished thought (both start closed), so its
|
|
3474
|
+
summary is a target: ≥24px tall, the list-link tier of Gems/CLAUDE.md §9.1 (WCAG 2.5.8). Measured
|
|
3475
|
+
21px before, 13/09. */
|
|
3476
|
+
.gemboss-narr-trace > summary, .gemboss-narr-thinking > summary { min-height: 24px; box-sizing: border-box; padding-block: 2px; }
|
|
3477
|
+
.gemboss-narr-trace > summary::before { content: "▸ "; }
|
|
3478
|
+
.gemboss-narr-trace[open] > summary::before { content: "▾ "; }
|
|
3479
|
+
.gemboss-narr-trace-body { margin-top: 6px; padding: 8px; border-left: 2px solid var(--gemboss-divider-soft); color: var(--gemboss-text-muted); white-space: pre-wrap; line-height: 1.5; overflow-x: auto; }
|
|
3480
|
+
@media (max-width: 640px) {
|
|
3481
|
+
/* Composer auto-grows from one line (see GemChat) — no fixed floor needed here. */
|
|
3482
|
+
.gemboss-ec-input textarea { min-height: 0; }
|
|
3483
|
+
.gemboss-ec-scroll { padding: 12px; }
|
|
3484
|
+
}
|
|
3485
|
+
/* ── Chat option chips (persona/goal polling) ── */
|
|
3486
|
+
.gemboss-ec-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 4px; }
|
|
3487
|
+
/* 44px because a chip is a CONTROL, not a link in a list — Apple HIG / WCAG 2.5.5 AAA, Gems CLAUDE.md
|
|
3488
|
+
§9.1. Measured 2026-09-09 at 390px and 1366px: padding alone rendered these at 34px, ten short, and
|
|
3489
|
+
they are the only way to answer a poll or resume an unfinished run on a phone. `min-height` rather
|
|
3490
|
+
than `height` so a wrapped two-line label still grows instead of clipping. */
|
|
3491
|
+
.gemboss-ec-chip {
|
|
3492
|
+
background: var(--gemboss-surface); border: 1px solid var(--gemboss-divider); border-radius: var(--gemboss-radius-full);
|
|
3493
|
+
padding: 8px 16px; font-size: 13px; cursor: pointer; color: var(--gemboss-text);
|
|
3494
|
+
min-height: 44px;
|
|
3495
|
+
}
|
|
3496
|
+
.gemboss-ec-chip:hover { border-color: var(--gemboss-primary); color: var(--gemboss-active-fg); }
|
|
3497
|
+
.gemboss-ec-chip:disabled { opacity: 0.5; cursor: default; }
|
|
3498
|
+
/* ════ Mouse targets — the same 44px, bought with an overlay instead of a bigger box ═══════
|
|
3499
|
+
§9.1 asks 44px of a CONTROL, and it does not say "on touch only". The chat panel's controls are
|
|
3500
|
+
28-37px under a mouse, which clears WCAG 2.5.8 AA (24px) and not 2.5.5 AAA (44px). The dense look
|
|
3501
|
+
is deliberate and worth keeping, so the TARGET grows and the ink does not — the same trick the
|
|
3502
|
+
editor topbar already uses on its device buttons.
|
|
3503
|
+
|
|
3504
|
+
WHICH ONES, AND WHY NOT THE REST. A target that reaches into a neighbour steals clicks meant for
|
|
3505
|
+
it, which is worse than a small one. So this was decided by measuring the gap between every pair
|
|
3506
|
+
of vertically adjacent controls at 390 / 768 / 1366 and asking whether BOTH could grow into it —
|
|
3507
|
+
a control's own clearance is not enough, because its neighbour is growing too. Measured
|
|
3508
|
+
2026-09-09, three pairs could not:
|
|
3509
|
+
|
|
3510
|
+
starter chips, row to row gap 8px, both need 7 -> the ROW GAP goes to 16px below, which buys
|
|
3511
|
+
it without changing how the chips look
|
|
3512
|
+
textarea / engine picker gap 8px, needs 10 -> the textarea does not grow (see below)
|
|
3513
|
+
textarea / send gap 6px, needs 9 -> same
|
|
3514
|
+
|
|
3515
|
+
THE MESSAGE BOX IS EXCLUDED ON PURPOSE. It is a full-width field 37px tall: its AREA is already
|
|
3516
|
+
enormous, the only dimension short of 44 is its height, and it is the one control here you reach
|
|
3517
|
+
by clicking INTO text rather than at a point. Growing it also collides with both of its
|
|
3518
|
+
neighbours. With it left alone, everything else fits: send needs 5 of the 6px above it, the
|
|
3519
|
+
picker needs 8 of 8.
|
|
3520
|
+
|
|
3521
|
+
Height only. Horizontally these controls are already 34-196px wide, and an overlay that also grew
|
|
3522
|
+
sideways would collide with the chip beside it, which is 8px away. */
|
|
3523
|
+
@media (pointer: fine) {
|
|
3524
|
+
.gemboss-ec-starters { row-gap: 16px; }
|
|
3525
|
+
.gemboss-ec-tab,
|
|
3526
|
+
.gemboss-ec-starter,
|
|
3527
|
+
.gemboss-ec-upload,
|
|
3528
|
+
.gemboss-ec-enginepick button,
|
|
3529
|
+
.gemboss-ec-workmode,
|
|
3530
|
+
.gemboss-ec-input .gemboss-ec-attach-btn,
|
|
3531
|
+
.gemboss-ec-stop,
|
|
3532
|
+
/* Shares the toolbar row with Stop, which is already 44px tall — so a 44px overlay here grows into
|
|
3533
|
+
a row that is that tall already, and reaches into nothing above or below it. */
|
|
3534
|
+
.gemboss-ec-stop-send,
|
|
3535
|
+
.gemboss-ec-input button[type="submit"] { position: relative; }
|
|
3536
|
+
.gemboss-ec-tab::after,
|
|
3537
|
+
.gemboss-ec-starter::after,
|
|
3538
|
+
.gemboss-ec-upload::after,
|
|
3539
|
+
.gemboss-ec-enginepick button::after,
|
|
3540
|
+
/* `::before` on the work-mode wrapper: its `::after` is the invisible label that SIZES it, and a
|
|
3541
|
+
second use of the same pseudo-element would silently replace that. */
|
|
3542
|
+
.gemboss-ec-workmode::before,
|
|
3543
|
+
.gemboss-ec-input .gemboss-ec-attach-btn::after,
|
|
3544
|
+
.gemboss-ec-stop::after,
|
|
3545
|
+
.gemboss-ec-stop-send::after,
|
|
3546
|
+
.gemboss-ec-input button[type="submit"]::after {
|
|
3547
|
+
content: "";
|
|
3548
|
+
position: absolute;
|
|
3549
|
+
left: 0; right: 0;
|
|
3550
|
+
top: 50%;
|
|
3551
|
+
transform: translateY(-50%);
|
|
3552
|
+
height: 44px;
|
|
3553
|
+
/* No background at all: a pseudo-element is transparent already, and the `background` shorthand
|
|
3554
|
+
is banned in this family because it resets `background-image` — the work-mode caret. My own
|
|
3555
|
+
guard from #2158 caught this line, which is the guard doing exactly what it was written for. */
|
|
3556
|
+
pointer-events: auto;
|
|
3557
|
+
/* Above the panel chrome it overhangs — the chrome is not interactive, so a click landing on the
|
|
3558
|
+
control the overlay belongs to is the point.
|
|
3559
|
+
This does NOT rescue a control sitting flush against the top of `.gemboss-ec-scroll`: an
|
|
3560
|
+
overflow container clips its absolutely-positioned descendants, and z-index cannot cross that.
|
|
3561
|
+
Measured at 768px, where the first row of starter chips begins at the exact pixel the header
|
|
3562
|
+
ends: their target is 37px until the list scrolls, and 44px after. A scroll position, not a
|
|
3563
|
+
layout constant — the same chips measure 44px at 390, 1366 and 1920. Left alone rather than
|
|
3564
|
+
bought with padding on the scroll area, which every seller would see all the time to fix a
|
|
3565
|
+
target in one transient position. */
|
|
3566
|
+
z-index: 1;
|
|
3567
|
+
}
|
|
3568
|
+
}
|
|
3569
|
+
@media (pointer: coarse) {
|
|
3570
|
+
.gemboss-ec-input button[type="submit"] { width: 44px; height: 44px; font-size: 17px; }
|
|
3571
|
+
.gemboss-ec-starter, .gemboss-ec-opt { min-height: 44px; }
|
|
3572
|
+
.gemboss-ec-input textarea { min-height: 44px; }
|
|
3573
|
+
}
|
|
3574
|
+
/* ── The answer bubble's markdown ────────────────────────────────────────────────────────────────
|
|
3575
|
+
*
|
|
3576
|
+
* The bubble understood bold, links and line breaks. The models writing into it do not restrict
|
|
3577
|
+
* themselves to three things: a question about pages came back as a markdown TABLE and rendered as
|
|
3578
|
+
* literal pipes down the page. These are the styles for what it actually writes. */
|
|
3579
|
+
|
|
3580
|
+
.gemboss-md-p { margin: 0 0 8px; }
|
|
3581
|
+
.gemboss-md-p:last-child { margin-bottom: 0; }
|
|
3582
|
+
/* Demoted one level in the markup (h1 → h3) because the bubble sits under the page's own headings.
|
|
3583
|
+
* The SIZE hierarchy is kept here, because that is what a reader uses to skim. */
|
|
3584
|
+
.gemboss-md-h { margin: 16px 0 6px; font-weight: 650; line-height: 1.3; }
|
|
3585
|
+
.gemboss-md-h:first-child { margin-top: 0; }
|
|
3586
|
+
/* RESTORED after #3370 deleted these three. MarkdownLite.tsx builds the class by interpolation —
|
|
3587
|
+
* `gemboss-md-h gemboss-md-h${b.level}` — so the literal `gemboss-md-h1` exists in no source file
|
|
3588
|
+
* and in no compiled chunk, and all three of that PR's detection passes read them as dead. The gate
|
|
3589
|
+
* now carries the prefix rule that catches this shape. Do not "clean" these up. */
|
|
3590
|
+
.gemboss-md-h1 { font-size: 17px; }
|
|
3591
|
+
.gemboss-md-h2 { font-size: 15px; }
|
|
3592
|
+
.gemboss-md-h3 { font-size: var(--gemboss-fs-body); color: var(--gemboss-text-subdued, #616161); }
|
|
3593
|
+
.gemboss-md-list { margin: 0 0 8px; padding-left: 20px; }
|
|
3594
|
+
.gemboss-md-list li { margin: 2px 0; }
|
|
3595
|
+
.gemboss-md-quote {
|
|
3596
|
+
margin: 0 0 8px;
|
|
3597
|
+
padding: 2px 0 2px 12px;
|
|
3598
|
+
border-left: 3px solid var(--gemboss-border, #e3e3e3);
|
|
3599
|
+
color: var(--gemboss-text-subdued, #616161);
|
|
3600
|
+
}
|
|
3601
|
+
.gemboss-md-rule { margin: 12px 0; border: 0; border-top: 1px solid var(--gemboss-border, #e3e3e3); }
|
|
3602
|
+
/* Inline code: a chip, so an identifier stops reading as prose. */
|
|
3603
|
+
.gemboss-md-code {
|
|
3604
|
+
padding: 1px 6px;
|
|
3605
|
+
border-radius: 6px;
|
|
3606
|
+
background: var(--gemboss-surface-subdued, #f1f1f1);
|
|
3607
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
3608
|
+
font-size: 0.9em;
|
|
3609
|
+
word-break: break-word;
|
|
3610
|
+
}
|
|
3611
|
+
/* Tables scroll INSIDE their own box. The chat lives in a 380px sidebar in the theme editor, and a
|
|
3612
|
+
* five-column table there would otherwise push the whole panel sideways — §9.1's rule that wide
|
|
3613
|
+
* content scrolls in its own container, not the page. */
|
|
3614
|
+
.gemboss-md-tablewrap { margin: 0 0 12px; overflow-x: auto; }
|
|
3615
|
+
.gemboss-md-table { border-collapse: collapse; font-size: var(--gemboss-fs-caption); min-width: 100%; }
|
|
3616
|
+
.gemboss-md-table th,
|
|
3617
|
+
.gemboss-md-table td {
|
|
3618
|
+
border: 1px solid var(--gemboss-border, #e3e3e3);
|
|
3619
|
+
padding: 6px 8px;
|
|
3620
|
+
text-align: left;
|
|
3621
|
+
vertical-align: top;
|
|
3622
|
+
white-space: nowrap;
|
|
3623
|
+
}
|
|
3624
|
+
.gemboss-md-table th { background: var(--gemboss-surface-subdued, #f7f7f7); font-weight: 600; }
|
|
3625
|
+
/* A picture in an answer (only from hosts markdown-blocks.ts trusts). Never wider than the bubble — the
|
|
3626
|
+
* chat can sit in a 380px sidebar — and capped in height so five thumbnails read as a shortlist, not
|
|
3627
|
+
* five screens. The link around it is the tap target: keyboard focus has to be visible on it. */
|
|
3628
|
+
.gemboss-md-imglink { display: inline-block; max-width: 100%; margin: 4px 0 8px; border-radius: 8px; line-height: 0; }
|
|
3629
|
+
.gemboss-md-imglink:focus-visible { outline: 2px solid var(--gemboss-focus, #005bd3); outline-offset: 2px; }
|
|
3630
|
+
.gemboss-md-img {
|
|
3631
|
+
display: block;
|
|
3632
|
+
max-width: 100%;
|
|
3633
|
+
max-height: 320px;
|
|
3634
|
+
height: auto;
|
|
3635
|
+
border: 1px solid var(--gemboss-border, #e3e3e3);
|
|
3636
|
+
border-radius: 8px;
|
|
3637
|
+
background: var(--gemboss-surface-subdued, #f7f7f7);
|
|
3638
|
+
object-fit: contain;
|
|
3639
|
+
}
|
|
3640
|
+
/* A code block, with the fold. A 200-line answer should not push everything after it off screen. */
|
|
3641
|
+
.gemboss-md-codeblock {
|
|
3642
|
+
position: relative;
|
|
3643
|
+
margin: 0 0 12px;
|
|
3644
|
+
border: 1px solid var(--gemboss-border, #e3e3e3);
|
|
3645
|
+
border-radius: 8px;
|
|
3646
|
+
background: var(--gemboss-surface-subdued, #f7f7f7);
|
|
3647
|
+
overflow: hidden;
|
|
3648
|
+
}
|
|
3649
|
+
.gemboss-md-codelang {
|
|
3650
|
+
display: block;
|
|
3651
|
+
padding: 4px 12px 0;
|
|
3652
|
+
color: var(--gemboss-text-subdued, #8a8a8a);
|
|
3653
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
3654
|
+
font-size: 11px;
|
|
3655
|
+
}
|
|
3656
|
+
.gemboss-md-codeblock pre { margin: 0; padding: 8px 12px; overflow-x: auto; }
|
|
3657
|
+
.gemboss-md-codeblock code {
|
|
3658
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
3659
|
+
font-size: 12px;
|
|
3660
|
+
line-height: 1.5;
|
|
3661
|
+
white-space: pre;
|
|
3662
|
+
}
|
|
3663
|
+
.gemboss-md-codemore {
|
|
3664
|
+
display: block;
|
|
3665
|
+
width: 100%;
|
|
3666
|
+
padding: 6px 12px;
|
|
3667
|
+
border: 0;
|
|
3668
|
+
border-top: 1px solid var(--gemboss-border, #e3e3e3);
|
|
3669
|
+
background: transparent;
|
|
3670
|
+
color: var(--gemboss-text-subdued, #616161);
|
|
3671
|
+
font-size: 12px;
|
|
3672
|
+
text-align: left;
|
|
3673
|
+
cursor: pointer;
|
|
3674
|
+
}
|
|
3675
|
+
.gemboss-md-codemore:hover { background: rgba(0, 0, 0, 0.03); }
|
|
3676
|
+
/* ── The payload, live, in the chat ──────────────────────────────────────────────────────────────
|
|
3677
|
+
A trace row said `apply_section_edit(section_id:"hero")` — that something happened, and nothing about
|
|
3678
|
+
what. This is what. Open by default: folding it would put the answer back behind a click. */
|
|
3679
|
+
.gemboss-narr-row.source { align-items: flex-start; }
|
|
3680
|
+
.gemboss-narr-source { min-width: 0; flex: 1; }
|
|
3681
|
+
.gemboss-narr-source-head { font-size: 11px; color: var(--gemboss-text-muted, #686b71); margin-bottom: 4px; }
|
|
3682
|
+
.gemboss-narr-source-head code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
3683
|
+
.gemboss-narr-source-code {
|
|
3684
|
+
margin: 0; padding: 8px 12px; border-radius: 8px; max-height: 220px; overflow: auto;
|
|
3685
|
+
background: var(--gemboss-primary-soft, #f2f3f5); border: 1px solid var(--gemboss-divider, #e5e7ea);
|
|
3686
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: var(--gemboss-fs-micro); line-height: 17px;
|
|
3687
|
+
white-space: pre; tab-size: 2;
|
|
3688
|
+
}
|
|
3689
|
+
/* Light-ground syntax colours for the payload rows (the code-highlight.ts `c-*` spans). Contrast checked
|
|
3690
|
+
against #fff: every one clears 4.5:1. (The page editor's Code tab shared them until it was removed, #2651.) */
|
|
3691
|
+
.gemboss-narr-source-code .c-com { color: #6a7a86; font-style: italic; }
|
|
3692
|
+
.gemboss-narr-source-code .c-str { color: #9a3412; }
|
|
3693
|
+
.gemboss-narr-source-code .c-num { color: #1d6fa5; }
|
|
3694
|
+
.gemboss-narr-source-code .c-kw { color: #7c3aed; font-weight: 600; }
|
|
3695
|
+
.gemboss-narr-source-code .c-key { color: #0b6b3a; }
|
|
3696
|
+
.gemboss-narr-source-code .c-tag { color: #b0197a; }
|
|
3697
|
+
.gemboss-narr-source-code .c-attr{ color: #7c3aed; }
|
|
3698
|
+
.gemboss-narr-source-code .c-pun { color: #6a6d73; }
|
|
3699
|
+
@media (max-width: 520px) {
|
|
3700
|
+
.gemboss-narr-source-code { max-height: 160px; font-size: 11px; }
|
|
3701
|
+
}
|
|
3702
|
+
/* A checklist row's box IS its marker; the dot column stays for alignment and shows nothing. */
|
|
3703
|
+
.gemboss-narr-dot-quiet { visibility: hidden; }
|
|
3704
|
+
|
|
3705
|
+
/* ═══ @gemboss/ui src/chat-composer.css ═══ */
|
|
3706
|
+
/*
|
|
3707
|
+
* The chat COMPOSER — @gemboss/ui (moved from apps/admin/src/components/chat/chat-composer.css, 02/10/2026).
|
|
3708
|
+
*
|
|
3709
|
+
* The toolbar row under the text box (chips lead, tools, status, the one action pinned right), the one-row
|
|
3710
|
+
* layout when there are no chips, the engine picker's layout and the Stop button. A second surface needs the
|
|
3711
|
+
* toolbar — gemcare's assistant composer is a text box and a send button, i.e. the one-row case — and a copy
|
|
3712
|
+
* would be a second owner. The box itself (`form.gemboss-ec-input`, its textarea and send button) is in chat.css.
|
|
3713
|
+
*
|
|
3714
|
+
* A SEPARATE file from chat.css on purpose, as it was a separate file in the admin: chat.css rides the shell's
|
|
3715
|
+
* first load on every screen, while this sheet follows the chat chunk — the admin imports it from ChatComposer
|
|
3716
|
+
* and EnginePicker, not from the layout (the shell sat at 239.1 / 240 KB brotli when this moved). Other surfaces
|
|
3717
|
+
* get it inside gemboss-ui.css. Rules and their order are unchanged.
|
|
3718
|
+
*
|
|
3719
|
+
* Every selector here names a composer-only class (chiplabel · chipmark · chipsub · enginepick · enginepick-btn ·
|
|
3720
|
+
* stop · toolbar · toolbar-lead · toolbar-tools), so a rule only matches while a composer is on screen.
|
|
3721
|
+
*/
|
|
3722
|
+
|
|
3723
|
+
/* The toolbar row: chips on the left, then status, then the one action button pinned right.
|
|
3724
|
+
`align-items: center` — the old row was `flex-end`, so 28px chips sat on the BOTTOM edge of a 44px
|
|
3725
|
+
button and their centres ran 8px below its centre. Measured at 1366: chips cy 819, send cy 811.
|
|
3726
|
+
The lead group is the only thing that shrinks. Send is `flex: none` and the chips inside the lead
|
|
3727
|
+
carry `min-width: 0`, so a narrow composer ellipsises a label instead of pushing send off the row. */
|
|
3728
|
+
.gemboss-ec-toolbar { flex: 1 0 100%; display: flex; align-items: center; gap: 4px; min-width: 0; }
|
|
3729
|
+
.gemboss-ec-toolbar-lead { flex: 1 1 auto; display: flex; align-items: center; gap: 4px; min-width: 0; }
|
|
3730
|
+
/* The lead GROWS, and that is what pins status + action to the right edge. With nothing in it (a
|
|
3731
|
+
browser tab has no engine picker, a driver without work mode has no select) it is removed, and
|
|
3732
|
+
whatever follows takes the auto margin instead. */
|
|
3733
|
+
.gemboss-ec-toolbar-lead:empty { display: none; }
|
|
3734
|
+
.gemboss-ec-toolbar-lead:empty + * { margin-left: auto; }
|
|
3735
|
+
/* TOOLS ARE NOT CHIPS. The 📎 lives here, never in the lead: the composer gate reads `leading` as the
|
|
3736
|
+
chip strip twice over — `edge` wants the first chip's text flush with the typed text, and `oneRow`
|
|
3737
|
+
only runs when the lead is EMPTY. A button parked in the lead pushed the chips 47px right AND left
|
|
3738
|
+
`oneRow` unable to fail at all; the gate's own negative control said so out loud
|
|
3739
|
+
("MÙ với oneRow — đối chứng ĐÃ QUAN SÁT ĐƯỢC và vẫn xanh") once the breakage stopped registering.
|
|
3740
|
+
`flex: none` so it never steals the shrink budget the lead needs to ellipsise a long chip label. */
|
|
3741
|
+
.gemboss-ec-toolbar-tools { display: flex; align-items: center; gap: 4px; flex: none; min-width: 0; }
|
|
3742
|
+
.gemboss-ec-toolbar-tools:empty { display: none; }
|
|
3743
|
+
/* NO CHIPS ⇒ ONE ROW. Product Studio has nothing in its lead, and it still spent a whole 44px row on
|
|
3744
|
+
a lone send button beside empty space — 99-120px tall for a one-line box. The text box shares the
|
|
3745
|
+
row now and send sits at its bottom edge, where a growing message keeps it. */
|
|
3746
|
+
.gemboss-ec-input:has(> .gemboss-ec-toolbar > .gemboss-ec-toolbar-lead:empty) { flex-wrap: nowrap; align-items: flex-end; }
|
|
3747
|
+
.gemboss-ec-input:has(> .gemboss-ec-toolbar > .gemboss-ec-toolbar-lead:empty) > textarea { flex: 1 1 0; }
|
|
3748
|
+
.gemboss-ec-input:has(> .gemboss-ec-toolbar > .gemboss-ec-toolbar-lead:empty) > .gemboss-ec-toolbar { flex: 0 0 auto; }
|
|
3749
|
+
/* The engine picker is a `gemboss-ec-chipctl` like the select beside it — its colour, weight, padding
|
|
3750
|
+
and hover all come from that one rule. What is left here is layout: Popover wraps the activator in an
|
|
3751
|
+
element of its own, and the chip can only shrink if every box between it and the toolbar can.
|
|
3752
|
+
DESCENDANT, not child, for that reason — `>` matched nothing when the button sat inside Polaris'
|
|
3753
|
+
wrapper, measured, after the first attempt left it unstyled. */
|
|
3754
|
+
.gemboss-ec-enginepick { flex: 0 3 auto; min-width: 0; display: inline-flex; }
|
|
3755
|
+
.gemboss-ec-enginepick > * { min-width: 0; max-width: 100%; display: flex; }
|
|
3756
|
+
.gemboss-ec-enginepick button {
|
|
3757
|
+
min-width: 0; max-width: 100%;
|
|
3758
|
+
/* The same caret the work-mode select draws, at the same inset, so the two chips read as one type. */
|
|
3759
|
+
padding-right: 24px;
|
|
3760
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='currentColor' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
|
3761
|
+
background-repeat: no-repeat;
|
|
3762
|
+
background-position: right 7px center;
|
|
3763
|
+
}
|
|
3764
|
+
.gemboss-ec-chiplabel { display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
3765
|
+
.gemboss-ec-chipmark { display: none; }
|
|
3766
|
+
.gemboss-ec-chipmark .gemboss-engine-mark { display: block; width: 16px; height: 16px; margin: 2px 0; }
|
|
3767
|
+
/* The steps ChatComposer measured its way down to (data-density on the form). The ellipsis above stays
|
|
3768
|
+
as the floor for an engine with no mark, never as the plan. */
|
|
3769
|
+
.gemboss-ec-input:not([data-density="full"]) .gemboss-ec-chipsub { display: none; }
|
|
3770
|
+
.gemboss-ec-input:is([data-density="icon"], [data-density="tight"]) .gemboss-ec-enginepick-btn.has-mark .gemboss-ec-chiplabel { display: none; }
|
|
3771
|
+
.gemboss-ec-input:is([data-density="icon"], [data-density="tight"]) .gemboss-ec-enginepick-btn.has-mark .gemboss-ec-chipmark { display: block; }
|
|
3772
|
+
/* A mark does not ellipsise, so once the chip IS the mark it holds its width — shrunk further it slid
|
|
3773
|
+
under the mode chip beside it. The overflow that leaves is what tells ChatComposer to step on. */
|
|
3774
|
+
.gemboss-ec-input:is([data-density="icon"], [data-density="tight"]) .gemboss-ec-enginepick:has(.has-mark) { flex-shrink: 0; }
|
|
3775
|
+
/* Open: it is showing a popover, so it stops being quiet. */
|
|
3776
|
+
.gemboss-ec-enginepick button[aria-expanded="true"] {
|
|
3777
|
+
color: var(--gemboss-text); background-color: var(--gemboss-hover, rgba(0,0,0,0.05));
|
|
3778
|
+
}
|
|
3779
|
+
@media (pointer: coarse) {
|
|
3780
|
+
/* SCOPED, and that word is load-bearing — this line was DEAD as `.gemboss-ec-stop`.
|
|
3781
|
+
A media query adds no specificity, so the unscoped rule for the same class, declared ~1800 lines
|
|
3782
|
+
LATER at its mouse size, beat this one on source order alone. Measured
|
|
3783
|
+
2026-09-09 in a real coarse-pointer context: `matchMedia('(pointer: coarse)').matches === true`
|
|
3784
|
+
and the computed width unchanged at its mouse size. Every phone got a Stop button ten pixels
|
|
3785
|
+
under the target, while the rule meant to
|
|
3786
|
+
fix that sat here reading as done — and the source test beside this block could not see it,
|
|
3787
|
+
because a name being PRESENT is not the same fact as a rule WINNING.
|
|
3788
|
+
`.gemboss-ec-input` is the same idiom the attach button above already uses for the same reason. */
|
|
3789
|
+
.gemboss-ec-input .gemboss-ec-stop { width: 44px; height: 44px; min-width: 44px; }
|
|
3790
|
+
/* SCOPED for the reason above: this control's own look is declared later, in the shell sheet. */
|
|
3791
|
+
.gemboss-ec-input .gemboss-ec-stop-send { min-height: 44px; }
|
|
3792
|
+
.gemboss-ec-enginepick button { min-height: 44px; }
|
|
3793
|
+
}
|
|
3794
|
+
/* Sized like the send button it replaces, so the composer does not reflow when a turn starts —
|
|
3795
|
+
* a control that moves under the cursor is a control people misclick. So it is THE SAME CIRCLE AS SEND
|
|
3796
|
+
* — same 44px, same 50% radius, only the colour and the glyph change. It used to be a 32px red
|
|
3797
|
+
* rounded square beside a 44px round send: two shapes, two sizes, one slot. */
|
|
3798
|
+
.gemboss-ec-stop {
|
|
3799
|
+
flex: 0 0 auto;
|
|
3800
|
+
display: inline-flex;
|
|
3801
|
+
align-items: center;
|
|
3802
|
+
justify-content: center;
|
|
3803
|
+
width: 44px;
|
|
3804
|
+
height: 44px;
|
|
3805
|
+
min-width: 44px;
|
|
3806
|
+
border: none;
|
|
3807
|
+
border-radius: 50%;
|
|
3808
|
+
background: var(--gemboss-critical, #d72c0d);
|
|
3809
|
+
color: #fff;
|
|
3810
|
+
cursor: pointer;
|
|
3042
3811
|
}
|
|
3812
|
+
.gemboss-ec-stop svg { width: 12px; height: 12px; fill: currentColor; }
|
|
3813
|
+
.gemboss-ec-stop:disabled { opacity: 0.6; cursor: default; }
|
|
3814
|
+
.gemboss-ec-stop:hover { filter: brightness(1.08); }
|
|
3815
|
+
.gemboss-ec-stop:focus-visible { outline: 2px solid var(--gemboss-focus, #005bd3); outline-offset: 2px; }
|