@gemboss/ui 0.7.1 → 0.8.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 +88 -6
- package/catalog/index.html +71 -34
- package/gemboss-ui.css +79 -31
- package/package.json +3 -1
- package/src/assistant/dock.ts +19 -18
- package/src/assistant/host.tsx +40 -5
- package/src/assistant/provider.tsx +43 -19
- package/src/kit-plain.css +31 -12
- package/src/portal.css +14 -62
- package/src/portal.tsx +24 -12
- package/src/theme-dark.css +65 -0
- package/src/ui.css +23 -13
package/src/kit-plain.css
CHANGED
|
@@ -61,23 +61,24 @@
|
|
|
61
61
|
Link: `<a class="ui-link" href>` for a link inside text. Iris (--gemboss-accent-strong, 6.2:1 on white), underlined
|
|
62
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
63
|
other generated HTML you cannot add classes to: wrap it in `.ui-prose` and its bare `<a>` get the same look.
|
|
64
|
+
`.ui-prose` links sit at :where() specificity (0.7.3), so any component's own link rule wins inside it.
|
|
64
65
|
|
|
65
66
|
Media card: `<article class="ui-card ui-card--media"><a|button class="ui-card__media"><img alt=""></…>
|
|
66
67
|
<div class="ui-card__body">…</div><div class="ui-card__foot">…</div></article>`. The picture is 16:10 and
|
|
67
68
|
cropped to fill; the card has no padding of its own, the body and foot do. */
|
|
68
69
|
.ui-link,
|
|
69
|
-
.ui-prose a:not([class]) {
|
|
70
|
+
.ui-prose :where(a:not([class])) {
|
|
70
71
|
color: var(--gemboss-accent-strong, #3a44de);
|
|
71
72
|
text-decoration: underline;
|
|
72
73
|
text-decoration-thickness: from-font;
|
|
73
74
|
text-underline-offset: 2px;
|
|
74
75
|
}
|
|
75
76
|
.ui-link:visited,
|
|
76
|
-
.ui-prose a:not([class]):visited { color: var(--gemboss-accent-strong, #3a44de); }
|
|
77
|
+
.ui-prose :where(a:not([class])):visited { color: var(--gemboss-accent-strong, #3a44de); }
|
|
77
78
|
.ui-link:hover,
|
|
78
|
-
.ui-prose a:not([class]):hover { color: var(--gemboss-accent, #5661f5); text-decoration-thickness: 2px; }
|
|
79
|
+
.ui-prose :where(a:not([class])):hover { color: var(--gemboss-accent, #5661f5); text-decoration-thickness: 2px; }
|
|
79
80
|
.ui-link:focus-visible,
|
|
80
|
-
.ui-prose a:not([class]):focus-visible { outline: 2px solid var(--ui-focus, #5661f5); outline-offset: 2px; border-radius: 2px; }
|
|
81
|
+
.ui-prose :where(a:not([class])):focus-visible { outline: 2px solid var(--ui-focus, #5661f5); outline-offset: 2px; border-radius: 2px; }
|
|
81
82
|
|
|
82
83
|
.ui-card--media { padding: 0; overflow: hidden; display: flex; flex-direction: column; }
|
|
83
84
|
.ui-card__media {
|
|
@@ -96,13 +97,16 @@ button.ui-card__media, a.ui-card__media { cursor: pointer; }
|
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
/* Popup body and foot: `<div class="ui-modal__body">…</div><div class="ui-modal__foot"><button class="ui-btn">…</button></div>`.
|
|
99
|
-
ui.css
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
100
|
+
Their padding moved to ui.css in 0.7.3 (the admin loads that file, not this one). kit-plain.js's openModal writes
|
|
101
|
+
exactly this shape. What stays here is what only the plain-HTML popups use:
|
|
102
|
+
|
|
103
|
+
A head that carries more than a title (a large popup's subtitle and its own actions, GemFactory's detail popup):
|
|
104
|
+
`<div class="ui-modal__head"><div class="ui-modal__title">Mega menu<span class="ui-modal__subtitle">chrome/header</span>
|
|
105
|
+
</div><div class="ui-modal__actions"><button class="ui-btn">Copy</button></div><button class="ui-modal__close">…` */
|
|
106
|
+
.ui-modal__subtitle { display: block; margin-top: 2px; font-size: var(--ui-fs-caption); line-height: 16px; font-weight: 450; color: var(--ui-text-muted); overflow-wrap: anywhere; }
|
|
107
|
+
.ui-modal__actions { flex: none; display: flex; flex-wrap: wrap; align-items: center; gap: var(--ui-space-2); }
|
|
108
|
+
/* An icon button in the popup's head or foot is square at the popup's 28 (ui.css gives those buttons 0/12 padding). */
|
|
109
|
+
:is(.ui-modal__head, .ui-modal__foot) .ui-btn--icon { width: 28px; padding: 0; }
|
|
106
110
|
/* A popup taller than the screen: the body scrolls, the head and foot stay, so the actions are always reachable
|
|
107
111
|
(320×568 with a long form put Huỷ/Xoá below the fold, unreachable under the locked page; review #3633). */
|
|
108
112
|
.ui-overlay > .ui-modal { display: flex; flex-direction: column; max-height: calc(100dvh - 40px); }
|
|
@@ -374,7 +378,7 @@ button.ui-card__media, a.ui-card__media { cursor: pointer; }
|
|
|
374
378
|
<div class="ui-modal__foot">…</div></dialog>` */
|
|
375
379
|
dialog.ui-modal { padding: 0; max-width: calc(100% - 40px); max-height: calc(100dvh - 40px); color: var(--ui-text); }
|
|
376
380
|
dialog.ui-modal[open] { display: flex; flex-direction: column; }
|
|
377
|
-
dialog.ui-modal::backdrop { background: rgba(0, 0, 0, 0.
|
|
381
|
+
dialog.ui-modal::backdrop { background: rgba(0, 0, 0, 0.5); } /* the overlay's own scrim (0.7.3: Polaris' .5, no blur) */
|
|
378
382
|
|
|
379
383
|
/* ── 0.5.1: what the first surfaces on 0.5.0 still drew themselves (GemFactory, content-radar, 03/10/2026) ─────────── */
|
|
380
384
|
|
|
@@ -435,3 +439,18 @@ button.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-off
|
|
|
435
439
|
/* Class doubled: the chosen card is usually the one under the pointer, and `button.ui-metric:hover` (0,2,1) would
|
|
436
440
|
repaint its edge grey the moment it is clicked (measured on gemwatcher Radar). */
|
|
437
441
|
.ui-metric.ui-metric:is([aria-pressed="true"], [aria-current="page"]) { border-color: var(--gemboss-accent); box-shadow: inset 0 0 0 1px var(--gemboss-accent); }
|
|
442
|
+
|
|
443
|
+
/* 0.7.3: numbered pager + ui-more, for the plain surfaces (the admin uses Polaris Pagination). Markup and rationale:
|
|
444
|
+
README "Long lists". Here, not in ui.css: ui.css is exported verbatim into the admin, under a 90 KB budget. */
|
|
445
|
+
.ui-pager > a, .ui-pager > [aria-disabled="true"] { min-width: 32px; justify-content: center; box-sizing: border-box; gap: 4px; }
|
|
446
|
+
.ui-pager > a[aria-current="page"] { background: var(--ui-text); border-color: var(--ui-text); color: var(--ui-surface); font-size: var(--ui-fs-body); font-weight: 600; }
|
|
447
|
+
.ui-pager > a[aria-current="page"]:hover { background: var(--ui-text); }
|
|
448
|
+
.ui-pager > [aria-disabled="true"] { display: inline-flex; align-items: center; min-height: 32px; padding: 0 12px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-md); color: var(--ui-text-muted); font-size: var(--ui-fs-body); opacity: .55; cursor: default; }
|
|
449
|
+
.ui-pager > [aria-disabled="true"]:hover { background: none; }
|
|
450
|
+
.ui-pager__gap { min-width: 16px; text-align: center; color: var(--ui-text-muted); }
|
|
451
|
+
.ui-pager__range { margin-left: auto; color: var(--ui-text-muted); font-size: var(--ui-fs-body); font-variant-numeric: tabular-nums; }
|
|
452
|
+
.ui-more { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 12px; padding: 16px 0; color: var(--ui-text-muted); font-size: var(--ui-fs-body); text-align: center; }
|
|
453
|
+
.ui-more[hidden] { display: none; }
|
|
454
|
+
@media (pointer: coarse), (max-width: 47.9975em) { .ui-pager > a, .ui-pager > [aria-disabled="true"] { min-width: 44px; min-height: 44px; } }
|
|
455
|
+
/* Phone: 11 targets of 44px wrapped into 3-4 rows at 320 (measured). Keep Previous · current · Next; the range wraps under. */
|
|
456
|
+
@media (max-width: 35.9975em) { .ui-pager > a:not([rel]):not([aria-current]):not([aria-disabled]), .ui-pager__gap { display: none; } }
|
package/src/portal.css
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* ═══ Portal pieces — @gemboss/ui 0.7.0 (04/10/2026) ════════════════════════════════════════════
|
|
2
|
-
TopbarNav, Avatar, Leaderboard, Podium (portal.tsx), SearchDialog (dialog.tsx), ThemeToggle
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
TopbarNav, Avatar, Leaderboard, Podium (portal.tsx), SearchDialog (dialog.tsx), ThemeToggle (theme.tsx).
|
|
3
|
+
Component rules only, no rule on :root, html or body (a test holds that): a surface with its own skin
|
|
4
|
+
can take this file with gemboss-tokens.css alone. The dark palette is theme-dark.css. */
|
|
5
5
|
|
|
6
6
|
/* ── TopbarNav: section tabs in the topbar, after the brand (Featurebase's "Feedback · Roadmap"). The
|
|
7
7
|
active tab is underlined on the topbar's bottom edge. 44px targets under a finger, 36 under a mouse
|
|
@@ -47,6 +47,11 @@
|
|
|
47
47
|
}
|
|
48
48
|
@media (prefers-reduced-motion: reduce) { .gb-topnav-chevron { transition: none; } }
|
|
49
49
|
|
|
50
|
+
/* Text for screen readers only ("Rank 1:", ", you"). Its own rule, not ui.css's .ui-sr-only: a surface that
|
|
51
|
+
takes portal.css without the rest of the kit (GemCommunity) would otherwise print it on the page. */
|
|
52
|
+
.gb-sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0);
|
|
53
|
+
white-space: nowrap; border: 0; }
|
|
54
|
+
|
|
50
55
|
/* ── Avatar: picture or initials on one of eight tones picked from the name. */
|
|
51
56
|
.gb-avatar { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; border-radius: 50%; overflow: hidden;
|
|
52
57
|
font-weight: 700; line-height: 1; letter-spacing: .01em; user-select: none; }
|
|
@@ -83,6 +88,9 @@ a.gb-leaderboard-link:focus-visible { outline: 2px solid var(--gemboss-accent);
|
|
|
83
88
|
.gb-leaderboard-name { flex: 1 1 auto; min-width: 0; font-size: 13px; font-weight: 560; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
84
89
|
.gb-leaderboard-value { flex: 0 0 auto; font-size: 13px; font-weight: 650; color: var(--gemboss-text-muted); font-variant-numeric: tabular-nums; }
|
|
85
90
|
.gb-leaderboard-row.is-first .gb-leaderboard-value { color: #8a5a00; font-weight: 750; }
|
|
91
|
+
/* The viewer's own row and podium slot: the selected tint, never a colour of its own. */
|
|
92
|
+
.gb-leaderboard-row.is-current > .gb-leaderboard-link { background: var(--gemboss-active-bg); }
|
|
93
|
+
.gb-leaderboard-badge { flex: 0 1 auto; min-width: 0; overflow: hidden; }
|
|
86
94
|
.gb-leaderboard-all { justify-self: center; display: inline-flex; align-items: center; min-height: 24px; font-size: 12px; font-weight: 600;
|
|
87
95
|
color: var(--gemboss-text-muted); text-decoration: none; }
|
|
88
96
|
.gb-leaderboard-all:hover { color: var(--gemboss-text-strong); text-decoration: underline; }
|
|
@@ -93,6 +101,8 @@ a.gb-leaderboard-link:focus-visible { outline: 2px solid var(--gemboss-accent);
|
|
|
93
101
|
background: var(--gemboss-surface); color: var(--gemboss-text); text-decoration: none; text-align: center; }
|
|
94
102
|
a.gb-podium-link:hover { background: var(--gemboss-hover); }
|
|
95
103
|
.gb-podium-slot { min-width: 0; }
|
|
104
|
+
/* A podium with fewer than three people keeps its place: second and third stay empty columns. */
|
|
105
|
+
.gb-podium-slot.is-empty { visibility: hidden; }
|
|
96
106
|
.gb-podium-slot--1 .gb-podium-link { padding-top: 22px; padding-bottom: 20px; border-color: color-mix(in srgb, #d4a017 45%, var(--gemboss-divider)); }
|
|
97
107
|
.gb-podium-slot--2 .gb-podium-link { padding-bottom: 12px; }
|
|
98
108
|
.gb-podium-slot--3 .gb-podium-link { padding-bottom: 8px; }
|
|
@@ -101,6 +111,7 @@ a.gb-podium-link:hover { background: var(--gemboss-hover); }
|
|
|
101
111
|
.gb-podium-value { font-size: 15px; font-weight: 750; font-variant-numeric: tabular-nums; }
|
|
102
112
|
.gb-podium-slot--1 .gb-podium-value { color: #8a5a00; }
|
|
103
113
|
.gb-podium-unit { font-size: 11px; font-weight: 500; color: var(--gemboss-text-muted); }
|
|
114
|
+
.gb-podium-slot.is-current .gb-podium-link { border-color: var(--gemboss-text-strong); background: var(--gemboss-active-bg); }
|
|
104
115
|
|
|
105
116
|
/* ── SearchDialog: the topbar's search button, and the popup (640 wide, near the top, like Featurebase's). */
|
|
106
117
|
.gb-search-trigger, .gb-theme-toggle { display: inline-flex; align-items: center; justify-content: center; width: 36px; height: 36px; flex: 0 0 auto;
|
|
@@ -126,62 +137,3 @@ dialog.ui-modal.gb-search { width: min(640px, 100%); margin-top: 8vh; max-height
|
|
|
126
137
|
.gb-search-foot { display: flex; gap: 16px; justify-content: flex-end; padding: 8px 14px; border-top: 1px solid var(--ui-border);
|
|
127
138
|
font-size: 12px; color: var(--ui-text-muted); }
|
|
128
139
|
@media (max-width: 760px) { dialog.ui-modal.gb-search { margin-top: 12px; } .gb-search-foot { display: none; } }
|
|
129
|
-
|
|
130
|
-
/* ── Dark palette. `ThemeToggle` / `themeScript` set data-theme on <html>; a surface without them stays
|
|
131
|
-
light. Only colours change: the components above and every kit class read these tokens. */
|
|
132
|
-
:root[data-theme="dark"] {
|
|
133
|
-
color-scheme: dark;
|
|
134
|
-
--gemboss-primary: #f2f3f5;
|
|
135
|
-
--gemboss-primary-dark: #ffffff;
|
|
136
|
-
--gemboss-primary-soft: #23242a;
|
|
137
|
-
--gemboss-on-primary: #0d0d12;
|
|
138
|
-
--gemboss-accent: #7c84ff;
|
|
139
|
-
--gemboss-accent-strong: #a3a9ff;
|
|
140
|
-
--gemboss-accent-soft: #1f2140;
|
|
141
|
-
--gemboss-text-strong: #f2f3f5;
|
|
142
|
-
--gemboss-text: #e3e4e8;
|
|
143
|
-
--gemboss-text-muted: #9a9ca6;
|
|
144
|
-
--gemboss-surface: #16171d;
|
|
145
|
-
--gemboss-canvas: #0d0d12;
|
|
146
|
-
--gemboss-app-bg: #0d0d12;
|
|
147
|
-
--gemboss-rail-bg: #0d0d12;
|
|
148
|
-
--gemboss-panel-bg: #16171d;
|
|
149
|
-
--gemboss-topbar-bg: rgba(13, 13, 18, 0.96);
|
|
150
|
-
--gemboss-nav-bg: #0d0d12;
|
|
151
|
-
--gemboss-nav-hover: rgba(255, 255, 255, 0.06);
|
|
152
|
-
--gemboss-divider: #2a2b33;
|
|
153
|
-
--gemboss-divider-soft: #22232a;
|
|
154
|
-
--gemboss-hover: #22232b;
|
|
155
|
-
--gemboss-active-bg: #262833;
|
|
156
|
-
--gemboss-active-fg: #f2f3f5;
|
|
157
|
-
--gemboss-field-bg: #121318;
|
|
158
|
-
--gemboss-field-bg-hover: #17181e;
|
|
159
|
-
--gemboss-field-ink: #e3e4e8;
|
|
160
|
-
--gemboss-field-placeholder: #8a8c96;
|
|
161
|
-
--gemboss-field-line: #3a3c46;
|
|
162
|
-
--gemboss-field-line-hover: #50525e;
|
|
163
|
-
--gemboss-field-line-active: #c9cbd3;
|
|
164
|
-
--gemboss-chip-ink: #d6d8de;
|
|
165
|
-
--gemboss-chip-line: #3a3c46;
|
|
166
|
-
--gemboss-chip-line-hover: #50525e;
|
|
167
|
-
--gemboss-chip-bg-hover: #22232b;
|
|
168
|
-
--gemboss-success: #34c38f;
|
|
169
|
-
--gemboss-success-strong: #5fd6a8;
|
|
170
|
-
--gemboss-success-soft: #13291f;
|
|
171
|
-
--gemboss-success-border: #22533e;
|
|
172
|
-
--gemboss-warning: #e0b341;
|
|
173
|
-
--gemboss-warning-soft: #2b2412;
|
|
174
|
-
--gemboss-warning-border: #5a4a1d;
|
|
175
|
-
--gemboss-danger: #ff6b5a;
|
|
176
|
-
--gemboss-danger-strong: #ff8a7c;
|
|
177
|
-
--gemboss-danger-soft: #2e1714;
|
|
178
|
-
--gemboss-danger-border: #5c2a24;
|
|
179
|
-
--ui-accent-fg: #0d0d12;
|
|
180
|
-
--ui-warning-fg: #e8c063;
|
|
181
|
-
--gb-brand-ink: #f2f3f5;
|
|
182
|
-
}
|
|
183
|
-
/* Avatar initials in dark: the tone's light end, so the text clears 4.5:1 on its tinted disc. */
|
|
184
|
-
:root[data-theme="dark"] .gb-avatar[data-tone] { background: color-mix(in srgb, var(--gb-tone) 30%, #16171d); color: color-mix(in srgb, var(--gb-tone) 35%, #ffffff); }
|
|
185
|
-
:root[data-theme="dark"] body { background: var(--gemboss-app-bg); color: var(--gemboss-text); }
|
|
186
|
-
:root[data-theme="dark"] .gb-leaderboard-row.is-first .gb-leaderboard-value,
|
|
187
|
-
:root[data-theme="dark"] .gb-podium-slot--1 .gb-podium-value { color: #e0b341; }
|
package/src/portal.tsx
CHANGED
|
@@ -80,7 +80,15 @@ export function Avatar({ name, src, size = 24, className }: { name: string; src?
|
|
|
80
80
|
);
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
export type LeaderRow = {
|
|
83
|
+
export type LeaderRow = {
|
|
84
|
+
key: string | number; name: string; value: number; avatar?: string | null; href?: string;
|
|
85
|
+
/** Shown after the name: a level, a role (GemCommunity's "Lv2 · Builder"). */
|
|
86
|
+
badge?: React.ReactNode;
|
|
87
|
+
/** The viewer's own row: tinted, and announced. */
|
|
88
|
+
current?: boolean;
|
|
89
|
+
/** The row's element id, for a "#me" link that scrolls to it. */
|
|
90
|
+
id?: string;
|
|
91
|
+
};
|
|
84
92
|
|
|
85
93
|
const MEDALS = ["🥇", "🥈", "🥉"];
|
|
86
94
|
const PODIUM_SLOT = ["gb-podium-slot--1", "gb-podium-slot--2", "gb-podium-slot--3"];
|
|
@@ -90,40 +98,44 @@ const PODIUM_SLOT = ["gb-podium-slot--1", "gb-podium-slot--2", "gb-podium-slot--
|
|
|
90
98
|
* screen readers ("163 votes") and can be shown with `showUnit`. Empty → nothing (a "top" list of
|
|
91
99
|
* zeros looks dead; GemCommunity hides it the same way).
|
|
92
100
|
*/
|
|
93
|
-
export function Leaderboard({ title, caption, rows, unit, showUnit = false, viewAll, as: L = "a", className, headingLevel = 2, id }: {
|
|
94
|
-
|
|
101
|
+
export function Leaderboard({ title, label, caption, rows, unit, showUnit = false, viewAll, as: L = "a", className, headingLevel = 2, id, currentLabel = "you" }: {
|
|
102
|
+
/** The visible heading. Leave it out on a page whose own <h1> already names the board; give `label`. */
|
|
103
|
+
title?: React.ReactNode; label?: string; caption?: React.ReactNode; rows: LeaderRow[]; unit?: string; showUnit?: boolean;
|
|
95
104
|
viewAll?: { href: string; label: React.ReactNode }; as?: LinkLike; className?: string; headingLevel?: 2 | 3;
|
|
96
105
|
/** The heading's id. Pass one when two leaderboards share a page or the title is not plain text. */
|
|
97
106
|
id?: string;
|
|
107
|
+
/** Read by screen readers after the viewer's own name ("Minh, you"). "" when the name already says it on screen. */
|
|
108
|
+
currentLabel?: string;
|
|
98
109
|
}) {
|
|
99
110
|
if (!rows.length) return null;
|
|
100
111
|
const H = `h${headingLevel}` as "h2" | "h3";
|
|
101
112
|
// From the title's letters (any script) and a hash of it, so "Most helpful" and a Vietnamese or
|
|
102
113
|
// Chinese title each get their own; two boards with the same title need `id`.
|
|
103
|
-
const text = typeof title === "string" ? title : "leaderboard";
|
|
114
|
+
const text = typeof title === "string" ? title : label ?? "leaderboard";
|
|
104
115
|
let h = 0;
|
|
105
116
|
for (const ch of text) h = (h * 31 + ch.codePointAt(0)!) >>> 0;
|
|
106
117
|
const headingId = id ?? `gb-lb-${text.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-|-$/g, "")}-${h.toString(36)}`;
|
|
107
118
|
return (
|
|
108
|
-
<section className={cx("gb-leaderboard", className)} aria-labelledby={headingId}>
|
|
109
|
-
<H className="gb-leaderboard-title" id={headingId}>{title}</H>
|
|
119
|
+
<section className={cx("gb-leaderboard", className)} aria-labelledby={title ? headingId : undefined} aria-label={title ? undefined : label}>
|
|
120
|
+
{title ? <H className="gb-leaderboard-title" id={headingId}>{title}</H> : null}
|
|
110
121
|
{caption ? <p className="gb-leaderboard-caption">{caption}</p> : null}
|
|
111
122
|
<ol className="gb-leaderboard-list">
|
|
112
123
|
{rows.map((r, i) => {
|
|
113
124
|
const inner = (
|
|
114
125
|
<>
|
|
115
126
|
<span className={cx("gb-leaderboard-rank", !MEDALS[i] && "is-number")}>
|
|
116
|
-
<span aria-hidden="true">{MEDALS[i] ?? i + 1}</span><span className="
|
|
127
|
+
<span aria-hidden="true">{MEDALS[i] ?? i + 1}</span><span className="gb-sr-only">Rank {i + 1}: </span>
|
|
117
128
|
</span>
|
|
118
129
|
<Avatar name={r.name} src={r.avatar} />
|
|
119
|
-
<span className="gb-leaderboard-name">{r.name}</span>
|
|
130
|
+
<span className="gb-leaderboard-name">{r.name}{r.current && currentLabel ? <span className="gb-sr-only">, {currentLabel}</span> : null}</span>
|
|
131
|
+
{r.badge ? <span className="gb-leaderboard-badge">{r.badge}</span> : null}
|
|
120
132
|
<span className="gb-leaderboard-value">
|
|
121
|
-
{r.value}{unit ? (showUnit ? <> {unit}</> : <span className="
|
|
133
|
+
{r.value}{unit ? (showUnit ? <> {unit}</> : <span className="gb-sr-only"> {unit}</span>) : null}
|
|
122
134
|
</span>
|
|
123
135
|
</>
|
|
124
136
|
);
|
|
125
137
|
return (
|
|
126
|
-
<li key={r.key} className={cx("gb-leaderboard-row", i === 0 && "is-first")}>
|
|
138
|
+
<li key={r.key} id={r.id} className={cx("gb-leaderboard-row", i === 0 && "is-first", r.current && "is-current")}>
|
|
127
139
|
{r.href ? <L href={r.href} className="gb-leaderboard-link">{inner}</L> : <div className="gb-leaderboard-link">{inner}</div>}
|
|
128
140
|
</li>
|
|
129
141
|
);
|
|
@@ -144,14 +156,14 @@ export function Podium({ rows, unit, as: L = "a", className }: { rows: LeaderRow
|
|
|
144
156
|
if (!r) return <li key={idx} className="gb-podium-slot is-empty" aria-hidden="true" />;
|
|
145
157
|
const body = (
|
|
146
158
|
<>
|
|
147
|
-
<span className="gb-podium-medal"><span aria-hidden="true">{MEDALS[idx]}</span><span className="
|
|
159
|
+
<span className="gb-podium-medal"><span aria-hidden="true">{MEDALS[idx]}</span><span className="gb-sr-only">Rank {idx + 1}: </span></span>
|
|
148
160
|
<Avatar name={r.name} src={r.avatar} size={56} />
|
|
149
161
|
<span className="gb-podium-name">{r.name}</span>
|
|
150
162
|
<span className="gb-podium-value">{r.value}{unit ? <span className="gb-podium-unit"> {unit}</span> : null}</span>
|
|
151
163
|
</>
|
|
152
164
|
);
|
|
153
165
|
return (
|
|
154
|
-
<li key={r.key} className={cx("gb-podium-slot", PODIUM_SLOT[idx])}>
|
|
166
|
+
<li key={r.key} className={cx("gb-podium-slot", PODIUM_SLOT[idx], r.current && "is-current")}>
|
|
155
167
|
{r.href ? <L href={r.href} className="gb-podium-link">{body}</L> : <div className="gb-podium-link">{body}</div>}
|
|
156
168
|
</li>
|
|
157
169
|
);
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/* ═══ Dark palette — @gemboss/ui 0.7.2 ═══════════════════════════════════════════════════════════════
|
|
2
|
+
Split out of portal.css (04/10/2026). `ThemeToggle` / `themeScript` set data-theme on <html>, and this
|
|
3
|
+
file turns every --gemboss-* colour dark under it. It lives apart so that a surface which takes only
|
|
4
|
+
the leaderboard (gemboss-tokens.css + portal.css) does not inherit it: GemCommunity sets
|
|
5
|
+
data-theme="dark" for its own sign-in pages and remembers it, and the rule
|
|
6
|
+
`:root[data-theme="dark"] body` here outranks its body rule, so a member who once picked dark there
|
|
7
|
+
would get a dark page behind light cards. gemboss-ui.css still carries this file. */
|
|
8
|
+
/* Dark palette. `ThemeToggle` / `themeScript` set data-theme on <html>; a surface without them stays
|
|
9
|
+
light. Only colours change: the components above and every kit class read these tokens. */
|
|
10
|
+
:root[data-theme="dark"] {
|
|
11
|
+
color-scheme: dark;
|
|
12
|
+
--gemboss-primary: #f2f3f5;
|
|
13
|
+
--gemboss-primary-dark: #ffffff;
|
|
14
|
+
--gemboss-primary-soft: #23242a;
|
|
15
|
+
--gemboss-on-primary: #0d0d12;
|
|
16
|
+
--gemboss-accent: #7c84ff;
|
|
17
|
+
--gemboss-accent-strong: #a3a9ff;
|
|
18
|
+
--gemboss-accent-soft: #1f2140;
|
|
19
|
+
--gemboss-text-strong: #f2f3f5;
|
|
20
|
+
--gemboss-text: #e3e4e8;
|
|
21
|
+
--gemboss-text-muted: #9a9ca6;
|
|
22
|
+
--gemboss-surface: #16171d;
|
|
23
|
+
--gemboss-canvas: #0d0d12;
|
|
24
|
+
--gemboss-app-bg: #0d0d12;
|
|
25
|
+
--gemboss-rail-bg: #0d0d12;
|
|
26
|
+
--gemboss-panel-bg: #16171d;
|
|
27
|
+
--gemboss-topbar-bg: rgba(13, 13, 18, 0.96);
|
|
28
|
+
--gemboss-nav-bg: #0d0d12;
|
|
29
|
+
--gemboss-nav-hover: rgba(255, 255, 255, 0.06);
|
|
30
|
+
--gemboss-divider: #2a2b33;
|
|
31
|
+
--gemboss-divider-soft: #22232a;
|
|
32
|
+
--gemboss-hover: #22232b;
|
|
33
|
+
--gemboss-active-bg: #262833;
|
|
34
|
+
--gemboss-active-fg: #f2f3f5;
|
|
35
|
+
--gemboss-field-bg: #121318;
|
|
36
|
+
--gemboss-field-bg-hover: #17181e;
|
|
37
|
+
--gemboss-field-ink: #e3e4e8;
|
|
38
|
+
--gemboss-field-placeholder: #8a8c96;
|
|
39
|
+
--gemboss-field-line: #3a3c46;
|
|
40
|
+
--gemboss-field-line-hover: #50525e;
|
|
41
|
+
--gemboss-field-line-active: #c9cbd3;
|
|
42
|
+
--gemboss-chip-ink: #d6d8de;
|
|
43
|
+
--gemboss-chip-line: #3a3c46;
|
|
44
|
+
--gemboss-chip-line-hover: #50525e;
|
|
45
|
+
--gemboss-chip-bg-hover: #22232b;
|
|
46
|
+
--gemboss-success: #34c38f;
|
|
47
|
+
--gemboss-success-strong: #5fd6a8;
|
|
48
|
+
--gemboss-success-soft: #13291f;
|
|
49
|
+
--gemboss-success-border: #22533e;
|
|
50
|
+
--gemboss-warning: #e0b341;
|
|
51
|
+
--gemboss-warning-soft: #2b2412;
|
|
52
|
+
--gemboss-warning-border: #5a4a1d;
|
|
53
|
+
--gemboss-danger: #ff6b5a;
|
|
54
|
+
--gemboss-danger-strong: #ff8a7c;
|
|
55
|
+
--gemboss-danger-soft: #2e1714;
|
|
56
|
+
--gemboss-danger-border: #5c2a24;
|
|
57
|
+
--ui-accent-fg: #0d0d12;
|
|
58
|
+
--ui-warning-fg: #e8c063;
|
|
59
|
+
--gb-brand-ink: #f2f3f5;
|
|
60
|
+
}
|
|
61
|
+
/* Avatar initials in dark: the tone's light end, so the text clears 4.5:1 on its tinted disc. */
|
|
62
|
+
:root[data-theme="dark"] .gb-avatar[data-tone] { background: color-mix(in srgb, var(--gb-tone) 30%, #16171d); color: color-mix(in srgb, var(--gb-tone) 35%, #ffffff); }
|
|
63
|
+
:root[data-theme="dark"] body { background: var(--gemboss-app-bg); color: var(--gemboss-text); }
|
|
64
|
+
:root[data-theme="dark"] .gb-leaderboard-row.is-first .gb-leaderboard-value,
|
|
65
|
+
:root[data-theme="dark"] .gb-podium-slot--1 .gb-podium-value { color: #e0b341; }
|
package/src/ui.css
CHANGED
|
@@ -274,10 +274,11 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
|
|
|
274
274
|
|
|
275
275
|
/* ── Modal ────────────────────────────────────────────────────────────────────
|
|
276
276
|
Rendered INLINE (not portaled): a portal escapes the .gemboss-admin-shell token
|
|
277
|
-
scope and would render light. Kept in-tree so --ui-* resolves to the surface.
|
|
277
|
+
scope and would render light. Kept in-tree so --ui-* resolves to the surface.
|
|
278
|
+
0.7.3: Polaris Modal's density (#1099, README "Popup density"). Popup only: --ui-control-h stays 32. */
|
|
278
279
|
.ui-overlay {
|
|
279
280
|
position: fixed; inset: 0; z-index: 500;
|
|
280
|
-
background: rgba(0, 0, 0, 0.
|
|
281
|
+
background: rgba(0, 0, 0, 0.5);
|
|
281
282
|
display: grid; place-items: center; padding: 20px;
|
|
282
283
|
}
|
|
283
284
|
.ui-modal {
|
|
@@ -287,30 +288,39 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
|
|
|
287
288
|
}
|
|
288
289
|
/* Head = title + the one close button every modal used to draw for itself. */
|
|
289
290
|
.ui-modal__head {
|
|
290
|
-
display: flex; align-items:
|
|
291
|
-
padding: 16px
|
|
292
|
-
font-size:
|
|
291
|
+
display: flex; align-items: center; gap: 12px;
|
|
292
|
+
padding: 12px 16px; border-bottom: 1px solid var(--ui-border);
|
|
293
|
+
font-size: 14px; line-height: 20px; font-weight: 650; color: var(--ui-text);
|
|
293
294
|
}
|
|
294
295
|
.ui-modal__title { min-width: 0; flex: 1 1 auto; }
|
|
295
296
|
.ui-modal__close {
|
|
296
297
|
flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
|
|
297
|
-
width:
|
|
298
|
+
width: 28px; height: 28px; padding: 0;
|
|
298
299
|
border: 0; border-radius: var(--ui-radius-md, 8px); background: transparent;
|
|
299
300
|
color: var(--ui-text-muted); cursor: pointer;
|
|
300
301
|
}
|
|
301
302
|
.ui-modal__close svg { width: 20px; height: 20px; }
|
|
302
303
|
.ui-modal__close:hover { background: var(--ui-surface-2); color: var(--ui-text); }
|
|
303
304
|
.ui-modal__close:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: -2px; }
|
|
305
|
+
/* Here, not in kit-plain.css: the admin loads only this file (0.7.3). */
|
|
306
|
+
.ui-modal__body { padding: 16px; }
|
|
307
|
+
.ui-modal__foot {
|
|
308
|
+
display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end;
|
|
309
|
+
padding: 12px 16px; border-top: 1px solid var(--ui-border);
|
|
310
|
+
}
|
|
311
|
+
:is(.ui-modal__head, .ui-modal__foot) .ui-btn { height: 28px; padding: 0 12px; font-size: var(--ui-fs-caption); }
|
|
312
|
+
/* 44px under a thumb or at phone width; -8px keeps the head 53. */
|
|
313
|
+
@media (pointer: coarse), (max-width: 47.9975em) {
|
|
314
|
+
.ui-modal__close { width: 44px; height: 44px; margin: -8px -8px -8px 0; }
|
|
315
|
+
:is(.ui-modal__head, .ui-modal__foot) .ui-btn { min-height: 44px; }
|
|
316
|
+
}
|
|
304
317
|
|
|
305
|
-
/* `size="lg"
|
|
306
|
-
|
|
307
|
-
consumer belongs in that consumer's chunk (see SettingsDialog.module.css). */
|
|
318
|
+
/* `size="lg"`: only the HOOK here (this sheet is paid on every screen); its geometry is in
|
|
319
|
+
SettingsDialog.module.css, the one consumer's chunk. */
|
|
308
320
|
.ui-modal--lg { position: relative; }
|
|
309
321
|
|
|
310
|
-
/* The kit's multi-line field
|
|
311
|
-
|
|
312
|
-
lighter --ui-border box at 14px with no focus ring, so on GemFactory's compose form a textarea sat
|
|
313
|
-
beside selects of a darker border and text fields of a third look (measured on prod). */
|
|
322
|
+
/* The kit's multi-line field is the ONE field family (01/10/2026, Chris: "form control đồng bộ"): same
|
|
323
|
+
border token, hover and focus ring as `gemboss-field` / `.ui-select` (it drew a third look on GemFactory). */
|
|
314
324
|
.ui-textarea {
|
|
315
325
|
width: 100%; min-height: 96px; resize: vertical; box-sizing: border-box;
|
|
316
326
|
background: var(--p-color-input-bg-surface, var(--gemboss-field-bg));
|