@studious-creative/yumekit 0.1.0

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.
@@ -0,0 +1,317 @@
1
+ class YumeProgress extends HTMLElement {
2
+ static get observedAttributes() {
3
+ return [
4
+ "value",
5
+ "min",
6
+ "max",
7
+ "step",
8
+ "size",
9
+ "color",
10
+ "label-display",
11
+ "label-format",
12
+ "indeterminate",
13
+ "disabled",
14
+ ];
15
+ }
16
+
17
+ constructor() {
18
+ super();
19
+ this.attachShadow({ mode: "open" });
20
+ }
21
+
22
+ connectedCallback() {
23
+ if (!this.hasAttribute("size")) this.setAttribute("size", "medium");
24
+ if (!this.hasAttribute("min")) this.setAttribute("min", "0");
25
+ if (!this.hasAttribute("max")) this.setAttribute("max", "100");
26
+ this.render();
27
+ }
28
+
29
+ attributeChangedCallback(name, oldValue, newValue) {
30
+ if (oldValue !== newValue) {
31
+ this.render();
32
+ }
33
+ }
34
+
35
+ get value() {
36
+ const val = parseFloat(this.getAttribute("value"));
37
+ return Number.isNaN(val) ? null : val;
38
+ }
39
+
40
+ set value(val) {
41
+ if (val === null || val === undefined) {
42
+ this.removeAttribute("value");
43
+ } else {
44
+ this.setAttribute("value", String(val));
45
+ }
46
+ }
47
+
48
+ get min() {
49
+ return parseFloat(this.getAttribute("min")) || 0;
50
+ }
51
+
52
+ set min(val) {
53
+ this.setAttribute("min", String(val));
54
+ }
55
+
56
+ get max() {
57
+ return parseFloat(this.getAttribute("max")) || 100;
58
+ }
59
+
60
+ set max(val) {
61
+ this.setAttribute("max", String(val));
62
+ }
63
+
64
+ get step() {
65
+ const s = parseFloat(this.getAttribute("step"));
66
+ return Number.isNaN(s) || s <= 0 ? null : s;
67
+ }
68
+
69
+ set step(val) {
70
+ if (val === null || val === undefined) {
71
+ this.removeAttribute("step");
72
+ } else {
73
+ this.setAttribute("step", String(val));
74
+ }
75
+ }
76
+
77
+ get size() {
78
+ return this.getAttribute("size") || "medium";
79
+ }
80
+
81
+ set size(val) {
82
+ this.setAttribute("size", val);
83
+ }
84
+
85
+ get color() {
86
+ return this.getAttribute("color") || "primary";
87
+ }
88
+
89
+ set color(val) {
90
+ this.setAttribute("color", val);
91
+ }
92
+
93
+ get labelDisplay() {
94
+ return this.getAttribute("label-display") !== "false";
95
+ }
96
+
97
+ set labelDisplay(val) {
98
+ this.setAttribute("label-display", val ? "true" : "false");
99
+ }
100
+
101
+ get labelFormat() {
102
+ return this.getAttribute("label-format") || "percent";
103
+ }
104
+
105
+ set labelFormat(val) {
106
+ this.setAttribute("label-format", val);
107
+ }
108
+
109
+ get indeterminate() {
110
+ return this.hasAttribute("indeterminate");
111
+ }
112
+
113
+ set indeterminate(val) {
114
+ if (val) this.setAttribute("indeterminate", "");
115
+ else this.removeAttribute("indeterminate");
116
+ }
117
+
118
+ get disabled() {
119
+ return this.hasAttribute("disabled");
120
+ }
121
+
122
+ set disabled(val) {
123
+ if (val) this.setAttribute("disabled", "");
124
+ else this.removeAttribute("disabled");
125
+ }
126
+
127
+ /**
128
+ * Increment the progress value by the step amount (or 1 if no step).
129
+ */
130
+ increment() {
131
+ if (this.value === null) return;
132
+ const s = this.step || 1;
133
+ this.value = Math.min(this.value + s, this.max);
134
+ }
135
+
136
+ /**
137
+ * Decrement the progress value by the step amount (or 1 if no step).
138
+ */
139
+ decrement() {
140
+ if (this.value === null) return;
141
+ const s = this.step || 1;
142
+ this.value = Math.max(this.value - s, this.min);
143
+ }
144
+
145
+ get percentage() {
146
+ if (this.value === null) return 0;
147
+ const range = this.max - this.min;
148
+ if (range <= 0) return 0;
149
+ let pct = ((this.value - this.min) / range) * 100;
150
+ if (this.step) {
151
+ const stepPct = (this.step / range) * 100;
152
+ pct = Math.round(pct / stepPct) * stepPct;
153
+ }
154
+ return Math.max(0, Math.min(100, pct));
155
+ }
156
+
157
+ getBarColor(color) {
158
+ const colorMap = {
159
+ primary: "var(--primary-content--)",
160
+ secondary: "var(--secondary-content--)",
161
+ base: "var(--base-content--)",
162
+ success: "var(--success-content--)",
163
+ warning: "var(--warning-content--)",
164
+ error: "var(--error-content--)",
165
+ help: "var(--help-content--)",
166
+ };
167
+ return colorMap[color] || color;
168
+ }
169
+
170
+ getSizeVar(size) {
171
+ const map = {
172
+ small: "var(--component-progress-size-small)",
173
+ medium: "var(--component-progress-size-medium)",
174
+ large: "var(--component-progress-size-large)",
175
+ };
176
+ return map[size] || map.medium;
177
+ }
178
+
179
+ getLabel() {
180
+ if (this.indeterminate) return "";
181
+ if (this.value === null) return "";
182
+
183
+ switch (this.labelFormat) {
184
+ case "value":
185
+ return `${this.value} / ${this.max}`;
186
+ case "fraction":
187
+ return `${this.value - this.min} / ${this.max - this.min}`;
188
+ case "percent":
189
+ default:
190
+ return `${Math.round(this.percentage)}%`;
191
+ }
192
+ }
193
+
194
+ render() {
195
+ const isIndeterminate = this.indeterminate;
196
+ const pct = this.percentage;
197
+ const barColor = this.getBarColor(this.color);
198
+ const sizeVar = this.getSizeVar(this.size);
199
+ const isDisabled = this.disabled;
200
+ const showLabel = this.labelDisplay && !isIndeterminate;
201
+ const labelText = this.getLabel();
202
+ const ariaValue = this.value !== null ? this.value : undefined;
203
+
204
+ this.shadowRoot.innerHTML = `
205
+ <style>
206
+ :host {
207
+ display: block;
208
+ font-family: var(--font-family-body);
209
+ color: var(--base-content--);
210
+ opacity: ${isDisabled ? "0.5" : "1"};
211
+ pointer-events: ${isDisabled ? "none" : "auto"};
212
+ }
213
+
214
+ .progress-wrapper {
215
+ display: flex;
216
+ flex-direction: column;
217
+ gap: var(--spacing-2x-small, 4px);
218
+ }
219
+
220
+ .progress-header {
221
+ display: flex;
222
+ align-items: center;
223
+ justify-content: space-between;
224
+ font-size: var(--font-size-label, 0.83em);
225
+ }
226
+
227
+ .label {
228
+ color: var(--base-content--);
229
+ }
230
+
231
+ .track {
232
+ position: relative;
233
+ width: 100%;
234
+ height: ${sizeVar};
235
+ background: var(--base-background-component);
236
+ border: var(--component-progress-border-width) solid var(--base-background-border);
237
+ border-radius: var(--component-progress-border-radius-outer);
238
+ overflow: hidden;
239
+ box-sizing: border-box;
240
+ padding: var(--component-progress-padding);
241
+ }
242
+
243
+ .bar {
244
+ position: relative;
245
+ height: 100%;
246
+ background: ${barColor};
247
+ border-radius: var(--component-progress-border-radius-inner);
248
+ width: ${isIndeterminate ? "30%" : `${pct}%`};
249
+ transition: ${isIndeterminate ? "none" : "width 0.3s ease"};
250
+ overflow: hidden;
251
+ ${isIndeterminate ? "animation: indeterminate 1.5s ease-in-out infinite;" : ""}
252
+ }
253
+
254
+ .value-label {
255
+ position: absolute;
256
+ top: 0;
257
+ left: 0;
258
+ right: 0;
259
+ bottom: 0;
260
+ display: flex;
261
+ align-items: center;
262
+ justify-content: center;
263
+ font-size: var(--font-size-small, 0.75em);
264
+ font-variant-numeric: tabular-nums;
265
+ white-space: nowrap;
266
+ pointer-events: none;
267
+ font-weight: 600;
268
+ }
269
+
270
+ .value-label--track {
271
+ color: ${barColor};
272
+ }
273
+
274
+ .value-label--bar {
275
+ color: var(--base-background-component);
276
+ width: calc(100% / (${pct || 1} / 100));
277
+ }
278
+
279
+ @keyframes indeterminate {
280
+ 0% {
281
+ transform: translateX(0%);
282
+ }
283
+ 50% {
284
+ transform: translateX(233%);
285
+ }
286
+ 100% {
287
+ transform: translateX(0%);
288
+ }
289
+ }
290
+ </style>
291
+
292
+ <div class="progress-wrapper">
293
+ <div class="progress-header">
294
+ <span class="label"><slot></slot></span>
295
+ </div>
296
+ <div
297
+ class="track"
298
+ part="track"
299
+ role="progressbar"
300
+ aria-valuenow="${ariaValue !== undefined ? ariaValue : ""}"
301
+ aria-valuemin="${this.min}"
302
+ aria-valuemax="${this.max}"
303
+ ${isIndeterminate ? 'aria-busy="true"' : ""}
304
+ >
305
+ ${showLabel ? `<span class="value-label value-label--track" part="value-label">${labelText}</span>` : ""}
306
+ <div class="bar" part="bar">${showLabel ? `<span class="value-label value-label--bar">${labelText}</span>` : ""}</div>
307
+ </div>
308
+ </div>
309
+ `;
310
+ }
311
+ }
312
+
313
+ if (!customElements.get("y-progress")) {
314
+ customElements.define("y-progress", YumeProgress);
315
+ }
316
+
317
+ export { YumeProgress };
@@ -0,0 +1,202 @@
1
+ class YumeRadio extends HTMLElement {
2
+ static formAssociated = true;
3
+
4
+ static get observedAttributes() {
5
+ return ["options", "name", "value", "disabled"];
6
+ }
7
+
8
+ constructor() {
9
+ super();
10
+ this._internals = this.attachInternals();
11
+ this.attachShadow({ mode: "open" });
12
+ this._value = "";
13
+ }
14
+
15
+ connectedCallback() {
16
+ this.render();
17
+ }
18
+
19
+ attributeChangedCallback(name, oldVal, newVal) {
20
+ if (oldVal !== newVal) {
21
+ if (name === "value") {
22
+ this._value = newVal;
23
+ this._internals.setFormValue(newVal, this.name);
24
+ this.updateChecked();
25
+ } else if (["options", "name", "disabled"].includes(name)) {
26
+ this.render();
27
+ }
28
+ }
29
+ }
30
+
31
+ get value() {
32
+ return this._value;
33
+ }
34
+
35
+ set value(val) {
36
+ this._value = val;
37
+ this.setAttribute("value", val);
38
+ this._internals.setFormValue(val, this.name);
39
+ this.updateChecked();
40
+ }
41
+
42
+ get name() {
43
+ return this.getAttribute("name") || "";
44
+ }
45
+
46
+ get options() {
47
+ try {
48
+ return JSON.parse(this.getAttribute("options") || "[]");
49
+ } catch {
50
+ return [];
51
+ }
52
+ }
53
+
54
+ set options(val) {
55
+ this.setAttribute("options", JSON.stringify(val));
56
+ }
57
+
58
+ updateChecked() {
59
+ const radios = this.shadowRoot.querySelectorAll("input[type=radio]");
60
+ radios.forEach((input, i) => {
61
+ const isSelected = input.value === this.value;
62
+ input.checked = isSelected;
63
+ input.setAttribute("aria-checked", isSelected);
64
+ input.setAttribute("tabindex", isSelected ? "0" : "-1");
65
+ });
66
+ }
67
+
68
+ handleKey(e, index, radios) {
69
+ const len = radios.length;
70
+ let newIndex = index;
71
+
72
+ if (e.key === "ArrowDown" || e.key === "ArrowRight") {
73
+ e.preventDefault();
74
+ newIndex = (index + 1) % len;
75
+ } else if (e.key === "ArrowUp" || e.key === "ArrowLeft") {
76
+ e.preventDefault();
77
+ newIndex = (index - 1 + len) % len;
78
+ } else if (e.key === " " || e.key === "Enter") {
79
+ e.preventDefault();
80
+ this.value = radios[index].value;
81
+ this.dispatchEvent(
82
+ new CustomEvent("change", {
83
+ detail: { value: this.value },
84
+ bubbles: true,
85
+ composed: true,
86
+ }),
87
+ );
88
+ return;
89
+ } else {
90
+ return;
91
+ }
92
+
93
+ // Shift focus only (no selection change)
94
+ radios[newIndex].focus();
95
+ }
96
+
97
+ render() {
98
+ const name = this.name;
99
+ const disabled = this.hasAttribute("disabled");
100
+ const value = this.value;
101
+ const options = this.options;
102
+
103
+ const style = `
104
+ :host {
105
+ display: block;
106
+ font-family: var(--font-family-body);
107
+ }
108
+ fieldset {
109
+ border: none;
110
+ padding: 0;
111
+ margin: 0;
112
+ display: flex;
113
+ flex-direction: column;
114
+ gap: var(--spacing-x-small, 8px);
115
+ }
116
+ label {
117
+ display: flex;
118
+ align-items: center;
119
+ gap: 0.5em;
120
+ cursor: pointer;
121
+ }
122
+ input[type="radio"] {
123
+ appearance: none;
124
+ width: var(--component-radio-size, 16px);
125
+ height: var(--component-radio-size, 16px);
126
+ border: var(--component-inputs-border-width, 2px) solid var(--component-radio-color);
127
+ border-radius: 50%;
128
+ position: relative;
129
+ outline: none;
130
+ cursor: pointer;
131
+ }
132
+ input[type="radio"]:checked::after {
133
+ content: '';
134
+ position: absolute;
135
+ top: 50%;
136
+ left: 50%;
137
+ width: var(--component-radio-dot-size, 8px);
138
+ height: var(--component-radio-dot-size, 8px);
139
+ background: var(--component-radio-color);
140
+ border-radius: 50%;
141
+ transform: translate(-50%, -50%);
142
+ }
143
+ input[type="radio"]:focus-visible {
144
+ outline: 2px solid var(--component-radio-accent);
145
+ outline-offset: 2px;
146
+ }
147
+ input[disabled] {
148
+ opacity: 0.5;
149
+ cursor: not-allowed;
150
+ }
151
+ `;
152
+
153
+ this.shadowRoot.innerHTML = `
154
+ <style>${style}</style>
155
+ <fieldset role="radiogroup">
156
+ ${options
157
+ .map(
158
+ (opt, idx) => `
159
+ <label>
160
+ <input
161
+ type="radio"
162
+ name="${name}"
163
+ value="${opt.value}"
164
+ ${disabled ? "disabled" : ""}
165
+ ${value === opt.value ? "checked" : ""}
166
+ tabindex="${value ? (value === opt.value ? "0" : "-1") : idx === 0 ? "0" : "-1"}"
167
+ role="radio"
168
+ aria-checked="${value === opt.value}"
169
+ />
170
+ ${opt.label}
171
+ </label>
172
+ `,
173
+ )
174
+ .join("")}
175
+ </fieldset>
176
+ `;
177
+
178
+ this.shadowRoot
179
+ .querySelectorAll("input[type=radio]")
180
+ .forEach((input, i, list) => {
181
+ input.addEventListener("keydown", (e) =>
182
+ this.handleKey(e, i, list),
183
+ );
184
+ input.addEventListener("click", (e) => {
185
+ this.value = e.target.value;
186
+ this.dispatchEvent(
187
+ new CustomEvent("change", {
188
+ detail: { value: this.value },
189
+ bubbles: true,
190
+ composed: true,
191
+ }),
192
+ );
193
+ });
194
+ });
195
+ }
196
+ }
197
+
198
+ if (!customElements.get("y-radio")) {
199
+ customElements.define("y-radio", YumeRadio);
200
+ }
201
+
202
+ export { YumeRadio };