@bazous/ui 0.3.2

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 (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +150 -0
  3. package/dist/answers/AnswerPicture.d.ts +13 -0
  4. package/dist/answers/contract.d.ts +160 -0
  5. package/dist/answers/draw.d.ts +16 -0
  6. package/dist/answers/index.d.ts +6 -0
  7. package/dist/answers/messages.d.ts +3 -0
  8. package/dist/answers.js +10 -0
  9. package/dist/answers.js.map +1 -0
  10. package/dist/canvas/index.d.ts +32 -0
  11. package/dist/canvas.js +285 -0
  12. package/dist/canvas.js.map +1 -0
  13. package/dist/contract/snapshot.d.ts +173 -0
  14. package/dist/engine/scenario.d.ts +40 -0
  15. package/dist/format/index.d.ts +37 -0
  16. package/dist/i18n/index.d.ts +25 -0
  17. package/dist/i18n/shared.d.ts +3 -0
  18. package/dist/index-yeBMtDf1.js +649 -0
  19. package/dist/index-yeBMtDf1.js.map +1 -0
  20. package/dist/index.d.ts +20 -0
  21. package/dist/index.js +1258 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/modules/Question.d.ts +18 -0
  24. package/dist/modules/available-now/index.d.ts +8 -0
  25. package/dist/modules/due-before-payday/index.d.ts +9 -0
  26. package/dist/modules/low-point/index.d.ts +12 -0
  27. package/dist/modules/margin-after-payday/index.d.ts +9 -0
  28. package/dist/modules/monthly-structure/index.d.ts +24 -0
  29. package/dist/modules/next-actions/index.d.ts +13 -0
  30. package/dist/modules/pay-cycles/index.d.ts +31 -0
  31. package/dist/modules/registry.d.ts +7 -0
  32. package/dist/modules/types.d.ts +77 -0
  33. package/dist/modules/what-if/index.d.ts +6 -0
  34. package/dist/primitives/CashflowChart.d.ts +22 -0
  35. package/dist/primitives/index.d.ts +57 -0
  36. package/dist/styles.css +1 -0
  37. package/package.json +84 -0
  38. package/schema/answers.schema.json +719 -0
  39. package/schema/canvas.json +181 -0
  40. package/schema/snapshot.schema.json +821 -0
package/dist/canvas.js ADDED
@@ -0,0 +1,285 @@
1
+ const o = [
2
+ "section",
3
+ "article",
4
+ "header",
5
+ "footer",
6
+ "div",
7
+ "span",
8
+ "p",
9
+ "h1",
10
+ "h2",
11
+ "h3",
12
+ "h4",
13
+ "ul",
14
+ "ol",
15
+ "li",
16
+ "dl",
17
+ "dt",
18
+ "dd",
19
+ "table",
20
+ "thead",
21
+ "tbody",
22
+ "tr",
23
+ "th",
24
+ "td",
25
+ "strong",
26
+ "em",
27
+ "b",
28
+ "i",
29
+ "small",
30
+ "br",
31
+ "hr",
32
+ "button",
33
+ "label",
34
+ "input",
35
+ "select",
36
+ "option",
37
+ "textarea",
38
+ "figure",
39
+ "figcaption",
40
+ "blockquote",
41
+ "code",
42
+ "time",
43
+ "mark"
44
+ ], l = [
45
+ "class",
46
+ "id",
47
+ "type",
48
+ "name",
49
+ "value",
50
+ "placeholder",
51
+ "for",
52
+ "checked",
53
+ "selected",
54
+ "disabled",
55
+ "min",
56
+ "max",
57
+ "step",
58
+ "rows",
59
+ "datetime",
60
+ "title",
61
+ "aria-label",
62
+ "role",
63
+ "data-intent",
64
+ "data-field",
65
+ "data-payload",
66
+ "colspan",
67
+ "scope",
68
+ "data-pct"
69
+ ], n = ["confirm", "discard", "answer", "reschedule", "mark_internal_transfer", "open_cockpit"], d = ["label", "amount", "currency", "due_date", "category"], s = [
70
+ "page",
71
+ "eyebrow",
72
+ "title",
73
+ "lead",
74
+ "muted",
75
+ "small",
76
+ "card",
77
+ "card-head",
78
+ "tile",
79
+ "tile-dark",
80
+ "tile-value",
81
+ "amount",
82
+ "neg",
83
+ "pos",
84
+ "row",
85
+ "date",
86
+ "list",
87
+ "grid-2",
88
+ "grid-3",
89
+ "chip",
90
+ "chip-warn",
91
+ "chip-ok",
92
+ "banner",
93
+ "banner-warn",
94
+ "impact",
95
+ "impact-before",
96
+ "impact-after",
97
+ "arrow",
98
+ "question",
99
+ "field",
100
+ "actions",
101
+ "button",
102
+ "button-primary",
103
+ "button-ghost",
104
+ "source",
105
+ "confidence",
106
+ "timeline",
107
+ "timeline-item",
108
+ "table",
109
+ "kv",
110
+ "kpis",
111
+ "kpi-label",
112
+ "kpi-value",
113
+ "kpi-note",
114
+ "bars",
115
+ "bar",
116
+ "bar-label",
117
+ "bar-track",
118
+ "bar-fill",
119
+ "bar-fill-2",
120
+ "bar-fill-3",
121
+ "bar-fill-4",
122
+ "bar-value",
123
+ "meter",
124
+ "meter-fill",
125
+ "stack",
126
+ "stack-seg-1",
127
+ "stack-seg-2",
128
+ "stack-seg-3",
129
+ "stack-seg-4",
130
+ "legend",
131
+ "legend-item",
132
+ "swatch-1",
133
+ "swatch-2",
134
+ "swatch-3",
135
+ "swatch-4",
136
+ "spark",
137
+ "spark-col",
138
+ "spark-col-warn",
139
+ "delta",
140
+ "delta-up",
141
+ "delta-down",
142
+ "timeline-h",
143
+ "timeline-h-item",
144
+ "timeline-h-item-now",
145
+ "timeline-h-item-warn",
146
+ "gauge",
147
+ "gauge-fill"
148
+ ], c = [
149
+ "bar-fill",
150
+ "bar-fill-2",
151
+ "bar-fill-3",
152
+ "bar-fill-4",
153
+ "meter-fill",
154
+ "gauge-fill",
155
+ "stack-seg-1",
156
+ "stack-seg-2",
157
+ "stack-seg-3",
158
+ "stack-seg-4",
159
+ "spark-col",
160
+ "spark-col-warn"
161
+ ], t = `
162
+ :root,[data-theme=dark]{color-scheme:dark;--ground:#0B0B0B;--ground-2:#1f1e1b;--card:#1A1A1A;--line:#3a3833;--line-2:#2a2926;--ink:#F5F3ED;--ink-2:#D9D6CE;--ink-3:#B7B5AE;--muted:#9a978f;--ember:#FF9292;--ember-2:#FFB4B4;--ember-bg:#392425;--ember-line:#6b3a3a;--gold:#DAAF37;--gold-2:#E8C46E;--leaf:#76CCA1;--leaf-bg:#1d3a2c;--contrast-bg:#0B0B0B;--contrast-line:#DAAF37;--contrast-ink:#F5F3ED;--contrast-muted:#B7B5AE;--quote:#23211d;--mark:#5a4a1c;--sans:"Montserrat",system-ui,-apple-system,"Segoe UI",sans-serif;--serif:"Playfair Display",Georgia,serif}
163
+ [data-theme=light]{color-scheme:light;--ground:#F5F3ED;--ground-2:#eae8e1;--card:#FFFFFF;--line:#cdd0d2;--line-2:#e2e3e2;--ink:#15191F;--ink-2:#323943;--ink-3:#555E6B;--muted:#838a93;--ember:#B33535;--ember-2:#8c2b2b;--ember-bg:#fff0f0;--ember-line:#e4b2b2;--gold:#896313;--gold-2:#DAAF37;--leaf:#1B704A;--leaf-bg:#e3f3ea;--contrast-bg:#0B0B0B;--contrast-line:#0B0B0B;--contrast-ink:#F5F3ED;--contrast-muted:#B7B5AE;--quote:#fbf8ee;--mark:#fbf3dd}
164
+ *{box-sizing:border-box}
165
+ html,body{margin:0;background:var(--ground);color:var(--ink);font-family:var(--sans);font-size:15px;line-height:1.45}
166
+ .page{display:flex;flex-direction:column;gap:16px;padding:8px 4px 24px}
167
+ h1,h2,h3,h4{font-family:var(--serif);font-weight:600;margin:0;letter-spacing:-.01em}
168
+ h1,.title{font-size:1.7rem;line-height:1.1}h2{font-size:1.2rem}h3{font-size:1.05rem}
169
+ p{margin:0}
170
+ .eyebrow{color:var(--gold);letter-spacing:.14em;font-size:.72rem;font-weight:600;text-transform:uppercase}
171
+ .lead{font-size:1.02rem;color:var(--ink-3)}
172
+ .muted{color:var(--ink-3)}.small{font-size:.8rem}
173
+ .card{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:18px 20px;display:flex;flex-direction:column;gap:10px}
174
+ .card-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;flex-wrap:wrap}
175
+ .tile{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:16px 18px;display:flex;flex-direction:column;gap:6px}
176
+ .tile-dark{background:var(--contrast-bg);border-color:var(--contrast-line);color:var(--contrast-ink)}
177
+ .tile-dark .eyebrow,.tile-dark .muted{color:var(--contrast-muted)}
178
+ .tile-value{font-family:var(--serif);font-size:2rem;font-weight:600;letter-spacing:-.02em;line-height:1;font-variant-numeric:tabular-nums}
179
+ .tile-dark .tile-value{color:var(--gold-2)}
180
+ .amount{font-variant-numeric:tabular-nums;font-weight:600;white-space:nowrap}
181
+ .neg{color:var(--ember)}.pos{color:var(--leaf)}
182
+ .grid-2{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}
183
+ .grid-3{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}
184
+ .list{display:flex;flex-direction:column}
185
+ .row{display:flex;align-items:center;gap:12px;padding:10px 0;border-top:1px solid var(--line-2);font-size:.9rem}
186
+ .row:first-child{border-top:0}
187
+ .date{width:48px;flex:none;color:var(--muted);font-variant-numeric:tabular-nums}
188
+ .chip{display:inline-block;font-size:.74rem;font-weight:600;padding:4px 10px;border-radius:999px;background:var(--ground-2);color:var(--ink-2)}
189
+ .chip-warn{background:var(--ember-bg);color:var(--ember-2)}
190
+ .chip-ok{background:var(--leaf-bg);color:var(--leaf)}
191
+ .banner{padding:12px 16px;border-radius:12px;background:var(--ground-2);font-size:.9rem}
192
+ .banner-warn{background:var(--ember-bg);border:1px solid var(--ember-line);color:var(--ember-2)}
193
+ .impact{display:flex;align-items:center;gap:14px;padding:14px 18px;background:var(--contrast-bg);border:1px solid var(--contrast-line);color:var(--contrast-ink);border-radius:12px;font-variant-numeric:tabular-nums;flex-wrap:wrap}
194
+ .impact-before{color:var(--contrast-muted);font-size:1.05rem}
195
+ .impact-after{color:var(--gold-2);font-family:var(--serif);font-size:1.6rem;font-weight:600}
196
+ .arrow{color:var(--contrast-muted);font-size:1.2rem}
197
+ .question{padding:12px 14px;background:var(--quote);border:1px solid var(--line);border-radius:10px;display:flex;flex-direction:column;gap:8px;font-size:.92rem}
198
+ .field{display:flex;flex-direction:column;gap:4px;font-size:.78rem;font-weight:600;color:var(--muted);letter-spacing:.04em;text-transform:uppercase}
199
+ .field input,.field select,.field textarea{font:500 15px var(--sans);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:9px 11px;background:var(--card);text-transform:none;letter-spacing:0}
200
+ .field input:focus,.field select:focus,.field textarea:focus{outline:2px solid var(--gold);border-color:var(--gold)}
201
+ .actions{display:flex;gap:10px;flex-wrap:wrap;align-items:center}
202
+ .button{font:600 13px var(--sans);border:1px solid var(--line);border-radius:10px;padding:0 14px;height:38px;background:var(--card);color:var(--ink);cursor:pointer}
203
+ .button-primary{background:var(--gold-2);border-color:var(--gold-2);color:#0B0B0B}
204
+ .button-ghost{background:transparent;color:var(--gold)}
205
+ .button:hover{filter:brightness(.97)}
206
+ .source{font-size:.82rem;color:var(--ink-3);border-left:3px solid var(--gold);padding:6px 12px;background:var(--quote);border-radius:0 8px 8px 0;font-style:italic}
207
+ .confidence{font-size:.78rem;color:var(--muted)}
208
+ .timeline{display:flex;flex-direction:column;gap:0}
209
+ .timeline-item{display:flex;gap:12px;padding:8px 0;border-left:2px solid var(--line);padding-left:14px;font-size:.88rem;position:relative}
210
+ .timeline-item::before{content:"";position:absolute;left:-6px;top:14px;width:10px;height:10px;border-radius:50%;background:var(--ink)}
211
+ .table{width:100%;border-collapse:collapse;font-size:.88rem}
212
+ .table th,.table td{text-align:left;padding:8px 6px;border-bottom:1px solid var(--line-2)}
213
+ .table th{font-size:.74rem;letter-spacing:.06em;text-transform:uppercase;color:var(--muted)}
214
+ .kv{display:grid;grid-template-columns:auto 1fr;gap:6px 16px;font-size:.9rem}
215
+ .kv dt{color:var(--muted)}.kv dd{margin:0;font-weight:500}
216
+ hr{border:0;border-top:1px solid var(--line-2);margin:4px 0}
217
+ /* dashboard primitives — sized by data-pct, applied by the bridge */
218
+ .kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px}
219
+ .kpi-label{font-size:.7rem;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
220
+ .tile-dark .kpi-label{color:var(--contrast-muted)}
221
+ .kpi-value{font-family:var(--serif);font-size:1.75rem;font-weight:600;letter-spacing:-.02em;line-height:1.05;font-variant-numeric:tabular-nums}
222
+ .tile-dark .kpi-value{color:var(--gold-2)}
223
+ .kpi-note{font-size:.78rem;color:var(--ink-3)}
224
+ .tile-dark .kpi-note{color:var(--contrast-muted)}
225
+ .bars{display:flex;flex-direction:column;gap:8px}
226
+ .bar{display:grid;grid-template-columns:minmax(90px,38%) 1fr auto;align-items:center;gap:10px;font-size:.86rem}
227
+ .bar-label{color:var(--ink-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
228
+ .bar-track{display:block;height:9px;border-radius:5px;background:var(--ground-2);overflow:hidden}
229
+ .bar-fill,.bar-fill-2,.bar-fill-3,.bar-fill-4{display:block;height:100%;border-radius:5px;width:0;transition:width .35s ease}
230
+ .bar-fill{background:var(--gold-2)}.bar-fill-2{background:var(--ink)}.bar-fill-3{background:var(--ink-2)}.bar-fill-4{background:var(--muted)}
231
+ .bar-value{font-variant-numeric:tabular-nums;font-weight:600;white-space:nowrap}
232
+ .meter,.gauge{display:block;height:8px;border-radius:4px;background:var(--ground-2);overflow:hidden}
233
+ .meter-fill,.gauge-fill{display:block;height:100%;border-radius:4px;width:0;background:var(--leaf);transition:width .35s ease}
234
+ .gauge{height:14px;border-radius:7px}.gauge-fill{background:var(--gold)}
235
+ .stack{display:flex;height:14px;border-radius:7px;overflow:hidden;background:var(--ground-2)}
236
+ .stack-seg-1,.stack-seg-2,.stack-seg-3,.stack-seg-4{display:block;height:100%;width:0}
237
+ .stack-seg-1{background:var(--gold-2)}.stack-seg-2{background:var(--ink)}.stack-seg-3{background:var(--ink-2)}.stack-seg-4{background:var(--muted)}
238
+ .legend{display:flex;flex-wrap:wrap;gap:6px 16px;font-size:.8rem;color:var(--ink-3)}
239
+ .legend-item{display:inline-flex;align-items:center;gap:6px}
240
+ .swatch-1,.swatch-2,.swatch-3,.swatch-4{display:inline-block;width:10px;height:10px;border-radius:2px}
241
+ .swatch-1{background:var(--gold-2)}.swatch-2{background:var(--ink)}.swatch-3{background:var(--ink-2)}.swatch-4{background:var(--muted)}
242
+ .spark{display:flex;align-items:flex-end;gap:3px;height:56px;padding:4px 0;border-bottom:1px solid var(--line)}
243
+ .spark-col,.spark-col-warn{display:block;flex:1;min-width:6px;height:0;border-radius:2px 2px 0 0;background:var(--ink)}
244
+ .spark-col-warn{background:var(--ember)}
245
+ .delta{display:inline-block;font-size:.8rem;font-weight:600;padding:2px 8px;border-radius:999px;background:var(--ground-2);font-variant-numeric:tabular-nums}
246
+ .delta-up{background:var(--leaf-bg);color:var(--leaf)}.delta-down{background:var(--ember-bg);color:var(--ember-2)}
247
+ .timeline-h{display:flex;gap:0;overflow:hidden;border-radius:10px;border:1px solid var(--line);background:var(--card)}
248
+ .timeline-h-item{flex:1;padding:10px 12px;font-size:.8rem;color:var(--ink-3);border-left:1px solid var(--line-2);position:relative;min-width:0}
249
+ .timeline-h-item:first-child{border-left:0}
250
+ .timeline-h-item::before{content:"";display:block;width:8px;height:8px;border-radius:50%;background:var(--muted);margin-bottom:6px}
251
+ .timeline-h-item-now{color:var(--gold);font-weight:600}.timeline-h-item-now::before{background:var(--gold)}
252
+ .timeline-h-item-warn{background:var(--ember-bg);color:var(--ember-2);font-weight:600}.timeline-h-item-warn::before{background:var(--ember)}
253
+ mark{background:var(--mark);padding:0 3px;border-radius:3px}
254
+ @media (max-width:640px){.grid-2,.grid-3{grid-template-columns:1fr}.tile-value{font-size:1.6rem}.kpi-value{font-size:1.4rem}.timeline-h{flex-direction:column}.timeline-h-item{border-left:0;border-top:1px solid var(--line-2)}.timeline-h-item:first-child{border-top:0}}
255
+ `, i = `
256
+ (function(){
257
+ function fields(){var o={};document.querySelectorAll('[data-field]').forEach(function(el){o[el.getAttribute('data-field')]=el.value;});return o;}
258
+ document.addEventListener('click',function(ev){
259
+ var btn=ev.target.closest('[data-intent]');if(!btn)return;ev.preventDefault();
260
+ var payload=btn.getAttribute('data-payload');
261
+ var q=btn.closest('.question');var answer=null;
262
+ if(q){var inp=q.querySelector('input,select,textarea');if(inp)answer=inp.value;}
263
+ parent.postMessage({source:'billminder-canvas',intent:btn.getAttribute('data-intent'),payload:payload,answer:answer,fields:fields()},'*');
264
+ });
265
+ function size(){document.querySelectorAll('[data-pct]').forEach(function(el){var p=Math.max(0,Math.min(100,parseInt(el.getAttribute('data-pct'),10)||0));var c=el.className||'';if(c.indexOf('spark-col')>=0){el.style.height=p+'%';}else{el.style.width=p+'%';}});}
266
+ size();
267
+ function report(){parent.postMessage({source:'billminder-canvas',intent:'resize',height:document.documentElement.scrollHeight},'*');}
268
+ new ResizeObserver(report).observe(document.documentElement);report();
269
+ })();
270
+ `;
271
+ function p(e, r = "dark", a = "fr") {
272
+ return `<!doctype html><html lang="${a}" data-theme="${r}"><head><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline' https://fonts.googleapis.com; script-src 'unsafe-inline'; font-src https://fonts.gstatic.com; img-src data:;"><link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@500;600&family=Montserrat:wght@400;500;600&display=swap" rel="stylesheet"><style>${t}</style></head><body>${e}<script>${i}<\/script></body></html>`;
273
+ }
274
+ export {
275
+ l as CANVAS_ATTRIBUTES,
276
+ i as CANVAS_BRIDGE,
277
+ s as CANVAS_CLASSES,
278
+ t as CANVAS_CSS,
279
+ d as CANVAS_FIELDS,
280
+ n as CANVAS_INTENTS,
281
+ c as CANVAS_SIZED_CLASSES,
282
+ o as CANVAS_TAGS,
283
+ p as canvasDocument
284
+ };
285
+ //# sourceMappingURL=canvas.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canvas.js","sources":["../src/canvas/index.ts"],"sourcesContent":["/**\n * The Capture canvas: the public contract between an AI model and the Bazous UI.\n *\n * A model writes an HTML fragment for a captured bill. The server rejects any tag,\n * attribute, class or intent outside these lists, the fragment runs in a sandboxed\n * iframe with a CSP that forbids the network, and buttons only emit named intents.\n * The same lists are enforced server-side; a test keeps them identical.\n */\n\nexport const CANVAS_TAGS = [\n \"section\", \"article\", \"header\", \"footer\", \"div\", \"span\", \"p\", \"h1\", \"h2\", \"h3\", \"h4\",\n \"ul\", \"ol\", \"li\", \"dl\", \"dt\", \"dd\", \"table\", \"thead\", \"tbody\", \"tr\", \"th\", \"td\",\n \"strong\", \"em\", \"b\", \"i\", \"small\", \"br\", \"hr\", \"button\", \"label\", \"input\", \"select\",\n \"option\", \"textarea\", \"figure\", \"figcaption\", \"blockquote\", \"code\", \"time\", \"mark\",\n] as const;\n\nexport const CANVAS_ATTRIBUTES = [\n \"class\", \"id\", \"type\", \"name\", \"value\", \"placeholder\", \"for\", \"checked\", \"selected\",\n \"disabled\", \"min\", \"max\", \"step\", \"rows\", \"datetime\", \"title\", \"aria-label\", \"role\",\n \"data-intent\", \"data-field\", \"data-payload\", \"colspan\", \"scope\", \"data-pct\",\n] as const;\n\nexport const CANVAS_INTENTS = [\"confirm\", \"discard\", \"answer\", \"reschedule\", \"mark_internal_transfer\", \"open_cockpit\"] as const;\nexport type CanvasIntent = (typeof CANVAS_INTENTS)[number];\n\nexport const CANVAS_FIELDS = [\"label\", \"amount\", \"currency\", \"due_date\", \"category\"] as const;\n\nexport const CANVAS_CLASSES = [\n \"page\", \"eyebrow\", \"title\", \"lead\", \"muted\", \"small\", \"card\", \"card-head\", \"tile\", \"tile-dark\",\n \"tile-value\", \"amount\", \"neg\", \"pos\", \"row\", \"date\", \"list\", \"grid-2\", \"grid-3\", \"chip\",\n \"chip-warn\", \"chip-ok\", \"banner\", \"banner-warn\", \"impact\", \"impact-before\", \"impact-after\",\n \"arrow\", \"question\", \"field\", \"actions\", \"button\", \"button-primary\", \"button-ghost\",\n \"source\", \"confidence\", \"timeline\", \"timeline-item\", \"table\", \"kv\",\n \"kpis\", \"kpi-label\", \"kpi-value\", \"kpi-note\", \"bars\", \"bar\", \"bar-label\", \"bar-track\", \"bar-fill\",\n \"bar-fill-2\", \"bar-fill-3\", \"bar-fill-4\", \"bar-value\", \"meter\", \"meter-fill\", \"stack\", \"stack-seg-1\",\n \"stack-seg-2\", \"stack-seg-3\", \"stack-seg-4\", \"legend\", \"legend-item\", \"swatch-1\", \"swatch-2\",\n \"swatch-3\", \"swatch-4\", \"spark\", \"spark-col\", \"spark-col-warn\", \"delta\", \"delta-up\", \"delta-down\",\n \"timeline-h\", \"timeline-h-item\", \"timeline-h-item-now\", \"timeline-h-item-warn\", \"gauge\", \"gauge-fill\",\n] as const;\n\n/** Classes whose width (height for spark columns) comes from `data-pct=\"0..100\"`, never from `style`. */\nexport const CANVAS_SIZED_CLASSES = [\n \"bar-fill\", \"bar-fill-2\", \"bar-fill-3\", \"bar-fill-4\", \"meter-fill\", \"gauge-fill\",\n \"stack-seg-1\", \"stack-seg-2\", \"stack-seg-3\", \"stack-seg-4\", \"spark-col\", \"spark-col-warn\",\n] as const;\n\nexport const CANVAS_CSS = `\n:root,[data-theme=dark]{color-scheme:dark;--ground:#0B0B0B;--ground-2:#1f1e1b;--card:#1A1A1A;--line:#3a3833;--line-2:#2a2926;--ink:#F5F3ED;--ink-2:#D9D6CE;--ink-3:#B7B5AE;--muted:#9a978f;--ember:#FF9292;--ember-2:#FFB4B4;--ember-bg:#392425;--ember-line:#6b3a3a;--gold:#DAAF37;--gold-2:#E8C46E;--leaf:#76CCA1;--leaf-bg:#1d3a2c;--contrast-bg:#0B0B0B;--contrast-line:#DAAF37;--contrast-ink:#F5F3ED;--contrast-muted:#B7B5AE;--quote:#23211d;--mark:#5a4a1c;--sans:\"Montserrat\",system-ui,-apple-system,\"Segoe UI\",sans-serif;--serif:\"Playfair Display\",Georgia,serif}\n[data-theme=light]{color-scheme:light;--ground:#F5F3ED;--ground-2:#eae8e1;--card:#FFFFFF;--line:#cdd0d2;--line-2:#e2e3e2;--ink:#15191F;--ink-2:#323943;--ink-3:#555E6B;--muted:#838a93;--ember:#B33535;--ember-2:#8c2b2b;--ember-bg:#fff0f0;--ember-line:#e4b2b2;--gold:#896313;--gold-2:#DAAF37;--leaf:#1B704A;--leaf-bg:#e3f3ea;--contrast-bg:#0B0B0B;--contrast-line:#0B0B0B;--contrast-ink:#F5F3ED;--contrast-muted:#B7B5AE;--quote:#fbf8ee;--mark:#fbf3dd}\n*{box-sizing:border-box}\nhtml,body{margin:0;background:var(--ground);color:var(--ink);font-family:var(--sans);font-size:15px;line-height:1.45}\n.page{display:flex;flex-direction:column;gap:16px;padding:8px 4px 24px}\nh1,h2,h3,h4{font-family:var(--serif);font-weight:600;margin:0;letter-spacing:-.01em}\nh1,.title{font-size:1.7rem;line-height:1.1}h2{font-size:1.2rem}h3{font-size:1.05rem}\np{margin:0}\n.eyebrow{color:var(--gold);letter-spacing:.14em;font-size:.72rem;font-weight:600;text-transform:uppercase}\n.lead{font-size:1.02rem;color:var(--ink-3)}\n.muted{color:var(--ink-3)}.small{font-size:.8rem}\n.card{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:18px 20px;display:flex;flex-direction:column;gap:10px}\n.card-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;flex-wrap:wrap}\n.tile{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:16px 18px;display:flex;flex-direction:column;gap:6px}\n.tile-dark{background:var(--contrast-bg);border-color:var(--contrast-line);color:var(--contrast-ink)}\n.tile-dark .eyebrow,.tile-dark .muted{color:var(--contrast-muted)}\n.tile-value{font-family:var(--serif);font-size:2rem;font-weight:600;letter-spacing:-.02em;line-height:1;font-variant-numeric:tabular-nums}\n.tile-dark .tile-value{color:var(--gold-2)}\n.amount{font-variant-numeric:tabular-nums;font-weight:600;white-space:nowrap}\n.neg{color:var(--ember)}.pos{color:var(--leaf)}\n.grid-2{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}\n.grid-3{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}\n.list{display:flex;flex-direction:column}\n.row{display:flex;align-items:center;gap:12px;padding:10px 0;border-top:1px solid var(--line-2);font-size:.9rem}\n.row:first-child{border-top:0}\n.date{width:48px;flex:none;color:var(--muted);font-variant-numeric:tabular-nums}\n.chip{display:inline-block;font-size:.74rem;font-weight:600;padding:4px 10px;border-radius:999px;background:var(--ground-2);color:var(--ink-2)}\n.chip-warn{background:var(--ember-bg);color:var(--ember-2)}\n.chip-ok{background:var(--leaf-bg);color:var(--leaf)}\n.banner{padding:12px 16px;border-radius:12px;background:var(--ground-2);font-size:.9rem}\n.banner-warn{background:var(--ember-bg);border:1px solid var(--ember-line);color:var(--ember-2)}\n.impact{display:flex;align-items:center;gap:14px;padding:14px 18px;background:var(--contrast-bg);border:1px solid var(--contrast-line);color:var(--contrast-ink);border-radius:12px;font-variant-numeric:tabular-nums;flex-wrap:wrap}\n.impact-before{color:var(--contrast-muted);font-size:1.05rem}\n.impact-after{color:var(--gold-2);font-family:var(--serif);font-size:1.6rem;font-weight:600}\n.arrow{color:var(--contrast-muted);font-size:1.2rem}\n.question{padding:12px 14px;background:var(--quote);border:1px solid var(--line);border-radius:10px;display:flex;flex-direction:column;gap:8px;font-size:.92rem}\n.field{display:flex;flex-direction:column;gap:4px;font-size:.78rem;font-weight:600;color:var(--muted);letter-spacing:.04em;text-transform:uppercase}\n.field input,.field select,.field textarea{font:500 15px var(--sans);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:9px 11px;background:var(--card);text-transform:none;letter-spacing:0}\n.field input:focus,.field select:focus,.field textarea:focus{outline:2px solid var(--gold);border-color:var(--gold)}\n.actions{display:flex;gap:10px;flex-wrap:wrap;align-items:center}\n.button{font:600 13px var(--sans);border:1px solid var(--line);border-radius:10px;padding:0 14px;height:38px;background:var(--card);color:var(--ink);cursor:pointer}\n.button-primary{background:var(--gold-2);border-color:var(--gold-2);color:#0B0B0B}\n.button-ghost{background:transparent;color:var(--gold)}\n.button:hover{filter:brightness(.97)}\n.source{font-size:.82rem;color:var(--ink-3);border-left:3px solid var(--gold);padding:6px 12px;background:var(--quote);border-radius:0 8px 8px 0;font-style:italic}\n.confidence{font-size:.78rem;color:var(--muted)}\n.timeline{display:flex;flex-direction:column;gap:0}\n.timeline-item{display:flex;gap:12px;padding:8px 0;border-left:2px solid var(--line);padding-left:14px;font-size:.88rem;position:relative}\n.timeline-item::before{content:\"\";position:absolute;left:-6px;top:14px;width:10px;height:10px;border-radius:50%;background:var(--ink)}\n.table{width:100%;border-collapse:collapse;font-size:.88rem}\n.table th,.table td{text-align:left;padding:8px 6px;border-bottom:1px solid var(--line-2)}\n.table th{font-size:.74rem;letter-spacing:.06em;text-transform:uppercase;color:var(--muted)}\n.kv{display:grid;grid-template-columns:auto 1fr;gap:6px 16px;font-size:.9rem}\n.kv dt{color:var(--muted)}.kv dd{margin:0;font-weight:500}\nhr{border:0;border-top:1px solid var(--line-2);margin:4px 0}\n/* dashboard primitives — sized by data-pct, applied by the bridge */\n.kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px}\n.kpi-label{font-size:.7rem;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}\n.tile-dark .kpi-label{color:var(--contrast-muted)}\n.kpi-value{font-family:var(--serif);font-size:1.75rem;font-weight:600;letter-spacing:-.02em;line-height:1.05;font-variant-numeric:tabular-nums}\n.tile-dark .kpi-value{color:var(--gold-2)}\n.kpi-note{font-size:.78rem;color:var(--ink-3)}\n.tile-dark .kpi-note{color:var(--contrast-muted)}\n.bars{display:flex;flex-direction:column;gap:8px}\n.bar{display:grid;grid-template-columns:minmax(90px,38%) 1fr auto;align-items:center;gap:10px;font-size:.86rem}\n.bar-label{color:var(--ink-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.bar-track{display:block;height:9px;border-radius:5px;background:var(--ground-2);overflow:hidden}\n.bar-fill,.bar-fill-2,.bar-fill-3,.bar-fill-4{display:block;height:100%;border-radius:5px;width:0;transition:width .35s ease}\n.bar-fill{background:var(--gold-2)}.bar-fill-2{background:var(--ink)}.bar-fill-3{background:var(--ink-2)}.bar-fill-4{background:var(--muted)}\n.bar-value{font-variant-numeric:tabular-nums;font-weight:600;white-space:nowrap}\n.meter,.gauge{display:block;height:8px;border-radius:4px;background:var(--ground-2);overflow:hidden}\n.meter-fill,.gauge-fill{display:block;height:100%;border-radius:4px;width:0;background:var(--leaf);transition:width .35s ease}\n.gauge{height:14px;border-radius:7px}.gauge-fill{background:var(--gold)}\n.stack{display:flex;height:14px;border-radius:7px;overflow:hidden;background:var(--ground-2)}\n.stack-seg-1,.stack-seg-2,.stack-seg-3,.stack-seg-4{display:block;height:100%;width:0}\n.stack-seg-1{background:var(--gold-2)}.stack-seg-2{background:var(--ink)}.stack-seg-3{background:var(--ink-2)}.stack-seg-4{background:var(--muted)}\n.legend{display:flex;flex-wrap:wrap;gap:6px 16px;font-size:.8rem;color:var(--ink-3)}\n.legend-item{display:inline-flex;align-items:center;gap:6px}\n.swatch-1,.swatch-2,.swatch-3,.swatch-4{display:inline-block;width:10px;height:10px;border-radius:2px}\n.swatch-1{background:var(--gold-2)}.swatch-2{background:var(--ink)}.swatch-3{background:var(--ink-2)}.swatch-4{background:var(--muted)}\n.spark{display:flex;align-items:flex-end;gap:3px;height:56px;padding:4px 0;border-bottom:1px solid var(--line)}\n.spark-col,.spark-col-warn{display:block;flex:1;min-width:6px;height:0;border-radius:2px 2px 0 0;background:var(--ink)}\n.spark-col-warn{background:var(--ember)}\n.delta{display:inline-block;font-size:.8rem;font-weight:600;padding:2px 8px;border-radius:999px;background:var(--ground-2);font-variant-numeric:tabular-nums}\n.delta-up{background:var(--leaf-bg);color:var(--leaf)}.delta-down{background:var(--ember-bg);color:var(--ember-2)}\n.timeline-h{display:flex;gap:0;overflow:hidden;border-radius:10px;border:1px solid var(--line);background:var(--card)}\n.timeline-h-item{flex:1;padding:10px 12px;font-size:.8rem;color:var(--ink-3);border-left:1px solid var(--line-2);position:relative;min-width:0}\n.timeline-h-item:first-child{border-left:0}\n.timeline-h-item::before{content:\"\";display:block;width:8px;height:8px;border-radius:50%;background:var(--muted);margin-bottom:6px}\n.timeline-h-item-now{color:var(--gold);font-weight:600}.timeline-h-item-now::before{background:var(--gold)}\n.timeline-h-item-warn{background:var(--ember-bg);color:var(--ember-2);font-weight:600}.timeline-h-item-warn::before{background:var(--ember)}\nmark{background:var(--mark);padding:0 3px;border-radius:3px}\n@media (max-width:640px){.grid-2,.grid-3{grid-template-columns:1fr}.tile-value{font-size:1.6rem}.kpi-value{font-size:1.4rem}.timeline-h{flex-direction:column}.timeline-h-item{border-left:0;border-top:1px solid var(--line-2)}.timeline-h-item:first-child{border-top:0}}\n`;\n\n/** Runs inside the sandbox: sizes data-pct elements and forwards intents and field values to the host. */\nexport const CANVAS_BRIDGE = `\n(function(){\n function fields(){var o={};document.querySelectorAll('[data-field]').forEach(function(el){o[el.getAttribute('data-field')]=el.value;});return o;}\n document.addEventListener('click',function(ev){\n var btn=ev.target.closest('[data-intent]');if(!btn)return;ev.preventDefault();\n var payload=btn.getAttribute('data-payload');\n var q=btn.closest('.question');var answer=null;\n if(q){var inp=q.querySelector('input,select,textarea');if(inp)answer=inp.value;}\n parent.postMessage({source:'billminder-canvas',intent:btn.getAttribute('data-intent'),payload:payload,answer:answer,fields:fields()},'*');\n });\n function size(){document.querySelectorAll('[data-pct]').forEach(function(el){var p=Math.max(0,Math.min(100,parseInt(el.getAttribute('data-pct'),10)||0));var c=el.className||'';if(c.indexOf('spark-col')>=0){el.style.height=p+'%';}else{el.style.width=p+'%';}});}\n size();\n function report(){parent.postMessage({source:'billminder-canvas',intent:'resize',height:document.documentElement.scrollHeight},'*');}\n new ResizeObserver(report).observe(document.documentElement);report();\n})();\n`;\n\nexport type CanvasMessage =\n | { source: \"billminder-canvas\"; intent: CanvasIntent; payload: string | null; answer: string | null; fields: Record<string, string> }\n | { source: \"billminder-canvas\"; intent: \"resize\"; height: number };\n\n/** A complete sandboxed document for `<iframe sandbox=\"allow-scripts\" srcdoc=…>`. */\nexport function canvasDocument(fragment: string, theme: \"dark\" | \"light\" = \"dark\", lang = \"fr\"): string {\n return (\n `<!doctype html><html lang=\"${lang}\" data-theme=\"${theme}\"><head><meta charset=\"utf-8\">` +\n \"<meta http-equiv=\\\"Content-Security-Policy\\\" content=\\\"default-src 'none'; style-src 'unsafe-inline' https://fonts.googleapis.com; script-src 'unsafe-inline'; font-src https://fonts.gstatic.com; img-src data:;\\\">\" +\n '<link href=\"https://fonts.googleapis.com/css2?family=Playfair+Display:wght@500;600&family=Montserrat:wght@400;500;600&display=swap\" rel=\"stylesheet\">' +\n `<style>${CANVAS_CSS}</style></head><body>${fragment}<script>${CANVAS_BRIDGE}</script></body></html>`\n );\n}\n"],"names":["CANVAS_TAGS","CANVAS_ATTRIBUTES","CANVAS_INTENTS","CANVAS_FIELDS","CANVAS_CLASSES","CANVAS_SIZED_CLASSES","CANVAS_CSS","CANVAS_BRIDGE","canvasDocument","fragment","theme","lang"],"mappings":"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"}
@@ -0,0 +1,173 @@
1
+ /**
2
+ * The snapshot contract: the only thing this kit knows about a household.
3
+ *
4
+ * It is the JSON returned by the Bazous API (`GET /api/v1/cockpit`), described
5
+ * formally in `schema/snapshot.schema.json`. Amounts are decimal strings in the
6
+ * household base currency unless a field says otherwise; dates are ISO `YYYY-MM-DD`.
7
+ * Nothing in this kit fetches it, stores it or recomputes the forecast from it.
8
+ */
9
+ export type Decimal = string;
10
+ export type IsoDate = string;
11
+ export type Direction = "inflow" | "outflow";
12
+ export type CashEvent = {
13
+ date: IsoDate;
14
+ label: string;
15
+ /** Amount in the base currency. */
16
+ amount: Decimal;
17
+ direction: Direction;
18
+ /** Amount in the currency of the bill itself. */
19
+ original_amount: Decimal;
20
+ currency: string;
21
+ kind: "obligation" | "recurrence" | string;
22
+ ref_id: string | null;
23
+ status: string | null;
24
+ };
25
+ export type DailyPoint = {
26
+ date: IsoDate;
27
+ inflows: Decimal;
28
+ outflows: Decimal;
29
+ closing_balance: Decimal;
30
+ };
31
+ export type Cycle = {
32
+ /** `before_income`, then `cycle_1`, `cycle_2`… */
33
+ key: string;
34
+ start: IsoDate;
35
+ end: IsoDate;
36
+ total_out: Decimal;
37
+ total_in: Decimal;
38
+ items: CashEvent[];
39
+ };
40
+ export type Driver = {
41
+ label: string;
42
+ date: IsoDate;
43
+ amount: Decimal;
44
+ share: Decimal;
45
+ };
46
+ /**
47
+ * The decision queue. Open bills are classified by the engine:
48
+ * `overdue` (before today), `due_today`, `before_income` (on or before the next income),
49
+ * `plan` (after it: `target` is the income date to pay on), `undated` (no due date yet).
50
+ */
51
+ export type BillAction = {
52
+ type: "overdue" | "due_today" | "before_income" | "plan" | "undated";
53
+ ref_id: string;
54
+ label: string;
55
+ description?: string | null;
56
+ date: IsoDate | null;
57
+ amount: Decimal;
58
+ currency: string;
59
+ amount_base: Decimal;
60
+ confidence?: Decimal | null;
61
+ target?: IsoDate | null;
62
+ };
63
+ export type Action = BillAction | {
64
+ type: "needs_review";
65
+ kind: "obligation" | "transaction";
66
+ ref_id: string;
67
+ label: string;
68
+ date: IsoDate | null;
69
+ amount: Decimal;
70
+ currency: string;
71
+ confidence: Decimal | null;
72
+ } | {
73
+ type: "missing_amounts";
74
+ label: string;
75
+ items: string[];
76
+ };
77
+ export type Account = {
78
+ id: string;
79
+ name: string;
80
+ account_type: string;
81
+ currency: string;
82
+ institution: string | null;
83
+ balance: Decimal | null;
84
+ balance_base: Decimal | null;
85
+ observed_at: string | null;
86
+ };
87
+ export type Snapshot = {
88
+ as_of: IsoDate;
89
+ horizon_end: IsoDate;
90
+ next_income_date: IsoDate | null;
91
+ second_income_date: IsoDate | null;
92
+ base_currency: string;
93
+ fx_rates: Record<string, Decimal>;
94
+ opening_balance: Decimal;
95
+ due_before_next_income: Decimal;
96
+ /** opening_balance − due_before_next_income: negative means short before payday. */
97
+ gap_before_next_income: Decimal;
98
+ low_point: {
99
+ date: IsoDate;
100
+ balance: Decimal;
101
+ days_from_now: number;
102
+ };
103
+ end_of_horizon: {
104
+ date: IsoDate;
105
+ balance: Decimal;
106
+ };
107
+ main_cycle: {
108
+ start: IsoDate;
109
+ end: IsoDate;
110
+ total_out: Decimal;
111
+ total_in: Decimal;
112
+ drivers: Driver[];
113
+ };
114
+ cycles: Cycle[];
115
+ points: DailyPoint[];
116
+ events: CashEvent[];
117
+ household: {
118
+ id: string;
119
+ name: string;
120
+ };
121
+ accounts: Account[];
122
+ structure: {
123
+ fixed_monthly: Decimal;
124
+ income_monthly: Decimal;
125
+ fixed_ratio_percent: Decimal | null;
126
+ structural_margin: Decimal | null;
127
+ by_category: {
128
+ category: string;
129
+ amount: Decimal;
130
+ }[];
131
+ };
132
+ debts: {
133
+ total_base: Decimal;
134
+ items: {
135
+ id: string;
136
+ name: string;
137
+ principal: Decimal;
138
+ currency: string;
139
+ due_day: number | null;
140
+ }[];
141
+ };
142
+ actions: Action[];
143
+ /** Last full month of imported transactions, by category. */
144
+ actual_vs_expected?: {
145
+ period_start: IsoDate | null;
146
+ period_end: IsoDate | null;
147
+ complete: boolean;
148
+ items: {
149
+ category: string;
150
+ expected: Decimal | null;
151
+ actual: Decimal;
152
+ delta: Decimal | null;
153
+ }[];
154
+ };
155
+ uncategorized?: {
156
+ id: string;
157
+ label: string;
158
+ amount: Decimal;
159
+ currency: string;
160
+ booking_date: IsoDate;
161
+ suggested_category: string | null;
162
+ }[];
163
+ data_quality: {
164
+ last_transaction_date: IsoDate | null;
165
+ days_since_last_transaction: number | null;
166
+ accounts_without_balance: string[];
167
+ fx_missing: string[];
168
+ rules_without_amount: string[];
169
+ needs_review_count: number;
170
+ };
171
+ };
172
+ /** Numbers travel as strings to keep cents exact; the kit only turns them into numbers to draw. */
173
+ export declare const toNumber: (value: Decimal | number | null | undefined) => number;
@@ -0,0 +1,40 @@
1
+ import type { CashEvent, IsoDate, Snapshot } from "../contract/snapshot";
2
+ /**
3
+ * What-if levers, replayed on the engine's own events.
4
+ *
5
+ * This is the one calculation the kit does: it re-sums the snapshot's events day by
6
+ * day after moving some of them. It does not invent events, amounts or dates beyond
7
+ * the lever's target, and with no lever active it reproduces the engine's curve exactly.
8
+ */
9
+ export type LeverKind = "split" | "defer" | "reserve";
10
+ export type Lever = {
11
+ id: string;
12
+ kind: LeverKind;
13
+ event: CashEvent;
14
+ /** Where the moved money lands: the income after the main cycle. */
15
+ target: IsoDate;
16
+ /** For `reserve`: the cushion kept after the next income. */
17
+ reserve?: number;
18
+ };
19
+ export type SeriesPoint = {
20
+ date: IsoDate;
21
+ balance: number;
22
+ /** Set on days that both receive and pay: balance right after the income. */
23
+ peak?: number;
24
+ };
25
+ export type Series = {
26
+ points: SeriesPoint[];
27
+ low: {
28
+ date: IsoDate;
29
+ balance: number;
30
+ daysFromNow: number;
31
+ };
32
+ end: {
33
+ date: IsoDate;
34
+ balance: number;
35
+ };
36
+ };
37
+ export declare function buildLevers(snapshot: Snapshot): Lever[];
38
+ export declare function computeSeries(snapshot: Snapshot, active: ReadonlySet<string>, levers: readonly Lever[]): Series;
39
+ /** The engine's own curve, read from `points` (no replay). */
40
+ export declare function baseSeries(snapshot: Snapshot): Series;
@@ -0,0 +1,37 @@
1
+ import type { Decimal, IsoDate } from "../contract/snapshot";
2
+ import type { Locale } from "../i18n";
3
+ /**
4
+ * Swiss figures in every language: 3’420.50 — typographic apostrophe for thousands,
5
+ * point for decimals, true minus sign. Browsers disagree on fr-CH / it-CH grouping
6
+ * (narrow space, none under 10’000, comma decimals), so numbers never go through Intl.
7
+ */
8
+ export declare const THOUSANDS = "\u2019";
9
+ export declare const MINUS = "\u2212";
10
+ export declare function num(value: Decimal | number | null | undefined, digits?: number): string;
11
+ /** Always shows the sign: +1’175, −1’053, 0. */
12
+ export declare function signed(value: Decimal | number | null | undefined, digits?: number): string;
13
+ /** Amount with its ISO code after it: 2’419.58 EUR. */
14
+ export declare function money(value: Decimal | number | null | undefined, currency: string, digits?: number): string;
15
+ export declare function parseDate(iso: IsoDate): Date;
16
+ export declare function isoDate(date: Date): IsoDate;
17
+ export declare function addDays(iso: IsoDate, days: number): IsoDate;
18
+ export declare function daysBetween(from: IsoDate, to: IsoDate): number;
19
+ /** 24.09 — the Swiss short date, identical in all five languages. */
20
+ export declare function shortDate(iso: IsoDate | null | undefined): string;
21
+ /** "05–10" inside one month, "30.09–07.10" across two. */
22
+ export declare function dateSpan(from: IsoDate, to: IsoDate): string;
23
+ export type Formatters = {
24
+ locale: Locale;
25
+ num: typeof num;
26
+ signed: typeof signed;
27
+ money: typeof money;
28
+ shortDate: typeof shortDate;
29
+ dateSpan: typeof dateSpan;
30
+ /** "jeudi 1 octobre", "Donnerstag, 1. Oktober"… */
31
+ longDate: (iso: IsoDate) => string;
32
+ /** "jeu.", "Do", "gio"… */
33
+ weekday: (iso: IsoDate) => string;
34
+ };
35
+ export declare function createFormatters(locale: Locale): Formatters;
36
+ export type Tone = "neg" | "pos" | "flat";
37
+ export declare function tone(value: Decimal | number | null | undefined): Tone;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Five languages, one rule: every key exists in every language (a test enforces it).
3
+ *
4
+ * Messages are plain strings with `{name}` placeholders. A key may have plural
5
+ * forms written as `key.one` / `key.other`; `t(key, { n })` picks the right one.
6
+ */
7
+ export declare const LOCALES: readonly ["fr", "de", "it", "rm", "en"];
8
+ export type Locale = (typeof LOCALES)[number];
9
+ export declare const LOCALE_NAMES: Record<Locale, string>;
10
+ /** BCP 47 tags used for calendar names. Numbers do not use Intl (see format/). */
11
+ export declare const LOCALE_TAGS: Record<Locale, string>;
12
+ export type Messages = Record<string, string>;
13
+ export type Catalog = Record<Locale, Messages>;
14
+ export type Vars = Record<string, string | number>;
15
+ export type Translate = (key: string, vars?: Vars) => string;
16
+ export declare function isLocale(value: unknown): value is Locale;
17
+ /** Pick a locale from a list of preferences (e.g. `navigator.languages`), French by default. */
18
+ export declare function pickLocale(preferences: readonly string[] | undefined, fallback?: Locale): Locale;
19
+ /**
20
+ * Build `t` over one or more catalogs; later catalogs win (a module can override a shared key).
21
+ * A missing key renders as `⟦key⟧` so it is visible in review instead of silently blank.
22
+ */
23
+ export declare function createT(locale: Locale, ...catalogs: Catalog[]): Translate;
24
+ /** The keys a catalog defines in one language, plural forms folded (`a.one`, `a.other` → `a`). */
25
+ export declare function catalogKeys(catalog: Catalog, locale: Locale): string[];
@@ -0,0 +1,3 @@
1
+ import type { Catalog } from "./index";
2
+ /** Words every module may use. Module catalogs can override a key for their own wording. */
3
+ export declare const shared: Catalog;