@danieldeusing/design 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.
- package/LICENSE +21 -0
- package/README.md +168 -0
- package/dist/danieldeusing-design.css +554 -0
- package/dist/danieldeusing-design.min.css +2 -0
- package/package.json +65 -0
- package/runtime/dropdown.js +44 -0
- package/runtime/index.js +16 -0
- package/runtime/terminal.js +120 -0
- package/runtime/theme.js +107 -0
- package/runtime/zoom.js +25 -0
- package/src/base.css +68 -0
- package/src/components.css +288 -0
- package/src/fonts.css +40 -0
- package/src/index.css +19 -0
- package/src/reset.css +74 -0
- package/src/tailwind.css +48 -0
- package/src/tokens.css +116 -0
- package/tokens/tokens.json +100 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* danieldeusing-design — terminal session animation.
|
|
3
|
+
*
|
|
4
|
+
* Progressive enhancement for the "live shell" effect: `$ command` prompts type
|
|
5
|
+
* out character by character, then their section's output reveals like the
|
|
6
|
+
* command just ran. Sections play in document order; a section scrolled into
|
|
7
|
+
* view later animates on arrival.
|
|
8
|
+
*
|
|
9
|
+
* Markup contract (styled by components.css):
|
|
10
|
+
* <section data-term>
|
|
11
|
+
* <p class="prompt">cat about.txt</p> <!-- the command that types out -->
|
|
12
|
+
* <div data-term-out>…</div> <!-- output, revealed after -->
|
|
13
|
+
* </section>
|
|
14
|
+
*
|
|
15
|
+
* Honours prefers-reduced-motion: with reduced motion (or JS disabled) the
|
|
16
|
+
* html.term-anim gate is never added, so components.css leaves everything
|
|
17
|
+
* visible. initTerminal() is therefore safe to call unconditionally.
|
|
18
|
+
*
|
|
19
|
+
* After the initial on-screen sections finish, it sets window.termContentDone
|
|
20
|
+
* and dispatches a "term:contentdone" event — a hook for chaining a follow-up
|
|
21
|
+
* animation (e.g. a nav that "runs" once the page has finished printing).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export function initTerminal() {
|
|
25
|
+
if (typeof window === "undefined") return;
|
|
26
|
+
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
|
27
|
+
// honour the manual kill-switch (components.css html.anim-off)
|
|
28
|
+
if (document.documentElement.classList.contains("anim-off")) return;
|
|
29
|
+
|
|
30
|
+
document.documentElement.classList.add("term-anim");
|
|
31
|
+
|
|
32
|
+
const typePrompt = (prompt) =>
|
|
33
|
+
new Promise((finished) => {
|
|
34
|
+
const text = (prompt.textContent || "").trim();
|
|
35
|
+
const typedText = document.createTextNode("");
|
|
36
|
+
const caret = document.createElement("span");
|
|
37
|
+
caret.className = "term-caret";
|
|
38
|
+
caret.setAttribute("aria-hidden", "true");
|
|
39
|
+
prompt.textContent = "";
|
|
40
|
+
prompt.classList.add("term-live");
|
|
41
|
+
prompt.append(typedText, caret);
|
|
42
|
+
// ~35ms per character, capped so long commands finish within ~700ms
|
|
43
|
+
const perCharMs = Math.min(38, 700 / Math.max(text.length, 1));
|
|
44
|
+
let typedCount = 0;
|
|
45
|
+
const typeNext = () => {
|
|
46
|
+
if (typedCount < text.length) {
|
|
47
|
+
typedCount += 1;
|
|
48
|
+
typedText.data = text.slice(0, typedCount);
|
|
49
|
+
setTimeout(typeNext, perCharMs * (0.75 + Math.random() * 0.5));
|
|
50
|
+
} else {
|
|
51
|
+
caret.remove();
|
|
52
|
+
finished();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
typeNext();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const revealOutputs = (section, instant) =>
|
|
59
|
+
new Promise((finished) => {
|
|
60
|
+
const chunks = section.querySelectorAll("[data-term-out]");
|
|
61
|
+
const stagger = (index) => Math.min(index * 70, 280);
|
|
62
|
+
chunks.forEach((chunk, index) => {
|
|
63
|
+
if (instant) chunk.classList.add("term-show");
|
|
64
|
+
else setTimeout(() => chunk.classList.add("term-show"), stagger(index));
|
|
65
|
+
});
|
|
66
|
+
if (instant) finished();
|
|
67
|
+
else setTimeout(finished, stagger(chunks.length - 1) + 200);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const playSection = async (section) => {
|
|
71
|
+
const rect = section.getBoundingClientRect();
|
|
72
|
+
const offscreen = rect.bottom < 0 || rect.top > window.innerHeight;
|
|
73
|
+
const prompt = section.querySelector(".prompt");
|
|
74
|
+
if (prompt) {
|
|
75
|
+
if (offscreen) prompt.classList.add("term-live");
|
|
76
|
+
else await typePrompt(prompt);
|
|
77
|
+
}
|
|
78
|
+
await revealOutputs(section, offscreen);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// sections play one after another, in the order they were triggered
|
|
82
|
+
let sequence = Promise.resolve();
|
|
83
|
+
const enqueue = (section) => {
|
|
84
|
+
sequence = sequence.then(() => playSection(section));
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const start = () => {
|
|
88
|
+
const observer = new IntersectionObserver(
|
|
89
|
+
(entries) => {
|
|
90
|
+
entries.forEach((entry) => {
|
|
91
|
+
if (entry.isIntersecting) {
|
|
92
|
+
observer.unobserve(entry.target);
|
|
93
|
+
enqueue(entry.target);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
{ rootMargin: "0px 0px -10% 0px" },
|
|
98
|
+
);
|
|
99
|
+
// keep a rooted reference: some engines GC observers held only by their targets
|
|
100
|
+
window.termAnimObserver = observer;
|
|
101
|
+
document.querySelectorAll("[data-term]").forEach((section) => {
|
|
102
|
+
// in or above the viewport: part of the load sequence (playSection reveals
|
|
103
|
+
// already-passed sections instantly); below: animate on scroll-in
|
|
104
|
+
if (section.getBoundingClientRect().top < window.innerHeight) enqueue(section);
|
|
105
|
+
else observer.observe(section);
|
|
106
|
+
});
|
|
107
|
+
// once the initial on-screen content has finished, signal it so callers can
|
|
108
|
+
// chain a follow-up (e.g. a nav that "runs" afterwards)
|
|
109
|
+
sequence = sequence.then(() => {
|
|
110
|
+
window.termContentDone = true;
|
|
111
|
+
window.dispatchEvent(new Event("term:contentdone"));
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
if (document.readyState === "loading") {
|
|
116
|
+
document.addEventListener("DOMContentLoaded", start);
|
|
117
|
+
} else {
|
|
118
|
+
start();
|
|
119
|
+
}
|
|
120
|
+
}
|
package/runtime/theme.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* danieldeusing-design — theme runtime.
|
|
3
|
+
*
|
|
4
|
+
* Framework-agnostic theme switching for the four terminal variants. No
|
|
5
|
+
* dependencies. The active theme lives in html[data-theme] and persists in
|
|
6
|
+
* localStorage under "theme".
|
|
7
|
+
*
|
|
8
|
+
* To avoid a flash of the wrong theme, call applyStoredTheme() from an inline
|
|
9
|
+
* <head> script BEFORE first paint (see the snippet in the README). Then call
|
|
10
|
+
* initThemeSwitcher() after the DOM is ready to wire up the toggle controls.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** @typedef {"warm" | "green" | "mono" | "paper"} Theme */
|
|
14
|
+
|
|
15
|
+
/** All available themes, in menu order. */
|
|
16
|
+
export const THEMES = /** @type {const} */ (["warm", "green", "mono", "paper"]);
|
|
17
|
+
|
|
18
|
+
/** Each theme's background, mirrored into <meta name="theme-color"> for mobile chrome. */
|
|
19
|
+
export const THEME_BACKGROUNDS = {
|
|
20
|
+
warm: "#f5efe2",
|
|
21
|
+
green: "#020604",
|
|
22
|
+
mono: "#050505",
|
|
23
|
+
paper: "#fafafa",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const STORAGE_KEY = "theme";
|
|
27
|
+
const DEFAULT_THEME = "warm";
|
|
28
|
+
|
|
29
|
+
/** The persisted theme, or "warm" if none/invalid. */
|
|
30
|
+
export function getStoredTheme() {
|
|
31
|
+
try {
|
|
32
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
33
|
+
if (stored && THEMES.includes(/** @type {Theme} */ (stored))) return stored;
|
|
34
|
+
} catch {
|
|
35
|
+
/* localStorage can throw in private mode / sandboxed frames */
|
|
36
|
+
}
|
|
37
|
+
return DEFAULT_THEME;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Apply a theme to <html> and sync the meta theme-color. Does NOT persist —
|
|
42
|
+
* use setTheme() for that.
|
|
43
|
+
*
|
|
44
|
+
* @param {Theme} theme
|
|
45
|
+
* @param {{ faviconHref?: (theme: Theme) => string }} [options]
|
|
46
|
+
* faviconHref, if given, also updates <link id="favicon"> — useful for apps
|
|
47
|
+
* that ship a per-theme favicon. Omit to leave the favicon untouched.
|
|
48
|
+
*/
|
|
49
|
+
export function applyTheme(theme, options = {}) {
|
|
50
|
+
const root = document.documentElement;
|
|
51
|
+
root.dataset.theme = theme;
|
|
52
|
+
document
|
|
53
|
+
.querySelector('meta[name="theme-color"]')
|
|
54
|
+
?.setAttribute("content", THEME_BACKGROUNDS[theme] ?? THEME_BACKGROUNDS[DEFAULT_THEME]);
|
|
55
|
+
if (options.faviconHref) {
|
|
56
|
+
document.getElementById("favicon")?.setAttribute("href", options.faviconHref(theme));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Persist and apply a theme. No-op for an unknown theme name.
|
|
62
|
+
* @param {Theme} theme
|
|
63
|
+
* @param {{ faviconHref?: (theme: Theme) => string }} [options]
|
|
64
|
+
*/
|
|
65
|
+
export function setTheme(theme, options) {
|
|
66
|
+
if (!THEMES.includes(theme)) return;
|
|
67
|
+
try {
|
|
68
|
+
localStorage.setItem(STORAGE_KEY, theme);
|
|
69
|
+
} catch {
|
|
70
|
+
/* ignore */
|
|
71
|
+
}
|
|
72
|
+
applyTheme(theme, options);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Apply the persisted theme. Call this pre-paint (inline, in <head>) to prevent
|
|
77
|
+
* a flash of the default theme on load.
|
|
78
|
+
* @param {{ faviconHref?: (theme: Theme) => string }} [options]
|
|
79
|
+
*/
|
|
80
|
+
export function applyStoredTheme(options) {
|
|
81
|
+
applyTheme(getStoredTheme(), options);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Wire up a theme switcher built from the standard markup:
|
|
86
|
+
* <button data-theme-value="green">green</button> (one per theme)
|
|
87
|
+
* <span data-theme-label></span> (shows the active theme)
|
|
88
|
+
* @param {{ faviconHref?: (theme: Theme) => string }} [options]
|
|
89
|
+
*/
|
|
90
|
+
export function initThemeSwitcher(options) {
|
|
91
|
+
const labels = document.querySelectorAll("[data-theme-label]");
|
|
92
|
+
const syncLabel = () => {
|
|
93
|
+
const theme = document.documentElement.dataset.theme ?? DEFAULT_THEME;
|
|
94
|
+
labels.forEach((label) => {
|
|
95
|
+
label.textContent = theme;
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
syncLabel();
|
|
99
|
+
|
|
100
|
+
document.querySelectorAll("[data-theme-value]").forEach((button) => {
|
|
101
|
+
button.addEventListener("click", () => {
|
|
102
|
+
const theme = /** @type {HTMLElement} */ (button).dataset.themeValue;
|
|
103
|
+
if (theme) setTheme(/** @type {Theme} */ (theme), options);
|
|
104
|
+
syncLabel();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
}
|
package/runtime/zoom.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* danieldeusing-design — resolution-independent zoom.
|
|
3
|
+
*
|
|
4
|
+
* Lays the page out at a fixed reference width, then scales the whole view up
|
|
5
|
+
* to fill wider screens — so 2K / 4K / 8K render the identical layout, just
|
|
6
|
+
* bigger (a true zoom of everything, not font scaling). Never scales below 1×,
|
|
7
|
+
* so screens up to the reference width keep their normal responsive layout.
|
|
8
|
+
*
|
|
9
|
+
* Apply pre-paint (inline, in <head>) to avoid a flash at the unscaled size.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {number} [referenceWidth=1920] The width the layout is authored for.
|
|
14
|
+
* On a viewport wider than this, the page is zoomed by viewport/reference.
|
|
15
|
+
*/
|
|
16
|
+
export function initResolutionZoom(referenceWidth = 1920) {
|
|
17
|
+
const applyZoom = () => {
|
|
18
|
+
// `zoom` is the right primitive here: unlike transform: scale it reflows
|
|
19
|
+
// and keeps the document height correct. Widely supported in evergreen
|
|
20
|
+
// browsers as of 2024.
|
|
21
|
+
document.documentElement.style.zoom = String(Math.max(1, window.innerWidth / referenceWidth));
|
|
22
|
+
};
|
|
23
|
+
applyZoom();
|
|
24
|
+
window.addEventListener("resize", applyZoom);
|
|
25
|
+
}
|
package/src/base.css
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* danieldeusing-design — base layer.
|
|
3
|
+
*
|
|
4
|
+
* The document-level look of the terminal theme: monospace body, CRT scanline
|
|
5
|
+
* overlay, phosphor selection, themed default border colour. Resolves against
|
|
6
|
+
* tokens.css (load that first). Plain CSS — no Tailwind required.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
html {
|
|
10
|
+
scroll-behavior: smooth;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* Default any element's border to the themed colour, so a bare `border-width`
|
|
14
|
+
(or a Tailwind `border` utility) paints in the theme border rather than the
|
|
15
|
+
element's currentColor. Mirrors the canonical `* { @apply border-border }`. */
|
|
16
|
+
*,
|
|
17
|
+
*::before,
|
|
18
|
+
*::after {
|
|
19
|
+
border-color: var(--border);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* Visible, on-theme focus ring for keyboard navigation. */
|
|
23
|
+
:focus-visible {
|
|
24
|
+
outline: 2px solid var(--ring);
|
|
25
|
+
outline-offset: 2px;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
body {
|
|
29
|
+
background: var(--background);
|
|
30
|
+
color: var(--foreground);
|
|
31
|
+
font-family: var(--font-mono);
|
|
32
|
+
font-size: 0.75rem;
|
|
33
|
+
line-height: 1.5;
|
|
34
|
+
overflow-x: hidden;
|
|
35
|
+
-webkit-font-smoothing: antialiased;
|
|
36
|
+
-moz-osx-font-smoothing: grayscale;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* Tailwind v4's reset drops the default button cursor — restore it for all
|
|
40
|
+
interactive controls (buttons, summaries, role=button). */
|
|
41
|
+
button:not(:disabled),
|
|
42
|
+
summary,
|
|
43
|
+
[role="button"] {
|
|
44
|
+
cursor: pointer;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* CRT scanlines: a fixed, non-interactive overlay above the page. Intensity is
|
|
48
|
+
per-theme via --scanline-opacity. */
|
|
49
|
+
body::after {
|
|
50
|
+
content: "";
|
|
51
|
+
position: fixed;
|
|
52
|
+
inset: 0;
|
|
53
|
+
z-index: 40;
|
|
54
|
+
pointer-events: none;
|
|
55
|
+
background: repeating-linear-gradient(
|
|
56
|
+
0deg,
|
|
57
|
+
rgba(0, 0, 0, 0.25) 0px,
|
|
58
|
+
rgba(0, 0, 0, 0.25) 1px,
|
|
59
|
+
transparent 1px,
|
|
60
|
+
transparent 3px
|
|
61
|
+
);
|
|
62
|
+
opacity: var(--scanline-opacity);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
::selection {
|
|
66
|
+
background: var(--primary);
|
|
67
|
+
color: var(--primary-foreground);
|
|
68
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* danieldeusing-design — component layer.
|
|
3
|
+
*
|
|
4
|
+
* Reusable, framework-agnostic primitives in plain CSS (no Tailwind @apply), so
|
|
5
|
+
* they render identically in an Astro/React/Angular app, a Tauri webview, or a
|
|
6
|
+
* single static HTML file. Every value resolves against tokens.css.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/* ─────────────────────────────────────────────────────────────────────────
|
|
10
|
+
Phosphor glow — for bright, "lit" elements (brand mark, active items).
|
|
11
|
+
───────────────────────────────────────────────────────────────────────── */
|
|
12
|
+
.glow {
|
|
13
|
+
color: var(--primary);
|
|
14
|
+
text-shadow: 0 0 8px var(--glow);
|
|
15
|
+
}
|
|
16
|
+
.glow-lg {
|
|
17
|
+
color: var(--primary);
|
|
18
|
+
text-shadow: 0 0 12px var(--glow);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/* ─────────────────────────────────────────────────────────────────────────
|
|
22
|
+
Shell affordances — `$ command` section headers and `# comment` lines.
|
|
23
|
+
───────────────────────────────────────────────────────────────────────── */
|
|
24
|
+
.prompt {
|
|
25
|
+
font-size: 0.75rem;
|
|
26
|
+
font-weight: 700;
|
|
27
|
+
color: var(--muted-foreground);
|
|
28
|
+
}
|
|
29
|
+
.prompt::before {
|
|
30
|
+
content: "$ ";
|
|
31
|
+
color: var(--primary);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.comment::before {
|
|
35
|
+
content: "# ";
|
|
36
|
+
color: var(--border);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* Blinking block cursor — pair with a brand mark or end-of-line. */
|
|
40
|
+
.cursor-block {
|
|
41
|
+
display: inline-block;
|
|
42
|
+
width: 0.55em;
|
|
43
|
+
height: 1em;
|
|
44
|
+
margin-left: 8px;
|
|
45
|
+
background: var(--primary);
|
|
46
|
+
vertical-align: baseline;
|
|
47
|
+
transform: translateY(0.12em);
|
|
48
|
+
animation: ddd-blink 1.1s step-end infinite;
|
|
49
|
+
}
|
|
50
|
+
@keyframes ddd-blink {
|
|
51
|
+
50% {
|
|
52
|
+
opacity: 0;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* ─────────────────────────────────────────────────────────────────────────
|
|
57
|
+
Primary CTA — `> label` with a phosphor glow that lifts on hover.
|
|
58
|
+
───────────────────────────────────────────────────────────────────────── */
|
|
59
|
+
.btn-terminal {
|
|
60
|
+
display: inline-block;
|
|
61
|
+
background: var(--primary);
|
|
62
|
+
color: var(--primary-foreground);
|
|
63
|
+
font-weight: 700;
|
|
64
|
+
padding: 12px 24px;
|
|
65
|
+
box-shadow: 0 0 16px var(--glow);
|
|
66
|
+
transition:
|
|
67
|
+
box-shadow 0.15s ease,
|
|
68
|
+
transform 0.15s ease;
|
|
69
|
+
}
|
|
70
|
+
.btn-terminal::before {
|
|
71
|
+
content: "> ";
|
|
72
|
+
}
|
|
73
|
+
.btn-terminal:hover {
|
|
74
|
+
box-shadow: 0 0 28px var(--glow);
|
|
75
|
+
transform: translateY(-2px);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* Quiet, underlined link that warms to the primary colour on hover. */
|
|
79
|
+
.link-quiet {
|
|
80
|
+
color: var(--muted-foreground);
|
|
81
|
+
text-decoration: underline;
|
|
82
|
+
text-underline-offset: 4px;
|
|
83
|
+
transition: color 0.15s ease;
|
|
84
|
+
}
|
|
85
|
+
.link-quiet:hover {
|
|
86
|
+
color: var(--primary);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* ASCII horizontal rule — a clipped, full-width row of box-drawing dashes that
|
|
90
|
+
reads like a terminal separator. Use on an <hr> or <div>; the dashes are a
|
|
91
|
+
decorative ::before clipped to the element width. */
|
|
92
|
+
.ascii-rule {
|
|
93
|
+
margin: 1rem 0;
|
|
94
|
+
border: 0;
|
|
95
|
+
color: var(--border);
|
|
96
|
+
line-height: 1;
|
|
97
|
+
white-space: nowrap;
|
|
98
|
+
overflow: hidden;
|
|
99
|
+
user-select: none;
|
|
100
|
+
}
|
|
101
|
+
.ascii-rule::before {
|
|
102
|
+
content: "────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* ─────────────────────────────────────────────────────────────────────────
|
|
106
|
+
Card — bordered surface that warms its border and glows softly on hover.
|
|
107
|
+
───────────────────────────────────────────────────────────────────────── */
|
|
108
|
+
.card-terminal {
|
|
109
|
+
background: var(--card);
|
|
110
|
+
border: 1px solid var(--border);
|
|
111
|
+
transition:
|
|
112
|
+
border-color 0.15s ease,
|
|
113
|
+
box-shadow 0.15s ease;
|
|
114
|
+
}
|
|
115
|
+
.card-terminal:hover {
|
|
116
|
+
border-color: var(--primary);
|
|
117
|
+
box-shadow: 0 0 20px var(--glow-soft);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* ─────────────────────────────────────────────────────────────────────────
|
|
121
|
+
Dropdown — a <details class="dropdown"> with a <summary> trigger and a
|
|
122
|
+
floating panel. Pairs with runtime/dropdown.js (close-others, click-away,
|
|
123
|
+
Escape). The panel opens UPWARD by default (for status-bar / footer menus);
|
|
124
|
+
add .dropdown-panel--down to open below the trigger.
|
|
125
|
+
───────────────────────────────────────────────────────────────────────── */
|
|
126
|
+
.dropdown {
|
|
127
|
+
position: relative;
|
|
128
|
+
}
|
|
129
|
+
.dropdown > summary {
|
|
130
|
+
display: inline-flex;
|
|
131
|
+
align-items: center;
|
|
132
|
+
gap: 6px;
|
|
133
|
+
cursor: pointer;
|
|
134
|
+
user-select: none;
|
|
135
|
+
list-style: none;
|
|
136
|
+
color: var(--muted-foreground);
|
|
137
|
+
transition: color 0.15s ease;
|
|
138
|
+
}
|
|
139
|
+
.dropdown > summary::-webkit-details-marker {
|
|
140
|
+
display: none;
|
|
141
|
+
}
|
|
142
|
+
.dropdown > summary:hover,
|
|
143
|
+
.dropdown[open] > summary {
|
|
144
|
+
color: var(--primary);
|
|
145
|
+
}
|
|
146
|
+
.dropdown-panel {
|
|
147
|
+
position: absolute;
|
|
148
|
+
right: 0;
|
|
149
|
+
bottom: calc(100% + 12px);
|
|
150
|
+
z-index: 50;
|
|
151
|
+
min-width: 128px;
|
|
152
|
+
margin: 0;
|
|
153
|
+
padding: 4px 0;
|
|
154
|
+
list-style: none;
|
|
155
|
+
background: var(--card);
|
|
156
|
+
border: 1px solid var(--border);
|
|
157
|
+
border-radius: 0;
|
|
158
|
+
box-shadow: 0 0 20px var(--glow-soft);
|
|
159
|
+
}
|
|
160
|
+
/* open below the trigger instead of above */
|
|
161
|
+
.dropdown-panel--down {
|
|
162
|
+
top: calc(100% + 8px);
|
|
163
|
+
bottom: auto;
|
|
164
|
+
left: 0;
|
|
165
|
+
right: auto;
|
|
166
|
+
}
|
|
167
|
+
.dropdown-item {
|
|
168
|
+
display: flex;
|
|
169
|
+
align-items: center;
|
|
170
|
+
gap: 8px;
|
|
171
|
+
width: 100%;
|
|
172
|
+
padding: 5px 14px;
|
|
173
|
+
text-align: left;
|
|
174
|
+
font: inherit;
|
|
175
|
+
line-height: 1.5;
|
|
176
|
+
background: none;
|
|
177
|
+
border: 0;
|
|
178
|
+
color: var(--muted-foreground);
|
|
179
|
+
cursor: pointer;
|
|
180
|
+
white-space: nowrap;
|
|
181
|
+
transition: color 0.15s ease;
|
|
182
|
+
}
|
|
183
|
+
.dropdown-item:hover {
|
|
184
|
+
color: var(--primary);
|
|
185
|
+
background: var(--muted);
|
|
186
|
+
}
|
|
187
|
+
.dropdown-item[aria-current="true"],
|
|
188
|
+
html:not([data-theme]) .dropdown-item[data-theme-value="warm"],
|
|
189
|
+
html[data-theme="green"] .dropdown-item[data-theme-value="green"],
|
|
190
|
+
html[data-theme="mono"] .dropdown-item[data-theme-value="mono"],
|
|
191
|
+
html[data-theme="paper"] .dropdown-item[data-theme-value="paper"],
|
|
192
|
+
html[data-theme="warm"] .dropdown-item[data-theme-value="warm"] {
|
|
193
|
+
color: var(--primary);
|
|
194
|
+
text-shadow: 0 0 8px var(--glow);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* ─────────────────────────────────────────────────────────────────────────
|
|
198
|
+
ELI5 — an "explain like I'm 5" callout box (first-time-term explainers).
|
|
199
|
+
───────────────────────────────────────────────────────────────────────── */
|
|
200
|
+
.eli5 {
|
|
201
|
+
margin: 1.25em 0;
|
|
202
|
+
padding: 0.7em 1em;
|
|
203
|
+
color: var(--foreground);
|
|
204
|
+
background: color-mix(in srgb, var(--primary) 7%, var(--card));
|
|
205
|
+
border: 1px solid var(--border);
|
|
206
|
+
border-left: 3px solid var(--primary);
|
|
207
|
+
border-radius: var(--radius);
|
|
208
|
+
font-size: 0.92em;
|
|
209
|
+
line-height: 1.6;
|
|
210
|
+
}
|
|
211
|
+
.eli5::before {
|
|
212
|
+
content: "ELI5";
|
|
213
|
+
display: inline-flex;
|
|
214
|
+
align-items: center;
|
|
215
|
+
justify-content: center;
|
|
216
|
+
margin-right: 0.6em;
|
|
217
|
+
vertical-align: 0.08em;
|
|
218
|
+
font-family: var(--font-mono);
|
|
219
|
+
font-size: 0.72em;
|
|
220
|
+
font-weight: 700;
|
|
221
|
+
line-height: 1;
|
|
222
|
+
letter-spacing: 0.04em;
|
|
223
|
+
padding: 0.34em 0.6em;
|
|
224
|
+
border-radius: 5px;
|
|
225
|
+
background: #b42318;
|
|
226
|
+
color: #fff;
|
|
227
|
+
}
|
|
228
|
+
.eli5-term {
|
|
229
|
+
font-weight: 700;
|
|
230
|
+
color: var(--primary);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/* ─────────────────────────────────────────────────────────────────────────
|
|
234
|
+
Animation kill-switch — add html.anim-off (e.g. from a footer "animations
|
|
235
|
+
off" toggle) to stop ALL motion regardless of the OS reduced-motion setting.
|
|
236
|
+
runtime/terminal.js also bails when this class is present, so content stays
|
|
237
|
+
visible. Complements the prefers-reduced-motion handling below.
|
|
238
|
+
───────────────────────────────────────────────────────────────────────── */
|
|
239
|
+
html.anim-off *,
|
|
240
|
+
html.anim-off *::before,
|
|
241
|
+
html.anim-off *::after {
|
|
242
|
+
animation: none !important;
|
|
243
|
+
transition: none !important;
|
|
244
|
+
}
|
|
245
|
+
html.anim-off .cursor-block {
|
|
246
|
+
opacity: 1;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/* ─────────────────────────────────────────────────────────────────────────
|
|
250
|
+
Terminal session animation — `$ command` prompts type out, then their
|
|
251
|
+
section's content reveals like command output. Pairs with
|
|
252
|
+
runtime/terminal.js, which adds html.term-anim pre-paint. With JS disabled
|
|
253
|
+
or prefers-reduced-motion, the gate is never added and nothing is hidden, so
|
|
254
|
+
all content is visible by default (progressive enhancement).
|
|
255
|
+
|
|
256
|
+
Markup: a section gets [data-term]; its prompt uses .prompt; each chunk of
|
|
257
|
+
output gets [data-term-out].
|
|
258
|
+
───────────────────────────────────────────────────────────────────────── */
|
|
259
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
260
|
+
/* pending prompt: fully invisible (incl. the `$ ` prefix), line box reserved */
|
|
261
|
+
html.term-anim [data-term] .prompt:not(.term-live),
|
|
262
|
+
html.term-anim [data-term] .prompt:not(.term-live)::before {
|
|
263
|
+
color: transparent;
|
|
264
|
+
}
|
|
265
|
+
/* pending output: keep layout (no CLS), no interaction until revealed */
|
|
266
|
+
html.term-anim [data-term] [data-term-out]:not(.term-show) {
|
|
267
|
+
visibility: hidden;
|
|
268
|
+
}
|
|
269
|
+
/* phosphor warm-up flicker as output appears */
|
|
270
|
+
html.term-anim [data-term] [data-term-out].term-show {
|
|
271
|
+
animation: ddd-term-output 0.2s steps(3, end) both;
|
|
272
|
+
}
|
|
273
|
+
@keyframes ddd-term-output {
|
|
274
|
+
from {
|
|
275
|
+
opacity: 0;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/* caret at the typing edge (created by runtime/terminal.js) */
|
|
279
|
+
.term-caret {
|
|
280
|
+
display: inline-block;
|
|
281
|
+
width: 0.55em;
|
|
282
|
+
height: 1em;
|
|
283
|
+
margin-left: 2px;
|
|
284
|
+
background: var(--primary);
|
|
285
|
+
vertical-align: baseline;
|
|
286
|
+
transform: translateY(0.12em);
|
|
287
|
+
}
|
|
288
|
+
}
|
package/src/fonts.css
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* danieldeusing-design — JetBrains Mono webfont (optional).
|
|
3
|
+
*
|
|
4
|
+
* The token stack leads with "JetBrains Mono Variable", which the apps self-host
|
|
5
|
+
* via @fontsource-variable/jetbrains-mono. On a build-free one-page HTML doc
|
|
6
|
+
* there is no bundler to provide it, so the stack silently falls back to Menlo.
|
|
7
|
+
* Import this file (or <link> it) to pull the real variable font from a pinned
|
|
8
|
+
* CDN so one-pagers are pixel-identical to the apps.
|
|
9
|
+
*
|
|
10
|
+
* <link rel="stylesheet" href=".../src/fonts.css"> (CDN)
|
|
11
|
+
* @import "@danieldeusing/design/fonts.css"; (bundler)
|
|
12
|
+
*
|
|
13
|
+
* Deliberately NOT part of index.css / the dist bundle — that keeps the core
|
|
14
|
+
* lean and fully offline. Subsets: latin + latin-ext (covers EN/DE/ES/PT).
|
|
15
|
+
* Pinned to @fontsource-variable/jetbrains-mono@5.2.8 (SIL OFL licensed).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/* latin */
|
|
19
|
+
@font-face {
|
|
20
|
+
font-family: "JetBrains Mono Variable";
|
|
21
|
+
font-style: normal;
|
|
22
|
+
font-display: swap;
|
|
23
|
+
font-weight: 100 800;
|
|
24
|
+
src: url("https://cdn.jsdelivr.net/npm/@fontsource-variable/jetbrains-mono@5.2.8/files/jetbrains-mono-latin-wght-normal.woff2")
|
|
25
|
+
format("woff2-variations");
|
|
26
|
+
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308,
|
|
27
|
+
U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* latin-ext */
|
|
31
|
+
@font-face {
|
|
32
|
+
font-family: "JetBrains Mono Variable";
|
|
33
|
+
font-style: normal;
|
|
34
|
+
font-display: swap;
|
|
35
|
+
font-weight: 100 800;
|
|
36
|
+
src: url("https://cdn.jsdelivr.net/npm/@fontsource-variable/jetbrains-mono@5.2.8/files/jetbrains-mono-latin-ext-wght-normal.woff2")
|
|
37
|
+
format("woff2-variations");
|
|
38
|
+
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+1D00-1DBF, U+1E00-1E9F,
|
|
39
|
+
U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
|
40
|
+
}
|
package/src/index.css
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* danieldeusing-design — build-free bundle.
|
|
3
|
+
*
|
|
4
|
+
* The no-Tailwind entry point. Import this one file and you get the full
|
|
5
|
+
* terminal theme: a minimal reset, the four-theme token set, the document base,
|
|
6
|
+
* and every component primitive. Works in plain HTML, React, Angular, Vue, a
|
|
7
|
+
* Tauri webview — anywhere CSS @import is understood, with no build step.
|
|
8
|
+
*
|
|
9
|
+
* import "@danieldeusing/design"; // package "." export
|
|
10
|
+
* <link rel="stylesheet" href=".../dist/danieldeusing-design.css"> // CDN
|
|
11
|
+
*
|
|
12
|
+
* Tailwind v4 apps should import "@danieldeusing/design/tailwind.css" instead,
|
|
13
|
+
* which adds the @theme mapping (and skips the reset, since Preflight covers it).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
@import "./reset.css";
|
|
17
|
+
@import "./tokens.css";
|
|
18
|
+
@import "./base.css";
|
|
19
|
+
@import "./components.css";
|