@gemboss/ui 0.2.1 → 0.4.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 +89 -6
- package/catalog/index.html +338 -10
- package/gemboss-ui.css +794 -3
- package/package.json +5 -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/kit-plain.css +161 -0
- package/src/kit-plain.js +179 -0
package/src/chat.css
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The chat BODY — what an answer looks like, wherever a GemBoss surface puts a chat (01/10/2026).
|
|
3
|
+
*
|
|
4
|
+
* Chris: every surface's AI assistant (gemcare, content-radar, partner-radar, GemCommunity) must be built from the
|
|
5
|
+
* GemBoss UI framework. Until today only the FRAME was shared (assistant.css: popup / bar / full screen; shell.css:
|
|
6
|
+
* the thread rail); the body lived in the admin's gemboss-shell.css and the exporter left it out on purpose, so every
|
|
7
|
+
* surface drew its own bubbles, markdown and composer. These are those rules, moved here VERBATIM (same selectors,
|
|
8
|
+
* same values, same order) — the admin @imports this file at the top of its shell, so it is still the one owner.
|
|
9
|
+
*
|
|
10
|
+
* What is here: the header (`.gemboss-ec-head/title/sub`), the message list (`.gemboss-ec-scroll`), messages and
|
|
11
|
+
* bubbles (`.gemboss-ec-msg.user|.assistant`, `.gemboss-ec-bubble`), the working indicator and the streaming cursor
|
|
12
|
+
* (`.gemboss-ec-pending*`, `.gemboss-ec-cursor`), markdown (`.gemboss-md-*`, `.gemboss-ec-md-link`), step narration
|
|
13
|
+
* (`.gemboss-narr*`), starters and option chips (`.gemboss-ec-starter(s)`, `.gemboss-ec-chip(s)`), and the composer
|
|
14
|
+
* (`.gemboss-ec-input` with its textarea and submit button).
|
|
15
|
+
* What is NOT: the panel's geometry (`.gemboss-ec` width, editor columns, tabs), attachments, CTA / action cards,
|
|
16
|
+
* the engine picker and work mode — those are still the admin's, and a surface brings its own. ONE exception, on
|
|
17
|
+
* purpose: the chat panel's touch / mouse TARGET blocks (`@media (pointer: coarse|fine)` below) name every control
|
|
18
|
+
* of the panel, the admin-only ones (tabs, upload, engine picker, work mode, attach, stop) included. They are one
|
|
19
|
+
* rule per pointer kind; split in two they wrote their declarations twice and the admin's shell budget had no room.
|
|
20
|
+
* Those selectors match nothing on a surface without those controls. (`.gemboss-ec-workmode`: the click-area
|
|
21
|
+
* `::before` is here, its label-sizing `::after` stays the admin's.)
|
|
22
|
+
*
|
|
23
|
+
* Markup: see the catalog's "Thân chat" section. Behaviour (streaming, stop, keyboard) belongs to the surface.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
.gemboss-ec-head { padding: 16px 16px; border-bottom: 1px solid var(--gemboss-divider); display: flex; flex-direction: column; gap: 2px; }
|
|
27
|
+
.gemboss-ec-title { font-family: var(--gemboss-serif); font-size: 15px; font-weight: 600; }
|
|
28
|
+
.gemboss-ec-sub { font-size: 12px; color: var(--gemboss-text-muted); }
|
|
29
|
+
.gemboss-ec-scroll { flex: 1; min-height: 0; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 16px; scrollbar-gutter: stable; }
|
|
30
|
+
/* Visible thin scrollbar on the chat message list — the default macOS overlay bar is
|
|
31
|
+
hidden until you actively scroll, so the list reads as "can't scroll" (esp. on the
|
|
32
|
+
dedicated Assistant page, a fixed-height chat where the page itself doesn't scroll).
|
|
33
|
+
A persistent thin bar makes the scroll affordance obvious; matches the composer input. */
|
|
34
|
+
.gemboss-ec-scroll { scrollbar-width: thin; scrollbar-color: rgba(31,30,29,0.22) transparent; }
|
|
35
|
+
.gemboss-ec-scroll::-webkit-scrollbar { width: 9px; }
|
|
36
|
+
.gemboss-ec-scroll::-webkit-scrollbar-track { background: transparent; }
|
|
37
|
+
.gemboss-ec-scroll::-webkit-scrollbar-thumb { background: rgba(31,30,29,0.2); border-radius: 6px; border: 2px solid transparent; background-clip: content-box; }
|
|
38
|
+
.gemboss-ec-msg { display: flex; flex-direction: column; gap: 8px; max-width: 100%; }
|
|
39
|
+
.gemboss-ec-msg.user { align-items: flex-end; }
|
|
40
|
+
.gemboss-ec-msg.assistant { align-items: flex-start; }
|
|
41
|
+
.gemboss-ec-bubble {
|
|
42
|
+
/* Cap reading measure: a chat reply stays at a readable line length, not the bubble's full 90%. */
|
|
43
|
+
font-size: var(--gemboss-fs-body); line-height: 1.5; padding: 12px 12px; border-radius: 16px; max-width: min(90%, 68ch);
|
|
44
|
+
white-space: pre-wrap; word-break: break-word;
|
|
45
|
+
}
|
|
46
|
+
.gemboss-ec-msg.user .gemboss-ec-bubble { background: var(--gemboss-primary); color: #fff; border-bottom-right-radius: 4px; }
|
|
47
|
+
.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; }
|
|
48
|
+
.gemboss-ec-input button[type="submit"] svg { width: 18px; height: 18px; fill: currentColor; }
|
|
49
|
+
/* Live "working" heartbeat — the Claude-Code-style running asterisk shown before the first token.
|
|
50
|
+
Slim, muted, single line so the chat stays clean; the motion (pulsing ✳, crossfading verb, live
|
|
51
|
+
counter) is what makes the wait feel fast instead of frozen. */
|
|
52
|
+
.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); }
|
|
53
|
+
.gemboss-ec-pending-star { color: var(--gemboss-primary); font-size: 12px; line-height: 1; animation: gemboss-star-pulse 1.1s ease-in-out infinite; }
|
|
54
|
+
.gemboss-ec-pending-verb { color: var(--gemboss-text); animation: gemboss-verb-in 0.4s ease-out; }
|
|
55
|
+
.gemboss-ec-pending-meta { font-variant-numeric: tabular-nums; opacity: 0.7; font-size: var(--gemboss-fs-caption); }
|
|
56
|
+
@keyframes gemboss-star-pulse { 0%,100% { transform: scale(0.85); opacity: 0.55; } 50% { transform: scale(1.15); opacity: 1; } }
|
|
57
|
+
@keyframes gemboss-verb-in { from { opacity: 0; transform: translateY(2px); } to { opacity: 1; transform: none; } }
|
|
58
|
+
/* Typewriter cursor — a thin blinking bar trailing the answer while tokens stream in. */
|
|
59
|
+
.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; }
|
|
60
|
+
@keyframes gemboss-cursor-blink { 0%,45% { opacity: 1; } 50%,95% { opacity: 0.15; } 100% { opacity: 1; } }
|
|
61
|
+
@media (prefers-reduced-motion: reduce) {
|
|
62
|
+
.gemboss-ec-pending-star, .gemboss-ec-cursor { animation: none; }
|
|
63
|
+
}
|
|
64
|
+
/* Markdown link inside the answer bubble — a friendly inline link, never a raw path. */
|
|
65
|
+
.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; }
|
|
66
|
+
.gemboss-ec-md-link:hover { border-bottom-color: var(--gemboss-primary); }
|
|
67
|
+
/* Suggestion starters = compact wrapping CHIPS (pills), not stacked full-width boxes —
|
|
68
|
+
reads as a chat's quick-reply chips and stops one prompt eating a whole row. */
|
|
69
|
+
.gemboss-ec-starters { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 4px; }
|
|
70
|
+
.gemboss-ec-starter {
|
|
71
|
+
text-align: left; font-size: 13px; padding: 6px 12px; border-radius: var(--gemboss-radius-full); cursor: pointer;
|
|
72
|
+
background: var(--gemboss-surface); border: 1px solid var(--gemboss-divider); color: var(--gemboss-text);
|
|
73
|
+
white-space: nowrap; max-width: 100%;
|
|
74
|
+
}
|
|
75
|
+
.gemboss-ec-starter:hover { border-color: var(--gemboss-primary); background: rgba(86, 97, 245, 0.06); }
|
|
76
|
+
/* (removed) dead legacy rail styles — `.icon-nav-assistant` + the hover-flyout submenu
|
|
77
|
+
(`.icon-nav-flyout*`). The current nav (IconNav.tsx) is a persistent 240px labeled
|
|
78
|
+
sidebar using `.gnav-*` classes with route-driven sub-nav; there is no 72px icon rail
|
|
79
|
+
and no hover flyout, so these selectors matched nothing. */
|
|
80
|
+
|
|
81
|
+
/* Composer = ONE bordered rounded shell (ChatGPT/Claude-style): the textarea sits borderless
|
|
82
|
+
inside it and the attach (📎) + send (↑) buttons float within the same box — cleaner than the
|
|
83
|
+
old three-separate-boxes row. The shell is inset from the panel edges (margin) and lifts its
|
|
84
|
+
border to primary on focus-within, so the whole composer reads as one focused field. */
|
|
85
|
+
/* TWO ROWS when there are chips (ChatComposer): the textarea on its own full-width row, the toolbar
|
|
86
|
+
under it. The toolbar is where chips of any length live, and when they shared a row with the text
|
|
87
|
+
box the send button was the thing that gave way — measured 22/09/2026 at 320px, it wrapped to a
|
|
88
|
+
THIRD line. With no chips at all, ONE row — see `:has` below.
|
|
89
|
+
8px inside, not 6: at 6 the text sat 13px from the border and the box read as cramped. What the
|
|
90
|
+
toolbar SAYS at each width is ChatComposer's job (data-density), measured rather than guessed. */
|
|
91
|
+
.gemboss-ec-input {
|
|
92
|
+
display: flex; flex-wrap: wrap; align-items: center; gap: 4px;
|
|
93
|
+
margin: 12px; padding: 8px;
|
|
94
|
+
border: 1px solid var(--gemboss-divider); border-radius: 16px;
|
|
95
|
+
background: var(--gemboss-surface);
|
|
96
|
+
transition: border-color var(--gemboss-transition-fast) ease, box-shadow var(--gemboss-transition-fast) ease;
|
|
97
|
+
}
|
|
98
|
+
.gemboss-ec-input:focus-within {
|
|
99
|
+
border-color: var(--gemboss-primary);
|
|
100
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--gemboss-primary) 12%, transparent);
|
|
101
|
+
}
|
|
102
|
+
.gemboss-ec-input textarea {
|
|
103
|
+
flex: 1 0 100%; min-width: 0; width: 100%; box-sizing: border-box; resize: none; border-radius: 0; background: transparent;
|
|
104
|
+
/* The CHIP's box model — a transparent 1px border and 8px of padding — so the first letter typed
|
|
105
|
+
lines up with the first letter of "Claude Code" below it. It was 6px with no border: the text
|
|
106
|
+
started 3px left of the chips, measured on every host at every width, and the box read as two
|
|
107
|
+
columns. (Box model rather than nine pixels of padding: nine is off the spacing scale the CI guard holds.) */
|
|
108
|
+
border: 1px solid transparent;
|
|
109
|
+
padding: 6px 8px; font-size: var(--gemboss-fs-body); font-family: inherit;
|
|
110
|
+
/* Starts at one line; GemChat's auto-grow JS expands it up to ~3 lines (max-height),
|
|
111
|
+
then scrolls inside (ChatGPT/Gemini-style). overflow-y is hidden until the JS flips
|
|
112
|
+
it to auto past the cap, so a 1–2 line message never shows a (rounding) scrollbar. */
|
|
113
|
+
min-height: 0; max-height: 84px; line-height: 1.5; overflow-y: hidden;
|
|
114
|
+
scrollbar-width: thin; scrollbar-color: rgba(31,30,29,0.22) transparent; /* thin, not the OS bar */
|
|
115
|
+
}
|
|
116
|
+
.gemboss-ec-input textarea::-webkit-scrollbar { width: 7px; }
|
|
117
|
+
.gemboss-ec-input textarea::-webkit-scrollbar-track { background: transparent; }
|
|
118
|
+
.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; }
|
|
119
|
+
.gemboss-ec-input textarea:focus { outline: none; }
|
|
120
|
+
/* Send button — a filled primary circle floating at the shell's bottom-right. inline-flex centers
|
|
121
|
+
the ↑ SVG (the old rule had no flex-centering, so the icon sat high).
|
|
122
|
+
|
|
123
|
+
SCOPED TO THE SUBMIT BUTTON, and that word is load-bearing. This rule used to read
|
|
124
|
+
`.gemboss-ec-input button`, which reached EVERY button in the composer — including whatever a host
|
|
125
|
+
passes in through `composerLeading`, a slot that exists precisely so hosts can put their own
|
|
126
|
+
controls here. The 📎 attach button already had to fight it back with a class of its own (see the
|
|
127
|
+
comment above `.gemboss-ec-attach-btn`), which was the first sign that this selector owns more than
|
|
128
|
+
it should. The second was the engine picker: a Polaris Button, rendered into the slot, came out as a
|
|
129
|
+
34px black circle with its label ("Claude Code · Opus") clipped to three characters. Nothing was
|
|
130
|
+
wrong with the picker — this rule was painting it.
|
|
131
|
+
|
|
132
|
+
§9.3: one visual owner per component. The composer owns its own send button and nothing that is
|
|
133
|
+
handed to it. Guarded by composer-owns-only-its-own-buttons.test.ts.
|
|
134
|
+
|
|
135
|
+
44px, not 34: it is a CONTROL (§9.1, Apple HIG / WCAG 2.5.5 AAA). This branch had already raised
|
|
136
|
+
it — but on the OLD unscoped selector, the very rule main had just narrowed. The two changes were
|
|
137
|
+
fixing different faults in one declaration, so both are kept: main's scope, this branch's size. */
|
|
138
|
+
.gemboss-ec-input button[type="submit"] {
|
|
139
|
+
flex: 0 0 auto; width: 44px; height: 44px; min-width: 44px; min-height: 44px; border-radius: 50%; border: 0; cursor: pointer;
|
|
140
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
141
|
+
background: var(--gemboss-primary); color: #fff; font-size: 16px; font-weight: 700;
|
|
142
|
+
transition: background var(--gemboss-transition-fast) ease, filter var(--gemboss-transition-fast) ease;
|
|
143
|
+
}
|
|
144
|
+
.gemboss-ec-input button[type="submit"]:hover:not(:disabled) { background: var(--gemboss-primary-dark); }
|
|
145
|
+
.gemboss-ec-input button[type="submit"]:active:not(:disabled) { background: var(--gemboss-primary-dark); filter: brightness(0.94); }
|
|
146
|
+
.gemboss-ec-input button[type="submit"]:focus-visible { outline: 2px solid var(--gemboss-primary); outline-offset: 2px; }
|
|
147
|
+
/* Disabled (empty input): a clear neutral grey circle with a muted arrow — reads as
|
|
148
|
+
"inactive" rather than a washed-out faint-primary blob. */
|
|
149
|
+
.gemboss-ec-input button[type="submit"]:disabled { background: var(--gemboss-divider); color: var(--gemboss-text-muted); opacity: 1; cursor: default; }
|
|
150
|
+
/* ── Narration timeline ── */
|
|
151
|
+
.gemboss-narr { list-style: none; margin: 0; padding: 8px 0 0; display: flex; flex-direction: column; gap: 8px; }
|
|
152
|
+
.gemboss-narr-row { display: flex; align-items: center; gap: 8px; font-size: var(--gemboss-fs-caption); color: var(--gemboss-text-muted); }
|
|
153
|
+
.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; }
|
|
154
|
+
.gemboss-narr-row.ok .gemboss-narr-dot { background: var(--gemboss-active-bg); color: var(--gemboss-active-fg); }
|
|
155
|
+
.gemboss-narr-row.failed .gemboss-narr-dot { background: #fde8e6; color: var(--gemboss-danger); }
|
|
156
|
+
.gemboss-narr-row.running .gemboss-narr-dot { background: transparent; }
|
|
157
|
+
/* THE ENGINE'S MARK, in the dot's slot. No pill behind it: the ✓/✕ dots earn their background by
|
|
158
|
+
being a glyph in a circle, and a logo in a coloured circle reads as a badge the engine awarded
|
|
159
|
+
itself. It takes the ROW's colour, so a failed turn's mark goes red with everything else — a mark
|
|
160
|
+
that stayed brand-coloured over a failure would be the one cheerful thing on a broken row. */
|
|
161
|
+
.gemboss-narr-dot-engine { background: transparent !important; color: inherit; }
|
|
162
|
+
.gemboss-narr-row.ok .gemboss-narr-dot-engine { color: var(--gemboss-text); }
|
|
163
|
+
.gemboss-narr-row.failed .gemboss-narr-dot-engine { color: var(--gemboss-danger); }
|
|
164
|
+
.gemboss-narr-row.ok .gemboss-narr-text, .gemboss-narr-row.failed .gemboss-narr-text { color: var(--gemboss-text); }
|
|
165
|
+
/* A message the seller typed INTO the running turn. It reads as THEIR line, not as a step the agent
|
|
166
|
+
took: full-strength text (the timeline is muted by default) against the page's own text colour, and
|
|
167
|
+
a `❯` prompt caret instead of a status dot. It must be legible at a glance while the agent is still
|
|
168
|
+
writing above it — a seller scanning a long timeline needs to find the moment they interrupted. */
|
|
169
|
+
.gemboss-narr-row.steer .gemboss-narr-dot { background: transparent; color: var(--gemboss-text-muted); }
|
|
170
|
+
/* `anywhere`, not `normal`: a seller correcting a turn pastes URLs and product handles, and one
|
|
171
|
+
unbroken 60-character token in a 320px chat column is a row that pushes the panel sideways. Measured
|
|
172
|
+
2026-09-23 — the default `overflow-wrap: normal` on this row does not break such a token. */
|
|
173
|
+
.gemboss-narr-steer { color: var(--gemboss-text); font-weight: 500; overflow-wrap: anywhere; }
|
|
174
|
+
.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; }
|
|
175
|
+
@keyframes gemboss-spin { to { transform: rotate(360deg); } }
|
|
176
|
+
/* Live-reasoning ("thinking") row: muted + italic while streaming, collapsible once settled. */
|
|
177
|
+
.gemboss-narr-row.thinking { align-items: flex-start; }
|
|
178
|
+
.gemboss-narr-row.thinking .gemboss-narr-dot { color: var(--gemboss-text-muted); background: transparent; }
|
|
179
|
+
.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; }
|
|
180
|
+
.gemboss-narr-thinking { font-size: var(--gemboss-fs-caption); }
|
|
181
|
+
.gemboss-narr-thinking > summary { cursor: pointer; color: var(--gemboss-text-muted); list-style: none; user-select: none; }
|
|
182
|
+
.gemboss-narr-thinking > summary::-webkit-details-marker { display: none; }
|
|
183
|
+
.gemboss-narr-thinking > summary::before { content: "▸ "; }
|
|
184
|
+
.gemboss-narr-thinking[open] > summary::before { content: "▾ "; }
|
|
185
|
+
.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; }
|
|
186
|
+
/* TOOL TRANSCRIPT rows — the Claude-Code shape: a monospace call line, its return value folded
|
|
187
|
+
behind a disclosure. Muted on purpose: the transcript is evidence you can audit, not the answer. */
|
|
188
|
+
.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; }
|
|
189
|
+
.gemboss-narr-trace-call { color: var(--gemboss-text-muted); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
|
190
|
+
.gemboss-narr-trace { font-size: var(--gemboss-fs-caption); min-width: 0; }
|
|
191
|
+
.gemboss-narr-trace > summary { cursor: pointer; color: var(--gemboss-text-muted); list-style: none; user-select: none;
|
|
192
|
+
/* A tool call with real arguments is long and the chat panel is narrow: a full payload wrapped to five
|
|
193
|
+
lines buries the rows around it. Clamp while closed; opening the row shows the call in full. */
|
|
194
|
+
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
|
195
|
+
.gemboss-narr-trace[open] > summary { display: block; overflow: visible; }
|
|
196
|
+
.gemboss-narr-trace > summary::-webkit-details-marker { display: none; }
|
|
197
|
+
/* The fold is now the ONLY way to see a payload or a finished thought (both start closed), so its
|
|
198
|
+
summary is a target: ≥24px tall, the list-link tier of Gems/CLAUDE.md §9.1 (WCAG 2.5.8). Measured
|
|
199
|
+
21px before, 13/09. */
|
|
200
|
+
.gemboss-narr-trace > summary, .gemboss-narr-thinking > summary { min-height: 24px; box-sizing: border-box; padding-block: 2px; }
|
|
201
|
+
.gemboss-narr-trace > summary::before { content: "▸ "; }
|
|
202
|
+
.gemboss-narr-trace[open] > summary::before { content: "▾ "; }
|
|
203
|
+
.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; }
|
|
204
|
+
@media (max-width: 640px) {
|
|
205
|
+
/* Composer auto-grows from one line (see GemChat) — no fixed floor needed here. */
|
|
206
|
+
.gemboss-ec-input textarea { min-height: 0; }
|
|
207
|
+
.gemboss-ec-scroll { padding: 12px; }
|
|
208
|
+
}
|
|
209
|
+
/* ── Chat option chips (persona/goal polling) ── */
|
|
210
|
+
.gemboss-ec-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 4px; }
|
|
211
|
+
/* 44px because a chip is a CONTROL, not a link in a list — Apple HIG / WCAG 2.5.5 AAA, Gems CLAUDE.md
|
|
212
|
+
§9.1. Measured 2026-09-09 at 390px and 1366px: padding alone rendered these at 34px, ten short, and
|
|
213
|
+
they are the only way to answer a poll or resume an unfinished run on a phone. `min-height` rather
|
|
214
|
+
than `height` so a wrapped two-line label still grows instead of clipping. */
|
|
215
|
+
.gemboss-ec-chip {
|
|
216
|
+
background: var(--gemboss-surface); border: 1px solid var(--gemboss-divider); border-radius: var(--gemboss-radius-full);
|
|
217
|
+
padding: 8px 16px; font-size: 13px; cursor: pointer; color: var(--gemboss-text);
|
|
218
|
+
min-height: 44px;
|
|
219
|
+
}
|
|
220
|
+
.gemboss-ec-chip:hover { border-color: var(--gemboss-primary); color: var(--gemboss-active-fg); }
|
|
221
|
+
.gemboss-ec-chip:disabled { opacity: 0.5; cursor: default; }
|
|
222
|
+
/* ════ Mouse targets — the same 44px, bought with an overlay instead of a bigger box ═══════
|
|
223
|
+
§9.1 asks 44px of a CONTROL, and it does not say "on touch only". The chat panel's controls are
|
|
224
|
+
28-37px under a mouse, which clears WCAG 2.5.8 AA (24px) and not 2.5.5 AAA (44px). The dense look
|
|
225
|
+
is deliberate and worth keeping, so the TARGET grows and the ink does not — the same trick the
|
|
226
|
+
editor topbar already uses on its device buttons.
|
|
227
|
+
|
|
228
|
+
WHICH ONES, AND WHY NOT THE REST. A target that reaches into a neighbour steals clicks meant for
|
|
229
|
+
it, which is worse than a small one. So this was decided by measuring the gap between every pair
|
|
230
|
+
of vertically adjacent controls at 390 / 768 / 1366 and asking whether BOTH could grow into it —
|
|
231
|
+
a control's own clearance is not enough, because its neighbour is growing too. Measured
|
|
232
|
+
2026-09-09, three pairs could not:
|
|
233
|
+
|
|
234
|
+
starter chips, row to row gap 8px, both need 7 -> the ROW GAP goes to 16px below, which buys
|
|
235
|
+
it without changing how the chips look
|
|
236
|
+
textarea / engine picker gap 8px, needs 10 -> the textarea does not grow (see below)
|
|
237
|
+
textarea / send gap 6px, needs 9 -> same
|
|
238
|
+
|
|
239
|
+
THE MESSAGE BOX IS EXCLUDED ON PURPOSE. It is a full-width field 37px tall: its AREA is already
|
|
240
|
+
enormous, the only dimension short of 44 is its height, and it is the one control here you reach
|
|
241
|
+
by clicking INTO text rather than at a point. Growing it also collides with both of its
|
|
242
|
+
neighbours. With it left alone, everything else fits: send needs 5 of the 6px above it, the
|
|
243
|
+
picker needs 8 of 8.
|
|
244
|
+
|
|
245
|
+
Height only. Horizontally these controls are already 34-196px wide, and an overlay that also grew
|
|
246
|
+
sideways would collide with the chip beside it, which is 8px away. */
|
|
247
|
+
@media (pointer: fine) {
|
|
248
|
+
.gemboss-ec-starters { row-gap: 16px; }
|
|
249
|
+
.gemboss-ec-tab,
|
|
250
|
+
.gemboss-ec-starter,
|
|
251
|
+
.gemboss-ec-upload,
|
|
252
|
+
.gemboss-ec-enginepick button,
|
|
253
|
+
.gemboss-ec-workmode,
|
|
254
|
+
.gemboss-ec-input .gemboss-ec-attach-btn,
|
|
255
|
+
.gemboss-ec-stop,
|
|
256
|
+
/* Shares the toolbar row with Stop, which is already 44px tall — so a 44px overlay here grows into
|
|
257
|
+
a row that is that tall already, and reaches into nothing above or below it. */
|
|
258
|
+
.gemboss-ec-stop-send,
|
|
259
|
+
.gemboss-ec-input button[type="submit"] { position: relative; }
|
|
260
|
+
.gemboss-ec-tab::after,
|
|
261
|
+
.gemboss-ec-starter::after,
|
|
262
|
+
.gemboss-ec-upload::after,
|
|
263
|
+
.gemboss-ec-enginepick button::after,
|
|
264
|
+
/* `::before` on the work-mode wrapper: its `::after` is the invisible label that SIZES it, and a
|
|
265
|
+
second use of the same pseudo-element would silently replace that. */
|
|
266
|
+
.gemboss-ec-workmode::before,
|
|
267
|
+
.gemboss-ec-input .gemboss-ec-attach-btn::after,
|
|
268
|
+
.gemboss-ec-stop::after,
|
|
269
|
+
.gemboss-ec-stop-send::after,
|
|
270
|
+
.gemboss-ec-input button[type="submit"]::after {
|
|
271
|
+
content: "";
|
|
272
|
+
position: absolute;
|
|
273
|
+
left: 0; right: 0;
|
|
274
|
+
top: 50%;
|
|
275
|
+
transform: translateY(-50%);
|
|
276
|
+
height: 44px;
|
|
277
|
+
/* No background at all: a pseudo-element is transparent already, and the `background` shorthand
|
|
278
|
+
is banned in this family because it resets `background-image` — the work-mode caret. My own
|
|
279
|
+
guard from #2158 caught this line, which is the guard doing exactly what it was written for. */
|
|
280
|
+
pointer-events: auto;
|
|
281
|
+
/* Above the panel chrome it overhangs — the chrome is not interactive, so a click landing on the
|
|
282
|
+
control the overlay belongs to is the point.
|
|
283
|
+
This does NOT rescue a control sitting flush against the top of `.gemboss-ec-scroll`: an
|
|
284
|
+
overflow container clips its absolutely-positioned descendants, and z-index cannot cross that.
|
|
285
|
+
Measured at 768px, where the first row of starter chips begins at the exact pixel the header
|
|
286
|
+
ends: their target is 37px until the list scrolls, and 44px after. A scroll position, not a
|
|
287
|
+
layout constant — the same chips measure 44px at 390, 1366 and 1920. Left alone rather than
|
|
288
|
+
bought with padding on the scroll area, which every seller would see all the time to fix a
|
|
289
|
+
target in one transient position. */
|
|
290
|
+
z-index: 1;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
@media (pointer: coarse) {
|
|
294
|
+
.gemboss-ec-input button[type="submit"] { width: 44px; height: 44px; font-size: 17px; }
|
|
295
|
+
.gemboss-ec-starter, .gemboss-ec-opt { min-height: 44px; }
|
|
296
|
+
.gemboss-ec-input textarea { min-height: 44px; }
|
|
297
|
+
}
|
|
298
|
+
/* ── The answer bubble's markdown ────────────────────────────────────────────────────────────────
|
|
299
|
+
*
|
|
300
|
+
* The bubble understood bold, links and line breaks. The models writing into it do not restrict
|
|
301
|
+
* themselves to three things: a question about pages came back as a markdown TABLE and rendered as
|
|
302
|
+
* literal pipes down the page. These are the styles for what it actually writes. */
|
|
303
|
+
|
|
304
|
+
.gemboss-md-p { margin: 0 0 8px; }
|
|
305
|
+
.gemboss-md-p:last-child { margin-bottom: 0; }
|
|
306
|
+
/* Demoted one level in the markup (h1 → h3) because the bubble sits under the page's own headings.
|
|
307
|
+
* The SIZE hierarchy is kept here, because that is what a reader uses to skim. */
|
|
308
|
+
.gemboss-md-h { margin: 16px 0 6px; font-weight: 650; line-height: 1.3; }
|
|
309
|
+
.gemboss-md-h:first-child { margin-top: 0; }
|
|
310
|
+
/* RESTORED after #3370 deleted these three. MarkdownLite.tsx builds the class by interpolation —
|
|
311
|
+
* `gemboss-md-h gemboss-md-h${b.level}` — so the literal `gemboss-md-h1` exists in no source file
|
|
312
|
+
* and in no compiled chunk, and all three of that PR's detection passes read them as dead. The gate
|
|
313
|
+
* now carries the prefix rule that catches this shape. Do not "clean" these up. */
|
|
314
|
+
.gemboss-md-h1 { font-size: 17px; }
|
|
315
|
+
.gemboss-md-h2 { font-size: 15px; }
|
|
316
|
+
.gemboss-md-h3 { font-size: var(--gemboss-fs-body); color: var(--gemboss-text-subdued, #616161); }
|
|
317
|
+
.gemboss-md-list { margin: 0 0 8px; padding-left: 20px; }
|
|
318
|
+
.gemboss-md-list li { margin: 2px 0; }
|
|
319
|
+
.gemboss-md-quote {
|
|
320
|
+
margin: 0 0 8px;
|
|
321
|
+
padding: 2px 0 2px 12px;
|
|
322
|
+
border-left: 3px solid var(--gemboss-border, #e3e3e3);
|
|
323
|
+
color: var(--gemboss-text-subdued, #616161);
|
|
324
|
+
}
|
|
325
|
+
.gemboss-md-rule { margin: 12px 0; border: 0; border-top: 1px solid var(--gemboss-border, #e3e3e3); }
|
|
326
|
+
/* Inline code: a chip, so an identifier stops reading as prose. */
|
|
327
|
+
.gemboss-md-code {
|
|
328
|
+
padding: 1px 6px;
|
|
329
|
+
border-radius: 6px;
|
|
330
|
+
background: var(--gemboss-surface-subdued, #f1f1f1);
|
|
331
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
332
|
+
font-size: 0.9em;
|
|
333
|
+
word-break: break-word;
|
|
334
|
+
}
|
|
335
|
+
/* Tables scroll INSIDE their own box. The chat lives in a 380px sidebar in the theme editor, and a
|
|
336
|
+
* five-column table there would otherwise push the whole panel sideways — §9.1's rule that wide
|
|
337
|
+
* content scrolls in its own container, not the page. */
|
|
338
|
+
.gemboss-md-tablewrap { margin: 0 0 12px; overflow-x: auto; }
|
|
339
|
+
.gemboss-md-table { border-collapse: collapse; font-size: var(--gemboss-fs-caption); min-width: 100%; }
|
|
340
|
+
.gemboss-md-table th,
|
|
341
|
+
.gemboss-md-table td {
|
|
342
|
+
border: 1px solid var(--gemboss-border, #e3e3e3);
|
|
343
|
+
padding: 6px 8px;
|
|
344
|
+
text-align: left;
|
|
345
|
+
vertical-align: top;
|
|
346
|
+
white-space: nowrap;
|
|
347
|
+
}
|
|
348
|
+
.gemboss-md-table th { background: var(--gemboss-surface-subdued, #f7f7f7); font-weight: 600; }
|
|
349
|
+
/* A picture in an answer (only from hosts markdown-blocks.ts trusts). Never wider than the bubble — the
|
|
350
|
+
* chat can sit in a 380px sidebar — and capped in height so five thumbnails read as a shortlist, not
|
|
351
|
+
* five screens. The link around it is the tap target: keyboard focus has to be visible on it. */
|
|
352
|
+
.gemboss-md-imglink { display: inline-block; max-width: 100%; margin: 4px 0 8px; border-radius: 8px; line-height: 0; }
|
|
353
|
+
.gemboss-md-imglink:focus-visible { outline: 2px solid var(--gemboss-focus, #005bd3); outline-offset: 2px; }
|
|
354
|
+
.gemboss-md-img {
|
|
355
|
+
display: block;
|
|
356
|
+
max-width: 100%;
|
|
357
|
+
max-height: 320px;
|
|
358
|
+
height: auto;
|
|
359
|
+
border: 1px solid var(--gemboss-border, #e3e3e3);
|
|
360
|
+
border-radius: 8px;
|
|
361
|
+
background: var(--gemboss-surface-subdued, #f7f7f7);
|
|
362
|
+
object-fit: contain;
|
|
363
|
+
}
|
|
364
|
+
/* A code block, with the fold. A 200-line answer should not push everything after it off screen. */
|
|
365
|
+
.gemboss-md-codeblock {
|
|
366
|
+
position: relative;
|
|
367
|
+
margin: 0 0 12px;
|
|
368
|
+
border: 1px solid var(--gemboss-border, #e3e3e3);
|
|
369
|
+
border-radius: 8px;
|
|
370
|
+
background: var(--gemboss-surface-subdued, #f7f7f7);
|
|
371
|
+
overflow: hidden;
|
|
372
|
+
}
|
|
373
|
+
.gemboss-md-codelang {
|
|
374
|
+
display: block;
|
|
375
|
+
padding: 4px 12px 0;
|
|
376
|
+
color: var(--gemboss-text-subdued, #8a8a8a);
|
|
377
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
378
|
+
font-size: 11px;
|
|
379
|
+
}
|
|
380
|
+
.gemboss-md-codeblock pre { margin: 0; padding: 8px 12px; overflow-x: auto; }
|
|
381
|
+
.gemboss-md-codeblock code {
|
|
382
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
383
|
+
font-size: 12px;
|
|
384
|
+
line-height: 1.5;
|
|
385
|
+
white-space: pre;
|
|
386
|
+
}
|
|
387
|
+
.gemboss-md-codemore {
|
|
388
|
+
display: block;
|
|
389
|
+
width: 100%;
|
|
390
|
+
padding: 6px 12px;
|
|
391
|
+
border: 0;
|
|
392
|
+
border-top: 1px solid var(--gemboss-border, #e3e3e3);
|
|
393
|
+
background: transparent;
|
|
394
|
+
color: var(--gemboss-text-subdued, #616161);
|
|
395
|
+
font-size: 12px;
|
|
396
|
+
text-align: left;
|
|
397
|
+
cursor: pointer;
|
|
398
|
+
}
|
|
399
|
+
.gemboss-md-codemore:hover { background: rgba(0, 0, 0, 0.03); }
|
|
400
|
+
/* ── The payload, live, in the chat ──────────────────────────────────────────────────────────────
|
|
401
|
+
A trace row said `apply_section_edit(section_id:"hero")` — that something happened, and nothing about
|
|
402
|
+
what. This is what. Open by default: folding it would put the answer back behind a click. */
|
|
403
|
+
.gemboss-narr-row.source { align-items: flex-start; }
|
|
404
|
+
.gemboss-narr-source { min-width: 0; flex: 1; }
|
|
405
|
+
.gemboss-narr-source-head { font-size: 11px; color: var(--gemboss-text-muted, #686b71); margin-bottom: 4px; }
|
|
406
|
+
.gemboss-narr-source-head code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
407
|
+
.gemboss-narr-source-code {
|
|
408
|
+
margin: 0; padding: 8px 12px; border-radius: 8px; max-height: 220px; overflow: auto;
|
|
409
|
+
background: var(--gemboss-primary-soft, #f2f3f5); border: 1px solid var(--gemboss-divider, #e5e7ea);
|
|
410
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: var(--gemboss-fs-micro); line-height: 17px;
|
|
411
|
+
white-space: pre; tab-size: 2;
|
|
412
|
+
}
|
|
413
|
+
/* Light-ground syntax colours for the payload rows (the code-highlight.ts `c-*` spans). Contrast checked
|
|
414
|
+
against #fff: every one clears 4.5:1. (The page editor's Code tab shared them until it was removed, #2651.) */
|
|
415
|
+
.gemboss-narr-source-code .c-com { color: #6a7a86; font-style: italic; }
|
|
416
|
+
.gemboss-narr-source-code .c-str { color: #9a3412; }
|
|
417
|
+
.gemboss-narr-source-code .c-num { color: #1d6fa5; }
|
|
418
|
+
.gemboss-narr-source-code .c-kw { color: #7c3aed; font-weight: 600; }
|
|
419
|
+
.gemboss-narr-source-code .c-key { color: #0b6b3a; }
|
|
420
|
+
.gemboss-narr-source-code .c-tag { color: #b0197a; }
|
|
421
|
+
.gemboss-narr-source-code .c-attr{ color: #7c3aed; }
|
|
422
|
+
.gemboss-narr-source-code .c-pun { color: #6a6d73; }
|
|
423
|
+
@media (max-width: 520px) {
|
|
424
|
+
.gemboss-narr-source-code { max-height: 160px; font-size: 11px; }
|
|
425
|
+
}
|
|
426
|
+
/* A checklist row's box IS its marker; the dot column stays for alignment and shows nothing. */
|
|
427
|
+
.gemboss-narr-dot-quiet { visibility: hidden; }
|
package/src/kit-plain.css
CHANGED
|
@@ -53,3 +53,164 @@
|
|
|
53
53
|
@media (pointer: coarse) { .ui-btn--icon:not(.ui-btn--sm) { min-width: 44px; } }
|
|
54
54
|
/* …and at phone WIDTH, as gemboss-field does: a desktop browser at 320px still reports pointer: fine. */
|
|
55
55
|
@media (max-width: 47.9975em) { .ui-btn--icon:not(.ui-btn--sm) { min-width: 44px; min-height: 44px; } }
|
|
56
|
+
|
|
57
|
+
/* ── Link · Media card (02/10/2026) ──────────────────────────────────────────
|
|
58
|
+
GemFactory, gemcare and gempress left text links in the browser's default blue-and-purple, and each drew its own
|
|
59
|
+
thumbnail card. The admin uses Polaris' Link and has no media card, so both live here, not in ui.css.
|
|
60
|
+
|
|
61
|
+
Link: `<a class="ui-link" href>` for a link inside text. Iris (--gemboss-accent-strong, 6.2:1 on white), underlined
|
|
62
|
+
so it is not told by colour alone (WCAG 1.4.1); visited looks the same (these are tools, not articles). Markdown or
|
|
63
|
+
other generated HTML you cannot add classes to: wrap it in `.ui-prose` and its bare `<a>` get the same look.
|
|
64
|
+
|
|
65
|
+
Media card: `<article class="ui-card ui-card--media"><a|button class="ui-card__media"><img alt=""></…>
|
|
66
|
+
<div class="ui-card__body">…</div><div class="ui-card__foot">…</div></article>`. The picture is 16:10 and
|
|
67
|
+
cropped to fill; the card has no padding of its own, the body and foot do. */
|
|
68
|
+
.ui-link,
|
|
69
|
+
.ui-prose a:not([class]) {
|
|
70
|
+
color: var(--gemboss-accent-strong, #3a44de);
|
|
71
|
+
text-decoration: underline;
|
|
72
|
+
text-decoration-thickness: from-font;
|
|
73
|
+
text-underline-offset: 2px;
|
|
74
|
+
}
|
|
75
|
+
.ui-link:visited,
|
|
76
|
+
.ui-prose a:not([class]):visited { color: var(--gemboss-accent-strong, #3a44de); }
|
|
77
|
+
.ui-link:hover,
|
|
78
|
+
.ui-prose a:not([class]):hover { color: var(--gemboss-accent, #5661f5); text-decoration-thickness: 2px; }
|
|
79
|
+
.ui-link:focus-visible,
|
|
80
|
+
.ui-prose a:not([class]):focus-visible { outline: 2px solid var(--ui-focus, #005bd3); outline-offset: 2px; border-radius: 2px; }
|
|
81
|
+
|
|
82
|
+
.ui-card--media { padding: 0; overflow: hidden; display: flex; flex-direction: column; }
|
|
83
|
+
.ui-card__media {
|
|
84
|
+
display: block; width: 100%; aspect-ratio: 16 / 10; margin: 0; padding: 0; border: 0;
|
|
85
|
+
border-bottom: 1px solid var(--ui-border); background: var(--ui-surface-2); overflow: hidden; color: inherit;
|
|
86
|
+
}
|
|
87
|
+
.ui-card__media > img { display: block; width: 100%; height: 100%; object-fit: cover; }
|
|
88
|
+
button.ui-card__media, a.ui-card__media { cursor: pointer; }
|
|
89
|
+
.ui-card__media:focus-visible { outline: 2px solid var(--ui-focus, #005bd3); outline-offset: -2px; }
|
|
90
|
+
/* justify-items: start: a tag or a button in the body keeps its own width instead of stretching across the card. */
|
|
91
|
+
.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; }
|
|
92
|
+
.ui-card__foot {
|
|
93
|
+
display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--ui-space-2, 8px);
|
|
94
|
+
margin-top: auto; padding: 0 var(--ui-space-4, 16px) var(--ui-space-3, 12px);
|
|
95
|
+
color: var(--ui-text-muted); font-size: var(--ui-fs-caption, 13px);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* Popup body and foot: `<div class="ui-modal__body">…</div><div class="ui-modal__foot"><button class="ui-btn">…</button></div>`.
|
|
99
|
+
The admin's Modal.tsx pads its own children, so ui.css never styled these; every non-React surface padded them by
|
|
100
|
+
hand (GemFactory: 16/20 and a hairline above the actions). kit-plain.js's openModal writes exactly this shape. */
|
|
101
|
+
.ui-modal__body { padding: 16px 20px; }
|
|
102
|
+
.ui-modal__foot {
|
|
103
|
+
display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end;
|
|
104
|
+
padding: 12px 20px; border-top: 1px solid var(--ui-border);
|
|
105
|
+
}
|
|
106
|
+
/* A popup taller than the screen: the body scrolls, the head and foot stay, so the actions are always reachable
|
|
107
|
+
(320×568 with a long form put Huỷ/Xoá below the fold, unreachable under the locked page; review #3633). */
|
|
108
|
+
.ui-overlay > .ui-modal { display: flex; flex-direction: column; max-height: calc(100dvh - 40px); }
|
|
109
|
+
.ui-modal > .ui-modal__head, .ui-modal > .ui-modal__foot { flex: none; }
|
|
110
|
+
.ui-modal > .ui-modal__body { flex: 1 1 auto; min-height: 0; overflow: auto; }
|
|
111
|
+
|
|
112
|
+
/* ── Báo cáo: delta · KPI lớn · thanh ngang · biểu đồ cột · chú thích · phễu · lưới · tiêu đề mục ─────────────────
|
|
113
|
+
Thêm 03/10/2026 cho trang số của gemnews (gemnews.ops.gemboss.ai/admin): nó tự vẽ thanh ngang, biểu đồ theo ngày và
|
|
114
|
+
lưới hai cột trong admin.css riêng, và bề mặt thứ hai cần báo cáo sẽ chép lại y như vậy. README "promote what a second
|
|
115
|
+
surface needs". Chỉ CSS: số liệu và tỉ lệ do bề mặt tính, kit chỉ vẽ.
|
|
116
|
+
|
|
117
|
+
Màu chuỗi: `--ui-series-1..5`, theo thứ tự dùng. 1 là iris thương hiệu; các màu sau đủ khác nhau cả với người mù màu
|
|
118
|
+
đỏ-lục (không đặt đỏ cạnh lục). Đặt màu cho một chuỗi bằng `style="--c:var(--ui-series-2)"` trên đúng phần tử. */
|
|
119
|
+
:root {
|
|
120
|
+
--ui-series-1: var(--gemboss-accent, #5661f5);
|
|
121
|
+
--ui-series-2: #0f9f8f;
|
|
122
|
+
--ui-series-3: #d9822b;
|
|
123
|
+
--ui-series-4: #c2417a;
|
|
124
|
+
--ui-series-5: #6b7280;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* Delta: `<span class="ui-delta ui-delta--good"><span aria-hidden="true">↑</span> 12%</span>`. Tốt / xấu là của BỀ MẶT
|
|
128
|
+
quyết (lượt đọc tăng là tốt, tỉ lệ thoát tăng là xấu), mũi tên là hướng. Không tông = trung tính (không đổi, hoặc
|
|
129
|
+
"chưa có kỳ trước"). Luôn kèm chữ cho trình đọc màn hình nếu mũi tên là thứ duy nhất nói hướng. */
|
|
130
|
+
.ui-delta { display: inline-flex; align-items: baseline; gap: 2px; font-size: var(--gemboss-fs-caption); font-weight: 600; color: var(--ui-text-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
131
|
+
.ui-delta--good { color: var(--ui-success-fg); }
|
|
132
|
+
.ui-delta--bad { color: var(--ui-danger-fg); }
|
|
133
|
+
|
|
134
|
+
/* KPI lớn trong lưới đều: `<div class="ui-metrics ui-metrics--grid"><div class="ui-metric ui-metric--lg">…`. Lưới
|
|
135
|
+
auto-fit chia đều hàng, nên 4 ô không bao giờ thành 3 + 1 mồ côi ở khổ rộng; ≤ 520px luôn hai cột. `__foot` là dòng
|
|
136
|
+
dưới cùng (delta + so với gì). */
|
|
137
|
+
.ui-metrics--grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr)); gap: var(--ui-space-3); margin: 0; }
|
|
138
|
+
.ui-metric--lg { gap: 4px; padding: var(--ui-space-3) var(--ui-space-4); }
|
|
139
|
+
.ui-metric--lg .ui-metric__val { font-size: var(--gemboss-fs-h1, 24px); line-height: 1.1; }
|
|
140
|
+
.ui-metric--lg .ui-metric__lbl { order: -1; font-size: var(--gemboss-fs-body, 14px); }
|
|
141
|
+
.ui-metric__foot { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; font-size: var(--gemboss-fs-caption); color: var(--ui-text-muted); }
|
|
142
|
+
@media (max-width: 520px) { .ui-metrics--grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
|
143
|
+
|
|
144
|
+
/* Thanh ngang: `<ol class="ui-barlist"><li><span class="ui-barlist__label">Google</span><span class="ui-barlist__val">60
|
|
145
|
+
<span class="ui-barlist__share">93,8%</span></span><span class="ui-barlist__bar" style="--v:.94"></span></li></ol>`.
|
|
146
|
+
`--v` = độ dài 0..1 (thường n / n lớn nhất), `--c` đổi màu. Nhãn và số ở một dòng, thanh ở dòng dưới chạy hết bề
|
|
147
|
+
ngang: không có cột nhãn cố định nào để bị bóp ở 320px. `__note` = dòng phụ (ví dụ "Đọc hết 30%"). */
|
|
148
|
+
.ui-barlist { display: grid; gap: var(--ui-space-3); margin: 0; padding: 0; list-style: none; font-size: var(--ui-fs-body, 13px); }
|
|
149
|
+
.ui-barlist > li { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: baseline; gap: 4px var(--ui-space-3); }
|
|
150
|
+
.ui-barlist__label { min-width: 0; overflow-wrap: anywhere; color: var(--ui-text); }
|
|
151
|
+
.ui-barlist__label a { color: inherit; }
|
|
152
|
+
.ui-barlist__val { font-weight: 600; color: var(--ui-text); font-variant-numeric: tabular-nums; white-space: nowrap; text-align: right; }
|
|
153
|
+
.ui-barlist__share { margin-left: 6px; font-weight: 400; color: var(--ui-text-muted); }
|
|
154
|
+
.ui-barlist__note { grid-column: 1 / -1; margin-top: -2px; font-size: var(--gemboss-fs-caption); color: var(--ui-text-muted); }
|
|
155
|
+
.ui-barlist__bar { grid-column: 1 / -1; position: relative; height: 6px; border-radius: 999px; background: var(--ui-surface-2); overflow: hidden; }
|
|
156
|
+
.ui-barlist__bar::after { content: ""; position: absolute; inset: 0 auto 0 0; width: calc(var(--v, 0) * 100%); min-width: 3px; border-radius: inherit; background: var(--c, var(--ui-series-1)); }
|
|
157
|
+
|
|
158
|
+
/* Biểu đồ cột chồng theo thời gian, thêm (tuỳ chọn) một chuỗi CHẤM:
|
|
159
|
+
`<figure class="ui-chart"><div class="ui-chart__plot" data-max="80"><div class="ui-chart__col" title="02/10 · …">
|
|
160
|
+
<span class="ui-chart__seg" style="--v:.4;--c:var(--ui-series-1)"></span><span class="ui-chart__seg" …></span>
|
|
161
|
+
<span class="ui-chart__dot" style="--v:.5"></span></div>…</div><div class="ui-chart__axis"><span>01/10</span>…</div>
|
|
162
|
+
<figcaption class="ui-legend">…</figcaption></figure>`. `--v` = giá trị / trục (0..1), chồng từ dưới lên theo thứ tự
|
|
163
|
+
markup. Cột rộng tối đa 48px và dàn đều hết bề ngang: 4 ngày số liệu không thành 4 khối to. `data-max` in ở góc
|
|
164
|
+
trên trái làm mốc trục. Đường kẻ ngang ở 25/50/75%. Số thật cho trình đọc màn hình là
|
|
165
|
+
việc của bề mặt (một bảng `ui-sr-only` cạnh biểu đồ): phần vẽ nên `aria-hidden`. */
|
|
166
|
+
.ui-chart { margin: 0; }
|
|
167
|
+
.ui-chart__plot { position: relative; display: flex; align-items: flex-end; justify-content: space-between; gap: 3px; height: 180px; padding-top: 18px; border-bottom: 1px solid var(--ui-border);
|
|
168
|
+
background: linear-gradient(to top, var(--ui-border) 1px, transparent 1px) 0 0 / 100% 25% repeat-y; background-origin: content-box; background-clip: content-box; }
|
|
169
|
+
.ui-chart__plot[data-max]::before { content: attr(data-max); position: absolute; top: 0; left: 0; font-size: var(--gemboss-fs-micro, 11px); line-height: 1; color: var(--ui-text-muted); font-variant-numeric: tabular-nums; }
|
|
170
|
+
.ui-chart__col { position: relative; flex: 1 1 0; min-width: 0; max-width: 48px; height: 100%; display: flex; flex-direction: column-reverse; }
|
|
171
|
+
.ui-chart__col:hover { background: color-mix(in srgb, var(--ui-text) 6%, transparent); }
|
|
172
|
+
.ui-chart__seg { display: block; flex: none; height: calc(var(--v, 0) * 100%); background: var(--c, var(--ui-series-1)); }
|
|
173
|
+
.ui-chart__dot { position: absolute; left: 50%; bottom: calc(var(--v, 0) * 100%); width: 8px; height: 8px; margin: 0 0 -4px -4px; box-sizing: border-box; border-radius: 50%; background: var(--ui-surface); border: 2px solid var(--c, var(--ui-text-strong, var(--ui-text))); }
|
|
174
|
+
.ui-chart__axis { display: flex; justify-content: space-between; gap: var(--ui-space-2); margin-top: 6px; font-size: var(--gemboss-fs-caption); color: var(--ui-text-muted); font-variant-numeric: tabular-nums; }
|
|
175
|
+
|
|
176
|
+
/* Chú thích: `<p class="ui-legend"><span class="ui-legend__item"><i style="--c:var(--ui-series-1)"></i>Tiện ích</span>
|
|
177
|
+
<span class="ui-legend__item ui-legend__item--dot"><i></i>Lượt mở</span></p>`. `--dot` = chuỗi vẽ bằng chấm rỗng. */
|
|
178
|
+
.ui-legend { display: flex; flex-wrap: wrap; gap: 6px 16px; margin: var(--ui-space-2) 0 0; font-size: var(--gemboss-fs-caption); color: var(--ui-text-muted); }
|
|
179
|
+
.ui-legend__item { display: inline-flex; align-items: center; gap: 6px; }
|
|
180
|
+
.ui-legend__item > i { display: inline-block; width: 10px; height: 10px; border-radius: 2px; background: var(--c, var(--ui-series-1)); }
|
|
181
|
+
.ui-legend__item--dot > i { border-radius: 50%; background: var(--ui-surface); border: 2px solid var(--c, var(--ui-text-strong, var(--ui-text))); box-sizing: border-box; }
|
|
182
|
+
|
|
183
|
+
/* Phễu: `<ol class="ui-funnel"><li><span class="ui-funnel__val">105</span><span class="ui-funnel__lbl">Lời mời hiện
|
|
184
|
+
</span></li><li><span class="ui-funnel__rate">4,8%</span><span class="ui-funnel__val">5</span>…</li></ol>`. Mỗi bước
|
|
185
|
+
sau bước đầu mang `__rate` = tỉ lệ đi tiếp từ bước NGAY TRƯỚC (bề mặt tính). Ngang ở khổ rộng, dọc ở ≤ 520px. */
|
|
186
|
+
.ui-funnel { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); gap: var(--ui-space-2); margin: 0; padding: 0; list-style: none; }
|
|
187
|
+
.ui-funnel > li { position: relative; display: flex; flex-direction: column; gap: 2px; padding: var(--ui-space-3); border-radius: var(--ui-radius-md); background: var(--ui-surface-2); min-width: 0; }
|
|
188
|
+
.ui-funnel__val { font-size: var(--gemboss-fs-h2, 20px); font-weight: 700; color: var(--ui-text); font-variant-numeric: tabular-nums; line-height: 1.15; }
|
|
189
|
+
.ui-funnel__lbl { font-size: var(--gemboss-fs-caption); color: var(--ui-text-muted); overflow-wrap: anywhere; }
|
|
190
|
+
.ui-funnel__rate { order: -1; font-size: var(--gemboss-fs-caption); font-weight: 600; color: var(--ui-series-1); font-variant-numeric: tabular-nums; }
|
|
191
|
+
@media (max-width: 520px) { .ui-funnel { grid-auto-flow: row; } }
|
|
192
|
+
|
|
193
|
+
/* Lưới bảng điều khiển: `<div class="ui-grid ui-grid--2">` card cạnh card, xuống một cột ở ≤ 960px (--3: ≤ 1100px hai
|
|
194
|
+
cột, ≤ 720px một). Card trong lưới không kéo cao theo card bên cạnh. Tiêu đề MỤC trong trang (nhỏ hơn ui-page-head,
|
|
195
|
+
lớn hơn tiêu đề card): `<div class="ui-section-head"><h2>Nội dung</h2><p>…</p></div>`. */
|
|
196
|
+
.ui-grid { display: grid; gap: var(--ui-space-4); align-items: start; }
|
|
197
|
+
.ui-grid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
198
|
+
.ui-grid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
199
|
+
@media (max-width: 1100px) { .ui-grid--3 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
|
200
|
+
@media (max-width: 960px) { .ui-grid--2 { grid-template-columns: minmax(0, 1fr); } }
|
|
201
|
+
@media (max-width: 720px) { .ui-grid--3 { grid-template-columns: minmax(0, 1fr); } }
|
|
202
|
+
.ui-section-head { margin: var(--ui-space-4) 0 0; }
|
|
203
|
+
.ui-section-head > h2 { margin: 0; font-size: var(--gemboss-fs-h3, 16px); font-weight: 650; color: var(--ui-text); }
|
|
204
|
+
.ui-section-head > p { margin: 2px 0 0; font-size: var(--gemboss-fs-caption); color: var(--ui-text-muted); }
|
|
205
|
+
|
|
206
|
+
/* Nhóm nút CHỮ không bao giờ bẻ chữ (03/10/2026, thanh lọc của gemnews ở 390px: "Mọi ngôn ngữ" thành hai dòng, "中文"
|
|
207
|
+
thành 中 / 文, vì ô cao cố định 36px mà nút được co). Nút giữ đủ bề ngang chữ; cả nhóm rộng hơn chỗ trống thì cuộn
|
|
208
|
+
ngang trong chính nó, trang không tràn. Ở đây chứ không ở ui.css: ngân sách CSS first-load của admin còn ~0,1 KB. */
|
|
209
|
+
.ui-seg { max-width: 100%; overflow-x: auto; } /* thanh cuộn KHÔNG ẩn: lựa chọn bị khuất phải có dấu hiệu (review #3630) */
|
|
210
|
+
.ui-seg :is(button, a).is-text { flex: none; white-space: nowrap; }
|
|
211
|
+
/* Ở 320px, bốn nút chữ (Tất cả · Tiếng Việt · English · 中文) thiếu ~10px: bớt padding thay vì bắt người dùng cuộn mới
|
|
212
|
+
thấy lựa chọn cuối. Nút vẫn cao 36px, chữ vẫn một dòng. */
|
|
213
|
+
@media (max-width: 360px) { .ui-seg :is(button, a).is-text { padding: 0 9px; } }
|
|
214
|
+
/* Tab / nhóm nút là ĐIỀU KHIỂN: ≥ 44px dưới ngón tay (§9.1). Khối coarse của ui.css chỉ bắt `pointer: coarse`; ở khổ điện
|
|
215
|
+
thoại trình duyệt máy tính vẫn báo `fine`, nên nâng theo khổ như ui-btn--icon ở trên. */
|
|
216
|
+
@media (pointer: coarse), (max-width: 47.9975em) { .ui-seg :is(button, a) { height: 44px; min-height: 44px; } }
|