@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.
- package/README.md +6 -0
- package/custom-elements.json +891 -22
- package/dist/address-autocomplete.d.ts +8 -0
- package/dist/address-autocomplete.d.ts.map +1 -1
- package/dist/address-autocomplete.js +165 -15
- package/dist/address-autocomplete.js.map +1 -1
- package/dist/animate-confetti.d.ts +8 -5
- package/dist/animate-confetti.d.ts.map +1 -1
- package/dist/animate-confetti.js +46 -19
- package/dist/animate-confetti.js.map +1 -1
- package/dist/autocomplete-input.d.ts +8 -0
- package/dist/autocomplete-input.d.ts.map +1 -1
- package/dist/autocomplete-input.js +165 -15
- package/dist/autocomplete-input.js.map +1 -1
- package/dist/calendar-month.d.ts.map +1 -1
- package/dist/calendar-month.js +27 -13
- package/dist/calendar-month.js.map +1 -1
- package/dist/calendar-year.d.ts.map +1 -1
- package/dist/calendar-year.js +11 -2
- package/dist/calendar-year.js.map +1 -1
- package/dist/chat-message.d.ts.map +1 -1
- package/dist/chat-message.js +77 -24
- package/dist/chat-message.js.map +1 -1
- package/dist/confirm-dialog.d.ts +8 -1
- package/dist/confirm-dialog.d.ts.map +1 -1
- package/dist/confirm-dialog.js +130 -18
- package/dist/confirm-dialog.js.map +1 -1
- package/dist/copy-link-button.d.ts +2 -0
- package/dist/copy-link-button.d.ts.map +1 -1
- package/dist/copy-link-button.js +35 -3
- package/dist/copy-link-button.js.map +1 -1
- package/dist/data-table.d.ts +2 -0
- package/dist/data-table.d.ts.map +1 -1
- package/dist/data-table.js +83 -11
- package/dist/data-table.js.map +1 -1
- package/dist/distance-value.d.ts +0 -1
- package/dist/distance-value.d.ts.map +1 -1
- package/dist/distance-value.js +3 -8
- package/dist/distance-value.js.map +1 -1
- package/dist/distribution-chart.d.ts.map +1 -1
- package/dist/distribution-chart.js +54 -17
- package/dist/distribution-chart.js.map +1 -1
- package/dist/dropdown-button.d.ts.map +1 -1
- package/dist/dropdown-button.js +60 -11
- package/dist/dropdown-button.js.map +1 -1
- package/dist/editable-text.d.ts.map +1 -1
- package/dist/editable-text.js +46 -13
- package/dist/editable-text.js.map +1 -1
- package/dist/form-select.d.ts.map +1 -1
- package/dist/form-select.js +70 -22
- package/dist/form-select.js.map +1 -1
- package/dist/frame-box.d.ts.map +1 -1
- package/dist/frame-box.js +20 -12
- package/dist/frame-box.js.map +1 -1
- package/dist/icon-button.d.ts.map +1 -1
- package/dist/icon-button.js +11 -0
- package/dist/icon-button.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/kbd-hint.d.ts +30 -0
- package/dist/kbd-hint.d.ts.map +1 -0
- package/dist/kbd-hint.js +184 -0
- package/dist/kbd-hint.js.map +1 -0
- package/dist/live-timer.d.ts +0 -1
- package/dist/live-timer.d.ts.map +1 -1
- package/dist/live-timer.js +1 -6
- package/dist/live-timer.js.map +1 -1
- package/dist/map-circle.d.ts.map +1 -1
- package/dist/map-circle.js +16 -1
- package/dist/map-circle.js.map +1 -1
- package/dist/map-pin.d.ts.map +1 -1
- package/dist/map-pin.js +16 -1
- package/dist/map-pin.js.map +1 -1
- package/dist/percent-bar-chart.d.ts +3 -0
- package/dist/percent-bar-chart.d.ts.map +1 -1
- package/dist/percent-bar-chart.js +60 -4
- package/dist/percent-bar-chart.js.map +1 -1
- package/dist/photo-gallery.d.ts.map +1 -1
- package/dist/photo-gallery.js +12 -3
- package/dist/photo-gallery.js.map +1 -1
- package/dist/popover-panel.d.ts.map +1 -1
- package/dist/popover-panel.js +132 -23
- package/dist/popover-panel.js.map +1 -1
- package/dist/price-history-chart.d.ts.map +1 -1
- package/dist/price-history-chart.js +52 -18
- package/dist/price-history-chart.js.map +1 -1
- package/dist/radio-cards.d.ts +2 -0
- package/dist/radio-cards.d.ts.map +1 -1
- package/dist/radio-cards.js +51 -5
- package/dist/radio-cards.js.map +1 -1
- package/dist/radio-pills.d.ts +2 -0
- package/dist/radio-pills.d.ts.map +1 -1
- package/dist/radio-pills.js +46 -4
- package/dist/radio-pills.js.map +1 -1
- package/dist/relative-time.d.ts +0 -1
- package/dist/relative-time.d.ts.map +1 -1
- package/dist/relative-time.js +1 -6
- package/dist/relative-time.js.map +1 -1
- package/dist/reveal-button.d.ts +2 -0
- package/dist/reveal-button.d.ts.map +1 -1
- package/dist/reveal-button.js +42 -5
- package/dist/reveal-button.js.map +1 -1
- package/dist/roman-numeral.js +1 -1
- package/dist/roman-numeral.js.map +1 -1
- package/dist/slide-panel.d.ts +7 -3
- package/dist/slide-panel.d.ts.map +1 -1
- package/dist/slide-panel.js +95 -23
- package/dist/slide-panel.js.map +1 -1
- package/dist/stat-meter.d.ts.map +1 -1
- package/dist/stat-meter.js +17 -7
- package/dist/stat-meter.js.map +1 -1
- package/dist/status-pill.d.ts.map +1 -1
- package/dist/status-pill.js +26 -12
- package/dist/status-pill.js.map +1 -1
- package/dist/tile-grid.d.ts +8 -0
- package/dist/tile-grid.d.ts.map +1 -1
- package/dist/tile-grid.js +88 -12
- package/dist/tile-grid.js.map +1 -1
- package/dist/toast-notification.d.ts.map +1 -1
- package/dist/toast-notification.js +40 -11
- package/dist/toast-notification.js.map +1 -1
- package/dist/tokens.css +19 -0
- package/dist/tokens.d.ts.map +1 -1
- package/dist/tokens.js +13 -0
- package/dist/tokens.js.map +1 -1
- package/dist/ui-button.d.ts +2 -0
- package/dist/ui-button.d.ts.map +1 -1
- package/dist/ui-button.js +58 -9
- package/dist/ui-button.js.map +1 -1
- package/dist/user-avatar.d.ts.map +1 -1
- package/dist/user-avatar.js +18 -3
- package/dist/user-avatar.js.map +1 -1
- package/dist/utils/form.d.ts +3 -0
- package/dist/utils/form.d.ts.map +1 -0
- package/dist/utils/form.js +14 -0
- package/dist/utils/form.js.map +1 -0
- package/dist/utils/layer-stack.d.ts +9 -0
- package/dist/utils/layer-stack.d.ts.map +1 -0
- package/dist/utils/layer-stack.js +35 -0
- package/dist/utils/layer-stack.js.map +1 -0
- package/dist/weight-bar-chart.d.ts.map +1 -1
- package/dist/weight-bar-chart.js +27 -8
- package/dist/weight-bar-chart.js.map +1 -1
- package/docs/address-autocomplete.md +5 -0
- package/docs/autocomplete-input.md +5 -0
- package/docs/calendar-month.md +2 -0
- package/docs/chat-message.md +4 -0
- package/docs/confirm-dialog.md +2 -0
- package/docs/copy-link-button.md +2 -0
- package/docs/data-table.md +2 -0
- package/docs/design-language.md +149 -0
- package/docs/distribution-chart.md +4 -0
- package/docs/dropdown-button.md +1 -0
- package/docs/frame-box.md +2 -0
- package/docs/kbd-hint.md +42 -0
- package/docs/percent-bar-chart.md +4 -1
- package/docs/photo-gallery.md +1 -0
- package/docs/popover-panel.md +2 -0
- package/docs/price-history-chart.md +8 -1
- package/docs/radio-cards.md +2 -0
- package/docs/radio-pills.md +2 -0
- package/docs/reveal-button.md +3 -0
- package/docs/slide-panel.md +4 -2
- package/docs/status-pill.md +3 -0
- package/docs/tile-grid.md +10 -1
- package/docs/toast-notification.md +4 -1
- package/docs/ui-button.md +2 -0
- package/docs/user-avatar.md +1 -0
- package/llms.txt +59 -34
- package/package.json +5 -1
package/dist/distance-value.js
CHANGED
|
@@ -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,
|
|
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,
|
|
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,
|
|
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:
|
|
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(
|
|
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:
|
|
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:
|
|
116
|
+
padding: 0.5rem 0;
|
|
93
117
|
}
|
|
94
118
|
.legend {
|
|
95
119
|
display: flex;
|
|
96
120
|
flex-wrap: wrap;
|
|
97
|
-
gap:
|
|
98
|
-
padding:
|
|
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:
|
|
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:
|
|
110
|
-
height:
|
|
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
|
|
204
|
-
<text font-size=${fs}
|
|
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
|
|
219
|
-
<stop
|
|
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}
|
|
228
|
-
<text font-size=${fs}
|
|
229
|
-
<text font-size=${fs}
|
|
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;
|
|
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"}
|
package/dist/dropdown-button.js
CHANGED
|
@@ -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(
|
|
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.
|
|
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: #
|
|
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
|
|
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
|
|
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:
|
|
86
|
-
padding:
|
|
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, #
|
|
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.
|
|
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,
|
|
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"}
|
package/dist/editable-text.js
CHANGED
|
@@ -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.
|
|
49
|
-
margin: -0.
|
|
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
|
|
72
|
+
.display:disabled {
|
|
61
73
|
cursor: inherit;
|
|
74
|
+
opacity: 0.6;
|
|
62
75
|
}
|
|
63
|
-
.display
|
|
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, #
|
|
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.
|
|
77
|
-
margin: -0.
|
|
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 `<
|
|
192
|
-
|
|
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([
|