frontend: move shadow-DOM component CSS to real .css files, not JS strings
mara: 'i dont like sharing css via js, thats not how it should be done.'
Replaced the DIALOG_CSS/TOAST_CSS template-string constants in modal.js
and the inline CSS text in component-styles.js with three real .css
files (hive-dialog.css, hive-toast.css, component-common.css),
imported as raw text via esbuild's 'text' loader and turned into
CSSStyleSheet objects at runtime (same replaceSync() call as before --
only where the CSS text comes from changed). Both packages' build.mjs
gained a '.css': 'text' loader entry on their JS-bundling step; this
doesn't collide with the separate page-stylesheet bundling ('css'
loader), which is a different esbuild invocation over different entry
points.
No behavior change -- same adoptedStyleSheets wiring, same rules,
same output. Verified with a full frontend build (grepped the bundled
JS to confirm the CSS text inlines correctly); nix fmt clean.
This commit is contained in:
parent
d79df3883d
commit
bce666a9b2
7 changed files with 197 additions and 151 deletions
|
|
@ -9,15 +9,20 @@
|
|||
// component-scoped `CSSStyleSheet` alongside the shared one in
|
||||
// `component-styles.js` (the native equivalent of a Sass `@include`) —
|
||||
// real style encapsulation, no global `.tc-*` class namespace needed any
|
||||
// more, and no stylesheet to import from every page. Theme vars (`--bg`,
|
||||
// `--purple`, …) keep resolving inside the shadow tree automatically:
|
||||
// CSS custom properties inherit through shadow boundaries even though
|
||||
// plain class rules don't. This started as a light-DOM pilot (see the
|
||||
// frontend components-split discussion on the forge issue tracker) —
|
||||
// light DOM validated the lifecycle-encapsulation win
|
||||
// (`connectedCallback`/`disconnectedCallback` owning the keydown
|
||||
// listener / auto-dismiss timer) cheaply; this is the shadow-DOM
|
||||
// follow-up once real per-component style scoping was worth the cost.
|
||||
// more, and no stylesheet to import from every page. The CSS itself
|
||||
// lives in real `.css` files (`hive-dialog.css`, `hive-toast.css`,
|
||||
// `component-common.css`), imported here as raw text via esbuild's
|
||||
// `text` loader and turned into `CSSStyleSheet`s at runtime — not JS
|
||||
// template strings, so it stays normal, editor-tooled CSS. Theme vars
|
||||
// (`--bg`, `--purple`, …) keep resolving inside the shadow tree
|
||||
// automatically: CSS custom properties inherit through shadow
|
||||
// boundaries even though plain class rules don't. This started as a
|
||||
// light-DOM pilot (see the frontend components-split discussion on the
|
||||
// forge issue tracker) — light DOM validated the lifecycle-
|
||||
// encapsulation win (`connectedCallback`/`disconnectedCallback` owning
|
||||
// the keydown listener / auto-dismiss timer) cheaply; this is the
|
||||
// shadow-DOM follow-up once real per-component style scoping was worth
|
||||
// the cost.
|
||||
//
|
||||
// `openDialog` is the general primitive (any title/message/content + a row of
|
||||
// buttons); `themedConfirm` is a thin cancel/confirm wrapper with optional
|
||||
|
|
@ -25,90 +30,23 @@
|
|||
|
||||
import { el } from './dom.js';
|
||||
import { sharedComponentStyleSheet } from './component-styles.js';
|
||||
import dialogCss from './hive-dialog.css';
|
||||
import toastCss from './hive-toast.css';
|
||||
|
||||
// Attach an open shadow root to `host`, adopt the shared component
|
||||
// stylesheet plus `ownCss` (a scoped stylesheet built fresh per call —
|
||||
// only the shared one is cached/reused across instances), and return the
|
||||
// shadow root for the caller to populate.
|
||||
function attachShadow(host, ownCss) {
|
||||
// stylesheet plus `ownCssText` (a scoped stylesheet built fresh per call
|
||||
// from a real .css file's contents, imported as raw text — see
|
||||
// component-styles.js's header comment; only the shared one is
|
||||
// cached/reused across instances), and return the shadow root for the
|
||||
// caller to populate.
|
||||
function attachShadow(host, ownCssText) {
|
||||
const root = host.attachShadow({ mode: 'open' });
|
||||
const own = new CSSStyleSheet();
|
||||
own.replaceSync(ownCss);
|
||||
own.replaceSync(ownCssText);
|
||||
root.adoptedStyleSheets = [sharedComponentStyleSheet(), own];
|
||||
return root;
|
||||
}
|
||||
|
||||
const DIALOG_CSS = `
|
||||
:host {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1em;
|
||||
background: color-mix(in srgb, var(--crust) 60%, transparent);
|
||||
-webkit-backdrop-filter: blur(2px);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
.box {
|
||||
background: var(--bg-elev);
|
||||
border: 1px solid var(--purple-dim);
|
||||
box-shadow: 0 8px 40px -8px var(--crust);
|
||||
padding: 1.2em 1.4em;
|
||||
max-width: min(32em, 92vw);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.9em;
|
||||
}
|
||||
.title {
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--subtext0);
|
||||
}
|
||||
.message { color: var(--fg); line-height: 1.45; }
|
||||
.checks { display: flex; flex-direction: column; gap: 0.4em; }
|
||||
.checkrow {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.5em;
|
||||
cursor: pointer;
|
||||
color: var(--subtext0);
|
||||
font-size: 0.92em;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.checkrow .check { margin-top: 0.2em; flex: 0 0 auto; }
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.6em;
|
||||
margin-top: 0.2em;
|
||||
}
|
||||
.cancel { color: var(--subtext0); }
|
||||
.confirm { color: var(--green); }
|
||||
.confirm.danger { color: var(--red); }
|
||||
.promptfield { display: flex; flex-direction: column; gap: 0.4em; }
|
||||
.promptlabel { color: var(--subtext0); font-size: 0.9em; }
|
||||
.input {
|
||||
font-family: inherit;
|
||||
font-size: 1em;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
background: var(--crust);
|
||||
color: var(--fg);
|
||||
border: 1px solid var(--purple-dim);
|
||||
padding: 0.4em 0.6em;
|
||||
}
|
||||
.input:focus { outline: 1px solid var(--green); }
|
||||
.textarea {
|
||||
resize: vertical;
|
||||
min-height: 4.5em;
|
||||
line-height: 1.4;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
`;
|
||||
|
||||
// <hive-dialog> — the backdrop + box custom element behind `openDialog`.
|
||||
// Not exported; constructed and configured by `openDialog` only. Callers
|
||||
// set `._opts` before `append()`ing it (custom elements can't take
|
||||
|
|
@ -128,7 +66,7 @@ class HiveDialog extends HTMLElement {
|
|||
danger = false, dismissable = true,
|
||||
} = this._opts || {};
|
||||
|
||||
const root = attachShadow(this, DIALOG_CSS);
|
||||
const root = attachShadow(this, dialogCss);
|
||||
|
||||
let settled = false;
|
||||
const done = (value) => {
|
||||
|
|
@ -294,28 +232,6 @@ export function themedPrompt(opts = {}) {
|
|||
return result;
|
||||
}
|
||||
|
||||
const TOAST_CSS = `
|
||||
:host {
|
||||
display: block;
|
||||
pointer-events: auto;
|
||||
cursor: pointer;
|
||||
background: var(--bg-elev);
|
||||
border: 1px solid var(--purple-dim);
|
||||
border-left-width: 3px;
|
||||
box-shadow: 0 4px 20px -6px var(--crust);
|
||||
padding: 0.6em 0.9em;
|
||||
font-size: 0.9em;
|
||||
color: var(--fg);
|
||||
white-space: pre-wrap;
|
||||
opacity: 1;
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
:host(.out) { opacity: 0; transform: translateX(0.5em); }
|
||||
:host(.error) { border-left-color: var(--red); }
|
||||
:host(.info) { border-left-color: var(--purple-dim); }
|
||||
:host(.ok) { border-left-color: var(--green); }
|
||||
`;
|
||||
|
||||
// <hive-toast> — one transient notification entry. Owns its own
|
||||
// auto-dismiss timer via connectedCallback/disconnectedCallback (cleared
|
||||
// on removal so a toast dismissed early by click doesn't leave a stray
|
||||
|
|
@ -330,7 +246,7 @@ class HiveToast extends HTMLElement {
|
|||
connectedCallback() {
|
||||
const { type = 'info', duration } = this._opts || {};
|
||||
const ms = duration != null ? duration : (type === 'error' ? 8000 : 4000);
|
||||
const root = attachShadow(this, TOAST_CSS);
|
||||
const root = attachShadow(this, toastCss);
|
||||
this.classList.add(type);
|
||||
this.setAttribute('role', type === 'error' ? 'alert' : 'status');
|
||||
root.textContent = this._message || '';
|
||||
|
|
|
|||
Loading…
Reference in a new issue