@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/index.cjs CHANGED
@@ -3,6 +3,11 @@
3
3
  var vue = require('vue');
4
4
  var componentContracts = require('@oods/component-contracts');
5
5
 
6
+ // src/primitives.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
+
6
11
  // src/primitives.ts
7
12
  var toneProp = String;
8
13
  var emphasisProp = String;
@@ -144,7 +149,7 @@ var Banner = 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 @@ var Button = 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 @@ var Button = 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
+ var Dialog = 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
  var Card = vue.defineComponent({
186
315
  name: "OodsCard",
@@ -460,6 +589,13 @@ var VizAreaPreview = vue.defineComponent({
460
589
  svgNarrow: String,
461
590
  /** The same chart rendered at the wide size; the figure shows it instead of svg when its own inline size is at least 900px. */
462
591
  svgWide: String,
592
+ /** s222-m02 (F7): the dark and high-contrast renders; with any of them, svg* are the light ones (see React's types). */
593
+ svgDark: String,
594
+ svgDarkNarrow: String,
595
+ svgDarkWide: String,
596
+ svgHc: String,
597
+ svgHcNarrow: String,
598
+ svgHcWide: String,
463
599
  title: String,
464
600
  description: String,
465
601
  width: { type: Number, default: 640 },
@@ -468,6 +604,7 @@ var VizAreaPreview = vue.defineComponent({
468
604
  setup(props, { slots }) {
469
605
  return () => {
470
606
  const content = authoredContent(slots.default?.());
607
+ const renders = (layer) => layer.renders.map(([attribute, markup]) => vue.h("div", { [attribute]: "true", innerHTML: markup }));
471
608
  if (props.svg !== void 0) return vue.h("figure", {
472
609
  class: "oods-viz-area-preview",
473
610
  "data-oods-component": "VizAreaPreview",
@@ -482,9 +619,11 @@ var VizAreaPreview = vue.defineComponent({
482
619
  style: { "--oods-viz-width": `${props.width}px` }
483
620
  }, [
484
621
  ...props.title && !componentContracts.svgCarriesTitle(props.svg, props.title) ? [vue.h("figcaption", props.title)] : [],
485
- vue.h("div", { "data-viz-svg": "true", innerHTML: componentContracts.assertStaticSvg(props.svg) }),
486
- ...props.svgNarrow !== void 0 ? [vue.h("div", { "data-viz-svg-narrow": "true", innerHTML: componentContracts.assertStaticSvg(props.svgNarrow) })] : [],
487
- ...props.svgWide !== void 0 ? [vue.h("div", { "data-viz-svg-wide": "true", innerHTML: componentContracts.assertStaticSvg(props.svgWide) })] : [],
622
+ ...componentContracts.vizPreviewLayers(props).flatMap((layer) => layer.theme === void 0 ? renders(layer) : [vue.h("div", {
623
+ "data-viz-theme": layer.theme,
624
+ ...layer.narrow ? { "data-viz-narrow": "true" } : {},
625
+ ...layer.wide ? { "data-viz-wide": "true" } : {}
626
+ }, renders(layer))]),
488
627
  ...props.description ? [vue.h("p", { "data-viz-description": "true" }, props.description)] : []
489
628
  ]);
490
629
  return vue.h("div", {
@@ -713,12 +852,15 @@ function createBadgeFamily(options) {
713
852
  };
714
853
  for (const key of options.extraProps) props[key] = String;
715
854
  for (const key of options.booleanProps ?? []) props[key] = { type: [String, Boolean], default: void 0 };
855
+ if (options.booleanProps) props.hideWhenFalse = { type: [Boolean, String], default: void 0 };
716
856
  return vue.defineComponent({
717
857
  name: `Oods${options.component}`,
718
858
  props,
719
859
  setup(componentProps, { slots }) {
720
860
  return () => {
721
861
  const record = componentProps;
862
+ const flag = options.booleanProps?.map((key) => record[key]).find((value) => typeof value === "boolean");
863
+ if (record.hideWhenFalse === true && flag === false) return null;
722
864
  const content = authoredContent(slots.default?.());
723
865
  const scalar = scalarContent(content);
724
866
  const read = (key) => typeof record[key] === "boolean" && options.booleanProps?.includes(key) ? String(record[key]) : record[key];
@@ -1345,12 +1487,13 @@ var TagSummary = vue.defineComponent({
1345
1487
  });
1346
1488
  var OwnershipMeta = vue.defineComponent({
1347
1489
  name: "OodsOwnershipMeta",
1348
- props: { title: String, label: String, heading: String, name: String, ownerType: String, owner_type: String, role: String, ownershipRole: String },
1490
+ props: { title: String, label: String, heading: String, name: String, ownerType: String, owner_type: String, role: String, ownershipRole: String, ownerLabel: String },
1349
1491
  setup(props, { slots }) {
1350
1492
  return () => {
1351
1493
  const content = authoredContent(slots.default?.());
1352
1494
  const terms = [["Owner Type", firstScalar(props.ownerType, props.owner_type)], ["Role", firstScalar(props.role, props.ownershipRole)]].filter((entry) => entry[1] !== void 0);
1353
- return vue.h("div", { class: "oods-ownership-meta", "data-oods-component": "OwnershipMeta", "data-meta-type": "ownership" }, content.length ? content : [
1495
+ const phrase = props.ownerLabel !== void 0 ? componentContracts.ownershipPhrase(props.ownerLabel, firstScalar(props.ownerType, props.owner_type), firstScalar(props.role, props.ownershipRole)) : void 0;
1496
+ return vue.h("div", { class: "oods-ownership-meta", "data-oods-component": "OwnershipMeta", "data-meta-type": "ownership" }, content.length ? content : phrase ? [vue.h("span", { "data-meta-item": "true" }, phrase)] : [
1354
1497
  vue.h("span", { "data-meta-title": "true" }, firstText(props.title, props.label, props.heading, props.name) ?? "Ownership"),
1355
1498
  ...terms.map(([term, value]) => vue.h("span", { key: term, "data-meta-item": "true" }, [vue.h("strong", `${term}:`), ` ${value}`]))
1356
1499
  ]);
@@ -1727,6 +1870,75 @@ var Checkbox = vue.defineComponent({
1727
1870
  };
1728
1871
  }
1729
1872
  });
1873
+ var Switch = vue.defineComponent({
1874
+ name: "OodsSwitch",
1875
+ props: {
1876
+ id: String,
1877
+ label: String,
1878
+ modelValue: { type: Boolean, default: void 0 },
1879
+ checked: { type: Boolean, default: void 0 },
1880
+ defaultChecked: Boolean,
1881
+ required: Boolean,
1882
+ disabled: Boolean,
1883
+ help: String,
1884
+ validation: Object,
1885
+ size: { type: String, default: "md" }
1886
+ },
1887
+ emits: {
1888
+ "update:modelValue": (_value) => true,
1889
+ change: (_value) => true
1890
+ },
1891
+ setup(props, { emit, slots }) {
1892
+ const internalChecked = vue.ref(props.defaultChecked);
1893
+ const currentChecked = vue.computed(() => props.modelValue ?? props.checked ?? internalChecked.value);
1894
+ const metadata = useFieldMetadata("Switch", props, slots);
1895
+ const toggle = () => {
1896
+ const next = !currentChecked.value;
1897
+ if (props.modelValue === void 0 && props.checked === void 0) internalChecked.value = next;
1898
+ emit("update:modelValue", next);
1899
+ emit("change", next);
1900
+ };
1901
+ return () => {
1902
+ const label = slots.label?.() ?? props.label;
1903
+ const help = slots.help?.() ?? props.help;
1904
+ const validation = slots.validation?.() ?? props.validation?.message;
1905
+ const controlId = metadata.controlId.value;
1906
+ return vue.h("div", {
1907
+ class: "oods-field oods-switch",
1908
+ style: validationStyle(props.validation),
1909
+ "data-oods-component": "Switch",
1910
+ "data-size": props.size,
1911
+ "data-validation-state": props.validation?.state
1912
+ }, [
1913
+ vue.h("div", { class: "oods-switch__row" }, [
1914
+ vue.h("button", {
1915
+ id: controlId,
1916
+ type: "button",
1917
+ role: "switch",
1918
+ class: "oods-switch__control",
1919
+ "aria-checked": currentChecked.value ? "true" : "false",
1920
+ "aria-labelledby": `${controlId}-label`,
1921
+ "aria-describedby": metadata.describedBy.value,
1922
+ "aria-required": props.required ? "true" : void 0,
1923
+ "aria-invalid": props.validation?.state === "error" ? "true" : void 0,
1924
+ disabled: props.disabled,
1925
+ onClick: toggle
1926
+ }, [vue.h("span", { class: "oods-switch__thumb", "aria-hidden": "true" })]),
1927
+ vue.h("label", { id: `${controlId}-label`, class: "oods-field-label oods-switch__label", for: controlId }, [
1928
+ label,
1929
+ props.required ? vue.h("span", { class: "oods-field-required", "aria-hidden": "true" }, "*") : null
1930
+ ])
1931
+ ]),
1932
+ help ? vue.h("p", { id: metadata.helpId.value, class: "oods-field-help" }, help) : null,
1933
+ validation ? vue.h("p", {
1934
+ id: metadata.validationId.value,
1935
+ class: "oods-field-error",
1936
+ role: props.validation?.state === "error" ? "alert" : void 0
1937
+ }, validation) : null
1938
+ ]);
1939
+ };
1940
+ }
1941
+ });
1730
1942
  var TableCaption = vue.defineComponent({
1731
1943
  name: "OodsTableCaption",
1732
1944
  setup(_props, { slots }) {
@@ -1771,6 +1983,8 @@ var Table = vue.defineComponent({
1771
1983
  inheritAttrs: false,
1772
1984
  props: {
1773
1985
  caption: { type: String, default: "" },
1986
+ // s222-m02 (#2502 ruling 11): false keeps the caption as the table's accessible name but hides it visually.
1987
+ showCaption: { type: Boolean, default: true },
1774
1988
  columns: { type: Array, default: () => [] },
1775
1989
  rows: { type: Array, default: () => [] },
1776
1990
  density: { type: String, default: "default" },
@@ -1787,7 +2001,7 @@ var Table = vue.defineComponent({
1787
2001
  "data-oods-component": "Table",
1788
2002
  "data-density": props.density
1789
2003
  }, [
1790
- slots.caption || props.caption ? vue.h(TableCaption, {}, {
2004
+ slots.caption || props.caption ? vue.h(TableCaption, { "data-visually-hidden": props.showCaption ? void 0 : "true" }, {
1791
2005
  default: () => slots.caption?.() ?? props.caption
1792
2006
  }) : null,
1793
2007
  vue.h(TableHead, {}, {
@@ -2005,82 +2219,63 @@ var Tabs = vue.defineComponent({
2005
2219
  ]);
2006
2220
  }
2007
2221
  });
2008
-
2009
- // src/ported-core.ts
2010
- var STATUS_METADATA = {
2011
- subscription: {
2012
- future: {
2013
- description: "Subscription is scheduled but not yet active.",
2014
- tone: "info",
2015
- icon: "\u23F2"
2016
- },
2017
- trialing: {
2018
- description: "Subscription is in a trial period before billing begins.",
2019
- tone: "accent",
2020
- icon: "\u2605"
2021
- },
2022
- active: {
2023
- description: "Subscription is paid and service is provisioned.",
2024
- tone: "success",
2025
- icon: "\u2714\uFE0E"
2026
- },
2027
- paused: {
2028
- description: "Billing paused temporarily by finance or customer request.",
2029
- tone: "neutral",
2030
- icon: "\u23F8"
2031
- },
2032
- pending_cancellation: {
2033
- description: "Subscription will cancel at period end but remains active now.",
2034
- tone: "info",
2035
- icon: "\u2026"
2036
- },
2037
- past_due: {
2038
- description: "Renewal payment failed; smart retries ongoing and access retained during the grace window.",
2039
- tone: "critical",
2040
- icon: "\u26A0\uFE0E"
2041
- },
2042
- unpaid: {
2043
- description: "Payment retries exhausted; service access revoked.",
2044
- tone: "critical",
2045
- icon: "\u2A2F"
2046
- },
2047
- terminated: {
2048
- description: "Subscription ended; service access revoked and billing stopped.",
2049
- tone: "neutral",
2050
- icon: "\u2205"
2051
- }
2222
+ var SegmentedControl = vue.defineComponent({
2223
+ name: "OodsSegmentedControl",
2224
+ props: {
2225
+ id: String,
2226
+ label: String,
2227
+ modelValue: String,
2228
+ value: String,
2229
+ defaultValue: { type: String, default: "" },
2230
+ name: String,
2231
+ size: { type: String, default: "md" },
2232
+ disabled: Boolean,
2233
+ options: { type: Array, default: () => [] }
2052
2234
  },
2053
- invoice: {
2054
- draft: {
2055
- description: "Invoice is editable and not yet finalized.",
2056
- tone: "neutral",
2057
- icon: "\u270E"
2058
- },
2059
- posted: {
2060
- description: "Invoice finalized with terms before collection.",
2061
- tone: "info",
2062
- icon: "\u2026"
2063
- },
2064
- paid: {
2065
- description: "Invoice collected successfully in full.",
2066
- tone: "success",
2067
- icon: "\u2714\uFE0E"
2068
- },
2069
- past_due: {
2070
- description: "Invoice is unpaid past its due date.",
2071
- tone: "critical",
2072
- icon: "\u2A2F"
2073
- },
2074
- void: {
2075
- description: "Invoice canceled and no longer payable.",
2076
- tone: "neutral",
2077
- icon: "\u2205"
2078
- }
2235
+ emits: {
2236
+ "update:modelValue": (_value) => true,
2237
+ change: (_value) => true
2238
+ },
2239
+ setup(props, { emit, slots }) {
2240
+ const generatedId = vue.useId();
2241
+ const internalValue = vue.ref(props.defaultValue);
2242
+ const currentValue = vue.computed(() => props.modelValue ?? props.value ?? internalValue.value);
2243
+ const select = (event) => {
2244
+ const next = event.target.value;
2245
+ if (props.modelValue === void 0 && props.value === void 0) internalValue.value = next;
2246
+ emit("update:modelValue", next);
2247
+ emit("change", next);
2248
+ };
2249
+ return () => {
2250
+ const id = props.id || `oods-segmentedcontrol-${generatedId}`;
2251
+ const label = slots.label?.() ?? props.label;
2252
+ return vue.h("div", {
2253
+ class: "oods-field oods-segmented-control",
2254
+ "data-oods-component": "SegmentedControl",
2255
+ "data-size": props.size
2256
+ }, [
2257
+ vue.h("span", { id: `${id}-label`, class: "oods-field-label" }, [label]),
2258
+ vue.h("div", {
2259
+ id,
2260
+ role: "radiogroup",
2261
+ "aria-labelledby": `${id}-label`,
2262
+ class: "oods-segmented-control__track"
2263
+ }, props.options.map((option) => vue.h("label", { key: option.value, class: "oods-segmented-control__option" }, [
2264
+ vue.h("input", {
2265
+ type: "radio",
2266
+ class: "oods-segmented-control__input",
2267
+ name: props.name || id,
2268
+ value: option.value,
2269
+ checked: option.value === currentValue.value,
2270
+ disabled: props.disabled || option.disabled,
2271
+ onChange: select
2272
+ }),
2273
+ vue.h("span", { class: "oods-segmented-control__label" }, option.label)
2274
+ ])))
2275
+ ]);
2276
+ };
2079
2277
  }
2080
- };
2081
- var STATUS_ALIASES = {
2082
- subscription: { delinquent: "unpaid" }
2083
- };
2278
+ });
2084
2279
  function isRecord2(value) {
2085
2280
  return typeof value === "object" && value !== null && !Array.isArray(value);
2086
2281
  }
@@ -2097,18 +2292,13 @@ function statusLabel2(status) {
2097
2292
  return status.split(/[_-]/).filter(Boolean).map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)).join(" ");
2098
2293
  }
2099
2294
  function getPortedStatusPresentation(domain, status) {
2100
- const normalizedDomain = domain.toLowerCase();
2101
- const normalizedStatus = status.toLowerCase();
2102
- const alias = STATUS_ALIASES[normalizedDomain]?.[normalizedStatus];
2103
- const metadata = STATUS_METADATA[normalizedDomain]?.[alias ?? normalizedStatus];
2104
- if (!metadata) {
2105
- return {
2106
- label: statusLabel2(status || "Unknown"),
2107
- description: "Status not found in registry; falling back to the requested token set.",
2108
- tone: "neutral"
2109
- };
2110
- }
2111
- return { ...metadata, label: statusLabel2(status) };
2295
+ const presentation = componentContracts.getStatusPresentation(domain.toLowerCase(), status);
2296
+ return {
2297
+ label: statusLabel2(status || "Unknown"),
2298
+ description: presentation.description,
2299
+ tone: presentation.tone,
2300
+ icon: componentContracts.resolveStatusIcon(presentation.iconName)
2301
+ };
2112
2302
  }
2113
2303
  function formatPrice(amountCents, amount, currency) {
2114
2304
  const normalizedAmount = amount;
@@ -2252,11 +2442,10 @@ var StatusBadge = vue.defineComponent({
2252
2442
  domain: props.domain,
2253
2443
  tone,
2254
2444
  emphasis: props.variant === "solid" ? "solid" : props.emphasis,
2255
- icon: props.showIcon ? presentation.icon : void 0,
2256
2445
  title: stringValue(attrs.title) ?? presentation.description,
2257
2446
  "aria-label": ariaLabel,
2258
2447
  "data-oods-component": "StatusBadge"
2259
- }, slots);
2448
+ }, props.showIcon && presentation.icon && !slots.icon ? { ...slots, icon: () => statusIcon(presentation.icon) } : slots);
2260
2449
  };
2261
2450
  }
2262
2451
  });
@@ -2569,7 +2758,7 @@ var SearchInput = vue.defineComponent({
2569
2758
  "aria-label": "Clear search",
2570
2759
  disabled: props.disabled,
2571
2760
  onClick: clear
2572
- }, "\xD7") : null
2761
+ }, [statusIcon("x")]) : null
2573
2762
  ]);
2574
2763
  };
2575
2764
  }
@@ -2716,17 +2905,18 @@ var RelativeTimestamp = vue.defineComponent({
2716
2905
  props.createdAt
2717
2906
  ) ?? "";
2718
2907
  if (props.hideWhenEmpty && (!datetime || Number.isNaN(Date.parse(datetime)))) return null;
2908
+ const absolute = componentContracts.formatDateTime(datetime, { timeZone: props.timezone });
2719
2909
  const content = firstDefined(
2720
2910
  props.relative,
2721
2911
  props.label,
2722
2912
  props.text,
2723
- props.now === void 0 ? componentContracts.formatDateTime(datetime, { timeZone: props.timezone }) : relativeTimestampLabel(datetime, props.now)
2913
+ !absolute ? "Unknown time" : props.now === void 0 ? absolute : relativeTimestampLabel(datetime, props.now)
2724
2914
  ) ?? "";
2725
2915
  return vue.h("time", {
2726
2916
  ...attrs,
2727
2917
  class: mergedClass("oods-relative-timestamp", attrs.class),
2728
2918
  datetime: datetime || void 0,
2729
- title: stringValue(attrs.title) ?? componentContracts.formatDateTime(datetime, { timeZone: props.timezone }),
2919
+ title: stringValue(attrs.title) ?? absolute,
2730
2920
  "data-oods-component": "RelativeTimestamp"
2731
2921
  }, content);
2732
2922
  };
@@ -2903,7 +3093,8 @@ var array = Array;
2903
3093
  function eventRecipe(id, kind, title) {
2904
3094
  return vue.defineComponent({ name: id, props: { id: String, title: { type: String, default: title }, archivedAt: String, restoredAt: String, archivedBy: String, reason: String, timestamp: String, code: String, history: array, status: String, showActor: { type: Boolean, default: true }, showReason: { type: Boolean, default: true } }, setup: (props) => () => {
2905
3095
  const rows = componentContracts.traitEventRows(kind, props);
2906
- return vue.h("article", { id: props.id, class: "oods-trait-recipe", "data-oods-component": id, "aria-label": props.title }, [vue.h("h2", props.title), rows.length ? vue.h("ol", rows.map((row, index) => vue.h("li", { key: index }, [vue.h("strong", row.title), vue.h("p", row.at ? vue.h("time", { datetime: row.at }, row.time) : row.time), row.actor && vue.h("p", `Actor: ${row.actor}`), row.reason && vue.h("p", `Reason: ${row.reason}`), row.code && vue.h("p", `Code: ${row.code}`)]))) : vue.h("p", "No events recorded.")]);
3096
+ if (!rows.length) return null;
3097
+ return vue.h("article", { id: props.id, class: "oods-trait-recipe", "data-oods-component": id, "aria-label": props.title }, [vue.h("h2", props.title), vue.h("ol", rows.map((row, index) => vue.h("li", { key: index }, [vue.h("strong", row.title), vue.h("p", row.at ? vue.h("time", { datetime: row.at }, row.time) : row.time), row.actor && vue.h("p", `Actor: ${row.actor}`), row.reason && vue.h("p", `Reason: ${row.reason}`), row.code && vue.h("p", `Code: ${row.code}`)])))]);
2907
3098
  } });
2908
3099
  }
2909
3100
  var ArchiveEvent = eventRecipe("ArchiveEvent", "archive", "Archive events");
@@ -2974,7 +3165,7 @@ function summary(id) {
2974
3165
  } });
2975
3166
  }
2976
3167
  function preview(id, type) {
2977
- return vue.defineComponent({ name: id, props: { svg: String, svgNarrow: String, svgWide: String, title: String, description: String, width: Number, height: Number }, setup(props, { slots }) {
3168
+ return vue.defineComponent({ name: id, props: { svg: String, svgNarrow: String, svgWide: String, svgDark: String, svgDarkNarrow: String, svgDarkWide: String, svgHc: String, svgHcNarrow: String, svgHcWide: String, title: String, description: String, width: Number, height: Number }, setup(props, { slots }) {
2978
3169
  return () => vue.h(VizAreaPreview, { ...props, title: props.title ?? `${type[0].toUpperCase()}${type.slice(1)} chart`, "data-oods-component": id, "data-viz-preview-type": type }, { default: slots.default ?? (() => vue.h(Text, { content: "No rendered chart supplied" })) });
2979
3170
  } });
2980
3171
  }
@@ -3004,6 +3195,211 @@ var VizLinePreview = preview("VizLinePreview", "line");
3004
3195
  var VizMarkPreview = preview("VizMarkPreview", "bar");
3005
3196
  var VizPointPreview = preview("VizPointPreview", "scatter");
3006
3197
  var VizScatterPreview = preview("VizScatterPreview", "scatter");
3198
+ var Combobox = vue.defineComponent({
3199
+ name: "OodsCombobox",
3200
+ props: {
3201
+ id: String,
3202
+ label: String,
3203
+ options: { type: Array, default: () => [] },
3204
+ modelValue: String,
3205
+ value: String,
3206
+ defaultValue: { type: String, default: "" },
3207
+ placeholder: String,
3208
+ name: String,
3209
+ required: Boolean,
3210
+ disabled: Boolean,
3211
+ help: String,
3212
+ validation: Object,
3213
+ /** The control height, as Button, Input and Select: xs 24, sm 28, md 32 (default) or lg 40. */
3214
+ size: { type: String, default: "md" }
3215
+ },
3216
+ emits: {
3217
+ "update:modelValue": (_value) => true,
3218
+ change: (_value) => true
3219
+ },
3220
+ setup(props, { emit, slots }) {
3221
+ const generatedId = vue.useId();
3222
+ const controlId = vue.computed(() => props.id || `oods-combobox-${generatedId}`);
3223
+ const internalValue = vue.ref(props.defaultValue);
3224
+ const query = vue.ref(null);
3225
+ const open = vue.ref(false);
3226
+ const active = vue.ref(-1);
3227
+ const root = vue.ref(null);
3228
+ const listbox = vue.ref(null);
3229
+ const chosenValue = vue.computed(() => props.modelValue ?? props.value ?? internalValue.value);
3230
+ const chosen = vue.computed(() => props.options.find((option) => option.value === chosenValue.value));
3231
+ const text = vue.computed(() => query.value ?? chosen.value?.label ?? "");
3232
+ const matches = vue.computed(() => {
3233
+ const needle = (query.value ?? "").trim().toLowerCase();
3234
+ return props.options.map((option) => needle === "" || option.label.toLowerCase().includes(needle));
3235
+ });
3236
+ const reachable = vue.computed(() => props.options.flatMap((option, index) => matches.value[index] && !option.disabled ? [index] : []));
3237
+ const optionId = (index) => `${controlId.value}-option-${index}`;
3238
+ const close = () => {
3239
+ open.value = false;
3240
+ active.value = -1;
3241
+ };
3242
+ const commit = (next) => {
3243
+ if (props.modelValue === void 0 && props.value === void 0) internalValue.value = next;
3244
+ emit("update:modelValue", next);
3245
+ emit("change", next);
3246
+ };
3247
+ const pick = (index) => {
3248
+ const option = props.options[index];
3249
+ if (!option || option.disabled) return;
3250
+ query.value = null;
3251
+ close();
3252
+ if (option.value !== chosenValue.value) commit(option.value);
3253
+ };
3254
+ const outside = (event) => {
3255
+ if (!root.value?.contains(event.target)) close();
3256
+ };
3257
+ vue.watch(open, (isOpen) => {
3258
+ if (isOpen) document.addEventListener("pointerdown", outside);
3259
+ else document.removeEventListener("pointerdown", outside);
3260
+ });
3261
+ vue.onBeforeUnmount(() => {
3262
+ if (typeof document !== "undefined") document.removeEventListener("pointerdown", outside);
3263
+ });
3264
+ vue.watch([open, active], async () => {
3265
+ if (!open.value || active.value < 0) return;
3266
+ await vue.nextTick();
3267
+ const element = listbox.value?.children[active.value];
3268
+ element?.scrollIntoView?.({ block: "nearest" });
3269
+ });
3270
+ const keydown = (event) => {
3271
+ if (event.defaultPrevented || props.disabled || event.isComposing) return;
3272
+ const order = reachable.value;
3273
+ const first = order[0] ?? -1;
3274
+ const last = order[order.length - 1] ?? -1;
3275
+ const at = order.indexOf(active.value);
3276
+ switch (event.key) {
3277
+ case "ArrowDown":
3278
+ event.preventDefault();
3279
+ active.value = !open.value || at === -1 ? first : order[(at + 1) % order.length];
3280
+ open.value = true;
3281
+ return;
3282
+ case "ArrowUp":
3283
+ event.preventDefault();
3284
+ active.value = !open.value || at <= 0 ? last : order[at - 1];
3285
+ open.value = true;
3286
+ return;
3287
+ case "Home":
3288
+ case "End":
3289
+ if (!open.value) return;
3290
+ event.preventDefault();
3291
+ active.value = event.key === "Home" ? first : last;
3292
+ return;
3293
+ case "Enter":
3294
+ if (!open.value) return;
3295
+ event.preventDefault();
3296
+ if (active.value >= 0) pick(active.value);
3297
+ return;
3298
+ case "Escape":
3299
+ if (open.value) {
3300
+ event.preventDefault();
3301
+ close();
3302
+ return;
3303
+ }
3304
+ if (text.value === "" && chosenValue.value === "") return;
3305
+ event.preventDefault();
3306
+ query.value = null;
3307
+ if (chosenValue.value !== "") commit("");
3308
+ return;
3309
+ case "Tab":
3310
+ if (open.value) close();
3311
+ return;
3312
+ }
3313
+ };
3314
+ return () => {
3315
+ const id = controlId.value;
3316
+ const label = slots.label?.() ?? props.label;
3317
+ const help = slots.help?.() ?? props.help;
3318
+ const validation = slots.validation?.() ?? props.validation?.message;
3319
+ const tone = props.validation ? props.validation.state === "error" ? "critical" : props.validation.state : void 0;
3320
+ const describedBy = [help ? `${id}-help` : void 0, validation ? `${id}-validation` : void 0].filter(Boolean).join(" ") || void 0;
3321
+ const noMatches = open.value && !matches.value.some(Boolean);
3322
+ return vue.h("div", {
3323
+ ref: root,
3324
+ class: "oods-field oods-combobox",
3325
+ ...tone ? { style: { "--cmp-input-message-border": `var(--sys-status-${tone}-border)`, "--cmp-input-message-text": `var(--sys-status-${tone}-text)` } } : {},
3326
+ "data-oods-component": "Combobox",
3327
+ "data-size": props.size,
3328
+ "data-validation-state": props.validation?.state
3329
+ }, [
3330
+ vue.h("label", { id: `${id}-label`, class: "oods-field-label", for: id }, [
3331
+ label,
3332
+ props.required ? vue.h("span", { class: "oods-field-required", "aria-hidden": "true" }, "*") : null
3333
+ ]),
3334
+ vue.h("div", { class: "oods-combobox__control" }, [
3335
+ vue.h("input", {
3336
+ id,
3337
+ type: "text",
3338
+ role: "combobox",
3339
+ class: "oods-field-control oods-combobox__input",
3340
+ value: text.value,
3341
+ placeholder: props.placeholder,
3342
+ autocomplete: "off",
3343
+ "aria-autocomplete": "list",
3344
+ "aria-expanded": open.value ? "true" : "false",
3345
+ "aria-controls": `${id}-listbox`,
3346
+ "aria-activedescendant": open.value && active.value >= 0 ? optionId(active.value) : void 0,
3347
+ "aria-describedby": describedBy,
3348
+ "aria-invalid": props.validation?.state === "error" ? "true" : void 0,
3349
+ required: props.required,
3350
+ disabled: props.disabled,
3351
+ onInput: (event) => {
3352
+ query.value = event.target.value;
3353
+ open.value = true;
3354
+ active.value = -1;
3355
+ },
3356
+ onKeydown: keydown,
3357
+ onClick: () => {
3358
+ if (!props.disabled) open.value = true;
3359
+ },
3360
+ onBlur: () => {
3361
+ query.value = null;
3362
+ close();
3363
+ }
3364
+ }),
3365
+ vue.h("span", { class: "oods-combobox__chevron", "aria-hidden": "true" }, [statusIcon("chevron-down")]),
3366
+ vue.h("div", {
3367
+ class: "oods-combobox__popup",
3368
+ hidden: !open.value,
3369
+ onMousedown: (event) => event.preventDefault()
3370
+ }, [
3371
+ vue.h(
3372
+ "ul",
3373
+ { ref: listbox, id: `${id}-listbox`, class: "oods-combobox__listbox", role: "listbox", "aria-labelledby": `${id}-label` },
3374
+ props.options.map((option, index) => vue.h("li", {
3375
+ key: option.value,
3376
+ id: optionId(index),
3377
+ class: "oods-combobox__option",
3378
+ role: "option",
3379
+ "aria-selected": option.value === chosenValue.value ? "true" : "false",
3380
+ "aria-disabled": option.disabled ? "true" : void 0,
3381
+ "data-value": option.value,
3382
+ "data-active": index === active.value ? "true" : void 0,
3383
+ hidden: !matches.value[index],
3384
+ onClick: () => pick(index),
3385
+ onPointermove: () => {
3386
+ if (!option.disabled && index !== active.value) active.value = index;
3387
+ }
3388
+ }, [
3389
+ vue.h("span", { class: "oods-combobox__option-label" }, option.label),
3390
+ vue.h("span", { class: "oods-combobox__check", "aria-hidden": "true" }, [statusIcon("check")])
3391
+ ]))
3392
+ ),
3393
+ vue.h("p", { class: "oods-combobox__empty", role: "status" }, noMatches ? "No matches" : "")
3394
+ ])
3395
+ ]),
3396
+ props.name ? vue.h("input", { type: "hidden", name: props.name, value: chosenValue.value }) : null,
3397
+ help ? vue.h("p", { id: `${id}-help`, class: "oods-field-help" }, help) : null,
3398
+ validation ? vue.h("p", { id: `${id}-validation`, class: "oods-field-error", role: props.validation?.state === "error" ? "alert" : void 0 }, validation) : null
3399
+ ]);
3400
+ };
3401
+ }
3402
+ });
3007
3403
 
3008
3404
  exports.AddressCollectionPanel = AddressCollectionPanel;
3009
3405
  exports.AddressEditor = AddressEditor;
@@ -3036,10 +3432,12 @@ exports.ClassificationPanel = ClassificationPanel;
3036
3432
  exports.ColorStatePicker = ColorStatePicker;
3037
3433
  exports.ColorSwatch = ColorSwatch;
3038
3434
  exports.ColorizedBadge = ColorizedBadge;
3435
+ exports.Combobox = Combobox;
3039
3436
  exports.CommunicationDetailPanel = CommunicationDetailPanel;
3040
3437
  exports.CycleProgressCard = CycleProgressCard;
3041
3438
  exports.DatePicker = DatePicker;
3042
3439
  exports.DetailHeader = DetailHeader;
3440
+ exports.Dialog = Dialog;
3043
3441
  exports.FilterPanel = FilterPanel;
3044
3442
  exports.FormLabelGroup = FormLabelGroup;
3045
3443
  exports.GeoFieldMappingForm = GeoFieldMappingForm;
@@ -3070,6 +3468,7 @@ exports.RelativeTimestamp = RelativeTimestamp;
3070
3468
  exports.RoleAssignmentForm = RoleAssignmentForm;
3071
3469
  exports.RoleBadgeList = RoleBadgeList;
3072
3470
  exports.SearchInput = SearchInput;
3471
+ exports.SegmentedControl = SegmentedControl;
3073
3472
  exports.Select = Select;
3074
3473
  exports.SortIndicator = SortIndicator;
3075
3474
  exports.Stack = Stack;
@@ -3078,6 +3477,7 @@ exports.StatusBadge = StatusBadge;
3078
3477
  exports.StatusColorLegend = StatusColorLegend;
3079
3478
  exports.StatusSelector = StatusSelector;
3080
3479
  exports.StatusTimeline = StatusTimeline;
3480
+ exports.Switch = Switch;
3081
3481
  exports.Table = Table;
3082
3482
  exports.TableBody = TableBody;
3083
3483
  exports.TableCaption = TableCaption;