@f-ewald/components 1.0.0 → 1.0.2

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.
Files changed (172) hide show
  1. package/README.md +6 -0
  2. package/custom-elements.json +891 -22
  3. package/dist/address-autocomplete.d.ts +8 -0
  4. package/dist/address-autocomplete.d.ts.map +1 -1
  5. package/dist/address-autocomplete.js +165 -15
  6. package/dist/address-autocomplete.js.map +1 -1
  7. package/dist/animate-confetti.d.ts +8 -5
  8. package/dist/animate-confetti.d.ts.map +1 -1
  9. package/dist/animate-confetti.js +46 -19
  10. package/dist/animate-confetti.js.map +1 -1
  11. package/dist/autocomplete-input.d.ts +8 -0
  12. package/dist/autocomplete-input.d.ts.map +1 -1
  13. package/dist/autocomplete-input.js +165 -15
  14. package/dist/autocomplete-input.js.map +1 -1
  15. package/dist/calendar-month.d.ts.map +1 -1
  16. package/dist/calendar-month.js +27 -13
  17. package/dist/calendar-month.js.map +1 -1
  18. package/dist/calendar-year.d.ts.map +1 -1
  19. package/dist/calendar-year.js +11 -2
  20. package/dist/calendar-year.js.map +1 -1
  21. package/dist/chat-message.d.ts.map +1 -1
  22. package/dist/chat-message.js +77 -24
  23. package/dist/chat-message.js.map +1 -1
  24. package/dist/confirm-dialog.d.ts +8 -1
  25. package/dist/confirm-dialog.d.ts.map +1 -1
  26. package/dist/confirm-dialog.js +130 -18
  27. package/dist/confirm-dialog.js.map +1 -1
  28. package/dist/copy-link-button.d.ts +2 -0
  29. package/dist/copy-link-button.d.ts.map +1 -1
  30. package/dist/copy-link-button.js +35 -3
  31. package/dist/copy-link-button.js.map +1 -1
  32. package/dist/data-table.d.ts +2 -0
  33. package/dist/data-table.d.ts.map +1 -1
  34. package/dist/data-table.js +83 -11
  35. package/dist/data-table.js.map +1 -1
  36. package/dist/distance-value.d.ts +0 -1
  37. package/dist/distance-value.d.ts.map +1 -1
  38. package/dist/distance-value.js +3 -8
  39. package/dist/distance-value.js.map +1 -1
  40. package/dist/distribution-chart.d.ts.map +1 -1
  41. package/dist/distribution-chart.js +54 -17
  42. package/dist/distribution-chart.js.map +1 -1
  43. package/dist/dropdown-button.d.ts.map +1 -1
  44. package/dist/dropdown-button.js +60 -11
  45. package/dist/dropdown-button.js.map +1 -1
  46. package/dist/editable-text.d.ts.map +1 -1
  47. package/dist/editable-text.js +46 -13
  48. package/dist/editable-text.js.map +1 -1
  49. package/dist/form-select.d.ts.map +1 -1
  50. package/dist/form-select.js +70 -22
  51. package/dist/form-select.js.map +1 -1
  52. package/dist/frame-box.d.ts.map +1 -1
  53. package/dist/frame-box.js +20 -12
  54. package/dist/frame-box.js.map +1 -1
  55. package/dist/icon-button.d.ts.map +1 -1
  56. package/dist/icon-button.js +11 -0
  57. package/dist/icon-button.js.map +1 -1
  58. package/dist/index.d.ts +1 -0
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +1 -0
  61. package/dist/index.js.map +1 -1
  62. package/dist/kbd-hint.d.ts +30 -0
  63. package/dist/kbd-hint.d.ts.map +1 -0
  64. package/dist/kbd-hint.js +184 -0
  65. package/dist/kbd-hint.js.map +1 -0
  66. package/dist/live-timer.d.ts +0 -1
  67. package/dist/live-timer.d.ts.map +1 -1
  68. package/dist/live-timer.js +1 -6
  69. package/dist/live-timer.js.map +1 -1
  70. package/dist/map-circle.d.ts.map +1 -1
  71. package/dist/map-circle.js +16 -1
  72. package/dist/map-circle.js.map +1 -1
  73. package/dist/map-pin.d.ts.map +1 -1
  74. package/dist/map-pin.js +16 -1
  75. package/dist/map-pin.js.map +1 -1
  76. package/dist/percent-bar-chart.d.ts +3 -0
  77. package/dist/percent-bar-chart.d.ts.map +1 -1
  78. package/dist/percent-bar-chart.js +60 -4
  79. package/dist/percent-bar-chart.js.map +1 -1
  80. package/dist/photo-gallery.d.ts.map +1 -1
  81. package/dist/photo-gallery.js +12 -3
  82. package/dist/photo-gallery.js.map +1 -1
  83. package/dist/popover-panel.d.ts.map +1 -1
  84. package/dist/popover-panel.js +132 -23
  85. package/dist/popover-panel.js.map +1 -1
  86. package/dist/price-history-chart.d.ts.map +1 -1
  87. package/dist/price-history-chart.js +52 -18
  88. package/dist/price-history-chart.js.map +1 -1
  89. package/dist/radio-cards.d.ts +2 -0
  90. package/dist/radio-cards.d.ts.map +1 -1
  91. package/dist/radio-cards.js +51 -5
  92. package/dist/radio-cards.js.map +1 -1
  93. package/dist/radio-pills.d.ts +2 -0
  94. package/dist/radio-pills.d.ts.map +1 -1
  95. package/dist/radio-pills.js +46 -4
  96. package/dist/radio-pills.js.map +1 -1
  97. package/dist/relative-time.d.ts +0 -1
  98. package/dist/relative-time.d.ts.map +1 -1
  99. package/dist/relative-time.js +1 -6
  100. package/dist/relative-time.js.map +1 -1
  101. package/dist/reveal-button.d.ts +2 -0
  102. package/dist/reveal-button.d.ts.map +1 -1
  103. package/dist/reveal-button.js +42 -5
  104. package/dist/reveal-button.js.map +1 -1
  105. package/dist/roman-numeral.js +1 -1
  106. package/dist/roman-numeral.js.map +1 -1
  107. package/dist/slide-panel.d.ts +7 -3
  108. package/dist/slide-panel.d.ts.map +1 -1
  109. package/dist/slide-panel.js +95 -23
  110. package/dist/slide-panel.js.map +1 -1
  111. package/dist/stat-meter.d.ts.map +1 -1
  112. package/dist/stat-meter.js +17 -7
  113. package/dist/stat-meter.js.map +1 -1
  114. package/dist/status-pill.d.ts.map +1 -1
  115. package/dist/status-pill.js +26 -12
  116. package/dist/status-pill.js.map +1 -1
  117. package/dist/tile-grid.d.ts +8 -0
  118. package/dist/tile-grid.d.ts.map +1 -1
  119. package/dist/tile-grid.js +88 -12
  120. package/dist/tile-grid.js.map +1 -1
  121. package/dist/toast-notification.d.ts.map +1 -1
  122. package/dist/toast-notification.js +40 -11
  123. package/dist/toast-notification.js.map +1 -1
  124. package/dist/tokens.css +19 -0
  125. package/dist/tokens.d.ts.map +1 -1
  126. package/dist/tokens.js +13 -0
  127. package/dist/tokens.js.map +1 -1
  128. package/dist/ui-button.d.ts +2 -0
  129. package/dist/ui-button.d.ts.map +1 -1
  130. package/dist/ui-button.js +58 -9
  131. package/dist/ui-button.js.map +1 -1
  132. package/dist/user-avatar.d.ts.map +1 -1
  133. package/dist/user-avatar.js +18 -3
  134. package/dist/user-avatar.js.map +1 -1
  135. package/dist/utils/form.d.ts +3 -0
  136. package/dist/utils/form.d.ts.map +1 -0
  137. package/dist/utils/form.js +14 -0
  138. package/dist/utils/form.js.map +1 -0
  139. package/dist/utils/layer-stack.d.ts +9 -0
  140. package/dist/utils/layer-stack.d.ts.map +1 -0
  141. package/dist/utils/layer-stack.js +35 -0
  142. package/dist/utils/layer-stack.js.map +1 -0
  143. package/dist/weight-bar-chart.d.ts.map +1 -1
  144. package/dist/weight-bar-chart.js +27 -8
  145. package/dist/weight-bar-chart.js.map +1 -1
  146. package/docs/address-autocomplete.md +5 -0
  147. package/docs/autocomplete-input.md +5 -0
  148. package/docs/calendar-month.md +2 -0
  149. package/docs/chat-message.md +4 -0
  150. package/docs/confirm-dialog.md +2 -0
  151. package/docs/copy-link-button.md +2 -0
  152. package/docs/data-table.md +2 -0
  153. package/docs/design-language.md +149 -0
  154. package/docs/distribution-chart.md +4 -0
  155. package/docs/dropdown-button.md +1 -0
  156. package/docs/frame-box.md +2 -0
  157. package/docs/kbd-hint.md +42 -0
  158. package/docs/percent-bar-chart.md +4 -1
  159. package/docs/photo-gallery.md +1 -0
  160. package/docs/popover-panel.md +2 -0
  161. package/docs/price-history-chart.md +8 -1
  162. package/docs/radio-cards.md +2 -0
  163. package/docs/radio-pills.md +2 -0
  164. package/docs/reveal-button.md +3 -0
  165. package/docs/slide-panel.md +4 -2
  166. package/docs/status-pill.md +3 -0
  167. package/docs/tile-grid.md +10 -1
  168. package/docs/toast-notification.md +4 -1
  169. package/docs/ui-button.md +2 -0
  170. package/docs/user-avatar.md +1 -0
  171. package/llms.txt +59 -34
  172. package/package.json +5 -1
@@ -4,7 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { LitElement, css, html, nothing } from "lit";
7
+ import { LitElement, html, nothing } from "lit";
8
8
  import { customElement, property } from "lit/decorators.js";
9
9
  import { formatMiles, formatKm } from "./utils/distance.js";
10
10
  /**
@@ -24,16 +24,11 @@ let DistanceValue = class DistanceValue extends LitElement {
24
24
  /** Distance in kilometers (metric, future). Switches to meters below 0.5 km. */
25
25
  this.km = null;
26
26
  }
27
- static { this.styles = css `
28
- :host {
29
- display: inline;
30
- }
31
- `; }
32
27
  render() {
33
28
  if (this.km != null)
34
- return html `${formatKm(this.km)}`;
29
+ return Number.isFinite(this.km) ? html `${formatKm(this.km)}` : nothing;
35
30
  if (this.miles != null)
36
- return html `${formatMiles(this.miles)}`;
31
+ return Number.isFinite(this.miles) ? html `${formatMiles(this.miles)}` : nothing;
37
32
  return nothing;
38
33
  }
39
34
  };
@@ -1 +1 @@
1
- {"version":3,"file":"distance-value.js","sourceRoot":"","sources":["../src/distance-value.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAE5D;;;;;;;;GAQG;AAEI,IAAM,aAAa,GAAnB,MAAM,aAAc,SAAQ,UAAU;IAAtC;;QAOL,oEAAoE;QACxC,UAAK,GAAkB,IAAI,CAAC;QAExD,gFAAgF;QACpD,OAAE,GAAkB,IAAI,CAAC;IAOvD,CAAC;aAjBiB,WAAM,GAAG,GAAG,CAAA;;;;GAI3B,AAJqB,CAIpB;IAQO,MAAM;QACb,IAAI,IAAI,CAAC,EAAE,IAAI,IAAI;YAAE,OAAO,IAAI,CAAA,GAAG,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;QACvD,IAAI,IAAI,CAAC,KAAK,IAAI,IAAI;YAAE,OAAO,IAAI,CAAA,GAAG,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAChE,OAAO,OAAO,CAAC;IACjB,CAAC;CACF,CAAA;AAV6B;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;4CAA6B;AAG5B;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;yCAA0B;AAX1C,aAAa;IADzB,aAAa,CAAC,gBAAgB,CAAC;GACnB,aAAa,CAkBzB","sourcesContent":["import { LitElement, css, html, nothing } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport { formatMiles, formatKm } from \"./utils/distance.js\";\n\n/**\n * Inline distance display. Renders miles/feet or km/m, switching units at\n * sensible thresholds (< 0.25 mi → ft; < 0.5 km → m).\n *\n * Supply exactly one of `miles` or `km`; the other stays null.\n * km support is present but reserved for future use.\n *\n * @element distance-value\n */\n@customElement(\"distance-value\")\nexport class DistanceValue extends LitElement {\n static override styles = css`\n :host {\n display: inline;\n }\n `;\n\n /** Distance in miles (imperial). Switches to feet below 0.25 mi. */\n @property({ type: Number }) miles: number | null = null;\n\n /** Distance in kilometers (metric, future). Switches to meters below 0.5 km. */\n @property({ type: Number }) km: number | null = null;\n\n override render() {\n if (this.km != null) return html`${formatKm(this.km)}`;\n if (this.miles != null) return html`${formatMiles(this.miles)}`;\n return nothing;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"distance-value\": DistanceValue;\n }\n}\n"]}
1
+ {"version":3,"file":"distance-value.js","sourceRoot":"","sources":["../src/distance-value.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAE5D;;;;;;;;GAQG;AAEI,IAAM,aAAa,GAAnB,MAAM,aAAc,SAAQ,UAAU;IAAtC;;QACL,oEAAoE;QACxC,UAAK,GAAkB,IAAI,CAAC;QAExD,gFAAgF;QACpD,OAAE,GAAkB,IAAI,CAAC;IAOvD,CAAC;IALU,MAAM;QACb,IAAI,IAAI,CAAC,EAAE,IAAI,IAAI;YAAE,OAAO,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA,GAAG,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC;QAC5F,IAAI,IAAI,CAAC,KAAK,IAAI,IAAI;YAAE,OAAO,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA,GAAG,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC;QACxG,OAAO,OAAO,CAAC;IACjB,CAAC;CACF,CAAA;AAV6B;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;4CAA6B;AAG5B;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;yCAA0B;AAL1C,aAAa;IADzB,aAAa,CAAC,gBAAgB,CAAC;GACnB,aAAa,CAYzB","sourcesContent":["import { LitElement, html, nothing } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport { formatMiles, formatKm } from \"./utils/distance.js\";\n\n/**\n * Inline distance display. Renders miles/feet or km/m, switching units at\n * sensible thresholds (< 0.25 mi → ft; < 0.5 km → m).\n *\n * Supply exactly one of `miles` or `km`; the other stays null.\n * km support is present but reserved for future use.\n *\n * @element distance-value\n */\n@customElement(\"distance-value\")\nexport class DistanceValue extends LitElement {\n /** Distance in miles (imperial). Switches to feet below 0.25 mi. */\n @property({ type: Number }) miles: number | null = null;\n\n /** Distance in kilometers (metric, future). Switches to meters below 0.5 km. */\n @property({ type: Number }) km: number | null = null;\n\n override render() {\n if (this.km != null) return Number.isFinite(this.km) ? html`${formatKm(this.km)}` : nothing;\n if (this.miles != null) return Number.isFinite(this.miles) ? html`${formatMiles(this.miles)}` : nothing;\n return nothing;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"distance-value\": DistanceValue;\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"distribution-chart.d.ts","sourceRoot":"","sources":["../src/distribution-chart.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAa,OAAO,EAAO,MAAM,KAAK,CAAC;AAqB1D,MAAM,WAAW,iBAAiB;IAChC,8FAA8F;IAC9F,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AASD;;;;;;;;;;;GAWG;AACH,qBACa,iBAAkB,SAAQ,UAAU;IAC/C,OAAgB,MAAM,4BAgEpB;IAEF,wEAAwE;IAC5D,MAAM,SAAM;IACxB,4DAA4D;IAC5B,MAAM,EAAE,iBAAiB,EAAE,CAAM;IACjE,oFAAoF;IACpD,YAAY,EAAE,MAAM,EAAE,CAAyB;IAC/E,8GAA8G;IAC1D,QAAQ,SAAM;IAEzD,OAAO,CAAC,KAAK,CAAiC;IAC9C,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,MAAM,CAAuB;IAC9C,0EAA0E;IACjE,OAAO,CAAC,MAAM,CAAK;IAC5B,wFAAwF;IAC/E,OAAO,CAAC,OAAO,CAAuB;IAE/C,OAAO,CAAC,WAAW,CAAM;IACzB,OAAO,CAAC,GAAG,CAA+B;IAEjC,iBAAiB,SAGzB;IAEQ,YAAY,SAMpB;IAEQ,oBAAoB,SAI5B;IAEQ,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,QAE7C;IAED,OAAO,CAAC,WAAW;IAoBnB,OAAO,CAAC,UAAU;IAuFlB,OAAO,CAAC,aAAa;IAmBZ,MAAM,0DAUd;CACF;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,oBAAoB,EAAE,iBAAiB,CAAC;KACzC;CACF"}
1
+ {"version":3,"file":"distribution-chart.d.ts","sourceRoot":"","sources":["../src/distribution-chart.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAa,OAAO,EAAO,MAAM,KAAK,CAAC;AAqB1D,MAAM,WAAW,iBAAiB;IAChC,8FAA8F;IAC9F,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AASD;;;;;;;;;;;GAWG;AACH,qBACa,iBAAkB,SAAQ,UAAU;IAC/C,OAAgB,MAAM,4BA6FpB;IAEF,wEAAwE;IAC5D,MAAM,SAAM;IACxB,4DAA4D;IAC5B,MAAM,EAAE,iBAAiB,EAAE,CAAM;IACjE,oFAAoF;IACpD,YAAY,EAAE,MAAM,EAAE,CAAyB;IAC/E,8GAA8G;IAC1D,QAAQ,SAAM;IAEzD,OAAO,CAAC,KAAK,CAAiC;IAC9C,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,MAAM,CAAuB;IAC9C,0EAA0E;IACjE,OAAO,CAAC,MAAM,CAAK;IAC5B,wFAAwF;IAC/E,OAAO,CAAC,OAAO,CAAuB;IAE/C,OAAO,CAAC,WAAW,CAAM;IACzB,OAAO,CAAC,GAAG,CAA+B;IAEjC,iBAAiB,SAGzB;IAEQ,YAAY,SAMpB;IAEQ,oBAAoB,SAI5B;IAEQ,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,QAE7C;IAED,OAAO,CAAC,WAAW;IAoBnB,OAAO,CAAC,UAAU;IAiGlB,OAAO,CAAC,aAAa;IAmBZ,MAAM,0DAUd;CACF;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,oBAAoB,EAAE,iBAAiB,CAAC;KACzC;CACF"}
@@ -56,27 +56,51 @@ let DistributionChart = class DistributionChart extends LitElement {
56
56
  width: 100%;
57
57
  }
58
58
  .wrap {
59
- padding: 4px 0 8px;
59
+ padding: 0.25rem 0 0.5rem;
60
60
  }
61
61
  svg {
62
62
  display: block;
63
63
  overflow: visible;
64
+ font-family: var(
65
+ --ui-font,
66
+ ui-sans-serif,
67
+ system-ui,
68
+ sans-serif,
69
+ "Apple Color Emoji",
70
+ "Segoe UI Emoji",
71
+ "Segoe UI Symbol",
72
+ "Noto Color Emoji"
73
+ );
64
74
  }
65
75
  .curve {
66
76
  fill: none;
67
- stroke: #4f46e5;
77
+ stroke: var(--ui-primary, #4f46e5);
68
78
  stroke-width: 1.5;
69
79
  stroke-linejoin: round;
70
80
  }
71
81
  .fill {
72
82
  fill: url(#dist-fill-grad);
73
83
  }
84
+ .fill-start {
85
+ stop-color: var(--ui-primary, #4f46e5);
86
+ }
87
+ .crosshair {
88
+ stroke: var(--ui-text-muted, #64748b);
89
+ }
90
+ .chart-label {
91
+ fill: var(--ui-text-muted, #64748b);
92
+ }
74
93
  .skeleton {
75
- background: linear-gradient(90deg, #f8fafc 25%, #e2e8f0 50%, #f8fafc 75%);
94
+ background: linear-gradient(
95
+ 90deg,
96
+ var(--ui-surface-muted, #f8fafc) 25%,
97
+ var(--ui-border, #e2e8f0) 50%,
98
+ var(--ui-surface-muted, #f8fafc) 75%
99
+ );
76
100
  background-size: 200% 100%;
77
101
  animation: shimmer 1.4s infinite;
78
102
  border-radius: var(--ui-radius-sm, 0.25rem);
79
- height: 100px;
103
+ height: 6.25rem;
80
104
  }
81
105
  @keyframes shimmer {
82
106
  0% {
@@ -89,28 +113,33 @@ let DistributionChart = class DistributionChart extends LitElement {
89
113
  .error {
90
114
  font-size: var(--ui-font-size-sm, 0.75rem);
91
115
  color: var(--ui-danger, #dc2626);
92
- padding: 8px 0;
116
+ padding: 0.5rem 0;
93
117
  }
94
118
  .legend {
95
119
  display: flex;
96
120
  flex-wrap: wrap;
97
- gap: 6px 12px;
98
- padding: 2px 0 2px 8px;
121
+ gap: 0.5rem 0.75rem;
122
+ padding: 0.25rem 0 0.25rem 0.5rem;
99
123
  }
100
124
  .legend-item {
101
125
  display: inline-flex;
102
126
  align-items: center;
103
- gap: 4px;
127
+ gap: 0.25rem;
104
128
  font-size: var(--ui-font-size-sm, 0.75rem);
105
129
  color: var(--ui-text-muted, #64748b);
106
130
  }
107
131
  .legend-dot {
108
132
  display: inline-block;
109
- width: 8px;
110
- height: 8px;
133
+ width: 0.5rem;
134
+ height: 0.5rem;
111
135
  border-radius: 50%;
112
136
  flex: 0 0 auto;
113
137
  }
138
+ @media (prefers-reduced-motion: reduce) {
139
+ .skeleton {
140
+ animation: none;
141
+ }
142
+ }
114
143
  `,
115
144
  ]; }
116
145
  connectedCallback() {
@@ -200,13 +229,21 @@ let DistributionChart = class DistributionChart extends LitElement {
200
229
  const anchor = hx < innerW * 0.2 ? "start" : hx > innerW * 0.8 ? "end" : "middle";
201
230
  const lx = hx < innerW * 0.2 ? 0 : hx > innerW * 0.8 ? innerW : hx;
202
231
  return svg `
203
- <line stroke="#94a3b8" stroke-width="1" x1=${hx} y1=${0} x2=${hx} y2=${innerH} />
204
- <text font-size=${fs} fill="#64748b" font-weight="500"
232
+ <line class="crosshair" stroke-width="1" x1=${hx} y1=${0} x2=${hx} y2=${innerH} />
233
+ <text class="chart-label" font-size=${fs} font-weight="500"
205
234
  text-anchor=${anchor} x=${lx} y=${-fs * 0.4}>${hLabel}</text>
206
235
  `;
207
236
  })() : nothing;
237
+ const markerSummary = this.values.length > 0
238
+ ? ` Markers: ${this.values
239
+ .map((value) => `${value.label || "value"} ${value.value.toLocaleString("en-US")} ${data.unit}`)
240
+ .join(", ")}.`
241
+ : "";
242
+ const summary = `${data.label}: ${data.min.toLocaleString("en-US")} to ${data.max.toLocaleString("en-US")} ${data.unit}, ` +
243
+ `mean ${data.mean.toLocaleString("en-US")} ${data.unit}.${markerSummary}`;
208
244
  return svg `
209
245
  <svg viewBox="0 0 ${this._width} ${SVG_H}" width=${this._width} height=${SVG_H}
246
+ role="img" aria-label=${summary}
210
247
  style="cursor:crosshair"
211
248
  @mousemove=${(e) => {
212
249
  const innerX = e.offsetX - PAD.left;
@@ -215,8 +252,8 @@ let DistributionChart = class DistributionChart extends LitElement {
215
252
  @mouseleave=${() => { this._hoverX = null; }}>
216
253
  <defs>
217
254
  <linearGradient id="dist-fill-grad" x1="0" y1="0" x2="0" y2="1">
218
- <stop offset="0%" stop-color="#4f46e5" stop-opacity="0.28"/>
219
- <stop offset="100%" stop-color="#4f46e5" stop-opacity="0"/>
255
+ <stop class="fill-start" offset="0%" stop-opacity="0.28"/>
256
+ <stop class="fill-start" offset="100%" stop-opacity="0"/>
220
257
  </linearGradient>
221
258
  </defs>
222
259
  <g transform="translate(${PAD.left},${PAD.top})">
@@ -224,9 +261,9 @@ let DistributionChart = class DistributionChart extends LitElement {
224
261
  <path class="curve" d=${curvePath} />
225
262
  ${markers}
226
263
  ${crosshair}
227
- <text font-size=${fs} fill="#94a3b8" text-anchor="start" x=${0} y=${innerH + fs * 1.4}>${minLabel}</text>
228
- <text font-size=${fs} fill="#94a3b8" text-anchor="end" x=${innerW} y=${innerH + fs * 1.4}>${maxLabel}</text>
229
- <text font-size=${fs} fill="#94a3b8" text-anchor="middle" x=${innerW / 2} y=${innerH + fs * 1.4}>${data.unit}</text>
264
+ <text class="chart-label" font-size=${fs} text-anchor="start" x=${0} y=${innerH + fs * 1.4}>${minLabel}</text>
265
+ <text class="chart-label" font-size=${fs} text-anchor="end" x=${innerW} y=${innerH + fs * 1.4}>${maxLabel}</text>
266
+ <text class="chart-label" font-size=${fs} text-anchor="middle" x=${innerW / 2} y=${innerH + fs * 1.4}>${data.unit}</text>
230
267
  </g>
231
268
  </svg>
232
269
  `;
@@ -1 +1 @@
1
- {"version":3,"file":"distribution-chart.js","sourceRoot":"","sources":["../src/distribution-chart.ts"],"names":[],"mappings":"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","sourcesContent":["import { LitElement, css, html, nothing, svg } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\nimport { scaleLinear } from \"d3-scale\";\nimport { line, area } from \"d3-shape\";\nimport { max } from \"d3-array\";\nimport { tokens } from \"./tokens.js\";\n\ninterface DistributionPoint {\n x: number;\n y: number;\n}\n\ninterface DistributionData {\n points: DistributionPoint[];\n unit: string;\n label: string;\n min: number;\n max: number;\n mean: number;\n}\n\nexport interface DistributionValue {\n /** Short label shown on the chart marker, e.g. 'A'. Empty string = show raw value instead. */\n label: string;\n value: number;\n}\n\n/** Fixed SVG height in CSS pixels — the coordinate system is always 1 user-unit = 1 px. */\nconst SVG_H = 130;\nconst PAD = { top: 22, right: 12, bottom: 18, left: 12 };\n\n/** Default marker colors (indigo/amber/teal/rose 600s), assigned in order. */\nconst DEFAULT_MARKER_COLORS = [\"#4f46e5\", \"#d97706\", \"#0d9488\", \"#e11d48\"];\n\n/**\n * Renders a KDE distribution curve for a named metric with one or more value\n * markers. The SVG viewBox is kept in sync with the element's pixel width via\n * ResizeObserver so that font sizes and stroke widths are always in real pixels\n * regardless of container width.\n *\n * Pass `fontSize` to control all text size (default 11). Pass a single\n * `{label:'', value}` for a single-value display or multiple\n * `{label:'A'|'B'|...}` entries to compare several values.\n *\n * @element distribution-chart\n */\n@customElement(\"distribution-chart\")\nexport class DistributionChart extends LitElement {\n static override styles = [\n tokens,\n css`\n :host {\n display: block;\n width: 100%;\n }\n .wrap {\n padding: 4px 0 8px;\n }\n svg {\n display: block;\n overflow: visible;\n }\n .curve {\n fill: none;\n stroke: #4f46e5;\n stroke-width: 1.5;\n stroke-linejoin: round;\n }\n .fill {\n fill: url(#dist-fill-grad);\n }\n .skeleton {\n background: linear-gradient(90deg, #f8fafc 25%, #e2e8f0 50%, #f8fafc 75%);\n background-size: 200% 100%;\n animation: shimmer 1.4s infinite;\n border-radius: var(--ui-radius-sm, 0.25rem);\n height: 100px;\n }\n @keyframes shimmer {\n 0% {\n background-position: 200% 0;\n }\n 100% {\n background-position: -200% 0;\n }\n }\n .error {\n font-size: var(--ui-font-size-sm, 0.75rem);\n color: var(--ui-danger, #dc2626);\n padding: 8px 0;\n }\n .legend {\n display: flex;\n flex-wrap: wrap;\n gap: 6px 12px;\n padding: 2px 0 2px 8px;\n }\n .legend-item {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n font-size: var(--ui-font-size-sm, 0.75rem);\n color: var(--ui-text-muted, #64748b);\n }\n .legend-dot {\n display: inline-block;\n width: 8px;\n height: 8px;\n border-radius: 50%;\n flex: 0 0 auto;\n }\n `,\n ];\n\n /** Metric name, fetched from `/api/distribution/<metric>` on change. */\n @property() metric = \"\";\n /** One or more values to mark on the distribution curve. */\n @property({ attribute: false }) values: DistributionValue[] = [];\n /** Colors assigned to markers in order. Defaults to indigo/amber/teal/rose 600s. */\n @property({ attribute: false }) markerColors: string[] = DEFAULT_MARKER_COLORS;\n /** Target font size in CSS pixels (default 11). Always renders at this size regardless of container width. */\n @property({ type: Number, attribute: \"font-size\" }) fontSize = 11;\n\n @state() private _data: DistributionData | null = null;\n @state() private _loading = false;\n @state() private _error: string | null = null;\n /** Measured element width in CSS pixels. 0 until ResizeObserver fires. */\n @state() private _width = 0;\n /** Inner x coordinate (px) of the current hover position, or null when not hovering. */\n @state() private _hoverX: number | null = null;\n\n private _fetchedFor = \"\";\n private _ro: ResizeObserver | null = null;\n\n override connectedCallback() {\n super.connectedCallback();\n this._maybeFetch();\n }\n\n override firstUpdated() {\n this._ro = new ResizeObserver((entries) => {\n const w = Math.floor(entries[0].contentRect.width);\n if (w > 0 && w !== this._width) this._width = w;\n });\n this._ro.observe(this);\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback();\n this._ro?.disconnect();\n this._ro = null;\n }\n\n override updated(changed: Map<string, unknown>) {\n if (changed.has(\"metric\")) this._maybeFetch();\n }\n\n private _maybeFetch() {\n if (!this.metric || this._fetchedFor === this.metric) return;\n this._fetchedFor = this.metric;\n this._loading = true;\n this._error = null;\n fetch(`/api/distribution/${encodeURIComponent(this.metric)}`)\n .then((r) => {\n if (!r.ok) throw new Error(`HTTP ${r.status}`);\n return r.json() as Promise<DistributionData>;\n })\n .then((data) => {\n this._data = data;\n this._loading = false;\n })\n .catch((err: unknown) => {\n this._error = err instanceof Error ? err.message : \"Failed to load\";\n this._loading = false;\n });\n }\n\n private _renderSvg(data: DistributionData) {\n if (this._width === 0) return nothing;\n\n const innerW = this._width - PAD.left - PAD.right;\n const innerH = SVG_H - PAD.top - PAD.bottom;\n const fs = this.fontSize;\n\n const pts = data.points;\n const maxY = max(pts, (p) => p.y) ?? 1;\n\n const xScale = scaleLinear().domain([pts[0].x, pts[pts.length - 1].x]).range([0, innerW]);\n const yScale = scaleLinear().domain([0, maxY]).range([innerH, 0]);\n\n const lineFn = line<DistributionPoint>()\n .x((p) => xScale(p.x))\n .y((p) => yScale(p.y));\n\n const areaFn = area<DistributionPoint>()\n .x((p) => xScale(p.x))\n .y0(innerH)\n .y1((p) => yScale(p.y));\n\n const curvePath = lineFn(pts) ?? \"\";\n const fillPath = areaFn(pts) ?? \"\";\n\n const minLabel = data.min.toLocaleString(\"en-US\", { maximumFractionDigits: 0 });\n const maxLabel = data.max.toLocaleString(\"en-US\", { maximumFractionDigits: 0 });\n\n const hasLabels = this.values.some((v) => v.label !== \"\");\n\n const markers = this.values\n .filter((v) => v.value >= pts[0].x && v.value <= pts[pts.length - 1].x)\n .map((v, i) => {\n const color = this.markerColors[i % this.markerColors.length];\n const mx = xScale(v.value);\n const labelText = hasLabels\n ? v.label\n : `${v.value.toLocaleString(\"en-US\", { maximumFractionDigits: 0 })} ${data.unit}`;\n return svg`\n <line stroke=${color} stroke-width=\"1.5\" stroke-dasharray=\"4 3\"\n x1=${mx} y1=${0} x2=${mx} y2=${innerH} />\n <text font-size=${fs} font-weight=\"600\" fill=${color} text-anchor=\"middle\"\n x=${mx} y=${-fs * 0.4}>${labelText}</text>\n `;\n });\n\n const crosshair = this._hoverX != null ? (() => {\n const hx = Math.max(0, Math.min(innerW, this._hoverX!));\n const hVal = xScale.invert(hx);\n const hLabel = `${Math.round(hVal).toLocaleString(\"en-US\")} ${data.unit}`;\n // Keep label anchor away from edges so it doesn't clip\n const anchor = hx < innerW * 0.2 ? \"start\" : hx > innerW * 0.8 ? \"end\" : \"middle\";\n const lx = hx < innerW * 0.2 ? 0 : hx > innerW * 0.8 ? innerW : hx;\n return svg`\n <line stroke=\"#94a3b8\" stroke-width=\"1\" x1=${hx} y1=${0} x2=${hx} y2=${innerH} />\n <text font-size=${fs} fill=\"#64748b\" font-weight=\"500\"\n text-anchor=${anchor} x=${lx} y=${-fs * 0.4}>${hLabel}</text>\n `;\n })() : nothing;\n\n return svg`\n <svg viewBox=\"0 0 ${this._width} ${SVG_H}\" width=${this._width} height=${SVG_H}\n style=\"cursor:crosshair\"\n @mousemove=${(e: MouseEvent) => {\n const innerX = e.offsetX - PAD.left;\n this._hoverX = (innerX >= 0 && innerX <= innerW) ? innerX : null;\n }}\n @mouseleave=${() => { this._hoverX = null; }}>\n <defs>\n <linearGradient id=\"dist-fill-grad\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"0%\" stop-color=\"#4f46e5\" stop-opacity=\"0.28\"/>\n <stop offset=\"100%\" stop-color=\"#4f46e5\" stop-opacity=\"0\"/>\n </linearGradient>\n </defs>\n <g transform=\"translate(${PAD.left},${PAD.top})\">\n <path class=\"fill\" d=${fillPath} />\n <path class=\"curve\" d=${curvePath} />\n ${markers}\n ${crosshair}\n <text font-size=${fs} fill=\"#94a3b8\" text-anchor=\"start\" x=${0} y=${innerH + fs * 1.4}>${minLabel}</text>\n <text font-size=${fs} fill=\"#94a3b8\" text-anchor=\"end\" x=${innerW} y=${innerH + fs * 1.4}>${maxLabel}</text>\n <text font-size=${fs} fill=\"#94a3b8\" text-anchor=\"middle\" x=${innerW / 2} y=${innerH + fs * 1.4}>${data.unit}</text>\n </g>\n </svg>\n `;\n }\n\n private _renderLegend(data: DistributionData) {\n const hasLabels = this.values.some((v) => v.label !== \"\");\n if (!hasLabels) return nothing;\n return html`\n <div class=\"legend\">\n ${this.values.map((v, i) => {\n const color = this.markerColors[i % this.markerColors.length];\n const formatted = v.value.toLocaleString(\"en-US\", { maximumFractionDigits: 0 });\n return html`\n <span class=\"legend-item\">\n <span class=\"legend-dot\" style=\"background:${color}\"></span>\n ${v.label}: ${formatted} ${data.unit}\n </span>\n `;\n })}\n </div>\n `;\n }\n\n override render() {\n if (this._loading || this._width === 0) return html`<div class=\"wrap\"><div class=\"skeleton\"></div></div>`;\n if (this._error) return html`<div class=\"wrap\"><div class=\"error\">Could not load distribution</div></div>`;\n if (!this._data) return nothing;\n return html`\n <div class=\"wrap\">\n ${this._renderSvg(this._data)}\n ${this._renderLegend(this._data)}\n </div>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"distribution-chart\": DistributionChart;\n }\n}\n"]}
1
+ {"version":3,"file":"distribution-chart.js","sourceRoot":"","sources":["../src/distribution-chart.ts"],"names":[],"mappings":"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","sourcesContent":["import { LitElement, css, html, nothing, svg } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\nimport { scaleLinear } from \"d3-scale\";\nimport { line, area } from \"d3-shape\";\nimport { max } from \"d3-array\";\nimport { tokens } from \"./tokens.js\";\n\ninterface DistributionPoint {\n x: number;\n y: number;\n}\n\ninterface DistributionData {\n points: DistributionPoint[];\n unit: string;\n label: string;\n min: number;\n max: number;\n mean: number;\n}\n\nexport interface DistributionValue {\n /** Short label shown on the chart marker, e.g. 'A'. Empty string = show raw value instead. */\n label: string;\n value: number;\n}\n\n/** Fixed SVG height in CSS pixels — the coordinate system is always 1 user-unit = 1 px. */\nconst SVG_H = 130;\nconst PAD = { top: 22, right: 12, bottom: 18, left: 12 };\n\n/** Default marker colors (indigo/amber/teal/rose 600s), assigned in order. */\nconst DEFAULT_MARKER_COLORS = [\"#4f46e5\", \"#d97706\", \"#0d9488\", \"#e11d48\"];\n\n/**\n * Renders a KDE distribution curve for a named metric with one or more value\n * markers. The SVG viewBox is kept in sync with the element's pixel width via\n * ResizeObserver so that font sizes and stroke widths are always in real pixels\n * regardless of container width.\n *\n * Pass `fontSize` to control all text size (default 11). Pass a single\n * `{label:'', value}` for a single-value display or multiple\n * `{label:'A'|'B'|...}` entries to compare several values.\n *\n * @element distribution-chart\n */\n@customElement(\"distribution-chart\")\nexport class DistributionChart extends LitElement {\n static override styles = [\n tokens,\n css`\n :host {\n display: block;\n width: 100%;\n }\n .wrap {\n padding: 0.25rem 0 0.5rem;\n }\n svg {\n display: block;\n overflow: visible;\n font-family: var(\n --ui-font,\n ui-sans-serif,\n system-ui,\n sans-serif,\n \"Apple Color Emoji\",\n \"Segoe UI Emoji\",\n \"Segoe UI Symbol\",\n \"Noto Color Emoji\"\n );\n }\n .curve {\n fill: none;\n stroke: var(--ui-primary, #4f46e5);\n stroke-width: 1.5;\n stroke-linejoin: round;\n }\n .fill {\n fill: url(#dist-fill-grad);\n }\n .fill-start {\n stop-color: var(--ui-primary, #4f46e5);\n }\n .crosshair {\n stroke: var(--ui-text-muted, #64748b);\n }\n .chart-label {\n fill: var(--ui-text-muted, #64748b);\n }\n .skeleton {\n background: linear-gradient(\n 90deg,\n var(--ui-surface-muted, #f8fafc) 25%,\n var(--ui-border, #e2e8f0) 50%,\n var(--ui-surface-muted, #f8fafc) 75%\n );\n background-size: 200% 100%;\n animation: shimmer 1.4s infinite;\n border-radius: var(--ui-radius-sm, 0.25rem);\n height: 6.25rem;\n }\n @keyframes shimmer {\n 0% {\n background-position: 200% 0;\n }\n 100% {\n background-position: -200% 0;\n }\n }\n .error {\n font-size: var(--ui-font-size-sm, 0.75rem);\n color: var(--ui-danger, #dc2626);\n padding: 0.5rem 0;\n }\n .legend {\n display: flex;\n flex-wrap: wrap;\n gap: 0.5rem 0.75rem;\n padding: 0.25rem 0 0.25rem 0.5rem;\n }\n .legend-item {\n display: inline-flex;\n align-items: center;\n gap: 0.25rem;\n font-size: var(--ui-font-size-sm, 0.75rem);\n color: var(--ui-text-muted, #64748b);\n }\n .legend-dot {\n display: inline-block;\n width: 0.5rem;\n height: 0.5rem;\n border-radius: 50%;\n flex: 0 0 auto;\n }\n @media (prefers-reduced-motion: reduce) {\n .skeleton {\n animation: none;\n }\n }\n `,\n ];\n\n /** Metric name, fetched from `/api/distribution/<metric>` on change. */\n @property() metric = \"\";\n /** One or more values to mark on the distribution curve. */\n @property({ attribute: false }) values: DistributionValue[] = [];\n /** Colors assigned to markers in order. Defaults to indigo/amber/teal/rose 600s. */\n @property({ attribute: false }) markerColors: string[] = DEFAULT_MARKER_COLORS;\n /** Target font size in CSS pixels (default 11). Always renders at this size regardless of container width. */\n @property({ type: Number, attribute: \"font-size\" }) fontSize = 11;\n\n @state() private _data: DistributionData | null = null;\n @state() private _loading = false;\n @state() private _error: string | null = null;\n /** Measured element width in CSS pixels. 0 until ResizeObserver fires. */\n @state() private _width = 0;\n /** Inner x coordinate (px) of the current hover position, or null when not hovering. */\n @state() private _hoverX: number | null = null;\n\n private _fetchedFor = \"\";\n private _ro: ResizeObserver | null = null;\n\n override connectedCallback() {\n super.connectedCallback();\n this._maybeFetch();\n }\n\n override firstUpdated() {\n this._ro = new ResizeObserver((entries) => {\n const w = Math.floor(entries[0].contentRect.width);\n if (w > 0 && w !== this._width) this._width = w;\n });\n this._ro.observe(this);\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback();\n this._ro?.disconnect();\n this._ro = null;\n }\n\n override updated(changed: Map<string, unknown>) {\n if (changed.has(\"metric\")) this._maybeFetch();\n }\n\n private _maybeFetch() {\n if (!this.metric || this._fetchedFor === this.metric) return;\n this._fetchedFor = this.metric;\n this._loading = true;\n this._error = null;\n fetch(`/api/distribution/${encodeURIComponent(this.metric)}`)\n .then((r) => {\n if (!r.ok) throw new Error(`HTTP ${r.status}`);\n return r.json() as Promise<DistributionData>;\n })\n .then((data) => {\n this._data = data;\n this._loading = false;\n })\n .catch((err: unknown) => {\n this._error = err instanceof Error ? err.message : \"Failed to load\";\n this._loading = false;\n });\n }\n\n private _renderSvg(data: DistributionData) {\n if (this._width === 0) return nothing;\n\n const innerW = this._width - PAD.left - PAD.right;\n const innerH = SVG_H - PAD.top - PAD.bottom;\n const fs = this.fontSize;\n\n const pts = data.points;\n const maxY = max(pts, (p) => p.y) ?? 1;\n\n const xScale = scaleLinear().domain([pts[0].x, pts[pts.length - 1].x]).range([0, innerW]);\n const yScale = scaleLinear().domain([0, maxY]).range([innerH, 0]);\n\n const lineFn = line<DistributionPoint>()\n .x((p) => xScale(p.x))\n .y((p) => yScale(p.y));\n\n const areaFn = area<DistributionPoint>()\n .x((p) => xScale(p.x))\n .y0(innerH)\n .y1((p) => yScale(p.y));\n\n const curvePath = lineFn(pts) ?? \"\";\n const fillPath = areaFn(pts) ?? \"\";\n\n const minLabel = data.min.toLocaleString(\"en-US\", { maximumFractionDigits: 0 });\n const maxLabel = data.max.toLocaleString(\"en-US\", { maximumFractionDigits: 0 });\n\n const hasLabels = this.values.some((v) => v.label !== \"\");\n\n const markers = this.values\n .filter((v) => v.value >= pts[0].x && v.value <= pts[pts.length - 1].x)\n .map((v, i) => {\n const color = this.markerColors[i % this.markerColors.length];\n const mx = xScale(v.value);\n const labelText = hasLabels\n ? v.label\n : `${v.value.toLocaleString(\"en-US\", { maximumFractionDigits: 0 })} ${data.unit}`;\n return svg`\n <line stroke=${color} stroke-width=\"1.5\" stroke-dasharray=\"4 3\"\n x1=${mx} y1=${0} x2=${mx} y2=${innerH} />\n <text font-size=${fs} font-weight=\"600\" fill=${color} text-anchor=\"middle\"\n x=${mx} y=${-fs * 0.4}>${labelText}</text>\n `;\n });\n\n const crosshair = this._hoverX != null ? (() => {\n const hx = Math.max(0, Math.min(innerW, this._hoverX!));\n const hVal = xScale.invert(hx);\n const hLabel = `${Math.round(hVal).toLocaleString(\"en-US\")} ${data.unit}`;\n // Keep label anchor away from edges so it doesn't clip\n const anchor = hx < innerW * 0.2 ? \"start\" : hx > innerW * 0.8 ? \"end\" : \"middle\";\n const lx = hx < innerW * 0.2 ? 0 : hx > innerW * 0.8 ? innerW : hx;\n return svg`\n <line class=\"crosshair\" stroke-width=\"1\" x1=${hx} y1=${0} x2=${hx} y2=${innerH} />\n <text class=\"chart-label\" font-size=${fs} font-weight=\"500\"\n text-anchor=${anchor} x=${lx} y=${-fs * 0.4}>${hLabel}</text>\n `;\n })() : nothing;\n\n const markerSummary = this.values.length > 0\n ? ` Markers: ${this.values\n .map((value) => `${value.label || \"value\"} ${value.value.toLocaleString(\"en-US\")} ${data.unit}`)\n .join(\", \")}.`\n : \"\";\n const summary =\n `${data.label}: ${data.min.toLocaleString(\"en-US\")} to ${data.max.toLocaleString(\"en-US\")} ${data.unit}, ` +\n `mean ${data.mean.toLocaleString(\"en-US\")} ${data.unit}.${markerSummary}`;\n\n return svg`\n <svg viewBox=\"0 0 ${this._width} ${SVG_H}\" width=${this._width} height=${SVG_H}\n role=\"img\" aria-label=${summary}\n style=\"cursor:crosshair\"\n @mousemove=${(e: MouseEvent) => {\n const innerX = e.offsetX - PAD.left;\n this._hoverX = (innerX >= 0 && innerX <= innerW) ? innerX : null;\n }}\n @mouseleave=${() => { this._hoverX = null; }}>\n <defs>\n <linearGradient id=\"dist-fill-grad\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop class=\"fill-start\" offset=\"0%\" stop-opacity=\"0.28\"/>\n <stop class=\"fill-start\" offset=\"100%\" stop-opacity=\"0\"/>\n </linearGradient>\n </defs>\n <g transform=\"translate(${PAD.left},${PAD.top})\">\n <path class=\"fill\" d=${fillPath} />\n <path class=\"curve\" d=${curvePath} />\n ${markers}\n ${crosshair}\n <text class=\"chart-label\" font-size=${fs} text-anchor=\"start\" x=${0} y=${innerH + fs * 1.4}>${minLabel}</text>\n <text class=\"chart-label\" font-size=${fs} text-anchor=\"end\" x=${innerW} y=${innerH + fs * 1.4}>${maxLabel}</text>\n <text class=\"chart-label\" font-size=${fs} text-anchor=\"middle\" x=${innerW / 2} y=${innerH + fs * 1.4}>${data.unit}</text>\n </g>\n </svg>\n `;\n }\n\n private _renderLegend(data: DistributionData) {\n const hasLabels = this.values.some((v) => v.label !== \"\");\n if (!hasLabels) return nothing;\n return html`\n <div class=\"legend\">\n ${this.values.map((v, i) => {\n const color = this.markerColors[i % this.markerColors.length];\n const formatted = v.value.toLocaleString(\"en-US\", { maximumFractionDigits: 0 });\n return html`\n <span class=\"legend-item\">\n <span class=\"legend-dot\" style=\"background:${color}\"></span>\n ${v.label}: ${formatted} ${data.unit}\n </span>\n `;\n })}\n </div>\n `;\n }\n\n override render() {\n if (this._loading || this._width === 0) return html`<div class=\"wrap\"><div class=\"skeleton\"></div></div>`;\n if (this._error) return html`<div class=\"wrap\"><div class=\"error\">Could not load distribution</div></div>`;\n if (!this._data) return nothing;\n return html`\n <div class=\"wrap\">\n ${this._renderSvg(this._data)}\n ${this._renderLegend(this._data)}\n </div>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"distribution-chart\": DistributionChart;\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"dropdown-button.d.ts","sourceRoot":"","sources":["../src/dropdown-button.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAsB,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAK1E,4BAA4B;AAC5B,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;GAQG;AACH,qBACa,cAAe,SAAQ,UAAU;;IAC5C,OAAgB,MAAM,4BAoEpB;IAEF,kCAAkC;IACtB,KAAK,SAAM;IACvB,0BAA0B;IACM,OAAO,EAAE,cAAc,EAAE,CAAM;IAC/D,8DAA8D;IACjC,QAAQ,UAAS;IAErC,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,YAAY,CAAM;IAE1B,oBAAoB,IAAI,IAAI,CAGpC;IAED,UAAmB,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI,CAQxD;IA4CD,OAAO,CAAC,UAAU;IAsBT,MAAM,yCAgBd;CACF;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,iBAAiB,EAAE,cAAc,CAAC;KACnC;CACF"}
1
+ {"version":3,"file":"dropdown-button.d.ts","sourceRoot":"","sources":["../src/dropdown-button.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAsB,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAO1E,4BAA4B;AAC5B,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;GAQG;AACH,qBACa,cAAe,SAAQ,UAAU;;IAC5C,OAAgB,MAAM,4BAkGpB;IAEF,kCAAkC;IACtB,KAAK,SAAM;IACvB,0BAA0B;IACM,OAAO,EAAE,cAAc,EAAE,CAAM;IAC/D,8DAA8D;IACjC,QAAQ,UAAS;IAErC,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,YAAY,CAAM;IAG1B,oBAAoB,IAAI,IAAI,CAKpC;IAED,UAAmB,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI,CAYxD;IA6CD,OAAO,CAAC,UAAU;IAwBT,MAAM,yCAsBd;CACF;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,iBAAiB,EAAE,cAAc,CAAC;KACnC;CACF"}
@@ -8,6 +8,7 @@ import { LitElement, css, html, nothing } from "lit";
8
8
  import { customElement, property, state } from "lit/decorators.js";
9
9
  import { iconChevronRight } from "./icons.js";
10
10
  import { tokens } from "./tokens.js";
11
+ let instanceCount = 0;
11
12
  /**
12
13
  * A primary-styled button with a label and chevron that opens an anchored
13
14
  * menu of actions — essentially `form-select` minus "current value"
@@ -28,6 +29,7 @@ let DropdownButton = class DropdownButton extends LitElement {
28
29
  this.disabled = false;
29
30
  this._open = false;
30
31
  this._activeIndex = -1;
32
+ this.#menuId = `dropdown-button-menu-${++instanceCount}`;
31
33
  this.#onWindowMousedown = (e) => {
32
34
  if (!e.composedPath().includes(this))
33
35
  this._open = false;
@@ -39,20 +41,29 @@ let DropdownButton = class DropdownButton extends LitElement {
39
41
  :host {
40
42
  display: inline-block;
41
43
  position: relative;
42
- font-family: var(--ui-font, ui-sans-serif, system-ui, sans-serif);
44
+ font-family: var(
45
+ --ui-font,
46
+ ui-sans-serif,
47
+ system-ui,
48
+ sans-serif,
49
+ "Apple Color Emoji",
50
+ "Segoe UI Emoji",
51
+ "Segoe UI Symbol",
52
+ "Noto Color Emoji"
53
+ );
43
54
  }
44
55
  button.trigger {
45
56
  display: flex;
46
57
  align-items: center;
47
- gap: 0.35rem;
58
+ gap: 0.25rem;
48
59
  font: inherit;
49
60
  font-size: var(--ui-font-size-sm, 0.75rem);
50
61
  font-weight: 500;
51
- color: #fff;
62
+ color: var(--ui-on-accent, #ffffff);
52
63
  background: var(--ui-primary, #4f46e5);
53
64
  border: 1px solid transparent;
54
65
  border-radius: var(--ui-radius-sm, 0.25rem);
55
- padding: 0.5rem 0.9rem;
66
+ padding: 0.5rem 1rem;
56
67
  cursor: pointer;
57
68
  }
58
69
  button.trigger:hover:not(:disabled) {
@@ -69,7 +80,7 @@ let DropdownButton = class DropdownButton extends LitElement {
69
80
  .chevron {
70
81
  display: flex;
71
82
  transform: rotate(90deg);
72
- transition: transform 0.1s ease;
83
+ transition: transform 150ms ease;
73
84
  }
74
85
  :host([open]) .chevron {
75
86
  transform: rotate(-90deg);
@@ -82,17 +93,17 @@ let DropdownButton = class DropdownButton extends LitElement {
82
93
  min-width: 100%;
83
94
  max-height: 40vh;
84
95
  overflow-y: auto;
85
- margin: 2px 0 0;
86
- padding: 4px 0;
96
+ margin: 0.25rem 0 0;
97
+ padding: 0.25rem 0;
87
98
  list-style: none;
88
99
  white-space: nowrap;
89
- background: var(--ui-surface, #fff);
100
+ background: var(--ui-surface, #ffffff);
90
101
  border: 1px solid var(--ui-border, #e2e8f0);
91
102
  border-radius: var(--ui-radius-sm, 0.25rem);
92
103
  box-shadow: var(--ui-shadow, 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1));
93
104
  }
94
105
  li {
95
- padding: 0.35rem 0.6rem;
106
+ padding: 0.5rem 0.75rem;
96
107
  cursor: pointer;
97
108
  color: var(--ui-text, #0f172a);
98
109
  }
@@ -100,13 +111,41 @@ let DropdownButton = class DropdownButton extends LitElement {
100
111
  li:hover {
101
112
  background: var(--ui-surface-muted, #f8fafc);
102
113
  }
114
+ @media (prefers-reduced-motion: reduce) {
115
+ .chevron {
116
+ transition: none;
117
+ }
118
+ }
119
+ @media (forced-colors: active) {
120
+ button.trigger:focus-visible {
121
+ outline: 2px solid CanvasText;
122
+ outline-offset: 2px;
123
+ box-shadow: none;
124
+ }
125
+ button.trigger:disabled {
126
+ color: GrayText;
127
+ opacity: 1;
128
+ }
129
+ li.active,
130
+ li:hover {
131
+ color: HighlightText;
132
+ background: Highlight;
133
+ }
134
+ }
103
135
  `,
104
136
  ]; }
137
+ #menuId;
105
138
  disconnectedCallback() {
106
139
  super.disconnectedCallback();
107
140
  window.removeEventListener("mousedown", this.#onWindowMousedown, true);
141
+ this._open = false;
142
+ this._activeIndex = -1;
108
143
  }
109
144
  updated(changed) {
145
+ if (changed.has("disabled") && this.disabled) {
146
+ this._open = false;
147
+ this._activeIndex = -1;
148
+ }
110
149
  if (!changed.has("_open"))
111
150
  return;
112
151
  this.toggleAttribute("open", this._open);
@@ -126,6 +165,8 @@ let DropdownButton = class DropdownButton extends LitElement {
126
165
  this._activeIndex = 0;
127
166
  }
128
167
  #select(option) {
168
+ if (this.disabled)
169
+ return;
129
170
  this._open = false;
130
171
  this.dispatchEvent(new CustomEvent("select", { detail: { value: option.value } }));
131
172
  }
@@ -163,12 +204,15 @@ let DropdownButton = class DropdownButton extends LitElement {
163
204
  if (!this._open)
164
205
  return nothing;
165
206
  return html `
166
- <ul class="options" role="menu">
207
+ <ul id=${this.#menuId} class="options" role="menu" aria-label=${this.label}>
167
208
  ${this.options.map((o, i) => html `
168
209
  <li
210
+ id=${`${this.#menuId}-item-${i}`}
169
211
  role="menuitem"
170
212
  class=${i === this._activeIndex ? "active" : ""}
171
213
  @mousedown=${(e) => {
214
+ if (e.button !== 0)
215
+ return;
172
216
  e.preventDefault();
173
217
  this.#select(o);
174
218
  }}
@@ -180,18 +224,23 @@ let DropdownButton = class DropdownButton extends LitElement {
180
224
  `;
181
225
  }
182
226
  render() {
227
+ const activeDescendant = this._open && this._activeIndex >= 0
228
+ ? `${this.#menuId}-item-${this._activeIndex}`
229
+ : nothing;
183
230
  return html `
184
231
  <button
185
232
  type="button"
186
233
  class="trigger"
187
234
  aria-haspopup="menu"
188
235
  aria-expanded=${this._open}
236
+ aria-controls=${this.#menuId}
237
+ aria-activedescendant=${activeDescendant}
189
238
  ?disabled=${this.disabled}
190
239
  @click=${() => this.#toggle()}
191
240
  @keydown=${(e) => this.#onTriggerKeydown(e)}
192
241
  >
193
242
  <span>${this.label}</span>
194
- <span class="chevron">${iconChevronRight(14)}</span>
243
+ <span class="chevron" aria-hidden="true">${iconChevronRight(14)}</span>
195
244
  </button>
196
245
  ${this.renderMenu()}
197
246
  `;
@@ -1 +1 @@
1
- {"version":3,"file":"dropdown-button.js","sourceRoot":"","sources":["../src/dropdown-button.ts"],"names":[],"mappings":"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","sourcesContent":["import { LitElement, css, html, nothing, type PropertyValues } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\nimport { iconChevronRight } from \"./icons.js\";\nimport { tokens } from \"./tokens.js\";\n\n/** A single menu action. */\nexport interface DropdownOption {\n value: string;\n label: string;\n}\n\n/**\n * A primary-styled button with a label and chevron that opens an anchored\n * menu of actions — essentially `form-select` minus \"current value\"\n * semantics: a menu, not a select. Use for a set of mutually exclusive\n * next-step actions (e.g. a failed task's Retry / Close / Backlog).\n *\n * @element dropdown-button\n * @fires select - Fired with `{ value: string }` when a menu item is picked.\n */\n@customElement(\"dropdown-button\")\nexport class DropdownButton extends LitElement {\n static override styles = [\n tokens,\n css`\n :host {\n display: inline-block;\n position: relative;\n font-family: var(--ui-font, ui-sans-serif, system-ui, sans-serif);\n }\n button.trigger {\n display: flex;\n align-items: center;\n gap: 0.35rem;\n font: inherit;\n font-size: var(--ui-font-size-sm, 0.75rem);\n font-weight: 500;\n color: #fff;\n background: var(--ui-primary, #4f46e5);\n border: 1px solid transparent;\n border-radius: var(--ui-radius-sm, 0.25rem);\n padding: 0.5rem 0.9rem;\n cursor: pointer;\n }\n button.trigger:hover:not(:disabled) {\n background: var(--ui-primary-hover, #4338ca);\n }\n button.trigger:disabled {\n cursor: not-allowed;\n opacity: 0.6;\n }\n button.trigger:focus-visible {\n outline: none;\n box-shadow: var(--ui-focus-ring, 0 0 0 3px rgb(79 70 229 / 0.35));\n }\n .chevron {\n display: flex;\n transform: rotate(90deg);\n transition: transform 0.1s ease;\n }\n :host([open]) .chevron {\n transform: rotate(-90deg);\n }\n ul.options {\n position: absolute;\n top: 100%;\n right: 0;\n z-index: 10;\n min-width: 100%;\n max-height: 40vh;\n overflow-y: auto;\n margin: 2px 0 0;\n padding: 4px 0;\n list-style: none;\n white-space: nowrap;\n background: var(--ui-surface, #fff);\n border: 1px solid var(--ui-border, #e2e8f0);\n border-radius: var(--ui-radius-sm, 0.25rem);\n box-shadow: var(--ui-shadow, 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1));\n }\n li {\n padding: 0.35rem 0.6rem;\n cursor: pointer;\n color: var(--ui-text, #0f172a);\n }\n li.active,\n li:hover {\n background: var(--ui-surface-muted, #f8fafc);\n }\n `,\n ];\n\n /** The trigger button's label. */\n @property() label = \"\";\n /** The menu's actions. */\n @property({ attribute: false }) options: DropdownOption[] = [];\n /** Disables the trigger, preventing the menu from opening. */\n @property({ type: Boolean }) disabled = false;\n\n @state() private _open = false;\n @state() private _activeIndex = -1;\n\n override disconnectedCallback(): void {\n super.disconnectedCallback();\n window.removeEventListener(\"mousedown\", this.#onWindowMousedown, true);\n }\n\n protected override updated(changed: PropertyValues): void {\n if (!changed.has(\"_open\")) return;\n this.toggleAttribute(\"open\", this._open);\n if (this._open) {\n window.addEventListener(\"mousedown\", this.#onWindowMousedown, true);\n } else {\n window.removeEventListener(\"mousedown\", this.#onWindowMousedown, true);\n }\n }\n\n #onWindowMousedown = (e: MouseEvent): void => {\n if (!e.composedPath().includes(this)) this._open = false;\n };\n\n #toggle(): void {\n if (this.disabled) return;\n this._open = !this._open;\n if (this._open) this._activeIndex = 0;\n }\n\n #select(option: DropdownOption): void {\n this._open = false;\n this.dispatchEvent(new CustomEvent(\"select\", { detail: { value: option.value } }));\n }\n\n #onTriggerKeydown(e: KeyboardEvent): void {\n if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\" || e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n if (!this._open) {\n this.#toggle();\n return;\n }\n if (e.key === \"ArrowDown\") this.#moveActive(1);\n else if (e.key === \"ArrowUp\") this.#moveActive(-1);\n else this.#confirmActive();\n } else if (e.key === \"Escape\" && this._open) {\n e.preventDefault();\n this._open = false;\n }\n }\n\n #moveActive(delta: number): void {\n if (this.options.length === 0) return;\n const n = this.options.length;\n this._activeIndex = (this._activeIndex + delta + n) % n;\n }\n\n #confirmActive(): void {\n const option = this.options[this._activeIndex];\n if (option) this.#select(option);\n }\n\n private renderMenu() {\n if (!this._open) return nothing;\n return html`\n <ul class=\"options\" role=\"menu\">\n ${this.options.map(\n (o, i) => html`\n <li\n role=\"menuitem\"\n class=${i === this._activeIndex ? \"active\" : \"\"}\n @mousedown=${(e: MouseEvent) => {\n e.preventDefault();\n this.#select(o);\n }}\n >\n ${o.label}\n </li>\n `,\n )}\n </ul>\n `;\n }\n\n override render() {\n return html`\n <button\n type=\"button\"\n class=\"trigger\"\n aria-haspopup=\"menu\"\n aria-expanded=${this._open}\n ?disabled=${this.disabled}\n @click=${() => this.#toggle()}\n @keydown=${(e: KeyboardEvent) => this.#onTriggerKeydown(e)}\n >\n <span>${this.label}</span>\n <span class=\"chevron\">${iconChevronRight(14)}</span>\n </button>\n ${this.renderMenu()}\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"dropdown-button\": DropdownButton;\n }\n}\n"]}
1
+ {"version":3,"file":"dropdown-button.js","sourceRoot":"","sources":["../src/dropdown-button.ts"],"names":[],"mappings":"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","sourcesContent":["import { LitElement, css, html, nothing, type PropertyValues } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\nimport { iconChevronRight } from \"./icons.js\";\nimport { tokens } from \"./tokens.js\";\n\nlet instanceCount = 0;\n\n/** A single menu action. */\nexport interface DropdownOption {\n value: string;\n label: string;\n}\n\n/**\n * A primary-styled button with a label and chevron that opens an anchored\n * menu of actions — essentially `form-select` minus \"current value\"\n * semantics: a menu, not a select. Use for a set of mutually exclusive\n * next-step actions (e.g. a failed task's Retry / Close / Backlog).\n *\n * @element dropdown-button\n * @fires select - Fired with `{ value: string }` when a menu item is picked.\n */\n@customElement(\"dropdown-button\")\nexport class DropdownButton extends LitElement {\n static override styles = [\n tokens,\n css`\n :host {\n display: inline-block;\n position: relative;\n font-family: var(\n --ui-font,\n ui-sans-serif,\n system-ui,\n sans-serif,\n \"Apple Color Emoji\",\n \"Segoe UI Emoji\",\n \"Segoe UI Symbol\",\n \"Noto Color Emoji\"\n );\n }\n button.trigger {\n display: flex;\n align-items: center;\n gap: 0.25rem;\n font: inherit;\n font-size: var(--ui-font-size-sm, 0.75rem);\n font-weight: 500;\n color: var(--ui-on-accent, #ffffff);\n background: var(--ui-primary, #4f46e5);\n border: 1px solid transparent;\n border-radius: var(--ui-radius-sm, 0.25rem);\n padding: 0.5rem 1rem;\n cursor: pointer;\n }\n button.trigger:hover:not(:disabled) {\n background: var(--ui-primary-hover, #4338ca);\n }\n button.trigger:disabled {\n cursor: not-allowed;\n opacity: 0.6;\n }\n button.trigger:focus-visible {\n outline: none;\n box-shadow: var(--ui-focus-ring, 0 0 0 3px rgb(79 70 229 / 0.35));\n }\n .chevron {\n display: flex;\n transform: rotate(90deg);\n transition: transform 150ms ease;\n }\n :host([open]) .chevron {\n transform: rotate(-90deg);\n }\n ul.options {\n position: absolute;\n top: 100%;\n right: 0;\n z-index: 10;\n min-width: 100%;\n max-height: 40vh;\n overflow-y: auto;\n margin: 0.25rem 0 0;\n padding: 0.25rem 0;\n list-style: none;\n white-space: nowrap;\n background: var(--ui-surface, #ffffff);\n border: 1px solid var(--ui-border, #e2e8f0);\n border-radius: var(--ui-radius-sm, 0.25rem);\n box-shadow: var(--ui-shadow, 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1));\n }\n li {\n padding: 0.5rem 0.75rem;\n cursor: pointer;\n color: var(--ui-text, #0f172a);\n }\n li.active,\n li:hover {\n background: var(--ui-surface-muted, #f8fafc);\n }\n @media (prefers-reduced-motion: reduce) {\n .chevron {\n transition: none;\n }\n }\n @media (forced-colors: active) {\n button.trigger:focus-visible {\n outline: 2px solid CanvasText;\n outline-offset: 2px;\n box-shadow: none;\n }\n button.trigger:disabled {\n color: GrayText;\n opacity: 1;\n }\n li.active,\n li:hover {\n color: HighlightText;\n background: Highlight;\n }\n }\n `,\n ];\n\n /** The trigger button's label. */\n @property() label = \"\";\n /** The menu's actions. */\n @property({ attribute: false }) options: DropdownOption[] = [];\n /** Disables the trigger, preventing the menu from opening. */\n @property({ type: Boolean }) disabled = false;\n\n @state() private _open = false;\n @state() private _activeIndex = -1;\n readonly #menuId = `dropdown-button-menu-${++instanceCount}`;\n\n override disconnectedCallback(): void {\n super.disconnectedCallback();\n window.removeEventListener(\"mousedown\", this.#onWindowMousedown, true);\n this._open = false;\n this._activeIndex = -1;\n }\n\n protected override updated(changed: PropertyValues): void {\n if (changed.has(\"disabled\") && this.disabled) {\n this._open = false;\n this._activeIndex = -1;\n }\n if (!changed.has(\"_open\")) return;\n this.toggleAttribute(\"open\", this._open);\n if (this._open) {\n window.addEventListener(\"mousedown\", this.#onWindowMousedown, true);\n } else {\n window.removeEventListener(\"mousedown\", this.#onWindowMousedown, true);\n }\n }\n\n #onWindowMousedown = (e: MouseEvent): void => {\n if (!e.composedPath().includes(this)) this._open = false;\n };\n\n #toggle(): void {\n if (this.disabled) return;\n this._open = !this._open;\n if (this._open) this._activeIndex = 0;\n }\n\n #select(option: DropdownOption): void {\n if (this.disabled) return;\n this._open = false;\n this.dispatchEvent(new CustomEvent(\"select\", { detail: { value: option.value } }));\n }\n\n #onTriggerKeydown(e: KeyboardEvent): void {\n if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\" || e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n if (!this._open) {\n this.#toggle();\n return;\n }\n if (e.key === \"ArrowDown\") this.#moveActive(1);\n else if (e.key === \"ArrowUp\") this.#moveActive(-1);\n else this.#confirmActive();\n } else if (e.key === \"Escape\" && this._open) {\n e.preventDefault();\n this._open = false;\n }\n }\n\n #moveActive(delta: number): void {\n if (this.options.length === 0) return;\n const n = this.options.length;\n this._activeIndex = (this._activeIndex + delta + n) % n;\n }\n\n #confirmActive(): void {\n const option = this.options[this._activeIndex];\n if (option) this.#select(option);\n }\n\n private renderMenu() {\n if (!this._open) return nothing;\n return html`\n <ul id=${this.#menuId} class=\"options\" role=\"menu\" aria-label=${this.label}>\n ${this.options.map(\n (o, i) => html`\n <li\n id=${`${this.#menuId}-item-${i}`}\n role=\"menuitem\"\n class=${i === this._activeIndex ? \"active\" : \"\"}\n @mousedown=${(e: MouseEvent) => {\n if (e.button !== 0) return;\n e.preventDefault();\n this.#select(o);\n }}\n >\n ${o.label}\n </li>\n `,\n )}\n </ul>\n `;\n }\n\n override render() {\n const activeDescendant =\n this._open && this._activeIndex >= 0\n ? `${this.#menuId}-item-${this._activeIndex}`\n : nothing;\n return html`\n <button\n type=\"button\"\n class=\"trigger\"\n aria-haspopup=\"menu\"\n aria-expanded=${this._open}\n aria-controls=${this.#menuId}\n aria-activedescendant=${activeDescendant}\n ?disabled=${this.disabled}\n @click=${() => this.#toggle()}\n @keydown=${(e: KeyboardEvent) => this.#onTriggerKeydown(e)}\n >\n <span>${this.label}</span>\n <span class=\"chevron\" aria-hidden=\"true\">${iconChevronRight(14)}</span>\n </button>\n ${this.renderMenu()}\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"dropdown-button\": DropdownButton;\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"editable-text.d.ts","sourceRoot":"","sources":["../src/editable-text.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAsB,MAAM,KAAK,CAAC;AAIrD;;;;;;;;GAQG;AACH,qBACa,YAAa,SAAQ,UAAU;IAC1C,OAAgB,MAAM,4BAuDpB;IAEF,oBAAoB;IACR,KAAK,SAAM;IACvB,yEAAyE;IAC5C,SAAS,UAAS;IAC/C,0FAA0F;IAC9E,WAAW,SAAM;IAC7B,mCAAmC;IACN,QAAQ,UAAS;IAC9C,kDAAkD;IACtC,KAAK,SAAM;IAEd,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,MAAM,CAAM;IAEH,OAAO,CAAC,MAAM,CAAC,CAAyC;IAElF,kEAAkE;IAClE,OAAO,CAAC,UAAU;IAMlB;;;;;;;OAOG;IACH,OAAO,CAAC,OAAO;IAUf,iEAAiE;IACjE,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,QAAQ;IAKhB,OAAO,CAAC,oBAAoB;IAU5B,OAAO,CAAC,mBAAmB;IAU3B,sDAAsD;IACtD,OAAO,CAAC,OAAO;IAOf,UAAmB,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,QASvD;IAEQ,MAAM,yCA4Bd;CACF;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,eAAe,EAAE,YAAY,CAAC;KAC/B;CACF"}
1
+ {"version":3,"file":"editable-text.d.ts","sourceRoot":"","sources":["../src/editable-text.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAsB,MAAM,KAAK,CAAC;AAIrD;;;;;;;;GAQG;AACH,qBACa,YAAa,SAAQ,UAAU;IAC1C,OAAgB,MAAM,4BAqFpB;IAEF,oBAAoB;IACR,KAAK,SAAM;IACvB,yEAAyE;IAC5C,SAAS,UAAS;IAC/C,0FAA0F;IAC9E,WAAW,SAAM;IAC7B,mCAAmC;IACN,QAAQ,UAAS;IAC9C,kDAAkD;IACtC,KAAK,SAAM;IAEd,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,MAAM,CAAM;IAEH,OAAO,CAAC,MAAM,CAAC,CAAyC;IAElF,kEAAkE;IAClE,OAAO,CAAC,UAAU;IAMlB;;;;;;;OAOG;IACH,OAAO,CAAC,OAAO;IAUf,iEAAiE;IACjE,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,QAAQ;IAKhB,OAAO,CAAC,oBAAoB;IAU5B,OAAO,CAAC,mBAAmB;IAU3B,sDAAsD;IACtD,OAAO,CAAC,OAAO;IAOf,UAAmB,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,QASvD;IAEQ,MAAM,yCA+Bd;CACF;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,eAAe,EAAE,YAAY,CAAC;KAC/B;CACF"}
@@ -43,26 +43,43 @@ let EditableText = class EditableText extends LitElement {
43
43
  line-height: inherit;
44
44
  }
45
45
  .display {
46
+ display: block;
47
+ width: 100%;
48
+ box-sizing: border-box;
49
+ border: 0;
50
+ color: inherit;
51
+ background: transparent;
52
+ font: inherit;
53
+ line-height: inherit;
54
+ text-align: inherit;
46
55
  cursor: text;
47
56
  border-radius: var(--ui-radius-sm, 0.25rem);
48
- padding: 0.1rem 0.3rem;
49
- margin: -0.1rem -0.3rem;
57
+ padding: 0.25rem;
58
+ margin: -0.25rem;
50
59
  }
51
60
  .display.multiline {
61
+ /* Inactive multiline text is flush; the active textarea keeps its editor inset. */
52
62
  white-space: pre-wrap;
63
+ padding: 0;
64
+ margin: 0;
53
65
  }
54
- .display:hover {
66
+ .display:hover:not(:disabled) {
55
67
  background: var(--ui-surface-muted, #f8fafc);
56
68
  }
57
69
  .display.empty {
58
70
  color: var(--ui-text-muted, #64748b);
59
71
  }
60
- .display.readonly {
72
+ .display:disabled {
61
73
  cursor: inherit;
74
+ opacity: 0.6;
62
75
  }
63
- .display.readonly:hover {
76
+ .display:disabled:hover {
64
77
  background: none;
65
78
  }
79
+ .display:focus-visible {
80
+ outline: none;
81
+ box-shadow: var(--ui-focus-ring, 0 0 0 3px rgb(79 70 229 / 0.35));
82
+ }
66
83
  input,
67
84
  textarea {
68
85
  display: block;
@@ -70,15 +87,15 @@ let EditableText = class EditableText extends LitElement {
70
87
  box-sizing: border-box;
71
88
  font: inherit;
72
89
  color: inherit;
73
- background: var(--ui-surface, #fff);
90
+ background: var(--ui-surface, #ffffff);
74
91
  border: 1px solid var(--ui-border, #e2e8f0);
75
92
  border-radius: var(--ui-radius-sm, 0.25rem);
76
- padding: 0.1rem 0.3rem;
77
- margin: -0.1rem -0.3rem;
93
+ padding: 0.25rem;
94
+ margin: -0.25rem;
78
95
  outline: none;
79
96
  }
80
- input:focus,
81
- textarea:focus {
97
+ input:focus-visible,
98
+ textarea:focus-visible {
82
99
  border-color: var(--ui-primary, #4f46e5);
83
100
  box-shadow: var(--ui-focus-ring, 0 0 0 3px rgb(79 70 229 / 0.35));
84
101
  }
@@ -86,6 +103,19 @@ let EditableText = class EditableText extends LitElement {
86
103
  resize: none;
87
104
  overflow: hidden;
88
105
  }
106
+ @media (forced-colors: active) {
107
+ .display:focus-visible,
108
+ input:focus-visible,
109
+ textarea:focus-visible {
110
+ outline: 2px solid CanvasText;
111
+ outline-offset: 2px;
112
+ box-shadow: none;
113
+ }
114
+ .display:disabled {
115
+ color: GrayText;
116
+ opacity: 1;
117
+ }
118
+ }
89
119
  `,
90
120
  ]; }
91
121
  /** Enters edit mode, seeding the draft from the current value. */
@@ -188,9 +218,12 @@ let EditableText = class EditableText extends LitElement {
188
218
  }
189
219
  const isEmpty = this.value.trim() === "";
190
220
  const classes = `display ${this.multiline ? "multiline" : ""} ${isEmpty ? "empty" : ""} ${this.readonly ? "readonly" : ""}`;
191
- return html `<span class=${classes} @click=${this._startEdit}
192
- >${isEmpty ? this.placeholder : this.value}</span
193
- >`;
221
+ return html `<button
222
+ type="button"
223
+ class=${classes}
224
+ ?disabled=${this.readonly}
225
+ @click=${this._startEdit}
226
+ >${isEmpty ? this.placeholder : this.value}</button>`;
194
227
  }
195
228
  };
196
229
  __decorate([