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
|
|
@ -41,6 +41,10 @@ await build({
|
|||
target: ['es2022'],
|
||||
sourcemap: true,
|
||||
logLevel: 'info',
|
||||
// `@hive/shared/modal.js` imports its shadow-DOM component CSS as raw
|
||||
// text (see dashboard/build.mjs's matching comment) — same reasoning
|
||||
// applies here.
|
||||
loader: { '.css': 'text' },
|
||||
});
|
||||
|
||||
// Bundle the CSS. `colors.css` re-exports the standalone base16 palette
|
||||
|
|
|
|||
|
|
@ -74,6 +74,15 @@ await build({
|
|||
target: ['es2022'],
|
||||
sourcemap: true,
|
||||
logLevel: 'info',
|
||||
// `@hive/shared/modal.js` imports its shadow-DOM component CSS
|
||||
// (hive-dialog.css, hive-toast.css, component-common.css) as raw text
|
||||
// via a plain `import css from './foo.css'` — the `text` loader turns
|
||||
// that into a string constant at bundle time instead of erroring on an
|
||||
// unrecognised extension. None of these JS entries import a `.css`
|
||||
// file any other way, so this doesn't collide with the separate
|
||||
// page-stylesheet bundling below (`loader: { '.css': 'css' }`), which
|
||||
// runs as its own esbuild invocation over different entry points.
|
||||
loader: { '.css': 'text' },
|
||||
});
|
||||
|
||||
// Stream-worker entry (#448). Lives in a separate bundle: SharedWorker
|
||||
|
|
|
|||
46
frontend/packages/shared/src/component-common.css
Normal file
46
frontend/packages/shared/src/component-common.css
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/* component-common.css — the shared stylesheet adopted alongside every
|
||||
shadow-DOM custom element's own scoped CSS (`adoptedStyleSheets`), the
|
||||
native equivalent of a Sass `@include`. Loaded as raw text at build time
|
||||
(esbuild's `text` loader — see build.mjs) and turned into a
|
||||
`CSSStyleSheet` via `replaceSync()` in component-styles.js; a real .css
|
||||
file, not a JS template string, so it gets normal editor tooling and
|
||||
stays syntactically obvious as CSS.
|
||||
|
||||
NOTE: `.btn` here is a near-duplicate of the app-wide `.btn` base rule
|
||||
in dashboard/src/common.css and agent/src/agent.css, which already
|
||||
differ slightly from each other. Shadow DOM can't see either — a
|
||||
shadow component that wants the same button chrome needs its own copy,
|
||||
so this is now a third copy. Picked dashboard's version (the fuller
|
||||
one) as canonical for shadow components going forward. Unifying all
|
||||
three into one source of truth is a separate follow-up (see
|
||||
hyperhive's forge issue tracker: the `<hive-btn>` component proposal),
|
||||
not bundled into whichever component conversion first needed a
|
||||
button. */
|
||||
|
||||
.btn {
|
||||
font-family: inherit;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
background: transparent;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
border: 1px solid;
|
||||
padding: 0.25em 0.8em;
|
||||
cursor: pointer;
|
||||
text-shadow: 0 0 4px currentColor;
|
||||
box-shadow: 0 0 0 0 currentColor;
|
||||
transition: box-shadow 0.15s ease;
|
||||
}
|
||||
.btn:hover {
|
||||
background: color-mix(in srgb, var(--fg) 6%, transparent);
|
||||
text-shadow: 0 0 10px currentColor;
|
||||
box-shadow: 0 0 10px -2px currentColor;
|
||||
}
|
||||
.btn:disabled,
|
||||
.btn[disabled] {
|
||||
opacity: 0.32;
|
||||
cursor: not-allowed;
|
||||
text-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
|
@ -1,58 +1,30 @@
|
|||
// Shared, adopted-alongside-component-specific stylesheets for shadow-DOM
|
||||
// custom elements — the native equivalent of a Sass `@include`: a
|
||||
// `CSSStyleSheet` is parsed once and adopted by reference
|
||||
// (`shadowRoot.adoptedStyleSheets = [sharedButtonStyleSheet(), ownSheet]`)
|
||||
// (`shadowRoot.adoptedStyleSheets = [sharedComponentStyleSheet(), ownSheet]`)
|
||||
// into as many shadow roots as want it, rather than each component
|
||||
// duplicating the rule text or re-fetching an external stylesheet.
|
||||
//
|
||||
// Start small: `.btn` is the one rule shadow-DOM components have needed so
|
||||
// far (the dialog buttons in modal.js). Add more shared rules here as more
|
||||
// components need them — don't grow this into a full reset/utility layer
|
||||
// preemptively.
|
||||
// The CSS itself lives in `component-common.css` — a real stylesheet, not
|
||||
// a JS template string — imported here as raw text via esbuild's `text`
|
||||
// loader (see both packages' `build.mjs`) and turned into a
|
||||
// `CSSStyleSheet` at module-init time. Component-scoped stylesheets
|
||||
// (`hive-dialog.css`, `hive-toast.css` in modal.js) follow the same
|
||||
// pattern.
|
||||
//
|
||||
// NOTE: this `.btn` is a near-duplicate of the app-wide `.btn` base rule in
|
||||
// dashboard/src/common.css and agent/src/agent.css, which already differ
|
||||
// slightly from each other (agent's is a simpler subset). Shadow DOM can't
|
||||
// see either — a shadow component that wants the same button chrome needs
|
||||
// its own copy, so this is now a third copy. Picked dashboard's version
|
||||
// (the fuller one) as canonical for shadow components going forward.
|
||||
// Unifying all three into one source of truth is a separate follow-up, not
|
||||
// bundled into whichever component conversion first needed a button.
|
||||
// Start small: `.btn` is the one rule shadow-DOM components have needed so
|
||||
// far (the dialog buttons in modal.js). Add more shared rules to
|
||||
// component-common.css as more components need them — don't grow this
|
||||
// into a full reset/utility layer preemptively.
|
||||
|
||||
import componentCommonCss from './component-common.css';
|
||||
|
||||
let sheet = null;
|
||||
|
||||
export function sharedComponentStyleSheet() {
|
||||
if (!sheet) {
|
||||
sheet = new CSSStyleSheet();
|
||||
sheet.replaceSync(`
|
||||
.btn {
|
||||
font-family: inherit;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
background: transparent;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
border: 1px solid;
|
||||
padding: 0.25em 0.8em;
|
||||
cursor: pointer;
|
||||
text-shadow: 0 0 4px currentColor;
|
||||
box-shadow: 0 0 0 0 currentColor;
|
||||
transition: box-shadow 0.15s ease;
|
||||
}
|
||||
.btn:hover {
|
||||
background: color-mix(in srgb, var(--fg) 6%, transparent);
|
||||
text-shadow: 0 0 10px currentColor;
|
||||
box-shadow: 0 0 10px -2px currentColor;
|
||||
}
|
||||
.btn:disabled,
|
||||
.btn[disabled] {
|
||||
opacity: 0.32;
|
||||
cursor: not-allowed;
|
||||
text-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
`);
|
||||
sheet.replaceSync(componentCommonCss);
|
||||
}
|
||||
return sheet;
|
||||
}
|
||||
|
|
|
|||
74
frontend/packages/shared/src/hive-dialog.css
Normal file
74
frontend/packages/shared/src/hive-dialog.css
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
/* hive-dialog.css — scoped stylesheet for the <hive-dialog> shadow-DOM
|
||||
custom element (modal.js). Loaded as raw text at build time (esbuild's
|
||||
`text` loader) and adopted via `adoptedStyleSheets` alongside
|
||||
component-common.css. `:host` styles the element itself, which *is*
|
||||
the fixed-position backdrop — no separate light-DOM backdrop div. */
|
||||
|
||||
: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;
|
||||
}
|
||||
25
frontend/packages/shared/src/hive-toast.css
Normal file
25
frontend/packages/shared/src/hive-toast.css
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/* hive-toast.css — scoped stylesheet for the <hive-toast> shadow-DOM
|
||||
custom element (modal.js). Loaded as raw text at build time (esbuild's
|
||||
`text` loader) and adopted via `adoptedStyleSheets`. `:host` styles the
|
||||
element itself, which *is* the visible toast box; `:host(.error)` etc.
|
||||
switch on a plain class the host element carries (set in JS). */
|
||||
|
||||
: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); }
|
||||
|
|
@ -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