@fasl-work/caos-app-shell 0.7.2 → 0.8.1

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 CHANGED
@@ -3,15 +3,18 @@
3
3
  [![License](https://img.shields.io/github/license/fsantibanezleal/CAOS_APP_SHELL)](LICENSE)
4
4
  [![Version](https://img.shields.io/github/v/tag/fsantibanezleal/CAOS_APP_SHELL?label=version&sort=semver)](https://github.com/fsantibanezleal/CAOS_APP_SHELL/tags)
5
5
 
6
- The shared **web-app shell + content primitives + design system** for the CAOS / Faena public apps
7
- (implements [ADR-0016](https://github.com/fsantibanezleal)). Define the header, footer, theme, language
8
- toggle and content primitives **once** here; every app consumes them so the chrome is identical and a
9
- fix lands in one place.
6
+ The shared **web-app shell, route types, views, drawings, design system and measured gate** of the CAOS / Faena
7
+ public apps, one of the three parts of the CAOS base with the product template
8
+ ([CAOS_PRODUCT_TEMPLATE](https://github.com/fsantibanezleal/CAOS_PRODUCT_TEMPLATE)) and the policy (ADR-0016,
9
+ ADR-0017, ADR-0071, ADR-0078). The frame, the header, the icons, the tab structure, the viewport, the areas, the text in
10
+ drawings and the numbers are defined **once** here; every product composes them, so a fix lands in one place and no
11
+ product writes them again. **Documentation: [`docs/`](docs/README.md)** (structure, tokens, navigation, views,
12
+ drawings, icons and numbers, the gate, releasing).
10
13
 
11
14
  ## Install
12
15
 
13
16
  ```bash
14
- npm i @fasl-work/caos-app-shell
17
+ npm i -E @fasl-work/caos-app-shell # an exact pin: the build you gated is the build you ship
15
18
  # peer deps (the app provides them):
16
19
  npm i react react-dom react-router lucide-react katex zustand
17
20
  # optional: the house chart and the measured gate
@@ -50,7 +53,11 @@ const config: ShellConfig = {
50
53
  </BrowserRouter>
51
54
  ```
52
55
 
53
- - **Land on the tool:** `/` is the workbench; the five documentation routes are separate pages.
56
+ - **Three route types** ([docs/01](docs/01_structure.md)): the **workbench** (`CaseWorkbench`, a product about
57
+ cases), the **surface** (`SurfacePage`, a hub, an explorer or a console: a head, one row of views, the open view
58
+ filling the rest), the **document** (`DocPage`). Under `contain` each fills or scrolls inside the viewport; no product
59
+ writes containment CSS.
60
+ - **Land on the tool:** `/` is the App route; the five documentation routes are separate pages.
54
61
  - **One React, one router:** set `resolve.dedupe: ['react', 'react-dom', 'react-router']` in Vite. A second copy
55
62
  leaves the shell outside the router context; `AppShell` reports it with `console.error` and names the cause.
56
63
  - **Product metadata:** `license` and `visibility` are required: the footer shows the product's licence and a
@@ -70,11 +77,16 @@ const config: ShellConfig = {
70
77
  - **Drawing:** put canvas, WebGL and SVG instruments in a `Stage` (or `useStageSize`): nothing draws at zero
71
78
  size, and colours come from `useThemeTokens()` (a canvas cannot read CSS variables).
72
79
  - **Charts:** `import { UPlotChart } from "@fasl-work/caos-app-shell/chart"`: x and y titles with units, series
73
- by token colour, `marks` for what the engine detected, a key of every series when there are two or more, a cursor
74
- readout row, theme and language rebuilds. Keep series labels short: the key wraps under the plot.
80
+ by token colour, `marks` for what the engine detected (placed inside the plot, haloed), a key of every series when
81
+ there are two or more, a cursor readout row, theme and language rebuilds, one tick notation per axis. Categories go
82
+ in `BarChart` (main entry, no uPlot). A drawing of your own measures its labels with the text kit (`textWidth`,
83
+ `fitLabel`, `niceTicks`) and declares `data-chart` ([docs/05](docs/05_drawings.md)).
84
+ - **Views in the URL:** `CaseWorkbench` and `SurfacePage` hold the open view in `?view=` (and the case selector the
85
+ case in `?case=`), so a view can be shared and survives a reload.
86
+ - **Icons:** lucide icons are sized by where they sit (`svg.lucide`); pass no `size` ([docs/06](docs/06_icons-and-numbers.md)).
75
87
  - **Numbers:** `formatNumber(value, lang, opts)` or `useFormat()`; never `toFixed` in a view. A magnitude below 1e-4
76
88
  is written in scientific notation (a p-value of 1e-200 is `1E-200`, not two hundred zeros); give `digits`, not
77
- `decimals`, for p-values.
89
+ `decimals`, for p-values. A percent and a unit are joined by a no-break space.
78
90
  - **Documentation routes:** `DocPage` and `DocSection` (each section ends in its own `Refs`, or states why it
79
91
  cites nothing), `Equation` with a caption, `CitationsProvider` and `Cite` (every citation has a DOI or URL).
80
92
  `Tabs`, `SubTabs` and `TabGroups` are controllable and render only the open panel inside a `PanelBoundary`.
@@ -85,9 +97,14 @@ const config: ShellConfig = {
85
97
  every route, tab, sub-tab and case at five sizes, both themes and both languages, and fails on: a wrong subject
86
98
  (G1) or mode (G2), console errors and 4xx/5xx (G3), broken deep links or HTML for an artifact (G4), anything a
87
99
  reader cannot scroll to, see whole or click (G5), blank or underfilled drawings and an instrument under half the
88
- viewport (G6), a view that never settles (G7), loops at rest (G8), and controls that change nothing (G9). The
89
- report (`gate-output/gate-report.json`) lists every failure and the smallest margins against each floor.
90
- - **Your own CSS:** never redefine a class in `reserved-classes.json`; the product guard reads that list.
100
+ viewport (G6), a view that never settles (G7), loops at rest (G8), controls that change nothing (G9), labels cut or
101
+ overlapping in a drawing, also in a wider fallback font (G10), a decimal point on a Spanish page (G11), a vertical
102
+ sub-tab list that scrolls away (G12), text under WCAG AA contrast (G13). The report
103
+ (`gate-output/gate-report.json`) lists every failure and the smallest margins against each floor; `index.html`
104
+ beside it shows every capture (G14). Details: [docs/07](docs/07_gate.md).
105
+ - **Your own CSS:** never restyle a class of `reserved-classes.json` `components`; a `modifiers` word (`on`,
106
+ `active`, `fill`, `wide`) may be joined to your own class (`.my-row.on`), never used alone. The template's guard reads
107
+ that list. Values come from the tokens (`var(--rail-w)`, `var(--space-3)`, [docs/02](docs/02_tokens.md)).
91
108
  - **Animated views:** drive every canvas or 3D loop through `usePausedViz` (default paused, run once, halt on a
92
109
  hidden tab), never call `requestAnimationFrame` directly.
93
110
 
@@ -102,8 +119,11 @@ const config: ShellConfig = {
102
119
  `ReferenceList` (deprecated), `DocPage`, `DocSection`, `WorkbenchLayout`, `CaseWorkbench`,
103
120
  `MAX_WORKBENCH_GROUPS`, `VariantBar`, `LaneBadge`, `Readout`, `Gauge`, `Verdict`, `PlotCard`, `Knob`,
104
121
  `ChipGroup`, `Stage`, `useStageSize`, `makeStateKey`, `isStale`, `useWorkbenchState`,
105
- `WorkbenchStateContext`, `pick`. Separate entries: `/chart` (`UPlotChart`), `/keys` (the storage keys, no
106
- React), `/styles.css`, `/chart.css`, `/reserved-classes.json`. Bin: `caos-shell-gate`.
122
+ `WorkbenchStateContext`, `pick`; since 0.8.0 `SurfacePage`, `useUrlView`, `viewParamOf`, `replaceQueryParam`,
123
+ `BREAKPOINTS`, `BarChart`, `textWidth`, `widestLabel`, `fitLabel`, `niceTicks`, `niceStep`, `fontFamily`,
124
+ `formatTicks`, `NBSP`, `SCIENTIFIC_BELOW`, the type `ShellColorToken`. Separate entries: `/chart` (`UPlotChart`,
125
+ `drawMarks`, `tickSteps`, `repeatedLabels`), `/keys` (the storage keys, no React), `/styles.css`, `/chart.css`,
126
+ `/reserved-classes.json`. Bin: `caos-shell-gate`.
107
127
 
108
128
  ### Authenticated workbenches
109
129
 
@@ -14,12 +14,13 @@
14
14
  // --case-sample id1,id2 cases walked outside the primary mode; default the first and the last
15
15
  // --single-case the workbench has one case and no case control
16
16
  // --idle-ms 3000 --settle-ms 15000 --text-floor 400 --instrument-min 0.5 --stage-fill-min 0.3
17
- // --out gate-output report (gate-report.json) and screenshots (shots/)
17
+ // --wide-font-sizes 390x844,1280x800 the sizes of the wide-font pass (G10); --no-wide-font skips it
18
+ // --out gate-output report (gate-report.json), screenshots (shots/) and the capture index (index.html)
18
19
  // Needs `playwright` in the consuming project (optional peer dependency). Exit 1 on any failure, 2 on misuse.
19
20
 
20
21
  import { runGate } from '../gate/run.mjs';
21
22
 
22
- const FLAGS = new Set(['single-case']);
23
+ const FLAGS = new Set(['single-case', 'no-wide-font']);
23
24
  const args = {};
24
25
  const argv = process.argv.slice(2);
25
26
  for (let i = 0; i < argv.length; i += 1) {
@@ -48,6 +49,8 @@ const options = {
48
49
  textFloor: num('text-floor'),
49
50
  instrumentMin: num('instrument-min'),
50
51
  stageFillMin: num('stage-fill-min'),
52
+ wideFontSizes: args['wide-font-sizes'],
53
+ wideFont: args['no-wide-font'] === true ? false : undefined,
51
54
  out: args.out,
52
55
  };
53
56
  for (const k of Object.keys(options)) if (options[k] === undefined) delete options[k];
package/dist/chart.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as react from 'react';
2
- import { F as FormatOptions, S as ShellToken } from './tokens-CSV0MOgR.js';
2
+ import uPlot from 'uplot';
3
+ import { F as FormatOptions, S as ShellColorToken } from './tokens-DkyEXX5g.js';
3
4
  import { B as BiText } from './text-BlY47nIt.js';
4
5
  import 'zustand';
5
6
 
@@ -8,7 +9,7 @@ interface ChartSeries {
8
9
  /** One value per x; `null` leaves a gap. */
9
10
  values: (number | null)[];
10
11
  /** A shell colour token; defaults rotate through the accent palette. */
11
- color?: ShellToken;
12
+ color?: ShellColorToken;
12
13
  width?: number;
13
14
  dash?: number[];
14
15
  /** `points` draws markers only (a scatter of cases over a curve); `null` values are simply absent. */
@@ -42,6 +43,30 @@ interface UPlotChartProps {
42
43
  /** Called with the index under the cursor (or null), for linked views. */
43
44
  onCursor?: (index: number | null) => void;
44
45
  }
46
+ /**
47
+ * The tick steps an axis may use. With fixed `decimals` (scaled by 100 for a percent) only multiples of the smallest
48
+ * difference its labels can show, so no two ticks read the same: an integer axis ticks at integers, never at 1.5,
49
+ * which reads "2" beside the 2 (CAOS_Contraste, 2026-10-05). `undefined` leaves uPlot its own steps: an axis formatted
50
+ * by significant digits has no fixed resolution, and `data-ticks-repeat` reports what it does.
51
+ */
52
+ declare function tickSteps(fmt: FormatOptions | undefined): number[] | undefined;
53
+ /** How many tick labels repeat the label before them (empty labels apart); the gate fails a chart with any (G6). */
54
+ declare function repeatedLabels(labels: readonly (string | null | undefined)[]): number;
55
+ /**
56
+ * The vertical marks of a chart and their labels. Every label is drawn with an explicit alignment (uPlot leaves the
57
+ * context right-aligned after its last axis, so a label meant to start at its line ended there and ran off the plot
58
+ * near the left edge, CAOS_Fragmenta 2026-10-06), on the side of its line where it fits inside the plot, on the next
59
+ * row down when it would overlap the label before it, with a halo in the surface colour so a curve under it does not
60
+ * cross its letters. A mark outside the x range is not drawn.
61
+ */
62
+ declare function drawMarks(u: uPlot, marks: {
63
+ x: number;
64
+ label: string;
65
+ }[], style: {
66
+ color: string;
67
+ halo: string;
68
+ family: string;
69
+ }): void;
45
70
  /**
46
71
  * The house chart (S12 of the 2026-10-04 requirements; uPlot, ADR on interactive charts). It is built only once its
47
72
  * box has a size; it resolves the theme's colours to concrete values and rebuilds when the theme or language changes
@@ -55,4 +80,4 @@ interface UPlotChartProps {
55
80
  */
56
81
  declare function UPlotChart({ x, y, series, marks, height, onCursor }: UPlotChartProps): react.JSX.Element;
57
82
 
58
- export { type ChartAxis, type ChartSeries, UPlotChart, type UPlotChartProps };
83
+ export { type ChartAxis, type ChartSeries, UPlotChart, type UPlotChartProps, drawMarks, repeatedLabels, tickSteps };
package/dist/chart.js CHANGED
@@ -1,4 +1,4 @@
1
- import { useShellLang, useThemeStore, useStageSize, pick, resolveToken, formatNumber } from './chunk-LOSSTLPP.js';
1
+ import { useShellLang, useThemeStore, useStageSize, pick, resolveToken, NBSP, formatNumber, formatTicks } from './chunk-ZLJCB7DD.js';
2
2
  import './chunk-X6Y5CDUD.js';
3
3
  import { useRef, useCallback, useState, useMemo, useEffect } from 'react';
4
4
  import uPlot from 'uplot';
@@ -17,6 +17,46 @@ function repeatedLabels(labels) {
17
17
  for (let i = 1; i < labels.length; i++) if (labels[i] && labels[i] === labels[i - 1]) n++;
18
18
  return n;
19
19
  }
20
+ function drawMarks(u, marks, style) {
21
+ const ctx = u.ctx;
22
+ const pr = uPlot.pxRatio || 1;
23
+ const left = u.bbox.left;
24
+ const right = u.bbox.left + u.bbox.width;
25
+ const gap = 4 * pr;
26
+ const rowH = 14 * pr;
27
+ const placed = [];
28
+ ctx.save();
29
+ ctx.font = `${Math.round(11 * pr)}px ${style.family}`;
30
+ ctx.textBaseline = "alphabetic";
31
+ ctx.lineJoin = "round";
32
+ for (const m of [...marks].sort((a, b) => a.x - b.x)) {
33
+ const px = u.valToPos(m.x, "x", true);
34
+ if (!Number.isFinite(px) || px < left - 0.5 || px > right + 0.5) continue;
35
+ ctx.strokeStyle = style.color;
36
+ ctx.lineWidth = Math.max(1, pr);
37
+ ctx.setLineDash([4 * pr, 4 * pr]);
38
+ ctx.beginPath();
39
+ ctx.moveTo(px, u.bbox.top);
40
+ ctx.lineTo(px, u.bbox.top + u.bbox.height);
41
+ ctx.stroke();
42
+ ctx.setLineDash([]);
43
+ const w = ctx.measureText(m.label).width;
44
+ let x0 = px + gap;
45
+ if (x0 + w > right) x0 = px - gap - w;
46
+ if (x0 < left) x0 = Math.max(left, Math.min(right - w, px - w / 2));
47
+ let row = 0;
48
+ while (placed.some((p) => p.row === row && x0 < p.x1 + gap && x0 + w + gap > p.x0)) row += 1;
49
+ placed.push({ x0, x1: x0 + w, row });
50
+ const y = u.bbox.top + 12 * pr + row * rowH;
51
+ ctx.textAlign = "left";
52
+ ctx.lineWidth = 3 * pr;
53
+ ctx.strokeStyle = style.halo;
54
+ ctx.strokeText(m.label, x0, y);
55
+ ctx.fillStyle = style.color;
56
+ ctx.fillText(m.label, x0, y);
57
+ }
58
+ ctx.restore();
59
+ }
20
60
  function UPlotChart({ x, y, series, marks, height = 280, onCursor }) {
21
61
  const lang = useShellLang();
22
62
  const theme = useThemeStore((s) => s.theme);
@@ -57,10 +97,12 @@ function UPlotChart({ x, y, series, marks, height = 280, onCursor }) {
57
97
  if (!el || width <= 0 || h <= 0) return;
58
98
  const fg = resolveToken("--color-fg-subtle", "#888");
59
99
  const grid = resolveToken("--color-border", "#ccc");
60
- const font = `11px ${resolveToken("--font-sans", "sans-serif")}`;
100
+ const family = resolveToken("--font-sans", "sans-serif");
101
+ const font = `11px ${family}`;
102
+ const surface = resolveToken("--color-surface", "#fff");
61
103
  const repeats = { x: 0, y: 0 };
62
104
  const tick = (fmt, axis) => (_u, vals) => {
63
- const labels = vals.map((v) => v === null || v === void 0 ? "" : formatNumber(v, lang, fmt ?? { digits: 4 }));
105
+ const labels = formatTicks(vals, lang, fmt ?? { digits: 4 });
64
106
  repeats[axis] = repeatedLabels(labels);
65
107
  hostRef.current?.setAttribute("data-ticks-repeat", String(repeats.x + repeats.y));
66
108
  return labels;
@@ -92,11 +134,15 @@ function UPlotChart({ x, y, series, marks, height = 280, onCursor }) {
92
134
  labelFont: font,
93
135
  values: tick(y.format, "y"),
94
136
  ...ySteps ? { incrs: ySteps } : {},
137
+ // Sized to the longest tick label. uPlot draws in device pixels with a font scaled by its pixel ratio, so the
138
+ // label is measured in that font and divided back (measuring the unscaled font and dividing by the ratio
139
+ // made the axis too narrow on every high-density screen; the gate runs at ratio 1 and never saw it).
95
140
  size: (u, values) => {
96
141
  if (!values || values.length === 0) return 48;
97
- u.ctx.font = font;
142
+ const pr = uPlot.pxRatio || 1;
143
+ u.ctx.font = `${Math.round(11 * pr)}px ${family}`;
98
144
  const widest = Math.max(...values.map((v) => u.ctx.measureText(v ?? "").width));
99
- return Math.ceil(widest / (window.devicePixelRatio || 1)) + 30;
145
+ return Math.ceil(widest / pr) + 30;
100
146
  }
101
147
  }
102
148
  ],
@@ -118,21 +164,11 @@ function UPlotChart({ x, y, series, marks, height = 280, onCursor }) {
118
164
  draw: [
119
165
  (u) => {
120
166
  if (!marks?.length) return;
121
- const ctx = u.ctx;
122
- ctx.save();
123
- ctx.strokeStyle = resolveToken("--color-warn", "#c80");
124
- ctx.fillStyle = resolveToken("--color-warn", "#c80");
125
- ctx.setLineDash([4, 4]);
126
- ctx.font = font;
127
- for (const m of marks) {
128
- const px = u.valToPos(m.x, "x", true);
129
- ctx.beginPath();
130
- ctx.moveTo(px, u.bbox.top);
131
- ctx.lineTo(px, u.bbox.top + u.bbox.height);
132
- ctx.stroke();
133
- ctx.fillText(pick(m.label, lang), px + 4, u.bbox.top + 12);
134
- }
135
- ctx.restore();
167
+ drawMarks(u, marks.map((m) => ({ x: m.x, label: pick(m.label, lang) })), {
168
+ color: resolveToken("--color-warn", "#c80"),
169
+ halo: surface,
170
+ family
171
+ });
136
172
  }
137
173
  ]
138
174
  }
@@ -148,9 +184,9 @@ function UPlotChart({ x, y, series, marks, height = 280, onCursor }) {
148
184
  uRef.current?.setData(data);
149
185
  }, [data]);
150
186
  const read = cursor !== null ? [
151
- `${xLabel} ${formatNumber(x.values[cursor], lang, x.format ?? { digits: 4 })}${xUnit ? ` ${xUnit}` : ""}`,
187
+ `${xLabel} ${formatNumber(x.values[cursor], lang, x.format ?? { digits: 4 })}${xUnit ? `${NBSP}${xUnit}` : ""}`,
152
188
  ...series.map(
153
- (s) => `${pick(s.label, lang)} ${formatNumber(s.values[cursor] ?? null, lang, y.format ?? { digits: 4 })}${yUnit ? ` ${yUnit}` : ""}`
189
+ (s) => `${pick(s.label, lang)} ${formatNumber(s.values[cursor] ?? null, lang, y.format ?? { digits: 4 })}${yUnit ? `${NBSP}${yUnit}` : ""}`
154
190
  )
155
191
  ].join(" \xB7 ") : lang === "es" ? "Pase el cursor sobre el gr\xE1fico para leer los valores" : "Hover the chart to read the values";
156
192
  return /* @__PURE__ */ jsxs(
@@ -175,4 +211,4 @@ function UPlotChart({ x, y, series, marks, height = 280, onCursor }) {
175
211
  );
176
212
  }
177
213
 
178
- export { UPlotChart };
214
+ export { UPlotChart, drawMarks, repeatedLabels, tickSteps };
@@ -93,8 +93,36 @@ var SHELL_TOKENS = [
93
93
  "--color-shadow",
94
94
  "--font-sans",
95
95
  "--font-mono",
96
+ "--text-xs",
97
+ "--text-sm",
98
+ "--text-md",
99
+ "--text-base",
100
+ "--text-lg",
101
+ "--text-xl",
102
+ "--text-2xl",
103
+ "--space-1",
104
+ "--space-2",
105
+ "--space-3",
106
+ "--space-4",
107
+ "--space-5",
108
+ "--space-6",
109
+ "--radius-sm",
110
+ "--radius-md",
111
+ "--radius-lg",
112
+ "--radius-pill",
96
113
  "--maxw",
97
- "--maxw-wide"
114
+ "--maxw-wide",
115
+ "--measure",
116
+ "--measure-text",
117
+ "--header-h",
118
+ "--rail-w",
119
+ "--fade",
120
+ "--icon-sm",
121
+ "--icon-md",
122
+ "--icon-lg",
123
+ "--z-header",
124
+ "--z-focus",
125
+ "--z-modal"
98
126
  ];
99
127
  function resolveToken(token, fallback = "") {
100
128
  if (typeof document === "undefined") return fallback;
@@ -112,14 +140,25 @@ function useThemeTokens() {
112
140
 
113
141
  // src/lib/format.ts
114
142
  var LOCALE = { en: "en-US", es: "es-CL" };
143
+ var NBSP = "\xA0";
115
144
  var SCIENTIFIC_BELOW = 1e-4;
116
145
  var NOT_AVAILABLE = { en: "not available", es: "no disponible" };
117
146
  function formatNumber(value, lang, opts = {}) {
118
147
  if (value === null || value === void 0 || !Number.isFinite(value)) return NOT_AVAILABLE[lang];
119
148
  const v = opts.percent ? value * 100 : value;
120
149
  const digits = opts.digits ?? 4;
121
- const nf = opts.decimals !== void 0 ? new Intl.NumberFormat(LOCALE[lang], { minimumFractionDigits: opts.decimals, maximumFractionDigits: opts.decimals }) : v !== 0 && Math.abs(v) < SCIENTIFIC_BELOW ? new Intl.NumberFormat(LOCALE[lang], { notation: "scientific", maximumSignificantDigits: digits }) : Math.abs(v) >= 10 ** (digits - 1) ? new Intl.NumberFormat(LOCALE[lang], { maximumFractionDigits: 0 }) : new Intl.NumberFormat(LOCALE[lang], { maximumSignificantDigits: digits });
122
- return opts.percent ? `${nf.format(v)} %` : nf.format(v);
150
+ const scientific = v !== 0 && (opts.notation === "scientific" || Math.abs(v) < SCIENTIFIC_BELOW);
151
+ const nf = opts.decimals !== void 0 ? new Intl.NumberFormat(LOCALE[lang], { minimumFractionDigits: opts.decimals, maximumFractionDigits: opts.decimals }) : scientific ? new Intl.NumberFormat(LOCALE[lang], { notation: "scientific", maximumSignificantDigits: digits }) : Math.abs(v) >= 10 ** (digits - 1) ? new Intl.NumberFormat(LOCALE[lang], { maximumFractionDigits: 0 }) : new Intl.NumberFormat(LOCALE[lang], { maximumSignificantDigits: digits });
152
+ return opts.percent ? `${nf.format(v)}${NBSP}%` : nf.format(v);
153
+ }
154
+ function formatTicks(values, lang, opts = {}) {
155
+ let o = opts;
156
+ if (opts.decimals === void 0 && opts.notation === void 0) {
157
+ const scale = opts.percent ? 100 : 1;
158
+ const small = values.some((v) => v !== null && v !== void 0 && Number.isFinite(v) && v !== 0 && Math.abs(v * scale) < SCIENTIFIC_BELOW);
159
+ if (small) o = { ...opts, notation: "scientific" };
160
+ }
161
+ return values.map((v) => v === null || v === void 0 ? "" : formatNumber(v, lang, o));
123
162
  }
124
163
  function useFormat() {
125
164
  const lang = useShellLang();
@@ -171,4 +210,4 @@ function Stage({ label, children, className }) {
171
210
  );
172
211
  }
173
212
 
174
- export { NOT_AVAILABLE, SHELL_TOKENS, Stage, applyTheme, formatNumber, pick, readTheme, resolveToken, useFormat, useLangStore, useShellLang, useStageSize, useThemeStore, useThemeTokens };
213
+ export { NBSP, NOT_AVAILABLE, SCIENTIFIC_BELOW, SHELL_TOKENS, Stage, applyTheme, formatNumber, formatTicks, pick, readTheme, resolveToken, useFormat, useLangStore, useShellLang, useStageSize, useThemeStore, useThemeTokens };