|
| 1 | +// _c2c_ui_kit.js — shared LTX-quality design system for C2C/MEC node UIs. |
| 2 | +// |
| 3 | +// WHY: our node editors used ad-hoc inline styles and read as "canvas |
| 4 | +// sketches" next to LTX Director, which ships one consistent stylesheet |
| 5 | +// (pill buttons, floating-label panels, popover menus, sub-status pills, |
| 6 | +// hover/active transitions, a disciplined neutral-dark palette). This module |
| 7 | +// is that stylesheet, injected once and shared by every node, so the whole |
| 8 | +// pack looks like one finished pro tool. |
| 9 | +// |
| 10 | +// USAGE: |
| 11 | +// import { ensureC2CKit } from "./_c2c_ui_kit.js"; |
| 12 | +// ensureC2CKit(); // inject the stylesheet (idempotent) |
| 13 | +// root.classList.add("c2ck"); // opt a subtree into the system |
| 14 | +// btn.className = "c2ck-btn"; // then use the classes below |
| 15 | +// |
| 16 | +// The functional accent colours a node needs (skeleton=blue, iris=red, …) |
| 17 | +// stay the node's own business — the kit only styles the CHROME. |
| 18 | +// |
| 19 | +// License: Apache-2.0 |
| 20 | + |
| 21 | +const KIT_ID = "c2ck-styles"; |
| 22 | + |
| 23 | +const CSS = ` |
| 24 | +.c2ck{--k-bg:#161616;--k-panel:#1e1e1e;--k-panel2:#222;--k-line:#111;--k-line2:#2c2c2c; |
| 25 | + --k-fg:#e6e6e6;--k-dim:#8a8a8a;--k-dim2:#666;--k-acc:#5b9dd9; |
| 26 | + font-family:ui-sans-serif,system-ui,-apple-system,sans-serif;color:var(--k-fg);box-sizing:border-box;} |
| 27 | +.c2ck *,.c2ck *::before,.c2ck *::after{box-sizing:border-box;} |
| 28 | +
|
| 29 | +/* buttons */ |
| 30 | +.c2ck-btn{background:var(--k-panel2);color:var(--k-fg);border:1px solid var(--k-line);border-radius:5px; |
| 31 | + padding:5px 11px;font-size:11px;font-weight:500;cursor:pointer;display:inline-flex;align-items:center;gap:5px; |
| 32 | + transition:background .15s ease,border-color .15s ease,transform .05s ease;} |
| 33 | +.c2ck-btn:hover:not(:disabled){background:#2e2e2e;border-color:#4a4a4a;} |
| 34 | +.c2ck-btn:active:not(:disabled){transform:translateY(1px);} |
| 35 | +.c2ck-btn:disabled{opacity:.45;cursor:not-allowed;} |
| 36 | +.c2ck-btn.on{background:#1c2733;border-color:#2f4a63;color:#cfe6ff;} |
| 37 | +.c2ck-btn-danger:hover:not(:disabled){background:#3a1717;border-color:#a44;color:#ffb4b4;} |
| 38 | +.c2ck-btn-icon{padding:5px 8px;font-size:12px;justify-content:center;min-width:28px;} |
| 39 | +.c2ck-sep{width:1px;align-self:stretch;background:var(--k-line2);margin:2px 3px;} |
| 40 | +.c2ck-select{background:var(--k-panel2);color:var(--k-fg);border:1px solid var(--k-line);border-radius:5px; |
| 41 | + padding:4px 6px;font-size:11px;cursor:pointer;} |
| 42 | +
|
| 43 | +/* toolbars / status */ |
| 44 | +.c2ck-toolbar{display:flex;gap:5px;align-items:center;flex-wrap:wrap;} |
| 45 | +.c2ck-status{font:11px ui-monospace,monospace;color:var(--k-dim);letter-spacing:.2px;} |
| 46 | +
|
| 47 | +/* panels + floating-label fields (the LTX .pr-prompt look) */ |
| 48 | +.c2ck-panel{background:var(--k-panel);border:1px solid var(--k-line);border-radius:7px;position:relative;box-sizing:border-box;} |
| 49 | +.c2ck-fieldwrap{position:relative;width:100%;background:var(--k-panel);border:1px solid var(--k-line);border-radius:7px; |
| 50 | + overflow:hidden;transition:border-color .2s ease;} |
| 51 | +.c2ck-fieldwrap:focus-within{border-color:#4d6a86;} |
| 52 | +.c2ck-flabel{position:absolute;top:6px;left:9px;font-size:9px;font-weight:700;color:var(--k-dim2); |
| 53 | + text-transform:uppercase;letter-spacing:.6px;pointer-events:none;user-select:none;z-index:2;} |
| 54 | +.c2ck-fmeta{position:absolute;top:6px;right:9px;font:9px ui-monospace,monospace;color:var(--k-dim2);pointer-events:none;z-index:2;} |
| 55 | +.c2ck-area{width:100%;background:transparent;color:var(--k-fg);border:none;padding:22px 9px 9px;resize:none; |
| 56 | + font-size:12px;line-height:1.45;outline:none;box-sizing:border-box;} |
| 57 | +.c2ck-area::placeholder{color:#555;} |
| 58 | +.c2ck-info{background:#191919;color:#bcbcbc;border:1px solid var(--k-line);border-radius:7px;padding:10px 11px; |
| 59 | + font-size:11.5px;line-height:1.6;} |
| 60 | +.c2ck-info b,.c2ck-info .hi{color:#fff;font-weight:600;} |
| 61 | +.c2ck-itag{display:block;font-size:9px;font-weight:700;color:var(--k-dim2);text-transform:uppercase; |
| 62 | + letter-spacing:.6px;margin-bottom:6px;} |
| 63 | +
|
| 64 | +/* sub-status pill (LTX 'Inpaint: ON' / 'Audio: OFF') */ |
| 65 | +.c2ck-pill{display:inline-flex;align-items:center;gap:4px;font-size:9px;font-weight:600;letter-spacing:.3px; |
| 66 | + padding:2px 7px;border-radius:999px;background:#242424;border:1px solid var(--k-line2);color:var(--k-dim);} |
| 67 | +.c2ck-pill.on{background:#12251a;border-color:#1f5b3a;color:#7fe0a6;} |
| 68 | +.c2ck-pill.off{background:#251414;border-color:#5b2020;color:#e08a8a;} |
| 69 | +
|
| 70 | +/* toggle chip */ |
| 71 | +.c2ck-chip{font-size:10.5px;padding:3px 9px;border-radius:999px;cursor:pointer;border:1px solid var(--k-line2); |
| 72 | + background:transparent;color:var(--k-dim);transition:background .12s ease,border-color .12s ease,color .12s ease;} |
| 73 | +.c2ck-chip:hover{border-color:#555;} |
| 74 | +
|
| 75 | +/* popover menu (build per-open on document.body) */ |
| 76 | +.c2ck-menu{position:fixed;z-index:2147483000;background:#1c1c1c;border:1px solid #333;border-radius:9px;padding:5px; |
| 77 | + box-shadow:0 10px 30px rgba(0,0,0,.6);display:flex;flex-direction:column;gap:1px;min-width:176px;} |
| 78 | +.c2ck-menu-head{font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.6px;color:var(--k-dim2);padding:5px 10px 3px;} |
| 79 | +.c2ck-menu-btn{display:flex;align-items:center;gap:9px;background:none;border:none;color:var(--k-fg); |
| 80 | + font:12px ui-sans-serif,system-ui;text-align:left;padding:7px 10px;border-radius:6px;cursor:pointer; |
| 81 | + transition:background .12s ease;width:100%;box-sizing:border-box;} |
| 82 | +.c2ck-menu-btn:hover:not(:disabled){background:#2c2c2c;} |
| 83 | +.c2ck-menu-btn:disabled{opacity:.4;cursor:not-allowed;} |
| 84 | +.c2ck-menu-btn .g{width:16px;text-align:center;flex:0 0 auto;color:#9aa;} |
| 85 | +.c2ck-menu-sep{height:1px;background:#2c2c2c;margin:3px 4px;} |
| 86 | +
|
| 87 | +/* range inputs adopt the accent */ |
| 88 | +.c2ck input[type=range]{accent-color:var(--k-acc);} |
| 89 | +`; |
| 90 | + |
| 91 | +/** Inject the shared stylesheet once. Safe to call from every node's setup. */ |
| 92 | +export function ensureC2CKit() { |
| 93 | + if (typeof document === "undefined") return; |
| 94 | + if (document.getElementById(KIT_ID)) return; |
| 95 | + const el = document.createElement("style"); |
| 96 | + el.id = KIT_ID; |
| 97 | + el.textContent = CSS; |
| 98 | + document.head.appendChild(el); |
| 99 | +} |
| 100 | + |
| 101 | +/** |
| 102 | + * Build a floating-label popover menu on document.body (eclipse-proof: |
| 103 | + * position:fixed, high z-index, dismiss on outside pointerdown/wheel). Returns |
| 104 | + * a `dismiss()` you should also call from the node's teardown. |
| 105 | + * |
| 106 | + * items: [{ label, glyph?, onPick, disabled?, title? } | { sep:true }] |
| 107 | + */ |
| 108 | +export function popoverMenu(clientX, clientY, { head, items } = {}) { |
| 109 | + ensureC2CKit(); |
| 110 | + const menu = document.createElement("div"); |
| 111 | + menu.className = "c2ck-menu"; |
| 112 | + if (head) { |
| 113 | + const h = document.createElement("div"); |
| 114 | + h.className = "c2ck-menu-head"; |
| 115 | + h.textContent = head; |
| 116 | + menu.appendChild(h); |
| 117 | + } |
| 118 | + for (const it of (items || [])) { |
| 119 | + if (it.sep) { const s = document.createElement("div"); s.className = "c2ck-menu-sep"; menu.appendChild(s); continue; } |
| 120 | + const b = document.createElement("button"); |
| 121 | + b.className = "c2ck-menu-btn"; |
| 122 | + b.innerHTML = `<span class="g">${it.glyph || ""}</span>${it.label}`; |
| 123 | + if (it.disabled) { b.disabled = true; if (it.title) b.title = it.title; } |
| 124 | + else b.addEventListener("pointerdown", (ev) => { ev.preventDefault(); ev.stopPropagation(); dismiss(); try { it.onPick?.(); } catch (_) {} }); |
| 125 | + menu.appendChild(b); |
| 126 | + } |
| 127 | + document.body.appendChild(menu); |
| 128 | + const vw = window.innerWidth, vh = window.innerHeight, r = menu.getBoundingClientRect(); |
| 129 | + let px = clientX + 4, py = clientY - 6; |
| 130 | + if (px + r.width > vw - 6) px = vw - r.width - 6; |
| 131 | + if (py + r.height > vh - 6) py = vh - r.height - 6; |
| 132 | + menu.style.left = Math.max(6, px) + "px"; |
| 133 | + menu.style.top = Math.max(6, py) + "px"; |
| 134 | + let onDoc = null; |
| 135 | + function dismiss() { |
| 136 | + try { menu.remove(); } catch (_) {} |
| 137 | + if (onDoc) { document.removeEventListener("pointerdown", onDoc, true); document.removeEventListener("wheel", onDoc, true); onDoc = null; } |
| 138 | + } |
| 139 | + setTimeout(() => { |
| 140 | + onDoc = (ev) => { if (!menu.contains(ev.target)) dismiss(); }; |
| 141 | + document.addEventListener("pointerdown", onDoc, true); |
| 142 | + document.addEventListener("wheel", onDoc, true); |
| 143 | + }, 0); |
| 144 | + return { menu, dismiss }; |
| 145 | +} |
0 commit comments