Skip to content

feat(ui): all-time range, filter fixes, currency switch, model-share card, artwork - #4

Merged
xinspark merged 12 commits into
bennett-dsh:mainfrom
xinspark:feat/ui-parity
Oct 18, 2026
Merged

xinspark merged 12 commits into
bennett-dsh:mainfrom
xinspark:feat/ui-parity

Conversation

@xinspark

Copy link
Copy Markdown
Collaborator

Six panel improvements, plus three defects found while exercising them on a live profile. Everything stays inside the plugin's existing shape: one client file, CSS in the same template string, both dictionaries, labelled ctx.effect() calls, no new dependencies.

What changed

1. All-time range, no artificial span limit

rangeFor('all') maps to {from: 0, to: now} with fill: false, so the all-time view only buckets days that carry data and never approaches MAX_BUCKETS. The custom range no longer advertises a "30 days max" limit: the only real bound is the bucket guard, and the hint now says so. fill stays on for the rolling presets, where empty days are meaningful.

2. Model filter: candidates no longer collapse, and survive records without a provider

Two separate defects:

  • The host collected seenModels after applying the model filter, so the response only offered the models already selected; picking one hid the rest.
  • The client composed provider + '/' + model unconditionally, while the host key is the bare model when a record has no provider. Selecting such a candidate matched nothing, and the panel silently showed zero.

The response now carries the host's composed key for every model and the client uses it verbatim, so both sides share one definition.

3. Popovers are no longer clipped by the settings panel

.tsd-popover was absolutely positioned inside a panel with overflow: hidden. A small useFixedPanel(anchorRef, open, width) hook measures the anchor, positions the panel fixed, flips it upward when there is no room below, and caps maxHeight. The model filter, the custom date range and the new currency menu all use it.

4. Currency switch for the reference cost (CNY / USD, fixed 1 USD = 7 CNY)

  • DEFAULT_PRICES holds DeepSeek's USD list (0.27 / 1.10 / 0.07) but was printed with a ¥. The table is now explicitly USD-based and CNY display multiplies by 7, which lands on DeepSeek's published CNY list — so this also corrects the previous numbers, which were 7× too low.
  • Conversion happens at the output boundary in query (costOf, daily, byModel, total, heatmap); the price table itself is never rewritten, and hasPrice still decides priced vs unpriced.
  • The switcher lives in the cost card's value row (symbol + chevron). The symbol has exactly one owner: costDigits returns digits only, since two owners produced ¥ ¥14.78. The menu states the fixed rate.
  • The choice is remembered per browser via localStorage as a display preference; it never touches the plugin configuration.

5. Model-share card (6th KPI)

A 10 px pill bar with an 11 px caption (deepseek-flash 53.5%), as a normal .tsd-cards item. Colours come from families the theme actually ships (--dsw-static-{deepseek,green,amber,red}-*); "others" is tinted deepseek-300 rather than neutral grey, which reads as disabled on a dark card. Per-segment detail is a fixed tooltip (the heatmap's technique) instead of a native title, whose black box covered the adjacent card.

6. Artwork and plugin-page metadata

  • package.json#icon plus icon.svg (transparent 36×36, diagonal #67C7FE → #3F77D8).
  • locale/{en,zh}.json carrying meta.title / meta.description and the matching ./locale/*.json export: without them the plugin page falls back to the bare package name and the truncated English description.
  • A settings-nav glyph, because settings.section cannot carry an icon: the runtime's navIcon(id) is a closed list and returns a gear for any other id. The patch only adds a data-tsd-nav attribute and paints the glyph with ::before + mask; the React-owned <svg> is never replaced (doing so makes React's removeChild throw when the panel unmounts). If the row cannot be identified, nothing happens. This block should be deleted once settings.section accepts an icon.

Verification

  • npm run build, node --check lib/client/index.js, npm test — 83 pass, 0 fail (6 new host tests for the filter and currency paths).
  • Installed into a live profile and exercised by hand: every range preset, empty-provider filtering, both currencies, the share card, the artwork.
  • There is no client test harness in this repo, so client-only changes are covered by manual checks only.

Ticking one model collapsed the candidate list to that single entry: the
query loop registered a model in `models` only after the selection filter,
so the response advertised just the selected key. Register the candidate
first, keep the aggregate filter where it was, and ship the composite `key`
on each entry so the client stops re-deriving `provider/model`. That second
half also fixes a silent blank report: with an empty provider the client
built `/model`, which matched nothing, so total/daily/byModel all came back 0.

The filter popovers were `position: absolute` inside the settings panel,
which is `overflow: hidden`, so their right half was clipped. They now sit in
a measured `position: fixed` panel that flips up when the space below is too
tight, caps its height, and re-measures on scroll/resize. The model list
declares `min-height: 0` so it shrinks instead of being cut, and the popover
declares border-box so `width: 320px` really is 320px.
The custom-range popover said "date only, up to 30 days", but nothing
enforced it: the host only checks that the bounds are numbers, the date
inputs carry no min/max, and the one real limit is MAX_BUCKETS (2000 output
buckets, only reached while zero-filling). The hint was the only thing that
looked like a limit.

Add an "all" preset that starts at 0 and asks for present-only buckets
(fill: false), so it never zero-fills 1970..today and never trips
MAX_BUCKETS. Custom ranges keep the existing day-step collapse, which governs
chart density rather than correctness; the hint now says so plainly.
`currency` was a bare display symbol with no conversion behind it, while the
default table is DeepSeek's USD list price per 1M tokens — printed with a ¥
sign, so the visible CNY number sat about 7x below the real CNY list
(0.27 / 1.10 / 0.07 USD is 2 / 8 / 0.5 CNY at 7:1).

The table is now documented as USD and never rewritten: `query` accepts a
currency, converts once at the output boundary (daily, byModel, total and
heatmap, a single rounding at 1e-6 so the sums stay exact), and returns the
currency it actually used. CNY/USD resolve through a fixed 7:1 table; unknown
or empty values fall back to the configured currency instead of throwing.

Client side, the reference-cost card carries a ¥/$ picker in its value row.
The choice is remembered locally (a display preference, not plugin config) and
triggers a re-query, because conversion happens host-side. Amounts below half
a cent render "<0.01" instead of a misleading 0.00.
The refresh button was a 34px bordered tile wrapping a 24-grid, stroke-2
glyph, which read as a foreign control inside a settings panel. It is now a
28px transparent icon button (hover fills, no border; the shared
:focus-visible group already covers it) using the DSH native refresh glyph on
its 16-grid at stroke 1.5, and it disables itself with aria-busy while a
refresh is in flight.

`SvgIcon` now accepts an icon that carries its own viewBox, stroke width and
path list, so both grid systems coexist; the string-based call sites are
untouched.

"成本" now reads "参考成本" ("Est. cost" in English) on the card and in the
model table header, which is what the number actually is: a price-table
estimate, not a billed amount.
Token share per model lived only in the summary table's ratio column, so at a
glance there was no sense of which model dominates the selected range.

Add it as a sixth cell of the same `.tsd-cards` grid instead of a new
container: the grid is `repeat(auto-fit, minmax(150px, 1fr))` and only direct
children take a cell. The cell reuses the host `byModel` rows the summary
table already renders, so the two views cannot drift — top four models by
total tokens plus an aggregated "Others" segment; hover/focus switches the
caption, and each segment's `title` carries input/output/cost with the same
unpriced rule as the table (tokens > 0 and no cost).

Deliberately no floating tooltip: this card sits against the settings panel's
overflow boundary, so the detail rides on the native title rather than
re-introducing the clipping the popovers just got rid of.
Two places, one shape. `icon.svg` (three bars, vertical gradient) is declared
in `package.json#icon` and shipped through `files[]`, so the plugin manager can
render it on the plugin card; the settings nav row uses the same motif.

The nav icon has no contract to go through: `settings.section` projects only
id/order/label, and the settings shell resolves the glyph from a hardcoded id
whitelist, so a third-party section always falls back to the gear. Registering
under a first-party id would take over a built-in row instead, so this marks
its own row by visible label and swaps the gear for a masked glyph — the same
technique the other third-party plugins in this profile use. It never replaces
the svg node (React throws on removeChild during unmount), degrades to the gear
when the row or the DOM is missing, and is disposed with the fiber. The mask
reads only the alpha channel, hence the flat single-colour glyph.

Commented for deletion once the shell grows an icon field on the section
contract.
Document the six dashboard changes and bump to a feature release.

The default price table stays DeepSeek's USD list; it is now converted for
display instead of being printed with a ¥ sign, which is why the visible CNY
figures grow about 7x. Called out in the changelog so it does not later read
as a regression.
The previous version put the switcher inside the value row, and
`formatCost` already prefixes the symbol, so the card rendered the
symbol twice (`¥ ▾ ¥14.78`) while the 15px symbol + 11px chevron broke
the numeric baseline.

Split the two concerns: `formatCost` stays the only place that draws a
symbol, and the switcher becomes a card-header action.

- `.tsd-card-actions` (margin-left:auto) holds card-level controls;
  the trigger is a 20px transparent button with a hover fill, matching
  the refresh button in `StatsHeader`.
- The trigger shows the currency *code* (`CNY`/`USD`) rather than the
  symbol: the symbol is already on the number, and `$` alone is
  ambiguous. Arrow keys change the value while closed, move the
  selection while open, Esc closes.
- The menu reuses `.tsd-popover` + `useFixedPanel`, shows symbol + name
  + a check on the active row, and states the rate in
  `.tsd-popover-note` (`1 USD = 7 CNY`).
The card only had a head, a thick bar and one caption line, so it read as
a chart wedged into the KPI grid instead of a KPI. Its detail text also
went through a native `title`, whose black box covered the neighbouring
card.

- Layout now mirrors the other cards: head / value (largest segment's
  percentage, same 20px/700 as `21,536`) / sub (that segment's short
  model name, 12px ellipsis) / a 4px bar under both.
- Hovering or focusing a segment swaps value and sub together, then
  restores the largest segment on leave.
- Detail moved from `title` to the shared `.tsd-tooltip` drawn at fixed
  coordinates from `getBoundingClientRect` (the heatmap already does
  this), so the settings panel's `overflow` cannot clip it.
- The "others" segment is a tinted brand colour (`color-mix`) rather
  than grey, which reads as disabled.
The plugin page showed the bare package name and the long English
`description`, because `readPluginMeta` in `dsh-app-boot` falls back to
`package.json#name` / `#description` when the package ships no
`locale/<lang>.json`.

Ship the dictionaries the loader already looks for:

- `locale/en.json` and `locale/zh.json` with `{"meta":{title,
  description}}`, the shape `dictionariesOf` parses.
- `"./locale/*.json"` in `exports` and `locale` in `files`, since the
  loader resolves `<pkg>/locale/en.json` through the package resolver
  and the tarball must carry it.
- Redraw `icon.svg` in the shipped art's idiom: transparent 36x36,
  opaque shapes on a diagonal `#67C7FE → #3F77D8` gradient. The old
  art stacked three 50/75/100% opacity bars, which read as washed out
  next to the official icons.
…re card

Side-by-side, the row-style versions read better, so both go back to the
shape the fork ships while keeping the fixes that came out of the remake.

Currency switch:
- Trigger returns to the value row and draws the symbol itself, so the
  symbol appears exactly once (`formatCost` no longer supplies one) and
  the control inherits the 20px/700 number's baseline instead of
  fighting it.
- Keeps the keyboard handling (arrows/Esc/Enter) and the menu note that
  states the fixed 1 USD = 7 CNY rate.

Model share:
- Back to a 10px pill bar with an 11px tertiary caption underneath
  (name + percentage), which keeps the KPI numbers as the row's
  headlines.
- Keeps the fixed-position tooltip: the native `title` it replaced was
  what painted a black box over the neighbouring card.
- Drops the icon's invented pie path for the shipped two-stroke glyph,
  and drops the now-unused `shareHint` string.
…ours

Three defects from the first work-exp pass:

- The cost card printed `¥ ¥14.78`: the switcher draws the symbol and
  `formatCost` still prefixed it. Split out `costDigits` so the card
  renders digits only and the symbol has exactly one owner.
- The rate note ran out of the popover: the switcher lives inside
  `.tsd-card-value`, which sets `white-space: nowrap` for the number,
  and the popover inherited it. Reset to `normal` on the popover.
- Two share segments rendered as grey and white: `--color-blue-500`
  and `--color-orange-500` are not DSH tokens, so those backgrounds
  fell back to nothing. Use only families the theme actually ships
  (deepseek / green / amber / red) and tint "others" with
  `--dsw-static-deepseek-300` instead of a neutral grey, which reads as
  disabled on a dark card.
@xinspark
xinspark merged commit 6c326bb into bennett-dsh:main Oct 18, 2026
2 checks passed
@xinspark

xinspark commented Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator Author

@ruanbw 一个发布上的缺口想确认下:v0.3.0 已经合进 main、打了 tag、Release 也出了,但 npm 上的 dsh-plugin-usage 还停在 0.2.2。

README 写的两种装法(dsh plugin add dsh-plugin-usage、设置 → 插件 → 添加插件)走的都是 npm,所以照文档装还是旧构建,这一版的「全部」范围、货币切换、模型占比卡、图标和插件页文案都不会出现。

release.yml 目前只到「校验 tag → 构建 → 测试 → 建 Release」,没有发布步骤。你方便的话,本地 npm publish 一次,或者在 release.yml 里加一步 npm publish + NPM_TOKEN,我都可以配合。如果卡的是 npm 权限,把我加成包的合作者也行——发之前我会先跟你确认,版本号 / tag / dist-tag 的策略我不动。不着急。

@ruanbw

ruanbw commented Oct 18, 2026

Copy link
Copy Markdown
Contributor

@xinspark 已经发布到 npm 官方源(v0.3.0),并将 npm 页面地址(https://www.npmjs.com/package/dsh-plugin-usage)加到了仓库的 Website/URL 中。感谢提醒!

@xinspark

Copy link
Copy Markdown
Collaborator Author

@ruanbw 谢谢,确认过了:npm 上 0.3.0 的构建与 v0.3.0 源码一致(拉下来和本地重新构建逐行比对,只有内联 sourcemap 的路径不同),我这边已经改用 npm 版本安装。另外我按 awesome-dsh-plugin 的格式提了一条收录(#7986),条目里引用的就是这个仓库。

@xinspark
xinspark deleted the feat/ui-parity branch October 30, 2026 18:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants