@oharness/server 0.9.1 → 0.9.3
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/dist/i18n.js +4 -4
- package/dist/i18n.js.map +1 -1
- package/dist/page/client/composer.js +16 -4
- package/dist/page/client/composer.js.map +1 -1
- package/dist/page/client/panels.js +8 -2
- package/dist/page/client/panels.js.map +1 -1
- package/dist/page/client/shell.js +44 -1
- package/dist/page/client/shell.js.map +1 -1
- package/dist/page/client/transcript.js +37 -1
- package/dist/page/client/transcript.js.map +1 -1
- package/dist/page/client/usage.js +85 -53
- package/dist/page/client/usage.js.map +1 -1
- package/dist/page/style.js +151 -46
- package/dist/page/style.js.map +1 -1
- package/dist/page.js +77 -48
- package/dist/page.js.map +1 -1
- package/dist/server.js +0 -6
- package/dist/server.js.map +1 -1
- package/dist/theme.js +5 -0
- package/dist/theme.js.map +1 -1
- package/package.json +2 -2
|
@@ -958,15 +958,13 @@ function showAccountUsage(body) {
|
|
|
958
958
|
title.appendChild(el("span", "muted small",
|
|
959
959
|
usageCredits(rows.reduce((sum, entry) => sum + (entry.spentMicro || 0), 0))));
|
|
960
960
|
panel.appendChild(title);
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
panel.appendChild(row);
|
|
969
|
-
}
|
|
961
|
+
const groupTotal = rows.reduce((sum, entry) => sum + (entry.spentMicro || 0), 0);
|
|
962
|
+
panel.appendChild(usageScrollList(rows, (entry) => usageModelRow(
|
|
963
|
+
entry.key,
|
|
964
|
+
usageCredits(entry.spentMicro || 0),
|
|
965
|
+
groupTotal ? (entry.spentMicro || 0) / groupTotal : 0,
|
|
966
|
+
usageSeriesFacts(entry),
|
|
967
|
+
)));
|
|
970
968
|
}
|
|
971
969
|
} else {
|
|
972
970
|
panel.appendChild(el("p", "muted", t(kind === "all" ? "usage.none" : "usage.kindNone")));
|
|
@@ -984,8 +982,6 @@ function showAccountUsage(body) {
|
|
|
984
982
|
panelBody.appendChild(panel);
|
|
985
983
|
}
|
|
986
984
|
|
|
987
|
-
/** How many sessions the usage page lists before "View more". */
|
|
988
|
-
const USAGE_RECENT = 10;
|
|
989
985
|
/** Sessions per request on the sessions page. */
|
|
990
986
|
const USAGE_SESSIONS_PAGE = 50;
|
|
991
987
|
|
|
@@ -1022,11 +1018,81 @@ function usageSessionRow(entry) {
|
|
|
1022
1018
|
return row;
|
|
1023
1019
|
}
|
|
1024
1020
|
|
|
1021
|
+
/**
|
|
1022
|
+
* One model: its name with what it cost at the top right, then a bar for its
|
|
1023
|
+
* share of the list's spend with what it moved written across it. The bar is
|
|
1024
|
+
* what makes a list of forty readable — the two that carried the bill stand
|
|
1025
|
+
* out before a number is read.
|
|
1026
|
+
*/
|
|
1027
|
+
function usageModelRow(name, amount, share, details) {
|
|
1028
|
+
const row = el("div", "row-card usage-model");
|
|
1029
|
+
const line = el("div", "row-head");
|
|
1030
|
+
line.appendChild(el("span", "row-title", name));
|
|
1031
|
+
line.appendChild(el("span", "muted small usage-amount", amount));
|
|
1032
|
+
row.appendChild(line);
|
|
1033
|
+
const track = el("div", "usage-share");
|
|
1034
|
+
const fill = el("span", "usage-share-fill");
|
|
1035
|
+
// A sliver for anything that cost something, so it does not read as zero.
|
|
1036
|
+
fill.style.width = (share > 0 ? Math.max(share * 100, 0.8) : 0).toFixed(2) + "%";
|
|
1037
|
+
track.appendChild(fill);
|
|
1038
|
+
const pct = share >= 0.995 ? "100%" : share >= 0.01 ? Math.round(share * 100) + "%" : share > 0 ? "<1%" : "0%";
|
|
1039
|
+
const label = el("span", "usage-share-label");
|
|
1040
|
+
label.appendChild(el("span", "usage-model-pct", pct));
|
|
1041
|
+
if (details.length) label.appendChild(el("span", "usage-share-facts", details.join(" · ")));
|
|
1042
|
+
track.appendChild(label);
|
|
1043
|
+
row.appendChild(track);
|
|
1044
|
+
return row;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
/** What an account row moved: its tokens, or the media it made. */
|
|
1048
|
+
function usageSeriesFacts(entry) {
|
|
1049
|
+
const facts = [];
|
|
1050
|
+
const tokens = (entry.inputTokens || 0) + (entry.outputTokens || 0);
|
|
1051
|
+
if (tokens) facts.push(t("usage.tokens", { n: formatTokens(tokens) }));
|
|
1052
|
+
const media = entry.media || {};
|
|
1053
|
+
if (media.images) facts.push(t("usage.images", { n: media.images.toLocaleString(LOCALE) }));
|
|
1054
|
+
if (media.videoSeconds) facts.push(t("usage.videoSeconds", { n: usageSeconds(media.videoSeconds) }));
|
|
1055
|
+
if (media.audioSeconds) facts.push(t("usage.audioSeconds", { n: usageSeconds(media.audioSeconds) }));
|
|
1056
|
+
if (media.characters) facts.push(t("usage.characters", { n: media.characters.toLocaleString(LOCALE) }));
|
|
1057
|
+
return facts;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
/** How many rows a model list draws at a time. */
|
|
1061
|
+
const USAGE_LIST_PAGE = 20;
|
|
1062
|
+
|
|
1063
|
+
/**
|
|
1064
|
+
* A list of models in a box of its own height.
|
|
1065
|
+
*
|
|
1066
|
+
* A heavy month on OpenRouter is a hundred models, and drawn in full they
|
|
1067
|
+
* pushed the sessions and the scope note a dozen screens down. The box keeps
|
|
1068
|
+
* the page's shape and scrolls on its own; rows are drawn a page at a time as
|
|
1069
|
+
* its bottom comes near, because most visits read the top few.
|
|
1070
|
+
*/
|
|
1071
|
+
function usageScrollList(entries, render) {
|
|
1072
|
+
const box = el("div", "usage-list");
|
|
1073
|
+
let shown = 0;
|
|
1074
|
+
function more() {
|
|
1075
|
+
for (const entry of entries.slice(shown, shown + USAGE_LIST_PAGE)) box.appendChild(render(entry));
|
|
1076
|
+
shown = Math.min(entries.length, shown + USAGE_LIST_PAGE);
|
|
1077
|
+
if (shown >= entries.length) box.removeEventListener("scroll", near);
|
|
1078
|
+
}
|
|
1079
|
+
// Read off the scroll position rather than an observer on a sentinel: a
|
|
1080
|
+
// sentinel that stays in view after a page lands never re-enters it, and
|
|
1081
|
+
// the list stopped at the second page.
|
|
1082
|
+
function near() {
|
|
1083
|
+
if (box.scrollTop + box.clientHeight >= box.scrollHeight - 160) more();
|
|
1084
|
+
}
|
|
1085
|
+
more();
|
|
1086
|
+
if (shown < entries.length) box.addEventListener("scroll", near, { passive: true });
|
|
1087
|
+
return box;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1025
1090
|
/** What the sessions page is searching for, kept while the page is open. */
|
|
1026
1091
|
let usageSessionsQuery = "";
|
|
1027
1092
|
|
|
1028
1093
|
/**
|
|
1029
|
-
* Every session that spent something on this machine, newest first
|
|
1094
|
+
* Every session that spent something on this machine, newest first — the
|
|
1095
|
+
* Sessions page in the sidebar.
|
|
1030
1096
|
*
|
|
1031
1097
|
* Read a page at a time and drawn as the list is scrolled, because a machine
|
|
1032
1098
|
* used daily has thousands and the first screen is the one almost always
|
|
@@ -1038,13 +1104,6 @@ async function showUsageSessions() {
|
|
|
1038
1104
|
const panel = el("div", "panel usage");
|
|
1039
1105
|
const head = el("div", "row-head");
|
|
1040
1106
|
head.appendChild(el("h2", null, t("usage.allSessions")));
|
|
1041
|
-
const back = el("button", "ghost", t("usage.backToUsage"));
|
|
1042
|
-
back.type = "button";
|
|
1043
|
-
back.addEventListener("click", () => {
|
|
1044
|
-
setNav("usage");
|
|
1045
|
-
void showUsage();
|
|
1046
|
-
});
|
|
1047
|
-
head.appendChild(back);
|
|
1048
1107
|
panel.appendChild(head);
|
|
1049
1108
|
|
|
1050
1109
|
const search = el("input", "art-search");
|
|
@@ -1078,7 +1137,7 @@ async function showUsageSessions() {
|
|
|
1078
1137
|
const body = await getJson("/usage/sessions?limit=" + USAGE_SESSIONS_PAGE + "&offset=" + offset +
|
|
1079
1138
|
(usageSessionsQuery ? "&q=" + encodeURIComponent(usageSessionsQuery) : ""));
|
|
1080
1139
|
loading = false;
|
|
1081
|
-
if (asked !== generation || shownPanel !== "
|
|
1140
|
+
if (asked !== generation || shownPanel !== "nav-sessions") return;
|
|
1082
1141
|
if (body.error) {
|
|
1083
1142
|
foot.textContent = body.error;
|
|
1084
1143
|
return;
|
|
@@ -1109,7 +1168,7 @@ async function showUsageSessions() {
|
|
|
1109
1168
|
const sentinel = el("div", "usage-sessions-sentinel");
|
|
1110
1169
|
panel.appendChild(sentinel);
|
|
1111
1170
|
const observer = new IntersectionObserver((entries) => {
|
|
1112
|
-
if (shownPanel !== "
|
|
1171
|
+
if (shownPanel !== "nav-sessions") return void observer.disconnect();
|
|
1113
1172
|
if (entries.some((entry) => entry.isIntersecting)) void load();
|
|
1114
1173
|
}, { root: panelView, rootMargin: "0px 0px 400px 0px" });
|
|
1115
1174
|
observer.observe(sentinel);
|
|
@@ -1215,38 +1274,11 @@ async function showUsage() {
|
|
|
1215
1274
|
|
|
1216
1275
|
if (models.length) {
|
|
1217
1276
|
panel.appendChild(el("h3", "section-title", t("usage.byModel")));
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
row.appendChild(line);
|
|
1224
|
-
const bits = [
|
|
1225
|
-
t("usage.tokens", { n: formatTokens(moved(entry)) }),
|
|
1226
|
-
overSessions(entry.sessions),
|
|
1227
|
-
];
|
|
1228
|
-
const share = cachePct(entry);
|
|
1229
|
-
if (share !== null) bits.push(t("usage.cached", { pct: share }));
|
|
1230
|
-
row.appendChild(el("div", "muted small", bits.join(" · ")));
|
|
1231
|
-
panel.appendChild(row);
|
|
1232
|
-
}
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
const sessions = body.sessions || [];
|
|
1236
|
-
if (sessions.length) {
|
|
1237
|
-
// The latest few, and a way to all of them: a page of rows here pushed
|
|
1238
|
-
// the scope note below off the screen and still was not every session.
|
|
1239
|
-
const title = el("div", "usage-section-head");
|
|
1240
|
-
title.appendChild(el("h3", "section-title", t("usage.bySession")));
|
|
1241
|
-
const more = el("button", "ghost", t("usage.moreSessions"));
|
|
1242
|
-
more.type = "button";
|
|
1243
|
-
more.addEventListener("click", () => {
|
|
1244
|
-
setNav("usage-sessions");
|
|
1245
|
-
void showUsageSessions();
|
|
1246
|
-
});
|
|
1247
|
-
title.appendChild(more);
|
|
1248
|
-
panel.appendChild(title);
|
|
1249
|
-
for (const entry of sessions.slice(0, USAGE_RECENT)) panel.appendChild(usageSessionRow(entry));
|
|
1277
|
+
const spent = totals.costUsd || 0;
|
|
1278
|
+
panel.appendChild(usageScrollList(models, (entry) => {
|
|
1279
|
+
return usageModelRow(entry.model, "$" + (entry.costUsd || 0).toFixed(2), spent ? (entry.costUsd || 0) / spent : 0,
|
|
1280
|
+
[t("usage.tokens", { n: formatTokens(moved(entry)) })]);
|
|
1281
|
+
}));
|
|
1250
1282
|
}
|
|
1251
1283
|
|
|
1252
1284
|
if (!models.length) panel.appendChild(el("p", "muted", t("usage.none")));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usage.js","sourceRoot":"","sources":["../../../src/page/client/usage.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAA
|
|
1
|
+
{"version":3,"file":"usage.js","sourceRoot":"","sources":["../../../src/page/client/usage.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA4xC3B,CAAC"}
|
package/dist/page/style.js
CHANGED
|
@@ -32,32 +32,98 @@ code,pre,.mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,
|
|
|
32
32
|
|
|
33
33
|
/* sidebar */
|
|
34
34
|
aside{
|
|
35
|
-
width:
|
|
36
|
-
display:flex;flex-direction:column;overflow:hidden;
|
|
37
|
-
}
|
|
38
|
-
/*
|
|
39
|
-
|
|
35
|
+
--side-w:296px;width:var(--side-w);flex:0 0 var(--side-w);
|
|
36
|
+
display:flex;flex-direction:column;align-items:flex-end;overflow:hidden;
|
|
37
|
+
}
|
|
38
|
+
/* Folding, the sidebar narrows while its contents keep their width, pinned
|
|
39
|
+
to its right edge, so they slide out to the left behind the frame rather
|
|
40
|
+
than squeezing; the tab strip's inset grows by as much as the sidebar
|
|
41
|
+
loses, so the first tab glides to its place beside the toggle. Only once a
|
|
42
|
+
hand has touched the toggle: a page opened folded starts folded. */
|
|
43
|
+
#side-top,#side-body{width:var(--side-w);flex-shrink:0}
|
|
44
|
+
body.nav-anim>aside{transition:width .26s cubic-bezier(.2,0,0,1),flex-basis .26s cubic-bezier(.2,0,0,1)}
|
|
45
|
+
body.nav-anim #tabs{transition:padding-left .26s cubic-bezier(.2,0,0,1)}
|
|
46
|
+
body.nav-anim .wordmark{transition:opacity .14s}
|
|
47
|
+
body.nav-collapsed .wordmark{opacity:0}
|
|
48
|
+
@media (prefers-reduced-motion:reduce){body.nav-anim>aside,body.nav-anim #tabs,body.nav-anim .wordmark{transition:none}}
|
|
49
|
+
/*
|
|
50
|
+
* A frame with the panel as a card inside it.
|
|
51
|
+
*
|
|
52
|
+
* The top bar and the rail are one L in the window's own colour, --frame, and
|
|
53
|
+
* the panel and the page sit inside it. It takes a third colour: a top bar
|
|
54
|
+
* over the sidebar in either of the other two runs into the surface below it
|
|
55
|
+
* in that colour — a grey one boxed the white rail in, a white one ran on
|
|
56
|
+
* into the page. The panel is a card set into the frame, rounded all round
|
|
57
|
+
* and clear of the page by a margin. The traffic lights sit on the frame, one
|
|
58
|
+
* colour, where the rail alone is too narrow to hold them.
|
|
59
|
+
*/
|
|
60
|
+
#side-top{height:36px;flex:0 0 36px;background:var(--frame);display:flex;align-items:center;overflow:hidden}
|
|
61
|
+
/* Just past the toggle, which ends at 40px on the web and at 108px beside
|
|
62
|
+
the traffic lights, and fixed there, so the mark stays in the corner when
|
|
63
|
+
the sidebar folds and only the name goes with it. */
|
|
64
|
+
.brand{position:fixed;z-index:30;top:0;left:48px;height:36px;display:flex;align-items:center;gap:8px;white-space:nowrap;pointer-events:none}
|
|
65
|
+
body.inset-titlebar .brand{left:116px}
|
|
66
|
+
body.inset-titlebar #side-top{-webkit-app-region:drag}
|
|
67
|
+
#side-body{flex:1 1 auto;display:flex;min-height:0;background:var(--frame)}
|
|
68
|
+
/*
|
|
69
|
+
* Two columns: a rail of the app's places, and the panel of the work.
|
|
70
|
+
*
|
|
71
|
+
* The rail is the frame and the panel a card in it, so the two read as
|
|
72
|
+
* different surfaces without a line between them.
|
|
73
|
+
*/
|
|
74
|
+
#rail{
|
|
75
|
+
width:52px;flex:0 0 52px;background:var(--frame);display:flex;flex-direction:column;
|
|
76
|
+
align-items:center;padding:8px 0 10px;gap:6px;overflow:hidden;
|
|
77
|
+
}
|
|
78
|
+
#rail #nav{padding:0;gap:4px;align-items:center}
|
|
79
|
+
#rail .nav-item{width:36px;height:36px;padding:0;justify-content:center;border-radius:9px;color:var(--dim)}
|
|
80
|
+
#rail .nav-item:hover{color:var(--fg)}
|
|
81
|
+
#rail .nav-item.active{color:var(--fg)}
|
|
82
|
+
/* Drawn at 18px on the 16 grid, the shared 1.5 stroke would render at 1.7px
|
|
83
|
+
and read heavier than every other icon on the page; 1.2 lands near 1.35. */
|
|
84
|
+
#rail .nav-item .ico{width:18px;height:18px;stroke-width:1.2}
|
|
85
|
+
/* The panel's icons follow at their own 14-16px, so the rail and the panel
|
|
86
|
+
beside it draw at one weight. Matched on the attribute, so the mark keeps its 2.5. */
|
|
87
|
+
#side-panel svg[stroke-width="1.5"]{stroke-width:1.25}
|
|
88
|
+
#rail .nav-item>span{display:none}
|
|
89
|
+
.rail-tip{
|
|
90
|
+
position:fixed;z-index:80;transform:translateY(-50%);padding:4px 9px;border-radius:7px;
|
|
91
|
+
background:var(--accent);color:var(--bg);font-size:12px;line-height:1.4;white-space:nowrap;
|
|
92
|
+
pointer-events:none;opacity:0;transition:opacity .08s;
|
|
93
|
+
}
|
|
94
|
+
.rail-tip.on{opacity:1}
|
|
95
|
+
#side-panel{
|
|
96
|
+
width:236px;flex:0 0 236px;background:var(--panel);border-radius:12px;margin:0 8px 8px 0;
|
|
97
|
+
display:flex;flex-direction:column;overflow:hidden;
|
|
98
|
+
}
|
|
99
|
+
#side-new{padding:0 8px 2px;display:flex;flex-direction:column}
|
|
100
|
+
/* Folded away on a desktop, the whole sidebar goes, rail and all, and the
|
|
101
|
+
page has the window: keeping the rail left a strip of chrome and pushed the
|
|
102
|
+
tabs a rail's width plus the traffic lights in from the edge. */
|
|
40
103
|
body.nav-collapsed>aside{width:0;flex-basis:0}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
body.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
104
|
+
/* In the top bar, at the same spot folded or not, so the click that folded the
|
|
105
|
+
sidebar can bring it straight back. On a desktop it steps past the traffic
|
|
106
|
+
lights (14-74px, set in desktop/main.js). */
|
|
107
|
+
#nav-toggle{
|
|
108
|
+
position:fixed;z-index:30;top:4px;left:12px;width:28px;height:28px;border:0;border-radius:7px;
|
|
109
|
+
background:transparent;color:var(--dim);cursor:pointer;padding:0;
|
|
110
|
+
display:grid;place-items:center;-webkit-app-region:no-drag;
|
|
111
|
+
}
|
|
112
|
+
#nav-toggle:hover{background:var(--hover);color:var(--fg)}
|
|
113
|
+
#nav-toggle svg{width:16px;height:16px;display:block;shape-rendering:geometricPrecision}
|
|
114
|
+
body.inset-titlebar #nav-toggle{left:80px}
|
|
115
|
+
body.shelf-full #nav-toggle{display:none}
|
|
116
|
+
/* The window has no title bar, so the strip's empty stretch is its handle.
|
|
117
|
+
Folded, the strip is the whole top bar and starts past the mark beside the
|
|
118
|
+
toggle: 66px on the web, 134px beside the traffic lights. */
|
|
119
|
+
body.inset-titlebar #tabs{-webkit-app-region:drag}
|
|
120
|
+
body.inset-titlebar #tabs>*{-webkit-app-region:no-drag}
|
|
121
|
+
body.nav-collapsed #tabs{padding-left:74px}
|
|
122
|
+
body.inset-titlebar.nav-collapsed #tabs{padding-left:142px}
|
|
123
|
+
/* The name is in the top bar on a wide screen, so the panel's header is only
|
|
124
|
+
the phone drawer's, where it carries the name and the close button. */
|
|
125
|
+
aside header{display:none;padding:8px 14px;min-height:44px;gap:8px;align-items:center}
|
|
126
|
+
#side-panel{padding-top:8px}
|
|
61
127
|
.mark{width:18px;height:18px;color:var(--fg);opacity:.85;flex:0 0 auto}
|
|
62
128
|
.mark svg{width:18px;height:18px;display:block}
|
|
63
129
|
/* Slightly tightened and a touch heavier than body text, which is what makes
|
|
@@ -518,31 +584,43 @@ details.provider>summary .vmark.filled,.pp-item .vmark.filled{padding:0}
|
|
|
518
584
|
* is in front. Spanning both left a band of empty chrome above the drawer that
|
|
519
585
|
* belonged to neither.
|
|
520
586
|
*
|
|
521
|
-
* The strip is the
|
|
522
|
-
* front tab is cut from the page under it — the
|
|
523
|
-
*
|
|
524
|
-
*
|
|
525
|
-
*
|
|
526
|
-
* or a grey line would sit between the front tab and the page it is part of.
|
|
587
|
+
* The strip is the top of the frame, at #side-top's height, so one bar runs
|
|
588
|
+
* across the window, and the front tab is cut from the page under it — the
|
|
589
|
+
* shape every browser uses. No bottom padding, or a line of frame would sit
|
|
590
|
+
* between the front tab and the page it is part of; no left padding, so the
|
|
591
|
+
* first tab's edge is the page's.
|
|
527
592
|
*/
|
|
528
593
|
#tabs{
|
|
529
|
-
display:flex;align-items:
|
|
530
|
-
background:var(--
|
|
594
|
+
display:flex;align-items:flex-end;gap:2px;height:36px;padding:0 12px 0 0;overflow-x:auto;flex:0 0 auto;
|
|
595
|
+
background:var(--frame);
|
|
531
596
|
}
|
|
532
597
|
#tabs:empty{display:none}
|
|
598
|
+
/* A scrollbar here takes its height out of the strip and lifts every tab off
|
|
599
|
+
the page, and a hovered tab's buttons were enough to bring one on. Tabs
|
|
600
|
+
shrink first, as a browser's do, and past that the strip still scrolls by
|
|
601
|
+
wheel or trackpad, only without the bar. */
|
|
602
|
+
#tabs{scrollbar-width:none}
|
|
603
|
+
#tabs::-webkit-scrollbar{display:none}
|
|
533
604
|
.tab{
|
|
534
|
-
display:flex;align-items:center;gap:7px;max-width:230px;height:30px;padding:0
|
|
605
|
+
display:flex;align-items:center;gap:7px;max-width:230px;min-width:72px;height:30px;padding:0 14px;
|
|
535
606
|
border:0;border-radius:8px 8px 0 0;background:transparent;color:var(--dim);
|
|
536
|
-
font:inherit;font-size:12.5px;cursor:pointer;flex:0
|
|
537
|
-
}
|
|
538
|
-
|
|
607
|
+
font:inherit;font-size:12.5px;cursor:pointer;flex:0 1 auto;
|
|
608
|
+
}
|
|
609
|
+
/* The tab under the pointer or in front keeps its width, so its buttons fit
|
|
610
|
+
and it is the others that give way. */
|
|
611
|
+
.tab:hover,.tab:focus-within,.tab.on{flex-shrink:0}
|
|
612
|
+
/* Hovered, a tab lifts halfway toward the page it would become, the way a
|
|
613
|
+
browser's do, rather than sinking into a darker grey the front tab never
|
|
614
|
+
is. Mixed from --bg so the dark theme lifts the same way. */
|
|
615
|
+
.tab:hover{background:color-mix(in srgb,var(--bg) 60%,transparent);color:var(--fg)}
|
|
539
616
|
.tab.on{background:var(--bg);color:var(--fg)}
|
|
540
|
-
.tab .
|
|
617
|
+
.tab-page .ico-sm{width:14px;height:14px;opacity:.75}
|
|
618
|
+
.tab .label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
|
|
541
619
|
/* Which project a tab is in, ahead of its title. Set quieter than the title,
|
|
542
620
|
which is still what identifies it. */
|
|
543
621
|
.tab .where{
|
|
544
622
|
max-width:80px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
|
|
545
|
-
font-size:11px;opacity:.6;flex:0
|
|
623
|
+
font-size:11px;opacity:.6;flex:0 1 auto;min-width:0;
|
|
546
624
|
}
|
|
547
625
|
.tab .where::after{content:"/";padding-left:5px;opacity:.5}
|
|
548
626
|
.tab .dot{width:6px;height:6px;border-radius:50%;background:var(--brand);flex:0 0 auto}
|
|
@@ -556,6 +634,10 @@ details.provider>summary .vmark.filled,.pp-item .vmark.filled{padding:0}
|
|
|
556
634
|
transparent — a transparent button still holds its space. A running
|
|
557
635
|
browser keeps its button, because its dot is what says so. */
|
|
558
636
|
.tab:hover .x,.tab:focus-within .x{display:grid}
|
|
637
|
+
/* Even padding while the title is alone, as a label is set; the right side
|
|
638
|
+
gives some back when a button comes in, so the button sits near the edge
|
|
639
|
+
and the tab grows by less. */
|
|
640
|
+
.tab:hover,.tab:focus-within,.tab:has(.tab-browser.running){padding-right:6px}
|
|
559
641
|
/* The tab's browser button's size. Much smaller and the 1.5px strokes thin
|
|
560
642
|
below a pixel and blur; the full 16px outweighs the tab's own text. */
|
|
561
643
|
.tab .x svg{width:14px;height:14px}
|
|
@@ -571,7 +653,7 @@ details.provider>summary .vmark.filled,.pp-item .vmark.filled{padding:0}
|
|
|
571
653
|
.tab:hover .tab-browser,.tab.on .tab-browser,.tab-browser.running{opacity:.62}
|
|
572
654
|
.tab-browser:hover,.tab-browser.on{opacity:1;background:var(--sel)}
|
|
573
655
|
.tab-browser svg{width:14px;height:14px}
|
|
574
|
-
.tab-browser i{display:none;position:absolute;right:2px;top:2px;width:5px;height:5px;border-radius:50%;background:var(--ok);box-shadow:0 0 0 1.5px var(--
|
|
656
|
+
.tab-browser i{display:none;position:absolute;right:2px;top:2px;width:5px;height:5px;border-radius:50%;background:var(--ok);box-shadow:0 0 0 1.5px var(--frame)}
|
|
575
657
|
.tab.on .tab-browser i{box-shadow:0 0 0 1.5px var(--bg)}
|
|
576
658
|
.tab-browser.running i{display:block}
|
|
577
659
|
#tab-add{
|
|
@@ -1472,6 +1554,17 @@ details.setting>summary:focus-visible{outline:none;background:var(--hover)}
|
|
|
1472
1554
|
.usage-kind.on{background:var(--accent);color:var(--bg);box-shadow:none}
|
|
1473
1555
|
.usage-kind.on .usage-kind-label,.usage-kind.on .usage-kind-sub,.usage-kind.on.empty .usage-kind-value{color:var(--bg);opacity:.72}
|
|
1474
1556
|
.usage-counted{margin:0 0 10px}
|
|
1557
|
+
/* A model list: its own height, its own scroll, no frame drawn round it. */
|
|
1558
|
+
.usage-list{max-height:440px;overflow-y:auto;overscroll-behavior:contain}
|
|
1559
|
+
.usage-list>.row-card:last-of-type{margin-bottom:0}
|
|
1560
|
+
/* The share bar: a filled track, no outline, thick enough to carry its facts. */
|
|
1561
|
+
.usage-model{padding:12px 14px}
|
|
1562
|
+
.usage-model .row-head{margin-bottom:8px}
|
|
1563
|
+
.usage-share{position:relative;height:28px;border-radius:6px;background:var(--field);overflow:hidden}
|
|
1564
|
+
.usage-share-fill{position:absolute;inset:0 auto 0 0;border-radius:6px;background:color-mix(in srgb,var(--brand) 34%,transparent)}
|
|
1565
|
+
.usage-share-label{position:relative;display:flex;align-items:center;gap:12px;height:100%;padding:0 10px;min-width:0;font-size:12px;font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
1566
|
+
.usage-model-pct{flex:none;color:var(--fg);font-weight:600}
|
|
1567
|
+
.usage-share-facts{min-width:0;overflow:hidden;text-overflow:ellipsis;color:var(--fg);opacity:.72}
|
|
1475
1568
|
/* Amounts in one column at the right edge, so a list is compared by glancing down it. */
|
|
1476
1569
|
.usage .row-head .usage-amount{margin-left:auto;flex:none;font-variant-numeric:tabular-nums}
|
|
1477
1570
|
.usage .row-head .row-title{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
@@ -2902,19 +2995,31 @@ dialog::backdrop{background:rgba(0,0,0,.55)}
|
|
|
2902
2995
|
*/
|
|
2903
2996
|
@media (max-width:760px){
|
|
2904
2997
|
body>aside{
|
|
2905
|
-
position:fixed;inset:0 auto 0 0;z-index:20;width:min(
|
|
2998
|
+
position:fixed;inset:0 auto 0 0;z-index:20;width:min(332px,90vw);flex-basis:auto;
|
|
2906
2999
|
transform:translateX(-100%);transition:transform .2s;
|
|
2907
3000
|
padding-bottom:env(safe-area-inset-bottom);
|
|
2908
3001
|
}
|
|
2909
3002
|
body>aside.open{transform:none;box-shadow:0 0 0 1px #0000000a,0 12px 40px #00000040}
|
|
2910
3003
|
body.nav-open #scrim{display:block}
|
|
2911
3004
|
#nav-close{display:grid}
|
|
2912
|
-
#nav-
|
|
2913
|
-
|
|
2914
|
-
|
|
3005
|
+
#nav-toggle,#side-top{display:none}
|
|
3006
|
+
aside header{display:flex}
|
|
3007
|
+
#side-panel{padding-top:0}
|
|
3008
|
+
#side-panel{border-radius:0;margin:0}
|
|
3009
|
+
body.nav-collapsed>aside{width:min(332px,90vw);flex-basis:auto}
|
|
3010
|
+
#side-body{width:auto;align-self:stretch}
|
|
3011
|
+
#side-panel,body.nav-collapsed #side-panel{flex:1 1 auto;width:auto;min-width:0;margin:0}
|
|
2915
3012
|
#burger{display:grid}
|
|
2916
|
-
|
|
2917
|
-
|
|
3013
|
+
/* One bar, not two: the burger sits at the head of the tab strip, in the
|
|
3014
|
+
frame's colour, rather than on a white row of its own above it. #top
|
|
3015
|
+
keeps its row only while it has the changes button to show. */
|
|
3016
|
+
#top{padding:0;min-height:0}
|
|
3017
|
+
#top:has(#changes-btn:not([hidden])){padding:6px 10px 6px 52px;background:var(--frame)}
|
|
3018
|
+
#burger{position:fixed;z-index:15;top:0;left:4px;width:40px;height:36px;margin:0;border-radius:8px}
|
|
3019
|
+
#tabs,body.nav-collapsed #tabs{padding-left:44px}
|
|
3020
|
+
/* The front tab has the width it needs: its buttons show on a phone, where
|
|
3021
|
+
there is no hover to wait for, and they left the title as "Unt…". */
|
|
3022
|
+
.tab.on{max-width:none}
|
|
2918
3023
|
|
|
2919
3024
|
/* Both panes are the screen while open, and each gets a way out of its own:
|
|
2920
3025
|
the top bar with the changes button is underneath them. */
|
package/dist/page/style.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"style.js","sourceRoot":"","sources":["../../src/page/style.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,0EAA0E;AAC1E,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAA;EAC7B,SAAS
|
|
1
|
+
{"version":3,"file":"style.js","sourceRoot":"","sources":["../../src/page/style.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,0EAA0E;AAC1E,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAA;EAC7B,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4kGV,CAAC"}
|