@reliability-design/web 1.4.2 → 1.4.3

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.mjs CHANGED
@@ -681,6 +681,7 @@ var RelTextfield = class extends LitElement3 {
681
681
  this.internalState = "default";
682
682
  this._wasError = false;
683
683
  this.uniqueId = `textfield-${Math.random().toString(36).slice(2)}`;
684
+ this._warnedNoName = false;
684
685
  }
685
686
  get isDisabled() {
686
687
  return this.disabled || this.state === "disabled";
@@ -696,6 +697,15 @@ var RelTextfield = class extends LitElement3 {
696
697
  get helper() {
697
698
  return this.helperText ?? this.supportingText;
698
699
  }
700
+ /** labelStyle supersedes the deprecated floatingLabel boolean. */
701
+ get resolvedLabelStyle() {
702
+ if (this.labelStyle) return this.labelStyle;
703
+ return this.floatingLabel ? "floating" : "default";
704
+ }
705
+ /** True when no visible label renders and the input needs an ARIA name. */
706
+ get labelHidden() {
707
+ return this.resolvedLabelStyle === "none";
708
+ }
699
709
  connectedCallback() {
700
710
  super.connectedCallback();
701
711
  const leftIcon = this.getAttribute("lefticon") || this.getAttribute("leftIcon");
@@ -703,6 +713,20 @@ var RelTextfield = class extends LitElement3 {
703
713
  const rightIcon = this.getAttribute("righticon") || this.getAttribute("rightIcon");
704
714
  if (rightIcon) this.rightIcon = rightIcon;
705
715
  }
716
+ willUpdate(changed) {
717
+ if (changed.has("labelStyle") && this.labelStyle) {
718
+ this.floatingLabel = this.labelStyle === "floating";
719
+ }
720
+ }
721
+ updated() {
722
+ if (this._warnedNoName || !this.labelHidden) return;
723
+ if (this.fieldAriaLabel || this.fieldAriaLabelledby || this.getAttribute("aria-label")) return;
724
+ this._warnedNoName = true;
725
+ console.warn(
726
+ '<rel-textfield label-style="none"> has no accessible name. Set `field-aria-label` or `field-aria-labelledby` \u2014 a placeholder is not a valid naming mechanism.',
727
+ this
728
+ );
729
+ }
706
730
  /** Focuses the inner input/textarea (the host itself is not focusable). */
707
731
  focus(options) {
708
732
  this.renderRoot.querySelector("input, textarea")?.focus(options);
@@ -770,15 +794,20 @@ var RelTextfield = class extends LitElement3 {
770
794
  !this.multiline && leadingIcon ? "has-left-icon" : "",
771
795
  !this.multiline && this.rightIcon && this.variant !== "legend" ? "has-right-icon" : ""
772
796
  ].filter(Boolean).join(" ");
797
+ const labelStyle = this.resolvedLabelStyle;
798
+ const labelHidden = labelStyle === "none";
773
799
  const labelTemplate = (extraClass = "") => {
774
- if (!this.label) return "";
800
+ if (!this.label || labelHidden) return "";
775
801
  return html3`<label
776
802
  part="label"
777
803
  class="textfield-label${extraClass}"
778
804
  for="${inputId}"
779
805
  >${this.label}${this.required ? html3`<span class="required-mark" aria-hidden="true">*</span>` : ""}</label>`;
780
806
  };
781
- const ariaLabelledby = this.variant === "legend" && this.label ? legendId : void 0;
807
+ const legendLabelledby = this.variant === "legend" && this.label && !labelHidden ? legendId : void 0;
808
+ const ariaLabelledby = this.fieldAriaLabelledby ?? legendLabelledby;
809
+ const hasVisibleLabel = !labelHidden && !!this.label;
810
+ const ariaLabel = !hasVisibleLabel && !ariaLabelledby ? this.fieldAriaLabel : void 0;
782
811
  const inputTemplate = this.multiline ? html3`<textarea part="input"
783
812
  id="${inputId}"
784
813
  class="${inputClasses}"
@@ -792,6 +821,7 @@ var RelTextfield = class extends LitElement3 {
792
821
  ?required="${this.required}"
793
822
  aria-invalid="${isError}"
794
823
  aria-required="${this.required}"
824
+ aria-label="${ariaLabel ?? nothing}"
795
825
  aria-labelledby="${ariaLabelledby ?? nothing}"
796
826
  aria-describedby="${describedById ?? nothing}"
797
827
  @focus="${this.handleFocus}"
@@ -810,6 +840,7 @@ var RelTextfield = class extends LitElement3 {
810
840
  ?required="${this.required}"
811
841
  aria-invalid="${isError}"
812
842
  aria-required="${this.required}"
843
+ aria-label="${ariaLabel ?? nothing}"
813
844
  aria-labelledby="${ariaLabelledby ?? nothing}"
814
845
  aria-describedby="${describedById ?? nothing}"
815
846
  @focus="${this.handleFocus}"
@@ -831,7 +862,7 @@ var RelTextfield = class extends LitElement3 {
831
862
  const fieldsetClass = ["field-fieldset", this.multiline ? "multiline" : ""].filter(Boolean).join(" ");
832
863
  return html3`
833
864
  <div class="${wrapperClasses}">
834
- <fieldset part="control" class="${fieldsetClass}"><legend class="field-legend" id="${legendId}">${this.label ?? ""}${this.required ? html3`<span class="required-mark" aria-hidden="true">*</span>` : ""}</legend>
865
+ <fieldset part="control" class="${fieldsetClass}"><legend class="field-legend" id="${legendId}">${labelHidden ? "" : this.label ?? ""}${this.required && !labelHidden ? html3`<span class="required-mark" aria-hidden="true">*</span>` : ""}</legend>
835
866
  ${!this.multiline ? leadingIconTemplate : ""}
836
867
  ${inputTemplate}
837
868
  </fieldset>
@@ -841,10 +872,10 @@ var RelTextfield = class extends LitElement3 {
841
872
  }
842
873
  return html3`
843
874
  <div class="${wrapperClasses}">
844
- ${!this.floatingLabel ? labelTemplate() : ""}
875
+ ${labelHidden ? html3`<div class="label-reserve" aria-hidden="true"></div>` : labelStyle !== "floating" ? labelTemplate() : ""}
845
876
 
846
877
  <div part="control" class="${["textfield-container", this.isDisabled ? "disabled" : "", this.multiline ? "multiline" : ""].filter(Boolean).join(" ")}">
847
- ${this.floatingLabel ? labelTemplate(leadingIcon && !this.multiline ? " has-left-icon" : "") : ""}
878
+ ${labelStyle === "floating" ? labelTemplate(leadingIcon && !this.multiline ? " has-left-icon" : "") : ""}
848
879
  ${!this.multiline ? leadingIconTemplate : ""}
849
880
 
850
881
  ${inputTemplate}
@@ -903,6 +934,22 @@ RelTextfield.styles = css3`
903
934
  margin-left: 2px;
904
935
  }
905
936
 
937
+ /* ── labelStyle="none" ──
938
+ No label renders. An empty spacer holds the label row's full height so the
939
+ input box still bottom-aligns with labelled siblings in a form row.
940
+ The reserve covers label line-height + the wrapper gap, and the negative
941
+ margin cancels the flex gap that would otherwise sit below it — so
942
+ --rel-field-label-reserve: 0 collapses the row to genuinely zero
943
+ (toolbars, table cells), not just to the leftover gap. */
944
+ .label-reserve {
945
+ flex: none;
946
+ height: var(
947
+ --rel-field-label-reserve,
948
+ calc(var(--rel-body-sm-medium-line-height) + var(--rel-spacing-2xs))
949
+ );
950
+ margin-bottom: calc(-1 * var(--rel-spacing-2xs));
951
+ }
952
+
906
953
  /* Floating label variant */
907
954
  :host([floatinglabel]) .textfield-wrapper {
908
955
  gap: 0;
@@ -1279,6 +1326,12 @@ RelTextfield.styles = css3`
1279
1326
  box-sizing: border-box;
1280
1327
  }
1281
1328
 
1329
+ /* labelStyle="none" + legend: nothing to seat in the notch, so drop the
1330
+ legend entirely and let the top border run continuous. */
1331
+ :host([label-style="none"][variant="legend"]) .field-legend {
1332
+ display: none;
1333
+ }
1334
+
1282
1335
  /* Prefix icon (legend): vertically centered; input inset via has-left-icon */
1283
1336
  :host([variant="legend"]) .icon.left {
1284
1337
  top: 50%;
@@ -1394,9 +1447,18 @@ __decorateClass([
1394
1447
  __decorateClass([
1395
1448
  property3({ type: String })
1396
1449
  ], RelTextfield.prototype, "size", 2);
1450
+ __decorateClass([
1451
+ property3({ type: String, attribute: "label-style", reflect: true })
1452
+ ], RelTextfield.prototype, "labelStyle", 2);
1397
1453
  __decorateClass([
1398
1454
  property3({ type: Boolean, reflect: true })
1399
1455
  ], RelTextfield.prototype, "floatingLabel", 2);
1456
+ __decorateClass([
1457
+ property3({ type: String, attribute: "field-aria-label" })
1458
+ ], RelTextfield.prototype, "fieldAriaLabel", 2);
1459
+ __decorateClass([
1460
+ property3({ type: String, attribute: "field-aria-labelledby" })
1461
+ ], RelTextfield.prototype, "fieldAriaLabelledby", 2);
1400
1462
  __decorateClass([
1401
1463
  property3({ type: String, reflect: true })
1402
1464
  ], RelTextfield.prototype, "variant", 2);
@@ -1509,6 +1571,11 @@ var RelAutocomplete = class extends LitElement4 {
1509
1571
  this.panelStyle = `top:${top}px;left:${left}px;width:${t.width}px;`;
1510
1572
  };
1511
1573
  }
1574
+ /** Name used for the suggestion panel — the visible label, or fieldAriaLabel
1575
+ * once labelStyle="none" hides it. Mirrors rel-textfield's own resolution. */
1576
+ get accessibleName() {
1577
+ return (this.labelStyle === "none" ? this.fieldAriaLabel : this.label) || "";
1578
+ }
1512
1579
  disconnectedCallback() {
1513
1580
  super.disconnectedCallback();
1514
1581
  this.removeGlobalListeners();
@@ -1613,17 +1680,17 @@ var RelAutocomplete = class extends LitElement4 {
1613
1680
  this.syncPanel();
1614
1681
  }
1615
1682
  }
1616
- async runSearch(query10) {
1683
+ async runSearch(query11) {
1617
1684
  const seq = ++this.searchSeq;
1618
1685
  try {
1619
- const results = await this.searchFn(query10);
1686
+ const results = await this.searchFn(query11);
1620
1687
  if (seq !== this.searchSeq) return;
1621
1688
  this.remoteItems = results ?? [];
1622
1689
  } catch (err) {
1623
1690
  if (seq !== this.searchSeq) return;
1624
1691
  this.remoteItems = [];
1625
1692
  this.dispatchEvent(
1626
- new CustomEvent("rel-search-error", { detail: { query: query10, error: err }, bubbles: true, composed: true })
1693
+ new CustomEvent("rel-search-error", { detail: { query: query11, error: err }, bubbles: true, composed: true })
1627
1694
  );
1628
1695
  } finally {
1629
1696
  if (seq === this.searchSeq) {
@@ -1769,7 +1836,10 @@ var RelAutocomplete = class extends LitElement4 {
1769
1836
  .value="${this.inputText}"
1770
1837
  .size="${this.size}"
1771
1838
  .variant="${this.variant}"
1839
+ .labelStyle="${this.labelStyle}"
1772
1840
  .floatingLabel="${this.floatingLabel}"
1841
+ .fieldAriaLabel="${this.fieldAriaLabel}"
1842
+ .fieldAriaLabelledby="${this.fieldAriaLabelledby}"
1773
1843
  .prefixIcon="${this.prefixIcon}"
1774
1844
  .disabled="${this.disabled}"
1775
1845
  .readonly="${this.readonly}"
@@ -1791,7 +1861,7 @@ var RelAutocomplete = class extends LitElement4 {
1791
1861
  part="panel list"
1792
1862
  class="ac-panel"
1793
1863
  role="listbox"
1794
- aria-label="${this.label || "Suggestions"}"
1864
+ aria-label="${this.accessibleName || "Suggestions"}"
1795
1865
  style="${this.panelStyle}"
1796
1866
  ?data-open="${this.open}"
1797
1867
  >
@@ -1970,9 +2040,18 @@ __decorateClass([
1970
2040
  __decorateClass([
1971
2041
  property4({ type: String })
1972
2042
  ], RelAutocomplete.prototype, "variant", 2);
2043
+ __decorateClass([
2044
+ property4({ type: String, attribute: "label-style", reflect: true })
2045
+ ], RelAutocomplete.prototype, "labelStyle", 2);
1973
2046
  __decorateClass([
1974
2047
  property4({ type: Boolean, attribute: "floating-label" })
1975
2048
  ], RelAutocomplete.prototype, "floatingLabel", 2);
2049
+ __decorateClass([
2050
+ property4({ type: String, attribute: "field-aria-label" })
2051
+ ], RelAutocomplete.prototype, "fieldAriaLabel", 2);
2052
+ __decorateClass([
2053
+ property4({ type: String, attribute: "field-aria-labelledby" })
2054
+ ], RelAutocomplete.prototype, "fieldAriaLabelledby", 2);
1976
2055
  __decorateClass([
1977
2056
  property4({ type: String, attribute: "prefix-icon" })
1978
2057
  ], RelAutocomplete.prototype, "prefixIcon", 2);
@@ -2684,8 +2763,8 @@ RelButton.styles = css8`
2684
2763
  pointer-events:none on the HOST would make it transparent to the
2685
2764
  pointer and suppress the cursor entirely. */
2686
2765
  cursor: not-allowed;
2687
- /* The disabled tokens are full-contrast pairs (light: #0B0B0B on
2688
- #C2C2C2, dark: #FFF on #404040), so on their own they render a
2766
+ /* The disabled tokens are full-contrast pairs (light: near-black on
2767
+ gray-300, dark: white on gray-700), so on their own they render a
2689
2768
  crisp button that reads as enabled. Dulling happens here instead —
2690
2769
  same opacity mechanism the rest of the library uses. */
2691
2770
  opacity: 0.5;
@@ -8230,6 +8309,8 @@ var RelRadio = class extends LitElement23 {
8230
8309
  super(...arguments);
8231
8310
  this.options = [];
8232
8311
  this.name = "radio-group";
8312
+ this.orientation = "vertical";
8313
+ this.size = "medium";
8233
8314
  this.selectedValue = null;
8234
8315
  this.disabled = false;
8235
8316
  this.error = false;
@@ -8295,7 +8376,12 @@ var RelRadio = class extends LitElement23 {
8295
8376
  }
8296
8377
  render() {
8297
8378
  return html23`
8298
- <div part="base" class="rel-radio-group">
8379
+ <div
8380
+ part="base"
8381
+ class="rel-radio-group"
8382
+ role="radiogroup"
8383
+ aria-orientation="${this.orientation}"
8384
+ >
8299
8385
  ${this.options.map((o) => this.renderOption(o))}
8300
8386
  </div>
8301
8387
  ${this.error && this.errorMessage ? html23`<div class="error-message">${this.errorMessage}</div>` : ""}
@@ -8316,6 +8402,15 @@ RelRadio.styles = css23`
8316
8402
  gap: var(--rel-spacing-xs);
8317
8403
  }
8318
8404
 
8405
+ /* Horizontal orientation — options flow in a wrapping row. Wider column
8406
+ gap than the vertical rhythm so adjacent option labels read as separate
8407
+ choices; row gap keeps the vertical value for wrapped lines. */
8408
+ :host([orientation="horizontal"]) .rel-radio-group {
8409
+ flex-direction: row;
8410
+ flex-wrap: wrap;
8411
+ gap: var(--rel-spacing-xs) var(--rel-spacing-xl);
8412
+ }
8413
+
8319
8414
  .rel-radio-option {
8320
8415
  display: inline-flex;
8321
8416
  flex-direction: column;
@@ -8418,6 +8513,34 @@ RelRadio.styles = css23`
8418
8513
 
8419
8514
  }
8420
8515
 
8516
+ /* Size variant (medium is the base scale above). Supporting-text indent =
8517
+ circle width + the radio-main gap, so it stays aligned under the label. */
8518
+ :host([size="small"]) .radio-custom {
8519
+ width: 16px;
8520
+ height: 16px;
8521
+ }
8522
+
8523
+ :host([size="small"]) input:checked + .radio-custom::after {
8524
+ inset: 3px;
8525
+ }
8526
+
8527
+ :host([size="small"]) .radio-label {
8528
+ font-family: var(--rel-body-md-regular-font-family);
8529
+ font-weight: var(--rel-body-md-regular-font-weight);
8530
+ font-size: var(--rel-body-md-regular-font-size);
8531
+ line-height: var(--rel-body-md-regular-line-height);
8532
+ letter-spacing: var(--rel-body-md-regular-letter-spacing);
8533
+ }
8534
+
8535
+ :host([size="small"]) .radio-supporting {
8536
+ margin-left: 24px;
8537
+ font-family: var(--rel-body-xs-regular-font-family);
8538
+ font-weight: var(--rel-body-xs-regular-font-weight);
8539
+ font-size: var(--rel-body-xs-regular-font-size);
8540
+ line-height: var(--rel-body-xs-regular-line-height);
8541
+ letter-spacing: var(--rel-body-xs-regular-letter-spacing);
8542
+ }
8543
+
8421
8544
  /* Error state (Figma State=Error, both Is Checked values) */
8422
8545
  .rel-radio-option.error-unchecked .radio-custom,
8423
8546
  .rel-radio-option.error-checked .radio-custom {
@@ -8461,6 +8584,12 @@ __decorateClass([
8461
8584
  __decorateClass([
8462
8585
  property22({ type: String })
8463
8586
  ], RelRadio.prototype, "name", 2);
8587
+ __decorateClass([
8588
+ property22({ type: String, reflect: true })
8589
+ ], RelRadio.prototype, "orientation", 2);
8590
+ __decorateClass([
8591
+ property22({ type: String, reflect: true })
8592
+ ], RelRadio.prototype, "size", 2);
8464
8593
  __decorateClass([
8465
8594
  property22({ type: String, reflect: true })
8466
8595
  ], RelRadio.prototype, "selectedValue", 2);
@@ -13060,6 +13189,7 @@ var RelSelect = class extends LitElement46 {
13060
13189
  this.internalState = "default";
13061
13190
  this.visibleChipCount = Infinity;
13062
13191
  this.uniqueId = `select-${Math.random().toString(36).slice(2)}`;
13192
+ this._warnedNoName = false;
13063
13193
  this.onOutsidePointerDown = (e) => {
13064
13194
  if (!this.open) return;
13065
13195
  const path = e.composedPath();
@@ -13118,6 +13248,13 @@ var RelSelect = class extends LitElement46 {
13118
13248
  }
13119
13249
  }
13120
13250
  updated(changed) {
13251
+ if (!this._warnedNoName && this.labelStyle === "none" && !this.fieldAriaLabel && !this.fieldAriaLabelledby && !this.getAttribute("aria-label")) {
13252
+ this._warnedNoName = true;
13253
+ console.warn(
13254
+ '<rel-select label-style="none"> has no accessible name. Set `field-aria-label` or `field-aria-labelledby` \u2014 a placeholder is not a valid naming mechanism.',
13255
+ this
13256
+ );
13257
+ }
13121
13258
  if (changed.has("selectedValues") || changed.has("options") || changed.has("multiple") || changed.has("size")) {
13122
13259
  requestAnimationFrame(() => this.computeVisibleChips());
13123
13260
  }
@@ -13366,7 +13503,9 @@ var RelSelect = class extends LitElement46 {
13366
13503
  const opts = this.normalizedOptions;
13367
13504
  const filtered = this.filteredOptions;
13368
13505
  const isDisabled = this.state === "disabled";
13506
+ const labelHidden = this.labelStyle === "none";
13369
13507
  const showFloatingChrome = this.labelStyle === "floating" && !!this.label;
13508
+ const accessibleName = (labelHidden ? this.fieldAriaLabel : this.label) || "";
13370
13509
  const wrapperClasses = [
13371
13510
  "select-wrapper",
13372
13511
  this.size,
@@ -13382,7 +13521,7 @@ var RelSelect = class extends LitElement46 {
13382
13521
  const selectedSingle = opts.find((o) => o.value === this.selected);
13383
13522
  return html46`
13384
13523
  <div class="${wrapperClasses}">
13385
- ${!showFloatingChrome && this.label ? html46`<label part="label" class="select-label" for="${this.uniqueId}">${this.label}</label>` : ""}
13524
+ ${labelHidden ? html46`<div class="label-reserve" aria-hidden="true"></div>` : !showFloatingChrome && this.label ? html46`<label part="label" class="select-label" for="${this.uniqueId}">${this.label}</label>` : ""}
13386
13525
 
13387
13526
  <div class="select-container">
13388
13527
  ${showFloatingChrome ? html46`<span class="floating-label${this.leftIcon ? " has-left-icon" : ""}"
@@ -13399,7 +13538,8 @@ var RelSelect = class extends LitElement46 {
13399
13538
  aria-controls="${listboxId}"
13400
13539
  aria-activedescendant="${activeOptionId}"
13401
13540
  aria-invalid="${this.state === "error"}"
13402
- aria-label="${this.label || this.placeholder}"
13541
+ aria-label="${this.fieldAriaLabelledby ? nothing9 : accessibleName || this.placeholder}"
13542
+ aria-labelledby="${this.fieldAriaLabelledby ?? nothing9}"
13403
13543
  ?disabled="${isDisabled}"
13404
13544
  @click="${this.onTriggerClick}"
13405
13545
  @keydown="${this.onTriggerKeyDown}"
@@ -13436,7 +13576,7 @@ var RelSelect = class extends LitElement46 {
13436
13576
  part="panel list" class="select-listbox"
13437
13577
  role="listbox"
13438
13578
  aria-multiselectable="${this.multiple}"
13439
- aria-label="${this.label || "Options"}"
13579
+ aria-label="${accessibleName || "Options"}"
13440
13580
  style="${this.panelStyle}"
13441
13581
  ?data-open="${this.open}"
13442
13582
  >
@@ -13565,6 +13705,22 @@ RelSelect.styles = css43`
13565
13705
 
13566
13706
  }
13567
13707
 
13708
+ /* ── labelStyle="none" ──
13709
+ No label renders. An empty spacer holds the label row's full height so the
13710
+ trigger still bottom-aligns with labelled siblings in a form row.
13711
+ The reserve covers label line-height + the wrapper gap, and the negative
13712
+ margin cancels the flex gap that would otherwise sit below it — so
13713
+ --rel-field-label-reserve: 0 collapses the row to genuinely zero
13714
+ (toolbars, table cells), not just to the leftover gap. */
13715
+ .label-reserve {
13716
+ flex: none;
13717
+ height: var(
13718
+ --rel-field-label-reserve,
13719
+ calc(var(--rel-body-sm-regular-line-height) + var(--rel-spacing-2xs))
13720
+ );
13721
+ margin-bottom: calc(-1 * var(--rel-spacing-2xs));
13722
+ }
13723
+
13568
13724
  /* Error only colors the border/focus ring and the supporting text below
13569
13725
  the field — label, floating label, placeholder, and icons stay normal. */
13570
13726
 
@@ -13958,8 +14114,14 @@ __decorateClass([
13958
14114
  property40({ type: String })
13959
14115
  ], RelSelect.prototype, "label", 2);
13960
14116
  __decorateClass([
13961
- property40({ type: String, attribute: "label-style" })
14117
+ property40({ type: String, attribute: "label-style", reflect: true })
13962
14118
  ], RelSelect.prototype, "labelStyle", 2);
14119
+ __decorateClass([
14120
+ property40({ type: String, attribute: "field-aria-label" })
14121
+ ], RelSelect.prototype, "fieldAriaLabel", 2);
14122
+ __decorateClass([
14123
+ property40({ type: String, attribute: "field-aria-labelledby" })
14124
+ ], RelSelect.prototype, "fieldAriaLabelledby", 2);
13963
14125
  __decorateClass([
13964
14126
  property40({ type: Array })
13965
14127
  ], RelSelect.prototype, "options", 2);
@@ -16558,88 +16720,1030 @@ RelTab = __decorateClass([
16558
16720
  customElement53("rel-tab")
16559
16721
  ], RelTab);
16560
16722
 
16561
- // src/components/rel-toast/rel-toast.ts
16562
- import { LitElement as LitElement54, html as html54, css as css51 } from "lit";
16563
- import { customElement as customElement54, property as property48, state as state21 } from "lit/decorators.js";
16564
- var RelToast = class extends LitElement54 {
16723
+ // src/components/rel-time-picker/rel-time-picker.ts
16724
+ import { LitElement as LitElement54, html as html54, css as css51, nothing as nothing10 } from "lit";
16725
+ import { customElement as customElement54, property as property48, state as state21, query as query10 } from "lit/decorators.js";
16726
+ import "@reliability-design/icons/rel-icon";
16727
+ var RelTimePicker = class extends LitElement54 {
16565
16728
  constructor() {
16566
16729
  super(...arguments);
16567
- this.message = "";
16568
- this.type = "default";
16569
- this.duration = 4e3;
16570
- this.show = false;
16571
- this.visible = false;
16572
- this._dismissTimer = null;
16573
- }
16574
- willUpdate(changed) {
16575
- if (!changed.has("show")) return;
16576
- if (this.show) {
16577
- this.visible = true;
16578
- if (this._dismissTimer) clearTimeout(this._dismissTimer);
16579
- if (this.duration > 0) {
16580
- this._dismissTimer = setTimeout(() => this.close(), this.duration);
16730
+ this.value = null;
16731
+ this.placeholder = "Select time";
16732
+ this.label = null;
16733
+ this.disabled = false;
16734
+ this.required = false;
16735
+ this.readonly = false;
16736
+ this.format = "12h";
16737
+ this.minuteStep = 1;
16738
+ this.minTime = null;
16739
+ this.maxTime = null;
16740
+ this.open = false;
16741
+ this.clearable = false;
16742
+ this.error = false;
16743
+ this.invalid = false;
16744
+ this.size = "medium";
16745
+ this.variant = "outlined";
16746
+ this.fullWidth = false;
16747
+ this.draftHour = null;
16748
+ this.draftMinute = null;
16749
+ this.handleClickOutside = (e) => {
16750
+ if (!this.open) return;
16751
+ const path = e.composedPath();
16752
+ if (!path.includes(this)) {
16753
+ this.cancelPopover();
16581
16754
  }
16582
- } else {
16583
- this.visible = false;
16584
- if (this._dismissTimer) {
16585
- clearTimeout(this._dismissTimer);
16586
- this._dismissTimer = null;
16755
+ };
16756
+ this.handleEscapeKey = (e) => {
16757
+ if (e.key === "Escape" && this.open) {
16758
+ this.cancelPopover();
16587
16759
  }
16588
- }
16760
+ };
16761
+ }
16762
+ connectedCallback() {
16763
+ super.connectedCallback();
16764
+ document.addEventListener("click", this.handleClickOutside);
16765
+ document.addEventListener("keydown", this.handleEscapeKey);
16589
16766
  }
16590
16767
  disconnectedCallback() {
16591
16768
  super.disconnectedCallback();
16592
- if (this._dismissTimer) clearTimeout(this._dismissTimer);
16769
+ document.removeEventListener("click", this.handleClickOutside);
16770
+ document.removeEventListener("keydown", this.handleEscapeKey);
16593
16771
  }
16594
- open() {
16595
- this.show = true;
16772
+ updated(changedProperties) {
16773
+ if (changedProperties.has("open") && this.open) {
16774
+ this.resetDraftFromValue();
16775
+ setTimeout(() => {
16776
+ this.scrollColumnsToSelection();
16777
+ const target = this.columnsElement?.querySelector(
16778
+ ".option-button.selected:not(:disabled)"
16779
+ ) ?? this.columnsElement?.querySelector(
16780
+ ".option-button:not(:disabled)"
16781
+ );
16782
+ target?.focus();
16783
+ }, 0);
16784
+ }
16596
16785
  }
16597
- close() {
16598
- this.show = false;
16599
- this.visible = false;
16600
- if (this._dismissTimer) {
16601
- clearTimeout(this._dismissTimer);
16602
- this._dismissTimer = null;
16786
+ // ── Parsing / formatting ──
16787
+ parseTime(time) {
16788
+ if (!time) return null;
16789
+ const match = time.match(/^(\d{1,2}):(\d{2})$/);
16790
+ if (!match) return null;
16791
+ const hour = parseInt(match[1], 10);
16792
+ const minute = parseInt(match[2], 10);
16793
+ if (hour > 23 || minute > 59) return null;
16794
+ return { hour, minute };
16795
+ }
16796
+ formatValue(hour, minute) {
16797
+ return `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
16798
+ }
16799
+ formatDisplay(hour, minute) {
16800
+ const mm = String(minute).padStart(2, "0");
16801
+ if (this.format === "24h") {
16802
+ return `${String(hour).padStart(2, "0")}:${mm}`;
16803
+ }
16804
+ const period = hour < 12 ? "AM" : "PM";
16805
+ const hour12 = hour % 12 === 0 ? 12 : hour % 12;
16806
+ return `${hour12}:${mm} ${period}`;
16807
+ }
16808
+ toMinutes(hour, minute) {
16809
+ return hour * 60 + minute;
16810
+ }
16811
+ get minuteStepSafe() {
16812
+ const step = Math.floor(this.minuteStep);
16813
+ return step >= 1 && step <= 60 ? step : 1;
16814
+ }
16815
+ get minBoundMinutes() {
16816
+ const parsed = this.parseTime(this.minTime);
16817
+ return parsed ? this.toMinutes(parsed.hour, parsed.minute) : 0;
16818
+ }
16819
+ get maxBoundMinutes() {
16820
+ const parsed = this.parseTime(this.maxTime);
16821
+ return parsed ? this.toMinutes(parsed.hour, parsed.minute) : 23 * 60 + 59;
16822
+ }
16823
+ isTimeInRange(hour, minute) {
16824
+ const total = this.toMinutes(hour, minute);
16825
+ return total >= this.minBoundMinutes && total <= this.maxBoundMinutes;
16826
+ }
16827
+ /** Minutes shown in the minutes column (respecting minuteStep). */
16828
+ getMinuteOptions() {
16829
+ const minutes = [];
16830
+ for (let m = 0; m < 60; m += this.minuteStepSafe) {
16831
+ minutes.push(m);
16832
+ }
16833
+ return minutes;
16834
+ }
16835
+ /** An hour is enabled if at least one of its step minutes is in range. */
16836
+ isHourEnabled(hour) {
16837
+ return this.getMinuteOptions().some((m) => this.isTimeInRange(hour, m));
16838
+ }
16839
+ isPeriodEnabled(period) {
16840
+ const start = period === "AM" ? 0 : 12;
16841
+ for (let h = start; h < start + 12; h++) {
16842
+ if (this.isHourEnabled(h)) return true;
16843
+ }
16844
+ return false;
16845
+ }
16846
+ /** Hour column entries: display label + the 0–23 hour it maps to. */
16847
+ getHourOptions() {
16848
+ if (this.format === "24h") {
16849
+ return Array.from({ length: 24 }, (_, h) => ({
16850
+ label: String(h).padStart(2, "0"),
16851
+ hour24: h
16852
+ }));
16853
+ }
16854
+ const period = this.draftPeriod ?? "AM";
16855
+ const base = period === "AM" ? 0 : 12;
16856
+ return Array.from({ length: 12 }, (_, i) => ({
16857
+ label: i === 0 ? "12" : String(i).padStart(2, "0"),
16858
+ hour24: base + i
16859
+ }));
16860
+ }
16861
+ get draftPeriod() {
16862
+ if (this.draftHour === null) return null;
16863
+ return this.draftHour < 12 ? "AM" : "PM";
16864
+ }
16865
+ resetDraftFromValue() {
16866
+ const parsed = this.parseTime(this.value);
16867
+ this.draftHour = parsed ? parsed.hour : null;
16868
+ this.draftMinute = parsed ? parsed.minute : null;
16869
+ }
16870
+ /** Snap the draft minute to the nearest valid step minute for the hour. */
16871
+ clampDraftToRange() {
16872
+ if (this.draftHour === null || this.draftMinute === null) return;
16873
+ if (this.isTimeInRange(this.draftHour, this.draftMinute)) return;
16874
+ const hour = this.draftHour;
16875
+ const valid = this.getMinuteOptions().filter((m) => this.isTimeInRange(hour, m));
16876
+ if (valid.length === 0) {
16877
+ this.draftMinute = null;
16878
+ return;
16603
16879
  }
16880
+ const current = this.draftMinute;
16881
+ this.draftMinute = valid.reduce(
16882
+ (best, m) => Math.abs(m - current) < Math.abs(best - current) ? m : best
16883
+ );
16884
+ }
16885
+ // ── Open / close / commit ──
16886
+ toggleTimePopover() {
16887
+ if (this.disabled || this.readonly) return;
16888
+ this.open = !this.open;
16604
16889
  this.dispatchEvent(
16605
- new CustomEvent("dismissed", {
16890
+ new CustomEvent("rel-open-change", {
16891
+ detail: { open: this.open },
16606
16892
  bubbles: true,
16607
16893
  composed: true
16608
16894
  })
16609
16895
  );
16610
16896
  }
16611
- handleActionClick(event) {
16612
- event.stopPropagation();
16897
+ /** Close discarding the draft (Cancel / Escape / outside click). */
16898
+ cancelPopover() {
16899
+ this.open = false;
16900
+ this.triggerButton?.focus();
16613
16901
  this.dispatchEvent(
16614
- new CustomEvent("action", {
16902
+ new CustomEvent("rel-open-change", {
16903
+ detail: { open: false },
16615
16904
  bubbles: true,
16616
16905
  composed: true
16617
16906
  })
16618
16907
  );
16619
- this.close();
16620
16908
  }
16909
+ applySelection() {
16910
+ if (this.draftHour === null) return;
16911
+ const newValue = this.formatValue(this.draftHour, this.draftMinute ?? 0);
16912
+ this.value = newValue;
16913
+ this.dispatchEvent(
16914
+ new CustomEvent("rel-change", {
16915
+ detail: { value: newValue },
16916
+ bubbles: true,
16917
+ composed: true
16918
+ })
16919
+ );
16920
+ this.cancelPopover();
16921
+ }
16922
+ /** Set the draft to the current time, snapped to the minute step and range. */
16923
+ selectNow() {
16924
+ const now = /* @__PURE__ */ new Date();
16925
+ const step = this.minuteStepSafe;
16926
+ this.draftHour = now.getHours();
16927
+ this.draftMinute = Math.round(now.getMinutes() / step) * step;
16928
+ if (this.draftMinute >= 60) {
16929
+ this.draftMinute = 0;
16930
+ this.draftHour = (this.draftHour + 1) % 24;
16931
+ }
16932
+ if (!this.isHourEnabled(this.draftHour)) {
16933
+ const enabled = [];
16934
+ for (let h = 0; h < 24; h++) if (this.isHourEnabled(h)) enabled.push(h);
16935
+ if (enabled.length === 0) return;
16936
+ const currentHour = this.draftHour;
16937
+ this.draftHour = enabled.reduce(
16938
+ (best, h) => Math.abs(h - currentHour) < Math.abs(best - currentHour) ? h : best
16939
+ );
16940
+ }
16941
+ this.clampDraftToRange();
16942
+ this.scrollColumnsToSelection();
16943
+ }
16944
+ clearValue(e) {
16945
+ e.stopPropagation();
16946
+ this.value = null;
16947
+ this.draftHour = null;
16948
+ this.draftMinute = null;
16949
+ this.dispatchEvent(
16950
+ new CustomEvent("rel-change", {
16951
+ detail: { value: null },
16952
+ bubbles: true,
16953
+ composed: true
16954
+ })
16955
+ );
16956
+ }
16957
+ // ── Column selection ──
16958
+ selectHour(hour24) {
16959
+ if (!this.isHourEnabled(hour24)) return;
16960
+ this.draftHour = hour24;
16961
+ if (this.draftMinute === null) this.draftMinute = 0;
16962
+ this.clampDraftToRange();
16963
+ }
16964
+ selectMinute(minute) {
16965
+ if (this.draftHour === null) {
16966
+ for (let h = 0; h < 24; h++) {
16967
+ if (this.isHourEnabled(h)) {
16968
+ this.draftHour = h;
16969
+ break;
16970
+ }
16971
+ }
16972
+ if (this.draftHour === null) return;
16973
+ }
16974
+ if (!this.isTimeInRange(this.draftHour, minute)) return;
16975
+ this.draftMinute = minute;
16976
+ }
16977
+ selectPeriod(period) {
16978
+ if (!this.isPeriodEnabled(period)) return;
16979
+ const base = period === "AM" ? 0 : 12;
16980
+ const hour12 = this.draftHour === null ? 0 : this.draftHour % 12;
16981
+ this.draftHour = base + hour12;
16982
+ if (this.draftMinute === null) this.draftMinute = 0;
16983
+ if (!this.isHourEnabled(this.draftHour)) {
16984
+ const enabled = [];
16985
+ for (let h = base; h < base + 12; h++) if (this.isHourEnabled(h)) enabled.push(h);
16986
+ const currentHour = this.draftHour;
16987
+ this.draftHour = enabled.reduce(
16988
+ (best, h) => Math.abs(h - currentHour) < Math.abs(best - currentHour) ? h : best
16989
+ );
16990
+ }
16991
+ this.clampDraftToRange();
16992
+ this.scrollColumnsToSelection();
16993
+ }
16994
+ scrollColumnsToSelection() {
16995
+ this.updateComplete.then(() => {
16996
+ this.columnsElement?.querySelectorAll(".option-button.selected").forEach((btn) => btn.scrollIntoView({ block: "center" }));
16997
+ });
16998
+ }
16999
+ // ── Keyboard ──
17000
+ handleTriggerKeydown(e) {
17001
+ if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown") {
17002
+ e.preventDefault();
17003
+ if (!this.open) {
17004
+ this.toggleTimePopover();
17005
+ }
17006
+ }
17007
+ }
17008
+ /**
17009
+ * Column navigation: Up/Down move within the column (and select), Left/Right
17010
+ * jump between columns, Home/End go to the first/last option, Enter applies.
17011
+ */
17012
+ handleOptionKeydown(e, column) {
17013
+ const list = e.currentTarget.closest(".column-list");
17014
+ if (!list) return;
17015
+ const buttons = Array.from(
17016
+ list.querySelectorAll(".option-button:not(:disabled)")
17017
+ );
17018
+ const index = buttons.indexOf(e.currentTarget);
17019
+ let target;
17020
+ switch (e.key) {
17021
+ case "ArrowUp":
17022
+ e.preventDefault();
17023
+ target = buttons[Math.max(0, index - 1)];
17024
+ break;
17025
+ case "ArrowDown":
17026
+ e.preventDefault();
17027
+ target = buttons[Math.min(buttons.length - 1, index + 1)];
17028
+ break;
17029
+ case "Home":
17030
+ e.preventDefault();
17031
+ target = buttons[0];
17032
+ break;
17033
+ case "End":
17034
+ e.preventDefault();
17035
+ target = buttons[buttons.length - 1];
17036
+ break;
17037
+ case "ArrowLeft":
17038
+ case "ArrowRight": {
17039
+ e.preventDefault();
17040
+ this.focusAdjacentColumn(column, e.key === "ArrowRight" ? 1 : -1);
17041
+ return;
17042
+ }
17043
+ case "Enter":
17044
+ e.preventDefault();
17045
+ e.currentTarget.click();
17046
+ this.applySelection();
17047
+ return;
17048
+ default:
17049
+ return;
17050
+ }
17051
+ if (target && target !== buttons[index]) {
17052
+ target.click();
17053
+ target.focus();
17054
+ target.scrollIntoView({ block: "nearest" });
17055
+ }
17056
+ }
17057
+ focusAdjacentColumn(from, direction) {
17058
+ const order = this.format === "12h" ? ["hour", "minute", "period"] : ["hour", "minute"];
17059
+ const nextIndex = order.indexOf(from) + direction;
17060
+ if (nextIndex < 0 || nextIndex >= order.length) return;
17061
+ const lists = this.columnsElement?.querySelectorAll(".column-list");
17062
+ const list = lists?.[nextIndex];
17063
+ const target = list?.querySelector(".option-button.selected:not(:disabled)") ?? list?.querySelector(".option-button:not(:disabled)");
17064
+ target?.focus();
17065
+ target?.scrollIntoView({ block: "nearest" });
17066
+ }
17067
+ // ── Render ──
16621
17068
  render() {
17069
+ const isError = this.error || this.invalid;
17070
+ const belowText = isError && this.errorMessage ? this.errorMessage : this.helperText;
17071
+ const describedById = belowText ? "time-picker-desc" : void 0;
17072
+ const parsedValue = this.parseTime(this.value);
17073
+ const displayText = parsedValue ? this.formatDisplay(parsedValue.hour, parsedValue.minute) : "";
17074
+ const draftDisplay = this.draftHour !== null ? this.formatDisplay(this.draftHour, this.draftMinute ?? 0) : "";
17075
+ const hourOptions = this.getHourOptions();
17076
+ const minuteOptions = this.getMinuteOptions();
17077
+ const draftPeriod = this.draftPeriod;
17078
+ const wrapperClasses = [
17079
+ "time-picker-wrapper",
17080
+ this.size,
17081
+ isError ? "error" : "",
17082
+ this.readonly ? "readonly" : ""
17083
+ ].filter(Boolean).join(" ");
16622
17084
  return html54`
16623
- <div part="base" class="toast ${this.type} ${this.visible ? "" : "hidden"}" role="status" aria-live="polite">
16624
- <span part="label" class="toast-message">${this.message}</span>
17085
+ <div part="base" class="${wrapperClasses}">
17086
+ ${this.label ? html54`<label part="label" class="label" id="time-picker-label"
17087
+ >${this.label}${this.required ? html54`<span class="required-mark" aria-hidden="true">*</span>` : nothing10}</label
17088
+ >` : nothing10}
16625
17089
 
16626
- ${this.actionLabel ? html54`<button
16627
- part="action" class="toast-action"
16628
- @click="${this.handleActionClick}"
16629
- >
16630
- ${this.actionLabel}
16631
- </button>` : ""}
17090
+ ${this.name ? html54`<input type="hidden" name=${this.name} .value=${this.value ?? ""} />` : nothing10}
16632
17091
 
16633
- <button part="close-button" class="toast-close" @click="${this.close}" aria-label="Dismiss">✕</button>
16634
- </div>
16635
- `;
16636
- }
16637
- };
16638
- RelToast.styles = css51`
16639
- :host {
16640
- color: var(--rel-text-default);
16641
- position: fixed;
16642
- bottom: 24px;
17092
+ <button
17093
+ part="trigger"
17094
+ class="trigger-button ${isError ? "error" : ""}"
17095
+ type="button"
17096
+ ?disabled=${this.disabled}
17097
+ @click=${this.toggleTimePopover}
17098
+ @keydown=${this.handleTriggerKeydown}
17099
+ @focus=${() => this.dispatchEvent(
17100
+ new CustomEvent("rel-focus", { bubbles: true, composed: true })
17101
+ )}
17102
+ @blur=${() => this.dispatchEvent(
17103
+ new CustomEvent("rel-blur", { bubbles: true, composed: true })
17104
+ )}
17105
+ aria-label=${this.label || "Select time"}
17106
+ aria-haspopup="dialog"
17107
+ aria-expanded=${this.open}
17108
+ aria-required=${this.required}
17109
+ aria-invalid=${isError}
17110
+ aria-readonly=${this.readonly}
17111
+ aria-describedby=${describedById ?? nothing10}
17112
+ aria-labelledby=${this.label ? "time-picker-label" : nothing10}
17113
+ >
17114
+ <span part="value" class="trigger-content ${displayText ? "" : "placeholder"}">
17115
+ ${displayText || this.placeholder}
17116
+ </span>
17117
+ <span class="trigger-icons">
17118
+ ${this.clearable && this.value ? html54`
17119
+ <button
17120
+ class="icon-button"
17121
+ type="button"
17122
+ aria-label="Clear time"
17123
+ @click=${this.clearValue}
17124
+ >
17125
+ <svg viewBox="0 0 16 16" fill="currentColor">
17126
+ <path
17127
+ d="M4 4l8 8M12 4l-8 8"
17128
+ stroke="currentColor"
17129
+ stroke-width="2"
17130
+ stroke-linecap="round"
17131
+ />
17132
+ </svg>
17133
+ </button>
17134
+ ` : nothing10}
17135
+ <rel-icon part="icon" name="time-filled" size="sm"></rel-icon>
17136
+ </span>
17137
+ </button>
17138
+
17139
+ <div
17140
+ part="panel"
17141
+ class="popover ${this.open ? "open" : ""}"
17142
+ role="dialog"
17143
+ aria-label="Choose time"
17144
+ >
17145
+ <div class="time-readout">
17146
+ <span class="time-readout-label">Time</span>
17147
+ <span class="time-readout-value ${draftDisplay ? "" : "empty"}">
17148
+ ${draftDisplay || (this.format === "24h" ? "--:--" : "--:-- --")}
17149
+ </span>
17150
+ </div>
17151
+
17152
+ <div class="columns">
17153
+ <div class="column">
17154
+ <div class="column-header" aria-hidden="true">Hour</div>
17155
+ <div class="column-list" role="listbox" aria-label="Hours">
17156
+ ${hourOptions.map(
17157
+ (opt, i) => html54`
17158
+ <button
17159
+ part="option"
17160
+ class="option-button ${this.draftHour === opt.hour24 ? "selected" : ""}"
17161
+ type="button"
17162
+ role="option"
17163
+ aria-selected=${this.draftHour === opt.hour24}
17164
+ ?disabled=${!this.isHourEnabled(opt.hour24)}
17165
+ @click=${() => this.selectHour(opt.hour24)}
17166
+ @keydown=${(e) => this.handleOptionKeydown(e, "hour")}
17167
+ tabindex=${this.draftHour === opt.hour24 || this.draftHour === null && i === 0 ? 0 : -1}
17168
+ >
17169
+ ${opt.label}
17170
+ </button>
17171
+ `
17172
+ )}
17173
+ </div>
17174
+ </div>
17175
+
17176
+ <div class="column">
17177
+ <div class="column-header" aria-hidden="true">Min</div>
17178
+ <div class="column-list" role="listbox" aria-label="Minutes">
17179
+ ${minuteOptions.map(
17180
+ (m, i) => html54`
17181
+ <button
17182
+ part="option"
17183
+ class="option-button ${this.draftMinute === m ? "selected" : ""}"
17184
+ type="button"
17185
+ role="option"
17186
+ aria-selected=${this.draftMinute === m}
17187
+ ?disabled=${this.draftHour !== null && !this.isTimeInRange(this.draftHour, m)}
17188
+ @click=${() => this.selectMinute(m)}
17189
+ @keydown=${(e) => this.handleOptionKeydown(e, "minute")}
17190
+ tabindex=${this.draftMinute === m || this.draftMinute === null && i === 0 ? 0 : -1}
17191
+ >
17192
+ ${String(m).padStart(2, "0")}
17193
+ </button>
17194
+ `
17195
+ )}
17196
+ </div>
17197
+ </div>
17198
+
17199
+ ${this.format === "12h" ? html54`
17200
+ <div class="column">
17201
+ <div class="column-header" aria-hidden="true">AM/PM</div>
17202
+ <div class="column-list" role="listbox" aria-label="AM or PM">
17203
+ ${["AM", "PM"].map(
17204
+ (p) => html54`
17205
+ <button
17206
+ part="option"
17207
+ class="option-button ${draftPeriod === p ? "selected" : ""}"
17208
+ type="button"
17209
+ role="option"
17210
+ aria-selected=${draftPeriod === p}
17211
+ ?disabled=${!this.isPeriodEnabled(p)}
17212
+ @click=${() => this.selectPeriod(p)}
17213
+ @keydown=${(e) => this.handleOptionKeydown(e, "period")}
17214
+ tabindex=${draftPeriod === p || draftPeriod === null && p === "AM" ? 0 : -1}
17215
+ >
17216
+ ${p}
17217
+ </button>
17218
+ `
17219
+ )}
17220
+ </div>
17221
+ </div>
17222
+ ` : nothing10}
17223
+ </div>
17224
+
17225
+ <div part="footer" class="time-footer">
17226
+ <rel-button
17227
+ variant="secondary"
17228
+ size="small"
17229
+ border="corner-radius"
17230
+ @click=${() => this.selectNow()}
17231
+ >
17232
+ Now
17233
+ </rel-button>
17234
+ <div class="footer-right">
17235
+ <rel-button
17236
+ variant="secondary"
17237
+ size="small"
17238
+ border="corner-radius"
17239
+ @click=${() => this.cancelPopover()}
17240
+ >
17241
+ Cancel
17242
+ </rel-button>
17243
+ <rel-button
17244
+ variant="primary"
17245
+ tone="default"
17246
+ size="small"
17247
+ border="corner-radius"
17248
+ ?disabled=${this.draftHour === null}
17249
+ @click=${() => this.applySelection()}
17250
+ >
17251
+ Apply
17252
+ </rel-button>
17253
+ </div>
17254
+ </div>
17255
+ </div>
17256
+
17257
+ ${belowText ? html54`<div
17258
+ part="helper"
17259
+ class="time-picker-support ${isError ? "error" : ""}"
17260
+ id=${describedById}
17261
+ >
17262
+ ${belowText}
17263
+ </div>` : nothing10}
17264
+ </div>
17265
+ `;
17266
+ }
17267
+ };
17268
+ RelTimePicker.styles = css51`
17269
+ :host {
17270
+ color: var(--rel-text-default);
17271
+ display: block;
17272
+ font-family: var(--rel-fonts-font-family);
17273
+ position: relative;
17274
+ }
17275
+
17276
+ /* Intrinsic width by default; set fullWidth to fill the parent. */
17277
+ :host(:not([fullwidth])) {
17278
+ display: inline-block;
17279
+ width: auto;
17280
+ }
17281
+
17282
+ :host([disabled]) {
17283
+ opacity: 0.6;
17284
+ cursor: not-allowed;
17285
+ }
17286
+
17287
+ :host([disabled]) * {
17288
+ pointer-events: none;
17289
+ }
17290
+
17291
+ :host([readonly]) .trigger-button {
17292
+ cursor: default;
17293
+ }
17294
+
17295
+ .time-picker-wrapper {
17296
+ display: flex;
17297
+ flex-direction: column;
17298
+ gap: var(--rel-spacing-xs);
17299
+ }
17300
+
17301
+ .label {
17302
+ color: var(--rel-text-label);
17303
+ font-family: var(--rel-body-sm-medium-font-family);
17304
+ font-weight: var(--rel-body-sm-medium-font-weight);
17305
+ font-size: var(--rel-body-sm-medium-font-size);
17306
+ line-height: var(--rel-body-sm-medium-line-height);
17307
+ letter-spacing: var(--rel-body-sm-medium-letter-spacing);
17308
+ }
17309
+
17310
+ .required-mark {
17311
+ color: var(--rel-text-danger);
17312
+ margin-left: 2px;
17313
+ }
17314
+
17315
+ /* Support text (helperText / errorMessage) */
17316
+ .time-picker-support {
17317
+ color: var(--rel-text-placeholder);
17318
+ font-family: var(--rel-body-xs-regular-font-family);
17319
+ font-weight: var(--rel-body-xs-regular-font-weight);
17320
+ font-size: var(--rel-body-xs-regular-font-size);
17321
+ line-height: var(--rel-body-xs-regular-line-height);
17322
+ letter-spacing: var(--rel-body-xs-regular-letter-spacing);
17323
+ }
17324
+
17325
+ .time-picker-support.error {
17326
+ color: var(--rel-text-danger);
17327
+ }
17328
+
17329
+ .trigger-button {
17330
+ width: 100%;
17331
+ min-width: 160px;
17332
+ display: flex;
17333
+ align-items: center;
17334
+ justify-content: space-between;
17335
+ gap: var(--rel-spacing-xs);
17336
+ padding: var(--rel-spacing-sm) var(--rel-spacing-md);
17337
+ background: var(--rel-bg-surface);
17338
+ border: 1px solid var(--rel-border-default);
17339
+ border-radius: var(--rel-border-radius-sm);
17340
+ color: var(--rel-text-default);
17341
+ cursor: pointer;
17342
+ transition: all 0.15s ease;
17343
+ text-align: left;
17344
+ font-family: var(--rel-body-md-regular-font-family);
17345
+ font-weight: var(--rel-body-md-regular-font-weight);
17346
+ font-size: var(--rel-body-md-regular-font-size);
17347
+ line-height: var(--rel-body-md-regular-line-height);
17348
+ letter-spacing: var(--rel-body-md-regular-letter-spacing);
17349
+ }
17350
+
17351
+ .trigger-button:hover:not(:disabled) {
17352
+ border-color: var(--rel-border-strong);
17353
+ }
17354
+
17355
+ .trigger-button:focus-visible {
17356
+ outline: 2px solid var(--rel-border-focus);
17357
+ outline-offset: 2px;
17358
+ border-color: var(--rel-border-focus);
17359
+ }
17360
+
17361
+ .trigger-button:disabled {
17362
+ cursor: not-allowed;
17363
+ background: var(--rel-bg-surface-elevated);
17364
+ }
17365
+
17366
+ /* Error state */
17367
+ .trigger-button.error,
17368
+ .trigger-button.error:hover:not(:disabled) {
17369
+ border-color: var(--rel-border-danger);
17370
+ }
17371
+
17372
+ .trigger-button.error:focus-visible {
17373
+ outline-color: var(--rel-border-danger);
17374
+ border-color: var(--rel-border-danger);
17375
+ }
17376
+
17377
+ /* Size variants (medium is the base .trigger-button style) */
17378
+ .time-picker-wrapper.small .trigger-button {
17379
+ padding: var(--rel-spacing-xs) var(--rel-spacing-sm);
17380
+ font-family: var(--rel-body-sm-regular-font-family);
17381
+ font-weight: var(--rel-body-sm-regular-font-weight);
17382
+ font-size: var(--rel-body-sm-regular-font-size);
17383
+ line-height: var(--rel-body-sm-regular-line-height);
17384
+ letter-spacing: var(--rel-body-sm-regular-letter-spacing);
17385
+ }
17386
+
17387
+ .time-picker-wrapper.large .trigger-button {
17388
+ padding: var(--rel-spacing-md) var(--rel-spacing-md);
17389
+ font-family: var(--rel-body-lg-regular-font-family);
17390
+ font-weight: var(--rel-body-lg-regular-font-weight);
17391
+ font-size: var(--rel-body-lg-regular-font-size);
17392
+ line-height: var(--rel-body-lg-regular-line-height);
17393
+ letter-spacing: var(--rel-body-lg-regular-letter-spacing);
17394
+ }
17395
+
17396
+ .trigger-content {
17397
+ flex: 1;
17398
+ color: var(--rel-text-default);
17399
+ }
17400
+
17401
+ .trigger-content.placeholder {
17402
+ color: var(--rel-text-placeholder);
17403
+ }
17404
+
17405
+ .trigger-icons {
17406
+ display: flex;
17407
+ align-items: center;
17408
+ gap: var(--rel-spacing-2xs);
17409
+ color: var(--rel-text-placeholder);
17410
+ }
17411
+
17412
+ .icon-button {
17413
+ background: none;
17414
+ border: none;
17415
+ padding: var(--rel-spacing-3xs);
17416
+ cursor: pointer;
17417
+ color: var(--rel-text-placeholder);
17418
+ display: flex;
17419
+ align-items: center;
17420
+ justify-content: center;
17421
+ border-radius: var(--rel-border-radius-xs);
17422
+ transition: all 0.15s ease;
17423
+ }
17424
+
17425
+ .icon-button:hover {
17426
+ background: var(--rel-bg-surface-elevated);
17427
+ color: var(--rel-text-default);
17428
+ }
17429
+
17430
+ .icon-button svg {
17431
+ width: 16px;
17432
+ height: 16px;
17433
+ }
17434
+
17435
+ .popover {
17436
+ position: absolute;
17437
+ top: calc(100% + 4px);
17438
+ left: 0;
17439
+ z-index: 1000;
17440
+ background: var(--rel-bg-surface);
17441
+ border: 1px solid var(--rel-border-default);
17442
+ border-radius: var(--rel-border-radius-sm);
17443
+ box-shadow: var(--rel-shadows-shadow-300);
17444
+ padding: var(--rel-spacing-md);
17445
+ display: none;
17446
+ }
17447
+
17448
+ .popover.open {
17449
+ display: block;
17450
+ }
17451
+
17452
+ /* Draft readout (mirrors the range-picker FROM/TO block) */
17453
+ .time-readout {
17454
+ display: flex;
17455
+ align-items: baseline;
17456
+ gap: var(--rel-spacing-sm);
17457
+ padding-bottom: var(--rel-spacing-sm);
17458
+ margin-bottom: var(--rel-spacing-sm);
17459
+ border-bottom: 1px solid var(--rel-border-subtle);
17460
+ }
17461
+
17462
+ .time-readout-label {
17463
+ color: var(--rel-text-placeholder);
17464
+ text-transform: uppercase;
17465
+ font-family: var(--rel-body-xs-semibold-font-family);
17466
+ font-weight: var(--rel-body-xs-semibold-font-weight);
17467
+ font-size: var(--rel-body-xs-semibold-font-size);
17468
+ line-height: var(--rel-body-xs-semibold-line-height);
17469
+ letter-spacing: var(--rel-body-xs-semibold-letter-spacing);
17470
+ }
17471
+
17472
+ .time-readout-value {
17473
+ color: var(--rel-text-default);
17474
+ font-family: var(--rel-body-md-semibold-font-family);
17475
+ font-weight: var(--rel-body-md-semibold-font-weight);
17476
+ font-size: var(--rel-body-md-semibold-font-size);
17477
+ line-height: var(--rel-body-md-semibold-line-height);
17478
+ letter-spacing: var(--rel-body-md-semibold-letter-spacing);
17479
+ }
17480
+
17481
+ .time-readout-value.empty {
17482
+ color: var(--rel-text-placeholder);
17483
+ }
17484
+
17485
+ .columns {
17486
+ display: flex;
17487
+ gap: var(--rel-spacing-xs);
17488
+ }
17489
+
17490
+ /* Columns share the panel width equally, so the two-column 24h layout
17491
+ fills the same footprint the footer dictates instead of bunching left. */
17492
+ .column {
17493
+ display: flex;
17494
+ flex-direction: column;
17495
+ flex: 1;
17496
+ min-width: 64px;
17497
+ }
17498
+
17499
+ .column-header {
17500
+ text-align: center;
17501
+ color: var(--rel-text-placeholder);
17502
+ padding: var(--rel-spacing-xs);
17503
+ text-transform: uppercase;
17504
+ font-family: var(--rel-body-xs-semibold-font-family);
17505
+ font-weight: var(--rel-body-xs-semibold-font-weight);
17506
+ font-size: var(--rel-body-xs-semibold-font-size);
17507
+ line-height: var(--rel-body-xs-semibold-line-height);
17508
+ letter-spacing: var(--rel-body-xs-semibold-letter-spacing);
17509
+ }
17510
+
17511
+ .column-list {
17512
+ display: flex;
17513
+ flex-direction: column;
17514
+ gap: var(--rel-spacing-3xs);
17515
+ max-height: 224px;
17516
+ overflow-y: auto;
17517
+ padding: var(--rel-spacing-3xs);
17518
+ scrollbar-width: thin;
17519
+ }
17520
+
17521
+ .option-button {
17522
+ border: none;
17523
+ background: transparent;
17524
+ border-radius: var(--rel-border-radius-sm);
17525
+ color: var(--rel-text-default);
17526
+ cursor: pointer;
17527
+ padding: var(--rel-spacing-xs) var(--rel-spacing-sm);
17528
+ text-align: center;
17529
+ flex-shrink: 0;
17530
+ transition: all 0.15s ease;
17531
+ font-family: var(--rel-body-sm-medium-font-family);
17532
+ font-weight: var(--rel-body-sm-medium-font-weight);
17533
+ font-size: var(--rel-body-sm-medium-font-size);
17534
+ line-height: var(--rel-body-sm-medium-line-height);
17535
+ letter-spacing: var(--rel-body-sm-medium-letter-spacing);
17536
+ }
17537
+
17538
+ .option-button:hover:not(:disabled):not(.selected) {
17539
+ background: var(--rel-bg-surface-elevated);
17540
+ }
17541
+
17542
+ .option-button:focus-visible {
17543
+ outline: 2px solid var(--rel-border-focus);
17544
+ outline-offset: -2px;
17545
+ z-index: 1;
17546
+ }
17547
+
17548
+ .option-button.selected {
17549
+ background: var(--rel-action-brand-fill);
17550
+ color: var(--rel-action-brand-label-icons-primary);
17551
+ font-weight: var(--rel-body-md-semibold-font-weight);
17552
+ }
17553
+
17554
+ .option-button:disabled {
17555
+ color: var(--rel-text-disabled);
17556
+ cursor: not-allowed;
17557
+ opacity: 0.5;
17558
+ }
17559
+
17560
+ .option-button:disabled:hover {
17561
+ background: transparent;
17562
+ }
17563
+
17564
+ /* Footer (mirrors the range-picker calendar-footer) */
17565
+ .time-footer {
17566
+ display: flex;
17567
+ align-items: center;
17568
+ justify-content: space-between;
17569
+ gap: var(--rel-spacing-sm);
17570
+ padding-top: var(--rel-spacing-sm);
17571
+ margin-top: var(--rel-spacing-sm);
17572
+ border-top: 1px solid var(--rel-border-subtle);
17573
+ }
17574
+
17575
+ .footer-right {
17576
+ display: flex;
17577
+ align-items: center;
17578
+ gap: var(--rel-spacing-xs);
17579
+ }
17580
+
17581
+ @media (prefers-reduced-motion: reduce) {
17582
+ .trigger-button,
17583
+ .icon-button,
17584
+ .option-button {
17585
+ transition: none;
17586
+ }
17587
+ }
17588
+ `;
17589
+ __decorateClass([
17590
+ property48({ type: String, reflect: true })
17591
+ ], RelTimePicker.prototype, "value", 2);
17592
+ __decorateClass([
17593
+ property48({ type: String })
17594
+ ], RelTimePicker.prototype, "placeholder", 2);
17595
+ __decorateClass([
17596
+ property48({ type: String })
17597
+ ], RelTimePicker.prototype, "label", 2);
17598
+ __decorateClass([
17599
+ property48({ type: Boolean, reflect: true })
17600
+ ], RelTimePicker.prototype, "disabled", 2);
17601
+ __decorateClass([
17602
+ property48({ type: Boolean })
17603
+ ], RelTimePicker.prototype, "required", 2);
17604
+ __decorateClass([
17605
+ property48({ type: Boolean, reflect: true })
17606
+ ], RelTimePicker.prototype, "readonly", 2);
17607
+ __decorateClass([
17608
+ property48({ type: String })
17609
+ ], RelTimePicker.prototype, "format", 2);
17610
+ __decorateClass([
17611
+ property48({ type: Number, attribute: "minute-step" })
17612
+ ], RelTimePicker.prototype, "minuteStep", 2);
17613
+ __decorateClass([
17614
+ property48({ type: String, attribute: "min-time" })
17615
+ ], RelTimePicker.prototype, "minTime", 2);
17616
+ __decorateClass([
17617
+ property48({ type: String, attribute: "max-time" })
17618
+ ], RelTimePicker.prototype, "maxTime", 2);
17619
+ __decorateClass([
17620
+ property48({ type: Boolean, reflect: true })
17621
+ ], RelTimePicker.prototype, "open", 2);
17622
+ __decorateClass([
17623
+ property48({ type: Boolean })
17624
+ ], RelTimePicker.prototype, "clearable", 2);
17625
+ __decorateClass([
17626
+ property48({ type: Boolean })
17627
+ ], RelTimePicker.prototype, "error", 2);
17628
+ __decorateClass([
17629
+ property48({ type: Boolean })
17630
+ ], RelTimePicker.prototype, "invalid", 2);
17631
+ __decorateClass([
17632
+ property48({ type: String, attribute: "error-message" })
17633
+ ], RelTimePicker.prototype, "errorMessage", 2);
17634
+ __decorateClass([
17635
+ property48({ type: String, attribute: "helper-text" })
17636
+ ], RelTimePicker.prototype, "helperText", 2);
17637
+ __decorateClass([
17638
+ property48({ type: String })
17639
+ ], RelTimePicker.prototype, "name", 2);
17640
+ __decorateClass([
17641
+ property48({ type: String })
17642
+ ], RelTimePicker.prototype, "size", 2);
17643
+ __decorateClass([
17644
+ property48({ type: String, reflect: true })
17645
+ ], RelTimePicker.prototype, "variant", 2);
17646
+ __decorateClass([
17647
+ property48({ type: Boolean, reflect: true })
17648
+ ], RelTimePicker.prototype, "fullWidth", 2);
17649
+ __decorateClass([
17650
+ state21()
17651
+ ], RelTimePicker.prototype, "draftHour", 2);
17652
+ __decorateClass([
17653
+ state21()
17654
+ ], RelTimePicker.prototype, "draftMinute", 2);
17655
+ __decorateClass([
17656
+ query10(".trigger-button")
17657
+ ], RelTimePicker.prototype, "triggerButton", 2);
17658
+ __decorateClass([
17659
+ query10(".columns")
17660
+ ], RelTimePicker.prototype, "columnsElement", 2);
17661
+ RelTimePicker = __decorateClass([
17662
+ customElement54("rel-time-picker")
17663
+ ], RelTimePicker);
17664
+
17665
+ // src/components/rel-toast/rel-toast.ts
17666
+ import { LitElement as LitElement55, html as html55, css as css52 } from "lit";
17667
+ import { customElement as customElement55, property as property49, state as state22 } from "lit/decorators.js";
17668
+ var RelToast = class extends LitElement55 {
17669
+ constructor() {
17670
+ super(...arguments);
17671
+ this.message = "";
17672
+ this.type = "default";
17673
+ this.duration = 4e3;
17674
+ this.show = false;
17675
+ this.visible = false;
17676
+ this._dismissTimer = null;
17677
+ }
17678
+ willUpdate(changed) {
17679
+ if (!changed.has("show")) return;
17680
+ if (this.show) {
17681
+ this.visible = true;
17682
+ if (this._dismissTimer) clearTimeout(this._dismissTimer);
17683
+ if (this.duration > 0) {
17684
+ this._dismissTimer = setTimeout(() => this.close(), this.duration);
17685
+ }
17686
+ } else {
17687
+ this.visible = false;
17688
+ if (this._dismissTimer) {
17689
+ clearTimeout(this._dismissTimer);
17690
+ this._dismissTimer = null;
17691
+ }
17692
+ }
17693
+ }
17694
+ disconnectedCallback() {
17695
+ super.disconnectedCallback();
17696
+ if (this._dismissTimer) clearTimeout(this._dismissTimer);
17697
+ }
17698
+ open() {
17699
+ this.show = true;
17700
+ }
17701
+ close() {
17702
+ this.show = false;
17703
+ this.visible = false;
17704
+ if (this._dismissTimer) {
17705
+ clearTimeout(this._dismissTimer);
17706
+ this._dismissTimer = null;
17707
+ }
17708
+ this.dispatchEvent(
17709
+ new CustomEvent("dismissed", {
17710
+ bubbles: true,
17711
+ composed: true
17712
+ })
17713
+ );
17714
+ }
17715
+ handleActionClick(event) {
17716
+ event.stopPropagation();
17717
+ this.dispatchEvent(
17718
+ new CustomEvent("action", {
17719
+ bubbles: true,
17720
+ composed: true
17721
+ })
17722
+ );
17723
+ this.close();
17724
+ }
17725
+ render() {
17726
+ return html55`
17727
+ <div part="base" class="toast ${this.type} ${this.visible ? "" : "hidden"}" role="status" aria-live="polite">
17728
+ <span part="label" class="toast-message">${this.message}</span>
17729
+
17730
+ ${this.actionLabel ? html55`<button
17731
+ part="action" class="toast-action"
17732
+ @click="${this.handleActionClick}"
17733
+ >
17734
+ ${this.actionLabel}
17735
+ </button>` : ""}
17736
+
17737
+ <button part="close-button" class="toast-close" @click="${this.close}" aria-label="Dismiss">✕</button>
17738
+ </div>
17739
+ `;
17740
+ }
17741
+ };
17742
+ RelToast.styles = css52`
17743
+ :host {
17744
+ color: var(--rel-text-default);
17745
+ position: fixed;
17746
+ bottom: 24px;
16643
17747
  left: 50%;
16644
17748
  transform: translateX(-50%);
16645
17749
  display: flex;
@@ -16753,31 +17857,31 @@ RelToast.styles = css51`
16753
17857
  }
16754
17858
  `;
16755
17859
  __decorateClass([
16756
- property48({ type: String })
17860
+ property49({ type: String })
16757
17861
  ], RelToast.prototype, "message", 2);
16758
17862
  __decorateClass([
16759
- property48({ type: String })
17863
+ property49({ type: String })
16760
17864
  ], RelToast.prototype, "type", 2);
16761
17865
  __decorateClass([
16762
- property48({ type: String })
17866
+ property49({ type: String })
16763
17867
  ], RelToast.prototype, "actionLabel", 2);
16764
17868
  __decorateClass([
16765
- property48({ type: Number })
17869
+ property49({ type: Number })
16766
17870
  ], RelToast.prototype, "duration", 2);
16767
17871
  __decorateClass([
16768
- property48({ type: Boolean, reflect: true })
17872
+ property49({ type: Boolean, reflect: true })
16769
17873
  ], RelToast.prototype, "show", 2);
16770
17874
  __decorateClass([
16771
- state21()
17875
+ state22()
16772
17876
  ], RelToast.prototype, "visible", 2);
16773
17877
  RelToast = __decorateClass([
16774
- customElement54("rel-toast")
17878
+ customElement55("rel-toast")
16775
17879
  ], RelToast);
16776
17880
 
16777
17881
  // src/components/rel-tooltip/rel-tooltip.ts
16778
- import { LitElement as LitElement55, html as html55, css as css52 } from "lit";
16779
- import { customElement as customElement55, property as property49 } from "lit/decorators.js";
16780
- var RelTooltip = class extends LitElement55 {
17882
+ import { LitElement as LitElement56, html as html56, css as css53 } from "lit";
17883
+ import { customElement as customElement56, property as property50 } from "lit/decorators.js";
17884
+ var RelTooltip = class extends LitElement56 {
16781
17885
  constructor() {
16782
17886
  super(...arguments);
16783
17887
  this.text = "";
@@ -16799,7 +17903,7 @@ var RelTooltip = class extends LitElement55 {
16799
17903
  }
16800
17904
  }
16801
17905
  render() {
16802
- return html55`
17906
+ return html56`
16803
17907
  <slot @slotchange="${this.linkTrigger}"></slot>
16804
17908
  <div
16805
17909
  part="panel" class="tooltip-bubble ${this.position}"
@@ -16807,12 +17911,12 @@ var RelTooltip = class extends LitElement55 {
16807
17911
  role="tooltip"
16808
17912
  >
16809
17913
  ${this.text}
16810
- ${this.showTail ? html55`<span part="indicator" class="tooltip-tail"></span>` : ""}
17914
+ ${this.showTail ? html56`<span part="indicator" class="tooltip-tail"></span>` : ""}
16811
17915
  </div>
16812
17916
  `;
16813
17917
  }
16814
17918
  };
16815
- RelTooltip.styles = css52`
17919
+ RelTooltip.styles = css53`
16816
17920
  :host {
16817
17921
  color: var(--rel-text-default);
16818
17922
  display: inline-block;
@@ -16926,19 +18030,19 @@ RelTooltip.styles = css52`
16926
18030
  }
16927
18031
  `;
16928
18032
  __decorateClass([
16929
- property49({ type: String })
18033
+ property50({ type: String })
16930
18034
  ], RelTooltip.prototype, "text", 2);
16931
18035
  __decorateClass([
16932
- property49({ type: String })
18036
+ property50({ type: String })
16933
18037
  ], RelTooltip.prototype, "position", 2);
16934
18038
  __decorateClass([
16935
- property49({ type: Boolean, attribute: "show-tail" })
18039
+ property50({ type: Boolean, attribute: "show-tail" })
16936
18040
  ], RelTooltip.prototype, "showTail", 2);
16937
18041
  __decorateClass([
16938
- property49({ type: String, reflect: true })
18042
+ property50({ type: String, reflect: true })
16939
18043
  ], RelTooltip.prototype, "type", 2);
16940
18044
  RelTooltip = __decorateClass([
16941
- customElement55("rel-tooltip")
18045
+ customElement56("rel-tooltip")
16942
18046
  ], RelTooltip);
16943
18047
 
16944
18048
  // src/index.ts
@@ -16991,6 +18095,7 @@ export {
16991
18095
  RelTab,
16992
18096
  RelTabItem,
16993
18097
  RelTextfield,
18098
+ RelTimePicker,
16994
18099
  RelToast,
16995
18100
  RelTooltip,
16996
18101
  RelTopbar,