@yugnex/nexui 2.0.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 +59 -0
- package/css/nexui-base.css +157 -0
- package/css/nexui-icons.css +86 -0
- package/css/nexui-tokens.css +113 -0
- package/css/nexui.css +16 -0
- package/dist/assets/geometry.d.ts +98 -0
- package/dist/assets/geometry.d.ts.map +1 -0
- package/dist/assets/geometry.js +114 -0
- package/dist/assets/geometry.js.map +1 -0
- package/dist/assets/typography.d.ts +3 -0
- package/dist/assets/typography.d.ts.map +1 -0
- package/dist/assets/typography.js +178 -0
- package/dist/assets/typography.js.map +1 -0
- package/dist/core/compiler.d.ts +30 -0
- package/dist/core/compiler.d.ts.map +1 -0
- package/dist/core/compiler.js +124 -0
- package/dist/core/compiler.js.map +1 -0
- package/dist/core/cx.d.ts +7 -0
- package/dist/core/cx.d.ts.map +1 -0
- package/dist/core/cx.js +34 -0
- package/dist/core/cx.js.map +1 -0
- package/dist/core/matrix.d.ts +118 -0
- package/dist/core/matrix.d.ts.map +1 -0
- package/dist/core/matrix.js +180 -0
- package/dist/core/matrix.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +74 -0
- package/dist/index.js.map +1 -0
- package/dist/primitives/avatar.d.ts +8 -0
- package/dist/primitives/avatar.d.ts.map +1 -0
- package/dist/primitives/avatar.js +146 -0
- package/dist/primitives/avatar.js.map +1 -0
- package/dist/primitives/badge.d.ts +8 -0
- package/dist/primitives/badge.d.ts.map +1 -0
- package/dist/primitives/badge.js +88 -0
- package/dist/primitives/badge.js.map +1 -0
- package/dist/primitives/button.d.ts +10 -0
- package/dist/primitives/button.d.ts.map +1 -0
- package/dist/primitives/button.js +137 -0
- package/dist/primitives/button.js.map +1 -0
- package/dist/primitives/checkbox.d.ts +13 -0
- package/dist/primitives/checkbox.d.ts.map +1 -0
- package/dist/primitives/checkbox.js +107 -0
- package/dist/primitives/checkbox.js.map +1 -0
- package/dist/primitives/input.d.ts +14 -0
- package/dist/primitives/input.d.ts.map +1 -0
- package/dist/primitives/input.js +177 -0
- package/dist/primitives/input.js.map +1 -0
- package/dist/primitives/panel.d.ts +9 -0
- package/dist/primitives/panel.d.ts.map +1 -0
- package/dist/primitives/panel.js +101 -0
- package/dist/primitives/panel.js.map +1 -0
- package/dist/primitives/progress.d.ts +8 -0
- package/dist/primitives/progress.d.ts.map +1 -0
- package/dist/primitives/progress.js +105 -0
- package/dist/primitives/progress.js.map +1 -0
- package/dist/primitives/separator.d.ts +8 -0
- package/dist/primitives/separator.d.ts.map +1 -0
- package/dist/primitives/separator.js +69 -0
- package/dist/primitives/separator.js.map +1 -0
- package/dist/primitives/skeleton.d.ts +8 -0
- package/dist/primitives/skeleton.d.ts.map +1 -0
- package/dist/primitives/skeleton.js +61 -0
- package/dist/primitives/skeleton.js.map +1 -0
- package/dist/primitives/spinner.d.ts +8 -0
- package/dist/primitives/spinner.d.ts.map +1 -0
- package/dist/primitives/spinner.js +64 -0
- package/dist/primitives/spinner.js.map +1 -0
- package/dist/primitives/status-ring.d.ts +8 -0
- package/dist/primitives/status-ring.d.ts.map +1 -0
- package/dist/primitives/status-ring.js +101 -0
- package/dist/primitives/status-ring.js.map +1 -0
- package/dist/primitives/switch.d.ts +12 -0
- package/dist/primitives/switch.d.ts.map +1 -0
- package/dist/primitives/switch.js +124 -0
- package/dist/primitives/switch.js.map +1 -0
- package/dist/primitives/text-stream.d.ts +23 -0
- package/dist/primitives/text-stream.d.ts.map +1 -0
- package/dist/primitives/text-stream.js +167 -0
- package/dist/primitives/text-stream.js.map +1 -0
- package/dist/tokens/colors.d.ts +127 -0
- package/dist/tokens/colors.d.ts.map +1 -0
- package/dist/tokens/colors.js +135 -0
- package/dist/tokens/colors.js.map +1 -0
- package/dist/tokens/motion.d.ts +37 -0
- package/dist/tokens/motion.d.ts.map +1 -0
- package/dist/tokens/motion.js +93 -0
- package/dist/tokens/motion.js.map +1 -0
- package/dist/tokens/shadows.d.ts +34 -0
- package/dist/tokens/shadows.d.ts.map +1 -0
- package/dist/tokens/shadows.js +45 -0
- package/dist/tokens/shadows.js.map +1 -0
- package/dist/tokens/spacing.d.ts +69 -0
- package/dist/tokens/spacing.d.ts.map +1 -0
- package/dist/tokens/spacing.js +71 -0
- package/dist/tokens/spacing.js.map +1 -0
- package/dist/tokens/type.d.ts +166 -0
- package/dist/tokens/type.d.ts.map +1 -0
- package/dist/tokens/type.js +215 -0
- package/dist/tokens/type.js.map +1 -0
- package/fonts/NexuiIcons.woff2 +0 -0
- package/fonts/NexuiMono-Regular.otf +0 -0
- package/fonts/NexuiMono-Regular.woff2 +0 -0
- package/fonts/NexuiSans-Bold.otf +0 -0
- package/fonts/NexuiSans-Bold.woff2 +0 -0
- package/fonts/NexuiSans-Medium.otf +0 -0
- package/fonts/NexuiSans-Medium.woff2 +0 -0
- package/fonts/NexuiSans-Regular.otf +0 -0
- package/fonts/NexuiSans-Regular.woff2 +0 -0
- package/native/Cargo.toml +16 -0
- package/native/src/lib.rs +127 -0
- package/nexui-utils.css +485 -0
- package/package.json +58 -0
- package/src/assets/geometry.ts +144 -0
- package/src/assets/typography.ts +184 -0
- package/src/core/compiler.ts +139 -0
- package/src/core/cx.ts +50 -0
- package/src/core/matrix.ts +195 -0
- package/src/index.ts +78 -0
- package/src/primitives/avatar.ts +159 -0
- package/src/primitives/badge.ts +98 -0
- package/src/primitives/button.ts +149 -0
- package/src/primitives/checkbox.ts +113 -0
- package/src/primitives/input.ts +187 -0
- package/src/primitives/panel.ts +111 -0
- package/src/primitives/progress.ts +112 -0
- package/src/primitives/separator.ts +73 -0
- package/src/primitives/skeleton.ts +68 -0
- package/src/primitives/spinner.ts +71 -0
- package/src/primitives/status-ring.ts +109 -0
- package/src/primitives/switch.ts +134 -0
- package/src/primitives/text-stream.ts +187 -0
- package/src/tokens/colors.ts +149 -0
- package/src/tokens/motion.ts +97 -0
- package/src/tokens/shadows.ts +58 -0
- package/src/tokens/spacing.ts +79 -0
- package/src/tokens/type.ts +224 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// @yugnex/nexui — NexSpinner Web Component
|
|
2
|
+
// Loading spinner. Lightweight — just CSS animation, no JS after render.
|
|
3
|
+
// Attributes:
|
|
4
|
+
// size: xs | sm | md | lg | xl (default: md)
|
|
5
|
+
// color: accent | live | success | error | muted (default: accent)
|
|
6
|
+
// label: accessible label (default: "Loading")
|
|
7
|
+
|
|
8
|
+
import { nexui_compiler } from "../core/compiler";
|
|
9
|
+
|
|
10
|
+
export class NexSpinner extends HTMLElement {
|
|
11
|
+
static get observedAttributes() {
|
|
12
|
+
return ["size", "color", "label"];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
constructor() {
|
|
16
|
+
super();
|
|
17
|
+
this.attachShadow({ mode: "open" });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
connectedCallback() { this.render(); }
|
|
21
|
+
attributeChangedCallback() { this.render(); }
|
|
22
|
+
|
|
23
|
+
private render() {
|
|
24
|
+
if (!this.shadowRoot) return;
|
|
25
|
+
|
|
26
|
+
const size = this.getAttribute("size") ?? "md";
|
|
27
|
+
const color = this.getAttribute("color") ?? "accent";
|
|
28
|
+
const label = this.getAttribute("label") ?? "Loading";
|
|
29
|
+
const themeCSS = nexui_compiler.getThemeCSS(nexui_compiler.getActiveTheme(), ":host");
|
|
30
|
+
|
|
31
|
+
const szMap: Record<string, { px: number; sw: number }> = {
|
|
32
|
+
xs: { px: 12, sw: 1.5 },
|
|
33
|
+
sm: { px: 16, sw: 2 },
|
|
34
|
+
md: { px: 24, sw: 2.5 },
|
|
35
|
+
lg: { px: 32, sw: 3 },
|
|
36
|
+
xl: { px: 48, sw: 3.5 },
|
|
37
|
+
};
|
|
38
|
+
const { px, sw } = szMap[size] ?? szMap.md;
|
|
39
|
+
|
|
40
|
+
const colorMap: Record<string, string> = {
|
|
41
|
+
accent: "var(--nx-accent, #E89010)",
|
|
42
|
+
live: "var(--nx-live, #0FD4C6)",
|
|
43
|
+
success: "var(--nx-success, #22C55E)",
|
|
44
|
+
error: "var(--nx-error, #EF4444)",
|
|
45
|
+
muted: "var(--nx-text-4, #484F58)",
|
|
46
|
+
};
|
|
47
|
+
const spinColor = colorMap[color] ?? colorMap.accent;
|
|
48
|
+
|
|
49
|
+
this.shadowRoot.innerHTML = `
|
|
50
|
+
<style>
|
|
51
|
+
${themeCSS}
|
|
52
|
+
:host { display: inline-flex; align-items: center; justify-content: center; }
|
|
53
|
+
.ring {
|
|
54
|
+
width: ${px}px;
|
|
55
|
+
height: ${px}px;
|
|
56
|
+
border-radius: 50%;
|
|
57
|
+
border: ${sw}px solid rgba(255,255,255,0.08);
|
|
58
|
+
border-top-color: ${spinColor};
|
|
59
|
+
animation: nx-spin 700ms linear infinite;
|
|
60
|
+
}
|
|
61
|
+
@media (prefers-reduced-motion: reduce) {
|
|
62
|
+
.ring { animation-duration: 2s; }
|
|
63
|
+
}
|
|
64
|
+
@keyframes nx-spin { to { transform: rotate(360deg); } }
|
|
65
|
+
</style>
|
|
66
|
+
<div class="ring" role="status" aria-label="${label}">
|
|
67
|
+
<span style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)">${label}</span>
|
|
68
|
+
</div>
|
|
69
|
+
`;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// @yugnex/nexui — NexStatusRing Web Component
|
|
2
|
+
// Circular progress ring with animated entry.
|
|
3
|
+
// Attributes: score (0-100), label, color, size
|
|
4
|
+
|
|
5
|
+
import { nexui_compiler } from "../core/compiler";
|
|
6
|
+
|
|
7
|
+
export class NexStatusRing extends HTMLElement {
|
|
8
|
+
static get observedAttributes() {
|
|
9
|
+
return ["score", "label", "color", "size"];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
constructor() {
|
|
13
|
+
super();
|
|
14
|
+
this.attachShadow({ mode: "open" });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
connectedCallback() {
|
|
18
|
+
this.render();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
attributeChangedCallback() {
|
|
22
|
+
this.render();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private render() {
|
|
26
|
+
if (!this.shadowRoot) return;
|
|
27
|
+
|
|
28
|
+
const score = Math.min(100, Math.max(0, parseInt(this.getAttribute("score") ?? "0", 10)));
|
|
29
|
+
const label = this.getAttribute("label") ?? "METRIC";
|
|
30
|
+
const color = this.getAttribute("color") ?? "var(--nx-accent, #E89010)";
|
|
31
|
+
const size = Math.max(40, parseInt(this.getAttribute("size") ?? "80", 10));
|
|
32
|
+
|
|
33
|
+
const cx = size / 2;
|
|
34
|
+
const cy = size / 2;
|
|
35
|
+
const radius = (size / 2) - (size * 0.075);
|
|
36
|
+
const sw = size * 0.075;
|
|
37
|
+
const circumference = 2 * Math.PI * radius;
|
|
38
|
+
const targetOffset = circumference - (score / 100) * circumference;
|
|
39
|
+
const fontSize = Math.round(size * 0.175);
|
|
40
|
+
const labelSize = Math.round(size * 0.08);
|
|
41
|
+
const themeCSS = nexui_compiler.getThemeCSS(nexui_compiler.getActiveTheme(), ":host");
|
|
42
|
+
|
|
43
|
+
// Key technique: render at full offset (empty ring), then in the next frame
|
|
44
|
+
// update to the target offset so the CSS transition actually fires.
|
|
45
|
+
// Without this two-frame approach the element starts at the final value
|
|
46
|
+
// and the transition is never observed.
|
|
47
|
+
this.shadowRoot.innerHTML = `
|
|
48
|
+
<style>
|
|
49
|
+
${themeCSS}
|
|
50
|
+
:host {
|
|
51
|
+
display: inline-flex;
|
|
52
|
+
flex-direction: column;
|
|
53
|
+
align-items: center;
|
|
54
|
+
font-family: var(--nx-font-mono, 'JetBrains Mono', monospace);
|
|
55
|
+
}
|
|
56
|
+
.ring-arc {
|
|
57
|
+
transition: stroke-dashoffset 700ms cubic-bezier(0, 0, 0.2, 1);
|
|
58
|
+
transform-origin: center;
|
|
59
|
+
transform: rotate(-90deg);
|
|
60
|
+
}
|
|
61
|
+
.label {
|
|
62
|
+
font-size: ${labelSize}px;
|
|
63
|
+
color: var(--nx-text-3, #6E7681);
|
|
64
|
+
margin-top: ${Math.round(size * 0.04)}px;
|
|
65
|
+
text-transform: uppercase;
|
|
66
|
+
letter-spacing: 0.06em;
|
|
67
|
+
user-select: none;
|
|
68
|
+
}
|
|
69
|
+
</style>
|
|
70
|
+
<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" aria-label="${label}: ${score}%" role="img">
|
|
71
|
+
<circle
|
|
72
|
+
cx="${cx}" cy="${cy}" r="${radius}"
|
|
73
|
+
fill="none"
|
|
74
|
+
stroke="var(--nx-border, rgba(255,255,255,0.08))"
|
|
75
|
+
stroke-width="${sw}"
|
|
76
|
+
/>
|
|
77
|
+
<circle
|
|
78
|
+
class="ring-arc"
|
|
79
|
+
cx="${cx}" cy="${cy}" r="${radius}"
|
|
80
|
+
fill="none"
|
|
81
|
+
stroke="${color}"
|
|
82
|
+
stroke-width="${sw}"
|
|
83
|
+
stroke-linecap="round"
|
|
84
|
+
stroke-dasharray="${circumference}"
|
|
85
|
+
stroke-dashoffset="${circumference}"
|
|
86
|
+
/>
|
|
87
|
+
<text
|
|
88
|
+
x="${cx}" y="${cy + fontSize * 0.35}"
|
|
89
|
+
text-anchor="middle"
|
|
90
|
+
fill="var(--nx-text, #E6EDF3)"
|
|
91
|
+
font-size="${fontSize}"
|
|
92
|
+
font-weight="600"
|
|
93
|
+
font-family="inherit"
|
|
94
|
+
>${score}</text>
|
|
95
|
+
</svg>
|
|
96
|
+
<div class="label">${label}</div>
|
|
97
|
+
`;
|
|
98
|
+
|
|
99
|
+
// Two-frame update triggers the CSS transition
|
|
100
|
+
const arc = this.shadowRoot.querySelector<SVGCircleElement>(".ring-arc");
|
|
101
|
+
if (arc) {
|
|
102
|
+
requestAnimationFrame(() => {
|
|
103
|
+
requestAnimationFrame(() => {
|
|
104
|
+
arc.style.strokeDashoffset = String(targetOffset);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// @yugnex/nexui — NexSwitch Web Component
|
|
2
|
+
// Toggle switch with label and ARIA.
|
|
3
|
+
// Attributes:
|
|
4
|
+
// checked: boolean
|
|
5
|
+
// disabled: boolean
|
|
6
|
+
// label: string
|
|
7
|
+
// size: sm | md | lg
|
|
8
|
+
// color: accent | live | success (default: accent)
|
|
9
|
+
|
|
10
|
+
import { nexui_compiler } from "../core/compiler";
|
|
11
|
+
|
|
12
|
+
export class NexSwitch extends HTMLElement {
|
|
13
|
+
private _checked = false;
|
|
14
|
+
|
|
15
|
+
static get observedAttributes() {
|
|
16
|
+
return ["checked", "disabled", "label", "size", "color"];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
constructor() {
|
|
20
|
+
super();
|
|
21
|
+
this.attachShadow({ mode: "open" });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
connectedCallback() {
|
|
25
|
+
this._checked = this.hasAttribute("checked");
|
|
26
|
+
this.render();
|
|
27
|
+
this.shadowRoot?.querySelector(".track")?.addEventListener("click", () => this.toggle());
|
|
28
|
+
this.shadowRoot?.querySelector(".track")?.addEventListener("keydown", (e) => {
|
|
29
|
+
if ((e as KeyboardEvent).key === " " || (e as KeyboardEvent).key === "Enter") {
|
|
30
|
+
e.preventDefault();
|
|
31
|
+
this.toggle();
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
attributeChangedCallback(name: string, _old: string, val: string) {
|
|
37
|
+
if (name === "checked") this._checked = val !== null;
|
|
38
|
+
this.render();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get checked() { return this._checked; }
|
|
42
|
+
set checked(v: boolean) {
|
|
43
|
+
this._checked = v;
|
|
44
|
+
v ? this.setAttribute("checked", "") : this.removeAttribute("checked");
|
|
45
|
+
this.render();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private toggle() {
|
|
49
|
+
if (this.hasAttribute("disabled")) return;
|
|
50
|
+
this.checked = !this._checked;
|
|
51
|
+
this.dispatchEvent(new CustomEvent("change", { detail: { checked: this._checked }, bubbles: true }));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private render() {
|
|
55
|
+
if (!this.shadowRoot) return;
|
|
56
|
+
|
|
57
|
+
const size = this.getAttribute("size") ?? "md";
|
|
58
|
+
const color = this.getAttribute("color") ?? "accent";
|
|
59
|
+
const label = this.getAttribute("label") ?? "";
|
|
60
|
+
const disabled = this.hasAttribute("disabled");
|
|
61
|
+
const themeCSS = nexui_compiler.getThemeCSS(nexui_compiler.getActiveTheme(), ":host");
|
|
62
|
+
|
|
63
|
+
const colorMap: Record<string, string> = {
|
|
64
|
+
accent: "var(--nx-accent, #E89010)",
|
|
65
|
+
live: "var(--nx-live, #0FD4C6)",
|
|
66
|
+
success: "var(--nx-success, #22C55E)",
|
|
67
|
+
};
|
|
68
|
+
const activeColor = colorMap[color] ?? colorMap.accent;
|
|
69
|
+
|
|
70
|
+
const dims = {
|
|
71
|
+
sm: { w: 28, h: 16, thumb: 12, fs: "11px" },
|
|
72
|
+
md: { w: 36, h: 20, thumb: 16, fs: "12px" },
|
|
73
|
+
lg: { w: 44, h: 24, thumb: 20, fs: "13px" },
|
|
74
|
+
};
|
|
75
|
+
const d = dims[size as keyof typeof dims] ?? dims.md;
|
|
76
|
+
const thumbTravel = d.w - d.thumb - 4;
|
|
77
|
+
|
|
78
|
+
this.shadowRoot.innerHTML = `
|
|
79
|
+
<style>
|
|
80
|
+
${themeCSS}
|
|
81
|
+
:host { display: inline-flex; align-items: center; gap: 8px; }
|
|
82
|
+
.track {
|
|
83
|
+
position: relative;
|
|
84
|
+
width: ${d.w}px;
|
|
85
|
+
height: ${d.h}px;
|
|
86
|
+
border-radius: 9999px;
|
|
87
|
+
background: ${this._checked ? activeColor : "var(--nx-bg-elevated, #1C2128)"};
|
|
88
|
+
border: 1px solid ${this._checked ? "transparent" : "var(--nx-border-strong, rgba(255,255,255,0.16))"};
|
|
89
|
+
cursor: ${disabled ? "not-allowed" : "pointer"};
|
|
90
|
+
opacity: ${disabled ? 0.45 : 1};
|
|
91
|
+
transition: background 150ms ease, border-color 150ms ease;
|
|
92
|
+
outline: none;
|
|
93
|
+
flex-shrink: 0;
|
|
94
|
+
}
|
|
95
|
+
.track:focus-visible {
|
|
96
|
+
box-shadow: 0 0 0 2px var(--nx-bg-base, #0D1117), 0 0 0 4px ${activeColor};
|
|
97
|
+
}
|
|
98
|
+
.thumb {
|
|
99
|
+
position: absolute;
|
|
100
|
+
top: 50%;
|
|
101
|
+
left: 2px;
|
|
102
|
+
transform: translateY(-50%) translateX(${this._checked ? `${thumbTravel}px` : "0"});
|
|
103
|
+
width: ${d.thumb}px;
|
|
104
|
+
height: ${d.thumb}px;
|
|
105
|
+
border-radius: 50%;
|
|
106
|
+
background: ${this._checked ? "var(--nx-bg-base, #0D1117)" : "var(--nx-text-4, #484F58)"};
|
|
107
|
+
transition: transform 150ms cubic-bezier(0.34,1.56,0.64,1), background 150ms ease;
|
|
108
|
+
pointer-events: none;
|
|
109
|
+
}
|
|
110
|
+
.label {
|
|
111
|
+
font-family: var(--nx-font-sans, system-ui, sans-serif);
|
|
112
|
+
font-size: ${d.fs};
|
|
113
|
+
color: ${disabled ? "var(--nx-text-4, #484F58)" : "var(--nx-text-2, #8B949E)"};
|
|
114
|
+
user-select: none;
|
|
115
|
+
cursor: ${disabled ? "not-allowed" : "pointer"};
|
|
116
|
+
}
|
|
117
|
+
</style>
|
|
118
|
+
<div class="track" role="switch" aria-checked="${this._checked}" tabindex="${disabled ? -1 : 0}" part="track">
|
|
119
|
+
<div class="thumb" part="thumb"></div>
|
|
120
|
+
</div>
|
|
121
|
+
${label ? `<span class="label" part="label">${label}</span>` : ""}
|
|
122
|
+
`;
|
|
123
|
+
|
|
124
|
+
// Re-attach events after innerHTML reset
|
|
125
|
+
const track = this.shadowRoot.querySelector(".track");
|
|
126
|
+
track?.addEventListener("click", () => this.toggle());
|
|
127
|
+
track?.addEventListener("keydown", (e) => {
|
|
128
|
+
if ((e as KeyboardEvent).key === " " || (e as KeyboardEvent).key === "Enter") {
|
|
129
|
+
e.preventDefault();
|
|
130
|
+
this.toggle();
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// @yugnex/nexui — NexTextStream Web Component
|
|
2
|
+
// Live log stream viewer with virtual tail rendering.
|
|
3
|
+
// XSS-safe: all user content set via textContent, never innerHTML.
|
|
4
|
+
// Performance: incremental DOM append, not full rebuild per message.
|
|
5
|
+
|
|
6
|
+
import { nexui_compiler } from "../core/compiler";
|
|
7
|
+
|
|
8
|
+
type LogFlag = "error" | "verify" | "system" | "warn" | "default";
|
|
9
|
+
|
|
10
|
+
function detectFlag(text: string): LogFlag {
|
|
11
|
+
const lower = text.toLowerCase();
|
|
12
|
+
if (text.includes("🚨") || lower.includes("error") || lower.includes("fatal")) return "error";
|
|
13
|
+
if (text.includes("⚠") || lower.includes("warn")) return "warn";
|
|
14
|
+
if (text.includes("✓") || lower.includes("verified") || lower.includes("pass")) return "verify";
|
|
15
|
+
if (text.includes("✦") || lower.includes("system") || lower.includes("[sys]")) return "system";
|
|
16
|
+
return "default";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const FLAG_COLORS: Record<LogFlag, string> = {
|
|
20
|
+
error: "var(--nx-error, #EF4444)",
|
|
21
|
+
warn: "var(--nx-warning, #EAB308)",
|
|
22
|
+
verify: "var(--nx-success, #22C55E)",
|
|
23
|
+
system: "var(--nx-accent, #E89010)",
|
|
24
|
+
default: "var(--nx-text, #E6EDF3)",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export class NexTextStream extends HTMLElement {
|
|
28
|
+
private logBuffer: string[] = [];
|
|
29
|
+
private lineElements: HTMLDivElement[] = [];
|
|
30
|
+
private viewport: HTMLDivElement | null = null;
|
|
31
|
+
private container: HTMLDivElement | null = null;
|
|
32
|
+
private lineCount = 0;
|
|
33
|
+
|
|
34
|
+
static readonly MAX_BUFFER = 5000;
|
|
35
|
+
static readonly VISIBLE_ROWS = 50;
|
|
36
|
+
|
|
37
|
+
static get observedAttributes() {
|
|
38
|
+
return ["max-rows", "show-numbers"];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
constructor() {
|
|
42
|
+
super();
|
|
43
|
+
this.attachShadow({ mode: "open" });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
connectedCallback() {
|
|
47
|
+
this.setupLayout();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
attributeChangedCallback() {
|
|
51
|
+
// No re-render needed for these attrs — they affect future appends only
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private setupLayout() {
|
|
55
|
+
if (!this.shadowRoot) return;
|
|
56
|
+
|
|
57
|
+
const themeCSS = nexui_compiler.getThemeCSS(nexui_compiler.getActiveTheme(), ":host");
|
|
58
|
+
|
|
59
|
+
const style = document.createElement("style");
|
|
60
|
+
style.textContent = `
|
|
61
|
+
${themeCSS}
|
|
62
|
+
:host {
|
|
63
|
+
display: block;
|
|
64
|
+
background-color: var(--nx-bg-void, #040610);
|
|
65
|
+
border: 1px solid var(--nx-border, #1e293b);
|
|
66
|
+
border-radius: 6px;
|
|
67
|
+
padding: 12px;
|
|
68
|
+
font-family: var(--nx-font-mono, 'JetBrains Mono', monospace);
|
|
69
|
+
font-size: 11px;
|
|
70
|
+
line-height: 1.8;
|
|
71
|
+
color: var(--nx-text, #E6EDF3);
|
|
72
|
+
overflow: hidden;
|
|
73
|
+
}
|
|
74
|
+
.viewport {
|
|
75
|
+
max-height: 400px;
|
|
76
|
+
overflow-y: auto;
|
|
77
|
+
scrollbar-width: thin;
|
|
78
|
+
scrollbar-color: rgba(255,255,255,0.12) transparent;
|
|
79
|
+
}
|
|
80
|
+
.viewport::-webkit-scrollbar { width: 4px; }
|
|
81
|
+
.viewport::-webkit-scrollbar-track { background: transparent; }
|
|
82
|
+
.viewport::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.12); border-radius: 2px; }
|
|
83
|
+
.row {
|
|
84
|
+
display: flex;
|
|
85
|
+
gap: 10px;
|
|
86
|
+
white-space: pre-wrap;
|
|
87
|
+
word-break: break-all;
|
|
88
|
+
padding: 1px 0;
|
|
89
|
+
}
|
|
90
|
+
.num {
|
|
91
|
+
color: var(--nx-text-4, #484F58);
|
|
92
|
+
user-select: none;
|
|
93
|
+
min-width: 36px;
|
|
94
|
+
text-align: right;
|
|
95
|
+
flex-shrink: 0;
|
|
96
|
+
}
|
|
97
|
+
.text {
|
|
98
|
+
flex: 1;
|
|
99
|
+
min-width: 0;
|
|
100
|
+
}
|
|
101
|
+
`;
|
|
102
|
+
|
|
103
|
+
const viewport = document.createElement("div");
|
|
104
|
+
viewport.className = "viewport";
|
|
105
|
+
|
|
106
|
+
const container = document.createElement("div");
|
|
107
|
+
container.setAttribute("aria-live", "polite");
|
|
108
|
+
container.setAttribute("aria-label", "Log stream");
|
|
109
|
+
viewport.appendChild(container);
|
|
110
|
+
|
|
111
|
+
this.shadowRoot.appendChild(style);
|
|
112
|
+
this.shadowRoot.appendChild(viewport);
|
|
113
|
+
|
|
114
|
+
this.viewport = viewport;
|
|
115
|
+
this.container = container;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Push a single log line. Safe for high-frequency calls (e.g. SSE tokens). */
|
|
119
|
+
public pushLogTrace(message: string) {
|
|
120
|
+
// Enforce buffer ceiling
|
|
121
|
+
if (this.logBuffer.length >= NexTextStream.MAX_BUFFER) {
|
|
122
|
+
this.logBuffer.shift();
|
|
123
|
+
// Remove the oldest visible row element
|
|
124
|
+
const oldest = this.lineElements.shift();
|
|
125
|
+
if (oldest && this.container) this.container.removeChild(oldest);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
this.logBuffer.push(message);
|
|
129
|
+
this.lineCount++;
|
|
130
|
+
this.appendRow(message, this.lineCount);
|
|
131
|
+
|
|
132
|
+
// Trim visible rows beyond the virtual window
|
|
133
|
+
const maxVisible = parseInt(this.getAttribute("max-rows") ?? "50", 10);
|
|
134
|
+
while (this.lineElements.length > maxVisible) {
|
|
135
|
+
const old = this.lineElements.shift();
|
|
136
|
+
if (old && this.container) this.container.removeChild(old);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
this.scrollToBottom();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Push multiple lines at once (batch). More efficient than repeated pushLogTrace. */
|
|
143
|
+
public pushBatch(messages: string[]) {
|
|
144
|
+
for (const msg of messages) this.pushLogTrace(msg);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Clear all log content. */
|
|
148
|
+
public clear() {
|
|
149
|
+
this.logBuffer = [];
|
|
150
|
+
this.lineElements = [];
|
|
151
|
+
this.lineCount = 0;
|
|
152
|
+
if (this.container) this.container.textContent = "";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private appendRow(text: string, lineNumber: number) {
|
|
156
|
+
if (!this.container) return;
|
|
157
|
+
|
|
158
|
+
const showNums = this.getAttribute("show-numbers") !== "false";
|
|
159
|
+
const flag = detectFlag(text);
|
|
160
|
+
const color = FLAG_COLORS[flag];
|
|
161
|
+
|
|
162
|
+
const row = document.createElement("div");
|
|
163
|
+
row.className = "row";
|
|
164
|
+
|
|
165
|
+
if (showNums) {
|
|
166
|
+
const num = document.createElement("span");
|
|
167
|
+
num.className = "num";
|
|
168
|
+
num.textContent = String(lineNumber);
|
|
169
|
+
row.appendChild(num);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const textSpan = document.createElement("span");
|
|
173
|
+
textSpan.className = "text";
|
|
174
|
+
textSpan.style.color = color;
|
|
175
|
+
textSpan.textContent = text; // textContent — never innerHTML
|
|
176
|
+
row.appendChild(textSpan);
|
|
177
|
+
|
|
178
|
+
this.container.appendChild(row);
|
|
179
|
+
this.lineElements.push(row);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private scrollToBottom() {
|
|
183
|
+
if (this.viewport) {
|
|
184
|
+
this.viewport.scrollTop = this.viewport.scrollHeight;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// @yugnex/nexui — Sovereign Color System
|
|
2
|
+
// Zero external dependencies. All values hand-authored.
|
|
3
|
+
|
|
4
|
+
export const NEXUI_PALETTE = {
|
|
5
|
+
void: {
|
|
6
|
+
0: "#000000",
|
|
7
|
+
50: "#05070C",
|
|
8
|
+
100: "#0A0D14",
|
|
9
|
+
200: "#0D1117",
|
|
10
|
+
300: "#161B22",
|
|
11
|
+
400: "#1C2128",
|
|
12
|
+
500: "#21262D",
|
|
13
|
+
600: "#30363D",
|
|
14
|
+
700: "#3D444D",
|
|
15
|
+
800: "#484F58",
|
|
16
|
+
900: "#6E7681",
|
|
17
|
+
950: "#8B949E",
|
|
18
|
+
},
|
|
19
|
+
amber: {
|
|
20
|
+
50: "#FFF8E7",
|
|
21
|
+
100: "#FDECC8",
|
|
22
|
+
200: "#F9D07A",
|
|
23
|
+
300: "#F5B342",
|
|
24
|
+
400: "#E89010",
|
|
25
|
+
500: "#C97A0E",
|
|
26
|
+
600: "#A3630B",
|
|
27
|
+
700: "#7D4D09",
|
|
28
|
+
800: "#573606",
|
|
29
|
+
900: "#3A2304",
|
|
30
|
+
},
|
|
31
|
+
teal: {
|
|
32
|
+
50: "#E6FFFE",
|
|
33
|
+
100: "#CCFFFE",
|
|
34
|
+
200: "#99FFFD",
|
|
35
|
+
300: "#4DFCF7",
|
|
36
|
+
400: "#0FD4C6",
|
|
37
|
+
500: "#0EB5A9",
|
|
38
|
+
600: "#0B8A81",
|
|
39
|
+
700: "#086059",
|
|
40
|
+
800: "#053D38",
|
|
41
|
+
900: "#021F1C",
|
|
42
|
+
},
|
|
43
|
+
green: {
|
|
44
|
+
50: "#F0FDF4",
|
|
45
|
+
100: "#DCFCE7",
|
|
46
|
+
200: "#BBF7D0",
|
|
47
|
+
300: "#86EFAC",
|
|
48
|
+
400: "#4ADE80",
|
|
49
|
+
500: "#22C55E",
|
|
50
|
+
600: "#16A34A",
|
|
51
|
+
700: "#15803D",
|
|
52
|
+
800: "#166534",
|
|
53
|
+
900: "#14532D",
|
|
54
|
+
},
|
|
55
|
+
red: {
|
|
56
|
+
50: "#FEF2F2",
|
|
57
|
+
100: "#FEE2E2",
|
|
58
|
+
200: "#FECACA",
|
|
59
|
+
300: "#FCA5A5",
|
|
60
|
+
400: "#F87171",
|
|
61
|
+
500: "#EF4444",
|
|
62
|
+
600: "#DC2626",
|
|
63
|
+
700: "#B91C1C",
|
|
64
|
+
800: "#991B1B",
|
|
65
|
+
900: "#7F1D1D",
|
|
66
|
+
},
|
|
67
|
+
yellow: {
|
|
68
|
+
400: "#FACC15",
|
|
69
|
+
500: "#EAB308",
|
|
70
|
+
600: "#CA8A04",
|
|
71
|
+
},
|
|
72
|
+
} as const;
|
|
73
|
+
|
|
74
|
+
// Semantic layer — what components reference, never raw palette values
|
|
75
|
+
export const NEXUI_SEMANTIC_VOID = {
|
|
76
|
+
// Surfaces (ground → highest elevation)
|
|
77
|
+
"bg-void": NEXUI_PALETTE.void[50],
|
|
78
|
+
"bg-base": NEXUI_PALETTE.void[200],
|
|
79
|
+
"bg-surface": NEXUI_PALETTE.void[300],
|
|
80
|
+
"bg-elevated": NEXUI_PALETTE.void[400],
|
|
81
|
+
"bg-overlay": NEXUI_PALETTE.void[500],
|
|
82
|
+
|
|
83
|
+
// Borders
|
|
84
|
+
"border": "rgba(255,255,255,0.08)",
|
|
85
|
+
"border-strong": "rgba(255,255,255,0.16)",
|
|
86
|
+
"border-focus": "rgba(232,144,16,0.60)",
|
|
87
|
+
|
|
88
|
+
// Text hierarchy
|
|
89
|
+
"text-primary": "#E6EDF3",
|
|
90
|
+
"text-secondary": NEXUI_PALETTE.void[950],
|
|
91
|
+
"text-dim": NEXUI_PALETTE.void[900],
|
|
92
|
+
"text-ghost": NEXUI_PALETTE.void[800],
|
|
93
|
+
"text-inverse": NEXUI_PALETTE.void[50],
|
|
94
|
+
|
|
95
|
+
// Accent (amber — primary brand)
|
|
96
|
+
"accent": NEXUI_PALETTE.amber[400],
|
|
97
|
+
"accent-dim": "rgba(232,144,16,0.09)",
|
|
98
|
+
"accent-border": "rgba(232,144,16,0.22)",
|
|
99
|
+
"accent-text": NEXUI_PALETTE.amber[300],
|
|
100
|
+
|
|
101
|
+
// Live / activity (teal)
|
|
102
|
+
"live": NEXUI_PALETTE.teal[400],
|
|
103
|
+
"live-dim": "rgba(15,212,198,0.09)",
|
|
104
|
+
"live-border": "rgba(15,212,198,0.22)",
|
|
105
|
+
|
|
106
|
+
// Status
|
|
107
|
+
"success": NEXUI_PALETTE.green[500],
|
|
108
|
+
"success-dim": "rgba(34,197,94,0.10)",
|
|
109
|
+
"error": NEXUI_PALETTE.red[500],
|
|
110
|
+
"error-dim": "rgba(239,68,68,0.10)",
|
|
111
|
+
"warning": NEXUI_PALETTE.yellow[500],
|
|
112
|
+
"warning-dim": "rgba(234,179,8,0.10)",
|
|
113
|
+
} as const;
|
|
114
|
+
|
|
115
|
+
export const NEXUI_SEMANTIC_TERMINAL = {
|
|
116
|
+
"bg-void": "#000000",
|
|
117
|
+
"bg-base": "#050505",
|
|
118
|
+
"bg-surface": "#0C0E12",
|
|
119
|
+
"bg-elevated": "#111418",
|
|
120
|
+
"bg-overlay": "#161B22",
|
|
121
|
+
|
|
122
|
+
"border": "#1E293B",
|
|
123
|
+
"border-strong": "#2D3748",
|
|
124
|
+
"border-focus": "rgba(16,185,129,0.60)",
|
|
125
|
+
|
|
126
|
+
"text-primary": "#10B981",
|
|
127
|
+
"text-secondary": "#6EE7B7",
|
|
128
|
+
"text-dim": "#34D399",
|
|
129
|
+
"text-ghost": "#065F46",
|
|
130
|
+
"text-inverse": "#000000",
|
|
131
|
+
|
|
132
|
+
"accent": NEXUI_PALETTE.amber[400],
|
|
133
|
+
"accent-dim": "rgba(232,144,16,0.09)",
|
|
134
|
+
"accent-border": "rgba(232,144,16,0.22)",
|
|
135
|
+
"accent-text": NEXUI_PALETTE.amber[300],
|
|
136
|
+
|
|
137
|
+
"live": "#10B981",
|
|
138
|
+
"live-dim": "rgba(16,185,129,0.09)",
|
|
139
|
+
"live-border": "rgba(16,185,129,0.22)",
|
|
140
|
+
|
|
141
|
+
"success": "#10B981",
|
|
142
|
+
"success-dim": "rgba(16,185,129,0.10)",
|
|
143
|
+
"error": NEXUI_PALETTE.red[500],
|
|
144
|
+
"error-dim": "rgba(239,68,68,0.10)",
|
|
145
|
+
"warning": NEXUI_PALETTE.yellow[500],
|
|
146
|
+
"warning-dim": "rgba(234,179,8,0.10)",
|
|
147
|
+
} as const;
|
|
148
|
+
|
|
149
|
+
export type NexuiSemanticToken = keyof typeof NEXUI_SEMANTIC_VOID;
|