@f-ewald/components 0.7.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 (193) hide show
  1. package/README.md +76 -47
  2. package/custom-elements.json +3049 -910
  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-entry.d.ts +33 -0
  16. package/dist/calendar-entry.d.ts.map +1 -0
  17. package/dist/calendar-entry.js +64 -0
  18. package/dist/calendar-entry.js.map +1 -0
  19. package/dist/calendar-month.d.ts +67 -0
  20. package/dist/calendar-month.d.ts.map +1 -0
  21. package/dist/calendar-month.js +469 -0
  22. package/dist/calendar-month.js.map +1 -0
  23. package/dist/calendar-year.d.ts +43 -0
  24. package/dist/calendar-year.d.ts.map +1 -0
  25. package/dist/calendar-year.js +145 -0
  26. package/dist/calendar-year.js.map +1 -0
  27. package/dist/chat-message.d.ts.map +1 -1
  28. package/dist/chat-message.js +77 -24
  29. package/dist/chat-message.js.map +1 -1
  30. package/dist/confirm-dialog.d.ts +8 -1
  31. package/dist/confirm-dialog.d.ts.map +1 -1
  32. package/dist/confirm-dialog.js +130 -18
  33. package/dist/confirm-dialog.js.map +1 -1
  34. package/dist/copy-link-button.d.ts +2 -0
  35. package/dist/copy-link-button.d.ts.map +1 -1
  36. package/dist/copy-link-button.js +35 -3
  37. package/dist/copy-link-button.js.map +1 -1
  38. package/dist/data-table.d.ts +2 -0
  39. package/dist/data-table.d.ts.map +1 -1
  40. package/dist/data-table.js +83 -11
  41. package/dist/data-table.js.map +1 -1
  42. package/dist/distance-value.d.ts +0 -1
  43. package/dist/distance-value.d.ts.map +1 -1
  44. package/dist/distance-value.js +3 -8
  45. package/dist/distance-value.js.map +1 -1
  46. package/dist/distribution-chart.d.ts.map +1 -1
  47. package/dist/distribution-chart.js +54 -17
  48. package/dist/distribution-chart.js.map +1 -1
  49. package/dist/dropdown-button.d.ts.map +1 -1
  50. package/dist/dropdown-button.js +60 -11
  51. package/dist/dropdown-button.js.map +1 -1
  52. package/dist/editable-text.d.ts.map +1 -1
  53. package/dist/editable-text.js +46 -13
  54. package/dist/editable-text.js.map +1 -1
  55. package/dist/form-select.d.ts +21 -2
  56. package/dist/form-select.d.ts.map +1 -1
  57. package/dist/form-select.js +489 -37
  58. package/dist/form-select.js.map +1 -1
  59. package/dist/frame-box.d.ts.map +1 -1
  60. package/dist/frame-box.js +20 -12
  61. package/dist/frame-box.js.map +1 -1
  62. package/dist/icon-button.d.ts.map +1 -1
  63. package/dist/icon-button.js +11 -0
  64. package/dist/icon-button.js.map +1 -1
  65. package/dist/index.d.ts +4 -1
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +4 -1
  68. package/dist/index.js.map +1 -1
  69. package/dist/kbd-hint.d.ts +30 -0
  70. package/dist/kbd-hint.d.ts.map +1 -0
  71. package/dist/kbd-hint.js +184 -0
  72. package/dist/kbd-hint.js.map +1 -0
  73. package/dist/live-timer.d.ts +0 -1
  74. package/dist/live-timer.d.ts.map +1 -1
  75. package/dist/live-timer.js +1 -6
  76. package/dist/live-timer.js.map +1 -1
  77. package/dist/map-circle.d.ts +3 -0
  78. package/dist/map-circle.d.ts.map +1 -1
  79. package/dist/map-circle.js +19 -1
  80. package/dist/map-circle.js.map +1 -1
  81. package/dist/map-pin.d.ts.map +1 -1
  82. package/dist/map-pin.js +16 -1
  83. package/dist/map-pin.js.map +1 -1
  84. package/dist/percent-bar-chart.d.ts +3 -0
  85. package/dist/percent-bar-chart.d.ts.map +1 -1
  86. package/dist/percent-bar-chart.js +60 -4
  87. package/dist/percent-bar-chart.js.map +1 -1
  88. package/dist/photo-gallery.d.ts.map +1 -1
  89. package/dist/photo-gallery.js +12 -3
  90. package/dist/photo-gallery.js.map +1 -1
  91. package/dist/popover-panel.d.ts.map +1 -1
  92. package/dist/popover-panel.js +132 -23
  93. package/dist/popover-panel.js.map +1 -1
  94. package/dist/price-history-chart.d.ts.map +1 -1
  95. package/dist/price-history-chart.js +52 -18
  96. package/dist/price-history-chart.js.map +1 -1
  97. package/dist/radio-cards.d.ts +2 -0
  98. package/dist/radio-cards.d.ts.map +1 -1
  99. package/dist/radio-cards.js +51 -5
  100. package/dist/radio-cards.js.map +1 -1
  101. package/dist/radio-pills.d.ts +2 -0
  102. package/dist/radio-pills.d.ts.map +1 -1
  103. package/dist/radio-pills.js +46 -4
  104. package/dist/radio-pills.js.map +1 -1
  105. package/dist/relative-time.d.ts +0 -1
  106. package/dist/relative-time.d.ts.map +1 -1
  107. package/dist/relative-time.js +1 -6
  108. package/dist/relative-time.js.map +1 -1
  109. package/dist/reveal-button.d.ts +2 -0
  110. package/dist/reveal-button.d.ts.map +1 -1
  111. package/dist/reveal-button.js +42 -5
  112. package/dist/reveal-button.js.map +1 -1
  113. package/dist/roman-numeral.js +1 -1
  114. package/dist/roman-numeral.js.map +1 -1
  115. package/dist/slide-panel.d.ts +7 -3
  116. package/dist/slide-panel.d.ts.map +1 -1
  117. package/dist/slide-panel.js +95 -23
  118. package/dist/slide-panel.js.map +1 -1
  119. package/dist/stat-meter.d.ts.map +1 -1
  120. package/dist/stat-meter.js +17 -7
  121. package/dist/stat-meter.js.map +1 -1
  122. package/dist/status-pill.d.ts.map +1 -1
  123. package/dist/status-pill.js +26 -12
  124. package/dist/status-pill.js.map +1 -1
  125. package/dist/tile-grid.d.ts +8 -0
  126. package/dist/tile-grid.d.ts.map +1 -1
  127. package/dist/tile-grid.js +88 -12
  128. package/dist/tile-grid.js.map +1 -1
  129. package/dist/toast-notification.d.ts.map +1 -1
  130. package/dist/toast-notification.js +40 -11
  131. package/dist/toast-notification.js.map +1 -1
  132. package/dist/tokens.css +23 -0
  133. package/dist/tokens.d.ts.map +1 -1
  134. package/dist/tokens.js +16 -0
  135. package/dist/tokens.js.map +1 -1
  136. package/dist/ui-button.d.ts +2 -0
  137. package/dist/ui-button.d.ts.map +1 -1
  138. package/dist/ui-button.js +58 -9
  139. package/dist/ui-button.js.map +1 -1
  140. package/dist/user-avatar.d.ts.map +1 -1
  141. package/dist/user-avatar.js +18 -3
  142. package/dist/user-avatar.js.map +1 -1
  143. package/dist/utils/calendar.d.ts +77 -0
  144. package/dist/utils/calendar.d.ts.map +1 -0
  145. package/dist/utils/calendar.js +135 -0
  146. package/dist/utils/calendar.js.map +1 -0
  147. package/dist/utils/form.d.ts +3 -0
  148. package/dist/utils/form.d.ts.map +1 -0
  149. package/dist/utils/form.js +14 -0
  150. package/dist/utils/form.js.map +1 -0
  151. package/dist/utils/layer-stack.d.ts +9 -0
  152. package/dist/utils/layer-stack.d.ts.map +1 -0
  153. package/dist/utils/layer-stack.js +35 -0
  154. package/dist/utils/layer-stack.js.map +1 -0
  155. package/dist/weight-bar-chart.d.ts.map +1 -1
  156. package/dist/weight-bar-chart.js +27 -8
  157. package/dist/weight-bar-chart.js.map +1 -1
  158. package/docs/address-autocomplete.md +5 -0
  159. package/docs/autocomplete-input.md +5 -0
  160. package/docs/calendar-entry.md +53 -0
  161. package/docs/calendar-month.md +79 -0
  162. package/docs/calendar-year.md +53 -0
  163. package/docs/chat-message.md +4 -0
  164. package/docs/confirm-dialog.md +2 -0
  165. package/docs/copy-link-button.md +2 -0
  166. package/docs/data-table.md +2 -0
  167. package/docs/design-language.md +149 -0
  168. package/docs/distribution-chart.md +4 -0
  169. package/docs/dropdown-button.md +1 -0
  170. package/docs/form-select.md +15 -3
  171. package/docs/frame-box.md +2 -0
  172. package/docs/kbd-hint.md +42 -0
  173. package/docs/map-circle.md +4 -0
  174. package/docs/percent-bar-chart.md +4 -1
  175. package/docs/photo-gallery.md +1 -0
  176. package/docs/popover-panel.md +2 -0
  177. package/docs/price-history-chart.md +8 -1
  178. package/docs/radio-cards.md +2 -0
  179. package/docs/radio-pills.md +2 -0
  180. package/docs/reveal-button.md +3 -0
  181. package/docs/slide-panel.md +4 -2
  182. package/docs/status-pill.md +3 -0
  183. package/docs/tile-grid.md +10 -1
  184. package/docs/toast-notification.md +4 -1
  185. package/docs/ui-button.md +2 -0
  186. package/docs/user-avatar.md +1 -0
  187. package/llms.txt +175 -61
  188. package/package.json +14 -2
  189. package/dist/map-point.d.ts +0 -31
  190. package/dist/map-point.d.ts.map +0 -1
  191. package/dist/map-point.js +0 -83
  192. package/dist/map-point.js.map +0 -1
  193. package/docs/map-point.md +0 -44
package/dist/tokens.css CHANGED
@@ -2,14 +2,21 @@
2
2
  color-scheme: light;
3
3
  --ui-primary: #4f46e5;
4
4
  --ui-primary-hover: #4338ca;
5
+ --ui-info: #0ea5e9;
6
+ --ui-info-hover: #0284c7;
5
7
  --ui-danger: #dc2626;
6
8
  --ui-danger-hover: #b91c1c;
7
9
  --ui-success: #16a34a;
10
+ --ui-warning: #d97706;
11
+ --ui-warning-hover: #b45309;
12
+ --ui-on-accent: #ffffff;
8
13
  --ui-text: #0f172a;
9
14
  --ui-text-muted: #64748b;
10
15
  --ui-border: #e2e8f0;
11
16
  --ui-surface: #ffffff;
12
17
  --ui-surface-muted: #f8fafc;
18
+ --ui-tooltip: #0f172a;
19
+ --ui-hover-overlay: rgb(255 255 255 / 0.32);
13
20
  --ui-overlay: rgb(15 23 42 / 0.45);
14
21
  --ui-radius: 0.5rem;
15
22
  --ui-radius-sm: 0.25rem;
@@ -17,8 +24,10 @@
17
24
  --ui-shadow-lg: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
18
25
  --ui-focus-ring: 0 0 0 3px rgb(79 70 229 / 0.35);
19
26
  --ui-font: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
27
+ --ui-font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
20
28
  --ui-font-size: 0.875rem;
21
29
  --ui-font-size-sm: 0.75rem;
30
+ --ui-font-size-xs: 0.6875rem;
22
31
  }
23
32
 
24
33
  @media (prefers-color-scheme: dark) {
@@ -26,14 +35,21 @@
26
35
  color-scheme: dark;
27
36
  --ui-primary: #6366f1;
28
37
  --ui-primary-hover: #818cf8;
38
+ --ui-info: #38bdf8;
39
+ --ui-info-hover: #7dd3fc;
29
40
  --ui-danger: #ef4444;
30
41
  --ui-danger-hover: #f87171;
31
42
  --ui-success: #22c55e;
43
+ --ui-warning: #f59e0b;
44
+ --ui-warning-hover: #fbbf24;
45
+ --ui-on-accent: #ffffff;
32
46
  --ui-text: #f1f5f9;
33
47
  --ui-text-muted: #94a3b8;
34
48
  --ui-border: #334155;
35
49
  --ui-surface: #0f172a;
36
50
  --ui-surface-muted: #1e293b;
51
+ --ui-tooltip: #020617;
52
+ --ui-hover-overlay: rgb(255 255 255 / 0.12);
37
53
  --ui-overlay: rgb(2 6 23 / 0.6);
38
54
  --ui-focus-ring: 0 0 0 3px rgb(99 102 241 / 0.45);
39
55
  }
@@ -43,14 +59,21 @@
43
59
  color-scheme: dark;
44
60
  --ui-primary: #6366f1;
45
61
  --ui-primary-hover: #818cf8;
62
+ --ui-info: #38bdf8;
63
+ --ui-info-hover: #7dd3fc;
46
64
  --ui-danger: #ef4444;
47
65
  --ui-danger-hover: #f87171;
48
66
  --ui-success: #22c55e;
67
+ --ui-warning: #f59e0b;
68
+ --ui-warning-hover: #fbbf24;
69
+ --ui-on-accent: #ffffff;
49
70
  --ui-text: #f1f5f9;
50
71
  --ui-text-muted: #94a3b8;
51
72
  --ui-border: #334155;
52
73
  --ui-surface: #0f172a;
53
74
  --ui-surface-muted: #1e293b;
75
+ --ui-tooltip: #020617;
76
+ --ui-hover-overlay: rgb(255 255 255 / 0.12);
54
77
  --ui-overlay: rgb(2 6 23 / 0.6);
55
78
  --ui-focus-ring: 0 0 0 3px rgb(99 102 241 / 0.45);
56
79
  }
@@ -1 +1 @@
1
- {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CA4B9C,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAalD,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,MAAM,yBAAQ,CAAC"}
1
+ {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAsC9C,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAoBlD,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,MAAM,yBAAQ,CAAC"}
package/dist/tokens.js CHANGED
@@ -11,14 +11,21 @@ export const tokenValues = {
11
11
  // Colors
12
12
  "--ui-primary": "#4f46e5", // indigo-600
13
13
  "--ui-primary-hover": "#4338ca", // indigo-700
14
+ "--ui-info": "#0ea5e9", // sky-500
15
+ "--ui-info-hover": "#0284c7", // sky-600
14
16
  "--ui-danger": "#dc2626", // red-600
15
17
  "--ui-danger-hover": "#b91c1c", // red-700
16
18
  "--ui-success": "#16a34a", // green-600
19
+ "--ui-warning": "#d97706", // amber-600
20
+ "--ui-warning-hover": "#b45309", // amber-700
21
+ "--ui-on-accent": "#ffffff",
17
22
  "--ui-text": "#0f172a", // slate-900
18
23
  "--ui-text-muted": "#64748b", // slate-500
19
24
  "--ui-border": "#e2e8f0", // slate-200
20
25
  "--ui-surface": "#ffffff", // white
21
26
  "--ui-surface-muted": "#f8fafc", // slate-50
27
+ "--ui-tooltip": "#0f172a", // slate-900
28
+ "--ui-hover-overlay": "rgb(255 255 255 / 0.32)",
22
29
  "--ui-overlay": "rgb(15 23 42 / 0.45)", // slate-900 / 45%
23
30
  // Shape / depth
24
31
  "--ui-radius": "0.5rem", // rounded-lg
@@ -28,8 +35,10 @@ export const tokenValues = {
28
35
  "--ui-focus-ring": "0 0 0 3px rgb(79 70 229 / 0.35)",
29
36
  // Type
30
37
  "--ui-font": 'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
38
+ "--ui-font-mono": "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
31
39
  "--ui-font-size": "0.875rem",
32
40
  "--ui-font-size-sm": "0.75rem",
41
+ "--ui-font-size-xs": "0.6875rem",
33
42
  };
34
43
  /**
35
44
  * Dark-mode overrides for the same `--ui-*` custom properties, applied by
@@ -42,14 +51,21 @@ export const tokenValues = {
42
51
  export const darkTokenValues = {
43
52
  "--ui-primary": "#6366f1", // indigo-500
44
53
  "--ui-primary-hover": "#818cf8", // indigo-400
54
+ "--ui-info": "#38bdf8", // sky-400
55
+ "--ui-info-hover": "#7dd3fc", // sky-300
45
56
  "--ui-danger": "#ef4444", // red-500
46
57
  "--ui-danger-hover": "#f87171", // red-400
47
58
  "--ui-success": "#22c55e", // green-500
59
+ "--ui-warning": "#f59e0b", // amber-500
60
+ "--ui-warning-hover": "#fbbf24", // amber-400
61
+ "--ui-on-accent": "#ffffff",
48
62
  "--ui-text": "#f1f5f9", // slate-100
49
63
  "--ui-text-muted": "#94a3b8", // slate-400
50
64
  "--ui-border": "#334155", // slate-700
51
65
  "--ui-surface": "#0f172a", // slate-900
52
66
  "--ui-surface-muted": "#1e293b", // slate-800
67
+ "--ui-tooltip": "#020617", // slate-950
68
+ "--ui-hover-overlay": "rgb(255 255 255 / 0.12)",
53
69
  "--ui-overlay": "rgb(2 6 23 / 0.6)", // slate-950 / 60%
54
70
  "--ui-focus-ring": "0 0 0 3px rgb(99 102 241 / 0.45)",
55
71
  };
@@ -1 +1 @@
1
- {"version":3,"file":"tokens.js","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAE1B;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,WAAW,GAA2B;IACjD,SAAS;IACT,cAAc,EAAE,SAAS,EAAE,aAAa;IACxC,oBAAoB,EAAE,SAAS,EAAE,aAAa;IAC9C,aAAa,EAAE,SAAS,EAAE,UAAU;IACpC,mBAAmB,EAAE,SAAS,EAAE,UAAU;IAC1C,cAAc,EAAE,SAAS,EAAE,YAAY;IACvC,WAAW,EAAE,SAAS,EAAE,YAAY;IACpC,iBAAiB,EAAE,SAAS,EAAE,YAAY;IAC1C,aAAa,EAAE,SAAS,EAAE,YAAY;IACtC,cAAc,EAAE,SAAS,EAAE,QAAQ;IACnC,oBAAoB,EAAE,SAAS,EAAE,WAAW;IAC5C,cAAc,EAAE,sBAAsB,EAAE,kBAAkB;IAE1D,gBAAgB;IAChB,aAAa,EAAE,QAAQ,EAAE,aAAa;IACtC,gBAAgB,EAAE,SAAS,EAAE,UAAU;IACvC,aAAa,EACX,kEAAkE,EAAE,YAAY;IAClF,gBAAgB,EACd,qEAAqE,EAAE,YAAY;IACrF,iBAAiB,EAAE,iCAAiC;IAEpD,OAAO;IACP,WAAW,EACT,oHAAoH;IACtH,gBAAgB,EAAE,UAAU;IAC5B,mBAAmB,EAAE,SAAS;CAC/B,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,eAAe,GAA2B;IACrD,cAAc,EAAE,SAAS,EAAE,aAAa;IACxC,oBAAoB,EAAE,SAAS,EAAE,aAAa;IAC9C,aAAa,EAAE,SAAS,EAAE,UAAU;IACpC,mBAAmB,EAAE,SAAS,EAAE,UAAU;IAC1C,cAAc,EAAE,SAAS,EAAE,YAAY;IACvC,WAAW,EAAE,SAAS,EAAE,YAAY;IACpC,iBAAiB,EAAE,SAAS,EAAE,YAAY;IAC1C,aAAa,EAAE,SAAS,EAAE,YAAY;IACtC,cAAc,EAAE,SAAS,EAAE,YAAY;IACvC,oBAAoB,EAAE,SAAS,EAAE,YAAY;IAC7C,cAAc,EAAE,mBAAmB,EAAE,kBAAkB;IACvD,iBAAiB,EAAE,kCAAkC;CACtD,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG,GAAG,CAAA,EAAE,CAAC","sourcesContent":["import { css } from \"lit\";\n\n/**\n * Design token values, sourced from Tailwind v4's default theme palette.\n * Each entry maps a `--ui-*` custom property name to its default value.\n * These are the same values baked in as `var(--ui-*, <fallback>)` fallbacks\n * throughout component styles, so components render correctly with zero\n * external CSS. Consumers may override any of these by setting the custom\n * property on `:root` or an ancestor element (see `tokens.css`).\n */\nexport const tokenValues: Record<string, string> = {\n // Colors\n \"--ui-primary\": \"#4f46e5\", // indigo-600\n \"--ui-primary-hover\": \"#4338ca\", // indigo-700\n \"--ui-danger\": \"#dc2626\", // red-600\n \"--ui-danger-hover\": \"#b91c1c\", // red-700\n \"--ui-success\": \"#16a34a\", // green-600\n \"--ui-text\": \"#0f172a\", // slate-900\n \"--ui-text-muted\": \"#64748b\", // slate-500\n \"--ui-border\": \"#e2e8f0\", // slate-200\n \"--ui-surface\": \"#ffffff\", // white\n \"--ui-surface-muted\": \"#f8fafc\", // slate-50\n \"--ui-overlay\": \"rgb(15 23 42 / 0.45)\", // slate-900 / 45%\n\n // Shape / depth\n \"--ui-radius\": \"0.5rem\", // rounded-lg\n \"--ui-radius-sm\": \"0.25rem\", // rounded\n \"--ui-shadow\":\n \"0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)\", // shadow-md\n \"--ui-shadow-lg\":\n \"0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)\", // shadow-xl\n \"--ui-focus-ring\": \"0 0 0 3px rgb(79 70 229 / 0.35)\",\n\n // Type\n \"--ui-font\":\n 'ui-sans-serif, system-ui, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\"',\n \"--ui-font-size\": \"0.875rem\",\n \"--ui-font-size-sm\": \"0.75rem\",\n};\n\n/**\n * Dark-mode overrides for the same `--ui-*` custom properties, applied by\n * `tokens.css` via `@media (prefers-color-scheme: dark)` and `[data-theme]`\n * (see generate-tokens-css.mjs). Same Tailwind slate scale as the light\n * palette, inverted, with primary/danger/success lightened one step (500\n * instead of 600) for sufficient contrast against dark surfaces. Shape/type\n * tokens are theme-independent and not repeated here.\n */\nexport const darkTokenValues: Record<string, string> = {\n \"--ui-primary\": \"#6366f1\", // indigo-500\n \"--ui-primary-hover\": \"#818cf8\", // indigo-400\n \"--ui-danger\": \"#ef4444\", // red-500\n \"--ui-danger-hover\": \"#f87171\", // red-400\n \"--ui-success\": \"#22c55e\", // green-500\n \"--ui-text\": \"#f1f5f9\", // slate-100\n \"--ui-text-muted\": \"#94a3b8\", // slate-400\n \"--ui-border\": \"#334155\", // slate-700\n \"--ui-surface\": \"#0f172a\", // slate-900\n \"--ui-surface-muted\": \"#1e293b\", // slate-800\n \"--ui-overlay\": \"rgb(2 6 23 / 0.6)\", // slate-950 / 60%\n \"--ui-focus-ring\": \"0 0 0 3px rgb(99 102 241 / 0.45)\",\n};\n\n/**\n * Historically a shared `:host { --ui-x: var(--ui-x, fallback); } ` block,\n * re-declaring every token on each component's own host element as a\n * \"materialize the inherited value, or this fallback\" trick. That pattern\n * turned out to be a genuine bug, not a working default: browsers treat\n * `--ui-x: var(--ui-x, ...)` on `:host` as a self-referencing declaration\n * that computes to the guaranteed-invalid value REGARDLESS of what an\n * ancestor (e.g. `:root`) set — the host's own `:host` rule wins the cascade\n * over the ancestor's, so this dropped whatever the app actually set and\n * silently substituted the component's own fallback everywhere. It went\n * unnoticed because every component's fallback happened to equal the\n * intended light-mode value — dark mode (a real, different value at\n * `:root`) exposed it immediately (colors rendering as if no theme were\n * applied at all). Fix: don't redeclare tokens on `:host` — plain\n * inheritance already carries `:root`'s values into every shadow root\n * correctly, and every component already has its own `var(--ui-x, fallback)`\n * at each point of use, so nothing here is needed for the \"zero external\n * CSS\" default to keep working. Kept as an empty export so the existing\n * `static override styles = [tokens, css\\`...\\`]` import in every component\n * keeps compiling.\n */\nexport const tokens = css``;\n"]}
1
+ {"version":3,"file":"tokens.js","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAE1B;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,WAAW,GAA2B;IACjD,SAAS;IACT,cAAc,EAAE,SAAS,EAAE,aAAa;IACxC,oBAAoB,EAAE,SAAS,EAAE,aAAa;IAC9C,WAAW,EAAE,SAAS,EAAE,UAAU;IAClC,iBAAiB,EAAE,SAAS,EAAE,UAAU;IACxC,aAAa,EAAE,SAAS,EAAE,UAAU;IACpC,mBAAmB,EAAE,SAAS,EAAE,UAAU;IAC1C,cAAc,EAAE,SAAS,EAAE,YAAY;IACvC,cAAc,EAAE,SAAS,EAAE,YAAY;IACvC,oBAAoB,EAAE,SAAS,EAAE,YAAY;IAC7C,gBAAgB,EAAE,SAAS;IAC3B,WAAW,EAAE,SAAS,EAAE,YAAY;IACpC,iBAAiB,EAAE,SAAS,EAAE,YAAY;IAC1C,aAAa,EAAE,SAAS,EAAE,YAAY;IACtC,cAAc,EAAE,SAAS,EAAE,QAAQ;IACnC,oBAAoB,EAAE,SAAS,EAAE,WAAW;IAC5C,cAAc,EAAE,SAAS,EAAE,YAAY;IACvC,oBAAoB,EAAE,yBAAyB;IAC/C,cAAc,EAAE,sBAAsB,EAAE,kBAAkB;IAE1D,gBAAgB;IAChB,aAAa,EAAE,QAAQ,EAAE,aAAa;IACtC,gBAAgB,EAAE,SAAS,EAAE,UAAU;IACvC,aAAa,EACX,kEAAkE,EAAE,YAAY;IAClF,gBAAgB,EACd,qEAAqE,EAAE,YAAY;IACrF,iBAAiB,EAAE,iCAAiC;IAEpD,OAAO;IACP,WAAW,EACT,oHAAoH;IACtH,gBAAgB,EACd,kEAAkE;IACpE,gBAAgB,EAAE,UAAU;IAC5B,mBAAmB,EAAE,SAAS;IAC9B,mBAAmB,EAAE,WAAW;CACjC,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,eAAe,GAA2B;IACrD,cAAc,EAAE,SAAS,EAAE,aAAa;IACxC,oBAAoB,EAAE,SAAS,EAAE,aAAa;IAC9C,WAAW,EAAE,SAAS,EAAE,UAAU;IAClC,iBAAiB,EAAE,SAAS,EAAE,UAAU;IACxC,aAAa,EAAE,SAAS,EAAE,UAAU;IACpC,mBAAmB,EAAE,SAAS,EAAE,UAAU;IAC1C,cAAc,EAAE,SAAS,EAAE,YAAY;IACvC,cAAc,EAAE,SAAS,EAAE,YAAY;IACvC,oBAAoB,EAAE,SAAS,EAAE,YAAY;IAC7C,gBAAgB,EAAE,SAAS;IAC3B,WAAW,EAAE,SAAS,EAAE,YAAY;IACpC,iBAAiB,EAAE,SAAS,EAAE,YAAY;IAC1C,aAAa,EAAE,SAAS,EAAE,YAAY;IACtC,cAAc,EAAE,SAAS,EAAE,YAAY;IACvC,oBAAoB,EAAE,SAAS,EAAE,YAAY;IAC7C,cAAc,EAAE,SAAS,EAAE,YAAY;IACvC,oBAAoB,EAAE,yBAAyB;IAC/C,cAAc,EAAE,mBAAmB,EAAE,kBAAkB;IACvD,iBAAiB,EAAE,kCAAkC;CACtD,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG,GAAG,CAAA,EAAE,CAAC","sourcesContent":["import { css } from \"lit\";\n\n/**\n * Design token values, sourced from Tailwind v4's default theme palette.\n * Each entry maps a `--ui-*` custom property name to its default value.\n * These are the same values baked in as `var(--ui-*, <fallback>)` fallbacks\n * throughout component styles, so components render correctly with zero\n * external CSS. Consumers may override any of these by setting the custom\n * property on `:root` or an ancestor element (see `tokens.css`).\n */\nexport const tokenValues: Record<string, string> = {\n // Colors\n \"--ui-primary\": \"#4f46e5\", // indigo-600\n \"--ui-primary-hover\": \"#4338ca\", // indigo-700\n \"--ui-info\": \"#0ea5e9\", // sky-500\n \"--ui-info-hover\": \"#0284c7\", // sky-600\n \"--ui-danger\": \"#dc2626\", // red-600\n \"--ui-danger-hover\": \"#b91c1c\", // red-700\n \"--ui-success\": \"#16a34a\", // green-600\n \"--ui-warning\": \"#d97706\", // amber-600\n \"--ui-warning-hover\": \"#b45309\", // amber-700\n \"--ui-on-accent\": \"#ffffff\",\n \"--ui-text\": \"#0f172a\", // slate-900\n \"--ui-text-muted\": \"#64748b\", // slate-500\n \"--ui-border\": \"#e2e8f0\", // slate-200\n \"--ui-surface\": \"#ffffff\", // white\n \"--ui-surface-muted\": \"#f8fafc\", // slate-50\n \"--ui-tooltip\": \"#0f172a\", // slate-900\n \"--ui-hover-overlay\": \"rgb(255 255 255 / 0.32)\",\n \"--ui-overlay\": \"rgb(15 23 42 / 0.45)\", // slate-900 / 45%\n\n // Shape / depth\n \"--ui-radius\": \"0.5rem\", // rounded-lg\n \"--ui-radius-sm\": \"0.25rem\", // rounded\n \"--ui-shadow\":\n \"0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)\", // shadow-md\n \"--ui-shadow-lg\":\n \"0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)\", // shadow-xl\n \"--ui-focus-ring\": \"0 0 0 3px rgb(79 70 229 / 0.35)\",\n\n // Type\n \"--ui-font\":\n 'ui-sans-serif, system-ui, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\"',\n \"--ui-font-mono\":\n \"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace\",\n \"--ui-font-size\": \"0.875rem\",\n \"--ui-font-size-sm\": \"0.75rem\",\n \"--ui-font-size-xs\": \"0.6875rem\",\n};\n\n/**\n * Dark-mode overrides for the same `--ui-*` custom properties, applied by\n * `tokens.css` via `@media (prefers-color-scheme: dark)` and `[data-theme]`\n * (see generate-tokens-css.mjs). Same Tailwind slate scale as the light\n * palette, inverted, with primary/danger/success lightened one step (500\n * instead of 600) for sufficient contrast against dark surfaces. Shape/type\n * tokens are theme-independent and not repeated here.\n */\nexport const darkTokenValues: Record<string, string> = {\n \"--ui-primary\": \"#6366f1\", // indigo-500\n \"--ui-primary-hover\": \"#818cf8\", // indigo-400\n \"--ui-info\": \"#38bdf8\", // sky-400\n \"--ui-info-hover\": \"#7dd3fc\", // sky-300\n \"--ui-danger\": \"#ef4444\", // red-500\n \"--ui-danger-hover\": \"#f87171\", // red-400\n \"--ui-success\": \"#22c55e\", // green-500\n \"--ui-warning\": \"#f59e0b\", // amber-500\n \"--ui-warning-hover\": \"#fbbf24\", // amber-400\n \"--ui-on-accent\": \"#ffffff\",\n \"--ui-text\": \"#f1f5f9\", // slate-100\n \"--ui-text-muted\": \"#94a3b8\", // slate-400\n \"--ui-border\": \"#334155\", // slate-700\n \"--ui-surface\": \"#0f172a\", // slate-900\n \"--ui-surface-muted\": \"#1e293b\", // slate-800\n \"--ui-tooltip\": \"#020617\", // slate-950\n \"--ui-hover-overlay\": \"rgb(255 255 255 / 0.12)\",\n \"--ui-overlay\": \"rgb(2 6 23 / 0.6)\", // slate-950 / 60%\n \"--ui-focus-ring\": \"0 0 0 3px rgb(99 102 241 / 0.45)\",\n};\n\n/**\n * Historically a shared `:host { --ui-x: var(--ui-x, fallback); } ` block,\n * re-declaring every token on each component's own host element as a\n * \"materialize the inherited value, or this fallback\" trick. That pattern\n * turned out to be a genuine bug, not a working default: browsers treat\n * `--ui-x: var(--ui-x, ...)` on `:host` as a self-referencing declaration\n * that computes to the guaranteed-invalid value REGARDLESS of what an\n * ancestor (e.g. `:root`) set — the host's own `:host` rule wins the cascade\n * over the ancestor's, so this dropped whatever the app actually set and\n * silently substituted the component's own fallback everywhere. It went\n * unnoticed because every component's fallback happened to equal the\n * intended light-mode value — dark mode (a real, different value at\n * `:root`) exposed it immediately (colors rendering as if no theme were\n * applied at all). Fix: don't redeclare tokens on `:host` — plain\n * inheritance already carries `:root`'s values into every shadow root\n * correctly, and every component already has its own `var(--ui-x, fallback)`\n * at each point of use, so nothing here is needed for the \"zero external\n * CSS\" default to keep working. Kept as an empty export so the existing\n * `static override styles = [tokens, css\\`...\\`]` import in every component\n * keeps compiling.\n */\nexport const tokens = css``;\n"]}
@@ -34,6 +34,8 @@ export declare class UiButton extends LitElement {
34
34
  busy: boolean;
35
35
  /** Drives submit/reset on the ancestor form via ElementInternals, since a shadow-DOM button can't do it natively. */
36
36
  private _onClick;
37
+ /** Suppresses navigation while a link-styled button is disabled or busy. */
38
+ private _onLinkClick;
37
39
  render(): import("lit-html").TemplateResult<1>;
38
40
  }
39
41
  declare global {
@@ -1 +1 @@
1
- {"version":3,"file":"ui-button.d.ts","sourceRoot":"","sources":["../src/ui-button.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAa,MAAM,KAAK,CAAC;AAK5C,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,CAAC;AAE/D;;;;;;;;;;;;;;;;;GAiBG;AACH,qBACa,QAAS,SAAQ,UAAU;;IACtC,MAAM,CAAC,cAAc,UAAQ;IAG7B,OAAgB,MAAM,4BA6DpB;IAEF,qBAAqB;IACT,OAAO,EAAE,aAAa,CAAa;IAC/C,oEAAoE;IACxD,IAAI,EAAE,MAAM,GAAG,IAAI,CAAQ;IACvC,wDAAwD;IAC5C,IAAI,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAY;IAC3D,wCAAwC;IACX,QAAQ,UAAS;IAC9C,0EAA0E;IAC7C,IAAI,UAAS;IAE1C,qHAAqH;IACrH,OAAO,CAAC,QAAQ;IAKP,MAAM,yCAmBd;CACF;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,WAAW,EAAE,QAAQ,CAAC;KACvB;CACF"}
1
+ {"version":3,"file":"ui-button.d.ts","sourceRoot":"","sources":["../src/ui-button.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAa,MAAM,KAAK,CAAC;AAK5C,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,CAAC;AAE/D;;;;;;;;;;;;;;;;;GAiBG;AACH,qBACa,QAAS,SAAQ,UAAU;;IACtC,MAAM,CAAC,cAAc,UAAQ;IAG7B,OAAgB,MAAM,4BA4FpB;IAEF,qBAAqB;IACT,OAAO,EAAE,aAAa,CAAa;IAC/C,oEAAoE;IACxD,IAAI,EAAE,MAAM,GAAG,IAAI,CAAQ;IACvC,wDAAwD;IAC5C,IAAI,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAY;IAC3D,wCAAwC;IACX,QAAQ,UAAS;IAC9C,0EAA0E;IAC7C,IAAI,UAAS;IAE1C,qHAAqH;IACrH,OAAO,CAAC,QAAQ;IAKhB,4EAA4E;IAC5E,OAAO,CAAC,YAAY;IAKX,MAAM,yCA+Bd;CACF;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,WAAW,EAAE,QAAQ,CAAC;KACvB;CACF"}
package/dist/ui-button.js CHANGED
@@ -52,12 +52,21 @@ let UiButton = class UiButton extends LitElement {
52
52
  .btn {
53
53
  display: inline-flex;
54
54
  align-items: center;
55
- gap: 0.35rem;
55
+ gap: 0.25rem;
56
56
  border-radius: var(--ui-radius-sm, 0.25rem);
57
- padding: 0.5rem 0.9rem;
57
+ padding: 0.5rem 1rem;
58
58
  font-size: var(--ui-font-size-sm, 0.75rem);
59
59
  font-weight: 500;
60
- font-family: var(--ui-font, ui-sans-serif, system-ui, sans-serif);
60
+ font-family: var(
61
+ --ui-font,
62
+ ui-sans-serif,
63
+ system-ui,
64
+ sans-serif,
65
+ "Apple Color Emoji",
66
+ "Segoe UI Emoji",
67
+ "Segoe UI Symbol",
68
+ "Noto Color Emoji"
69
+ );
61
70
  cursor: pointer;
62
71
  border: 1px solid transparent;
63
72
  text-decoration: none;
@@ -65,7 +74,7 @@ let UiButton = class UiButton extends LitElement {
65
74
  }
66
75
  .btn.primary {
67
76
  background: var(--ui-primary, #4f46e5);
68
- color: #fff;
77
+ color: var(--ui-on-accent, #ffffff);
69
78
  }
70
79
  .btn.primary:hover:not(:disabled) {
71
80
  background: var(--ui-primary-hover, #4338ca);
@@ -80,7 +89,7 @@ let UiButton = class UiButton extends LitElement {
80
89
  }
81
90
  .btn.danger {
82
91
  background: var(--ui-danger, #dc2626);
83
- color: #fff;
92
+ color: var(--ui-on-accent, #ffffff);
84
93
  }
85
94
  .btn.danger:hover:not(:disabled) {
86
95
  background: var(--ui-danger-hover, #b91c1c);
@@ -91,6 +100,10 @@ let UiButton = class UiButton extends LitElement {
91
100
  cursor: default;
92
101
  pointer-events: none;
93
102
  }
103
+ .btn:focus-visible {
104
+ outline: none;
105
+ box-shadow: var(--ui-focus-ring, 0 0 0 3px rgb(79 70 229 / 0.35));
106
+ }
94
107
  .spin {
95
108
  display: inline-flex;
96
109
  animation: spin 0.8s linear infinite;
@@ -103,6 +116,24 @@ let UiButton = class UiButton extends LitElement {
103
116
  transform: rotate(360deg);
104
117
  }
105
118
  }
119
+ @media (prefers-reduced-motion: reduce) {
120
+ .spin {
121
+ animation: none;
122
+ }
123
+ }
124
+ @media (forced-colors: active) {
125
+ .btn:focus-visible {
126
+ outline: 2px solid CanvasText;
127
+ outline-offset: 2px;
128
+ box-shadow: none;
129
+ }
130
+ .btn:disabled,
131
+ .btn[aria-disabled="true"] {
132
+ color: GrayText;
133
+ border-color: GrayText;
134
+ opacity: 1;
135
+ }
136
+ }
106
137
  `,
107
138
  ]; }
108
139
  /** Drives submit/reset on the ancestor form via ElementInternals, since a shadow-DOM button can't do it natively. */
@@ -112,21 +143,39 @@ let UiButton = class UiButton extends LitElement {
112
143
  else if (this.type === "reset")
113
144
  this.#internals.form?.reset();
114
145
  }
146
+ /** Suppresses navigation while a link-styled button is disabled or busy. */
147
+ _onLinkClick(e) {
148
+ if (!this.disabled && !this.busy)
149
+ return;
150
+ e.preventDefault();
151
+ }
115
152
  render() {
116
153
  const classes = `btn ${this.variant}`;
117
154
  const isDisabled = this.disabled || this.busy;
118
155
  if (this.href) {
119
156
  return html `
120
- <a class=${classes} href=${this.href} aria-disabled=${isDisabled ? "true" : "false"}>
121
- <span class="spin" ?hidden=${!this.busy}>${iconArrowPath(14)}</span>
157
+ <a
158
+ class=${classes}
159
+ href=${this.href}
160
+ aria-disabled=${isDisabled ? "true" : "false"}
161
+ aria-busy=${this.busy ? "true" : "false"}
162
+ @click=${this._onLinkClick}
163
+ >
164
+ <span class="spin" aria-hidden="true" ?hidden=${!this.busy}>${iconArrowPath(14)}</span>
122
165
  <slot name="icon" ?hidden=${this.busy}></slot>
123
166
  <slot></slot>
124
167
  </a>
125
168
  `;
126
169
  }
127
170
  return html `
128
- <button class=${classes} type="button" ?disabled=${isDisabled} @click=${this._onClick}>
129
- <span class="spin" ?hidden=${!this.busy}>${iconArrowPath(14)}</span>
171
+ <button
172
+ class=${classes}
173
+ type="button"
174
+ ?disabled=${isDisabled}
175
+ aria-busy=${this.busy ? "true" : "false"}
176
+ @click=${this._onClick}
177
+ >
178
+ <span class="spin" aria-hidden="true" ?hidden=${!this.busy}>${iconArrowPath(14)}</span>
130
179
  <slot name="icon" ?hidden=${this.busy}></slot>
131
180
  <slot></slot>
132
181
  </button>
@@ -1 +1 @@
1
- {"version":3,"file":"ui-button.js","sourceRoot":"","sources":["../src/ui-button.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAIrC;;;;;;;;;;;;;;;;;GAiBG;AAEI,IAAM,QAAQ,GAAd,MAAM,QAAS,SAAQ,UAAU;IAAjC;;QAGL,eAAU,GAAG,IAAI,CAAC,eAAe,EAAE,CAAC;QAgEpC,qBAAqB;QACT,YAAO,GAAkB,SAAS,CAAC;QAC/C,oEAAoE;QACxD,SAAI,GAAkB,IAAI,CAAC;QACvC,wDAAwD;QAC5C,SAAI,GAAkC,QAAQ,CAAC;QAC3D,wCAAwC;QACX,aAAQ,GAAG,KAAK,CAAC;QAC9C,0EAA0E;QAC7C,SAAI,GAAG,KAAK,CAAC;IA4B5C,CAAC;aAvGQ,mBAAc,GAAG,IAAI,AAAP,CAAQ;IAE7B,UAAU,CAA0B;aACpB,WAAM,GAAG;QACvB,MAAM;QACN,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA0DF;KACF,AA7DqB,CA6DpB;IAaF,qHAAqH;IAC7G,QAAQ;QACd,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ;YAAE,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,aAAa,EAAE,CAAC;aAC7D,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO;YAAE,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC;IAChE,CAAC;IAEQ,MAAM;QACb,MAAM,OAAO,GAAG,OAAO,IAAI,CAAC,OAAO,EAAE,CAAC;QACtC,MAAM,UAAU,GAAG,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,IAAI,CAAC;QAC9C,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YACd,OAAO,IAAI,CAAA;mBACE,OAAO,SAAS,IAAI,CAAC,IAAI,kBAAkB,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO;uCACpD,CAAC,IAAI,CAAC,IAAI,IAAI,aAAa,CAAC,EAAE,CAAC;sCAChC,IAAI,CAAC,IAAI;;;OAGxC,CAAC;QACJ,CAAC;QACD,OAAO,IAAI,CAAA;sBACO,OAAO,4BAA4B,UAAU,WAAW,IAAI,CAAC,QAAQ;qCACtD,CAAC,IAAI,CAAC,IAAI,IAAI,aAAa,CAAC,EAAE,CAAC;oCAChC,IAAI,CAAC,IAAI;;;KAGxC,CAAC;IACJ,CAAC;CACF,CAAA;AApCa;IAAX,QAAQ,EAAE;yCAAoC;AAEnC;IAAX,QAAQ,EAAE;sCAA4B;AAE3B;IAAX,QAAQ,EAAE;sCAAgD;AAE9B;IAA5B,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;0CAAkB;AAEjB;IAA5B,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;sCAAc;AA5E/B,QAAQ;IADpB,aAAa,CAAC,WAAW,CAAC;GACd,QAAQ,CAwGpB","sourcesContent":["import { LitElement, css, html } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport { iconArrowPath } from \"./icons.js\";\nimport { tokens } from \"./tokens.js\";\n\nexport type ButtonVariant = \"primary\" | \"secondary\" | \"danger\";\n\n/**\n * Button (or link styled as one) with an optional leading icon, in three\n * visual weights. Set `href` to render an `<a>` instead of a `<button>` —\n * same styling either way — for cross-page navigation that should look like\n * an action button; a disabled/busy link stays a real `<a>` with\n * `aria-disabled` + `pointer-events: none` rather than losing its href.\n * Put the icon in the `icon` slot and the label in the default slot.\n *\n * Form-associated (`type=\"submit\"`/`\"reset\"`): the actual `<button>` lives in\n * this element's shadow root, which native HTML form association does not\n * cross into from an ancestor light-DOM `<form>`. `type=\"submit\"`/`\"reset\"`\n * is instead wired through `ElementInternals.form` — the same mechanism\n * `address-autocomplete` uses to associate with an ancestor form.\n *\n * @element ui-button\n * @slot icon - Optional leading icon (e.g. an inline SVG).\n * @slot - Button label.\n */\n@customElement(\"ui-button\")\nexport class UiButton extends LitElement {\n static formAssociated = true;\n\n #internals = this.attachInternals();\n static override styles = [\n tokens,\n css`\n :host {\n display: inline-flex;\n }\n .btn {\n display: inline-flex;\n align-items: center;\n gap: 0.35rem;\n border-radius: var(--ui-radius-sm, 0.25rem);\n padding: 0.5rem 0.9rem;\n font-size: var(--ui-font-size-sm, 0.75rem);\n font-weight: 500;\n font-family: var(--ui-font, ui-sans-serif, system-ui, sans-serif);\n cursor: pointer;\n border: 1px solid transparent;\n text-decoration: none;\n box-sizing: border-box;\n }\n .btn.primary {\n background: var(--ui-primary, #4f46e5);\n color: #fff;\n }\n .btn.primary:hover:not(:disabled) {\n background: var(--ui-primary-hover, #4338ca);\n }\n .btn.secondary {\n background: none;\n border-color: var(--ui-border, #e2e8f0);\n color: var(--ui-text, #0f172a);\n }\n .btn.secondary:hover:not(:disabled) {\n border-color: var(--ui-text-muted, #64748b);\n }\n .btn.danger {\n background: var(--ui-danger, #dc2626);\n color: #fff;\n }\n .btn.danger:hover:not(:disabled) {\n background: var(--ui-danger-hover, #b91c1c);\n }\n .btn:disabled,\n .btn[aria-disabled=\"true\"] {\n opacity: 0.6;\n cursor: default;\n pointer-events: none;\n }\n .spin {\n display: inline-flex;\n animation: spin 0.8s linear infinite;\n }\n .spin[hidden] {\n display: none;\n }\n @keyframes spin {\n to {\n transform: rotate(360deg);\n }\n }\n `,\n ];\n\n /** Visual weight. */\n @property() variant: ButtonVariant = \"primary\";\n /** Renders an `<a href=\"...\">` instead of a `<button>` when set. */\n @property() href: string | null = null;\n /** Native button `type`. Ignored when `href` is set. */\n @property() type: \"button\" | \"submit\" | \"reset\" = \"button\";\n /** Disables the control and dims it. */\n @property({ type: Boolean }) disabled = false;\n /** Shows a spinner in place of the icon slot and disables the control. */\n @property({ type: Boolean }) busy = false;\n\n /** Drives submit/reset on the ancestor form via ElementInternals, since a shadow-DOM button can't do it natively. */\n private _onClick() {\n if (this.type === \"submit\") this.#internals.form?.requestSubmit();\n else if (this.type === \"reset\") this.#internals.form?.reset();\n }\n\n override render() {\n const classes = `btn ${this.variant}`;\n const isDisabled = this.disabled || this.busy;\n if (this.href) {\n return html`\n <a class=${classes} href=${this.href} aria-disabled=${isDisabled ? \"true\" : \"false\"}>\n <span class=\"spin\" ?hidden=${!this.busy}>${iconArrowPath(14)}</span>\n <slot name=\"icon\" ?hidden=${this.busy}></slot>\n <slot></slot>\n </a>\n `;\n }\n return html`\n <button class=${classes} type=\"button\" ?disabled=${isDisabled} @click=${this._onClick}>\n <span class=\"spin\" ?hidden=${!this.busy}>${iconArrowPath(14)}</span>\n <slot name=\"icon\" ?hidden=${this.busy}></slot>\n <slot></slot>\n </button>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ui-button\": UiButton;\n }\n}\n"]}
1
+ {"version":3,"file":"ui-button.js","sourceRoot":"","sources":["../src/ui-button.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAIrC;;;;;;;;;;;;;;;;;GAiBG;AAEI,IAAM,QAAQ,GAAd,MAAM,QAAS,SAAQ,UAAU;IAAjC;;QAGL,eAAU,GAAG,IAAI,CAAC,eAAe,EAAE,CAAC;QA+FpC,qBAAqB;QACT,YAAO,GAAkB,SAAS,CAAC;QAC/C,oEAAoE;QACxD,SAAI,GAAkB,IAAI,CAAC;QACvC,wDAAwD;QAC5C,SAAI,GAAkC,QAAQ,CAAC;QAC3D,wCAAwC;QACX,aAAQ,GAAG,KAAK,CAAC;QAC9C,0EAA0E;QAC7C,SAAI,GAAG,KAAK,CAAC;IA8C5C,CAAC;aAxJQ,mBAAc,GAAG,IAAI,AAAP,CAAQ;IAE7B,UAAU,CAA0B;aACpB,WAAM,GAAG;QACvB,MAAM;QACN,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAyFF;KACF,AA5FqB,CA4FpB;IAaF,qHAAqH;IAC7G,QAAQ;QACd,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ;YAAE,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,aAAa,EAAE,CAAC;aAC7D,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO;YAAE,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC;IAChE,CAAC;IAED,4EAA4E;IACpE,YAAY,CAAC,CAAa;QAChC,IAAI,CAAC,IAAI,CAAC,QAAQ,IAAI,CAAC,IAAI,CAAC,IAAI;YAAE,OAAO;QACzC,CAAC,CAAC,cAAc,EAAE,CAAC;IACrB,CAAC;IAEQ,MAAM;QACb,MAAM,OAAO,GAAG,OAAO,IAAI,CAAC,OAAO,EAAE,CAAC;QACtC,MAAM,UAAU,GAAG,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,IAAI,CAAC;QAC9C,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YACd,OAAO,IAAI,CAAA;;kBAEC,OAAO;iBACR,IAAI,CAAC,IAAI;0BACA,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO;sBACjC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO;mBAC/B,IAAI,CAAC,YAAY;;0DAEsB,CAAC,IAAI,CAAC,IAAI,IAAI,aAAa,CAAC,EAAE,CAAC;sCACnD,IAAI,CAAC,IAAI;;;OAGxC,CAAC;QACJ,CAAC;QACD,OAAO,IAAI,CAAA;;gBAEC,OAAO;;oBAEH,UAAU;oBACV,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO;iBAC/B,IAAI,CAAC,QAAQ;;wDAE0B,CAAC,IAAI,CAAC,IAAI,IAAI,aAAa,CAAC,EAAE,CAAC;oCACnD,IAAI,CAAC,IAAI;;;KAGxC,CAAC;IACJ,CAAC;CACF,CAAA;AAtDa;IAAX,QAAQ,EAAE;yCAAoC;AAEnC;IAAX,QAAQ,EAAE;sCAA4B;AAE3B;IAAX,QAAQ,EAAE;sCAAgD;AAE9B;IAA5B,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;0CAAkB;AAEjB;IAA5B,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;sCAAc;AA3G/B,QAAQ;IADpB,aAAa,CAAC,WAAW,CAAC;GACd,QAAQ,CAyJpB","sourcesContent":["import { LitElement, css, html } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport { iconArrowPath } from \"./icons.js\";\nimport { tokens } from \"./tokens.js\";\n\nexport type ButtonVariant = \"primary\" | \"secondary\" | \"danger\";\n\n/**\n * Button (or link styled as one) with an optional leading icon, in three\n * visual weights. Set `href` to render an `<a>` instead of a `<button>` —\n * same styling either way — for cross-page navigation that should look like\n * an action button; a disabled/busy link stays a real `<a>` with\n * `aria-disabled` + `pointer-events: none` rather than losing its href.\n * Put the icon in the `icon` slot and the label in the default slot.\n *\n * Form-associated (`type=\"submit\"`/`\"reset\"`): the actual `<button>` lives in\n * this element's shadow root, which native HTML form association does not\n * cross into from an ancestor light-DOM `<form>`. `type=\"submit\"`/`\"reset\"`\n * is instead wired through `ElementInternals.form` — the same mechanism\n * `address-autocomplete` uses to associate with an ancestor form.\n *\n * @element ui-button\n * @slot icon - Optional leading icon (e.g. an inline SVG).\n * @slot - Button label.\n */\n@customElement(\"ui-button\")\nexport class UiButton extends LitElement {\n static formAssociated = true;\n\n #internals = this.attachInternals();\n static override styles = [\n tokens,\n css`\n :host {\n display: inline-flex;\n }\n .btn {\n display: inline-flex;\n align-items: center;\n gap: 0.25rem;\n border-radius: var(--ui-radius-sm, 0.25rem);\n padding: 0.5rem 1rem;\n font-size: var(--ui-font-size-sm, 0.75rem);\n font-weight: 500;\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 cursor: pointer;\n border: 1px solid transparent;\n text-decoration: none;\n box-sizing: border-box;\n }\n .btn.primary {\n background: var(--ui-primary, #4f46e5);\n color: var(--ui-on-accent, #ffffff);\n }\n .btn.primary:hover:not(:disabled) {\n background: var(--ui-primary-hover, #4338ca);\n }\n .btn.secondary {\n background: none;\n border-color: var(--ui-border, #e2e8f0);\n color: var(--ui-text, #0f172a);\n }\n .btn.secondary:hover:not(:disabled) {\n border-color: var(--ui-text-muted, #64748b);\n }\n .btn.danger {\n background: var(--ui-danger, #dc2626);\n color: var(--ui-on-accent, #ffffff);\n }\n .btn.danger:hover:not(:disabled) {\n background: var(--ui-danger-hover, #b91c1c);\n }\n .btn:disabled,\n .btn[aria-disabled=\"true\"] {\n opacity: 0.6;\n cursor: default;\n pointer-events: none;\n }\n .btn:focus-visible {\n outline: none;\n box-shadow: var(--ui-focus-ring, 0 0 0 3px rgb(79 70 229 / 0.35));\n }\n .spin {\n display: inline-flex;\n animation: spin 0.8s linear infinite;\n }\n .spin[hidden] {\n display: none;\n }\n @keyframes spin {\n to {\n transform: rotate(360deg);\n }\n }\n @media (prefers-reduced-motion: reduce) {\n .spin {\n animation: none;\n }\n }\n @media (forced-colors: active) {\n .btn:focus-visible {\n outline: 2px solid CanvasText;\n outline-offset: 2px;\n box-shadow: none;\n }\n .btn:disabled,\n .btn[aria-disabled=\"true\"] {\n color: GrayText;\n border-color: GrayText;\n opacity: 1;\n }\n }\n `,\n ];\n\n /** Visual weight. */\n @property() variant: ButtonVariant = \"primary\";\n /** Renders an `<a href=\"...\">` instead of a `<button>` when set. */\n @property() href: string | null = null;\n /** Native button `type`. Ignored when `href` is set. */\n @property() type: \"button\" | \"submit\" | \"reset\" = \"button\";\n /** Disables the control and dims it. */\n @property({ type: Boolean }) disabled = false;\n /** Shows a spinner in place of the icon slot and disables the control. */\n @property({ type: Boolean }) busy = false;\n\n /** Drives submit/reset on the ancestor form via ElementInternals, since a shadow-DOM button can't do it natively. */\n private _onClick() {\n if (this.type === \"submit\") this.#internals.form?.requestSubmit();\n else if (this.type === \"reset\") this.#internals.form?.reset();\n }\n\n /** Suppresses navigation while a link-styled button is disabled or busy. */\n private _onLinkClick(e: MouseEvent) {\n if (!this.disabled && !this.busy) return;\n e.preventDefault();\n }\n\n override render() {\n const classes = `btn ${this.variant}`;\n const isDisabled = this.disabled || this.busy;\n if (this.href) {\n return html`\n <a\n class=${classes}\n href=${this.href}\n aria-disabled=${isDisabled ? \"true\" : \"false\"}\n aria-busy=${this.busy ? \"true\" : \"false\"}\n @click=${this._onLinkClick}\n >\n <span class=\"spin\" aria-hidden=\"true\" ?hidden=${!this.busy}>${iconArrowPath(14)}</span>\n <slot name=\"icon\" ?hidden=${this.busy}></slot>\n <slot></slot>\n </a>\n `;\n }\n return html`\n <button\n class=${classes}\n type=\"button\"\n ?disabled=${isDisabled}\n aria-busy=${this.busy ? \"true\" : \"false\"}\n @click=${this._onClick}\n >\n <span class=\"spin\" aria-hidden=\"true\" ?hidden=${!this.busy}>${iconArrowPath(14)}</span>\n <slot name=\"icon\" ?hidden=${this.busy}></slot>\n <slot></slot>\n </button>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ui-button\": UiButton;\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"user-avatar.d.ts","sourceRoot":"","sources":["../src/user-avatar.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAa,MAAM,KAAK,CAAC;AAK5C;;;;;;;;GAQG;AACH,qBACa,UAAW,SAAQ,UAAU;IACxC,OAAgB,MAAM,4BAyBpB;IAEF,mFAAmF;IACvE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAQ;IACtC,2GAA2G;IAC/F,IAAI,EAAE,MAAM,GAAG,IAAI,CAAQ;IACvC,qFAAqF;IACzE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAM;IAE9B,OAAO,CAAC,SAAS,CAAS;IAEnC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAKlC;IAEF,gFAAgF;IAChF,OAAO,CAAC,WAAW;IAMnB,UAAmB,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,QAE1D;IAEQ,MAAM,yCAiBd;CACF;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,aAAa,EAAE,UAAU,CAAC;KAC3B;CACF"}
1
+ {"version":3,"file":"user-avatar.d.ts","sourceRoot":"","sources":["../src/user-avatar.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAa,MAAM,KAAK,CAAC;AAK5C;;;;;;;;GAQG;AACH,qBACa,UAAW,SAAQ,UAAU;IACxC,OAAgB,MAAM,4BAwCpB;IAEF,mFAAmF;IACvE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAQ;IACtC,2GAA2G;IAC/F,IAAI,EAAE,MAAM,GAAG,IAAI,CAAQ;IACvC,qFAAqF;IACzE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAM;IAE9B,OAAO,CAAC,SAAS,CAAS;IAEnC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAKlC;IAEF,gFAAgF;IAChF,OAAO,CAAC,WAAW;IAMnB,UAAmB,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,QAE1D;IAEQ,MAAM,yCAiBd;CACF;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,aAAa,EAAE,UAAU,CAAC;KAC3B;CACF"}
@@ -38,12 +38,27 @@ let UserAvatar = class UserAvatar extends LitElement {
38
38
  }
39
39
  .avatar {
40
40
  border-radius: 50%;
41
- background: var(--ui-primary, #4f46e5);
42
- color: #fff;
41
+ /* Match map-circle's 30% white/black vertical depth without tinting images. */
42
+ background: linear-gradient(
43
+ to bottom,
44
+ color-mix(in srgb, var(--ui-primary, #4f46e5) 70%, #ffffff) 0%,
45
+ color-mix(in srgb, var(--ui-primary, #4f46e5) 70%, #000000) 100%
46
+ );
47
+ color: var(--ui-on-accent, #ffffff);
48
+ text-shadow: 0 1px 2px rgb(0 0 0 / 0.35);
43
49
  display: flex;
44
50
  align-items: center;
45
51
  justify-content: center;
46
- font-family: var(--ui-font, ui-sans-serif, system-ui, sans-serif);
52
+ font-family: var(
53
+ --ui-font,
54
+ ui-sans-serif,
55
+ system-ui,
56
+ sans-serif,
57
+ "Apple Color Emoji",
58
+ "Segoe UI Emoji",
59
+ "Segoe UI Symbol",
60
+ "Noto Color Emoji"
61
+ );
47
62
  font-weight: 600;
48
63
  overflow: hidden;
49
64
  flex: 0 0 auto;
@@ -1 +1 @@
1
- {"version":3,"file":"user-avatar.js","sourceRoot":"","sources":["../src/user-avatar.ts"],"names":[],"mappings":";;;;;;;AAAA,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AACnE,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC;;;;;;;;GAQG;AAEI,IAAM,UAAU,GAAhB,MAAM,UAAW,SAAQ,UAAU;IAAnC;;QA4BL,mFAAmF;QACvE,QAAG,GAAkB,IAAI,CAAC;QACtC,2GAA2G;QAC/F,SAAI,GAAkB,IAAI,CAAC;QACvC,qFAAqF;QACzE,SAAI,GAAoB,EAAE,CAAC;QAEtB,cAAS,GAAG,KAAK,CAAC;IAsCrC,CAAC;;aAxEiB,WAAM,GAAG;QACvB,MAAM;QACN,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;KAsBF;KACF,AAzBqB,CAyBpB;aAWsB,iBAAY,GAA2B;QAC7D,EAAE,EAAE,EAAE;QACN,EAAE,EAAE,EAAE;QACN,EAAE,EAAE,EAAE;QACN,EAAE,EAAE,EAAE;KACP,AALmC,CAKlC;IAEF,gFAAgF;IACxE,WAAW;QACjB,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAClC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC;YAAE,OAAO,OAAO,CAAC;QAC3C,OAAO,aAAW,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;IAC1D,CAAC;IAEkB,UAAU,CAAC,OAA6B;QACzD,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC;YAAE,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC;IACjD,CAAC;IAEQ,MAAM;QACb,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;QAChC,MAAM,GAAG,GAAG,GAAG,IAAI,IAAI,CAAC;QACxB,MAAM,SAAS,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC;QAChD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC;QACpD,OAAO,IAAI,CAAA;0CAC2B,GAAG,WAAW,GAAG,cAAc,IAAI,GAAG,IAAI;UAC1E,SAAS;YACT,CAAC,CAAC,IAAI,CAAA;oBACI,IAAI,CAAC,GAAI;;;uBAGN,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;eACrC;YACL,CAAC,CAAC,OAAO,IAAI,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;;KAEzD,CAAC;IACJ,CAAC;CACF,CAAA;AA5Ca;IAAX,QAAQ,EAAE;uCAA2B;AAE1B;IAAX,QAAQ,EAAE;wCAA4B;AAE3B;IAAX,QAAQ,EAAE;wCAA4B;AAEtB;IAAhB,KAAK,EAAE;6CAA2B;AAnCxB,UAAU;IADtB,aAAa,CAAC,aAAa,CAAC;GAChB,UAAU,CAyEtB","sourcesContent":["import { LitElement, css, html } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\nimport { iconUserCircle } from \"./icons.js\";\nimport { tokens } from \"./tokens.js\";\n\n/**\n * Circular avatar. Shows `src` when it loads successfully; falls back to the\n * first letter of `name` (uppercased) if `src` is unset or fails to load\n * (e.g. an expired OAuth profile-photo URL); falls back further to a generic\n * person icon if `name` is also unset. A broken image never leaves a blank\n * circle.\n *\n * @element user-avatar\n */\n@customElement(\"user-avatar\")\nexport class UserAvatar extends LitElement {\n static override styles = [\n tokens,\n css`\n :host {\n display: inline-flex;\n }\n .avatar {\n border-radius: 50%;\n background: var(--ui-primary, #4f46e5);\n color: #fff;\n display: flex;\n align-items: center;\n justify-content: center;\n font-family: var(--ui-font, ui-sans-serif, system-ui, sans-serif);\n font-weight: 600;\n overflow: hidden;\n flex: 0 0 auto;\n }\n img {\n width: 100%;\n height: 100%;\n border-radius: 50%;\n object-fit: cover;\n }\n `,\n ];\n\n /** Image URL to show. Falls back to initials/icon if unset or it fails to load. */\n @property() src: string | null = null;\n /** Source string for the fallback initial (e.g. a display name or email) — first character, uppercased. */\n @property() name: string | null = null;\n /** Diameter in CSS pixels, or a preset name (`xs`=18, `sm`=24, `md`=32, `lg`=48). */\n @property() size: string | number = 32;\n\n @state() private _imgError = false;\n\n private static readonly SIZE_PRESETS: Record<string, number> = {\n xs: 18,\n sm: 24,\n md: 32,\n lg: 48,\n };\n\n /** Resolves the `size` property (number or preset name) to a pixel diameter. */\n private resolveSize(): number {\n const numeric = Number(this.size);\n if (!Number.isNaN(numeric)) return numeric;\n return UserAvatar.SIZE_PRESETS[String(this.size)] ?? 32;\n }\n\n protected override willUpdate(changed: Map<string, unknown>) {\n if (changed.has(\"src\")) this._imgError = false;\n }\n\n override render() {\n const size = this.resolveSize();\n const dim = `${size}px`;\n const showImage = !!this.src && !this._imgError;\n const initial = this.name?.trim()[0]?.toUpperCase();\n return html`\n <span class=\"avatar\" style=\"width:${dim};height:${dim};font-size:${size * 0.42}px\">\n ${showImage\n ? html`<img\n src=${this.src!}\n alt=\"\"\n referrerpolicy=\"no-referrer\"\n @error=${() => (this._imgError = true)}\n />`\n : initial || iconUserCircle(Math.round(size * 0.65))}\n </span>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"user-avatar\": UserAvatar;\n }\n}\n"]}
1
+ {"version":3,"file":"user-avatar.js","sourceRoot":"","sources":["../src/user-avatar.ts"],"names":[],"mappings":";;;;;;;AAAA,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AACnE,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC;;;;;;;;GAQG;AAEI,IAAM,UAAU,GAAhB,MAAM,UAAW,SAAQ,UAAU;IAAnC;;QA2CL,mFAAmF;QACvE,QAAG,GAAkB,IAAI,CAAC;QACtC,2GAA2G;QAC/F,SAAI,GAAkB,IAAI,CAAC;QACvC,qFAAqF;QACzE,SAAI,GAAoB,EAAE,CAAC;QAEtB,cAAS,GAAG,KAAK,CAAC;IAsCrC,CAAC;;aAvFiB,WAAM,GAAG;QACvB,MAAM;QACN,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAqCF;KACF,AAxCqB,CAwCpB;aAWsB,iBAAY,GAA2B;QAC7D,EAAE,EAAE,EAAE;QACN,EAAE,EAAE,EAAE;QACN,EAAE,EAAE,EAAE;QACN,EAAE,EAAE,EAAE;KACP,AALmC,CAKlC;IAEF,gFAAgF;IACxE,WAAW;QACjB,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAClC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC;YAAE,OAAO,OAAO,CAAC;QAC3C,OAAO,aAAW,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;IAC1D,CAAC;IAEkB,UAAU,CAAC,OAA6B;QACzD,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC;YAAE,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC;IACjD,CAAC;IAEQ,MAAM;QACb,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;QAChC,MAAM,GAAG,GAAG,GAAG,IAAI,IAAI,CAAC;QACxB,MAAM,SAAS,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC;QAChD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC;QACpD,OAAO,IAAI,CAAA;0CAC2B,GAAG,WAAW,GAAG,cAAc,IAAI,GAAG,IAAI;UAC1E,SAAS;YACT,CAAC,CAAC,IAAI,CAAA;oBACI,IAAI,CAAC,GAAI;;;uBAGN,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;eACrC;YACL,CAAC,CAAC,OAAO,IAAI,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;;KAEzD,CAAC;IACJ,CAAC;CACF,CAAA;AA5Ca;IAAX,QAAQ,EAAE;uCAA2B;AAE1B;IAAX,QAAQ,EAAE;wCAA4B;AAE3B;IAAX,QAAQ,EAAE;wCAA4B;AAEtB;IAAhB,KAAK,EAAE;6CAA2B;AAlDxB,UAAU;IADtB,aAAa,CAAC,aAAa,CAAC;GAChB,UAAU,CAwFtB","sourcesContent":["import { LitElement, css, html } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\nimport { iconUserCircle } from \"./icons.js\";\nimport { tokens } from \"./tokens.js\";\n\n/**\n * Circular avatar. Shows `src` when it loads successfully; falls back to the\n * first letter of `name` (uppercased) if `src` is unset or fails to load\n * (e.g. an expired OAuth profile-photo URL); falls back further to a generic\n * person icon if `name` is also unset. A broken image never leaves a blank\n * circle.\n *\n * @element user-avatar\n */\n@customElement(\"user-avatar\")\nexport class UserAvatar extends LitElement {\n static override styles = [\n tokens,\n css`\n :host {\n display: inline-flex;\n }\n .avatar {\n border-radius: 50%;\n /* Match map-circle's 30% white/black vertical depth without tinting images. */\n background: linear-gradient(\n to bottom,\n color-mix(in srgb, var(--ui-primary, #4f46e5) 70%, #ffffff) 0%,\n color-mix(in srgb, var(--ui-primary, #4f46e5) 70%, #000000) 100%\n );\n color: var(--ui-on-accent, #ffffff);\n text-shadow: 0 1px 2px rgb(0 0 0 / 0.35);\n display: flex;\n align-items: center;\n justify-content: center;\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 font-weight: 600;\n overflow: hidden;\n flex: 0 0 auto;\n }\n img {\n width: 100%;\n height: 100%;\n border-radius: 50%;\n object-fit: cover;\n }\n `,\n ];\n\n /** Image URL to show. Falls back to initials/icon if unset or it fails to load. */\n @property() src: string | null = null;\n /** Source string for the fallback initial (e.g. a display name or email) — first character, uppercased. */\n @property() name: string | null = null;\n /** Diameter in CSS pixels, or a preset name (`xs`=18, `sm`=24, `md`=32, `lg`=48). */\n @property() size: string | number = 32;\n\n @state() private _imgError = false;\n\n private static readonly SIZE_PRESETS: Record<string, number> = {\n xs: 18,\n sm: 24,\n md: 32,\n lg: 48,\n };\n\n /** Resolves the `size` property (number or preset name) to a pixel diameter. */\n private resolveSize(): number {\n const numeric = Number(this.size);\n if (!Number.isNaN(numeric)) return numeric;\n return UserAvatar.SIZE_PRESETS[String(this.size)] ?? 32;\n }\n\n protected override willUpdate(changed: Map<string, unknown>) {\n if (changed.has(\"src\")) this._imgError = false;\n }\n\n override render() {\n const size = this.resolveSize();\n const dim = `${size}px`;\n const showImage = !!this.src && !this._imgError;\n const initial = this.name?.trim()[0]?.toUpperCase();\n return html`\n <span class=\"avatar\" style=\"width:${dim};height:${dim};font-size:${size * 0.42}px\">\n ${showImage\n ? html`<img\n src=${this.src!}\n alt=\"\"\n referrerpolicy=\"no-referrer\"\n @error=${() => (this._imgError = true)}\n />`\n : initial || iconUserCircle(Math.round(size * 0.65))}\n </span>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"user-avatar\": UserAvatar;\n }\n}\n"]}
@@ -0,0 +1,77 @@
1
+ import type { StatusPillColor } from "../status-pill.js";
2
+ import type { CalendarEntry } from "../calendar-entry.js";
3
+ /** Plain data extracted from a `calendar-entry` element's properties. */
4
+ export interface CalendarEntryData {
5
+ start: string;
6
+ end: string;
7
+ label: string;
8
+ details?: string[];
9
+ footer?: string;
10
+ color: StatusPillColor;
11
+ href?: string;
12
+ }
13
+ /** A `CalendarEntryData` with its date strings resolved to local `Date`s. */
14
+ export interface ResolvedCalendarEntry extends CalendarEntryData {
15
+ startDate: Date;
16
+ endDate: Date;
17
+ }
18
+ /** A `ResolvedCalendarEntry` assigned a stacking column by `assignLanes`. */
19
+ export interface LanedEntry extends ResolvedCalendarEntry {
20
+ lane: number;
21
+ }
22
+ /** Attributes whose mutations can change a rendered calendar entry. */
23
+ export declare const CALENDAR_ENTRY_ATTRIBUTES: string[];
24
+ /** Number of days in `month` (1-12) of `year`, leap years included. */
25
+ export declare function daysInMonth(year: number, month: number): number;
26
+ /**
27
+ * Parses `"YYYY-MM-DD"` as a local-midnight `Date`. Returns `null` for
28
+ * anything that doesn't match or doesn't round-trip (e.g. "2026-02-30") —
29
+ * deliberately never uses `new Date(iso)`/`.toISOString()`, both of which
30
+ * are UTC-based and would silently shift the date in negative-UTC-offset
31
+ * timezones.
32
+ */
33
+ export declare function parseIsoDate(iso: string): Date | null;
34
+ /** Formats a `Date`'s local year/month/day as `"YYYY-MM-DD"`. */
35
+ export declare function toIsoDate(date: Date): string;
36
+ /** Locale-aware full month name for `month` (1-12). */
37
+ export declare function monthName(month: number): string;
38
+ /** Sunday-first weekday abbreviations, indexed by `Date.getDay()` (0 = Sunday). */
39
+ export declare const WEEKDAY_ABBR: readonly string[];
40
+ /**
41
+ * Whether `date` falls on a Saturday or Sunday, per JS's `Date.getDay()`
42
+ * convention (Sunday = 0 .. Saturday = 6) — note this differs from Python's
43
+ * `calendar` module (Monday = 0), which the original reference
44
+ * implementation this component's layout is based on used.
45
+ */
46
+ export declare function isWeekend(date: Date): boolean;
47
+ /** Reads a `calendar-entry` element's properties and named text slots into plain data. */
48
+ export declare function readCalendarEntryElement(el: CalendarEntry): CalendarEntryData;
49
+ /**
50
+ * Resolves a `CalendarEntryData`'s date strings to `Date`s. Returns `null`
51
+ * when `start` is missing or unparseable — such entries are dropped
52
+ * silently rather than thrown, since this reads declarative markup a
53
+ * consumer authored, not validated user input. A blank `end` defaults to
54
+ * `start` (single-day entry); an `end` before `start` is clamped up to
55
+ * `start` rather than treated as an error.
56
+ */
57
+ export declare function resolveEntry(data: CalendarEntryData): ResolvedCalendarEntry | null;
58
+ /** Whether `entry`'s inclusive date range intersects the inclusive `[rangeStart, rangeEnd]` range. */
59
+ export declare function overlapsRange(entry: ResolvedCalendarEntry, rangeStart: Date, rangeEnd: Date): boolean;
60
+ /**
61
+ * Assigns each entry a 0-based stacking lane via greedy interval
62
+ * partitioning ("meeting rooms II"): entries are sorted by start date, then
63
+ * each is placed in the lowest-numbered lane whose current occupant ends
64
+ * strictly before the entry starts. Because entry ranges are
65
+ * inclusive-inclusive, an entry ending on day X and one starting on day X
66
+ * both occupy day X and must not share a lane — hence the strict `<`.
67
+ *
68
+ * Lane assignment is meant to be computed independently per `calendar-month`
69
+ * (from only the entries overlapping that month); an entry spanning a month
70
+ * boundary may therefore land in a different lane in adjacent months. This
71
+ * is an accepted v1 limitation, not a bug.
72
+ */
73
+ export declare function assignLanes(entries: ResolvedCalendarEntry[]): {
74
+ entries: LanedEntry[];
75
+ laneCount: number;
76
+ };
77
+ //# sourceMappingURL=calendar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"calendar.d.ts","sourceRoot":"","sources":["../../src/utils/calendar.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAE1D,yEAAyE;AACzE,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,eAAe,CAAC;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,6EAA6E;AAC7E,MAAM,WAAW,qBAAsB,SAAQ,iBAAiB;IAC9D,SAAS,EAAE,IAAI,CAAC;IAChB,OAAO,EAAE,IAAI,CAAC;CACf;AAED,6EAA6E;AAC7E,MAAM,WAAW,UAAW,SAAQ,qBAAqB;IACvD,IAAI,EAAE,MAAM,CAAC;CACd;AAID,uEAAuE;AACvE,eAAO,MAAM,yBAAyB,UAAqD,CAAC;AAE5F,uEAAuE;AACvE,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAYrD;AAED,iEAAiE;AACjE,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAK5C;AAID,uDAAuD;AACvD,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED,mFAAmF;AACnF,eAAO,MAAM,YAAY,EAAE,SAAS,MAAM,EAAsD,CAAC;AAEjG;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAG7C;AAQD,0FAA0F;AAC1F,wBAAgB,wBAAwB,CAAC,EAAE,EAAE,aAAa,GAAG,iBAAiB,CAY7E;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,iBAAiB,GAAG,qBAAqB,GAAG,IAAI,CAWlF;AAED,sGAAsG;AACtG,wBAAgB,aAAa,CAAC,KAAK,EAAE,qBAAqB,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,GAAG,OAAO,CAErG;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,qBAAqB,EAAE,GAAG;IAC7D,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;CACnB,CAyBA"}
@@ -0,0 +1,135 @@
1
+ const ISO_DATE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
2
+ /** Attributes whose mutations can change a rendered calendar entry. */
3
+ export const CALENDAR_ENTRY_ATTRIBUTES = ["start", "end", "label", "color", "href", "slot"];
4
+ /** Number of days in `month` (1-12) of `year`, leap years included. */
5
+ export function daysInMonth(year, month) {
6
+ return new Date(year, month, 0).getDate();
7
+ }
8
+ /**
9
+ * Parses `"YYYY-MM-DD"` as a local-midnight `Date`. Returns `null` for
10
+ * anything that doesn't match or doesn't round-trip (e.g. "2026-02-30") —
11
+ * deliberately never uses `new Date(iso)`/`.toISOString()`, both of which
12
+ * are UTC-based and would silently shift the date in negative-UTC-offset
13
+ * timezones.
14
+ */
15
+ export function parseIsoDate(iso) {
16
+ const match = ISO_DATE_PATTERN.exec(iso);
17
+ if (!match)
18
+ return null;
19
+ const year = Number(match[1]);
20
+ const month = Number(match[2]);
21
+ const day = Number(match[3]);
22
+ const date = new Date(year, month - 1, day);
23
+ if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {
24
+ return null;
25
+ }
26
+ return date;
27
+ }
28
+ /** Formats a `Date`'s local year/month/day as `"YYYY-MM-DD"`. */
29
+ export function toIsoDate(date) {
30
+ const year = String(date.getFullYear()).padStart(4, "0");
31
+ const month = String(date.getMonth() + 1).padStart(2, "0");
32
+ const day = String(date.getDate()).padStart(2, "0");
33
+ return `${year}-${month}-${day}`;
34
+ }
35
+ const monthNameFormatter = new Intl.DateTimeFormat(undefined, { month: "long" });
36
+ /** Locale-aware full month name for `month` (1-12). */
37
+ export function monthName(month) {
38
+ return monthNameFormatter.format(new Date(2000, month - 1, 1));
39
+ }
40
+ /** Sunday-first weekday abbreviations, indexed by `Date.getDay()` (0 = Sunday). */
41
+ export const WEEKDAY_ABBR = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
42
+ /**
43
+ * Whether `date` falls on a Saturday or Sunday, per JS's `Date.getDay()`
44
+ * convention (Sunday = 0 .. Saturday = 6) — note this differs from Python's
45
+ * `calendar` module (Monday = 0), which the original reference
46
+ * implementation this component's layout is based on used.
47
+ */
48
+ export function isWeekend(date) {
49
+ const day = date.getDay();
50
+ return day === 0 || day === 6;
51
+ }
52
+ /** Reads normalized plain-text content from direct children assigned to a named slot. */
53
+ function slottedText(el, slotName) {
54
+ return Array.from(el.querySelectorAll(`:scope > [slot="${slotName}"]`))
55
+ .map((node) => (node.textContent ?? "").replace(/\s+/g, " ").trim());
56
+ }
57
+ /** Reads a `calendar-entry` element's properties and named text slots into plain data. */
58
+ export function readCalendarEntryElement(el) {
59
+ const title = slottedText(el, "title").find((line) => line.length > 0);
60
+ const footer = slottedText(el, "footer").find((line) => line.length > 0);
61
+ return {
62
+ start: el.start,
63
+ end: el.end,
64
+ label: title ?? el.label,
65
+ details: slottedText(el, "detail"),
66
+ footer,
67
+ color: el.color,
68
+ href: el.href,
69
+ };
70
+ }
71
+ /**
72
+ * Resolves a `CalendarEntryData`'s date strings to `Date`s. Returns `null`
73
+ * when `start` is missing or unparseable — such entries are dropped
74
+ * silently rather than thrown, since this reads declarative markup a
75
+ * consumer authored, not validated user input. A blank `end` defaults to
76
+ * `start` (single-day entry); an `end` before `start` is clamped up to
77
+ * `start` rather than treated as an error.
78
+ */
79
+ export function resolveEntry(data) {
80
+ const startDate = parseIsoDate(data.start);
81
+ if (!startDate)
82
+ return null;
83
+ const endDate = parseIsoDate(data.end) ?? startDate;
84
+ return {
85
+ ...data,
86
+ details: data.details ?? [],
87
+ startDate,
88
+ endDate: endDate < startDate ? startDate : endDate,
89
+ };
90
+ }
91
+ /** Whether `entry`'s inclusive date range intersects the inclusive `[rangeStart, rangeEnd]` range. */
92
+ export function overlapsRange(entry, rangeStart, rangeEnd) {
93
+ return entry.startDate <= rangeEnd && entry.endDate >= rangeStart;
94
+ }
95
+ /**
96
+ * Assigns each entry a 0-based stacking lane via greedy interval
97
+ * partitioning ("meeting rooms II"): entries are sorted by start date, then
98
+ * each is placed in the lowest-numbered lane whose current occupant ends
99
+ * strictly before the entry starts. Because entry ranges are
100
+ * inclusive-inclusive, an entry ending on day X and one starting on day X
101
+ * both occupy day X and must not share a lane — hence the strict `<`.
102
+ *
103
+ * Lane assignment is meant to be computed independently per `calendar-month`
104
+ * (from only the entries overlapping that month); an entry spanning a month
105
+ * boundary may therefore land in a different lane in adjacent months. This
106
+ * is an accepted v1 limitation, not a bug.
107
+ */
108
+ export function assignLanes(entries) {
109
+ const sorted = entries
110
+ .map((entry, index) => ({ entry, index }))
111
+ .sort((a, b) => {
112
+ const startDiff = a.entry.startDate.getTime() - b.entry.startDate.getTime();
113
+ if (startDiff !== 0)
114
+ return startDiff;
115
+ const endDiff = a.entry.endDate.getTime() - b.entry.endDate.getTime();
116
+ if (endDiff !== 0)
117
+ return endDiff;
118
+ return a.index - b.index;
119
+ });
120
+ const laneEnds = [];
121
+ const laned = [];
122
+ for (const { entry } of sorted) {
123
+ let lane = laneEnds.findIndex((end) => end < entry.startDate);
124
+ if (lane === -1) {
125
+ lane = laneEnds.length;
126
+ laneEnds.push(entry.endDate);
127
+ }
128
+ else {
129
+ laneEnds[lane] = entry.endDate;
130
+ }
131
+ laned.push({ ...entry, lane });
132
+ }
133
+ return { entries: laned, laneCount: laneEnds.length };
134
+ }
135
+ //# sourceMappingURL=calendar.js.map