@umituz/web-design-system 6.6.0 → 6.8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/web-design-system",
3
- "version": "6.6.0",
3
+ "version": "6.8.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Web Design System - Tailwind v4 authored, precompiled CSS shipped. Atomic Design components (Atoms, Molecules, Organisms, Templates) for React applications.",
@@ -20,6 +20,7 @@
20
20
  "./hooks": "./src/presentation/hooks/index.ts",
21
21
  "./molecules": "./src/presentation/molecules/index.ts",
22
22
  "./organisms": "./src/presentation/organisms/index.ts",
23
+ "./page-kit": "./src/presentation/page-kit/index.ts",
23
24
  "./package.json": "./package.json",
24
25
  "./source.css": "./src/styles/source.css",
25
26
  "./styles.css": "./dist/styles.css",
@@ -0,0 +1,88 @@
1
+
2
+ import { describe, expect, it } from 'vitest';
3
+ import {
4
+ formatCurrency,
5
+ formatCurrencyCompact,
6
+ CURRENCY_LOCALES,
7
+ } from '../infrastructure/utils';
8
+
9
+ describe('formatCurrency', () => {
10
+ it('formats major-unit amounts with currency fraction digits', () => {
11
+ expect(formatCurrency(1234.5, 'USD')).toBe('$1,234.50');
12
+ expect(formatCurrency(1000, 'JPY')).toBe('¥1,000');
13
+ expect(formatCurrency(1.234, 'KWD')).toContain('1.234');
14
+ });
15
+
16
+ it('maps currencies to their canonical locale', () => {
17
+ expect(formatCurrency(1234.5, 'TRY')).toContain('1.234,50');
18
+ expect(formatCurrency(1234.5, 'EUR')).toContain('1.234,50');
19
+ expect(formatCurrency(1234.5, 'GBP')).toBe('£1,234.50');
20
+ });
21
+
22
+ it('honors an explicit locale over the currency map', () => {
23
+ expect(formatCurrency(1234.5, 'USD', { locale: 'de-DE' })).toContain('1.234,50');
24
+ });
25
+
26
+ it('drops cents on demand', () => {
27
+ expect(formatCurrency(1234.56, 'USD', { cents: false })).toBe('$1,235');
28
+ });
29
+
30
+ it('applies explicit decimals', () => {
31
+ expect(formatCurrency(1234.5, 'USD', { decimals: 0 })).toBe('$1,235');
32
+ expect(formatCurrency(1234.5, 'USD', { decimals: 2 })).toBe('$1,234.50');
33
+ });
34
+
35
+ it('applies the smart-fraction heuristic', () => {
36
+ expect(formatCurrency(1500, 'USD', { smartFractions: true })).toBe('$1,500');
37
+ expect(formatCurrency(12.34, 'USD', { smartFractions: true })).toBe('$12.34');
38
+ });
39
+
40
+ it('respects currency digits when smart fractions keep cents', () => {
41
+ expect(formatCurrency(12.5, 'JPY', { smartFractions: true })).toBe('¥13');
42
+ });
43
+
44
+ it('compacts large amounts', () => {
45
+ expect(formatCurrency(1_500_000, 'USD', { compact: true })).toBe('$1.5M');
46
+ });
47
+
48
+ it('keeps explicit fraction digits under compact', () => {
49
+ expect(formatCurrency(1_500_000, 'USD', { compact: true, maximumFractionDigits: 2 })).toBe(
50
+ '$1.50M',
51
+ );
52
+ });
53
+
54
+ it('signs and appends the code on demand', () => {
55
+ expect(formatCurrency(12.5, 'USD', { signed: true })).toBe('+$12.50');
56
+ expect(formatCurrency(12.5, 'USD', { showCode: true })).toBe('$12.50 USD');
57
+ });
58
+
59
+ it('passes currencyDisplay through', () => {
60
+ expect(formatCurrency(12.5, 'USD', { currencyDisplay: 'code' })).toBe('USD 12.50');
61
+ });
62
+
63
+ it('formats exact decimal strings without float artifacts', () => {
64
+ expect(formatCurrency('1234.565', 'USD', { maximumFractionDigits: 2 })).toBe('$1,234.57');
65
+ expect(formatCurrency('0.1', 'USD')).toBe('$0.10');
66
+ });
67
+
68
+ it('caches formatters without cross-contaminating options', () => {
69
+ expect(formatCurrency(5, 'USD')).toBe('$5.00');
70
+ expect(formatCurrency(5, 'USD', { cents: false })).toBe('$5');
71
+ expect(formatCurrency(5, 'USD')).toBe('$5.00');
72
+ });
73
+ });
74
+
75
+ describe('formatCurrencyCompact', () => {
76
+ it('compacts with the full option surface', () => {
77
+ expect(formatCurrencyCompact(1_250_000, 'USD')).toBe('$1.3M');
78
+ expect(formatCurrencyCompact(95_000, 'EUR', { maximumFractionDigits: 2 })).toContain('95');
79
+ });
80
+ });
81
+
82
+ describe('CURRENCY_LOCALES', () => {
83
+ it('covers the fleet currency set', () => {
84
+ expect(CURRENCY_LOCALES.USD).toBe('en-US');
85
+ expect(CURRENCY_LOCALES.TRY).toBe('tr-TR');
86
+ expect(CURRENCY_LOCALES.AED).toBe('en-AE');
87
+ });
88
+ });
@@ -0,0 +1,42 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import * as pageKit from '../presentation/page-kit';
3
+
4
+ const COMPONENTS = [
5
+ 'PageHeader',
6
+ 'SectionHeader',
7
+ 'FilterBar',
8
+ 'FilterSelect',
9
+ 'FilterPanel',
10
+ 'FilterSection',
11
+ 'CheckFilterList',
12
+ 'SingleSelectFilterList',
13
+ 'TabNav',
14
+ 'NextSteps',
15
+ ] as const;
16
+
17
+ describe('page-kit barrel', () => {
18
+ it('exports every page scaffolding component as a renderable', () => {
19
+ for (const name of COMPONENTS) {
20
+ expect(['function', 'object'], `pageKit.${name}`).toContain(typeof pageKit[name]);
21
+ }
22
+ });
23
+
24
+ it('exports the full surface with no extra values beyond the documented kit', () => {
25
+ const record = pageKit as unknown as Record<string, unknown>;
26
+ const valueNames = Object.keys(record).filter((k) => record[k] !== undefined);
27
+ expect(valueNames.sort()).toEqual([...COMPONENTS].sort());
28
+ });
29
+
30
+ it('re-exports the same bindings as their layer barrels', async () => {
31
+ const templates = await import('../presentation/templates');
32
+ const molecules = await import('../presentation/molecules');
33
+ const organisms = await import('../presentation/organisms');
34
+ expect(pageKit.PageHeader).toBe(templates.PageHeader);
35
+ expect(pageKit.SectionHeader).toBe(templates.SectionHeader);
36
+ expect(pageKit.FilterSelect).toBe(molecules.FilterSelect);
37
+ expect(pageKit.TabNav).toBe(molecules.TabNav);
38
+ expect(pageKit.FilterBar).toBe(organisms.FilterBar);
39
+ expect(pageKit.FilterPanel).toBe(organisms.FilterPanel);
40
+ expect(pageKit.NextSteps).toBe(organisms.NextSteps);
41
+ });
42
+ });
@@ -47,5 +47,8 @@ export {
47
47
  lineTotals,
48
48
  sumTotals,
49
49
  sumMinor,
50
+ formatCurrency,
51
+ formatCurrencyCompact,
52
+ CURRENCY_LOCALES,
50
53
  } from './money.utils';
51
- export type { LineTotals, TotalsInputLine } from './money.utils';
54
+ export type { LineTotals, TotalsInputLine, MoneyValue, FormatCurrencyOptions } from './money.utils';
@@ -99,3 +99,96 @@ export function sumTotals(lines: readonly TotalsInputLine[]): LineTotals {
99
99
  export function sumMinor(amounts: readonly number[]): number {
100
100
  return amounts.reduce((acc, value) => acc + value, 0);
101
101
  }
102
+
103
+ export type MoneyValue = number | string;
104
+
105
+ export const CURRENCY_LOCALES: Readonly<Record<string, string>> = {
106
+ USD: 'en-US',
107
+ EUR: 'de-DE',
108
+ GBP: 'en-GB',
109
+ TRY: 'tr-TR',
110
+ AED: 'en-AE',
111
+ };
112
+
113
+ export interface FormatCurrencyOptions {
114
+ locale?: string;
115
+ compact?: boolean;
116
+ signed?: boolean;
117
+ showCode?: boolean;
118
+ cents?: boolean;
119
+ decimals?: 0 | 2;
120
+ smartFractions?: boolean;
121
+ minimumFractionDigits?: number;
122
+ maximumFractionDigits?: number;
123
+ currencyDisplay?: Intl.NumberFormatOptions['currencyDisplay'];
124
+ }
125
+
126
+ interface FractionRange {
127
+ min: number;
128
+ max: number;
129
+ }
130
+
131
+ function resolveFractionRange(amount: MoneyValue, currency: string, options: FormatCurrencyOptions): FractionRange {
132
+ const digits = currencyFractionDigits(currency);
133
+ let min = digits;
134
+ let max = digits;
135
+ if (options.smartFractions) {
136
+ const numeric = typeof amount === 'number' ? amount : Number.parseFloat(amount);
137
+ const dropFractions = Math.abs(numeric) >= 1000 || Number.isInteger(numeric);
138
+ min = dropFractions ? 0 : digits;
139
+ max = dropFractions ? 0 : digits;
140
+ }
141
+ if (options.decimals !== undefined) {
142
+ min = options.decimals;
143
+ max = options.decimals;
144
+ }
145
+ if (options.cents === false) {
146
+ min = 0;
147
+ max = 0;
148
+ }
149
+ if (options.minimumFractionDigits !== undefined) min = options.minimumFractionDigits;
150
+ if (options.maximumFractionDigits !== undefined) max = options.maximumFractionDigits;
151
+ if (options.compact && options.maximumFractionDigits === undefined) max = Math.min(max, 1);
152
+ if (min > max) min = max;
153
+ return { min, max };
154
+ }
155
+
156
+ const currencyFormatterCache = new Map<string, Intl.NumberFormat>();
157
+
158
+ export function formatCurrency(
159
+ amount: MoneyValue,
160
+ currency: string,
161
+ options: FormatCurrencyOptions = {},
162
+ ): string {
163
+ const upper = currency.toUpperCase();
164
+ const locale = options.locale ?? CURRENCY_LOCALES[upper] ?? 'en-US';
165
+ const { min, max } = resolveFractionRange(amount, upper, options);
166
+ const key = `${locale}|${upper}|${options.compact ? 'compact' : 'standard'}|${options.signed ? 'signed' : 'auto'}|${options.currencyDisplay ?? 'default'}|${min}|${max}`;
167
+ let formatter = currencyFormatterCache.get(key);
168
+ if (!formatter) {
169
+ formatter = new Intl.NumberFormat(locale, {
170
+ style: 'currency',
171
+ currency: upper,
172
+ notation: options.compact ? 'compact' : 'standard',
173
+ minimumFractionDigits: min,
174
+ maximumFractionDigits: max,
175
+ signDisplay: options.signed ? 'always' : 'auto',
176
+ ...(options.currencyDisplay ? { currencyDisplay: options.currencyDisplay } : {}),
177
+ });
178
+ currencyFormatterCache.set(key, formatter);
179
+ }
180
+ const value =
181
+ typeof amount === 'number'
182
+ ? amount
183
+ : (amount as unknown as Parameters<Intl.NumberFormat['format']>[0]);
184
+ const formatted = formatter.format(value);
185
+ return options.showCode ? `${formatted} ${upper}` : formatted;
186
+ }
187
+
188
+ export function formatCurrencyCompact(
189
+ amount: MoneyValue,
190
+ currency: string,
191
+ options: Omit<FormatCurrencyOptions, 'compact'> = {},
192
+ ): string {
193
+ return formatCurrency(amount, currency, { ...options, compact: true });
194
+ }
@@ -0,0 +1,26 @@
1
+ export { FilterBar } from '../organisms/FilterBar';
2
+ export type { FilterBarProps, FilterBarSearch } from '../organisms/FilterBar';
3
+
4
+ export { FilterSelect } from '../molecules/FilterSelect';
5
+ export type { FilterSelectProps, FilterSelectOption } from '../molecules/FilterSelect';
6
+
7
+ export { PageHeader } from '../templates/PageHeader';
8
+ export type { PageHeaderProps, PageHeaderCrumb, TextAlign, HeaderSize } from '../templates/PageHeader';
9
+
10
+ export { SectionHeader } from '../templates/SectionHeader';
11
+ export type { SectionHeaderProps } from '../templates/SectionHeader';
12
+
13
+ export { TabNav } from '../molecules/TabNav';
14
+ export type { TabNavItem, TabNavProps } from '../molecules/TabNav';
15
+
16
+ export { FilterPanel, FilterSection, CheckFilterList, SingleSelectFilterList } from '../organisms/FilterPanel';
17
+ export type {
18
+ FilterPanelProps,
19
+ FilterSectionProps,
20
+ FilterListOption,
21
+ CheckFilterListProps,
22
+ SingleSelectFilterListProps,
23
+ } from '../organisms/FilterPanel';
24
+
25
+ export { NextSteps } from '../organisms/NextSteps';
26
+ export type { NextStepsProps, NextStepLink } from '../organisms/NextSteps';