@oods/components-vue 0.3.3 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ported.js CHANGED
@@ -1,7 +1,12 @@
1
- import { dateTimeInputValue, formatDateTime, formatReadOnlyValue, currencyMinorUnits } from '@oods/component-contracts';
2
- import { defineComponent, h, ref, computed, useId, onBeforeUnmount } from 'vue';
1
+ import { dateTimeInputValue, formatDateTime, formatReadOnlyValue, STATUS_ICON_SVG_ATTRS, STATUS_ICON_SHAPES, getStatusPresentation, resolveStatusIcon, currencyMinorUnits } from '@oods/component-contracts';
2
+ import { defineComponent, h, useId, computed, ref, onMounted, onBeforeUnmount, watch } from 'vue';
3
3
 
4
4
  // src/ported.ts
5
+ function statusIcon(name) {
6
+ return h("svg", { ...STATUS_ICON_SVG_ATTRS }, STATUS_ICON_SHAPES[name].map((shape) => h(shape.tag, { ...shape.attrs })));
7
+ }
8
+
9
+ // src/primitives.ts
5
10
  var toneProp = String;
6
11
  var emphasisProp = String;
7
12
  var sizeProp = String;
@@ -142,7 +147,7 @@ defineComponent({
142
147
  class: "oods-banner-dismiss oods-banner__dismiss",
143
148
  "aria-label": props.dismissLabel,
144
149
  onClick: () => emit("dismiss")
145
- }, [h("span", { "aria-hidden": "true" }, "\xD7")]) : null
150
+ }, [statusIcon("x")]) : null
146
151
  ]);
147
152
  };
148
153
  }
@@ -152,7 +157,7 @@ defineComponent({
152
157
  props: {
153
158
  content: { type: [String, Number], default: "" },
154
159
  intent: { type: String, default: "neutral" },
155
- size: { type: sizeProp, default: "md" },
160
+ size: { type: String, default: "md" },
156
161
  disabled: Boolean,
157
162
  type: { type: String, default: "button" }
158
163
  },
@@ -179,6 +184,130 @@ defineComponent({
179
184
  }, [slotOrValue(slots.default, props.content)]);
180
185
  }
181
186
  });
187
+ var canShowModal = () => typeof HTMLDialogElement !== "undefined" && typeof HTMLDialogElement.prototype.showModal === "function";
188
+ var pageLocks = 0;
189
+ var pageStyle;
190
+ function lockPageScroll() {
191
+ const page = document.documentElement;
192
+ if (pageLocks++ === 0) {
193
+ const scrollbar = window.innerWidth - page.clientWidth;
194
+ pageStyle = { overflow: page.style.overflow, paddingRight: page.style.paddingRight };
195
+ if (scrollbar > 0) page.style.paddingRight = `${parseFloat(getComputedStyle(page).paddingRight) + scrollbar}px`;
196
+ page.style.overflow = "hidden";
197
+ }
198
+ let held = true;
199
+ return () => {
200
+ if (!held) return;
201
+ held = false;
202
+ if (--pageLocks === 0 && pageStyle) {
203
+ page.style.overflow = pageStyle.overflow;
204
+ page.style.paddingRight = pageStyle.paddingRight;
205
+ pageStyle = void 0;
206
+ }
207
+ };
208
+ }
209
+ var outsideBox = (event, dialog) => {
210
+ const box = dialog.getBoundingClientRect();
211
+ return event.clientX < box.left || event.clientX > box.right || event.clientY < box.top || event.clientY > box.bottom;
212
+ };
213
+ defineComponent({
214
+ name: "OodsDialog",
215
+ props: {
216
+ id: String,
217
+ open: Boolean,
218
+ modal: { type: Boolean, default: true },
219
+ title: { type: String, default: "" },
220
+ description: String,
221
+ dismissLabel: { type: String, default: "Close" },
222
+ size: { type: String, default: "md" },
223
+ actions: { type: [String, Number, Object, Array], default: void 0 }
224
+ },
225
+ emits: {
226
+ close: () => true
227
+ },
228
+ setup(props, { emit, slots }) {
229
+ const generatedId = useId().replace(/[^\w-]/g, "");
230
+ const dialogId = computed(() => props.id || `oods-dialog-${generatedId}`);
231
+ const element = ref(null);
232
+ const mounted = ref(false);
233
+ const managed = computed(() => props.modal && mounted.value && canShowModal());
234
+ let unlockScroll;
235
+ let pressedBackdrop = false;
236
+ const releaseScroll = () => {
237
+ unlockScroll?.();
238
+ unlockScroll = void 0;
239
+ };
240
+ const sync = () => {
241
+ const dialog = element.value;
242
+ if (!dialog || !managed.value) return;
243
+ if (props.open && !dialog.open) {
244
+ dialog.showModal();
245
+ unlockScroll ??= lockPageScroll();
246
+ } else if (!props.open && dialog.open) dialog.close();
247
+ };
248
+ onMounted(() => {
249
+ mounted.value = true;
250
+ });
251
+ onBeforeUnmount(releaseScroll);
252
+ watch([() => props.open, managed], sync, { flush: "post" });
253
+ const requestClose = (event) => {
254
+ event.preventDefault();
255
+ emit("close");
256
+ };
257
+ return () => {
258
+ const titleId = `${dialogId.value}-title`;
259
+ const descriptionId = `${dialogId.value}-description`;
260
+ const actions = slotOrValue(slots.actions, props.actions);
261
+ return h("dialog", {
262
+ ref: element,
263
+ id: dialogId.value,
264
+ class: "oods-dialog",
265
+ "data-oods-component": "Dialog",
266
+ "data-size": props.size,
267
+ "aria-labelledby": titleId,
268
+ "aria-describedby": props.description ? descriptionId : void 0,
269
+ open: managed.value ? void 0 : props.open,
270
+ onKeydown: (event) => {
271
+ if (event.key === "Escape" && !event.defaultPrevented) requestClose(event);
272
+ },
273
+ onCancel: requestClose,
274
+ // A close event from moving a mounted dialog into the top layer finds it open again, and is not reported. Any other
275
+ // close releases the page; when the browser closed it (a close request it would not let be cancelled) while open is
276
+ // still true, it is reported.
277
+ onClose: () => {
278
+ if (element.value?.open) return;
279
+ releaseScroll();
280
+ if (props.open) emit("close");
281
+ },
282
+ onMousedown: (event) => {
283
+ pressedBackdrop = managed.value && event.target === event.currentTarget && outsideBox(event, event.currentTarget);
284
+ },
285
+ // s223-m02 (#2527 ruling 13b): a click on the backdrop emits close, as the close control does. A press that began
286
+ // inside the dialog (text selected and dragged out) is not a backdrop click.
287
+ onClick: (event) => {
288
+ const backdrop = pressedBackdrop && event.target === event.currentTarget && outsideBox(event, event.currentTarget);
289
+ pressedBackdrop = false;
290
+ if (backdrop) emit("close");
291
+ }
292
+ }, [
293
+ h("div", { class: "oods-dialog__header" }, [
294
+ h("div", { class: "oods-dialog__heading" }, [
295
+ h("h2", { id: titleId, class: "oods-dialog__title" }, props.title),
296
+ props.description ? h("p", { id: descriptionId, class: "oods-dialog__description" }, props.description) : null
297
+ ]),
298
+ h("button", {
299
+ type: "button",
300
+ class: "oods-dialog__close",
301
+ "aria-label": props.dismissLabel,
302
+ onClick: () => emit("close")
303
+ }, [statusIcon("x")])
304
+ ]),
305
+ slots.default ? h("div", { class: "oods-dialog__body" }, slots.default()) : null,
306
+ actions !== void 0 && actions !== null && actions !== "" ? h("div", { class: "oods-dialog__footer" }, [actions]) : null
307
+ ]);
308
+ };
309
+ }
310
+ });
182
311
  var SAFE_CARD_ELEMENTS = /* @__PURE__ */ new Set(["div", "article", "section", "aside"]);
183
312
  defineComponent({
184
313
  name: "OodsCard",
@@ -573,82 +702,75 @@ defineComponent({
573
702
  };
574
703
  }
575
704
  });
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
- }
705
+ defineComponent({
706
+ name: "OodsSwitch",
707
+ props: {
708
+ id: String,
709
+ label: String,
710
+ modelValue: { type: Boolean, default: void 0 },
711
+ checked: { type: Boolean, default: void 0 },
712
+ defaultChecked: Boolean,
713
+ required: Boolean,
714
+ disabled: Boolean,
715
+ help: String,
716
+ validation: Object,
717
+ size: { type: String, default: "md" }
620
718
  },
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
- }
719
+ emits: {
720
+ "update:modelValue": (_value) => true,
721
+ change: (_value) => true
722
+ },
723
+ setup(props, { emit, slots }) {
724
+ const internalChecked = ref(props.defaultChecked);
725
+ const currentChecked = computed(() => props.modelValue ?? props.checked ?? internalChecked.value);
726
+ const metadata = useFieldMetadata("Switch", props, slots);
727
+ const toggle = () => {
728
+ const next = !currentChecked.value;
729
+ if (props.modelValue === void 0 && props.checked === void 0) internalChecked.value = next;
730
+ emit("update:modelValue", next);
731
+ emit("change", next);
732
+ };
733
+ return () => {
734
+ const label = slots.label?.() ?? props.label;
735
+ const help = slots.help?.() ?? props.help;
736
+ const validation = slots.validation?.() ?? props.validation?.message;
737
+ const controlId = metadata.controlId.value;
738
+ return h("div", {
739
+ class: "oods-field oods-switch",
740
+ style: validationStyle(props.validation),
741
+ "data-oods-component": "Switch",
742
+ "data-size": props.size,
743
+ "data-validation-state": props.validation?.state
744
+ }, [
745
+ h("div", { class: "oods-switch__row" }, [
746
+ h("button", {
747
+ id: controlId,
748
+ type: "button",
749
+ role: "switch",
750
+ class: "oods-switch__control",
751
+ "aria-checked": currentChecked.value ? "true" : "false",
752
+ "aria-labelledby": `${controlId}-label`,
753
+ "aria-describedby": metadata.describedBy.value,
754
+ "aria-required": props.required ? "true" : void 0,
755
+ "aria-invalid": props.validation?.state === "error" ? "true" : void 0,
756
+ disabled: props.disabled,
757
+ onClick: toggle
758
+ }, [h("span", { class: "oods-switch__thumb", "aria-hidden": "true" })]),
759
+ h("label", { id: `${controlId}-label`, class: "oods-field-label oods-switch__label", for: controlId }, [
760
+ label,
761
+ props.required ? h("span", { class: "oods-field-required", "aria-hidden": "true" }, "*") : null
762
+ ])
763
+ ]),
764
+ help ? h("p", { id: metadata.helpId.value, class: "oods-field-help" }, help) : null,
765
+ validation ? h("p", {
766
+ id: metadata.validationId.value,
767
+ class: "oods-field-error",
768
+ role: props.validation?.state === "error" ? "alert" : void 0
769
+ }, validation) : null
770
+ ]);
771
+ };
647
772
  }
648
- };
649
- var STATUS_ALIASES = {
650
- subscription: { delinquent: "unpaid" }
651
- };
773
+ });
652
774
  function isRecord(value) {
653
775
  return typeof value === "object" && value !== null && !Array.isArray(value);
654
776
  }
@@ -665,18 +787,13 @@ function statusLabel2(status) {
665
787
  return status.split(/[_-]/).filter(Boolean).map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)).join(" ");
666
788
  }
667
789
  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) };
790
+ const presentation = getStatusPresentation(domain.toLowerCase(), status);
791
+ return {
792
+ label: statusLabel2(status || "Unknown"),
793
+ description: presentation.description,
794
+ tone: presentation.tone,
795
+ icon: resolveStatusIcon(presentation.iconName)
796
+ };
680
797
  }
681
798
  function formatPrice(amountCents, amount, currency) {
682
799
  const normalizedAmount = amount;
@@ -820,11 +937,10 @@ var StatusBadge = defineComponent({
820
937
  domain: props.domain,
821
938
  tone,
822
939
  emphasis: props.variant === "solid" ? "solid" : props.emphasis,
823
- icon: props.showIcon ? presentation.icon : void 0,
824
940
  title: stringValue(attrs.title) ?? presentation.description,
825
941
  "aria-label": ariaLabel,
826
942
  "data-oods-component": "StatusBadge"
827
- }, slots);
943
+ }, props.showIcon && presentation.icon && !slots.icon ? { ...slots, icon: () => statusIcon(presentation.icon) } : slots);
828
944
  };
829
945
  }
830
946
  });
@@ -1137,7 +1253,7 @@ var SearchInput = defineComponent({
1137
1253
  "aria-label": "Clear search",
1138
1254
  disabled: props.disabled,
1139
1255
  onClick: clear
1140
- }, "\xD7") : null
1256
+ }, [statusIcon("x")]) : null
1141
1257
  ]);
1142
1258
  };
1143
1259
  }
@@ -1284,17 +1400,18 @@ var RelativeTimestamp = defineComponent({
1284
1400
  props.createdAt
1285
1401
  ) ?? "";
1286
1402
  if (props.hideWhenEmpty && (!datetime || Number.isNaN(Date.parse(datetime)))) return null;
1403
+ const absolute = formatDateTime(datetime, { timeZone: props.timezone });
1287
1404
  const content = firstDefined(
1288
1405
  props.relative,
1289
1406
  props.label,
1290
1407
  props.text,
1291
- props.now === void 0 ? formatDateTime(datetime, { timeZone: props.timezone }) : relativeTimestampLabel(datetime, props.now)
1408
+ !absolute ? "Unknown time" : props.now === void 0 ? absolute : relativeTimestampLabel(datetime, props.now)
1292
1409
  ) ?? "";
1293
1410
  return h("time", {
1294
1411
  ...attrs,
1295
1412
  class: mergedClass("oods-relative-timestamp", attrs.class),
1296
1413
  datetime: datetime || void 0,
1297
- title: stringValue(attrs.title) ?? formatDateTime(datetime, { timeZone: props.timezone }),
1414
+ title: stringValue(attrs.title) ?? absolute,
1298
1415
  "data-oods-component": "RelativeTimestamp"
1299
1416
  }, content);
1300
1417
  };