@reliantlabs/forge-web-runtime 0.3.2 → 0.4.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 +26 -3
- package/dist/devlog.js +6 -2
- package/dist/devlog.js.map +1 -1
- package/dist/error-boundary.d.ts +1 -1
- package/dist/error-boundary.js +9 -2
- package/dist/error-boundary.js.map +1 -1
- package/dist/error-reporter.d.ts +27 -0
- package/dist/error-reporter.js +55 -0
- package/dist/error-reporter.js.map +1 -0
- package/dist/index.d.ts +15 -2
- package/dist/index.js +15 -2
- package/dist/index.js.map +1 -1
- package/dist/mock-transport.d.ts +8 -1
- package/dist/mock-transport.js +198 -6
- package/dist/mock-transport.js.map +1 -1
- package/dist/money.d.ts +69 -0
- package/dist/money.js +335 -0
- package/dist/money.js.map +1 -0
- package/dist/resource.js +7 -2
- package/dist/resource.js.map +1 -1
- package/dist/telemetry.d.ts +61 -21
- package/dist/telemetry.js +254 -111
- package/dist/telemetry.js.map +1 -1
- package/package.json +12 -45
- package/dist/otel.d.ts +0 -31
- package/dist/otel.js +0 -94
- package/dist/otel.js.map +0 -1
package/dist/money.js
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
// Part of @reliantlabs/forge-web-runtime — the web twin of forge/pkg.
|
|
2
|
+
//
|
|
3
|
+
// Money and basis points as INTEGERS, formatted and parsed at the edge of the
|
|
4
|
+
// UI. This is the web counterpart of forge/pkg/money: an amount is an integer
|
|
5
|
+
// count of its currency's minor unit (cents for USD, yen for JPY, fils for
|
|
6
|
+
// KWD) plus an ISO 4217 code, and no float ever sits between the wire and the
|
|
7
|
+
// screen. protobuf-es surfaces an int64 as a `bigint`; keep it one until it is
|
|
8
|
+
// rendered, and turn user input straight back into one.
|
|
9
|
+
//
|
|
10
|
+
// formatMinorUnits(123456n) -> "$1,234.56"
|
|
11
|
+
// formatMinorUnits(1234n, { currency: "JPY" }) -> "¥1,234"
|
|
12
|
+
// formatMinorUnits(123456n, { currency: "EUR", locale: "de-DE" }) -> "1.234,56 €"
|
|
13
|
+
// parseMinorUnits("$1,234.5") -> 123450n
|
|
14
|
+
// parseMinorUnits("1.234") -> null (USD has 2 digits)
|
|
15
|
+
// minorUnitsToInput(123450n) -> "1234.50"
|
|
16
|
+
// currencyMinorDigits("KWD") -> 3
|
|
17
|
+
// formatBasisPoints(825) -> "8.25%"
|
|
18
|
+
// parseBasisPoints("8.25%") -> 825
|
|
19
|
+
// basisPointsToInput(850) -> "8.5"
|
|
20
|
+
//
|
|
21
|
+
// THE RULES
|
|
22
|
+
//
|
|
23
|
+
// - Amounts are `bigint | number`. A `number` must be an integer: anything
|
|
24
|
+
// else throws a RangeError, because 12.5 "cents" is a major-unit value
|
|
25
|
+
// that leaked into a minor-unit field and rounding it would hide the bug.
|
|
26
|
+
// `null` / `undefined` format as "—" and write an input value of "".
|
|
27
|
+
// - `currency` defaults to "USD", and "" means the default too (a proto3
|
|
28
|
+
// string field that was never set arrives as ""). A malformed code throws
|
|
29
|
+
// Intl's RangeError. `locale` defaults to "en-US".
|
|
30
|
+
// - A currency's minor-unit digits come from Intl (CLDR) —
|
|
31
|
+
// `resolvedOptions().maximumFractionDigits` — and formatting and parsing
|
|
32
|
+
// both use that one source, so they cannot disagree. Note CLDR, not ISO
|
|
33
|
+
// 4217, is the authority: HUF and a few others differ.
|
|
34
|
+
// - Formatting is exact at any size. The amount is handed to Intl as a
|
|
35
|
+
// decimal STRING (Intl.NumberFormat v3 — Node 20+, every evergreen browser
|
|
36
|
+
// since 2023), never as a Number, so int64 extremes print to the last
|
|
37
|
+
// digit. An engine without v3 coerces the string to a Number, which is
|
|
38
|
+
// still exact below about 10^15 minor units.
|
|
39
|
+
// - Parsing returns null rather than guessing. Accepted: ASCII digits or
|
|
40
|
+
// the locale's own (Arabic-Indic for ar-EG), surrounding and grouping
|
|
41
|
+
// whitespace, the locale's group and decimal separators, the currency's
|
|
42
|
+
// own symbol or ISO code before or after the number, a leading "+", and —
|
|
43
|
+
// only with `allowNegative` — a leading "-" or "−". Anything Intl
|
|
44
|
+
// rendered for the same options therefore parses back. Refused:
|
|
45
|
+
// more fraction digits than the currency has (never rounded), another
|
|
46
|
+
// currency's symbol, a misplaced group separator ("12,50" typed into an
|
|
47
|
+
// en-US field is a decimal comma, so it is not read as 1,250.00), and an
|
|
48
|
+
// amount outside the int64 range the wire carries.
|
|
49
|
+
// - minorUnitsToInput writes the locale's decimal separator with no
|
|
50
|
+
// grouping, so it round-trips through parseMinorUnits given the same
|
|
51
|
+
// options. For an `<input type="number">` keep the en-US default: that
|
|
52
|
+
// element's value is always "."-decimal whatever the page's locale.
|
|
53
|
+
// - Basis points are an integer count of 0.01%. Parsing allows at most two
|
|
54
|
+
// decimals of a percent and caps at 100% (10000) unless `max` says
|
|
55
|
+
// otherwise — the typical rate (tax, discount, commission) lives in
|
|
56
|
+
// 0–100%, and the cap catches "825" typed where "8.25" was meant.
|
|
57
|
+
//
|
|
58
|
+
// Differences from forge/pkg/money: the Go Format is locale-neutral with its
|
|
59
|
+
// own symbol table, while this renders through Intl for the caller's locale;
|
|
60
|
+
// and the Go Parse accepts a negative amount, while this one makes negatives
|
|
61
|
+
// opt-in, because most amounts a form collects cannot be below zero.
|
|
62
|
+
//
|
|
63
|
+
// This module is in the BARREL: it imports nothing, so there is no optional
|
|
64
|
+
// dependency for a subpath to fence off.
|
|
65
|
+
const DEFAULT_CURRENCY = "USD";
|
|
66
|
+
const DEFAULT_LOCALE = "en-US";
|
|
67
|
+
const UNSET = "—";
|
|
68
|
+
const BASIS_POINT_DIGITS = 2; // 1 bp = 0.01 percent
|
|
69
|
+
const DEFAULT_MAX_BASIS_POINTS = 10_000;
|
|
70
|
+
const INT64_MAX = 9223372036854775807n;
|
|
71
|
+
const INT64_MIN = -9223372036854775808n;
|
|
72
|
+
/**
|
|
73
|
+
* formatMinorUnits renders an integer minor-unit amount as currency for a
|
|
74
|
+
* locale: 123456n → "$1,234.56", 1234n JPY → "¥1,234". Exact at any size.
|
|
75
|
+
* Unset renders "—"; a non-integer number throws RangeError.
|
|
76
|
+
*/
|
|
77
|
+
export function formatMinorUnits(minor, options = {}) {
|
|
78
|
+
if (minor === null || minor === undefined)
|
|
79
|
+
return UNSET;
|
|
80
|
+
const currency = currencyOf(options);
|
|
81
|
+
const decimal = toDecimalString(toInteger(minor), currencyMinorDigits(currency));
|
|
82
|
+
return currencyFormat(localeOf(options), currency).format(decimal);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* parseMinorUnits reads user input such as "1,234.5", "$1234.56" or "12" into
|
|
86
|
+
* minor units. Returns null for anything it cannot represent exactly — too
|
|
87
|
+
* many decimals, junk, a misplaced separator, a negative amount without
|
|
88
|
+
* `allowNegative`, or a value beyond int64 — so a form rejects it instead of
|
|
89
|
+
* silently rounding.
|
|
90
|
+
*/
|
|
91
|
+
export function parseMinorUnits(input, options = {}) {
|
|
92
|
+
const currency = currencyOf(options);
|
|
93
|
+
const value = parseScaled(input, currencyMinorDigits(currency), moneyGrammar(localeOf(options), currency), options.allowNegative ?? false);
|
|
94
|
+
if (value === null || value > INT64_MAX || value < INT64_MIN)
|
|
95
|
+
return null;
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* minorUnitsToInput renders minor units as the value of an editable field:
|
|
100
|
+
* 123456n → "1234.56" (de-DE: "1234,56"). No grouping, every minor digit
|
|
101
|
+
* shown, and the result parses back through parseMinorUnits. Unset → "";
|
|
102
|
+
* a non-integer number throws RangeError.
|
|
103
|
+
*/
|
|
104
|
+
export function minorUnitsToInput(minor, options = {}) {
|
|
105
|
+
if (minor === null || minor === undefined)
|
|
106
|
+
return "";
|
|
107
|
+
const currency = currencyOf(options);
|
|
108
|
+
const decimal = toDecimalString(toInteger(minor), currencyMinorDigits(currency));
|
|
109
|
+
return decimal.replace(".", moneyGrammar(localeOf(options), currency).decimal);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* currencyMinorDigits returns how many decimal digits a currency's minor unit
|
|
113
|
+
* has — USD 2, JPY 0, KWD 3 — as Intl reports it. Useful for an input's
|
|
114
|
+
* `step` or a validation message. Throws RangeError on a malformed code.
|
|
115
|
+
*/
|
|
116
|
+
export function currencyMinorDigits(currency) {
|
|
117
|
+
const code = currency || DEFAULT_CURRENCY;
|
|
118
|
+
// CLDR's digits are per currency, not per locale, so en-US answers for all.
|
|
119
|
+
return cached(minorDigitsCache, code, () => currencyFormat(DEFAULT_LOCALE, code).resolvedOptions()
|
|
120
|
+
.maximumFractionDigits ?? 2);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* formatBasisPoints renders an integer basis-point rate as a percentage:
|
|
124
|
+
* 825 → "8.25%", 1000 → "10%". Unset renders "—"; a non-integer number
|
|
125
|
+
* (a percent passed where basis points belong) throws RangeError.
|
|
126
|
+
*/
|
|
127
|
+
export function formatBasisPoints(bps, options = {}) {
|
|
128
|
+
if (bps === null || bps === undefined)
|
|
129
|
+
return UNSET;
|
|
130
|
+
// Intl's percent style multiplies by 100, so hand it the fraction: 825 bp
|
|
131
|
+
// is "0.0825".
|
|
132
|
+
const fraction = toDecimalString(toInteger(bps), BASIS_POINT_DIGITS + 2);
|
|
133
|
+
return percentFormat(localeOf(options)).format(fraction);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* parseBasisPoints reads a percentage such as "8.25" or "8.25%" into basis
|
|
137
|
+
* points (825). Returns null for more than two decimals, junk, a negative
|
|
138
|
+
* rate without `allowNegative`, or a magnitude above `max` (default 100%).
|
|
139
|
+
*/
|
|
140
|
+
export function parseBasisPoints(input, options = {}) {
|
|
141
|
+
const value = parseScaled(input, BASIS_POINT_DIGITS, percentGrammar(localeOf(options)), options.allowNegative ?? false);
|
|
142
|
+
if (value === null)
|
|
143
|
+
return null;
|
|
144
|
+
const bps = Number(value);
|
|
145
|
+
const max = options.max ?? DEFAULT_MAX_BASIS_POINTS;
|
|
146
|
+
if (!Number.isSafeInteger(bps) || Math.abs(bps) > max)
|
|
147
|
+
return null;
|
|
148
|
+
return bps;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* basisPointsToInput renders basis points as the value of an editable percent
|
|
152
|
+
* field: 825 → "8.25", 850 → "8.5", 1000 → "10". Parses back through
|
|
153
|
+
* parseBasisPoints. Unset → "".
|
|
154
|
+
*/
|
|
155
|
+
export function basisPointsToInput(bps, options = {}) {
|
|
156
|
+
if (bps === null || bps === undefined)
|
|
157
|
+
return "";
|
|
158
|
+
const decimal = toDecimalString(toInteger(bps), BASIS_POINT_DIGITS).replace(/\.?0+$/, "");
|
|
159
|
+
return decimal.replace(".", percentGrammar(localeOf(options)).decimal);
|
|
160
|
+
}
|
|
161
|
+
const minorDigitsCache = new Map();
|
|
162
|
+
const formatCache = new Map();
|
|
163
|
+
const grammarCache = new Map();
|
|
164
|
+
function currencyOf(options) {
|
|
165
|
+
return options.currency || DEFAULT_CURRENCY;
|
|
166
|
+
}
|
|
167
|
+
function localeOf(options) {
|
|
168
|
+
return options.locale || DEFAULT_LOCALE;
|
|
169
|
+
}
|
|
170
|
+
function toInteger(value) {
|
|
171
|
+
if (typeof value === "bigint")
|
|
172
|
+
return value;
|
|
173
|
+
if (!Number.isInteger(value)) {
|
|
174
|
+
throw new RangeError(`forge-web-runtime money: ${value} is not an integer — amounts and rates are integer minor units / basis points`);
|
|
175
|
+
}
|
|
176
|
+
return BigInt(value);
|
|
177
|
+
}
|
|
178
|
+
/** toDecimalString(123456n, 2) → "1234.56"; (-5n, 2) → "-0.05"; (7n, 0) → "7". */
|
|
179
|
+
function toDecimalString(value, digits) {
|
|
180
|
+
const negative = value < 0n;
|
|
181
|
+
const magnitude = (negative ? -value : value)
|
|
182
|
+
.toString()
|
|
183
|
+
.padStart(digits + 1, "0");
|
|
184
|
+
const whole = digits === 0 ? magnitude : magnitude.slice(0, -digits);
|
|
185
|
+
const fraction = digits === 0 ? "" : `.${magnitude.slice(-digits)}`;
|
|
186
|
+
return `${negative ? "-" : ""}${whole}${fraction}`;
|
|
187
|
+
}
|
|
188
|
+
function cached(cache, key, build) {
|
|
189
|
+
let value = cache.get(key);
|
|
190
|
+
if (value === undefined) {
|
|
191
|
+
value = build();
|
|
192
|
+
cache.set(key, value);
|
|
193
|
+
}
|
|
194
|
+
return value;
|
|
195
|
+
}
|
|
196
|
+
function currencyFormat(locale, currency) {
|
|
197
|
+
return cached(formatCache, `currency|${locale}|${currency}`, () => new Intl.NumberFormat(locale, { style: "currency", currency }));
|
|
198
|
+
}
|
|
199
|
+
function percentFormat(locale) {
|
|
200
|
+
return cached(formatCache, `percent|${locale}`, () => new Intl.NumberFormat(locale, {
|
|
201
|
+
style: "percent",
|
|
202
|
+
maximumFractionDigits: BASIS_POINT_DIGITS,
|
|
203
|
+
}));
|
|
204
|
+
}
|
|
205
|
+
function moneyGrammar(locale, currency) {
|
|
206
|
+
return cached(grammarCache, `currency|${locale}|${currency}`, () => {
|
|
207
|
+
// Read the separators off a formatter forced to show a fraction and a
|
|
208
|
+
// group, so a zero-digit currency (JPY) still reveals its decimal mark.
|
|
209
|
+
const separators = new Intl.NumberFormat(locale, {
|
|
210
|
+
style: "currency",
|
|
211
|
+
currency,
|
|
212
|
+
minimumFractionDigits: 1,
|
|
213
|
+
maximumFractionDigits: 1,
|
|
214
|
+
}).formatToParts(1234567.5);
|
|
215
|
+
// The currency's own symbols: as this locale writes them, and as en-US
|
|
216
|
+
// does ("¥" beside ja-JP's full-width "¥"), each in the default and the
|
|
217
|
+
// narrow form ("CA$" and "$"). Never another currency's.
|
|
218
|
+
const symbols = [locale, DEFAULT_LOCALE].flatMap((symbolLocale) => ["symbol", "narrowSymbol"].map((currencyDisplay) => new Intl.NumberFormat(symbolLocale, {
|
|
219
|
+
style: "currency",
|
|
220
|
+
currency,
|
|
221
|
+
currencyDisplay,
|
|
222
|
+
})
|
|
223
|
+
.formatToParts(1)
|
|
224
|
+
.find((part) => part.type === "currency")?.value ?? ""));
|
|
225
|
+
return buildGrammar(locale, separators, [...symbols, currency]);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
function percentGrammar(locale) {
|
|
229
|
+
return cached(grammarCache, `percent|${locale}`, () => {
|
|
230
|
+
const parts = new Intl.NumberFormat(locale, {
|
|
231
|
+
style: "percent",
|
|
232
|
+
minimumFractionDigits: 1,
|
|
233
|
+
maximumFractionDigits: 1,
|
|
234
|
+
}).formatToParts(12345.675);
|
|
235
|
+
const percentSign = parts.find((part) => part.type === "percentSign")?.value ?? "%";
|
|
236
|
+
return buildGrammar(locale, parts, [percentSign, "%"]);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
function buildGrammar(locale, parts, affixes) {
|
|
240
|
+
// Locales such as ar-EG render Arabic-Indic digits; read them back as
|
|
241
|
+
// ASCII so a formatted amount parses. ASCII digits are always accepted too.
|
|
242
|
+
const digitFormat = new Intl.NumberFormat(locale, { useGrouping: false });
|
|
243
|
+
const nativeDigits = new Map();
|
|
244
|
+
for (let digit = 0; digit <= 9; digit++) {
|
|
245
|
+
const native = digitFormat.format(digit);
|
|
246
|
+
if (native !== String(digit))
|
|
247
|
+
nativeDigits.set(native, String(digit));
|
|
248
|
+
}
|
|
249
|
+
const decimal = parts.find((part) => part.type === "decimal")?.value ?? ".";
|
|
250
|
+
const rawGroup = parts.find((part) => part.type === "group")?.value ?? ",";
|
|
251
|
+
const group = /^\s$/.test(rawGroup) ? " " : rawGroup;
|
|
252
|
+
const g = escapeRegExp(group);
|
|
253
|
+
return {
|
|
254
|
+
decimal,
|
|
255
|
+
group,
|
|
256
|
+
affixes: [...new Set(affixes.map(stripInvisible).filter(Boolean))].sort((a, b) => b.length - a.length),
|
|
257
|
+
// First group 1–3 digits, inner groups 2–3 (Western and Indian
|
|
258
|
+
// grouping), last group exactly 3. The last-group rule is the one that
|
|
259
|
+
// refuses a decimal comma typed where the locale groups with commas.
|
|
260
|
+
groupedInteger: new RegExp(`^\\d{1,3}(?:${g}\\d{2,3})*${g}\\d{3}$`),
|
|
261
|
+
nativeDigits,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* parseScaled reads a decimal number in the given grammar and returns it
|
|
266
|
+
* scaled by 10^digits as an exact bigint, or null when the input is not a
|
|
267
|
+
* number in that grammar or carries more than `digits` fraction digits.
|
|
268
|
+
*/
|
|
269
|
+
function parseScaled(input, digits, grammar, allowNegative) {
|
|
270
|
+
let text = stripInvisible(input).trim();
|
|
271
|
+
if (grammar.nativeDigits.size > 0) {
|
|
272
|
+
text = Array.from(text, (char) => grammar.nativeDigits.get(char) ?? char).join("");
|
|
273
|
+
}
|
|
274
|
+
let sign = "";
|
|
275
|
+
const takeSign = () => {
|
|
276
|
+
if (sign === "" && /^[+\-\u2212]/.test(text)) {
|
|
277
|
+
sign = text.charAt(0);
|
|
278
|
+
text = text.slice(1).trimStart();
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
takeSign(); // "-$12"
|
|
282
|
+
text = stripAffix(text, grammar.affixes);
|
|
283
|
+
takeSign(); // "$-12"
|
|
284
|
+
// Any run of whitespace left inside the number is grouping, held to the
|
|
285
|
+
// same placement rule as the locale's own separator: "1 234" passes,
|
|
286
|
+
// "12 50" does not.
|
|
287
|
+
text = text.replace(/\s+/g, grammar.group);
|
|
288
|
+
const [integerPart = "", fractionPart, extra] = text.split(grammar.decimal);
|
|
289
|
+
if (extra !== undefined)
|
|
290
|
+
return null;
|
|
291
|
+
if (integerPart === "" && !fractionPart)
|
|
292
|
+
return null;
|
|
293
|
+
if (integerPart !== "" &&
|
|
294
|
+
!/^\d+$/.test(integerPart) &&
|
|
295
|
+
!grammar.groupedInteger.test(integerPart)) {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
if (fractionPart !== undefined &&
|
|
299
|
+
(!/^\d*$/.test(fractionPart) || fractionPart.length > digits)) {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
const negative = sign === "-" || sign === "\u2212";
|
|
303
|
+
if (negative && !allowNegative)
|
|
304
|
+
return null;
|
|
305
|
+
const whole = BigInt(integerPart.split(grammar.group).join("") || "0");
|
|
306
|
+
const fraction = BigInt((fractionPart ?? "").padEnd(digits, "0") || "0");
|
|
307
|
+
const magnitude = whole * 10n ** BigInt(digits) + fraction;
|
|
308
|
+
return negative ? -magnitude : magnitude;
|
|
309
|
+
}
|
|
310
|
+
/** Remove one currency symbol / code / percent sign from either end. */
|
|
311
|
+
function stripAffix(text, affixes) {
|
|
312
|
+
for (const affix of affixes) {
|
|
313
|
+
// Case-insensitive so "usd 12" reads like "USD 12". Compared slice by
|
|
314
|
+
// slice: upper-casing the whole input could change its length.
|
|
315
|
+
if (text.slice(0, affix.length).toUpperCase() === affix.toUpperCase()) {
|
|
316
|
+
return text.slice(affix.length).trimStart();
|
|
317
|
+
}
|
|
318
|
+
if (text.slice(-affix.length).toUpperCase() === affix.toUpperCase()) {
|
|
319
|
+
return text.slice(0, -affix.length).trimEnd();
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return text;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Drop the bidi controls Intl places around numbers in right-to-left locales
|
|
326
|
+
* (LRM, RLM, ALM, embeddings, isolates). Invisible, so a pasted amount
|
|
327
|
+
* carries them without the user knowing.
|
|
328
|
+
*/
|
|
329
|
+
function stripInvisible(text) {
|
|
330
|
+
return text.replace(/[\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/g, "");
|
|
331
|
+
}
|
|
332
|
+
function escapeRegExp(text) {
|
|
333
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
334
|
+
}
|
|
335
|
+
//# sourceMappingURL=money.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"money.js","sourceRoot":"","sources":["../src/money.ts"],"names":[],"mappings":"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","sourcesContent":["// Part of @reliantlabs/forge-web-runtime — the web twin of forge/pkg.\n//\n// Money and basis points as INTEGERS, formatted and parsed at the edge of the\n// UI. This is the web counterpart of forge/pkg/money: an amount is an integer\n// count of its currency's minor unit (cents for USD, yen for JPY, fils for\n// KWD) plus an ISO 4217 code, and no float ever sits between the wire and the\n// screen. protobuf-es surfaces an int64 as a `bigint`; keep it one until it is\n// rendered, and turn user input straight back into one.\n//\n// formatMinorUnits(123456n) -> \"$1,234.56\"\n// formatMinorUnits(1234n, { currency: \"JPY\" }) -> \"¥1,234\"\n// formatMinorUnits(123456n, { currency: \"EUR\", locale: \"de-DE\" }) -> \"1.234,56 €\"\n// parseMinorUnits(\"$1,234.5\") -> 123450n\n// parseMinorUnits(\"1.234\") -> null (USD has 2 digits)\n// minorUnitsToInput(123450n) -> \"1234.50\"\n// currencyMinorDigits(\"KWD\") -> 3\n// formatBasisPoints(825) -> \"8.25%\"\n// parseBasisPoints(\"8.25%\") -> 825\n// basisPointsToInput(850) -> \"8.5\"\n//\n// THE RULES\n//\n// - Amounts are `bigint | number`. A `number` must be an integer: anything\n// else throws a RangeError, because 12.5 \"cents\" is a major-unit value\n// that leaked into a minor-unit field and rounding it would hide the bug.\n// `null` / `undefined` format as \"—\" and write an input value of \"\".\n// - `currency` defaults to \"USD\", and \"\" means the default too (a proto3\n// string field that was never set arrives as \"\"). A malformed code throws\n// Intl's RangeError. `locale` defaults to \"en-US\".\n// - A currency's minor-unit digits come from Intl (CLDR) —\n// `resolvedOptions().maximumFractionDigits` — and formatting and parsing\n// both use that one source, so they cannot disagree. Note CLDR, not ISO\n// 4217, is the authority: HUF and a few others differ.\n// - Formatting is exact at any size. The amount is handed to Intl as a\n// decimal STRING (Intl.NumberFormat v3 — Node 20+, every evergreen browser\n// since 2023), never as a Number, so int64 extremes print to the last\n// digit. An engine without v3 coerces the string to a Number, which is\n// still exact below about 10^15 minor units.\n// - Parsing returns null rather than guessing. Accepted: ASCII digits or\n// the locale's own (Arabic-Indic for ar-EG), surrounding and grouping\n// whitespace, the locale's group and decimal separators, the currency's\n// own symbol or ISO code before or after the number, a leading \"+\", and —\n// only with `allowNegative` — a leading \"-\" or \"−\". Anything Intl\n// rendered for the same options therefore parses back. Refused:\n// more fraction digits than the currency has (never rounded), another\n// currency's symbol, a misplaced group separator (\"12,50\" typed into an\n// en-US field is a decimal comma, so it is not read as 1,250.00), and an\n// amount outside the int64 range the wire carries.\n// - minorUnitsToInput writes the locale's decimal separator with no\n// grouping, so it round-trips through parseMinorUnits given the same\n// options. For an `<input type=\"number\">` keep the en-US default: that\n// element's value is always \".\"-decimal whatever the page's locale.\n// - Basis points are an integer count of 0.01%. Parsing allows at most two\n// decimals of a percent and caps at 100% (10000) unless `max` says\n// otherwise — the typical rate (tax, discount, commission) lives in\n// 0–100%, and the cap catches \"825\" typed where \"8.25\" was meant.\n//\n// Differences from forge/pkg/money: the Go Format is locale-neutral with its\n// own symbol table, while this renders through Intl for the caller's locale;\n// and the Go Parse accepts a negative amount, while this one makes negatives\n// opt-in, because most amounts a form collects cannot be below zero.\n//\n// This module is in the BARREL: it imports nothing, so there is no optional\n// dependency for a subpath to fence off.\n\n/** Currency and locale for a money helper. Both optional. */\nexport interface MoneyOptions {\n /** ISO 4217 code. Default \"USD\"; \"\" (a proto3 unset string) also means USD. */\n currency?: string;\n /** BCP 47 locale for separators and symbol placement. Default \"en-US\". */\n locale?: string;\n}\n\n/** Options for {@link parseMinorUnits}. */\nexport interface ParseMoneyOptions extends MoneyOptions {\n /** Accept a leading \"-\" / \"−\". Default false: a minus sign returns null. */\n allowNegative?: boolean;\n}\n\n/** Locale for a basis-point helper. */\nexport interface BasisPointsOptions {\n /** BCP 47 locale for separators and the percent sign. Default \"en-US\". */\n locale?: string;\n}\n\n/** Options for {@link parseBasisPoints}. */\nexport interface ParseBasisPointsOptions extends BasisPointsOptions {\n /** Accept a leading \"-\" / \"−\". Default false: a minus sign returns null. */\n allowNegative?: boolean;\n /** Largest magnitude accepted, in basis points. Default 10000 (100%). */\n max?: number;\n}\n\nconst DEFAULT_CURRENCY = \"USD\";\nconst DEFAULT_LOCALE = \"en-US\";\nconst UNSET = \"—\";\nconst BASIS_POINT_DIGITS = 2; // 1 bp = 0.01 percent\nconst DEFAULT_MAX_BASIS_POINTS = 10_000;\nconst INT64_MAX = 9_223_372_036_854_775_807n;\nconst INT64_MIN = -9_223_372_036_854_775_808n;\n\n/**\n * formatMinorUnits renders an integer minor-unit amount as currency for a\n * locale: 123456n → \"$1,234.56\", 1234n JPY → \"¥1,234\". Exact at any size.\n * Unset renders \"—\"; a non-integer number throws RangeError.\n */\nexport function formatMinorUnits(\n minor: bigint | number | null | undefined,\n options: MoneyOptions = {},\n): string {\n if (minor === null || minor === undefined) return UNSET;\n const currency = currencyOf(options);\n const decimal = toDecimalString(\n toInteger(minor),\n currencyMinorDigits(currency),\n );\n return currencyFormat(localeOf(options), currency).format(decimal);\n}\n\n/**\n * parseMinorUnits reads user input such as \"1,234.5\", \"$1234.56\" or \"12\" into\n * minor units. Returns null for anything it cannot represent exactly — too\n * many decimals, junk, a misplaced separator, a negative amount without\n * `allowNegative`, or a value beyond int64 — so a form rejects it instead of\n * silently rounding.\n */\nexport function parseMinorUnits(\n input: string,\n options: ParseMoneyOptions = {},\n): bigint | null {\n const currency = currencyOf(options);\n const value = parseScaled(\n input,\n currencyMinorDigits(currency),\n moneyGrammar(localeOf(options), currency),\n options.allowNegative ?? false,\n );\n if (value === null || value > INT64_MAX || value < INT64_MIN) return null;\n return value;\n}\n\n/**\n * minorUnitsToInput renders minor units as the value of an editable field:\n * 123456n → \"1234.56\" (de-DE: \"1234,56\"). No grouping, every minor digit\n * shown, and the result parses back through parseMinorUnits. Unset → \"\";\n * a non-integer number throws RangeError.\n */\nexport function minorUnitsToInput(\n minor: bigint | number | null | undefined,\n options: MoneyOptions = {},\n): string {\n if (minor === null || minor === undefined) return \"\";\n const currency = currencyOf(options);\n const decimal = toDecimalString(\n toInteger(minor),\n currencyMinorDigits(currency),\n );\n return decimal.replace(\n \".\",\n moneyGrammar(localeOf(options), currency).decimal,\n );\n}\n\n/**\n * currencyMinorDigits returns how many decimal digits a currency's minor unit\n * has — USD 2, JPY 0, KWD 3 — as Intl reports it. Useful for an input's\n * `step` or a validation message. Throws RangeError on a malformed code.\n */\nexport function currencyMinorDigits(currency?: string): number {\n const code = currency || DEFAULT_CURRENCY;\n // CLDR's digits are per currency, not per locale, so en-US answers for all.\n return cached(\n minorDigitsCache,\n code,\n () =>\n currencyFormat(DEFAULT_LOCALE, code).resolvedOptions()\n .maximumFractionDigits ?? 2,\n );\n}\n\n/**\n * formatBasisPoints renders an integer basis-point rate as a percentage:\n * 825 → \"8.25%\", 1000 → \"10%\". Unset renders \"—\"; a non-integer number\n * (a percent passed where basis points belong) throws RangeError.\n */\nexport function formatBasisPoints(\n bps: bigint | number | null | undefined,\n options: BasisPointsOptions = {},\n): string {\n if (bps === null || bps === undefined) return UNSET;\n // Intl's percent style multiplies by 100, so hand it the fraction: 825 bp\n // is \"0.0825\".\n const fraction = toDecimalString(toInteger(bps), BASIS_POINT_DIGITS + 2);\n return percentFormat(localeOf(options)).format(fraction);\n}\n\n/**\n * parseBasisPoints reads a percentage such as \"8.25\" or \"8.25%\" into basis\n * points (825). Returns null for more than two decimals, junk, a negative\n * rate without `allowNegative`, or a magnitude above `max` (default 100%).\n */\nexport function parseBasisPoints(\n input: string,\n options: ParseBasisPointsOptions = {},\n): number | null {\n const value = parseScaled(\n input,\n BASIS_POINT_DIGITS,\n percentGrammar(localeOf(options)),\n options.allowNegative ?? false,\n );\n if (value === null) return null;\n const bps = Number(value);\n const max = options.max ?? DEFAULT_MAX_BASIS_POINTS;\n if (!Number.isSafeInteger(bps) || Math.abs(bps) > max) return null;\n return bps;\n}\n\n/**\n * basisPointsToInput renders basis points as the value of an editable percent\n * field: 825 → \"8.25\", 850 → \"8.5\", 1000 → \"10\". Parses back through\n * parseBasisPoints. Unset → \"\".\n */\nexport function basisPointsToInput(\n bps: bigint | number | null | undefined,\n options: BasisPointsOptions = {},\n): string {\n if (bps === null || bps === undefined) return \"\";\n const decimal = toDecimalString(toInteger(bps), BASIS_POINT_DIGITS).replace(\n /\\.?0+$/,\n \"\",\n );\n return decimal.replace(\".\", percentGrammar(localeOf(options)).decimal);\n}\n\n// --- internals ---------------------------------------------------------------\n\n/** A decimal string Intl.NumberFormat v3 formats exactly. */\ntype DecimalString = `${number}`;\n\n/** How a locale spells a number, as far as parsing it back needs to know. */\ninterface NumberGrammar {\n decimal: string;\n /** Group separator; whitespace-like separators are normalised to \" \". */\n group: string;\n /** Symbols and codes that may precede or follow the number, longest first. */\n affixes: string[];\n /** A well-formed grouped integer part: \"1,234,567\" or \"12,34,567\". */\n groupedInteger: RegExp;\n /** The locale's own digits mapped to ASCII; empty when it uses ASCII. */\n nativeDigits: Map<string, string>;\n}\n\nconst minorDigitsCache = new Map<string, number>();\nconst formatCache = new Map<string, Intl.NumberFormat>();\nconst grammarCache = new Map<string, NumberGrammar>();\n\nfunction currencyOf(options: MoneyOptions): string {\n return options.currency || DEFAULT_CURRENCY;\n}\n\nfunction localeOf(options: { locale?: string }): string {\n return options.locale || DEFAULT_LOCALE;\n}\n\nfunction toInteger(value: bigint | number): bigint {\n if (typeof value === \"bigint\") return value;\n if (!Number.isInteger(value)) {\n throw new RangeError(\n `forge-web-runtime money: ${value} is not an integer — amounts and rates are integer minor units / basis points`,\n );\n }\n return BigInt(value);\n}\n\n/** toDecimalString(123456n, 2) → \"1234.56\"; (-5n, 2) → \"-0.05\"; (7n, 0) → \"7\". */\nfunction toDecimalString(value: bigint, digits: number): DecimalString {\n const negative = value < 0n;\n const magnitude = (negative ? -value : value)\n .toString()\n .padStart(digits + 1, \"0\");\n const whole = digits === 0 ? magnitude : magnitude.slice(0, -digits);\n const fraction = digits === 0 ? \"\" : `.${magnitude.slice(-digits)}`;\n return `${negative ? \"-\" : \"\"}${whole}${fraction}` as DecimalString;\n}\n\nfunction cached<T>(cache: Map<string, T>, key: string, build: () => T): T {\n let value = cache.get(key);\n if (value === undefined) {\n value = build();\n cache.set(key, value);\n }\n return value;\n}\n\nfunction currencyFormat(locale: string, currency: string): Intl.NumberFormat {\n return cached(\n formatCache,\n `currency|${locale}|${currency}`,\n () => new Intl.NumberFormat(locale, { style: \"currency\", currency }),\n );\n}\n\nfunction percentFormat(locale: string): Intl.NumberFormat {\n return cached(\n formatCache,\n `percent|${locale}`,\n () =>\n new Intl.NumberFormat(locale, {\n style: \"percent\",\n maximumFractionDigits: BASIS_POINT_DIGITS,\n }),\n );\n}\n\nfunction moneyGrammar(locale: string, currency: string): NumberGrammar {\n return cached(grammarCache, `currency|${locale}|${currency}`, () => {\n // Read the separators off a formatter forced to show a fraction and a\n // group, so a zero-digit currency (JPY) still reveals its decimal mark.\n const separators = new Intl.NumberFormat(locale, {\n style: \"currency\",\n currency,\n minimumFractionDigits: 1,\n maximumFractionDigits: 1,\n }).formatToParts(1234567.5);\n // The currency's own symbols: as this locale writes them, and as en-US\n // does (\"¥\" beside ja-JP's full-width \"¥\"), each in the default and the\n // narrow form (\"CA$\" and \"$\"). Never another currency's.\n const symbols = [locale, DEFAULT_LOCALE].flatMap((symbolLocale) =>\n ([\"symbol\", \"narrowSymbol\"] as const).map(\n (currencyDisplay) =>\n new Intl.NumberFormat(symbolLocale, {\n style: \"currency\",\n currency,\n currencyDisplay,\n })\n .formatToParts(1)\n .find((part) => part.type === \"currency\")?.value ?? \"\",\n ),\n );\n return buildGrammar(locale, separators, [...symbols, currency]);\n });\n}\n\nfunction percentGrammar(locale: string): NumberGrammar {\n return cached(grammarCache, `percent|${locale}`, () => {\n const parts = new Intl.NumberFormat(locale, {\n style: \"percent\",\n minimumFractionDigits: 1,\n maximumFractionDigits: 1,\n }).formatToParts(12345.675);\n const percentSign =\n parts.find((part) => part.type === \"percentSign\")?.value ?? \"%\";\n return buildGrammar(locale, parts, [percentSign, \"%\"]);\n });\n}\n\nfunction buildGrammar(\n locale: string,\n parts: Intl.NumberFormatPart[],\n affixes: string[],\n): NumberGrammar {\n // Locales such as ar-EG render Arabic-Indic digits; read them back as\n // ASCII so a formatted amount parses. ASCII digits are always accepted too.\n const digitFormat = new Intl.NumberFormat(locale, { useGrouping: false });\n const nativeDigits = new Map<string, string>();\n for (let digit = 0; digit <= 9; digit++) {\n const native = digitFormat.format(digit);\n if (native !== String(digit)) nativeDigits.set(native, String(digit));\n }\n const decimal = parts.find((part) => part.type === \"decimal\")?.value ?? \".\";\n const rawGroup = parts.find((part) => part.type === \"group\")?.value ?? \",\";\n const group = /^\\s$/.test(rawGroup) ? \" \" : rawGroup;\n const g = escapeRegExp(group);\n return {\n decimal,\n group,\n affixes: [...new Set(affixes.map(stripInvisible).filter(Boolean))].sort(\n (a, b) => b.length - a.length,\n ),\n // First group 1–3 digits, inner groups 2–3 (Western and Indian\n // grouping), last group exactly 3. The last-group rule is the one that\n // refuses a decimal comma typed where the locale groups with commas.\n groupedInteger: new RegExp(`^\\\\d{1,3}(?:${g}\\\\d{2,3})*${g}\\\\d{3}$`),\n nativeDigits,\n };\n}\n\n/**\n * parseScaled reads a decimal number in the given grammar and returns it\n * scaled by 10^digits as an exact bigint, or null when the input is not a\n * number in that grammar or carries more than `digits` fraction digits.\n */\nfunction parseScaled(\n input: string,\n digits: number,\n grammar: NumberGrammar,\n allowNegative: boolean,\n): bigint | null {\n let text = stripInvisible(input).trim();\n if (grammar.nativeDigits.size > 0) {\n text = Array.from(\n text,\n (char) => grammar.nativeDigits.get(char) ?? char,\n ).join(\"\");\n }\n let sign = \"\";\n const takeSign = (): void => {\n if (sign === \"\" && /^[+\\-\\u2212]/.test(text)) {\n sign = text.charAt(0);\n text = text.slice(1).trimStart();\n }\n };\n takeSign(); // \"-$12\"\n text = stripAffix(text, grammar.affixes);\n takeSign(); // \"$-12\"\n\n // Any run of whitespace left inside the number is grouping, held to the\n // same placement rule as the locale's own separator: \"1 234\" passes,\n // \"12 50\" does not.\n text = text.replace(/\\s+/g, grammar.group);\n\n const [integerPart = \"\", fractionPart, extra] = text.split(grammar.decimal);\n if (extra !== undefined) return null;\n if (integerPart === \"\" && !fractionPart) return null;\n if (\n integerPart !== \"\" &&\n !/^\\d+$/.test(integerPart) &&\n !grammar.groupedInteger.test(integerPart)\n ) {\n return null;\n }\n if (\n fractionPart !== undefined &&\n (!/^\\d*$/.test(fractionPart) || fractionPart.length > digits)\n ) {\n return null;\n }\n\n const negative = sign === \"-\" || sign === \"\\u2212\";\n if (negative && !allowNegative) return null;\n\n const whole = BigInt(integerPart.split(grammar.group).join(\"\") || \"0\");\n const fraction = BigInt((fractionPart ?? \"\").padEnd(digits, \"0\") || \"0\");\n const magnitude = whole * 10n ** BigInt(digits) + fraction;\n return negative ? -magnitude : magnitude;\n}\n\n/** Remove one currency symbol / code / percent sign from either end. */\nfunction stripAffix(text: string, affixes: string[]): string {\n for (const affix of affixes) {\n // Case-insensitive so \"usd 12\" reads like \"USD 12\". Compared slice by\n // slice: upper-casing the whole input could change its length.\n if (text.slice(0, affix.length).toUpperCase() === affix.toUpperCase()) {\n return text.slice(affix.length).trimStart();\n }\n if (text.slice(-affix.length).toUpperCase() === affix.toUpperCase()) {\n return text.slice(0, -affix.length).trimEnd();\n }\n }\n return text;\n}\n\n/**\n * Drop the bidi controls Intl places around numbers in right-to-left locales\n * (LRM, RLM, ALM, embeddings, isolates). Invisible, so a pasted amount\n * carries them without the user knowing.\n */\nfunction stripInvisible(text: string): string {\n return text.replace(/[\\u061c\\u200e\\u200f\\u202a-\\u202e\\u2066-\\u2069]/g, \"\");\n}\n\nfunction escapeRegExp(text: string): string {\n return text.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n"]}
|
package/dist/resource.js
CHANGED
|
@@ -12,6 +12,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
12
12
|
// cursor-based (page_token / next_page_token in Connect list RPCs): the page
|
|
13
13
|
// owns the cursor stack and passes onNextPage/onPrevPage.
|
|
14
14
|
import { useEffect, useState } from "react";
|
|
15
|
+
import { userMessage } from "./errors.js";
|
|
15
16
|
export function Resource({ status, data, columns, rowKey, title, actions, error, onRetry, onRowClick, emptyTitle = "Nothing here yet", emptyMessage = "No records match.", filter, onFilterChange, filterPlaceholder = "Search…", filterDebounceMs = 300, onNextPage, onPrevPage, hasNextPage, hasPrevPage, isFetching, }) {
|
|
16
17
|
const [query, setQuery] = useState(filter ?? "");
|
|
17
18
|
// Keep local input in sync when the parent resets the filter externally.
|
|
@@ -66,10 +67,14 @@ export function Resource({ status, data, columns, rowKey, title, actions, error,
|
|
|
66
67
|
// no-aria-hidden-on-focusable — correctly, since a hidden focusable node is
|
|
67
68
|
// a focus trap for screen-reader users.
|
|
68
69
|
function SkeletonRows({ rows, cols }) {
|
|
69
|
-
return (_jsx("tr", { children: _jsxs("td", { colSpan: cols, className: "p-0", children: [_jsx("span", { className: "sr-only", children: "Loading\u2026" }), _jsx("div", { "aria-hidden": "true", className: "divide-y divide-border", children: Array.from({ length: rows }).map((_, r) => (_jsx("div", { className: "flex items-center gap-4 px-4 py-3", children: Array.from({ length: cols }).map((__, c) => (_jsx("div", { className: "h-4 flex-1 animate-pulse rounded bg-surface-muted" }, c))) }, r))) })] }) }));
|
|
70
|
+
return (_jsx("tr", { children: _jsxs("td", { colSpan: cols, className: "p-0", children: [_jsx("span", { className: "sr-only", children: "Loading\u2026" }), _jsx("div", { "aria-hidden": "true", className: "divide-y divide-border", children: Array.from({ length: rows }).map((_, r) => (_jsx("div", { className: "flex items-center gap-4 px-4 py-3", children: Array.from({ length: cols }).map((__, c) => (_jsx("div", { className: "h-4 flex-1 animate-pulse motion-reduce:animate-none rounded bg-surface-muted" }, c))) }, r))) })] }) }));
|
|
70
71
|
}
|
|
72
|
+
// ErrorState shows userMessage(error), never error.message: a ConnectError's
|
|
73
|
+
// message carries the "[code] " transport framing, which is not copy a user
|
|
74
|
+
// should read. It is body text in the empty rung's style, not a monospace
|
|
75
|
+
// debug readout.
|
|
71
76
|
function ErrorState({ error, onRetry, }) {
|
|
72
|
-
return (_jsxs("div", { role: "alert", className: "flex flex-col items-center gap-2 text-center", children: [_jsx("p", { className: "text-sm font-medium text-danger-ink", children: "Couldn't load data" }), error
|
|
77
|
+
return (_jsxs("div", { role: "alert", className: "flex flex-col items-center gap-2 text-center", children: [_jsx("p", { className: "text-sm font-medium text-danger-ink", children: "Couldn't load data" }), error ? (_jsx("p", { className: "max-w-md text-sm text-ink-muted", children: userMessage(error) })) : null, onRetry ? (_jsx("button", { type: "button", onClick: onRetry, className: "mt-1 rounded-md bg-danger px-3 py-1.5 text-sm font-medium text-on-danger hover:bg-danger-hover", children: "Try again" })) : null] }));
|
|
73
78
|
}
|
|
74
79
|
function EmptyState({ title, message }) {
|
|
75
80
|
return (_jsxs("div", { className: "flex flex-col items-center gap-1 text-center", children: [_jsx("p", { className: "text-sm font-medium text-ink", children: title }), _jsx("p", { className: "text-sm text-ink-muted", children: message })] }));
|
package/dist/resource.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resource.js","sourceRoot":"","sources":["../src/resource.tsx"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\n// Part of @reliantlabs/forge-web-runtime — the web twin of forge/pkg.\n//\n// <Resource> — the owned-once data-table container. It encapsulates the\n// loading / error / empty / data tristate ladder, a server-side (debounced)\n// filter, and cursor pagination, so list pages stop re-hand-rolling the\n// ladder and stop client-side-filtering a single page cap.\n//\n// Pairs with the app's useQueryResource hook: map a query result to\n// { status, data, error } and hand it here. Pagination is\n// cursor-based (page_token / next_page_token in Connect list RPCs): the page\n// owns the cursor stack and passes onNextPage/onPrevPage.\nimport { useEffect, useState, type ReactElement, type ReactNode } from \"react\";\n\nexport interface ResourceColumn<T> {\n header: string;\n cell: (row: T) => ReactNode;\n /** Extra classes for the cell + header (alignment, width). */\n className?: string;\n}\n\nexport type ResourceStatus = \"loading\" | \"error\" | \"success\";\n\nexport interface ResourceProps<T> {\n status: ResourceStatus;\n data: T[] | undefined;\n columns: ResourceColumn<T>[];\n rowKey: (row: T) => string;\n\n title?: string;\n /** Header-right slot, e.g. a \"New\" button. */\n actions?: ReactNode;\n error?: Error | null;\n onRetry?: () => void;\n onRowClick?: (row: T) => void;\n\n emptyTitle?: string;\n emptyMessage?: string;\n\n // Server-side filter (debounced). Omit onFilterChange to hide the search box.\n filter?: string;\n onFilterChange?: (value: string) => void;\n filterPlaceholder?: string;\n filterDebounceMs?: number;\n\n // Cursor pagination. Omit the handlers to hide the footer.\n onNextPage?: () => void;\n onPrevPage?: () => void;\n hasNextPage?: boolean;\n hasPrevPage?: boolean;\n /** True during a background refetch (page change / filter change). */\n isFetching?: boolean;\n}\n\nexport function Resource<T>({\n status,\n data,\n columns,\n rowKey,\n title,\n actions,\n error,\n onRetry,\n onRowClick,\n emptyTitle = \"Nothing here yet\",\n emptyMessage = \"No records match.\",\n filter,\n onFilterChange,\n filterPlaceholder = \"Search…\",\n filterDebounceMs = 300,\n onNextPage,\n onPrevPage,\n hasNextPage,\n hasPrevPage,\n isFetching,\n}: ResourceProps<T>): ReactElement {\n const [query, setQuery] = useState(filter ?? \"\");\n\n // Keep local input in sync when the parent resets the filter externally.\n useEffect(() => {\n setQuery(filter ?? \"\");\n }, [filter]);\n\n // Debounce filter changes so each keystroke doesn't fire an RPC.\n useEffect(() => {\n if (!onFilterChange || query === (filter ?? \"\")) {\n return;\n }\n const handle = setTimeout(() => onFilterChange(query), filterDebounceMs);\n return () => clearTimeout(handle);\n }, [query, filter, onFilterChange, filterDebounceMs]);\n\n const showFooter = Boolean(onNextPage || onPrevPage);\n const colCount = columns.length;\n const captionText = title ?? \"Results\";\n\n return (\n <section className=\"flex flex-col gap-4\">\n {(title || actions) && (\n <header className=\"flex items-center justify-between gap-4\">\n {title ? (\n <h1 className=\"text-xl font-semibold text-ink\">{title}</h1>\n ) : (\n <span />\n )}\n {actions ? (\n <div className=\"flex items-center gap-2\">{actions}</div>\n ) : null}\n </header>\n )}\n\n {onFilterChange ? (\n <input\n type=\"search\"\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder={filterPlaceholder}\n aria-label={title ? `Search ${title.toLowerCase()}` : \"Search\"}\n className=\"w-full max-w-sm rounded-md border border-border bg-surface px-3 py-2 text-sm text-ink shadow-sm focus:border-accent focus:outline-none focus:ring-1 focus:ring-accent\"\n />\n ) : null}\n\n <div className=\"overflow-x-auto rounded-lg border border-border bg-surface\">\n <table className=\"min-w-full divide-y divide-border text-sm\">\n <caption className=\"sr-only\">{captionText}</caption>\n <thead className=\"bg-surface-muted\">\n <tr>\n {columns.map((col, i) => (\n <th\n key={i}\n scope=\"col\"\n className={`px-4 py-2.5 text-left font-medium text-ink-muted ${\n col.className ?? \"\"\n }`}\n >\n {col.header}\n </th>\n ))}\n </tr>\n </thead>\n <tbody className=\"divide-y divide-border\">\n {status === \"loading\" ? (\n <SkeletonRows rows={5} cols={colCount} />\n ) : status === \"error\" ? (\n <tr>\n <td colSpan={colCount} className=\"px-4 py-10\">\n <ErrorState error={error} onRetry={onRetry} />\n </td>\n </tr>\n ) : !data || data.length === 0 ? (\n <tr>\n <td colSpan={colCount} className=\"px-4 py-12\">\n <EmptyState title={emptyTitle} message={emptyMessage} />\n </td>\n </tr>\n ) : (\n data.map((row) => (\n <tr\n key={rowKey(row)}\n onClick={onRowClick ? () => onRowClick(row) : undefined}\n // Keyboard-operable when the row navigates: Enter/Space\n // activate it, and it takes focus in tab order.\n role={onRowClick ? \"button\" : undefined}\n tabIndex={onRowClick ? 0 : undefined}\n onKeyDown={\n onRowClick\n ? (e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onRowClick(row);\n }\n }\n : undefined\n }\n className={\n onRowClick\n ? \"cursor-pointer transition-colors hover:bg-surface-muted focus:bg-surface-muted focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-accent\"\n : undefined\n }\n >\n {columns.map((col, i) => (\n <td\n key={i}\n className={`px-4 py-2.5 text-ink ${col.className ?? \"\"}`}\n >\n {col.cell(row)}\n </td>\n ))}\n </tr>\n ))\n )}\n </tbody>\n </table>\n </div>\n\n {showFooter ? (\n <footer className=\"flex items-center justify-end gap-2\">\n {isFetching ? (\n <span className=\"mr-auto text-xs text-ink-subtle\">Loading…</span>\n ) : null}\n <button\n type=\"button\"\n onClick={onPrevPage}\n disabled={!hasPrevPage}\n className=\"rounded-md border border-border px-3 py-1.5 text-sm font-medium text-ink-muted hover:bg-surface-muted disabled:cursor-not-allowed disabled:opacity-40\"\n >\n Previous\n </button>\n <button\n type=\"button\"\n onClick={onNextPage}\n disabled={!hasNextPage}\n className=\"rounded-md border border-border px-3 py-1.5 text-sm font-medium text-ink-muted hover:bg-surface-muted disabled:cursor-not-allowed disabled:opacity-40\"\n >\n Next\n </button>\n </footer>\n ) : null}\n </section>\n );\n}\n\n// SkeletonRows is the loading rung of the tristate ladder: a shimmer the\n// width of the table while the first page is in flight.\n//\n// It is ONE row with a colSpan cell, matching the error and empty rungs\n// above — not `rows` × `cols` real table cells. That shape is what makes it\n// accessible, and the reasoning is worth keeping:\n//\n// - The shimmer is pure decoration. It carries no data, so it is wrapped\n// in aria-hidden and the cell carries one sr-only \"Loading…\" instead.\n// Assistive tech hears the state once, not once per placeholder.\n// - A grid of empty <td>s announced as a table of blank cells, and read\n// as a real 5-row result set by anything counting rows. The placeholder\n// geometry is now plain <div>s inside the single cell, so the table has\n// exactly the structure it claims to have at every moment.\n// - Empty cells also tripped jsx-a11y/control-has-associated-label under\n// the eslint config forge scaffolds. That was the symptom that surfaced\n// this; the row-shape mismatch above is the actual defect.\n//\n// aria-hidden goes on the inner <div>, never on the <td> or <tr>: jsx-a11y\n// treats table elements as focusable, so hiding one is\n// no-aria-hidden-on-focusable — correctly, since a hidden focusable node is\n// a focus trap for screen-reader users.\nfunction SkeletonRows({ rows, cols }: { rows: number; cols: number }) {\n return (\n <tr>\n <td colSpan={cols} className=\"p-0\">\n <span className=\"sr-only\">Loading…</span>\n <div aria-hidden=\"true\" className=\"divide-y divide-border\">\n {Array.from({ length: rows }).map((_, r) => (\n <div key={r} className=\"flex items-center gap-4 px-4 py-3\">\n {Array.from({ length: cols }).map((__, c) => (\n <div\n key={c}\n className=\"h-4 flex-1 animate-pulse rounded bg-surface-muted\"\n />\n ))}\n </div>\n ))}\n </div>\n </td>\n </tr>\n );\n}\n\nfunction ErrorState({\n error,\n onRetry,\n}: {\n error?: Error | null;\n onRetry?: () => void;\n}) {\n return (\n <div role=\"alert\" className=\"flex flex-col items-center gap-2 text-center\">\n <p className=\"text-sm font-medium text-danger-ink\">\n Couldn't load data\n </p>\n {error?.message ? (\n <p className=\"max-w-md font-mono text-xs text-danger\">\n {error.message}\n </p>\n ) : null}\n {onRetry ? (\n <button\n type=\"button\"\n onClick={onRetry}\n className=\"mt-1 rounded-md bg-danger px-3 py-1.5 text-sm font-medium text-on-danger hover:bg-danger-hover\"\n >\n Try again\n </button>\n ) : null}\n </div>\n );\n}\n\nfunction EmptyState({ title, message }: { title: string; message: string }) {\n return (\n <div className=\"flex flex-col items-center gap-1 text-center\">\n <p className=\"text-sm font-medium text-ink\">{title}</p>\n <p className=\"text-sm text-ink-muted\">{message}</p>\n </div>\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"resource.js","sourceRoot":"","sources":["../src/resource.tsx"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\n// Part of @reliantlabs/forge-web-runtime — the web twin of forge/pkg.\n//\n// <Resource> — the owned-once data-table container. It encapsulates the\n// loading / error / empty / data tristate ladder, a server-side (debounced)\n// filter, and cursor pagination, so list pages stop re-hand-rolling the\n// ladder and stop client-side-filtering a single page cap.\n//\n// Pairs with the app's useQueryResource hook: map a query result to\n// { status, data, error } and hand it here. Pagination is\n// cursor-based (page_token / next_page_token in Connect list RPCs): the page\n// owns the cursor stack and passes onNextPage/onPrevPage.\nimport { useEffect, useState, type ReactElement, type ReactNode } from \"react\";\n\nimport { userMessage } from \"./errors.js\";\n\nexport interface ResourceColumn<T> {\n header: string;\n cell: (row: T) => ReactNode;\n /** Extra classes for the cell + header (alignment, width). */\n className?: string;\n}\n\nexport type ResourceStatus = \"loading\" | \"error\" | \"success\";\n\nexport interface ResourceProps<T> {\n status: ResourceStatus;\n data: T[] | undefined;\n columns: ResourceColumn<T>[];\n rowKey: (row: T) => string;\n\n title?: string;\n /** Header-right slot, e.g. a \"New\" button. */\n actions?: ReactNode;\n error?: Error | null;\n onRetry?: () => void;\n onRowClick?: (row: T) => void;\n\n emptyTitle?: string;\n emptyMessage?: string;\n\n // Server-side filter (debounced). Omit onFilterChange to hide the search box.\n filter?: string;\n onFilterChange?: (value: string) => void;\n filterPlaceholder?: string;\n filterDebounceMs?: number;\n\n // Cursor pagination. Omit the handlers to hide the footer.\n onNextPage?: () => void;\n onPrevPage?: () => void;\n hasNextPage?: boolean;\n hasPrevPage?: boolean;\n /** True during a background refetch (page change / filter change). */\n isFetching?: boolean;\n}\n\nexport function Resource<T>({\n status,\n data,\n columns,\n rowKey,\n title,\n actions,\n error,\n onRetry,\n onRowClick,\n emptyTitle = \"Nothing here yet\",\n emptyMessage = \"No records match.\",\n filter,\n onFilterChange,\n filterPlaceholder = \"Search…\",\n filterDebounceMs = 300,\n onNextPage,\n onPrevPage,\n hasNextPage,\n hasPrevPage,\n isFetching,\n}: ResourceProps<T>): ReactElement {\n const [query, setQuery] = useState(filter ?? \"\");\n\n // Keep local input in sync when the parent resets the filter externally.\n useEffect(() => {\n setQuery(filter ?? \"\");\n }, [filter]);\n\n // Debounce filter changes so each keystroke doesn't fire an RPC.\n useEffect(() => {\n if (!onFilterChange || query === (filter ?? \"\")) {\n return;\n }\n const handle = setTimeout(() => onFilterChange(query), filterDebounceMs);\n return () => clearTimeout(handle);\n }, [query, filter, onFilterChange, filterDebounceMs]);\n\n const showFooter = Boolean(onNextPage || onPrevPage);\n const colCount = columns.length;\n const captionText = title ?? \"Results\";\n\n return (\n <section className=\"flex flex-col gap-4\">\n {(title || actions) && (\n <header className=\"flex items-center justify-between gap-4\">\n {title ? (\n <h1 className=\"text-xl font-semibold text-ink\">{title}</h1>\n ) : (\n <span />\n )}\n {actions ? (\n <div className=\"flex items-center gap-2\">{actions}</div>\n ) : null}\n </header>\n )}\n\n {onFilterChange ? (\n <input\n type=\"search\"\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder={filterPlaceholder}\n aria-label={title ? `Search ${title.toLowerCase()}` : \"Search\"}\n className=\"w-full max-w-sm rounded-md border border-border bg-surface px-3 py-2 text-sm text-ink shadow-sm focus:border-accent focus:outline-none focus:ring-1 focus:ring-accent\"\n />\n ) : null}\n\n <div className=\"overflow-x-auto rounded-lg border border-border bg-surface\">\n <table className=\"min-w-full divide-y divide-border text-sm\">\n <caption className=\"sr-only\">{captionText}</caption>\n <thead className=\"bg-surface-muted\">\n <tr>\n {columns.map((col, i) => (\n <th\n key={i}\n scope=\"col\"\n className={`px-4 py-2.5 text-left font-medium text-ink-muted ${\n col.className ?? \"\"\n }`}\n >\n {col.header}\n </th>\n ))}\n </tr>\n </thead>\n <tbody className=\"divide-y divide-border\">\n {status === \"loading\" ? (\n <SkeletonRows rows={5} cols={colCount} />\n ) : status === \"error\" ? (\n <tr>\n <td colSpan={colCount} className=\"px-4 py-10\">\n <ErrorState error={error} onRetry={onRetry} />\n </td>\n </tr>\n ) : !data || data.length === 0 ? (\n <tr>\n <td colSpan={colCount} className=\"px-4 py-12\">\n <EmptyState title={emptyTitle} message={emptyMessage} />\n </td>\n </tr>\n ) : (\n data.map((row) => (\n <tr\n key={rowKey(row)}\n onClick={onRowClick ? () => onRowClick(row) : undefined}\n // Keyboard-operable when the row navigates: Enter/Space\n // activate it, and it takes focus in tab order.\n role={onRowClick ? \"button\" : undefined}\n tabIndex={onRowClick ? 0 : undefined}\n onKeyDown={\n onRowClick\n ? (e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onRowClick(row);\n }\n }\n : undefined\n }\n className={\n onRowClick\n ? \"cursor-pointer transition-colors hover:bg-surface-muted focus:bg-surface-muted focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-accent\"\n : undefined\n }\n >\n {columns.map((col, i) => (\n <td\n key={i}\n className={`px-4 py-2.5 text-ink ${col.className ?? \"\"}`}\n >\n {col.cell(row)}\n </td>\n ))}\n </tr>\n ))\n )}\n </tbody>\n </table>\n </div>\n\n {showFooter ? (\n <footer className=\"flex items-center justify-end gap-2\">\n {isFetching ? (\n <span className=\"mr-auto text-xs text-ink-subtle\">Loading…</span>\n ) : null}\n <button\n type=\"button\"\n onClick={onPrevPage}\n disabled={!hasPrevPage}\n className=\"rounded-md border border-border px-3 py-1.5 text-sm font-medium text-ink-muted hover:bg-surface-muted disabled:cursor-not-allowed disabled:opacity-40\"\n >\n Previous\n </button>\n <button\n type=\"button\"\n onClick={onNextPage}\n disabled={!hasNextPage}\n className=\"rounded-md border border-border px-3 py-1.5 text-sm font-medium text-ink-muted hover:bg-surface-muted disabled:cursor-not-allowed disabled:opacity-40\"\n >\n Next\n </button>\n </footer>\n ) : null}\n </section>\n );\n}\n\n// SkeletonRows is the loading rung of the tristate ladder: a shimmer the\n// width of the table while the first page is in flight.\n//\n// It is ONE row with a colSpan cell, matching the error and empty rungs\n// above — not `rows` × `cols` real table cells. That shape is what makes it\n// accessible, and the reasoning is worth keeping:\n//\n// - The shimmer is pure decoration. It carries no data, so it is wrapped\n// in aria-hidden and the cell carries one sr-only \"Loading…\" instead.\n// Assistive tech hears the state once, not once per placeholder.\n// - A grid of empty <td>s announced as a table of blank cells, and read\n// as a real 5-row result set by anything counting rows. The placeholder\n// geometry is now plain <div>s inside the single cell, so the table has\n// exactly the structure it claims to have at every moment.\n// - Empty cells also tripped jsx-a11y/control-has-associated-label under\n// the eslint config forge scaffolds. That was the symptom that surfaced\n// this; the row-shape mismatch above is the actual defect.\n//\n// aria-hidden goes on the inner <div>, never on the <td> or <tr>: jsx-a11y\n// treats table elements as focusable, so hiding one is\n// no-aria-hidden-on-focusable — correctly, since a hidden focusable node is\n// a focus trap for screen-reader users.\nfunction SkeletonRows({ rows, cols }: { rows: number; cols: number }) {\n return (\n <tr>\n <td colSpan={cols} className=\"p-0\">\n <span className=\"sr-only\">Loading…</span>\n <div aria-hidden=\"true\" className=\"divide-y divide-border\">\n {Array.from({ length: rows }).map((_, r) => (\n <div key={r} className=\"flex items-center gap-4 px-4 py-3\">\n {Array.from({ length: cols }).map((__, c) => (\n <div\n key={c}\n className=\"h-4 flex-1 animate-pulse motion-reduce:animate-none rounded bg-surface-muted\"\n />\n ))}\n </div>\n ))}\n </div>\n </td>\n </tr>\n );\n}\n\n// ErrorState shows userMessage(error), never error.message: a ConnectError's\n// message carries the \"[code] \" transport framing, which is not copy a user\n// should read. It is body text in the empty rung's style, not a monospace\n// debug readout.\nfunction ErrorState({\n error,\n onRetry,\n}: {\n error?: Error | null;\n onRetry?: () => void;\n}) {\n return (\n <div role=\"alert\" className=\"flex flex-col items-center gap-2 text-center\">\n <p className=\"text-sm font-medium text-danger-ink\">\n Couldn't load data\n </p>\n {error ? (\n <p className=\"max-w-md text-sm text-ink-muted\">{userMessage(error)}</p>\n ) : null}\n {onRetry ? (\n <button\n type=\"button\"\n onClick={onRetry}\n className=\"mt-1 rounded-md bg-danger px-3 py-1.5 text-sm font-medium text-on-danger hover:bg-danger-hover\"\n >\n Try again\n </button>\n ) : null}\n </div>\n );\n}\n\nfunction EmptyState({ title, message }: { title: string; message: string }) {\n return (\n <div className=\"flex flex-col items-center gap-1 text-center\">\n <p className=\"text-sm font-medium text-ink\">{title}</p>\n <p className=\"text-sm text-ink-muted\">{message}</p>\n </div>\n );\n}\n"]}
|
package/dist/telemetry.d.ts
CHANGED
|
@@ -1,25 +1,65 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export interface
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
/** Where the browser ships telemetry: this origin's `/_otel`. */
|
|
2
|
+
export declare const DEFAULT_INGEST_PATH = "/_otel";
|
|
3
|
+
/**
|
|
4
|
+
* The apiKey handed to the SDK. Public by construction: it identifies
|
|
5
|
+
* nothing and authorises nothing. See the header — the proxy or collector
|
|
6
|
+
* owns the real credential.
|
|
7
|
+
*/
|
|
8
|
+
export declare const PLACEHOLDER_API_KEY = "forge-browser-public";
|
|
9
|
+
export interface BrowserTelemetryConfig {
|
|
10
|
+
/** `service.name` on everything this page emits. */
|
|
11
|
+
serviceName: string;
|
|
12
|
+
/** `service.version`. Omit for none. */
|
|
13
|
+
serviceVersion?: string;
|
|
14
|
+
/**
|
|
15
|
+
* The forge environment name. Emitted as BOTH `deployment.environment.name`
|
|
16
|
+
* (current semconv) and `deployment.environment` (the legacy key HyperDX's
|
|
17
|
+
* default sources still key on).
|
|
18
|
+
*/
|
|
19
|
+
environment?: string;
|
|
20
|
+
/** `service.namespace`, conventionally the project name. */
|
|
21
|
+
serviceNamespace?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Ingest base: a path on this origin (`/_otel`) or, as an escape hatch, an
|
|
24
|
+
* absolute URL. Unset, empty, or `off` disables telemetry — the app runs
|
|
25
|
+
* untraced rather than throwing, which is what makes it safe to call
|
|
26
|
+
* unconditionally from the app shell.
|
|
27
|
+
*/
|
|
12
28
|
endpoint?: string;
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Origins (or URLs — only the origin is used) of the app's own backend.
|
|
31
|
+
* Requests to them always get a `traceparent` from the SDK's fetch/XHR
|
|
32
|
+
* spans. This page's own origin is always included.
|
|
33
|
+
*/
|
|
34
|
+
apiOrigins?: Array<string | undefined>;
|
|
35
|
+
/** Mirror console.* into telemetry. Default true. */
|
|
36
|
+
consoleCapture?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Capture full request/response headers AND BODIES on fetch/XHR spans.
|
|
39
|
+
* Default false, and it should stay false: bodies are user data.
|
|
40
|
+
*/
|
|
41
|
+
advancedNetworkCapture?: boolean;
|
|
42
|
+
/** Session replay (rrweb). Default false. When true, all text and inputs are masked. */
|
|
43
|
+
replay?: boolean;
|
|
44
|
+
/** Verbose SDK diagnostics to the console. Default false. */
|
|
45
|
+
debug?: boolean;
|
|
19
46
|
}
|
|
20
47
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
|
|
48
|
+
* initBrowserTelemetry starts the HyperDX browser SDK once per page load and
|
|
49
|
+
* resolves to whether it is running. See the module header for the contract.
|
|
50
|
+
*/
|
|
51
|
+
export declare function initBrowserTelemetry(config: BrowserTelemetryConfig): Promise<boolean>;
|
|
52
|
+
/**
|
|
53
|
+
* The options passed to HyperDX.init. Exported for tests and for apps that
|
|
54
|
+
* want to see exactly what is configured — it is pure.
|
|
55
|
+
*/
|
|
56
|
+
export declare function buildSdkOptions(config: BrowserTelemetryConfig, ingestUrl: string, pageOrigin: string): Record<string, unknown>;
|
|
57
|
+
/**
|
|
58
|
+
* resolveIngestUrl turns the configured endpoint into the absolute URL the SDK
|
|
59
|
+
* is given, or null when telemetry is off. A path resolves against the page
|
|
60
|
+
* origin, so the default is same-origin by construction.
|
|
24
61
|
*/
|
|
25
|
-
export declare function
|
|
62
|
+
export declare function resolveIngestUrl(endpoint: string | undefined, pageOrigin: string): string | null;
|
|
63
|
+
/** The tracer to open manual spans on. Resolves HyperDX's provider once it is running. */
|
|
64
|
+
export declare function getTracer(name?: string): import("@opentelemetry/api").Tracer;
|
|
65
|
+
export { context, propagation, trace } from "@opentelemetry/api";
|