@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,353 @@
1
+ class YumeSwitch extends HTMLElement {
2
+ static formAssociated = true;
3
+
4
+ static get observedAttributes() {
5
+ return [
6
+ "checked",
7
+ "disabled",
8
+ "animate",
9
+ "toggle-label",
10
+ "label-display",
11
+ "label-position",
12
+ "size",
13
+ "value",
14
+ ];
15
+ }
16
+
17
+ constructor() {
18
+ super();
19
+ this._internals = this.attachInternals();
20
+ this.attachShadow({ mode: "open" });
21
+ }
22
+
23
+ connectedCallback() {
24
+ if (!this.hasAttribute("size")) this.setAttribute("size", "medium");
25
+ if (!this.hasAttribute("label-display"))
26
+ this.setAttribute("label-display", "true");
27
+ if (!this.hasAttribute("label-position"))
28
+ this.setAttribute("label-position", "top");
29
+ if (!this.hasAttribute("animate")) this.setAttribute("animate", "true");
30
+
31
+ this.render();
32
+ this.mirrorToggleLabels();
33
+
34
+ const sw = this.shadowRoot.querySelector(".switch");
35
+ sw.addEventListener("click", () => this.toggle());
36
+ sw.addEventListener("keydown", (e) => {
37
+ if (e.key === " " || e.key === "Enter") {
38
+ e.preventDefault();
39
+ this.toggle();
40
+ }
41
+ });
42
+ }
43
+
44
+ attributeChangedCallback(name, oldValue, newValue) {
45
+ if (oldValue !== newValue) {
46
+ this.update();
47
+ }
48
+ }
49
+
50
+ get checked() {
51
+ return this.hasAttribute("checked");
52
+ }
53
+
54
+ set checked(val) {
55
+ if (val) this.setAttribute("checked", "");
56
+ else this.removeAttribute("checked");
57
+ this.update();
58
+ }
59
+
60
+ get value() {
61
+ return this.getAttribute("value") || "on";
62
+ }
63
+
64
+ set value(val) {
65
+ this.setAttribute("value", val);
66
+ this.update();
67
+ }
68
+
69
+ toggle() {
70
+ if (this.hasAttribute("disabled")) return;
71
+ this.checked = !this.checked;
72
+ this.dispatchEvent(
73
+ new Event("change", { bubbles: true, composed: true }),
74
+ );
75
+ }
76
+
77
+ labelTag(pos) {
78
+ const labelPos = this.getAttribute("label-position");
79
+ const shouldRender =
80
+ (pos === "top" && (labelPos === "top" || labelPos === "left")) ||
81
+ (pos === "bottom" &&
82
+ (labelPos === "bottom" || labelPos === "right"));
83
+ return shouldRender ? `<label><slot name="label"></slot></label>` : "";
84
+ }
85
+
86
+ mirrorToggleLabels() {
87
+ requestAnimationFrame(() => {
88
+ const toggle = this.shadowRoot?.querySelector(".toggle");
89
+ if (!toggle) return;
90
+
91
+ toggle.innerHTML = "";
92
+
93
+ const offSlot = this.querySelector('[slot="off-label"]');
94
+ const onSlot = this.querySelector('[slot="on-label"]');
95
+
96
+ const fallbackOff = document.createTextNode("Off");
97
+ const fallbackOn = document.createTextNode("On");
98
+
99
+ const offClone = offSlot?.cloneNode(true) || fallbackOff;
100
+ const onClone = onSlot?.cloneNode(true) || fallbackOn;
101
+
102
+ const offWrapper = document.createElement("span");
103
+ offWrapper.className = "off";
104
+ offWrapper.appendChild(offClone);
105
+
106
+ const onWrapper = document.createElement("span");
107
+ onWrapper.className = "on";
108
+ onWrapper.appendChild(onClone);
109
+
110
+ toggle.appendChild(offWrapper);
111
+ toggle.appendChild(onWrapper);
112
+ });
113
+ }
114
+
115
+ update() {
116
+ this.updateSizeStyles();
117
+ this.updateTogglePosition();
118
+ this.updateToggleLabelDisplay();
119
+ this.updateLabelDisplay();
120
+ this.updateDirection();
121
+ this.updateAria();
122
+ this.updateFormValue();
123
+ this.mirrorToggleLabels();
124
+ }
125
+
126
+ updateSizeStyles() {
127
+ const size = this.getAttribute("size") || "medium";
128
+ const heightMap = { small: "24px", medium: "32px", large: "40px" };
129
+ const widthMap = { small: "44px", medium: "56px", large: "72px" };
130
+ const fontMap = {
131
+ small: "var(--font-size-small)",
132
+ medium: "var(--font-size-label)",
133
+ large: "var(--font-size-h4)",
134
+ };
135
+ this.style.setProperty("--switch-height", heightMap[size]);
136
+ this.style.setProperty("--switch-width-fixed", widthMap[size]);
137
+ this.style.setProperty(
138
+ "--toggle-size",
139
+ "calc(var(--switch-height) - (var(--switch-padding) * 2) - (var(--component-switch-border-width, 0px) * 2))",
140
+ );
141
+ this.style.setProperty("--switch-font-size", fontMap[size]);
142
+ }
143
+
144
+ updateTogglePosition() {
145
+ const isChecked = this.checked;
146
+ const showToggleLabels =
147
+ this.hasAttribute("toggle-label") &&
148
+ this.getAttribute("toggle-label") !== "false";
149
+ this.style.setProperty(
150
+ "--toggle-x",
151
+ isChecked
152
+ ? showToggleLabels
153
+ ? "100%"
154
+ : "calc(var(--switch-width) - var(--toggle-size) - (var(--switch-padding) * 2) - (var(--component-switch-border-width, 0px) * 2))"
155
+ : "0",
156
+ );
157
+ this.style.setProperty(
158
+ "--toggle-bg",
159
+ isChecked
160
+ ? "var(--primary-content--)"
161
+ : "var(--base-content-light)",
162
+ );
163
+ this.style.setProperty(
164
+ "--toggle-transition",
165
+ this.getAttribute("animate") === "false"
166
+ ? "none"
167
+ : "transform 0.25s ease, background 0.25s ease",
168
+ );
169
+ }
170
+
171
+ updateLabelDisplay() {
172
+ const showLabels = this.getAttribute("label-display") !== "false";
173
+ const showToggleLabels =
174
+ this.hasAttribute("toggle-label") &&
175
+ this.getAttribute("toggle-label") !== "false";
176
+ this.style.setProperty(
177
+ "--show-labels",
178
+ showLabels && showToggleLabels ? "flex" : "none",
179
+ );
180
+ }
181
+
182
+ updateToggleLabelDisplay() {
183
+ const showToggleLabels =
184
+ this.hasAttribute("toggle-label") &&
185
+ this.getAttribute("toggle-label") !== "false";
186
+ this.style.setProperty(
187
+ "--show-toggle-label",
188
+ showToggleLabels ? "inline-flex" : "none",
189
+ );
190
+ this.style.setProperty(
191
+ "--switch-width",
192
+ showToggleLabels ? "max-content" : "var(--switch-width-fixed)",
193
+ );
194
+ this.style.setProperty(
195
+ "--toggle-width",
196
+ showToggleLabels ? "auto" : "var(--toggle-size)",
197
+ );
198
+ this.style.setProperty(
199
+ "--toggle-padding",
200
+ showToggleLabels ? "0 var(--spacing-small)" : "0",
201
+ );
202
+ this.style.setProperty(
203
+ "--toggle-radius",
204
+ showToggleLabels
205
+ ? "var(--component-switch-border-radius)"
206
+ : "9999px",
207
+ );
208
+ }
209
+
210
+ updateDirection() {
211
+ const pos = this.getAttribute("label-position");
212
+ const directionMap = {
213
+ top: "column",
214
+ bottom: "column-reverse",
215
+ left: "row-reverse",
216
+ right: "row",
217
+ };
218
+ this.style.setProperty("--switch-dir", directionMap[pos] || "column");
219
+ }
220
+
221
+ updateAria() {
222
+ const sw = this.shadowRoot?.querySelector(".switch");
223
+ if (sw) {
224
+ sw.setAttribute("aria-checked", this.checked);
225
+ sw.setAttribute("aria-disabled", this.disabled ? "true" : "false");
226
+ }
227
+ }
228
+
229
+ updateFormValue() {
230
+ this._internals.setFormValue(this.checked ? this.value : "");
231
+ }
232
+
233
+ render() {
234
+ this.shadowRoot.innerHTML = `
235
+ <style>
236
+ :host {
237
+ display: inline-flex;
238
+ flex-direction: var(--switch-dir, column);
239
+ align-items: center;
240
+ gap: var(--spacing-x-small);
241
+ font-family: var(--font-family-body);
242
+ --switch-padding: var(--component-switch-padding, 2px);
243
+ --show-labels: flex;
244
+ --show-toggle-label: none;
245
+ --switch-width: max-content;
246
+ --toggle-width: auto;
247
+ --toggle-padding: 0 var(--spacing-small);
248
+ --toggle-radius: var(--component-switch-border-radius);
249
+ }
250
+
251
+ .label {
252
+ font-size: var(--font-size-label);
253
+ color: var(--base-content--);
254
+ }
255
+
256
+ .switch {
257
+ position: relative;
258
+ display: inline-flex;
259
+ align-items: center;
260
+ background: var(--base-background-component);
261
+ border: var(--component-switch-border-width) solid var(--base-background-border);
262
+ border-radius: var(--component-switch-border-radius);
263
+ cursor: pointer;
264
+ height: var(--switch-height);
265
+ font-size: var(--switch-font-size);
266
+ box-sizing: border-box;
267
+ padding: var(--switch-padding);
268
+ width: var(--switch-width, max-content);
269
+ }
270
+
271
+ .track {
272
+ display: flex;
273
+ align-items: center;
274
+ height: 100%;
275
+ position: relative;
276
+ z-index: 0;
277
+ }
278
+
279
+ .label-content {
280
+ flex: 0 0 auto;
281
+ align-items: center;
282
+ justify-content: center;
283
+ padding: 0 var(--spacing-small);
284
+ white-space: nowrap;
285
+ position: relative;
286
+ z-index: 0;
287
+ color: var(--base-content-light);
288
+ display: var(--show-labels, flex);
289
+ }
290
+
291
+ .toggle {
292
+ position: absolute;
293
+ top: var(--switch-padding);
294
+ bottom: var(--switch-padding);
295
+ left: var(--switch-padding);
296
+ height: calc(100% - (var(--switch-padding) + var(--switch-padding)));
297
+ width: var(--toggle-width, auto);
298
+ background: var(--toggle-bg, var(--base-content-light));
299
+ color: var(--base-background-component);
300
+ border-radius: var(--toggle-radius, var(--component-switch-border-radius));
301
+ display: flex;
302
+ align-items: center;
303
+ justify-content: center;
304
+ padding: var(--toggle-padding, 0 var(--spacing-small));
305
+ font-weight: 500;
306
+ z-index: 1;
307
+ white-space: nowrap;
308
+ transform: translateX(var(--toggle-x, 0));
309
+ transition: var(--toggle-transition, transform 0.25s ease, background 0.25s ease);
310
+ }
311
+
312
+ .toggle .on,
313
+ .toggle .off {
314
+ display: none;
315
+ }
316
+
317
+ :host([checked]) .toggle .on {
318
+ display: var(--show-toggle-label, none);
319
+ }
320
+
321
+ :host(:not([checked])) .toggle .off {
322
+ display: var(--show-toggle-label, none);
323
+ }
324
+
325
+
326
+ :host([animate="false"]) .toggle {
327
+ transition: none !important;
328
+ }
329
+ </style>
330
+
331
+ ${this.labelTag("top")}
332
+
333
+ <div class="switch" part="switch" tabindex="0" role="switch" aria-checked="${this.checked}" aria-disabled="${this.disabled}">
334
+ <div class="track">
335
+ <div class="label-content"><slot name="off-label">Off</slot></div>
336
+ <div class="label-content"><slot name="on-label">On</slot></div>
337
+ </div>
338
+ <div class="toggle" part="toggle">
339
+ <span class="off"></span>
340
+ <span class="on"></span>
341
+ </div>
342
+ </div>
343
+
344
+ ${this.labelTag("bottom")}
345
+ `;
346
+
347
+ this.update();
348
+ }
349
+ }
350
+
351
+ if (!customElements.get("y-switch")) {
352
+ customElements.define("y-switch", YumeSwitch);
353
+ }
@@ -0,0 +1,323 @@
1
+ /* ================================================================== */
2
+ /* Centralized SVG icon strings for the YumeKit component library. */
3
+ /* */
4
+ /* Each static icon also lives in its own .svg file in this directory */
5
+ /* so it can be used standalone (e.g. <img src="…">, CSS background, */
6
+ /* design tools, etc.). The strings below mirror those files — keep */
7
+ /* them in sync when editing an icon. */
8
+ /* ================================================================== */
9
+
10
+
11
+ /**
12
+ * Dual-arrow sort indicator for table headers.
13
+ * @param {string} topColor – fill for the up-arrow triangle.
14
+ * @param {string} bottomColor – fill for the down-arrow triangle.
15
+ */
16
+ function sortArrows(topColor, bottomColor) {
17
+ return `<svg class="sort-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="16" viewBox="0 0 12 16" aria-hidden="true">
18
+ <path d="M6 1 L11 7 L1 7 Z" fill="${topColor}"/>
19
+ <path d="M6 15 L11 9 L1 9 Z" fill="${bottomColor}"/>
20
+ </svg>`;
21
+ }
22
+
23
+ class YumeTable extends HTMLElement {
24
+ static get observedAttributes() {
25
+ return ["columns", "data", "striped", "size"];
26
+ }
27
+
28
+ constructor() {
29
+ super();
30
+ this.attachShadow({ mode: "open" });
31
+ this._sortField = null;
32
+ this._sortDir = "none"; // "none" | "asc" | "desc"
33
+ this._parsedData = [];
34
+ this._parsedColumns = [];
35
+ }
36
+
37
+ connectedCallback() {
38
+ this._parseAttributes();
39
+ this.render();
40
+ }
41
+
42
+ attributeChangedCallback(name, oldVal, newVal) {
43
+ if (oldVal === newVal) return;
44
+ this._parseAttributes();
45
+ this.render();
46
+ }
47
+
48
+ get columns() {
49
+ return this.getAttribute("columns");
50
+ }
51
+ set columns(val) {
52
+ this.setAttribute(
53
+ "columns",
54
+ typeof val === "string" ? val : JSON.stringify(val),
55
+ );
56
+ }
57
+
58
+ get data() {
59
+ return this.getAttribute("data");
60
+ }
61
+ set data(val) {
62
+ this.setAttribute(
63
+ "data",
64
+ typeof val === "string" ? val : JSON.stringify(val),
65
+ );
66
+ }
67
+
68
+ get striped() {
69
+ return this.hasAttribute("striped");
70
+ }
71
+ set striped(val) {
72
+ if (val) this.setAttribute("striped", "");
73
+ else this.removeAttribute("striped");
74
+ }
75
+
76
+ /**
77
+ * Cell padding size: "small" | "medium" | "large" (default "medium").
78
+ */
79
+ get size() {
80
+ return this.getAttribute("size") || "medium";
81
+ }
82
+ set size(val) {
83
+ this.setAttribute("size", val);
84
+ }
85
+
86
+ _parseAttributes() {
87
+ try {
88
+ this._parsedColumns = JSON.parse(this.columns || "[]");
89
+ } catch {
90
+ this._parsedColumns = [];
91
+ }
92
+ try {
93
+ this._parsedData = JSON.parse(this.data || "[]");
94
+ } catch {
95
+ this._parsedData = [];
96
+ }
97
+ }
98
+
99
+ _onHeaderClick(field) {
100
+ if (this._sortField === field) {
101
+ this._sortDir =
102
+ this._sortDir === "asc"
103
+ ? "desc"
104
+ : this._sortDir === "desc"
105
+ ? "none"
106
+ : "asc";
107
+ if (this._sortDir === "none") this._sortField = null;
108
+ } else {
109
+ this._sortField = field;
110
+ this._sortDir = "asc";
111
+ }
112
+
113
+ this.dispatchEvent(
114
+ new CustomEvent("sort", {
115
+ detail: { field: this._sortField, direction: this._sortDir },
116
+ bubbles: true,
117
+ composed: true,
118
+ }),
119
+ );
120
+
121
+ this.render();
122
+ }
123
+
124
+ _getSortedData() {
125
+ const data = [...this._parsedData];
126
+ if (!this._sortField || this._sortDir === "none") return data;
127
+
128
+ const dir = this._sortDir === "asc" ? 1 : -1;
129
+ const field = this._sortField;
130
+
131
+ return data.sort((a, b) => {
132
+ const aVal = a[field];
133
+ const bVal = b[field];
134
+
135
+ if (aVal == null && bVal == null) return 0;
136
+ if (aVal == null) return 1;
137
+ if (bVal == null) return -1;
138
+
139
+ if (typeof aVal === "number" && typeof bVal === "number") {
140
+ return (aVal - bVal) * dir;
141
+ }
142
+
143
+ return String(aVal).localeCompare(String(bVal)) * dir;
144
+ });
145
+ }
146
+
147
+ _sortIcon(field) {
148
+ const active = this._sortField === field;
149
+ const dir = active ? this._sortDir : "none";
150
+
151
+ const topColor =
152
+ dir === "asc"
153
+ ? "var(--component-table-color, #333)"
154
+ : "var(--component-table-color-light, #bbb)";
155
+ const bottomColor =
156
+ dir === "desc"
157
+ ? "var(--component-table-color, #333)"
158
+ : "var(--component-table-color-light, #bbb)";
159
+
160
+ return sortArrows(topColor, bottomColor);
161
+ }
162
+
163
+ render() {
164
+ const columns = this._parsedColumns;
165
+ const rows = this._getSortedData();
166
+ const size = this.size;
167
+ const striped = this.striped;
168
+
169
+ const paddingVar = `var(--component-table-padding-${size}, 8px)`;
170
+
171
+ this.shadowRoot.innerHTML = "";
172
+
173
+ const style = document.createElement("style");
174
+ style.textContent = `
175
+ :host {
176
+ display: block;
177
+ font-family: var(--font-family-body, sans-serif);
178
+ color: var(--component-table-color, #000);
179
+ }
180
+
181
+ .table-wrapper {
182
+ overflow-x: auto;
183
+ }
184
+
185
+ table {
186
+ width: 100%;
187
+ border-collapse: collapse;
188
+ table-layout: auto;
189
+ }
190
+
191
+ thead th {
192
+ position: relative;
193
+ padding: ${paddingVar};
194
+ text-align: left;
195
+ font-weight: 500;
196
+ font-size: var(--font-size-paragraph, 1em);
197
+ white-space: nowrap;
198
+ background: transparent;
199
+ border-bottom: var(--component-table-border-width-header, 2px) solid var(--component-table-border-color, #ccc);
200
+ user-select: none;
201
+ }
202
+
203
+ thead th.sortable {
204
+ cursor: pointer;
205
+ }
206
+
207
+ thead th.sortable:hover {
208
+ background: var(--component-table-hover-background, #f5f5f5);
209
+ }
210
+
211
+ .th-content {
212
+ display: inline-flex;
213
+ align-items: center;
214
+ gap: 6px;
215
+ }
216
+
217
+ .sort-icon {
218
+ flex-shrink: 0;
219
+ vertical-align: middle;
220
+ }
221
+
222
+ tbody td {
223
+ padding: ${paddingVar};
224
+ font-size: var(--font-size-paragraph, 1em);
225
+ border-bottom: var(--component-table-border-width, 2px) solid var(--component-table-border-color, #ccc);
226
+ }
227
+
228
+ tbody tr:last-child td {
229
+ border-bottom: none;
230
+ }
231
+
232
+ tbody td.row-header {
233
+ font-weight: 500;
234
+ }
235
+
236
+ ${
237
+ striped
238
+ ? `tbody tr:nth-child(even) {
239
+ background: var(--component-table-hover-background, #f9f9f9);
240
+ }`
241
+ : ""
242
+ }
243
+
244
+ tbody tr:hover {
245
+ background: var(--component-table-active-background, #eee);
246
+ }
247
+ `;
248
+ this.shadowRoot.appendChild(style);
249
+
250
+ const wrapper = document.createElement("div");
251
+ wrapper.className = "table-wrapper";
252
+
253
+ const table = document.createElement("table");
254
+ table.setAttribute("role", "grid");
255
+
256
+ const thead = document.createElement("thead");
257
+ const headerRow = document.createElement("tr");
258
+
259
+ columns.forEach((col) => {
260
+ const th = document.createElement("th");
261
+ th.setAttribute("scope", "col");
262
+
263
+ const sortable = col.sortable !== false;
264
+ if (sortable) {
265
+ th.classList.add("sortable");
266
+ th.setAttribute(
267
+ "aria-sort",
268
+ this._sortField === col.field
269
+ ? this._sortDir === "asc"
270
+ ? "ascending"
271
+ : this._sortDir === "desc"
272
+ ? "descending"
273
+ : "none"
274
+ : "none",
275
+ );
276
+ th.addEventListener("click", () =>
277
+ this._onHeaderClick(col.field),
278
+ );
279
+ }
280
+
281
+ const inner = document.createElement("span");
282
+ inner.className = "th-content";
283
+ inner.textContent = col.header || col.field;
284
+
285
+ if (sortable) {
286
+ const iconSpan = document.createElement("span");
287
+ iconSpan.innerHTML = this._sortIcon(col.field);
288
+ inner.appendChild(iconSpan);
289
+ }
290
+
291
+ th.appendChild(inner);
292
+ headerRow.appendChild(th);
293
+ });
294
+
295
+ thead.appendChild(headerRow);
296
+ table.appendChild(thead);
297
+
298
+ const tbody = document.createElement("tbody");
299
+
300
+ rows.forEach((row) => {
301
+ const tr = document.createElement("tr");
302
+ columns.forEach((col, colIdx) => {
303
+ const td = document.createElement("td");
304
+ if (col.rowHeader || colIdx === 0) {
305
+ td.classList.add("row-header");
306
+ }
307
+ td.textContent = row[col.field] ?? "";
308
+ tr.appendChild(td);
309
+ });
310
+ tbody.appendChild(tr);
311
+ });
312
+
313
+ table.appendChild(tbody);
314
+ wrapper.appendChild(table);
315
+ this.shadowRoot.appendChild(wrapper);
316
+ }
317
+ }
318
+
319
+ if (!customElements.get("y-table")) {
320
+ customElements.define("y-table", YumeTable);
321
+ }
322
+
323
+ export { YumeTable };