@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.cjs CHANGED
@@ -4,6 +4,11 @@ var componentContracts = require('@oods/component-contracts');
4
4
  var vue = require('vue');
5
5
 
6
6
  // src/ported.ts
7
+ function statusIcon(name) {
8
+ return vue.h("svg", { ...componentContracts.STATUS_ICON_SVG_ATTRS }, componentContracts.STATUS_ICON_SHAPES[name].map((shape) => vue.h(shape.tag, { ...shape.attrs })));
9
+ }
10
+
11
+ // src/primitives.ts
7
12
  var toneProp = String;
8
13
  var emphasisProp = String;
9
14
  var sizeProp = String;
@@ -144,7 +149,7 @@ vue.defineComponent({
144
149
  class: "oods-banner-dismiss oods-banner__dismiss",
145
150
  "aria-label": props.dismissLabel,
146
151
  onClick: () => emit("dismiss")
147
- }, [vue.h("span", { "aria-hidden": "true" }, "\xD7")]) : null
152
+ }, [statusIcon("x")]) : null
148
153
  ]);
149
154
  };
150
155
  }
@@ -154,7 +159,7 @@ vue.defineComponent({
154
159
  props: {
155
160
  content: { type: [String, Number], default: "" },
156
161
  intent: { type: String, default: "neutral" },
157
- size: { type: sizeProp, default: "md" },
162
+ size: { type: String, default: "md" },
158
163
  disabled: Boolean,
159
164
  type: { type: String, default: "button" }
160
165
  },
@@ -181,6 +186,130 @@ vue.defineComponent({
181
186
  }, [slotOrValue(slots.default, props.content)]);
182
187
  }
183
188
  });
189
+ var canShowModal = () => typeof HTMLDialogElement !== "undefined" && typeof HTMLDialogElement.prototype.showModal === "function";
190
+ var pageLocks = 0;
191
+ var pageStyle;
192
+ function lockPageScroll() {
193
+ const page = document.documentElement;
194
+ if (pageLocks++ === 0) {
195
+ const scrollbar = window.innerWidth - page.clientWidth;
196
+ pageStyle = { overflow: page.style.overflow, paddingRight: page.style.paddingRight };
197
+ if (scrollbar > 0) page.style.paddingRight = `${parseFloat(getComputedStyle(page).paddingRight) + scrollbar}px`;
198
+ page.style.overflow = "hidden";
199
+ }
200
+ let held = true;
201
+ return () => {
202
+ if (!held) return;
203
+ held = false;
204
+ if (--pageLocks === 0 && pageStyle) {
205
+ page.style.overflow = pageStyle.overflow;
206
+ page.style.paddingRight = pageStyle.paddingRight;
207
+ pageStyle = void 0;
208
+ }
209
+ };
210
+ }
211
+ var outsideBox = (event, dialog) => {
212
+ const box = dialog.getBoundingClientRect();
213
+ return event.clientX < box.left || event.clientX > box.right || event.clientY < box.top || event.clientY > box.bottom;
214
+ };
215
+ vue.defineComponent({
216
+ name: "OodsDialog",
217
+ props: {
218
+ id: String,
219
+ open: Boolean,
220
+ modal: { type: Boolean, default: true },
221
+ title: { type: String, default: "" },
222
+ description: String,
223
+ dismissLabel: { type: String, default: "Close" },
224
+ size: { type: String, default: "md" },
225
+ actions: { type: [String, Number, Object, Array], default: void 0 }
226
+ },
227
+ emits: {
228
+ close: () => true
229
+ },
230
+ setup(props, { emit, slots }) {
231
+ const generatedId = vue.useId().replace(/[^\w-]/g, "");
232
+ const dialogId = vue.computed(() => props.id || `oods-dialog-${generatedId}`);
233
+ const element = vue.ref(null);
234
+ const mounted = vue.ref(false);
235
+ const managed = vue.computed(() => props.modal && mounted.value && canShowModal());
236
+ let unlockScroll;
237
+ let pressedBackdrop = false;
238
+ const releaseScroll = () => {
239
+ unlockScroll?.();
240
+ unlockScroll = void 0;
241
+ };
242
+ const sync = () => {
243
+ const dialog = element.value;
244
+ if (!dialog || !managed.value) return;
245
+ if (props.open && !dialog.open) {
246
+ dialog.showModal();
247
+ unlockScroll ??= lockPageScroll();
248
+ } else if (!props.open && dialog.open) dialog.close();
249
+ };
250
+ vue.onMounted(() => {
251
+ mounted.value = true;
252
+ });
253
+ vue.onBeforeUnmount(releaseScroll);
254
+ vue.watch([() => props.open, managed], sync, { flush: "post" });
255
+ const requestClose = (event) => {
256
+ event.preventDefault();
257
+ emit("close");
258
+ };
259
+ return () => {
260
+ const titleId = `${dialogId.value}-title`;
261
+ const descriptionId = `${dialogId.value}-description`;
262
+ const actions = slotOrValue(slots.actions, props.actions);
263
+ return vue.h("dialog", {
264
+ ref: element,
265
+ id: dialogId.value,
266
+ class: "oods-dialog",
267
+ "data-oods-component": "Dialog",
268
+ "data-size": props.size,
269
+ "aria-labelledby": titleId,
270
+ "aria-describedby": props.description ? descriptionId : void 0,
271
+ open: managed.value ? void 0 : props.open,
272
+ onKeydown: (event) => {
273
+ if (event.key === "Escape" && !event.defaultPrevented) requestClose(event);
274
+ },
275
+ onCancel: requestClose,
276
+ // A close event from moving a mounted dialog into the top layer finds it open again, and is not reported. Any other
277
+ // close releases the page; when the browser closed it (a close request it would not let be cancelled) while open is
278
+ // still true, it is reported.
279
+ onClose: () => {
280
+ if (element.value?.open) return;
281
+ releaseScroll();
282
+ if (props.open) emit("close");
283
+ },
284
+ onMousedown: (event) => {
285
+ pressedBackdrop = managed.value && event.target === event.currentTarget && outsideBox(event, event.currentTarget);
286
+ },
287
+ // s223-m02 (#2527 ruling 13b): a click on the backdrop emits close, as the close control does. A press that began
288
+ // inside the dialog (text selected and dragged out) is not a backdrop click.
289
+ onClick: (event) => {
290
+ const backdrop = pressedBackdrop && event.target === event.currentTarget && outsideBox(event, event.currentTarget);
291
+ pressedBackdrop = false;
292
+ if (backdrop) emit("close");
293
+ }
294
+ }, [
295
+ vue.h("div", { class: "oods-dialog__header" }, [
296
+ vue.h("div", { class: "oods-dialog__heading" }, [
297
+ vue.h("h2", { id: titleId, class: "oods-dialog__title" }, props.title),
298
+ props.description ? vue.h("p", { id: descriptionId, class: "oods-dialog__description" }, props.description) : null
299
+ ]),
300
+ vue.h("button", {
301
+ type: "button",
302
+ class: "oods-dialog__close",
303
+ "aria-label": props.dismissLabel,
304
+ onClick: () => emit("close")
305
+ }, [statusIcon("x")])
306
+ ]),
307
+ slots.default ? vue.h("div", { class: "oods-dialog__body" }, slots.default()) : null,
308
+ actions !== void 0 && actions !== null && actions !== "" ? vue.h("div", { class: "oods-dialog__footer" }, [actions]) : null
309
+ ]);
310
+ };
311
+ }
312
+ });
184
313
  var SAFE_CARD_ELEMENTS = /* @__PURE__ */ new Set(["div", "article", "section", "aside"]);
185
314
  vue.defineComponent({
186
315
  name: "OodsCard",
@@ -575,82 +704,75 @@ vue.defineComponent({
575
704
  };
576
705
  }
577
706
  });
578
-
579
- // src/ported-core.ts
580
- var STATUS_METADATA = {
581
- subscription: {
582
- future: {
583
- description: "Subscription is scheduled but not yet active.",
584
- tone: "info",
585
- icon: "\u23F2"
586
- },
587
- trialing: {
588
- description: "Subscription is in a trial period before billing begins.",
589
- tone: "accent",
590
- icon: "\u2605"
591
- },
592
- active: {
593
- description: "Subscription is paid and service is provisioned.",
594
- tone: "success",
595
- icon: "\u2714\uFE0E"
596
- },
597
- paused: {
598
- description: "Billing paused temporarily by finance or customer request.",
599
- tone: "neutral",
600
- icon: "\u23F8"
601
- },
602
- pending_cancellation: {
603
- description: "Subscription will cancel at period end but remains active now.",
604
- tone: "info",
605
- icon: "\u2026"
606
- },
607
- past_due: {
608
- description: "Renewal payment failed; smart retries ongoing and access retained during the grace window.",
609
- tone: "critical",
610
- icon: "\u26A0\uFE0E"
611
- },
612
- unpaid: {
613
- description: "Payment retries exhausted; service access revoked.",
614
- tone: "critical",
615
- icon: "\u2A2F"
616
- },
617
- terminated: {
618
- description: "Subscription ended; service access revoked and billing stopped.",
619
- tone: "neutral",
620
- icon: "\u2205"
621
- }
707
+ vue.defineComponent({
708
+ name: "OodsSwitch",
709
+ props: {
710
+ id: String,
711
+ label: String,
712
+ modelValue: { type: Boolean, default: void 0 },
713
+ checked: { type: Boolean, default: void 0 },
714
+ defaultChecked: Boolean,
715
+ required: Boolean,
716
+ disabled: Boolean,
717
+ help: String,
718
+ validation: Object,
719
+ size: { type: String, default: "md" }
622
720
  },
623
- invoice: {
624
- draft: {
625
- description: "Invoice is editable and not yet finalized.",
626
- tone: "neutral",
627
- icon: "\u270E"
628
- },
629
- posted: {
630
- description: "Invoice finalized with terms before collection.",
631
- tone: "info",
632
- icon: "\u2026"
633
- },
634
- paid: {
635
- description: "Invoice collected successfully in full.",
636
- tone: "success",
637
- icon: "\u2714\uFE0E"
638
- },
639
- past_due: {
640
- description: "Invoice is unpaid past its due date.",
641
- tone: "critical",
642
- icon: "\u2A2F"
643
- },
644
- void: {
645
- description: "Invoice canceled and no longer payable.",
646
- tone: "neutral",
647
- icon: "\u2205"
648
- }
721
+ emits: {
722
+ "update:modelValue": (_value) => true,
723
+ change: (_value) => true
724
+ },
725
+ setup(props, { emit, slots }) {
726
+ const internalChecked = vue.ref(props.defaultChecked);
727
+ const currentChecked = vue.computed(() => props.modelValue ?? props.checked ?? internalChecked.value);
728
+ const metadata = useFieldMetadata("Switch", props, slots);
729
+ const toggle = () => {
730
+ const next = !currentChecked.value;
731
+ if (props.modelValue === void 0 && props.checked === void 0) internalChecked.value = next;
732
+ emit("update:modelValue", next);
733
+ emit("change", next);
734
+ };
735
+ return () => {
736
+ const label = slots.label?.() ?? props.label;
737
+ const help = slots.help?.() ?? props.help;
738
+ const validation = slots.validation?.() ?? props.validation?.message;
739
+ const controlId = metadata.controlId.value;
740
+ return vue.h("div", {
741
+ class: "oods-field oods-switch",
742
+ style: validationStyle(props.validation),
743
+ "data-oods-component": "Switch",
744
+ "data-size": props.size,
745
+ "data-validation-state": props.validation?.state
746
+ }, [
747
+ vue.h("div", { class: "oods-switch__row" }, [
748
+ vue.h("button", {
749
+ id: controlId,
750
+ type: "button",
751
+ role: "switch",
752
+ class: "oods-switch__control",
753
+ "aria-checked": currentChecked.value ? "true" : "false",
754
+ "aria-labelledby": `${controlId}-label`,
755
+ "aria-describedby": metadata.describedBy.value,
756
+ "aria-required": props.required ? "true" : void 0,
757
+ "aria-invalid": props.validation?.state === "error" ? "true" : void 0,
758
+ disabled: props.disabled,
759
+ onClick: toggle
760
+ }, [vue.h("span", { class: "oods-switch__thumb", "aria-hidden": "true" })]),
761
+ vue.h("label", { id: `${controlId}-label`, class: "oods-field-label oods-switch__label", for: controlId }, [
762
+ label,
763
+ props.required ? vue.h("span", { class: "oods-field-required", "aria-hidden": "true" }, "*") : null
764
+ ])
765
+ ]),
766
+ help ? vue.h("p", { id: metadata.helpId.value, class: "oods-field-help" }, help) : null,
767
+ validation ? vue.h("p", {
768
+ id: metadata.validationId.value,
769
+ class: "oods-field-error",
770
+ role: props.validation?.state === "error" ? "alert" : void 0
771
+ }, validation) : null
772
+ ]);
773
+ };
649
774
  }
650
- };
651
- var STATUS_ALIASES = {
652
- subscription: { delinquent: "unpaid" }
653
- };
775
+ });
654
776
  function isRecord(value) {
655
777
  return typeof value === "object" && value !== null && !Array.isArray(value);
656
778
  }
@@ -667,18 +789,13 @@ function statusLabel2(status) {
667
789
  return status.split(/[_-]/).filter(Boolean).map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)).join(" ");
668
790
  }
669
791
  function getPortedStatusPresentation(domain, status) {
670
- const normalizedDomain = domain.toLowerCase();
671
- const normalizedStatus = status.toLowerCase();
672
- const alias = STATUS_ALIASES[normalizedDomain]?.[normalizedStatus];
673
- const metadata = STATUS_METADATA[normalizedDomain]?.[alias ?? normalizedStatus];
674
- if (!metadata) {
675
- return {
676
- label: statusLabel2(status || "Unknown"),
677
- description: "Status not found in registry; falling back to the requested token set.",
678
- tone: "neutral"
679
- };
680
- }
681
- return { ...metadata, label: statusLabel2(status) };
792
+ const presentation = componentContracts.getStatusPresentation(domain.toLowerCase(), status);
793
+ return {
794
+ label: statusLabel2(status || "Unknown"),
795
+ description: presentation.description,
796
+ tone: presentation.tone,
797
+ icon: componentContracts.resolveStatusIcon(presentation.iconName)
798
+ };
682
799
  }
683
800
  function formatPrice(amountCents, amount, currency) {
684
801
  const normalizedAmount = amount;
@@ -822,11 +939,10 @@ var StatusBadge = vue.defineComponent({
822
939
  domain: props.domain,
823
940
  tone,
824
941
  emphasis: props.variant === "solid" ? "solid" : props.emphasis,
825
- icon: props.showIcon ? presentation.icon : void 0,
826
942
  title: stringValue(attrs.title) ?? presentation.description,
827
943
  "aria-label": ariaLabel,
828
944
  "data-oods-component": "StatusBadge"
829
- }, slots);
945
+ }, props.showIcon && presentation.icon && !slots.icon ? { ...slots, icon: () => statusIcon(presentation.icon) } : slots);
830
946
  };
831
947
  }
832
948
  });
@@ -1139,7 +1255,7 @@ var SearchInput = vue.defineComponent({
1139
1255
  "aria-label": "Clear search",
1140
1256
  disabled: props.disabled,
1141
1257
  onClick: clear
1142
- }, "\xD7") : null
1258
+ }, [statusIcon("x")]) : null
1143
1259
  ]);
1144
1260
  };
1145
1261
  }
@@ -1286,17 +1402,18 @@ var RelativeTimestamp = vue.defineComponent({
1286
1402
  props.createdAt
1287
1403
  ) ?? "";
1288
1404
  if (props.hideWhenEmpty && (!datetime || Number.isNaN(Date.parse(datetime)))) return null;
1405
+ const absolute = componentContracts.formatDateTime(datetime, { timeZone: props.timezone });
1289
1406
  const content = firstDefined(
1290
1407
  props.relative,
1291
1408
  props.label,
1292
1409
  props.text,
1293
- props.now === void 0 ? componentContracts.formatDateTime(datetime, { timeZone: props.timezone }) : relativeTimestampLabel(datetime, props.now)
1410
+ !absolute ? "Unknown time" : props.now === void 0 ? absolute : relativeTimestampLabel(datetime, props.now)
1294
1411
  ) ?? "";
1295
1412
  return vue.h("time", {
1296
1413
  ...attrs,
1297
1414
  class: mergedClass("oods-relative-timestamp", attrs.class),
1298
1415
  datetime: datetime || void 0,
1299
- title: stringValue(attrs.title) ?? componentContracts.formatDateTime(datetime, { timeZone: props.timezone }),
1416
+ title: stringValue(attrs.title) ?? absolute,
1300
1417
  "data-oods-component": "RelativeTimestamp"
1301
1418
  }, content);
1302
1419
  };
package/dist/ported.d.cts CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as vue from 'vue';
2
2
  import { PropType, VNode } from 'vue';
3
+ import { StatusIconName } from '@oods/component-contracts';
3
4
 
4
5
  type ComponentTone = 'neutral' | 'info' | 'accent' | 'positive' | 'success' | 'warning' | 'critical' | 'danger';
5
6
  type ComponentEmphasis = 'subtle' | 'solid';
@@ -21,7 +22,7 @@ type PaginationItem = {
21
22
  type StatusMetadata = {
22
23
  description: string;
23
24
  tone: ComponentTone;
24
- icon?: string;
25
+ icon?: StatusIconName;
25
26
  };
26
27
  type StatusPresentation = StatusMetadata & {
27
28
  label: string;
package/dist/ported.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as vue from 'vue';
2
2
  import { PropType, VNode } from 'vue';
3
+ import { StatusIconName } from '@oods/component-contracts';
3
4
 
4
5
  type ComponentTone = 'neutral' | 'info' | 'accent' | 'positive' | 'success' | 'warning' | 'critical' | 'danger';
5
6
  type ComponentEmphasis = 'subtle' | 'solid';
@@ -21,7 +22,7 @@ type PaginationItem = {
21
22
  type StatusMetadata = {
22
23
  description: string;
23
24
  tone: ComponentTone;
24
- icon?: string;
25
+ icon?: StatusIconName;
25
26
  };
26
27
  type StatusPresentation = StatusMetadata & {
27
28
  label: string;