@withpotter/lib 0.13.0-next.84 → 0.13.0-next.85
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/utils.js +4 -4
- package/dist/utils.js.map +1 -1
- package/dist/wallet-format.js +1 -0
- package/dist/wallet-format.js.map +1 -1
- package/package.json +2 -2
package/dist/utils.js
CHANGED
|
@@ -23,7 +23,7 @@ const CURRENCY_SYMBOLS = {
|
|
|
23
23
|
MXN: "$",
|
|
24
24
|
AED: "\u062F.\u0625"
|
|
25
25
|
};
|
|
26
|
-
function normalizeCurrencyCode(currency, fallback = "
|
|
26
|
+
function normalizeCurrencyCode(currency, fallback = "NGN") {
|
|
27
27
|
const normalized = typeof currency === "string" ? currency.trim().toUpperCase() : "";
|
|
28
28
|
const supportedCurrencies = Intl.supportedValuesOf?.("currency") ?? [];
|
|
29
29
|
return supportedCurrencies.includes(normalized) ? normalized : fallback;
|
|
@@ -53,19 +53,19 @@ function setActiveCurrency(currency) {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
function getActiveCurrency() {
|
|
56
|
-
if (typeof window === "undefined") return override ?? "
|
|
56
|
+
if (typeof window === "undefined") return override ?? "NGN";
|
|
57
57
|
let slug = null;
|
|
58
58
|
try {
|
|
59
59
|
slug = localStorage.getItem(WORKSPACE_KEY);
|
|
60
60
|
} catch {
|
|
61
|
-
return override ?? cachedCurrency ?? "
|
|
61
|
+
return override ?? cachedCurrency ?? "NGN";
|
|
62
62
|
}
|
|
63
63
|
if (slug !== cachedSlug) {
|
|
64
64
|
cachedSlug = slug;
|
|
65
65
|
cachedCurrency = readStoredCurrency();
|
|
66
66
|
override = null;
|
|
67
67
|
}
|
|
68
|
-
return override ?? cachedCurrency ?? "
|
|
68
|
+
return override ?? cachedCurrency ?? "NGN";
|
|
69
69
|
}
|
|
70
70
|
function formatCurrency(amount, currency) {
|
|
71
71
|
const code = currency || getActiveCurrency();
|
package/dist/utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/utils.ts"],"sourcesContent":["import { type ClassValue, clsx } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n\n/**\n * The value as a list, whatever actually arrived.\n *\n * `apiFetch<Thing[]>` is a promise of a type, not a guarantee of one: the\n * generic tells TypeScript what to expect and nothing checks it at runtime. A\n * 200 carrying an error object, a `{ data: [...] }` envelope from a changed\n * endpoint, or `null` all satisfy the compiler and then meet `.filter`.\n *\n * A `= []` default does not cover this — that only fires when the value is\n * `undefined`, and neither does `.catch(() => [])`, which only fires when the\n * promise rejects. This is for the case in between, where the request\n * succeeded and returned the wrong shape.\n *\n * Worth using anywhere the list drives render in the shell, where a throw\n * takes down every page rather than one screen.\n */\nexport function asList<T>(value: T[] | null | undefined | unknown): T[] {\n return Array.isArray(value) ? (value as T[]) : [];\n}\n\n// Currency symbol map – avoids Intl locale inconsistencies (en-US renders NGN\n// as \"NGN\" instead of \"₦\"). Keep in sync with dynamic-form.tsx CURRENCY_SYMBOLS.\nconst CURRENCY_SYMBOLS: Record<string, string> = {\n NGN: '₦',\n USD: '$',\n EUR: '€',\n GBP: '£',\n CAD: 'C$',\n AUD: 'A$',\n GHS: '₵',\n KES: 'KSh',\n ZAR: 'R',\n INR: '₹',\n JPY: '¥',\n CNY: '¥',\n BRL: 'R$',\n MXN: '$',\n AED: 'د.إ',\n};\n\nexport function normalizeCurrencyCode(\n currency: unknown,\n fallback = 'USD',\n): string {\n const normalized =\n typeof currency === 'string' ? currency.trim().toUpperCase() : '';\n const supportedCurrencies = Intl.supportedValuesOf?.('currency') ?? [];\n return supportedCurrencies.includes(normalized) ? normalized : fallback;\n}\n\n/**\n * The workspace's own currency, for the 250-odd places that format money\n * without naming one.\n *\n * `formatCurrency` defaulted to USD, so a Lagos merchant's ₦500,000 quote read\n * \"$500,000\" on every screen that did not pass a code — Orders, Payments,\n * Customers, invoice totals, the enquiry stats. The ones that DO pass a code\n * are passing a product's or an invoice's own currency, which answers a\n * different question: what this row is priced in, not what this business\n * trades in. Those still win.\n *\n * Read from the persisted workspace rather than set by a provider. Auth already\n * writes the whole tenant to `localStorage.tenant` every time one becomes\n * current — login, reload and `switchWorkspace` all land there — so the value\n * is available to plain functions with no hook and no context, which is what\n * `toAnswers` and the table cell formatters are. It also keeps this out of\n * `auth-context.tsx`, which sits exactly on its length ledger.\n *\n * The parse is cached against `currentWorkspace`, a one-key string read: the\n * cheap read happens per call, the JSON parse only when the workspace changes.\n */\nconst WORKSPACE_KEY = 'currentWorkspace';\nconst TENANT_KEY = 'tenant';\n\nlet cachedSlug: string | null | undefined;\nlet cachedCurrency: string | null = null;\n/** Set from a live tenant, so a fresh fetch beats what was last persisted. */\nlet override: string | null = null;\n\nfunction readStoredCurrency(): string | null {\n try {\n const raw = localStorage.getItem(TENANT_KEY);\n if (!raw) return null;\n const code = (JSON.parse(raw) as { settings?: { currency?: unknown } })\n ?.settings?.currency;\n return typeof code === 'string' && code ? code.toUpperCase() : null;\n } catch {\n // Unparseable, or storage blocked. Dollars is wrong for some merchants and\n // right for others; throwing is wrong for all of them.\n return null;\n }\n}\n\n/**\n * Point the formatter at a currency directly.\n *\n * `useCurrency` calls this when a live tenant differs from the stored one. An\n * empty value is ignored rather than resetting a correct currency to dollars —\n * workspaces load asynchronously and some have none set.\n */\nexport function setActiveCurrency(currency: string | undefined | null): void {\n if (!currency) return;\n override = currency.toUpperCase();\n try {\n cachedSlug = localStorage.getItem(WORKSPACE_KEY);\n } catch {\n cachedSlug = undefined;\n }\n}\n\nexport function getActiveCurrency(): string {\n if (typeof window === 'undefined') return override ?? 'USD';\n\n let slug: string | null = null;\n try {\n slug = localStorage.getItem(WORKSPACE_KEY);\n } catch {\n return override ?? cachedCurrency ?? 'USD';\n }\n\n if (slug !== cachedSlug) {\n cachedSlug = slug;\n cachedCurrency = readStoredCurrency();\n // The override belonged to the workspace that just went away.\n override = null;\n }\n\n return override ?? cachedCurrency ?? 'USD';\n}\n\nexport function formatCurrency(amount: number, currency?: string): string {\n const code = currency || getActiveCurrency();\n const symbol = CURRENCY_SYMBOLS[code] || code;\n const formatted = new Intl.NumberFormat('en-US', {\n minimumFractionDigits: 0,\n maximumFractionDigits: 0,\n }).format(amount);\n return `${symbol}${formatted}`;\n}\n\nexport function formatDate(\n date: string | Date,\n options?: Intl.DateTimeFormatOptions,\n): string {\n const defaultOptions: Intl.DateTimeFormatOptions = {\n year: 'numeric',\n month: 'short',\n day: 'numeric',\n ...options,\n };\n return new Intl.DateTimeFormat('en-NG', defaultOptions).format(\n new Date(date),\n );\n}\n\n/**\n * How long ago something happened: \"just now\", \"4m\", \"3h\", \"2d\", then a date.\n *\n * The counterpart to `formatRelativeTime`, which rounds to whole days and\n * leans forward (\"Tomorrow\", \"In 3 days\") because it was written for things\n * that are due. Anything that has already happened lands in its `Today` bucket\n * and stops being a time at all, which is no use in a feed where every row is\n * minutes old and the order is the whole point.\n *\n * Bare units, no \"ago\": in a list where every row is a past event, the word is\n * on every line and carries nothing.\n */\nexport function formatTimeAgo(date: string | Date): string {\n const then = new Date(date).getTime();\n if (Number.isNaN(then)) return '';\n const seconds = Math.max(0, Math.floor((Date.now() - then) / 1000));\n\n if (seconds < 45) return 'just now';\n if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;\n if (seconds < 86_400) return `${Math.floor(seconds / 3600)}h`;\n if (seconds < 604_800) return `${Math.floor(seconds / 86_400)}d`;\n return formatDate(date, { year: undefined, month: 'short', day: 'numeric' });\n}\n\nexport function formatRelativeTime(date: string | Date): string {\n const now = new Date();\n const target = new Date(date);\n const diffInDays = Math.ceil(\n (target.getTime() - now.getTime()) / (1000 * 60 * 60 * 24),\n );\n\n if (diffInDays < 0) {\n return `${Math.abs(diffInDays)} days ago`;\n } else if (diffInDays === 0) {\n return 'Today';\n } else if (diffInDays === 1) {\n return 'Tomorrow';\n } else if (diffInDays < 7) {\n return `In ${diffInDays} days`;\n } else if (diffInDays < 30) {\n const weeks = Math.ceil(diffInDays / 7);\n return `In ${weeks} week${weeks > 1 ? 's' : ''}`;\n } else {\n return formatDate(date);\n }\n}\n\nexport function capitalize(str: string): string {\n return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();\n}\n\nexport function formatBytes(bytes: number): string {\n if (bytes === 0) return '0 B';\n const units = ['B', 'KB', 'MB', 'GB', 'TB'];\n const i = Math.floor(Math.log(bytes) / Math.log(1024));\n if (i === 0) return `${bytes} B`;\n const value = bytes / Math.pow(1024, i);\n return `${parseFloat(value.toFixed(1))} ${units[i]}`;\n}\n\n/**\n * \"3d ago\". Lived in `reviews/_components` and was copied by hand into the\n * abandoned-cart page, so two surfaces rounded time differently. It is a date\n * helper, so it belongs beside the other date helpers.\n */\nexport function timeAgo(dateStr: string): string {\n const now = Date.now();\n const d = new Date(dateStr).getTime();\n const diff = now - d;\n const mins = Math.floor(diff / 60000);\n if (mins < 1) return 'just now';\n if (mins < 60) return `${mins}m ago`;\n const hrs = Math.floor(mins / 60);\n if (hrs < 24) return `${hrs}h ago`;\n const days = Math.floor(hrs / 24);\n if (days < 30) return `${days}d ago`;\n const months = Math.floor(days / 30);\n if (months < 12) return `${months}mo ago`;\n return `${Math.floor(months / 12)}y ago`;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/utils.ts"],"sourcesContent":["import { type ClassValue, clsx } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n\n/**\n * The value as a list, whatever actually arrived.\n *\n * `apiFetch<Thing[]>` is a promise of a type, not a guarantee of one: the\n * generic tells TypeScript what to expect and nothing checks it at runtime. A\n * 200 carrying an error object, a `{ data: [...] }` envelope from a changed\n * endpoint, or `null` all satisfy the compiler and then meet `.filter`.\n *\n * A `= []` default does not cover this — that only fires when the value is\n * `undefined`, and neither does `.catch(() => [])`, which only fires when the\n * promise rejects. This is for the case in between, where the request\n * succeeded and returned the wrong shape.\n *\n * Worth using anywhere the list drives render in the shell, where a throw\n * takes down every page rather than one screen.\n */\nexport function asList<T>(value: T[] | null | undefined | unknown): T[] {\n return Array.isArray(value) ? (value as T[]) : [];\n}\n\n// Currency symbol map – avoids Intl locale inconsistencies (en-US renders NGN\n// as \"NGN\" instead of \"₦\"). Keep in sync with dynamic-form.tsx CURRENCY_SYMBOLS.\nconst CURRENCY_SYMBOLS: Record<string, string> = {\n NGN: '₦',\n USD: '$',\n EUR: '€',\n GBP: '£',\n CAD: 'C$',\n AUD: 'A$',\n GHS: '₵',\n KES: 'KSh',\n ZAR: 'R',\n INR: '₹',\n JPY: '¥',\n CNY: '¥',\n BRL: 'R$',\n MXN: '$',\n AED: 'د.إ',\n};\n\nexport function normalizeCurrencyCode(\n currency: unknown,\n fallback = 'NGN',\n): string {\n const normalized =\n typeof currency === 'string' ? currency.trim().toUpperCase() : '';\n const supportedCurrencies = Intl.supportedValuesOf?.('currency') ?? [];\n return supportedCurrencies.includes(normalized) ? normalized : fallback;\n}\n\n/**\n * The workspace's own currency, for the 250-odd places that format money\n * without naming one.\n *\n * `formatCurrency` defaulted to USD, so a Lagos merchant's ₦500,000 quote read\n * \"$500,000\" on every screen that did not pass a code — Orders, Payments,\n * Customers, invoice totals, the enquiry stats. The ones that DO pass a code\n * are passing a product's or an invoice's own currency, which answers a\n * different question: what this row is priced in, not what this business\n * trades in. Those still win.\n *\n * Read from the persisted workspace rather than set by a provider. Auth already\n * writes the whole tenant to `localStorage.tenant` every time one becomes\n * current — login, reload and `switchWorkspace` all land there — so the value\n * is available to plain functions with no hook and no context, which is what\n * `toAnswers` and the table cell formatters are. It also keeps this out of\n * `auth-context.tsx`, which sits exactly on its length ledger.\n *\n * The parse is cached against `currentWorkspace`, a one-key string read: the\n * cheap read happens per call, the JSON parse only when the workspace changes.\n */\nconst WORKSPACE_KEY = 'currentWorkspace';\nconst TENANT_KEY = 'tenant';\n\nlet cachedSlug: string | null | undefined;\nlet cachedCurrency: string | null = null;\n/** Set from a live tenant, so a fresh fetch beats what was last persisted. */\nlet override: string | null = null;\n\nfunction readStoredCurrency(): string | null {\n try {\n const raw = localStorage.getItem(TENANT_KEY);\n if (!raw) return null;\n const code = (JSON.parse(raw) as { settings?: { currency?: unknown } })\n ?.settings?.currency;\n return typeof code === 'string' && code ? code.toUpperCase() : null;\n } catch {\n // Unparseable, or storage blocked. The default is naira; throwing is wrong\n // for every merchant.\n return null;\n }\n}\n\n/**\n * Point the formatter at a currency directly.\n *\n * `useCurrency` calls this when a live tenant differs from the stored one. An\n * empty value is ignored rather than resetting a correct currency to dollars —\n * workspaces load asynchronously and some have none set.\n */\nexport function setActiveCurrency(currency: string | undefined | null): void {\n if (!currency) return;\n override = currency.toUpperCase();\n try {\n cachedSlug = localStorage.getItem(WORKSPACE_KEY);\n } catch {\n cachedSlug = undefined;\n }\n}\n\nexport function getActiveCurrency(): string {\n if (typeof window === 'undefined') return override ?? 'NGN';\n\n let slug: string | null = null;\n try {\n slug = localStorage.getItem(WORKSPACE_KEY);\n } catch {\n return override ?? cachedCurrency ?? 'NGN';\n }\n\n if (slug !== cachedSlug) {\n cachedSlug = slug;\n cachedCurrency = readStoredCurrency();\n // The override belonged to the workspace that just went away.\n override = null;\n }\n\n return override ?? cachedCurrency ?? 'NGN';\n}\n\nexport function formatCurrency(amount: number, currency?: string): string {\n const code = currency || getActiveCurrency();\n const symbol = CURRENCY_SYMBOLS[code] || code;\n const formatted = new Intl.NumberFormat('en-US', {\n minimumFractionDigits: 0,\n maximumFractionDigits: 0,\n }).format(amount);\n return `${symbol}${formatted}`;\n}\n\nexport function formatDate(\n date: string | Date,\n options?: Intl.DateTimeFormatOptions,\n): string {\n const defaultOptions: Intl.DateTimeFormatOptions = {\n year: 'numeric',\n month: 'short',\n day: 'numeric',\n ...options,\n };\n return new Intl.DateTimeFormat('en-NG', defaultOptions).format(\n new Date(date),\n );\n}\n\n/**\n * How long ago something happened: \"just now\", \"4m\", \"3h\", \"2d\", then a date.\n *\n * The counterpart to `formatRelativeTime`, which rounds to whole days and\n * leans forward (\"Tomorrow\", \"In 3 days\") because it was written for things\n * that are due. Anything that has already happened lands in its `Today` bucket\n * and stops being a time at all, which is no use in a feed where every row is\n * minutes old and the order is the whole point.\n *\n * Bare units, no \"ago\": in a list where every row is a past event, the word is\n * on every line and carries nothing.\n */\nexport function formatTimeAgo(date: string | Date): string {\n const then = new Date(date).getTime();\n if (Number.isNaN(then)) return '';\n const seconds = Math.max(0, Math.floor((Date.now() - then) / 1000));\n\n if (seconds < 45) return 'just now';\n if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;\n if (seconds < 86_400) return `${Math.floor(seconds / 3600)}h`;\n if (seconds < 604_800) return `${Math.floor(seconds / 86_400)}d`;\n return formatDate(date, { year: undefined, month: 'short', day: 'numeric' });\n}\n\nexport function formatRelativeTime(date: string | Date): string {\n const now = new Date();\n const target = new Date(date);\n const diffInDays = Math.ceil(\n (target.getTime() - now.getTime()) / (1000 * 60 * 60 * 24),\n );\n\n if (diffInDays < 0) {\n return `${Math.abs(diffInDays)} days ago`;\n } else if (diffInDays === 0) {\n return 'Today';\n } else if (diffInDays === 1) {\n return 'Tomorrow';\n } else if (diffInDays < 7) {\n return `In ${diffInDays} days`;\n } else if (diffInDays < 30) {\n const weeks = Math.ceil(diffInDays / 7);\n return `In ${weeks} week${weeks > 1 ? 's' : ''}`;\n } else {\n return formatDate(date);\n }\n}\n\nexport function capitalize(str: string): string {\n return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();\n}\n\nexport function formatBytes(bytes: number): string {\n if (bytes === 0) return '0 B';\n const units = ['B', 'KB', 'MB', 'GB', 'TB'];\n const i = Math.floor(Math.log(bytes) / Math.log(1024));\n if (i === 0) return `${bytes} B`;\n const value = bytes / Math.pow(1024, i);\n return `${parseFloat(value.toFixed(1))} ${units[i]}`;\n}\n\n/**\n * \"3d ago\". Lived in `reviews/_components` and was copied by hand into the\n * abandoned-cart page, so two surfaces rounded time differently. It is a date\n * helper, so it belongs beside the other date helpers.\n */\nexport function timeAgo(dateStr: string): string {\n const now = Date.now();\n const d = new Date(dateStr).getTime();\n const diff = now - d;\n const mins = Math.floor(diff / 60000);\n if (mins < 1) return 'just now';\n if (mins < 60) return `${mins}m ago`;\n const hrs = Math.floor(mins / 60);\n if (hrs < 24) return `${hrs}h ago`;\n const days = Math.floor(hrs / 24);\n if (days < 30) return `${days}d ago`;\n const months = Math.floor(days / 30);\n if (months < 12) return `${months}mo ago`;\n return `${Math.floor(months / 12)}y ago`;\n}\n"],"mappings":"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","names":[]}
|
package/dist/wallet-format.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/wallet-format.ts"],"sourcesContent":["/**\n * Wallet amounts arrive as minor units in strings (\"150000\" = ₦1,500.00).\n * Divide by the currency's own number of decimals, in BigInt so large\n * balances stay exact.\n */\nexport function formatMinor(minor: string, currency: string): string {\n const format = new Intl.NumberFormat(undefined, {\n style: 'currency',\n currency,\n });\n const digits = format.resolvedOptions().maximumFractionDigits ?? 2;\n\n let value: bigint;\n\n try {\n value = BigInt(minor);\n } catch {\n return `${minor} ${currency}`;\n }\n\n const negative = value < BigInt(0);\n const abs = negative ? -value : value;\n const scale = BigInt(10) ** BigInt(digits);\n const whole = abs / scale;\n const fraction = (abs % scale).toString().padStart(digits, '0');\n const parts = format.formatToParts(0);\n const decimal = parts.find((p) => p.type === 'decimal')?.value ?? '.';\n const grouped = format\n .formatToParts(Number(whole) <= Number.MAX_SAFE_INTEGER ? Number(whole) : 0)\n .map((p) => {\n if (p.type === 'integer' && whole > BigInt(Number.MAX_SAFE_INTEGER)) {\n return whole.toString();\n }\n if (p.type === 'fraction') return fraction;\n if (p.type === 'decimal') return digits ? decimal : '';\n return p.value;\n })\n .join('');\n\n return negative ? `-${grouped}` : grouped;\n}\n\n/** Statuses come back in either case (\"ACTIVE\" from money, \"active\" elsewhere). */\nexport const sameStatus = (status: string) => status.toLowerCase();\n"],"mappings":"AAKO,SAAS,YAAY,OAAe,UAA0B;AACnE,QAAM,SAAS,IAAI,KAAK,aAAa,QAAW;AAAA,IAC9C,OAAO;AAAA,IACP;AAAA,EACF,CAAC;AACD,QAAM,SAAS,OAAO,gBAAgB,EAAE,yBAAyB;AAEjE,MAAI;AAEJ,MAAI;AACF,YAAQ,OAAO,KAAK;AAAA,EACtB,QAAQ;AACN,WAAO,GAAG,KAAK,IAAI,QAAQ;AAAA,EAC7B;AAEA,QAAM,WAAW,QAAQ,OAAO,CAAC;AACjC,QAAM,MAAM,WAAW,CAAC,QAAQ;AAChC,QAAM,QAAQ,OAAO,EAAE,KAAK,OAAO,MAAM;AACzC,QAAM,QAAQ,MAAM;AACpB,QAAM,YAAY,MAAM,OAAO,SAAS,EAAE,SAAS,QAAQ,GAAG;AAC9D,QAAM,QAAQ,OAAO,cAAc,CAAC;AACpC,QAAM,UAAU,MAAM,KAAK,CAAC,MAAM,EAAE,SAAS,SAAS,GAAG,SAAS;AAClE,QAAM,UAAU,OACb,cAAc,OAAO,KAAK,KAAK,OAAO,mBAAmB,OAAO,KAAK,IAAI,CAAC,EAC1E,IAAI,CAAC,MAAM;AACV,QAAI,EAAE,SAAS,aAAa,QAAQ,OAAO,OAAO,gBAAgB,GAAG;AACnE,aAAO,MAAM,SAAS;AAAA,IACxB;AACA,QAAI,EAAE,SAAS,WAAY,QAAO;AAClC,QAAI,EAAE,SAAS,UAAW,QAAO,SAAS,UAAU;AACpD,WAAO,EAAE;AAAA,EACX,CAAC,EACA,KAAK,EAAE;AAEV,SAAO,WAAW,IAAI,OAAO,KAAK;AACpC;AAGO,MAAM,aAAa,CAAC,WAAmB,OAAO,YAAY;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/wallet-format.ts"],"sourcesContent":["/**\n * Wallet amounts arrive as minor units in strings (\"150000\" = ₦1,500.00).\n * Divide by the currency's own number of decimals, in BigInt so large\n * balances stay exact.\n */\nexport function formatMinor(minor: string, currency: string): string {\n const format = new Intl.NumberFormat(undefined, {\n style: 'currency',\n currencyDisplay: 'narrowSymbol',\n currency,\n });\n const digits = format.resolvedOptions().maximumFractionDigits ?? 2;\n\n let value: bigint;\n\n try {\n value = BigInt(minor);\n } catch {\n return `${minor} ${currency}`;\n }\n\n const negative = value < BigInt(0);\n const abs = negative ? -value : value;\n const scale = BigInt(10) ** BigInt(digits);\n const whole = abs / scale;\n const fraction = (abs % scale).toString().padStart(digits, '0');\n const parts = format.formatToParts(0);\n const decimal = parts.find((p) => p.type === 'decimal')?.value ?? '.';\n const grouped = format\n .formatToParts(Number(whole) <= Number.MAX_SAFE_INTEGER ? Number(whole) : 0)\n .map((p) => {\n if (p.type === 'integer' && whole > BigInt(Number.MAX_SAFE_INTEGER)) {\n return whole.toString();\n }\n if (p.type === 'fraction') return fraction;\n if (p.type === 'decimal') return digits ? decimal : '';\n return p.value;\n })\n .join('');\n\n return negative ? `-${grouped}` : grouped;\n}\n\n/** Statuses come back in either case (\"ACTIVE\" from money, \"active\" elsewhere). */\nexport const sameStatus = (status: string) => status.toLowerCase();\n"],"mappings":"AAKO,SAAS,YAAY,OAAe,UAA0B;AACnE,QAAM,SAAS,IAAI,KAAK,aAAa,QAAW;AAAA,IAC9C,OAAO;AAAA,IACP,iBAAiB;AAAA,IACjB;AAAA,EACF,CAAC;AACD,QAAM,SAAS,OAAO,gBAAgB,EAAE,yBAAyB;AAEjE,MAAI;AAEJ,MAAI;AACF,YAAQ,OAAO,KAAK;AAAA,EACtB,QAAQ;AACN,WAAO,GAAG,KAAK,IAAI,QAAQ;AAAA,EAC7B;AAEA,QAAM,WAAW,QAAQ,OAAO,CAAC;AACjC,QAAM,MAAM,WAAW,CAAC,QAAQ;AAChC,QAAM,QAAQ,OAAO,EAAE,KAAK,OAAO,MAAM;AACzC,QAAM,QAAQ,MAAM;AACpB,QAAM,YAAY,MAAM,OAAO,SAAS,EAAE,SAAS,QAAQ,GAAG;AAC9D,QAAM,QAAQ,OAAO,cAAc,CAAC;AACpC,QAAM,UAAU,MAAM,KAAK,CAAC,MAAM,EAAE,SAAS,SAAS,GAAG,SAAS;AAClE,QAAM,UAAU,OACb,cAAc,OAAO,KAAK,KAAK,OAAO,mBAAmB,OAAO,KAAK,IAAI,CAAC,EAC1E,IAAI,CAAC,MAAM;AACV,QAAI,EAAE,SAAS,aAAa,QAAQ,OAAO,OAAO,gBAAgB,GAAG;AACnE,aAAO,MAAM,SAAS;AAAA,IACxB;AACA,QAAI,EAAE,SAAS,WAAY,QAAO;AAClC,QAAI,EAAE,SAAS,UAAW,QAAO,SAAS,UAAU;AACpD,WAAO,EAAE;AAAA,EACX,CAAC,EACA,KAAK,EAAE;AAEV,SAAO,WAAW,IAAI,OAAO,KAAK;AACpC;AAGO,MAAM,aAAa,CAAC,WAAmB,OAAO,YAAY;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@withpotter/lib",
|
|
3
|
-
"version": "0.13.0-next.
|
|
3
|
+
"version": "0.13.0-next.85",
|
|
4
4
|
"description": "Box dashboard foundation: API client, contexts, types, intent config, and utilities.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"typescript": "^5"
|
|
52
52
|
},
|
|
53
53
|
"publishConfig": {
|
|
54
|
-
"registry": "https://registry.npmjs.org
|
|
54
|
+
"registry": "https://registry.npmjs.org",
|
|
55
55
|
"access": "restricted"
|
|
56
56
|
}
|
|
57
57
|
}
|