@isi-ui7/bos7-shared 0.2.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.
Files changed (108) hide show
  1. package/dist/api.d.ts +4 -0
  2. package/dist/auth7/client.d.ts +26 -0
  3. package/dist/auth7/delegated-proxy.d.ts +33 -0
  4. package/dist/auth7/hooks/useAuth.d.ts +10 -0
  5. package/dist/auth7/hooks/useSession.d.ts +12 -0
  6. package/dist/auth7/index.d.ts +12 -0
  7. package/dist/auth7/provider.d.ts +13 -0
  8. package/dist/auth7/scope-guard.d.ts +5 -0
  9. package/dist/auth7/service-account.d.ts +2 -0
  10. package/dist/auth7/token-exchange.d.ts +11 -0
  11. package/dist/auth7/types.d.ts +63 -0
  12. package/dist/backend.d.ts +20 -0
  13. package/dist/crud-components.d.ts +47 -0
  14. package/dist/crud-hooks.d.ts +15 -0
  15. package/dist/crud-types.d.ts +86 -0
  16. package/dist/data-table/cursor-adapter.d.ts +74 -0
  17. package/dist/event-bus/index.d.ts +20 -0
  18. package/dist/event-bus/useEventBus.d.ts +21 -0
  19. package/dist/event-bus.d.ts +1 -0
  20. package/dist/export.d.ts +7 -0
  21. package/dist/form-layout.d.ts +39 -0
  22. package/dist/form-numeric.d.ts +35 -0
  23. package/dist/form-renderer-utils.d.ts +10 -0
  24. package/dist/form-renderer.d.ts +18 -0
  25. package/dist/form-rules.d.ts +12 -0
  26. package/dist/form-types.d.ts +130 -0
  27. package/dist/html2canvas.esm-d2sM-0Wm.js +4870 -0
  28. package/dist/i18n.d.ts +35 -0
  29. package/dist/index.d.ts +18 -0
  30. package/dist/index.es-Bylk5fh-.js +5649 -0
  31. package/dist/index.js +20779 -0
  32. package/dist/jspdf.es.min-Dzm5j8wC.js +9977 -0
  33. package/dist/jspdf.plugin.autotable-DcntYaes.js +1078 -0
  34. package/dist/modal-layout.d.ts +45 -0
  35. package/dist/nats/client.d.ts +54 -0
  36. package/dist/notif7.d.ts +21 -0
  37. package/dist/notification/routing.d.ts +4 -0
  38. package/dist/notifications-bff.d.ts +22 -0
  39. package/dist/observability/otel-init.d.ts +1 -0
  40. package/dist/page-layout.d.ts +24 -0
  41. package/dist/purify.es-CiEWEeUM.js +605 -0
  42. package/dist/shell/app-shell-layout.d.ts +70 -0
  43. package/dist/shell/branch.d.ts +6 -0
  44. package/dist/shell/interaction.d.ts +2 -0
  45. package/dist/shell/provider.d.ts +15 -0
  46. package/dist/style-contract.d.ts +72 -0
  47. package/dist/types/core.d.ts +39 -0
  48. package/dist/types/notification.d.ts +30 -0
  49. package/dist/workflow/api-client.d.ts +2 -0
  50. package/dist/workflow/backend.d.ts +9 -0
  51. package/dist/workflow/crud-client.d.ts +3 -0
  52. package/dist/workflow/notif7.d.ts +6 -0
  53. package/dist/workflow/starter.d.ts +44 -0
  54. package/dist/workflow/use-crud-form.d.ts +27 -0
  55. package/dist/workflow/use-workflow-tracker.d.ts +40 -0
  56. package/package.json +92 -0
  57. package/src/api.ts +40 -0
  58. package/src/auth7/client.ts +248 -0
  59. package/src/auth7/delegated-proxy.ts +80 -0
  60. package/src/auth7/hooks/useAuth.ts +21 -0
  61. package/src/auth7/hooks/useSession.ts +75 -0
  62. package/src/auth7/index.ts +14 -0
  63. package/src/auth7/provider.tsx +256 -0
  64. package/src/auth7/scope-guard.ts +54 -0
  65. package/src/auth7/service-account.ts +71 -0
  66. package/src/auth7/token-exchange.ts +125 -0
  67. package/src/auth7/types.ts +72 -0
  68. package/src/backend.ts +96 -0
  69. package/src/crud-components.tsx +580 -0
  70. package/src/crud-hooks.test.ts +32 -0
  71. package/src/crud-hooks.ts +31 -0
  72. package/src/crud-types.ts +64 -0
  73. package/src/data-table/cursor-adapter.ts +239 -0
  74. package/src/event-bus/index.ts +90 -0
  75. package/src/event-bus/useEventBus.ts +93 -0
  76. package/src/event-bus.ts +1 -0
  77. package/src/export.ts +62 -0
  78. package/src/form-contract.css +52 -0
  79. package/src/form-layout.tsx +90 -0
  80. package/src/form-numeric.ts +173 -0
  81. package/src/form-renderer-utils.ts +139 -0
  82. package/src/form-renderer.tsx +624 -0
  83. package/src/form-rules.ts +54 -0
  84. package/src/form-types.ts +126 -0
  85. package/src/i18n.tsx +92 -0
  86. package/src/index.ts +20 -0
  87. package/src/modal-layout.tsx +60 -0
  88. package/src/nats/client.ts +130 -0
  89. package/src/notif7.ts +76 -0
  90. package/src/notification/routing.ts +64 -0
  91. package/src/notifications-bff.ts +273 -0
  92. package/src/observability/otel-init.ts +41 -0
  93. package/src/page-layout.tsx +69 -0
  94. package/src/shell/app-shell-layout.tsx +494 -0
  95. package/src/shell/branch.ts +48 -0
  96. package/src/shell/interaction.ts +17 -0
  97. package/src/shell/provider.tsx +34 -0
  98. package/src/style-contract.test.ts +59 -0
  99. package/src/style-contract.ts +132 -0
  100. package/src/types/core.ts +46 -0
  101. package/src/types/notification.ts +35 -0
  102. package/src/workflow/api-client.ts +7 -0
  103. package/src/workflow/backend.ts +53 -0
  104. package/src/workflow/crud-client.ts +43 -0
  105. package/src/workflow/notif7.ts +28 -0
  106. package/src/workflow/starter.ts +153 -0
  107. package/src/workflow/use-crud-form.ts +176 -0
  108. package/src/workflow/use-workflow-tracker.ts +109 -0
@@ -0,0 +1,173 @@
1
+ export type DecimalParseResult =
2
+ | { ok: true; value: number; normalized: string; scale: number }
3
+ | { ok: false; reason: "invalid" | "too-many-decimals" };
4
+
5
+ export type DecimalSanitizeOptions = {
6
+ maxFractionDigits?: number;
7
+ allowNegative?: boolean;
8
+ };
9
+
10
+ export type CurrencyPrecisionConfig = {
11
+ precisionByCurrency: Record<string, number>;
12
+ defaultPrecision?: number;
13
+ };
14
+
15
+ export type PhoneRule = {
16
+ minDigits: number;
17
+ maxDigits: number;
18
+ allowedPrefixes?: string[];
19
+ };
20
+
21
+ function assertFiniteNonNegativeInteger(value: number, label: string): void {
22
+ if (!Number.isInteger(value) || value < 0) {
23
+ throw new Error(`Invalid ${label}: ${String(value)}`);
24
+ }
25
+ }
26
+
27
+ function extractDecimalParts(raw: string, allowNegative: boolean) {
28
+ const trimmed = raw.trim().replace(/\s+/g, "").replace(/-/g, "");
29
+ if (!trimmed) return { sign: "" as const, integer: "", fraction: "", hasDecimalPoint: false };
30
+
31
+ let sign: "" | "-" = "";
32
+ let body = trimmed;
33
+ if (raw.trim().startsWith("+")) {
34
+ body = body.slice(0);
35
+ } else if (raw.trim().startsWith("-")) {
36
+ if (!allowNegative) return { sign: "" as const, integer: "", fraction: "", hasDecimalPoint: false, invalid: true };
37
+ sign = "-";
38
+ }
39
+
40
+ body = body.replace(/,/g, ".");
41
+ const decimalMatch = body.match(/^[0-9]*\.?[0-9]*$/);
42
+ if (!decimalMatch) return { sign: "" as const, integer: "", fraction: "", hasDecimalPoint: false, invalid: true };
43
+
44
+ const hasDecimalPoint = body.includes(".");
45
+ const [integerRaw = "", fractionRaw = ""] = body.split(".", 2);
46
+
47
+ return {
48
+ sign,
49
+ integer: integerRaw.replace(/\D/g, ""),
50
+ fraction: fractionRaw.replace(/\D/g, ""),
51
+ hasDecimalPoint,
52
+ };
53
+ }
54
+
55
+ export function sanitizeDecimalInput(raw: string, options: DecimalSanitizeOptions = {}): string {
56
+ const parts = extractDecimalParts(raw, Boolean(options.allowNegative));
57
+ if ("invalid" in parts) return "";
58
+
59
+ const { sign, hasDecimalPoint } = parts;
60
+ let { integer, fraction } = parts;
61
+ if (!integer && !fraction) return "";
62
+
63
+ if (!integer) integer = "0";
64
+ if (typeof options.maxFractionDigits === "number") {
65
+ fraction = fraction.slice(0, options.maxFractionDigits);
66
+ }
67
+
68
+ if (fraction) return `${sign}${integer}.${fraction}`;
69
+ return hasDecimalPoint ? `${sign}${integer}.` : `${sign}${integer}`;
70
+ }
71
+
72
+ export function parseDecimalStrict(raw: string, options: DecimalSanitizeOptions = {}): DecimalParseResult {
73
+ const parts = extractDecimalParts(raw, Boolean(options.allowNegative));
74
+ if ("invalid" in parts) return { ok: false, reason: "invalid" };
75
+
76
+ const { integer, fraction, sign, hasDecimalPoint } = parts;
77
+ if (!integer && !fraction) return { ok: true, value: 0, normalized: "", scale: 0 };
78
+
79
+ const scale = fraction.length;
80
+ if (typeof options.maxFractionDigits === "number" && scale > options.maxFractionDigits) {
81
+ return { ok: false, reason: "too-many-decimals" };
82
+ }
83
+
84
+ const normalized = `${sign}${integer || "0"}${scale > 0 || hasDecimalPoint ? `.${fraction}` : ""}`;
85
+ const value = Number(normalized);
86
+ if (!Number.isFinite(value)) return { ok: false, reason: "invalid" };
87
+ return { ok: true, value, normalized, scale };
88
+ }
89
+
90
+ export function getCurrencyPrecision(currencyCode: string, config: CurrencyPrecisionConfig): number {
91
+ const explicit = config.precisionByCurrency[currencyCode];
92
+ if (typeof explicit === "number") {
93
+ assertFiniteNonNegativeInteger(explicit, `precision for ${currencyCode}`);
94
+ return explicit;
95
+ }
96
+ if (config.defaultPrecision === undefined) {
97
+ throw new Error(`Missing default currency precision for ${currencyCode}`);
98
+ }
99
+ assertFiniteNonNegativeInteger(config.defaultPrecision, "default currency precision");
100
+ return config.defaultPrecision;
101
+ }
102
+
103
+ export function formatCurrencyAmount(
104
+ value: number,
105
+ currencyCode: string,
106
+ config: CurrencyPrecisionConfig,
107
+ locale?: "id" | "en",
108
+ ): string {
109
+ const digits = getCurrencyPrecision(currencyCode, config);
110
+ const nloc = locale === "en" ? "en-US" : "id-ID";
111
+ try {
112
+ return new Intl.NumberFormat(nloc, {
113
+ style: "currency",
114
+ currency: currencyCode,
115
+ minimumFractionDigits: digits,
116
+ maximumFractionDigits: digits,
117
+ }).format(value);
118
+ } catch {
119
+ const formatted = new Intl.NumberFormat(nloc, {
120
+ minimumFractionDigits: digits,
121
+ maximumFractionDigits: digits,
122
+ }).format(value);
123
+ return `${currencyCode} ${formatted}`.trim();
124
+ }
125
+ }
126
+
127
+ export function normalizePhoneInput(raw: string): string {
128
+ const trimmed = raw.trim();
129
+ if (!trimmed) return "";
130
+ const digits = trimmed.replace(/\D/g, "");
131
+ if (!digits) return trimmed.startsWith("+") ? "+" : "";
132
+ return trimmed.startsWith("+") ? `+${digits}` : digits;
133
+ }
134
+
135
+ export function isValidPhoneInput(value: string, rule: PhoneRule): boolean {
136
+ const normalized = normalizePhoneInput(value);
137
+ if (!normalized) return false;
138
+ if (!/^\+?\d+$/.test(normalized)) return false;
139
+ const digitsOnly = normalized.startsWith("+") ? normalized.slice(1) : normalized;
140
+ if (digitsOnly.length < rule.minDigits || digitsOnly.length > rule.maxDigits) return false;
141
+ if (rule.allowedPrefixes?.length) {
142
+ return rule.allowedPrefixes.some((prefix) => normalized.startsWith(prefix));
143
+ }
144
+ return true;
145
+ }
146
+
147
+ export function normalizePhoneForSave(raw: string, rule: PhoneRule): string {
148
+ const normalized = normalizePhoneInput(raw);
149
+ if (!normalized) return "";
150
+ if (!isValidPhoneInput(normalized, rule)) return normalized;
151
+ return normalized;
152
+ }
153
+
154
+ // ── Pre-built defaults ─────────────────────────────────────────────────────────
155
+
156
+ /** Indonesian phone: 08xx / +62xx, 10–15 digits. */
157
+ export const INDONESIA_PHONE_RULE: PhoneRule = {
158
+ minDigits: 10,
159
+ maxDigits: 15,
160
+ allowedPrefixes: ["08", "+62"],
161
+ };
162
+
163
+ /** International phone: any valid digit sequence, 7–15 digits, no prefix restriction. */
164
+ export const GLOBAL_PHONE_RULE: PhoneRule = {
165
+ minDigits: 7,
166
+ maxDigits: 15,
167
+ };
168
+
169
+ /** Standard currency precision for Indonesian banking (IDR 2 dec, USD 4 dec). */
170
+ export const INDONESIA_CURRENCY_CONFIG: CurrencyPrecisionConfig = {
171
+ precisionByCurrency: { IDR: 2, USD: 4 },
172
+ defaultPrecision: 2,
173
+ };
@@ -0,0 +1,139 @@
1
+ import type { ApiPath, FieldValidation, FormField, FormLayout, FormMode } from "./form-types";
2
+ import type { BosSharedLabels } from "./i18n";
3
+
4
+ // Non-React fallback — callers (React components) should always pass i18n from useBosSharedI18n().
5
+ const FALLBACK_I18N = {
6
+ validRequired: (label: string) => `${label} wajib diisi`,
7
+ validMinLength: (n: number) => `Minimal ${n} karakter`,
8
+ validMaxLength: (n: number) => `Maksimal ${n} karakter`,
9
+ validMin: (n: number) => `Nilai minimal ${n}`,
10
+ validMax: (n: number) => `Nilai maksimal ${n}`,
11
+ validPattern: "Format tidak valid",
12
+ } as const;
13
+
14
+ // ── Path resolution ───────────────────────────────────────────────────────────
15
+
16
+ export function resolveApiPath(path: ApiPath, params: Record<string, string>): string {
17
+ if (typeof path === "function") return path(params);
18
+ return path.replace(/\{([^}]+)\}/g, (_, key: string) => params[key] ?? "");
19
+ }
20
+
21
+ // ── Validation ────────────────────────────────────────────────────────────────
22
+
23
+ function msg(rule: unknown, fallback: string): string {
24
+ if (Array.isArray(rule) && typeof rule[1] === "string") return rule[1] as string;
25
+ if (typeof rule === "string") return rule;
26
+ return fallback;
27
+ }
28
+
29
+ function ruleVal<T>(rule: T | [T, string]): T {
30
+ return Array.isArray(rule) ? (rule[0] as T) : rule;
31
+ }
32
+
33
+ type ValidationI18n = Pick<
34
+ BosSharedLabels,
35
+ | "validRequired"
36
+ | "validMinLength"
37
+ | "validMaxLength"
38
+ | "validMin"
39
+ | "validMax"
40
+ | "validPattern"
41
+ >;
42
+
43
+ export function runFieldValidation<TData extends Record<string, unknown>>(
44
+ fields: FormField<TData>[],
45
+ data: TData,
46
+ mode: FormMode,
47
+ i18n?: Partial<ValidationI18n>,
48
+ ): Partial<Record<keyof TData, string>> {
49
+ const l: ValidationI18n = { ...FALLBACK_I18N, ...i18n };
50
+ const errors: Partial<Record<keyof TData, string>> = {};
51
+
52
+ for (const field of fields) {
53
+ const v: FieldValidation<TData> | undefined = field.validation;
54
+ if (!v) continue;
55
+
56
+ const isHidden = typeof field.hidden === "function"
57
+ ? field.hidden(mode, data)
58
+ : Boolean(field.hidden);
59
+ if (isHidden) continue;
60
+
61
+ if (field.mode) {
62
+ const allowed = Array.isArray(field.mode) ? field.mode : [field.mode];
63
+ if (!allowed.includes(mode)) continue;
64
+ }
65
+
66
+ const value = data[field.key];
67
+ const key = field.key;
68
+
69
+ const isEmpty = value === null || value === undefined || value === ""
70
+ || (typeof value === "string" && !value.trim());
71
+
72
+ if (v.required) {
73
+ if (isEmpty) {
74
+ errors[key] = msg(v.required, l.validRequired(String(field.label)));
75
+ continue;
76
+ }
77
+ }
78
+
79
+ if (isEmpty) continue;
80
+
81
+ if (!errors[key] && v.minLength !== undefined && typeof value === "string") {
82
+ if (value.length < ruleVal(v.minLength))
83
+ errors[key] = msg(v.minLength, l.validMinLength(ruleVal(v.minLength)));
84
+ }
85
+ if (!errors[key] && v.maxLength !== undefined && typeof value === "string") {
86
+ if (value.length > ruleVal(v.maxLength))
87
+ errors[key] = msg(v.maxLength, l.validMaxLength(ruleVal(v.maxLength)));
88
+ }
89
+ if (!errors[key] && v.min !== undefined && typeof value === "number") {
90
+ if (value < ruleVal(v.min))
91
+ errors[key] = msg(v.min, l.validMin(ruleVal(v.min)));
92
+ }
93
+ if (!errors[key] && v.max !== undefined && typeof value === "number") {
94
+ if (value > ruleVal(v.max))
95
+ errors[key] = msg(v.max, l.validMax(ruleVal(v.max)));
96
+ }
97
+ if (!errors[key] && v.pattern !== undefined && typeof value === "string") {
98
+ const regex = Array.isArray(v.pattern) ? v.pattern[0] : v.pattern;
99
+ if (!regex.test(value)) errors[key] = msg(v.pattern, l.validPattern);
100
+ }
101
+ if (!errors[key] && v.custom) {
102
+ const m = v.custom(value, data);
103
+ if (m) errors[key] = m;
104
+ }
105
+ if (!errors[key] && v.validators?.length) {
106
+ for (const fn of v.validators) {
107
+ const m = fn(value, data);
108
+ if (m) { errors[key] = m; break; }
109
+ }
110
+ }
111
+ }
112
+
113
+ return errors;
114
+ }
115
+
116
+ // ── Layout helpers ────────────────────────────────────────────────────────────
117
+
118
+ export function collectAllFields<TData extends Record<string, unknown>>(
119
+ layout: FormLayout<TData>,
120
+ ): FormField<TData>[] {
121
+ const sections = layout.type === "single-page"
122
+ ? layout.sections
123
+ : layout.pages.flatMap((p) => p.sections);
124
+ return sections.flatMap((s) => s.fields);
125
+ }
126
+
127
+ export function collectPageFields<TData extends Record<string, unknown>>(
128
+ layout: FormLayout<TData>,
129
+ pageIndex: number,
130
+ ): FormField<TData>[] {
131
+ if (layout.type === "single-page") return collectAllFields(layout);
132
+ return (layout.pages[pageIndex]?.sections ?? []).flatMap((s) => s.fields);
133
+ }
134
+
135
+ export function getLayoutPageCount<TData extends Record<string, unknown>>(
136
+ layout: FormLayout<TData>,
137
+ ): number {
138
+ return layout.type === "single-page" ? 1 : layout.pages.length;
139
+ }