@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,454 @@
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
+ /* ── Dynamic icons (parameterised — kept as functions) ────────────── */
12
+
13
+ /**
14
+ * 3-dot grip handle for drawers.
15
+ * @param {boolean} horizontal – true for left/right drawers, false for top/bottom.
16
+ */
17
+ function gripDots(horizontal) {
18
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${horizontal ? 4 : 20}" height="${horizontal ? 20 : 4}" viewBox="0 0 ${horizontal ? 4 : 20} ${horizontal ? 20 : 4}" fill="currentColor">
19
+ ${
20
+ horizontal
21
+ ? `<circle cx="2" cy="4" r="1.5"/>
22
+ <circle cx="2" cy="10" r="1.5"/>
23
+ <circle cx="2" cy="16" r="1.5"/>`
24
+ : `<circle cx="4" cy="2" r="1.5"/>
25
+ <circle cx="10" cy="2" r="1.5"/>
26
+ <circle cx="16" cy="2" r="1.5"/>`
27
+ }
28
+ </svg>`;
29
+ }
30
+
31
+ class YumeDrawer extends HTMLElement {
32
+ static get observedAttributes() {
33
+ return ["visible", "anchor", "position", "resizable"];
34
+ }
35
+
36
+ constructor() {
37
+ super();
38
+ this.attachShadow({ mode: "open" });
39
+ this._onAnchorClick = this._onAnchorClick.bind(this);
40
+ this._onKeyDown = this._onKeyDown.bind(this);
41
+ this._onResizePointerDown = this._onResizePointerDown.bind(this);
42
+ this._onResizePointerMove = this._onResizePointerMove.bind(this);
43
+ this._onResizePointerUp = this._onResizePointerUp.bind(this);
44
+ }
45
+
46
+ connectedCallback() {
47
+ this.render();
48
+ this._setupAnchor();
49
+ if (this.visible) this._show();
50
+ }
51
+
52
+ disconnectedCallback() {
53
+ this._teardownAnchor();
54
+ document.removeEventListener("keydown", this._onKeyDown);
55
+ this._cleanupResize();
56
+ }
57
+
58
+ attributeChangedCallback(name, oldVal, newVal) {
59
+ if (oldVal === newVal) return;
60
+ if (name === "visible") {
61
+ this.visible ? this._show() : this._hide();
62
+ }
63
+ if (name === "anchor") {
64
+ this._teardownAnchor();
65
+ this._setupAnchor();
66
+ }
67
+ if (name === "position") {
68
+ this._applyPosition();
69
+ }
70
+ if (name === "resizable") {
71
+ this._applyResizable();
72
+ }
73
+ }
74
+
75
+ get visible() {
76
+ return this.hasAttribute("visible");
77
+ }
78
+ set visible(val) {
79
+ if (val) this.setAttribute("visible", "");
80
+ else this.removeAttribute("visible");
81
+ }
82
+
83
+ get anchor() {
84
+ return this.getAttribute("anchor");
85
+ }
86
+ set anchor(id) {
87
+ this.setAttribute("anchor", id);
88
+ }
89
+
90
+ /**
91
+ * Which edge the drawer slides in from.
92
+ * Accepted values: "left" | "right" | "top" | "bottom" (default "left").
93
+ */
94
+ get position() {
95
+ return this.getAttribute("position") || "left";
96
+ }
97
+ set position(val) {
98
+ this.setAttribute("position", val);
99
+ }
100
+
101
+ get resizable() {
102
+ return this.hasAttribute("resizable");
103
+ }
104
+ set resizable(val) {
105
+ if (val) this.setAttribute("resizable", "");
106
+ else this.removeAttribute("resizable");
107
+ }
108
+
109
+ _setupAnchor() {
110
+ const id = this.anchor;
111
+ if (id) {
112
+ const el = document.getElementById(id);
113
+ if (el) {
114
+ this._anchorEl = el;
115
+ this._anchorEl.addEventListener("click", this._onAnchorClick);
116
+ }
117
+ }
118
+ }
119
+
120
+ _teardownAnchor() {
121
+ if (this._anchorEl) {
122
+ this._anchorEl.removeEventListener("click", this._onAnchorClick);
123
+ this._anchorEl = null;
124
+ }
125
+ }
126
+
127
+ _onAnchorClick() {
128
+ this.visible = !this.visible;
129
+ }
130
+
131
+ _show() {
132
+ this.style.display = "block";
133
+ // Force a reflow so the browser registers the initial state
134
+ this.offsetHeight; // eslint-disable-line no-unused-expressions
135
+
136
+ const overlay = this.shadowRoot.querySelector(".overlay");
137
+ const panel = this.shadowRoot.querySelector(".drawer-panel");
138
+
139
+ if (overlay) overlay.classList.add("open");
140
+ if (panel) {
141
+ panel.classList.add("open");
142
+ panel.focus();
143
+ }
144
+
145
+ document.addEventListener("keydown", this._onKeyDown);
146
+ }
147
+
148
+ _hide() {
149
+ const overlay = this.shadowRoot.querySelector(".overlay");
150
+ const panel = this.shadowRoot.querySelector(".drawer-panel");
151
+
152
+ if (overlay) overlay.classList.remove("open");
153
+ if (panel) panel.classList.remove("open");
154
+
155
+ document.removeEventListener("keydown", this._onKeyDown);
156
+
157
+ const duration = this._getTransitionDuration(panel);
158
+ if (duration > 0) {
159
+ clearTimeout(this._hideTimer);
160
+ this._hideTimer = setTimeout(() => {
161
+ if (!this.visible) this.style.display = "none";
162
+ }, duration);
163
+ } else {
164
+ this.style.display = "none";
165
+ }
166
+ }
167
+
168
+ _getTransitionDuration(el) {
169
+ if (!el) return 0;
170
+ const style = getComputedStyle(el);
171
+ const raw = style.transitionDuration || "0s";
172
+ const seconds = parseFloat(raw);
173
+ return isNaN(seconds) ? 0 : seconds * 1000;
174
+ }
175
+
176
+ _onKeyDown(e) {
177
+ if (e.key === "Escape" && this.visible) {
178
+ this.visible = false;
179
+ }
180
+ }
181
+
182
+ _onOverlayClick() {
183
+ this.visible = false;
184
+ }
185
+
186
+ _applyPosition() {
187
+ const panel = this.shadowRoot.querySelector(".drawer-panel");
188
+ if (!panel) return;
189
+ panel.setAttribute("data-position", this.position);
190
+ }
191
+
192
+ _applyResizable() {
193
+ const handle = this.shadowRoot.querySelector(".resize-handle");
194
+ if (!handle) return;
195
+ handle.style.display = this.resizable ? "flex" : "none";
196
+ }
197
+
198
+ _isHorizontal() {
199
+ return this.position === "left" || this.position === "right";
200
+ }
201
+
202
+ _onResizePointerDown(e) {
203
+ e.preventDefault();
204
+ this._resizing = true;
205
+ this._startPointer = this._isHorizontal() ? e.clientX : e.clientY;
206
+ const panel = this.shadowRoot.querySelector(".drawer-panel");
207
+ this._startSize = this._isHorizontal()
208
+ ? panel.offsetWidth
209
+ : panel.offsetHeight;
210
+
211
+ panel.style.transition = "none";
212
+ document.addEventListener("pointermove", this._onResizePointerMove);
213
+ document.addEventListener("pointerup", this._onResizePointerUp);
214
+ }
215
+
216
+ _onResizePointerMove(e) {
217
+ if (!this._resizing) return;
218
+ const panel = this.shadowRoot.querySelector(".drawer-panel");
219
+ const current = this._isHorizontal() ? e.clientX : e.clientY;
220
+ const delta = current - this._startPointer;
221
+ let newSize;
222
+
223
+ if (this.position === "left") newSize = this._startSize + delta;
224
+ else if (this.position === "right") newSize = this._startSize - delta;
225
+ else if (this.position === "top") newSize = this._startSize + delta;
226
+ else newSize = this._startSize - delta;
227
+
228
+ const minSize = 100;
229
+ newSize = Math.max(minSize, newSize);
230
+
231
+ if (this._isHorizontal()) {
232
+ panel.style.width = `${newSize}px`;
233
+ } else {
234
+ panel.style.height = `${newSize}px`;
235
+ }
236
+ }
237
+
238
+ _onResizePointerUp() {
239
+ this._resizing = false;
240
+ const panel = this.shadowRoot.querySelector(".drawer-panel");
241
+ if (panel) panel.style.transition = "";
242
+ document.removeEventListener("pointermove", this._onResizePointerMove);
243
+ document.removeEventListener("pointerup", this._onResizePointerUp);
244
+ }
245
+
246
+ _cleanupResize() {
247
+ document.removeEventListener("pointermove", this._onResizePointerMove);
248
+ document.removeEventListener("pointerup", this._onResizePointerUp);
249
+ }
250
+
251
+ _gripSVG() {
252
+ return gripDots(this._isHorizontal());
253
+ }
254
+
255
+ render() {
256
+ this.shadowRoot.innerHTML = "";
257
+
258
+ const style = document.createElement("style");
259
+ style.textContent = `
260
+ :host {
261
+ position: fixed;
262
+ top: 0; left: 0; right: 0; bottom: 0;
263
+ display: none;
264
+ z-index: 1000;
265
+ }
266
+ :host([visible]) {
267
+ display: block;
268
+ }
269
+
270
+ .overlay {
271
+ position: absolute;
272
+ top: 0; left: 0; right: 0; bottom: 0;
273
+ background: rgba(0, 0, 0, 0);
274
+ transition: background var(--drawer-transition-duration, 0.3s) ease;
275
+ }
276
+ .overlay.open {
277
+ background: rgba(0, 0, 0, 0.4);
278
+ }
279
+
280
+ .drawer-panel {
281
+ position: absolute;
282
+ background: var(--component-drawer-background, #fff);
283
+ color: var(--component-drawer-color, #000);
284
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
285
+ overflow: hidden;
286
+ outline: none;
287
+ display: flex;
288
+ flex-direction: column;
289
+ border: none;
290
+ border-radius: var(--component-drawer-border-radius, 0);
291
+ transition: transform var(--drawer-transition-duration, 0.3s) ease;
292
+ }
293
+
294
+ .drawer-panel[data-position="left"],
295
+ .drawer-panel[data-position="right"] {
296
+ top: 0;
297
+ bottom: 0;
298
+ width: var(--drawer-width, 300px);
299
+ flex-direction: row;
300
+ }
301
+ .drawer-panel[data-position="left"] {
302
+ left: 0;
303
+ transform: translateX(-100%);
304
+ border-right: var(--component-drawer-border-width, 2px) solid var(--component-drawer-border-color, #ccc);
305
+ }
306
+ .drawer-panel[data-position="right"] {
307
+ right: 0;
308
+ transform: translateX(100%);
309
+ border-left: var(--component-drawer-border-width, 2px) solid var(--component-drawer-border-color, #ccc);
310
+ }
311
+
312
+ .drawer-panel[data-position="top"],
313
+ .drawer-panel[data-position="bottom"] {
314
+ left: 0;
315
+ right: 0;
316
+ height: var(--drawer-height, 300px);
317
+ }
318
+ .drawer-panel[data-position="top"] {
319
+ top: 0;
320
+ transform: translateY(-100%);
321
+ border-bottom: var(--component-drawer-border-width, 2px) solid var(--component-drawer-border-color, #ccc);
322
+ }
323
+ .drawer-panel[data-position="bottom"] {
324
+ bottom: 0;
325
+ transform: translateY(100%);
326
+ border-top: var(--component-drawer-border-width, 2px) solid var(--component-drawer-border-color, #ccc);
327
+ }
328
+
329
+ .drawer-panel.open { transform: translate(0, 0); }
330
+
331
+ .drawer-header {
332
+ padding: var(--component-drawer-padding, 1rem);
333
+ font-weight: bold;
334
+ }
335
+ .drawer-body {
336
+ padding: var(--component-drawer-padding, 1rem);
337
+ flex: 1;
338
+ overflow: auto;
339
+ }
340
+ .drawer-footer {
341
+ padding: var(--component-drawer-padding, 1rem);
342
+ }
343
+
344
+ /* Wrapper so header/body/footer stack vertically inside a row layout */
345
+ .drawer-content {
346
+ display: flex;
347
+ flex-direction: column;
348
+ flex: 1;
349
+ min-width: 0;
350
+ min-height: 0;
351
+ overflow: hidden;
352
+ }
353
+
354
+ ::slotted(*) {
355
+ margin: 0;
356
+ }
357
+
358
+ .resize-handle {
359
+ display: none; /* hidden until resizable attr */
360
+ flex-shrink: 0;
361
+ align-items: center;
362
+ justify-content: center;
363
+ color: var(--component-drawer-color, #999);
364
+ opacity: 0.6;
365
+ touch-action: none; /* needed for pointer events */
366
+ user-select: none;
367
+ transition: opacity 0.15s, background 0.15s;
368
+ }
369
+ .resize-handle:hover,
370
+ .resize-handle:active {
371
+ opacity: 1;
372
+ background: var(--component-drawer-hover-background, rgba(128,128,128,0.15));
373
+ }
374
+
375
+ .drawer-panel[data-position="left"] > .resize-handle,
376
+ .drawer-panel[data-position="right"] > .resize-handle {
377
+ width: var(--component-drawer-handle-width, 6px);
378
+ padding: var(--component-drawer-handle-padding, 4px);
379
+ cursor: ew-resize;
380
+ }
381
+ .drawer-panel[data-position="left"] > .resize-handle {
382
+ order: 99;
383
+ }
384
+ .drawer-panel[data-position="right"] > .resize-handle {
385
+ order: -1;
386
+ }
387
+
388
+ .drawer-panel[data-position="top"] > .resize-handle,
389
+ .drawer-panel[data-position="bottom"] > .resize-handle {
390
+ height: var(--component-drawer-handle-width, 6px);
391
+ padding: var(--component-drawer-handle-padding, 4px);
392
+ cursor: ns-resize;
393
+ }
394
+ .drawer-panel[data-position="top"] > .resize-handle {
395
+ order: 99;
396
+ }
397
+ .drawer-panel[data-position="bottom"] > .resize-handle {
398
+ order: -1;
399
+ }
400
+ `;
401
+ this.shadowRoot.appendChild(style);
402
+
403
+ const overlay = document.createElement("div");
404
+ overlay.className = "overlay";
405
+ overlay.addEventListener("click", () => this._onOverlayClick());
406
+ this.shadowRoot.appendChild(overlay);
407
+
408
+ const panel = document.createElement("div");
409
+ panel.className = "drawer-panel";
410
+ panel.setAttribute("role", "dialog");
411
+ panel.setAttribute("aria-modal", "true");
412
+ panel.setAttribute("tabindex", "-1");
413
+ panel.setAttribute("data-position", this.position);
414
+
415
+ const handle = document.createElement("div");
416
+ handle.className = "resize-handle";
417
+ handle.innerHTML = this._gripSVG();
418
+ handle.style.display = this.resizable ? "flex" : "none";
419
+ handle.addEventListener("pointerdown", this._onResizePointerDown);
420
+ panel.appendChild(handle);
421
+
422
+ const content = document.createElement("div");
423
+ content.className = "drawer-content";
424
+
425
+ const header = document.createElement("div");
426
+ header.className = "drawer-header";
427
+ header.innerHTML = `<slot name="header"></slot>`;
428
+
429
+ const body = document.createElement("div");
430
+ body.className = "drawer-body";
431
+ body.innerHTML = `<slot name="body"></slot>`;
432
+
433
+ const footer = document.createElement("div");
434
+ footer.className = "drawer-footer";
435
+ footer.innerHTML = `<slot name="footer"></slot>`;
436
+
437
+ content.appendChild(header);
438
+ content.appendChild(body);
439
+ content.appendChild(footer);
440
+ panel.appendChild(content);
441
+ this.shadowRoot.appendChild(panel);
442
+
443
+ if (this.visible) {
444
+ requestAnimationFrame(() => {
445
+ overlay.classList.add("open");
446
+ panel.classList.add("open");
447
+ });
448
+ }
449
+ }
450
+ }
451
+
452
+ if (!customElements.get("y-drawer")) {
453
+ customElements.define("y-drawer", YumeDrawer);
454
+ }
@@ -0,0 +1,225 @@
1
+ class YumeInput extends HTMLElement {
2
+ static formAssociated = true;
3
+
4
+ static get observedAttributes() {
5
+ return [
6
+ "type",
7
+ "size",
8
+ "value",
9
+ "label-position",
10
+ "disabled",
11
+ "invalid",
12
+ "name",
13
+ ];
14
+ }
15
+
16
+ constructor() {
17
+ super();
18
+ this._internals = this.attachInternals();
19
+ this.attachShadow({ mode: "open" });
20
+ this.render();
21
+ }
22
+
23
+ connectedCallback() {
24
+ if (!this.hasAttribute("size")) {
25
+ this.setAttribute("size", "medium");
26
+ }
27
+ if (!this.hasAttribute("label-position")) {
28
+ this.setAttribute("label-position", "top");
29
+ }
30
+ this._internals.setFormValue(this.value);
31
+ }
32
+
33
+ attributeChangedCallback(name, oldValue, newValue) {
34
+ if (oldValue === newValue) return;
35
+
36
+ if (name === "value") {
37
+ if (this.input) this.input.value = newValue;
38
+ if (this._internals) {
39
+ this._internals.setFormValue(
40
+ newValue,
41
+ this.getAttribute("name"),
42
+ );
43
+ }
44
+ return;
45
+ }
46
+
47
+ if (name === "name") {
48
+ this._internals.setFormValue(this.value, newValue);
49
+ return;
50
+ }
51
+
52
+ if (name === "invalid") {
53
+ this.updateValidationState();
54
+ return;
55
+ }
56
+
57
+ this.render();
58
+ }
59
+
60
+ get value() {
61
+ return this.input?.value || "";
62
+ }
63
+
64
+ set value(val) {
65
+ if (this.input) this.input.value = val;
66
+ else this.setAttribute("value", val);
67
+ this._internals.setFormValue(val, this.getAttribute("name"));
68
+ }
69
+
70
+ checkValidity() {
71
+ return this.input?.checkValidity?.() ?? true;
72
+ }
73
+
74
+ updateValidationState() {
75
+ const isManuallyInvalid = this.hasAttribute("invalid");
76
+ const isAutomaticallyInvalid = this.input && !this.checkValidity();
77
+ const isInvalid = isManuallyInvalid || isAutomaticallyInvalid;
78
+
79
+ this.inputContainer?.classList.toggle("is-invalid", isInvalid);
80
+ this.labelWrapper?.classList.toggle("is-invalid", isInvalid);
81
+ }
82
+
83
+ render() {
84
+ const type = this.getAttribute("type") || "text";
85
+ const size = this.getAttribute("size") || "medium";
86
+ const value = this.getAttribute("value") || "";
87
+ const labelPosition = this.getAttribute("label-position") || "top";
88
+ const isDisabled = this.hasAttribute("disabled");
89
+ const isLabelTop = labelPosition === "top";
90
+
91
+ const paddingVar = {
92
+ small: "--component-inputs-padding-small",
93
+ medium: "--component-inputs-padding-medium",
94
+ large: "--component-inputs-padding-large",
95
+ }[size];
96
+
97
+ const sheet = new CSSStyleSheet();
98
+ sheet.replaceSync(`
99
+ :host {
100
+ display: block;
101
+ font-family: var(--font-family-body);
102
+ color: var(--component-input-color);
103
+ opacity: ${isDisabled ? "0.75" : "1"};
104
+ pointer-events: ${isDisabled ? "none" : "auto"};
105
+ }
106
+
107
+ .input-wrapper {
108
+ position: relative;
109
+ display: flex;
110
+ flex-direction: column;
111
+ gap: var(--spacing-2x-small, 4px);
112
+ }
113
+
114
+ .input-container {
115
+ display: flex;
116
+ align-items: center;
117
+ gap: var(--spacing-x-small);
118
+ background: ${isDisabled ? "var(--component-input-background-disabled)" : "var(--component-input-background)"};
119
+ border: var(--component-inputs-border-width) solid var(--component-input-border-color);
120
+ border-radius: var(--component-inputs-border-radius-outer);
121
+ padding: var(${paddingVar});
122
+ box-sizing: border-box;
123
+ transition: border-color 0.2s ease-in-out;
124
+ }
125
+
126
+ .input-container.is-invalid {
127
+ border-color: var(--component-input-error-border-color);
128
+ background: var(--component-input-error-background);
129
+ }
130
+
131
+ .input-container.is-invalid input {
132
+ color: var(--component-input-error-color);
133
+ }
134
+
135
+ .input-container.is-invalid:hover {
136
+ border-color: var(--component-input-error-color);
137
+ }
138
+
139
+ .input-container.is-invalid:focus-within {
140
+ border-color: var(--component-input-error-color);
141
+ }
142
+
143
+ .input-container.is-invalid:focus-within input {
144
+ color: var(--component-input-color);
145
+ }
146
+
147
+ input {
148
+ all: unset;
149
+ flex: 1;
150
+ font-family: inherit;
151
+ font-size: 1em;
152
+ color: inherit;
153
+ min-width: 0;
154
+ min-height: 20px;
155
+ }
156
+
157
+ .input-container:hover {
158
+ border-color: var(--component-input-color);
159
+ transition: border-color 0.2s ease-in-out;
160
+ }
161
+
162
+ .input-container:focus-within {
163
+ border-color: var(--component-input-accent);
164
+ }
165
+
166
+ .label-wrapper.is-invalid ::slotted([slot="label"]) {
167
+ color: var(--component-input-error-color);
168
+ }
169
+
170
+ ::slotted([slot="label"]) {
171
+ font-weight: 500;
172
+ font-size: 0.875em;
173
+ color: var(--component-input-label-color);
174
+ }
175
+
176
+ ::slotted([slot="left-icon"]),
177
+ ::slotted([slot="right-icon"]) {
178
+ display: flex;
179
+ align-items: center;
180
+ justify-content: center;
181
+ color: var(--component-input-icon-color);
182
+ }
183
+ `);
184
+
185
+ this.shadowRoot.adoptedStyleSheets = [sheet];
186
+
187
+ this.shadowRoot.innerHTML = `
188
+ <div class="input-wrapper">
189
+ ${isLabelTop ? '<div class="label-wrapper"><slot name="label"></slot></div>' : ""}
190
+ <div class="input-container">
191
+ <slot name="left-icon"></slot>
192
+ <input part="input" type="${type}" value="${value}" ${isDisabled ? "disabled" : ""} />
193
+ <slot name="right-icon"></slot>
194
+ </div>
195
+ ${!isLabelTop ? '<div class="label-wrapper"><slot name="label"></slot></div>' : ""}
196
+ </div>
197
+ `;
198
+
199
+ this.input = this.shadowRoot.querySelector("input");
200
+ this.inputContainer = this.shadowRoot.querySelector(".input-container");
201
+ this.labelWrapper = this.shadowRoot.querySelector(".label-wrapper");
202
+
203
+ if (!isDisabled) {
204
+ this.input.addEventListener("input", (e) => {
205
+ this.setAttribute("value", e.target.value);
206
+ this.dispatchEvent(
207
+ new CustomEvent("input", {
208
+ detail: { value: e.target.value },
209
+ bubbles: true,
210
+ composed: true,
211
+ }),
212
+ );
213
+ this.updateValidationState();
214
+ });
215
+
216
+ this.updateValidationState();
217
+ }
218
+ }
219
+ }
220
+
221
+ if (!customElements.get("y-input")) {
222
+ customElements.define("y-input", YumeInput);
223
+ }
224
+
225
+ export { YumeInput };