@cfasim-ui/docs 0.4.6 → 0.4.8

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.
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { ref, watch, computed, onMounted, getCurrentInstance } from "vue";
3
3
  import { SliderRoot, SliderTrack, SliderRange, SliderThumb } from "reka-ui";
4
+ import { formatNumber, type NumberFormat } from "@cfasim-ui/shared";
4
5
  import Hint from "../Hint/Hint.vue";
5
6
 
6
7
  export type NumberRange = [number, number];
@@ -29,9 +30,17 @@ const props = defineProps<{
29
30
  numberType?: "integer" | "float";
30
31
  required?: boolean;
31
32
  decimals?: number;
32
- // Custom formatter for slider thumb labels and min/max labels. Overrides
33
- // the default percent/decimal formatting when provided. Only consulted in
34
- // slider/range mode the text input keeps its own number-shaped formatting.
33
+ // Custom formatter for the displayed value. Accepts a NumberFormat
34
+ // (preset name, printf string, or function) see `formatNumber` in
35
+ // `@cfasim-ui/shared`. When set, overrides the default percent/decimal
36
+ // formatting for both the text input value and slider thumb/min/max
37
+ // labels. The model stays a raw number; only the display changes. Note
38
+ // that formats which add suffixes or scale the value (e.g.
39
+ // `"percent:1"`) may not round-trip through the text input — use
40
+ // `percent: true` for value scaling and `format` for display shaping.
41
+ format?: NumberFormat;
42
+ /** @deprecated Use `format` instead. Still honored for slider labels
43
+ * when `format` is unset, but will be removed in a future release. */
35
44
  sliderDisplay?: (value: number) => string;
36
45
  }>();
37
46
 
@@ -113,7 +122,11 @@ function roundToDecimals(v: number, d: number): number {
113
122
 
114
123
  function formatSliderValue(v: number | undefined) {
115
124
  if (v == null) return "";
116
- if (props.sliderDisplay) return props.sliderDisplay(v);
125
+ // sliderDisplay (deprecated) is a function — i.e. already a valid
126
+ // NumberFormat — so route it through formatNumber. `format` wins when
127
+ // both are set.
128
+ const fmt = props.format ?? props.sliderDisplay;
129
+ if (fmt !== undefined) return formatNumber(v, fmt);
117
130
  const d = displayDecimals.value;
118
131
  if (props.percent) return (v * 100).toFixed(d) + "%";
119
132
  return v.toLocaleString("en-US", {
@@ -149,6 +162,7 @@ function formatWithCommas(v: number | undefined): string {
149
162
 
150
163
  function formatForDisplay(v: number | undefined): string {
151
164
  if (v == null) return "";
165
+ if (props.format !== undefined) return formatNumber(v, props.format);
152
166
  const d = displayDecimals.value;
153
167
  if (d > 0) {
154
168
  return v.toLocaleString("en-US", {
package/index.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.4.6",
2
+ "version": "0.4.8",
3
3
  "package": "@cfasim-ui/docs",
4
4
  "content": {
5
5
  "components": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cfasim-ui/docs",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
4
4
  "description": "LLM-friendly component and chart documentation for cfasim-ui",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -0,0 +1,146 @@
1
+ import { sprintf } from "sprintf-js";
2
+
3
+ /**
4
+ * Named number-format presets, modelled on Streamlit's number column formats.
5
+ * - `plain` — `String(value)` (no grouping)
6
+ * - `localized` — `Intl.NumberFormat()` with the user's locale
7
+ * - `percent` — formats as a percent (value `0.5` → `"50%"`)
8
+ * - `compact` — short form (`1.2K`, `3.4M`)
9
+ * - `scientific` — scientific notation (`1.23E4`)
10
+ * - `engineering` — engineering notation (powers of 1000)
11
+ *
12
+ * Presets preserve the raw value's precision by default (no rounding).
13
+ * Append `:N` (a digit) to fix the number of fractional digits, e.g.
14
+ * `"percent:1"` → `"12.3%"`, `"localized:2"` → `"1,234.50"`.
15
+ */
16
+ export type NumberFormatPreset =
17
+ | "plain"
18
+ | "localized"
19
+ | "percent"
20
+ | "compact"
21
+ | "scientific"
22
+ | "engineering";
23
+
24
+ /**
25
+ * A number format specifier:
26
+ * - A {@link NumberFormatPreset} name, optionally with `:N` digits suffix
27
+ * (e.g. `"percent:1"`, `"compact:2"`)
28
+ * - A printf-style format string (must contain a `%` placeholder, parsed
29
+ * by sprintf-js — e.g. `"%.2f"`, `"%05d"`)
30
+ * - A function `(value) => string` for full control
31
+ *
32
+ * Strings that contain no `%` and don't match a preset throw at format
33
+ * time, so typos surface immediately instead of silently rendering wrong.
34
+ */
35
+ export type NumberFormat =
36
+ | NumberFormatPreset
37
+ | string
38
+ | ((value: number) => string);
39
+
40
+ const PRESET_NAMES = new Set<NumberFormatPreset>([
41
+ "plain",
42
+ "localized",
43
+ "percent",
44
+ "compact",
45
+ "scientific",
46
+ "engineering",
47
+ ]);
48
+
49
+ function isPreset(s: string): s is NumberFormatPreset {
50
+ return PRESET_NAMES.has(s as NumberFormatPreset);
51
+ }
52
+
53
+ /**
54
+ * Parse `"preset"` or `"preset:N"` into a name and optional digit count.
55
+ * Returns null if the string isn't a recognized preset.
56
+ */
57
+ function parsePreset(
58
+ s: string,
59
+ ): { preset: NumberFormatPreset; digits: number | undefined } | null {
60
+ const colon = s.indexOf(":");
61
+ if (colon === -1) {
62
+ return isPreset(s) ? { preset: s, digits: undefined } : null;
63
+ }
64
+ const name = s.slice(0, colon);
65
+ const rest = s.slice(colon + 1);
66
+ if (!isPreset(name)) return null;
67
+ // Digits must be a non-negative integer (matches Intl's 0..100 range).
68
+ if (!/^\d+$/.test(rest)) return null;
69
+ const digits = Number(rest);
70
+ if (digits > 100) return null;
71
+ return { preset: name, digits };
72
+ }
73
+
74
+ // Covers JS double-precision (≤17 significant digits). When the caller
75
+ // hasn't asked for a specific digit count, we use this to preserve the
76
+ // raw value rather than letting Intl round to its default precision.
77
+ const RAW_PRECISION_DIGITS = 20;
78
+
79
+ function formatPreset(
80
+ value: number,
81
+ preset: NumberFormatPreset,
82
+ digits: number | undefined,
83
+ ): string {
84
+ const fractionOpts: Intl.NumberFormatOptions =
85
+ digits !== undefined
86
+ ? { minimumFractionDigits: digits, maximumFractionDigits: digits }
87
+ : { maximumFractionDigits: RAW_PRECISION_DIGITS };
88
+ switch (preset) {
89
+ case "plain":
90
+ return digits !== undefined ? value.toFixed(digits) : String(value);
91
+ case "localized":
92
+ return new Intl.NumberFormat(undefined, fractionOpts).format(value);
93
+ case "percent":
94
+ return new Intl.NumberFormat(undefined, {
95
+ style: "percent",
96
+ ...fractionOpts,
97
+ }).format(value);
98
+ case "compact":
99
+ return new Intl.NumberFormat(undefined, {
100
+ notation: "compact",
101
+ ...fractionOpts,
102
+ }).format(value);
103
+ case "scientific":
104
+ return new Intl.NumberFormat(undefined, {
105
+ notation: "scientific",
106
+ ...fractionOpts,
107
+ }).format(value);
108
+ case "engineering":
109
+ return new Intl.NumberFormat(undefined, {
110
+ notation: "engineering",
111
+ ...fractionOpts,
112
+ }).format(value);
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Format a number using a preset name, a printf-style format string, or a
118
+ * custom function. Non-finite values (NaN, ±Infinity) are returned as
119
+ * `String(value)`; if `format` is omitted, falls back to `String(value)`.
120
+ *
121
+ * Throws if `format` is a string that's neither a recognized preset (with
122
+ * an optional `:N` digit suffix) nor a printf string containing `%`.
123
+ */
124
+ export function formatNumber(value: number, format?: NumberFormat): string {
125
+ if (!Number.isFinite(value)) return String(value);
126
+ if (format === undefined) return String(value);
127
+ if (typeof format === "function") return format(value);
128
+ // printf strings always contain a `%` placeholder; everything else must
129
+ // be a recognized preset (with an optional `:N` digits suffix).
130
+ if (format.includes("%")) return sprintf(format, value);
131
+ const parsed = parsePreset(format);
132
+ if (!parsed) {
133
+ const names = [...PRESET_NAMES].join(", ");
134
+ throw new Error(
135
+ `formatNumber: invalid format ${JSON.stringify(format)}. ` +
136
+ `Expected one of ${names} (optionally with ":N" digits), ` +
137
+ `a printf format string containing "%", or a function.`,
138
+ );
139
+ }
140
+ return formatPreset(value, parsed.preset, parsed.digits);
141
+ }
142
+
143
+ /** True if `f` is a recognized {@link NumberFormat} value. */
144
+ export function isNumberFormat(f: unknown): f is NumberFormat {
145
+ return typeof f === "string" || typeof f === "function";
146
+ }
package/shared/index.ts CHANGED
@@ -14,6 +14,8 @@ export type {
14
14
  ModelOutputsWire,
15
15
  } from "./ModelOutput.js";
16
16
  export { modelOutputToCSV } from "./csv.js";
17
+ export { formatNumber, isNumberFormat } from "./formatNumber.js";
18
+ export type { NumberFormat, NumberFormatPreset } from "./formatNumber.js";
17
19
  export {
18
20
  useUrlParams,
19
21
  serialize,