@oods/components-vue 0.3.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.
@@ -0,0 +1,1307 @@
1
+ 'use strict';
2
+
3
+ var componentContracts = require('@oods/component-contracts');
4
+ var vue = require('vue');
5
+
6
+ // src/ported.ts
7
+ var toneProp = String;
8
+ var emphasisProp = String;
9
+ var sizeProp = String;
10
+ var STATUS_TONES = {
11
+ active: "success",
12
+ paid: "success",
13
+ success: "success",
14
+ successful: "success",
15
+ future: "info",
16
+ pending: "info",
17
+ pending_cancellation: "info",
18
+ posted: "info",
19
+ processing: "info",
20
+ trialing: "accent",
21
+ caution: "warning",
22
+ warning: "warning",
23
+ delinquent: "critical",
24
+ error: "critical",
25
+ failed: "critical",
26
+ past_due: "critical",
27
+ unpaid: "critical"
28
+ };
29
+ var DOMAIN_STATUS_TONES = {
30
+ subscription: {
31
+ future: "info",
32
+ trialing: "accent",
33
+ active: "success",
34
+ paused: "neutral",
35
+ pending_cancellation: "info",
36
+ past_due: "critical",
37
+ unpaid: "critical",
38
+ terminated: "neutral"
39
+ },
40
+ invoice: {
41
+ draft: "neutral",
42
+ posted: "info",
43
+ paid: "success",
44
+ past_due: "critical",
45
+ void: "neutral"
46
+ }
47
+ };
48
+ function normalizeStatus(status) {
49
+ return status?.trim().toLowerCase().replace(/[\s-]+/g, "_") ?? "";
50
+ }
51
+ function resolveTone(tone, status, domain) {
52
+ const normalizedStatus = normalizeStatus(status);
53
+ const normalizedDomain = domain?.trim().toLowerCase() ?? "";
54
+ return tone ?? DOMAIN_STATUS_TONES[normalizedDomain]?.[normalizedStatus] ?? STATUS_TONES[normalizedStatus] ?? "neutral";
55
+ }
56
+ function tokenTone(tone) {
57
+ if (tone === "positive") return "success";
58
+ if (tone === "danger") return "critical";
59
+ return tone;
60
+ }
61
+ function statusLabel(status) {
62
+ return normalizeStatus(status).split("_").filter(Boolean).map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`).join(" ");
63
+ }
64
+ function slotOrValue(slot, value) {
65
+ return slot ? slot() : value;
66
+ }
67
+ var Badge = vue.defineComponent({
68
+ name: "OodsBadge",
69
+ props: {
70
+ content: { type: [String, Number], default: void 0 },
71
+ status: String,
72
+ domain: { type: String, default: "subscription" },
73
+ tone: toneProp,
74
+ emphasis: { type: emphasisProp, default: "subtle" },
75
+ icon: { type: [String, Number, Object], default: void 0 }
76
+ },
77
+ setup(props, { slots }) {
78
+ return () => {
79
+ const tone = resolveTone(props.tone, props.status, props.domain);
80
+ const palette = tokenTone(tone);
81
+ return vue.h("span", {
82
+ class: "oods-badge",
83
+ "data-oods-component": "Badge",
84
+ "data-status": props.status,
85
+ "data-domain": props.domain,
86
+ "data-tone": tone,
87
+ "data-emphasis": props.emphasis,
88
+ style: {
89
+ "--cmp-badge-background": `var(--sys-status-${palette}-surface)`,
90
+ "--cmp-badge-border": `var(--sys-status-${palette}-border)`,
91
+ "--cmp-badge-text": `var(--sys-status-${palette}-text)`
92
+ }
93
+ }, [
94
+ props.icon || slots.icon ? vue.h("span", { class: "oods-badge-icon", "aria-hidden": "true" }, [slotOrValue(slots.icon, props.icon)]) : null,
95
+ slotOrValue(slots.default, props.content ?? statusLabel(props.status))
96
+ ]);
97
+ };
98
+ }
99
+ });
100
+ vue.defineComponent({
101
+ name: "OodsBanner",
102
+ props: {
103
+ title: { type: String, default: "" },
104
+ detail: { type: String, default: "" },
105
+ content: { type: [String, Number], default: "" },
106
+ status: String,
107
+ domain: { type: String, default: "subscription" },
108
+ tone: toneProp,
109
+ emphasis: { type: emphasisProp, default: "subtle" },
110
+ dismissLabel: String
111
+ },
112
+ emits: {
113
+ dismiss: () => true
114
+ },
115
+ setup(props, { emit, slots }) {
116
+ return () => {
117
+ const tone = resolveTone(props.tone, props.status, props.domain);
118
+ const palette = tokenTone(tone);
119
+ return vue.h("section", {
120
+ class: "oods-banner",
121
+ "data-oods-component": "Banner",
122
+ "data-status": props.status,
123
+ "data-domain": props.domain,
124
+ "data-tone": tone,
125
+ "data-emphasis": props.emphasis,
126
+ role: tone === "critical" || tone === "danger" ? "alert" : "status",
127
+ "aria-live": tone === "critical" || tone === "danger" ? "assertive" : "polite",
128
+ style: {
129
+ "--cmp-banner-background": `var(--sys-status-${palette}-surface)`,
130
+ "--cmp-banner-border": `var(--sys-status-${palette}-border)`,
131
+ "--cmp-banner-text": `var(--sys-status-${palette}-text)`
132
+ }
133
+ }, [
134
+ vue.h("div", { class: "oods-banner-content" }, [
135
+ props.title || props.status || slots.title ? vue.h("strong", { class: "oods-banner-title" }, [
136
+ slotOrValue(slots.title, props.title || statusLabel(props.status))
137
+ ]) : null,
138
+ props.detail ? vue.h("p", { class: "oods-banner-detail" }, props.detail) : null,
139
+ slots.default || props.content ? vue.h(slots.default ? "div" : "p", { class: "oods-banner-body" }, [slotOrValue(slots.default, props.content)]) : null,
140
+ slots.actions ? vue.h("div", { class: "oods-banner-actions" }, slots.actions()) : null
141
+ ]),
142
+ props.dismissLabel ? vue.h("button", {
143
+ type: "button",
144
+ class: "oods-banner-dismiss",
145
+ "aria-label": props.dismissLabel,
146
+ onClick: () => emit("dismiss")
147
+ }, [vue.h("span", { "aria-hidden": "true" }, "\xD7")]) : null
148
+ ]);
149
+ };
150
+ }
151
+ });
152
+ vue.defineComponent({
153
+ name: "OodsButton",
154
+ props: {
155
+ content: { type: [String, Number], default: "" },
156
+ intent: { type: String, default: "neutral" },
157
+ size: { type: sizeProp, default: "md" },
158
+ disabled: Boolean,
159
+ type: { type: String, default: "button" }
160
+ },
161
+ emits: {
162
+ activate: (_event) => true
163
+ },
164
+ setup(props, { emit, slots }) {
165
+ return () => vue.h("button", {
166
+ type: props.type,
167
+ disabled: props.disabled,
168
+ class: "oods-button",
169
+ "data-oods-component": "Button",
170
+ "data-intent": props.intent,
171
+ "data-size": props.size,
172
+ style: {
173
+ "--cmp-button-background": "var(--sys-surface-interactive-primary-default)",
174
+ "--cmp-button-background-hover": "var(--sys-surface-interactive-primary-hover)",
175
+ "--cmp-button-background-disabled": "var(--sys-surface-disabled)",
176
+ "--cmp-button-border": "var(--sys-border-subtle)",
177
+ "--cmp-button-text": "var(--sys-text-on-interactive)",
178
+ "--cmp-button-text-disabled": "var(--sys-text-disabled)"
179
+ },
180
+ onClick: (event) => emit("activate", event)
181
+ }, [slotOrValue(slots.default, props.content)]);
182
+ }
183
+ });
184
+ var SAFE_CARD_ELEMENTS = /* @__PURE__ */ new Set(["div", "article", "section", "aside"]);
185
+ vue.defineComponent({
186
+ name: "OodsCard",
187
+ props: {
188
+ elevated: Boolean,
189
+ as: { type: String, default: "div" }
190
+ },
191
+ setup(props, { slots }) {
192
+ return () => vue.h(SAFE_CARD_ELEMENTS.has(props.as) ? props.as : "div", {
193
+ class: "oods-card",
194
+ "data-oods-component": "Card",
195
+ "data-elevated": String(props.elevated)
196
+ }, slots.default?.());
197
+ }
198
+ });
199
+ var GAP_VALUES = {
200
+ xs: "var(--cmp-spacing-inline-xs, 0.5rem)",
201
+ sm: "var(--cmp-spacing-inline-sm, 0.75rem)",
202
+ md: "var(--cmp-spacing-stack-default, 1rem)",
203
+ lg: "var(--cmp-spacing-stack-lg, 1.5rem)",
204
+ xl: "var(--cmp-spacing-stack-xl, 2rem)"
205
+ };
206
+ function resolveGap(gap) {
207
+ return GAP_VALUES[gap] ?? gap;
208
+ }
209
+ function maxColumns(columns) {
210
+ const count = Number(columns);
211
+ return Number.isFinite(count) && count > 0 ? count : void 0;
212
+ }
213
+ vue.defineComponent({
214
+ name: "OodsGrid",
215
+ props: {
216
+ columns: { type: [Number, String], default: "auto-fit" },
217
+ minColumnWidth: { type: String, default: "16rem" },
218
+ gap: { type: String, default: "md" },
219
+ align: { type: String, default: "stretch" },
220
+ justify: { type: String, default: "normal" }
221
+ },
222
+ setup(props, { slots }) {
223
+ return () => vue.h("div", {
224
+ class: "oods-grid",
225
+ "data-oods-component": "Grid",
226
+ "data-max-columns": maxColumns(props.columns),
227
+ style: {
228
+ "--oods-grid-columns": String(props.columns),
229
+ "--oods-grid-min-column": props.minColumnWidth,
230
+ "--oods-layout-gap": resolveGap(props.gap),
231
+ "--oods-layout-align": props.align,
232
+ "--oods-layout-justify": props.justify
233
+ }
234
+ }, slots.default?.());
235
+ }
236
+ });
237
+ vue.defineComponent({
238
+ name: "OodsStack",
239
+ props: {
240
+ direction: { type: String, default: "column" },
241
+ gap: { type: String, default: "md" },
242
+ align: { type: String, default: "stretch" },
243
+ justify: { type: String, default: "flex-start" },
244
+ wrap: Boolean
245
+ },
246
+ setup(props, { slots }) {
247
+ return () => vue.h("div", {
248
+ class: "oods-stack",
249
+ "data-oods-component": "Stack",
250
+ style: {
251
+ "--oods-stack-direction": props.direction,
252
+ "--oods-stack-wrap": props.wrap ? "wrap" : "nowrap",
253
+ "--oods-layout-gap": resolveGap(props.gap),
254
+ "--oods-layout-align": props.align,
255
+ "--oods-layout-justify": props.justify
256
+ }
257
+ }, slots.default?.());
258
+ }
259
+ });
260
+ var SAFE_TEXT_ELEMENTS = /* @__PURE__ */ new Set([
261
+ "span",
262
+ "p",
263
+ "strong",
264
+ "em",
265
+ "small",
266
+ "div",
267
+ "label",
268
+ "h1",
269
+ "h2",
270
+ "h3",
271
+ "h4",
272
+ "h5",
273
+ "h6"
274
+ ]);
275
+ vue.defineComponent({
276
+ name: "OodsText",
277
+ props: {
278
+ content: { type: [String, Number], default: "" },
279
+ label: String,
280
+ as: { type: String, default: "span" },
281
+ size: { type: sizeProp, default: "md" },
282
+ weight: { type: String, default: "normal" }
283
+ },
284
+ setup(props, { slots }) {
285
+ return () => vue.h(SAFE_TEXT_ELEMENTS.has(props.as) ? props.as : "span", {
286
+ class: "oods-text",
287
+ "data-oods-component": "Text",
288
+ "data-size": props.size,
289
+ "data-weight": props.weight,
290
+ "aria-description": props.label
291
+ }, [slotOrValue(slots.default, props.content)]);
292
+ }
293
+ });
294
+ function validationStyle(validation) {
295
+ if (!validation) return void 0;
296
+ const tone = validation.state === "error" ? "critical" : validation.state;
297
+ return {
298
+ "--cmp-input-message-border": `var(--sys-status-${tone}-border)`,
299
+ "--cmp-input-message-text": `var(--sys-status-${tone}-text)`
300
+ };
301
+ }
302
+ function useFieldMetadata(componentName, props, slots) {
303
+ const generatedId = vue.useId();
304
+ const controlId = vue.computed(() => props.id || `oods-${componentName.toLowerCase()}-${generatedId}`);
305
+ const helpId = vue.computed(() => `${controlId.value}-help`);
306
+ const validationId = vue.computed(() => `${controlId.value}-validation`);
307
+ const describedBy = vue.computed(() => {
308
+ const ids = [];
309
+ if (props.help || slots.help) ids.push(helpId.value);
310
+ if (props.validation?.message || slots.validation) ids.push(validationId.value);
311
+ return ids.length > 0 ? ids.join(" ") : void 0;
312
+ });
313
+ return { controlId, helpId, validationId, describedBy };
314
+ }
315
+ function renderField(componentName, props, slots, metadata, control) {
316
+ const label = slots.label?.() ?? props.label;
317
+ const help = slots.help?.() ?? props.help;
318
+ const validation = slots.validation?.() ?? props.validation?.message;
319
+ return vue.h("div", {
320
+ class: "oods-field",
321
+ "data-oods-component": componentName,
322
+ style: validationStyle(props.validation)
323
+ }, [
324
+ label ? vue.h("label", { class: "oods-field-label", for: metadata.controlId.value }, [label, props.required ? vue.h("span", { class: "oods-field-required", "aria-hidden": "true" }, "*") : null]) : null,
325
+ control,
326
+ help ? vue.h("p", { id: metadata.helpId.value, class: "oods-field-help" }, help) : null,
327
+ validation ? vue.h("p", {
328
+ id: metadata.validationId.value,
329
+ class: "oods-field-error",
330
+ role: props.validation?.state === "error" ? "alert" : void 0
331
+ }, validation) : null
332
+ ]);
333
+ }
334
+ var textFieldProps = {
335
+ id: String,
336
+ label: String,
337
+ modelValue: String,
338
+ value: String,
339
+ defaultValue: { type: String, default: "" },
340
+ placeholder: String,
341
+ required: Boolean,
342
+ disabled: Boolean,
343
+ readOnly: Boolean,
344
+ help: String,
345
+ validation: Object,
346
+ name: String,
347
+ min: String,
348
+ max: String,
349
+ step: [String, Number]
350
+ };
351
+ function eventValue(event) {
352
+ return event.target.value;
353
+ }
354
+ var Input = vue.defineComponent({
355
+ name: "OodsInput",
356
+ props: {
357
+ ...textFieldProps,
358
+ type: { type: String, default: "text" }
359
+ },
360
+ emits: {
361
+ "update:modelValue": (_value) => true,
362
+ input: (_value) => true,
363
+ change: (_value) => true
364
+ },
365
+ setup(props, { emit, slots }) {
366
+ const internalValue = vue.ref(props.defaultValue);
367
+ const currentValue = vue.computed(() => props.modelValue ?? props.value ?? internalValue.value);
368
+ const metadata = useFieldMetadata("Input", props, slots);
369
+ const updateValue = (nextValue) => {
370
+ if (props.modelValue === void 0 && props.value === void 0) internalValue.value = nextValue;
371
+ emit("update:modelValue", nextValue);
372
+ emit("input", nextValue);
373
+ };
374
+ return () => renderField("Input", props, slots, metadata, vue.h("input", {
375
+ id: metadata.controlId.value,
376
+ class: "oods-field-control",
377
+ type: props.type,
378
+ name: props.name,
379
+ value: props.type === "datetime-local" ? componentContracts.dateTimeInputValue(currentValue.value) : currentValue.value,
380
+ placeholder: props.placeholder,
381
+ required: props.required,
382
+ disabled: props.disabled,
383
+ readonly: props.readOnly,
384
+ min: props.min,
385
+ max: props.max,
386
+ step: props.step,
387
+ "aria-invalid": props.validation?.state === "error" ? "true" : void 0,
388
+ "aria-describedby": metadata.describedBy.value,
389
+ "aria-errormessage": props.validation?.state === "error" ? metadata.validationId.value : void 0,
390
+ onInput: (event) => updateValue(eventValue(event)),
391
+ onChange: (event) => emit("change", eventValue(event))
392
+ }));
393
+ }
394
+ });
395
+ vue.defineComponent({
396
+ name: "OodsDatePicker",
397
+ inheritAttrs: false,
398
+ props: textFieldProps,
399
+ emits: {
400
+ "update:modelValue": (_value) => true,
401
+ input: (_value) => true,
402
+ change: (_value) => true
403
+ },
404
+ setup(props, { attrs, emit, slots }) {
405
+ return () => vue.h("div", {
406
+ ...attrs,
407
+ class: ["oods-date-picker", attrs.class],
408
+ "data-oods-component": "DatePicker"
409
+ }, [vue.h(Input, {
410
+ ...props,
411
+ type: "date",
412
+ "onUpdate:modelValue": (value) => emit("update:modelValue", value),
413
+ onInput: (value) => emit("input", value),
414
+ onChange: (value) => emit("change", value)
415
+ }, slots)]);
416
+ }
417
+ });
418
+ vue.defineComponent({
419
+ name: "OodsTextarea",
420
+ props: {
421
+ ...textFieldProps,
422
+ rows: { type: Number, default: 3 }
423
+ },
424
+ emits: {
425
+ "update:modelValue": (_value) => true,
426
+ input: (_value) => true,
427
+ change: (_value) => true
428
+ },
429
+ setup(props, { emit, slots }) {
430
+ const internalValue = vue.ref(props.defaultValue);
431
+ const currentValue = vue.computed(() => props.modelValue ?? props.value ?? internalValue.value);
432
+ const metadata = useFieldMetadata("Textarea", props, slots);
433
+ const updateValue = (nextValue) => {
434
+ if (props.modelValue === void 0 && props.value === void 0) internalValue.value = nextValue;
435
+ emit("update:modelValue", nextValue);
436
+ emit("input", nextValue);
437
+ };
438
+ return () => renderField("Textarea", props, slots, metadata, vue.h("textarea", {
439
+ id: metadata.controlId.value,
440
+ class: "oods-field-control",
441
+ name: props.name,
442
+ value: currentValue.value,
443
+ rows: props.rows,
444
+ placeholder: props.placeholder,
445
+ required: props.required,
446
+ disabled: props.disabled,
447
+ readonly: props.readOnly,
448
+ "aria-invalid": props.validation?.state === "error" ? "true" : void 0,
449
+ "aria-describedby": metadata.describedBy.value,
450
+ "aria-errormessage": props.validation?.state === "error" ? metadata.validationId.value : void 0,
451
+ onInput: (event) => updateValue(eventValue(event)),
452
+ onChange: (event) => emit("change", eventValue(event))
453
+ }));
454
+ }
455
+ });
456
+ vue.defineComponent({
457
+ name: "OodsSelect",
458
+ props: {
459
+ id: String,
460
+ label: String,
461
+ modelValue: String,
462
+ value: String,
463
+ defaultValue: { type: String, default: "" },
464
+ placeholder: String,
465
+ required: Boolean,
466
+ disabled: Boolean,
467
+ help: String,
468
+ validation: Object,
469
+ name: String,
470
+ options: { type: Array, default: () => [] }
471
+ },
472
+ emits: {
473
+ "update:modelValue": (_value) => true,
474
+ change: (_value) => true
475
+ },
476
+ setup(props, { emit, slots }) {
477
+ const internalValue = vue.ref(props.defaultValue);
478
+ const currentValue = vue.computed(() => props.modelValue ?? props.value ?? internalValue.value);
479
+ const metadata = useFieldMetadata("Select", props, slots);
480
+ const selectValue = (nextValue) => {
481
+ if (props.modelValue === void 0 && props.value === void 0) internalValue.value = nextValue;
482
+ emit("update:modelValue", nextValue);
483
+ emit("change", nextValue);
484
+ };
485
+ return () => renderField("Select", props, slots, metadata, vue.h("select", {
486
+ id: metadata.controlId.value,
487
+ class: "oods-field-control",
488
+ name: props.name,
489
+ value: currentValue.value,
490
+ required: props.required,
491
+ disabled: props.disabled,
492
+ "aria-invalid": props.validation?.state === "error" ? "true" : void 0,
493
+ "aria-describedby": metadata.describedBy.value,
494
+ "aria-errormessage": props.validation?.state === "error" ? metadata.validationId.value : void 0,
495
+ onChange: (event) => selectValue(eventValue(event))
496
+ }, [
497
+ props.placeholder ? vue.h("option", {
498
+ value: "",
499
+ selected: currentValue.value === "",
500
+ disabled: true
501
+ }, props.placeholder) : null,
502
+ ...props.options.map((option) => vue.h("option", {
503
+ key: option.value,
504
+ value: option.value,
505
+ selected: option.value === currentValue.value,
506
+ disabled: option.disabled
507
+ }, slots.option?.({ option }) ?? option.label))
508
+ ]));
509
+ }
510
+ });
511
+ vue.defineComponent({
512
+ name: "OodsCheckbox",
513
+ props: {
514
+ id: String,
515
+ label: String,
516
+ modelValue: { type: Boolean, default: void 0 },
517
+ checked: { type: Boolean, default: void 0 },
518
+ defaultChecked: Boolean,
519
+ required: Boolean,
520
+ disabled: Boolean,
521
+ help: String,
522
+ validation: Object,
523
+ name: String
524
+ },
525
+ emits: {
526
+ "update:modelValue": (_value) => true,
527
+ change: (_value) => true
528
+ },
529
+ setup(props, { emit, slots }) {
530
+ const internalChecked = vue.ref(props.defaultChecked);
531
+ const currentChecked = vue.computed(() => props.modelValue ?? props.checked ?? internalChecked.value);
532
+ const metadata = useFieldMetadata("Checkbox", props, slots);
533
+ const change = (event) => {
534
+ const checked = event.target.checked;
535
+ if (props.modelValue === void 0 && props.checked === void 0) internalChecked.value = checked;
536
+ emit("update:modelValue", checked);
537
+ emit("change", checked);
538
+ };
539
+ return () => {
540
+ const label = slots.label?.() ?? props.label;
541
+ const help = slots.help?.() ?? props.help;
542
+ const validation = slots.validation?.() ?? props.validation?.message;
543
+ return vue.h("div", {
544
+ class: "oods-field",
545
+ "data-oods-component": "Checkbox",
546
+ style: validationStyle(props.validation)
547
+ }, [
548
+ vue.h("label", { class: "oods-checkbox", for: metadata.controlId.value }, [
549
+ vue.h("input", {
550
+ id: metadata.controlId.value,
551
+ type: "checkbox",
552
+ name: props.name,
553
+ checked: currentChecked.value,
554
+ required: props.required,
555
+ disabled: props.disabled,
556
+ "aria-invalid": props.validation?.state === "error" ? "true" : void 0,
557
+ "aria-describedby": metadata.describedBy.value,
558
+ "aria-errormessage": props.validation?.state === "error" ? metadata.validationId.value : void 0,
559
+ onChange: change
560
+ }),
561
+ vue.h("span", { class: "oods-field-label" }, [
562
+ label,
563
+ props.required ? vue.h("span", { class: "oods-field-required", "aria-hidden": "true" }, "*") : null
564
+ ])
565
+ ]),
566
+ help ? vue.h("p", { id: metadata.helpId.value, class: "oods-field-help" }, help) : null,
567
+ validation ? vue.h("p", {
568
+ id: metadata.validationId.value,
569
+ class: "oods-field-error",
570
+ role: props.validation?.state === "error" ? "alert" : void 0
571
+ }, validation) : null
572
+ ]);
573
+ };
574
+ }
575
+ });
576
+
577
+ // src/ported-core.ts
578
+ var STATUS_METADATA = {
579
+ subscription: {
580
+ future: {
581
+ description: "Subscription is scheduled but not yet active.",
582
+ tone: "info",
583
+ icon: "\u23F2"
584
+ },
585
+ trialing: {
586
+ description: "Subscription is in a trial period before billing begins.",
587
+ tone: "accent",
588
+ icon: "\u2605"
589
+ },
590
+ active: {
591
+ description: "Subscription is paid and service is provisioned.",
592
+ tone: "success",
593
+ icon: "\u2714\uFE0E"
594
+ },
595
+ paused: {
596
+ description: "Billing paused temporarily by finance or customer request.",
597
+ tone: "neutral",
598
+ icon: "\u23F8"
599
+ },
600
+ pending_cancellation: {
601
+ description: "Subscription will cancel at period end but remains active now.",
602
+ tone: "info",
603
+ icon: "\u2026"
604
+ },
605
+ past_due: {
606
+ description: "Renewal payment failed; smart retries ongoing and access retained during the grace window.",
607
+ tone: "critical",
608
+ icon: "\u26A0\uFE0E"
609
+ },
610
+ unpaid: {
611
+ description: "Payment retries exhausted; service access revoked.",
612
+ tone: "critical",
613
+ icon: "\u2A2F"
614
+ },
615
+ terminated: {
616
+ description: "Subscription ended; service access revoked and billing stopped.",
617
+ tone: "neutral",
618
+ icon: "\u2205"
619
+ }
620
+ },
621
+ invoice: {
622
+ draft: {
623
+ description: "Invoice is editable and not yet finalized.",
624
+ tone: "neutral",
625
+ icon: "\u270E"
626
+ },
627
+ posted: {
628
+ description: "Invoice finalized with terms before collection.",
629
+ tone: "info",
630
+ icon: "\u2026"
631
+ },
632
+ paid: {
633
+ description: "Invoice collected successfully in full.",
634
+ tone: "success",
635
+ icon: "\u2714\uFE0E"
636
+ },
637
+ past_due: {
638
+ description: "Invoice is unpaid past its due date.",
639
+ tone: "critical",
640
+ icon: "\u2A2F"
641
+ },
642
+ void: {
643
+ description: "Invoice canceled and no longer payable.",
644
+ tone: "neutral",
645
+ icon: "\u2205"
646
+ }
647
+ }
648
+ };
649
+ var STATUS_ALIASES = {
650
+ subscription: { delinquent: "unpaid" }
651
+ };
652
+ function isRecord(value) {
653
+ return typeof value === "object" && value !== null && !Array.isArray(value);
654
+ }
655
+ function firstText(record, keys) {
656
+ for (const key of keys) {
657
+ const value = record[key];
658
+ if (value === void 0 || value === null || value === "") continue;
659
+ if (typeof value === "string") return value;
660
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
661
+ }
662
+ return void 0;
663
+ }
664
+ function statusLabel2(status) {
665
+ return status.split(/[_-]/).filter(Boolean).map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)).join(" ");
666
+ }
667
+ function getPortedStatusPresentation(domain, status) {
668
+ const normalizedDomain = domain.toLowerCase();
669
+ const normalizedStatus = status.toLowerCase();
670
+ const alias = STATUS_ALIASES[normalizedDomain]?.[normalizedStatus];
671
+ const metadata = STATUS_METADATA[normalizedDomain]?.[alias ?? normalizedStatus];
672
+ if (!metadata) {
673
+ return {
674
+ label: statusLabel2(status || "Unknown"),
675
+ description: "Status not found in registry; falling back to the requested token set.",
676
+ tone: "neutral"
677
+ };
678
+ }
679
+ return { ...metadata, label: statusLabel2(status) };
680
+ }
681
+ function formatPrice(amountCents, amount, currency) {
682
+ const normalizedAmount = amount;
683
+ if (normalizedAmount === void 0 || !Number.isFinite(normalizedAmount)) return void 0;
684
+ const formatted = Number.isInteger(normalizedAmount) ? String(normalizedAmount) : normalizedAmount.toFixed(2);
685
+ return currency ? `${currency.toUpperCase()} ${formatted}` : formatted;
686
+ }
687
+ function normalizeTimelineEvents(raw) {
688
+ if (!Array.isArray(raw)) return [];
689
+ const events = [];
690
+ for (const entry of raw) {
691
+ if (entry === void 0 || entry === null) continue;
692
+ if (!isRecord(entry)) {
693
+ events.push({ label: String(entry) });
694
+ continue;
695
+ }
696
+ const from = firstText(entry, ["from"]);
697
+ const to = firstText(entry, ["to"]);
698
+ events.push({
699
+ label: firstText(entry, ["label", "title"]) ?? (firstText(entry, ["event", "status", "state"]) ? statusLabel2(firstText(entry, ["event", "status", "state"])) : void 0) ?? firstText(entry, ["text", "name"]) ?? (to ? from ? `${statusLabel2(from)} \u2192 ${statusLabel2(to)}` : statusLabel2(to) : void 0) ?? "Event",
700
+ timestamp: firstText(entry, ["timestamp", "datetime", "time", "at", "createdAt", "updatedAt"]),
701
+ detail: firstText(entry, ["detail", "description", "message"]) ?? (to ? from ? `${statusLabel2(from)} \u2192 ${statusLabel2(to)}` : statusLabel2(to) : void 0),
702
+ actorId: firstText(entry, ["actorId", "actor_id", "actor"]),
703
+ reason: firstText(entry, ["reason"])
704
+ });
705
+ }
706
+ return events;
707
+ }
708
+ function integerAtLeast(value, minimum, fallback) {
709
+ return Number.isFinite(value) ? Math.max(minimum, Math.floor(value)) : fallback;
710
+ }
711
+ function buildPaginationItems(options) {
712
+ const count = integerAtLeast(options.count, 0, 0);
713
+ const rawPage = integerAtLeast(options.page, 1, 1);
714
+ const page = count > 0 ? Math.min(rawPage, count) : 1;
715
+ const items = [{
716
+ type: "previous",
717
+ page: page > 1 ? page - 1 : page,
718
+ selected: false,
719
+ disabled: count === 0 || page <= 1
720
+ }];
721
+ if (count === 0) {
722
+ items.push({ type: "next", page: 1, selected: false, disabled: true });
723
+ return { page, count, items };
724
+ }
725
+ const siblingCount = integerAtLeast(options.siblingCount ?? 1, 0, 1);
726
+ const boundaryCount = integerAtLeast(options.boundaryCount ?? 1, 1, 1);
727
+ const candidates = count <= boundaryCount * 2 + siblingCount * 2 + 3 ? Array.from({ length: count }, (_, index) => index + 1) : [.../* @__PURE__ */ new Set([
728
+ ...Array.from({ length: boundaryCount }, (_, index) => index + 1),
729
+ ...Array.from({ length: siblingCount * 2 + 1 }, (_, index) => page - siblingCount + index),
730
+ ...Array.from({ length: boundaryCount }, (_, index) => count - boundaryCount + index + 1)
731
+ ])].filter((candidate) => candidate >= 1 && candidate <= count).sort((left, right) => left - right);
732
+ let priorPage;
733
+ let ellipsisIndex = 0;
734
+ for (const pageNumber of candidates) {
735
+ if (priorPage !== void 0 && pageNumber - priorPage > 1) {
736
+ items.push({
737
+ type: "ellipsis",
738
+ selected: false,
739
+ disabled: true,
740
+ index: ellipsisIndex++
741
+ });
742
+ }
743
+ items.push({
744
+ type: "page",
745
+ page: pageNumber,
746
+ selected: pageNumber === page,
747
+ disabled: false
748
+ });
749
+ priorPage = pageNumber;
750
+ }
751
+ items.push({
752
+ type: "next",
753
+ page: page < count ? page + 1 : page,
754
+ selected: false,
755
+ disabled: page >= count
756
+ });
757
+ return { page, count, items };
758
+ }
759
+ function relativeTimestampLabel(datetime, now) {
760
+ if (now === void 0) return datetime;
761
+ const timestamp = Date.parse(datetime);
762
+ const reference = now instanceof Date ? now.getTime() : typeof now === "number" ? now : Date.parse(now);
763
+ if (!Number.isFinite(timestamp) || !Number.isFinite(reference)) return datetime;
764
+ const seconds = Math.round((timestamp - reference) / 1e3);
765
+ const absoluteSeconds = Math.abs(seconds);
766
+ const formatter = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
767
+ if (absoluteSeconds < 60) return formatter.format(seconds, "second");
768
+ const minutes = Math.round(seconds / 60);
769
+ if (Math.abs(minutes) < 60) return formatter.format(minutes, "minute");
770
+ const hours = Math.round(minutes / 60);
771
+ if (Math.abs(hours) < 24) return formatter.format(hours, "hour");
772
+ return formatter.format(Math.round(hours / 24), "day");
773
+ }
774
+
775
+ // src/ported.ts
776
+ function stringValue(value) {
777
+ if (typeof value === "string") return value;
778
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
779
+ return void 0;
780
+ }
781
+ function firstDefined(...values) {
782
+ return values.find((value) => value !== void 0);
783
+ }
784
+ function firstArray(...values) {
785
+ return values.find((value) => Array.isArray(value)) ?? [];
786
+ }
787
+ function mergedClass(componentClass, value) {
788
+ return value ? [componentClass, value] : [componentClass];
789
+ }
790
+ var StatusBadge = vue.defineComponent({
791
+ name: "OodsStatusBadge",
792
+ inheritAttrs: false,
793
+ props: {
794
+ content: { type: [String, Number], default: void 0 },
795
+ status: String,
796
+ value: String,
797
+ label: String,
798
+ domain: { type: String, default: "subscription" },
799
+ tone: String,
800
+ emphasis: { type: String, default: "subtle" },
801
+ showIcon: { type: Boolean, default: true },
802
+ field: String,
803
+ statusField: String,
804
+ domainField: String,
805
+ readOnly: Boolean,
806
+ compact: Boolean,
807
+ variant: String
808
+ },
809
+ setup(props, { attrs, slots }) {
810
+ return () => {
811
+ const status = props.status ?? props.value ?? "unknown";
812
+ const presentation = getPortedStatusPresentation(props.domain, status);
813
+ const tone = props.tone === "lifecycle" ? presentation.tone : props.tone ?? presentation.tone;
814
+ const ariaLabel = stringValue(attrs["aria-label"]) ?? props.label ?? `Status: ${presentation.label}`;
815
+ return vue.h(Badge, {
816
+ ...attrs,
817
+ class: mergedClass("oods-status-badge", attrs.class),
818
+ content: slots.default ? void 0 : props.content ?? presentation.label,
819
+ status,
820
+ domain: props.domain,
821
+ tone,
822
+ emphasis: props.variant === "solid" ? "solid" : props.emphasis,
823
+ icon: props.showIcon ? presentation.icon : void 0,
824
+ title: stringValue(attrs.title) ?? presentation.description,
825
+ "aria-label": ariaLabel,
826
+ "data-oods-component": "StatusBadge"
827
+ }, slots);
828
+ };
829
+ }
830
+ });
831
+ function currencyLabel(amountCents, amount, currency, minorUnits) {
832
+ const normalizedAmount = amountCents !== void 0 ? amountCents / componentContracts.currencyMinorUnits(currency, minorUnits) : amount;
833
+ if (normalizedAmount === void 0 || !Number.isFinite(normalizedAmount)) return void 0;
834
+ if (!currency) return formatPrice(void 0, normalizedAmount, void 0);
835
+ try {
836
+ return new Intl.NumberFormat("en-US", {
837
+ style: "currency",
838
+ currency: currency.toUpperCase()
839
+ }).format(normalizedAmount);
840
+ } catch {
841
+ return formatPrice(void 0, normalizedAmount, currency);
842
+ }
843
+ }
844
+ var PriceBadge = vue.defineComponent({
845
+ name: "OodsPriceBadge",
846
+ inheritAttrs: false,
847
+ props: {
848
+ amountCents: Number,
849
+ unitAmountCents: Number,
850
+ minorUnits: Number,
851
+ amount: Number,
852
+ unitAmount: Number,
853
+ currency: String,
854
+ currencyCode: String,
855
+ label: String,
856
+ value: [String, Number],
857
+ emphasis: { type: String, default: "subtle" },
858
+ field: String,
859
+ amountField: String,
860
+ currencyField: String,
861
+ intervalField: String,
862
+ minorUnitsParameter: String
863
+ },
864
+ setup(props, { attrs, slots }) {
865
+ return () => {
866
+ const currency = props.currency ?? props.currencyCode;
867
+ const cents = props.amountCents ?? props.unitAmountCents;
868
+ const amount = props.amount ?? props.unitAmount;
869
+ const content = props.label ?? props.value ?? currencyLabel(cents, amount, currency, props.minorUnits) ?? "Price";
870
+ return vue.h(Badge, {
871
+ ...attrs,
872
+ class: mergedClass("oods-price-badge", attrs.class),
873
+ content,
874
+ emphasis: props.emphasis,
875
+ title: stringValue(attrs.title) ?? props.label,
876
+ "data-oods-component": "PriceBadge",
877
+ "data-price": "true",
878
+ "data-badge-variant": "price",
879
+ "data-currency": currency?.toUpperCase()
880
+ }, slots);
881
+ };
882
+ }
883
+ });
884
+ var timelineProps = {
885
+ title: String,
886
+ source: Array,
887
+ events: Array,
888
+ history: Array,
889
+ entries: Array,
890
+ stateHistory: Array,
891
+ auditLog: Array,
892
+ status: String,
893
+ allowedTransitions: Array,
894
+ showActorId: { type: Boolean, default: true },
895
+ showReason: { type: Boolean, default: true },
896
+ maxVisible: Number,
897
+ field: String,
898
+ historyField: String,
899
+ statesParameter: String,
900
+ auditLogField: String,
901
+ createdField: String,
902
+ updatedField: String,
903
+ eventField: String,
904
+ eventTimestampField: String,
905
+ eventOptionsParameter: String,
906
+ showFromState: Boolean
907
+ };
908
+ function renderTimelineEvent(event, index, showActorId, showReason) {
909
+ return vue.h("li", { key: `${event.timestamp ?? "event"}-${index}` }, [
910
+ vue.h("article", { "data-timeline-event": "true" }, [
911
+ vue.h("p", { "data-timeline-label": "true" }, event.label),
912
+ event.timestamp ? vue.h("time", { "data-timeline-time": "true", datetime: event.timestamp }, componentContracts.formatDateTime(event.timestamp)) : null,
913
+ event.detail ? vue.h("p", { "data-timeline-detail": "true" }, event.detail) : null,
914
+ showActorId && event.actorId ? vue.h("p", { "data-timeline-actor": "true" }, `Actor: ${event.actorId}`) : null,
915
+ showReason && event.reason ? vue.h("p", { "data-timeline-reason": "true" }, `Reason: ${event.reason}`) : null
916
+ ])
917
+ ]);
918
+ }
919
+ function timelineComponent(configuration) {
920
+ return vue.defineComponent({
921
+ name: `Oods${configuration.componentId}`,
922
+ inheritAttrs: false,
923
+ props: timelineProps,
924
+ setup(props, { attrs, slots }) {
925
+ return () => {
926
+ const rawEvents = configuration.componentId === "StatusTimeline" ? firstArray(props.source, props.events, props.history, props.entries, props.stateHistory) : firstArray(props.source, props.events, props.history, props.entries, props.auditLog);
927
+ const normalized = normalizeTimelineEvents(rawEvents);
928
+ const events = props.maxVisible === void 0 ? normalized : normalized.slice(0, Math.max(0, Math.floor(props.maxVisible)));
929
+ const title = props.title ?? configuration.defaultTitle;
930
+ const slottedEvents = slots.default?.();
931
+ return vue.h("div", {
932
+ ...attrs,
933
+ class: mergedClass("oods-timeline", attrs.class),
934
+ role: "log",
935
+ "aria-label": stringValue(attrs["aria-label"]) ?? title,
936
+ "data-oods-component": configuration.componentId,
937
+ "data-timeline-type": configuration.timelineType
938
+ }, [
939
+ vue.h("h2", { "data-timeline-title": "true" }, title),
940
+ props.status ? vue.h("p", { "data-timeline-current": "true" }, `Current status: ${statusLabel2(props.status)}`) : null,
941
+ props.allowedTransitions?.length ? vue.h("p", { "data-timeline-transitions": "true" }, `Allowed transitions: ${props.allowedTransitions.join(", ")}`) : null,
942
+ vue.h("ol", { "data-timeline-events": "true" }, slottedEvents?.length ? slottedEvents : events.length ? events.map((event, index) => renderTimelineEvent(
943
+ event,
944
+ index,
945
+ props.showActorId,
946
+ props.showReason
947
+ )) : [vue.h("li", { "data-timeline-empty": "true" }, "No events")])
948
+ ]);
949
+ };
950
+ }
951
+ });
952
+ }
953
+ var StatusTimeline = timelineComponent({
954
+ componentId: "StatusTimeline",
955
+ defaultTitle: "Status Timeline",
956
+ timelineType: "status"
957
+ });
958
+ var AuditTimeline = timelineComponent({
959
+ componentId: "AuditTimeline",
960
+ defaultTitle: "Audit Timeline",
961
+ timelineType: "audit"
962
+ });
963
+ function slotSummaryValue(slots) {
964
+ const nodes = slots.default?.() ?? [];
965
+ for (const node of nodes) {
966
+ if (typeof node.children === "string") {
967
+ const text = node.children.trim().toLowerCase();
968
+ if (text === "true") return true;
969
+ if (text === "false") return false;
970
+ if (node.children.length > 0) return node.children;
971
+ }
972
+ }
973
+ return void 0;
974
+ }
975
+ function summaryEntry(label, value, key) {
976
+ return vue.h("div", { key, "data-summary-item": "true" }, [
977
+ vue.h("dt", label),
978
+ vue.h("dd", [value])
979
+ ]);
980
+ }
981
+ var CancellationSummary = vue.defineComponent({
982
+ name: "OodsCancellationSummary",
983
+ inheritAttrs: false,
984
+ props: {
985
+ title: String,
986
+ label: String,
987
+ cancelAtPeriodEnd: { type: Boolean, default: void 0 },
988
+ requestedAt: String,
989
+ reason: String,
990
+ code: String,
991
+ field: String,
992
+ cancelAtPeriodEndField: String,
993
+ requestedAtField: String,
994
+ reasonField: String,
995
+ codeField: String
996
+ },
997
+ setup(props, { attrs, slots }) {
998
+ return () => {
999
+ const slotValue = slotSummaryValue(slots);
1000
+ const cancellationValue = props.cancelAtPeriodEnd ?? slotValue;
1001
+ const rows = [];
1002
+ if (cancellationValue !== void 0) {
1003
+ rows.push(summaryEntry(
1004
+ "Cancel at period end",
1005
+ typeof cancellationValue === "boolean" ? cancellationValue ? "Yes" : "No" : cancellationValue,
1006
+ "cancel-at-period-end"
1007
+ ));
1008
+ }
1009
+ if (props.requestedAt) {
1010
+ rows.push(summaryEntry(
1011
+ "Requested at",
1012
+ vue.h("time", { datetime: props.requestedAt }, componentContracts.formatDateTime(props.requestedAt)),
1013
+ "requested-at"
1014
+ ));
1015
+ }
1016
+ if (props.reason) rows.push(summaryEntry("Reason", props.reason, "reason"));
1017
+ if (props.code) rows.push(summaryEntry("Code", componentContracts.formatReadOnlyValue(props.code, "string", true), "code"));
1018
+ return vue.h("section", {
1019
+ ...attrs,
1020
+ class: mergedClass("oods-cancellation-summary", attrs.class),
1021
+ "data-oods-component": "CancellationSummary",
1022
+ "data-summary-type": "cancellation",
1023
+ "data-cancellation-state": props.cancelAtPeriodEnd === void 0 ? void 0 : String(props.cancelAtPeriodEnd)
1024
+ }, [
1025
+ vue.h("h2", { "data-summary-title": "true" }, props.title ?? props.label ?? "Cancellation Summary"),
1026
+ vue.h("dl", rows)
1027
+ ]);
1028
+ };
1029
+ }
1030
+ });
1031
+ function inputValue(event) {
1032
+ return event.target.value;
1033
+ }
1034
+ var SearchInput = vue.defineComponent({
1035
+ name: "OodsSearchInput",
1036
+ inheritAttrs: false,
1037
+ props: {
1038
+ id: String,
1039
+ label: String,
1040
+ modelValue: String,
1041
+ value: String,
1042
+ defaultValue: { type: String, default: "" },
1043
+ placeholder: { type: String, default: "Search\u2026" },
1044
+ clearable: { type: Boolean, default: true },
1045
+ debounceMs: { type: Number, default: 0 },
1046
+ debounce: Number,
1047
+ minQueryLength: { type: Number, default: 0 },
1048
+ disabled: Boolean,
1049
+ field: String,
1050
+ placeholderParameter: String,
1051
+ debounceParameter: String,
1052
+ minQueryLengthParameter: String,
1053
+ clearableParameter: String
1054
+ },
1055
+ emits: {
1056
+ "update:modelValue": (_value) => true,
1057
+ valueChange: (_value) => true,
1058
+ update: (_value) => true,
1059
+ input: (_value) => true,
1060
+ search: (_value) => true,
1061
+ clear: () => true
1062
+ },
1063
+ setup(props, { attrs, emit }) {
1064
+ const generatedId = vue.useId();
1065
+ const internalValue = vue.ref(props.defaultValue);
1066
+ const currentValue = vue.computed(() => props.modelValue ?? props.value ?? internalValue.value);
1067
+ let timer;
1068
+ vue.onBeforeUnmount(() => {
1069
+ if (timer !== void 0) clearTimeout(timer);
1070
+ });
1071
+ const emitSearch = (value) => {
1072
+ if (value.length < Math.max(0, props.minQueryLength) && value.length !== 0) return;
1073
+ emit("valueChange", value);
1074
+ emit("update", value);
1075
+ emit("search", value);
1076
+ };
1077
+ const scheduleSearch = (value) => {
1078
+ if (timer !== void 0) clearTimeout(timer);
1079
+ const debounceMs = Math.max(0, props.debounceMs || props.debounce || 0);
1080
+ if (debounceMs > 0) {
1081
+ timer = setTimeout(() => emitSearch(value), debounceMs);
1082
+ } else {
1083
+ emitSearch(value);
1084
+ }
1085
+ };
1086
+ const updateValue = (value) => {
1087
+ if (props.modelValue === void 0 && props.value === void 0) internalValue.value = value;
1088
+ emit("update:modelValue", value);
1089
+ emit("input", value);
1090
+ scheduleSearch(value);
1091
+ };
1092
+ const clear = () => {
1093
+ if (props.disabled || currentValue.value.length === 0) return;
1094
+ if (timer !== void 0) clearTimeout(timer);
1095
+ updateValue("");
1096
+ emit("clear");
1097
+ };
1098
+ const submit = (event) => {
1099
+ event.preventDefault();
1100
+ if (!props.disabled) {
1101
+ if (timer !== void 0) clearTimeout(timer);
1102
+ emitSearch(currentValue.value);
1103
+ }
1104
+ };
1105
+ return () => {
1106
+ const id = props.id ?? `oods-search-${generatedId.replace(/:/g, "")}`;
1107
+ const ariaLabel = stringValue(attrs["aria-label"]) ?? props.label ?? props.placeholder;
1108
+ return vue.h("form", {
1109
+ ...attrs,
1110
+ class: mergedClass("oods-search-input", attrs.class),
1111
+ role: "search",
1112
+ "aria-label": ariaLabel,
1113
+ "data-oods-component": "SearchInput",
1114
+ "data-behavioral": "search",
1115
+ onSubmit: submit
1116
+ }, [
1117
+ vue.h(Input, {
1118
+ id,
1119
+ label: props.label,
1120
+ type: "search",
1121
+ value: currentValue.value,
1122
+ placeholder: props.placeholder,
1123
+ disabled: props.disabled,
1124
+ "aria-label": props.label ? void 0 : ariaLabel,
1125
+ onInput: (value) => updateValue(value),
1126
+ onKeydown: (event) => {
1127
+ if (event.key === "Escape") clear();
1128
+ }
1129
+ }),
1130
+ props.clearable && currentValue.value ? vue.h("button", {
1131
+ type: "button",
1132
+ "data-search-clear": "true",
1133
+ "aria-label": "Clear search",
1134
+ disabled: props.disabled,
1135
+ onClick: clear
1136
+ }, "\xD7") : null
1137
+ ]);
1138
+ };
1139
+ }
1140
+ });
1141
+ var PaginationBar = vue.defineComponent({
1142
+ name: "OodsPaginationBar",
1143
+ inheritAttrs: false,
1144
+ props: {
1145
+ page: { type: Number, default: 1 },
1146
+ pageSize: { type: Number, default: 25 },
1147
+ totalItems: { type: Number, default: 0 },
1148
+ totalPages: Number,
1149
+ pageSizeOptions: { type: Array, default: () => [10, 25, 50, 100] },
1150
+ showPageSizeSelector: Boolean,
1151
+ showGotoPage: Boolean,
1152
+ showItemRange: { type: Boolean, default: true },
1153
+ pageField: String,
1154
+ pageSizeField: String,
1155
+ totalItemsField: String,
1156
+ totalPagesField: String,
1157
+ pageSizeOptionsParameter: String,
1158
+ showPageSizeSelectorParameter: String,
1159
+ showGotoPageParameter: String,
1160
+ showItemRangeParameter: String
1161
+ },
1162
+ emits: {
1163
+ "update:page": (_page) => true,
1164
+ pageChange: (_page) => true,
1165
+ change: (_page) => true,
1166
+ update: (_page) => true,
1167
+ pageSizeChange: (_pageSize) => true
1168
+ },
1169
+ setup(props, { attrs, emit }) {
1170
+ const generatedId = vue.useId().replace(/:/g, "");
1171
+ const gotoPage = vue.ref("");
1172
+ const count = vue.computed(() => props.totalPages ?? (props.pageSize > 0 ? Math.ceil(props.totalItems / props.pageSize) : 0));
1173
+ const pagination = vue.computed(() => buildPaginationItems({
1174
+ page: props.page,
1175
+ count: count.value
1176
+ }));
1177
+ const setPage = (page) => {
1178
+ if (pagination.value.count === 0) return;
1179
+ const nextPage = Math.max(1, Math.min(Math.floor(page), pagination.value.count));
1180
+ if (nextPage === pagination.value.page) return;
1181
+ emit("update:page", nextPage);
1182
+ emit("pageChange", nextPage);
1183
+ emit("change", nextPage);
1184
+ emit("update", nextPage);
1185
+ };
1186
+ const submitGoto = (event) => {
1187
+ event.preventDefault();
1188
+ const nextPage = Number(gotoPage.value);
1189
+ if (Number.isFinite(nextPage)) setPage(nextPage);
1190
+ };
1191
+ return () => {
1192
+ const { page, count: totalPages, items } = pagination.value;
1193
+ return vue.h("nav", {
1194
+ ...attrs,
1195
+ class: mergedClass("oods-pagination-bar", attrs.class),
1196
+ "aria-label": stringValue(attrs["aria-label"]) ?? "Pagination",
1197
+ "data-oods-component": "PaginationBar",
1198
+ "data-behavioral": "pagination"
1199
+ }, [
1200
+ props.showItemRange ? vue.h("span", { "data-pagination-count": "true" }, `${props.totalItems} ${props.totalItems === 1 ? "record" : "records"}`) : null,
1201
+ // s211-m02: on one page the range only repeats the count ("15 records Showing 1–15 of 15").
1202
+ props.showItemRange && props.totalItems > 0 && totalPages > 1 ? vue.h("span", { "data-pagination-range": "true" }, `Showing ${(page - 1) * Math.max(1, props.pageSize) + 1}\u2013${Math.min(page * Math.max(1, props.pageSize), props.totalItems)} of ${props.totalItems}`) : null,
1203
+ // s210-m01: one page needs no page controls; the count and range above already say what is shown.
1204
+ totalPages > 1 ? vue.h("ul", { class: "oods-pagination-bar__pages" }, items.map((item, index) => {
1205
+ if (item.type === "ellipsis") {
1206
+ return vue.h("li", { key: `ellipsis-${item.index ?? index}`, "aria-hidden": "true" }, "\u2026");
1207
+ }
1208
+ const label = item.type === "page" ? `Page ${item.page}` : item.type === "previous" ? "Previous page" : "Next page";
1209
+ const content = item.type === "page" ? String(item.page) : item.type === "previous" ? "\u2039" : "\u203A";
1210
+ return vue.h("li", { key: `${item.type}-${item.page}` }, [
1211
+ vue.h("button", {
1212
+ type: "button",
1213
+ "data-pagination-page": item.type === "page" ? item.page : void 0,
1214
+ "data-pagination-prev": item.type === "previous" ? "true" : void 0,
1215
+ "data-pagination-next": item.type === "next" ? "true" : void 0,
1216
+ "aria-label": label,
1217
+ "aria-current": item.selected ? "page" : void 0,
1218
+ disabled: item.disabled,
1219
+ onClick: () => item.page !== void 0 && setPage(item.page)
1220
+ }, content)
1221
+ ]);
1222
+ })) : null,
1223
+ totalPages > 1 ? vue.h("span", { "data-pagination-current": "true" }, `Page ${page} of ${Math.max(1, totalPages)}`) : null,
1224
+ props.showPageSizeSelector ? vue.h("label", { class: "oods-pagination-size" }, [
1225
+ vue.h("span", "Items per page"),
1226
+ vue.h("select", {
1227
+ value: props.pageSize,
1228
+ onChange: (event) => {
1229
+ const nextSize = Number(event.target.value);
1230
+ if (Number.isFinite(nextSize) && nextSize > 0 && nextSize !== props.pageSize) {
1231
+ emit("pageSizeChange", nextSize);
1232
+ }
1233
+ }
1234
+ }, props.pageSizeOptions.map((option) => vue.h("option", { value: option }, String(option))))
1235
+ ]) : null,
1236
+ props.showGotoPage ? vue.h("form", { class: "oods-pagination-goto", onSubmit: submitGoto }, [
1237
+ vue.h("label", { for: `oods-pagination-goto-${generatedId}` }, "Go to page"),
1238
+ vue.h("input", {
1239
+ id: `oods-pagination-goto-${generatedId}`,
1240
+ type: "number",
1241
+ min: 1,
1242
+ max: totalPages,
1243
+ value: gotoPage.value,
1244
+ onInput: (event) => {
1245
+ gotoPage.value = inputValue(event);
1246
+ }
1247
+ }),
1248
+ vue.h("button", { type: "submit" }, "Go")
1249
+ ]) : null
1250
+ ]);
1251
+ };
1252
+ }
1253
+ });
1254
+ var RelativeTimestamp = vue.defineComponent({
1255
+ name: "OodsRelativeTimestamp",
1256
+ inheritAttrs: false,
1257
+ props: {
1258
+ datetime: String,
1259
+ timestamp: String,
1260
+ value: String,
1261
+ updatedAt: String,
1262
+ createdAt: String,
1263
+ relative: String,
1264
+ label: String,
1265
+ text: String,
1266
+ timezone: String,
1267
+ hideWhenEmpty: Boolean,
1268
+ now: [String, Number, Date],
1269
+ field: String,
1270
+ fallbackField: String,
1271
+ timezoneParameter: String
1272
+ },
1273
+ setup(props, { attrs }) {
1274
+ return () => {
1275
+ const datetime = firstDefined(
1276
+ props.datetime,
1277
+ props.timestamp,
1278
+ props.value,
1279
+ props.updatedAt,
1280
+ props.createdAt
1281
+ ) ?? "";
1282
+ if (props.hideWhenEmpty && (!datetime || Number.isNaN(Date.parse(datetime)))) return null;
1283
+ const content = firstDefined(
1284
+ props.relative,
1285
+ props.label,
1286
+ props.text,
1287
+ props.now === void 0 ? componentContracts.formatDateTime(datetime, { timeZone: props.timezone }) : relativeTimestampLabel(datetime, props.now)
1288
+ ) ?? "";
1289
+ return vue.h("time", {
1290
+ ...attrs,
1291
+ class: mergedClass("oods-relative-timestamp", attrs.class),
1292
+ datetime: datetime || void 0,
1293
+ title: stringValue(attrs.title) ?? componentContracts.formatDateTime(datetime, { timeZone: props.timezone }),
1294
+ "data-oods-component": "RelativeTimestamp"
1295
+ }, content);
1296
+ };
1297
+ }
1298
+ });
1299
+
1300
+ exports.AuditTimeline = AuditTimeline;
1301
+ exports.CancellationSummary = CancellationSummary;
1302
+ exports.PaginationBar = PaginationBar;
1303
+ exports.PriceBadge = PriceBadge;
1304
+ exports.RelativeTimestamp = RelativeTimestamp;
1305
+ exports.SearchInput = SearchInput;
1306
+ exports.StatusBadge = StatusBadge;
1307
+ exports.StatusTimeline = StatusTimeline;