CSS Files Styling

This commit is contained in:
2025-11-28 17:43:48 +00:00
parent 8cdb39afbe
commit 4d17b599b3
54 changed files with 4817 additions and 1764 deletions

View File

@@ -0,0 +1,13 @@
.scene-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
pointer-events: none;
}
.scene-container :global(canvas) {
display: block;
}

View File

@@ -0,0 +1,34 @@
/* TUI Components CSS Index */
/* This file can be imported to get all TUI component styles at once */
/* TUI Core Components */
@import './tui-header.css';
@import './tui-footer.css';
@import './tui-body.css';
/* TUI Display Components */
@import './tui-button.css';
@import './tui-link.css';
@import './tui-card.css';
@import './tui-card-grid.css';
@import './tui-progress.css';
@import './tui-accordion.css';
@import './tui-table.css';
@import './tui-tooltip.css';
/* TUI Form Components */
@import './tui-input.css';
@import './tui-textarea.css';
@import './tui-checkbox.css';
@import './tui-radio.css';
@import './tui-select.css';
@import './tui-toggle.css';
/* Main Components */
@import './terminal.css';
@import './terminal-tui.css';
@import './terminal-page.css';
@import './model-viewer.css';
@import './navbar.css';
@import './navbar-waybar.css';
@import './background-3d.css';

View File

@@ -0,0 +1,172 @@
.model-viewer {
display: flex;
flex-direction: column;
background: var(--viewer-bg);
border: 1px solid var(--viewer-border);
border-radius: 8px;
overflow: hidden;
transition: all 0.3s ease;
}
.model-viewer.fullscreen {
position: fixed;
inset: 60px 0 0 0;
z-index: 100;
border-radius: 0;
border: none;
}
.viewer-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5rem 0.75rem;
background: color-mix(in srgb, var(--viewer-bg) 80%, black);
border-bottom: 1px solid var(--viewer-border);
}
.header-left {
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--viewer-primary);
font-size: 0.85rem;
font-weight: 500;
}
.header-controls {
display: flex;
gap: 0.25rem;
}
.control-btn {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
background: transparent;
border: 1px solid transparent;
border-radius: 4px;
color: var(--viewer-muted);
cursor: pointer;
transition: all 0.2s ease;
}
.control-btn:hover {
background: var(--viewer-border);
color: var(--viewer-text);
border-color: var(--viewer-border);
}
.control-btn.active {
background: var(--viewer-primary);
color: var(--viewer-bg);
border-color: var(--viewer-primary);
}
.control-btn.small {
width: 24px;
height: 24px;
}
/* Extended controls panel */
.controls-panel {
display: flex;
flex-wrap: wrap;
gap: 1rem;
padding: 0.5rem 0.75rem;
background: color-mix(in srgb, var(--viewer-bg) 90%, black);
border-bottom: 1px solid var(--viewer-border);
}
.control-group {
display: flex;
align-items: center;
gap: 0.5rem;
}
.control-label {
font-size: 0.7rem;
color: var(--viewer-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.control-buttons {
display: flex;
align-items: center;
gap: 0.25rem;
}
.control-value {
font-size: 0.7rem;
color: var(--viewer-text);
min-width: 2.5rem;
text-align: center;
font-family: 'JetBrains Mono', monospace;
}
.canvas-container {
flex: 1;
min-height: 300px;
position: relative;
background: radial-gradient(
circle at center,
color-mix(in srgb, var(--viewer-primary) 5%, transparent),
transparent 70%
);
outline: none;
}
.canvas-container:focus {
box-shadow: inset 0 0 0 2px var(--viewer-primary);
}
.canvas-container :global(canvas) {
display: block;
}
.loading-overlay,
.error-overlay {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.75rem;
color: var(--viewer-muted);
font-size: 0.9rem;
}
.error-overlay {
color: #f38ba8;
}
:global(.spin) {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.viewer-footer {
display: flex;
align-items: center;
justify-content: center;
gap: 1.5rem;
padding: 0.5rem;
background: color-mix(in srgb, var(--viewer-bg) 80%, black);
border-top: 1px solid var(--viewer-border);
}
.hint {
display: flex;
align-items: center;
gap: 0.35rem;
color: var(--viewer-muted);
font-size: 0.75rem;
}

View File

@@ -0,0 +1,377 @@
/* Waybar main container */
.waybar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
height: var(--navbar-height);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 0.75rem;
background: var(--bar-bg);
border-bottom: 1px solid var(--bar-border);
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: 0.8rem;
}
/* Bar sections */
.bar-left,
.bar-center,
.bar-right {
display: flex;
align-items: center;
gap: 0.5rem;
}
.bar-left {
flex: 1;
}
.bar-right {
flex: 1;
justify-content: flex-end;
}
/* Desktop-only items */
.desktop-only {
display: flex;
}
@media (max-width: 768px) {
.desktop-only {
display: none;
}
}
/* Module base style */
.module {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.35rem 0.6rem;
background: var(--bar-bg-module);
border-radius: 6px;
transition: all 0.15s ease;
}
/* Launcher / Logo */
.launcher {
padding: 0.4rem;
border-radius: 6px;
}
.launcher:hover {
background: color-mix(in srgb, var(--bar-primary) 20%, transparent);
}
/* Mobile menu toggle button */
.mobile-menu-toggle {
display: none;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
padding: 0;
background: var(--bar-bg-module);
border: 1px solid var(--bar-border);
border-radius: 6px;
color: var(--bar-text);
cursor: pointer;
transition: all 0.15s ease;
}
.mobile-menu-toggle:hover {
background: color-mix(in srgb, var(--bar-primary) 20%, transparent);
border-color: var(--bar-primary);
}
@media (max-width: 768px) {
.mobile-menu-toggle {
display: flex;
}
}
/* Workspaces */
.workspaces {
gap: 0.25rem;
background: transparent;
padding: 0;
}
.workspace {
display: flex;
align-items: center;
gap: 0.35rem;
padding: 0.35rem 0.6rem;
color: var(--bar-muted);
text-decoration: none;
border-radius: 4px;
transition: all 0.15s ease;
font-size: 0.75rem;
}
.workspace:hover {
color: var(--bar-text);
background: var(--bar-bg-module);
}
.workspace.active {
color: var(--bar-primary);
background: color-mix(in srgb, var(--bar-primary) 15%, transparent);
}
.workspace.external {
color: var(--bar-accent);
}
.workspace.external:hover {
background: color-mix(in srgb, var(--bar-accent) 15%, transparent);
}
.ws-name {
font-weight: 500;
}
/* Window title */
.window-title {
color: var(--bar-text);
font-size: 0.75rem;
}
.title-icon {
color: var(--bar-primary);
}
.title-text {
color: var(--bar-muted);
}
/* Clock */
.clock {
gap: 0.75rem;
}
.time {
color: var(--bar-text);
font-weight: 600;
}
.date {
color: var(--bar-muted);
}
/* Theme selector */
.theme-selector {
position: relative;
background: transparent;
padding: 0;
}
.theme-trigger {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
padding: 0;
background: var(--bar-bg-module);
border: 1px solid var(--bar-border);
border-radius: 6px;
color: var(--bar-text);
cursor: pointer;
transition: all 0.15s ease;
}
.theme-trigger:hover {
background: color-mix(in srgb, var(--bar-primary) 20%, transparent);
border-color: var(--bar-primary);
}
.theme-dropdown {
position: absolute;
top: calc(100% + 0.5rem);
right: 0;
min-width: 160px;
background: var(--bar-bg);
border: 1px solid var(--bar-border);
border-radius: 8px;
padding: 0.5rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
z-index: 1001;
}
.dropdown-header {
padding: 0.25rem 0.5rem;
font-size: 0.65rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--bar-muted);
}
.dropdown-divider {
height: 1px;
background: var(--bar-border);
margin: 0.5rem 0;
}
.theme-option {
display: flex;
align-items: center;
gap: 0.5rem;
width: 100%;
padding: 0.5rem 0.75rem;
background: transparent;
border: none;
border-radius: 4px;
color: var(--bar-text);
cursor: pointer;
font-family: inherit;
font-size: 0.8rem;
text-align: left;
transition: all 0.15s ease;
}
.theme-option:hover {
background: color-mix(in srgb, var(--bar-primary) 15%, transparent);
}
.theme-option.active {
color: var(--bar-primary);
}
:global(.check) {
margin-left: auto;
color: var(--bar-primary);
}
/* Backdrop */
.backdrop {
position: fixed;
inset: 0;
background: transparent;
z-index: 999;
border: none;
cursor: default;
}
/* Mobile menu dropdown */
.mobile-menu {
position: fixed;
top: var(--navbar-height);
left: 0;
right: 0;
background: var(--bar-bg);
border-bottom: 1px solid var(--bar-border);
z-index: 998;
padding: 1rem;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}
.mobile-nav-links {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.mobile-nav-link {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
color: var(--bar-text);
text-decoration: none;
border-radius: 6px;
transition: all 0.15s ease;
font-size: 0.9rem;
}
.mobile-nav-link:hover {
background: var(--bar-bg-module);
}
.mobile-nav-link.active {
color: var(--bar-primary);
background: color-mix(in srgb, var(--bar-primary) 15%, transparent);
}
.mobile-nav-link.external {
color: var(--bar-accent);
}
.mobile-menu-divider {
height: 1px;
background: var(--bar-border);
margin: 1rem 0;
}
.mobile-theme-section {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.mobile-section-header {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--bar-muted);
padding: 0 0.5rem;
}
.mobile-theme-options {
display: flex;
gap: 0.5rem;
}
.mobile-theme-btn {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.35rem;
padding: 0.75rem 0.5rem;
background: var(--bar-bg-module);
border: 1px solid var(--bar-border);
border-radius: 6px;
color: var(--bar-text);
cursor: pointer;
font-family: inherit;
font-size: 0.75rem;
transition: all 0.15s ease;
}
.mobile-theme-btn:hover {
background: color-mix(in srgb, var(--bar-primary) 15%, transparent);
border-color: var(--bar-primary);
}
.mobile-theme-btn.active {
color: var(--bar-primary);
border-color: var(--bar-primary);
background: color-mix(in srgb, var(--bar-primary) 20%, transparent);
}
.mobile-mode-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.75rem;
background: var(--bar-bg-module);
border: 1px solid var(--bar-border);
border-radius: 6px;
color: var(--bar-text);
cursor: pointer;
font-family: inherit;
font-size: 0.8rem;
transition: all 0.15s ease;
width: 100%;
}
.mobile-mode-btn:hover {
background: color-mix(in srgb, var(--bar-primary) 15%, transparent);
border-color: var(--bar-primary);
}

View File

@@ -0,0 +1,331 @@
.navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
background: var(--nav-bg);
border-bottom: 1px solid var(--nav-border);
backdrop-filter: blur(10px);
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
}
.nav-container {
max-width: 1400px;
margin: 0 auto;
padding: 0.75rem 1.5rem;
display: flex;
align-items: center;
justify-content: space-between;
gap: 2rem;
}
.nav-brand {
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 0.9rem;
font-weight: 600;
}
.terminal-prompt {
display: flex;
align-items: center;
gap: 0;
}
.terminal-prompt .user {
color: var(--nav-accent);
}
.terminal-prompt .separator {
color: var(--nav-text);
}
.terminal-prompt .path {
color: var(--nav-primary);
}
.terminal-prompt .symbol {
color: var(--nav-text);
margin-left: 0.25rem;
}
.cursor {
width: 8px;
height: 1em;
background: var(--nav-primary);
animation: blink 1s step-end infinite;
margin-left: 0.25rem;
vertical-align: text-bottom;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
.mobile-toggle {
display: none;
background: none;
border: none;
padding: 0.5rem;
cursor: pointer;
}
.hamburger {
display: block;
width: 24px;
height: 2px;
background: var(--nav-text);
position: relative;
transition: all 0.3s ease;
}
.hamburger::before,
.hamburger::after {
content: '';
position: absolute;
width: 24px;
height: 2px;
background: var(--nav-text);
transition: all 0.3s ease;
}
.hamburger::before {
top: -7px;
}
.hamburger::after {
top: 7px;
}
.hamburger.open {
background: transparent;
}
.hamburger.open::before {
top: 0;
transform: rotate(45deg);
}
.hamburger.open::after {
top: 0;
transform: rotate(-45deg);
}
.nav-links {
display: flex;
align-items: center;
gap: 1.5rem;
flex: 1;
}
.nav-link {
color: var(--nav-text);
text-decoration: none;
font-size: 0.85rem;
padding: 0.5rem 0.75rem;
border-radius: 4px;
transition: all 0.2s ease;
display: flex;
align-items: center;
position: relative;
overflow: hidden;
}
.nav-link::before {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 0;
height: 2px;
background: var(--nav-primary);
transition: width 0.3s ease;
}
.nav-link:hover::before {
width: 100%;
}
.nav-link:hover {
color: var(--nav-primary);
}
.link-prefix {
color: var(--nav-muted);
margin-right: 2px;
}
.nav-controls {
display: flex;
align-items: center;
gap: 1rem;
}
.theme-selector {
position: relative;
}
.theme-button {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
background: transparent;
border: 1px solid var(--nav-border);
border-radius: 6px;
color: var(--nav-text);
cursor: pointer;
font-family: inherit;
font-size: 0.8rem;
transition: all 0.2s ease;
}
.theme-button:hover {
border-color: var(--nav-primary);
background: color-mix(in srgb, var(--nav-primary) 10%, transparent);
}
.theme-icon {
font-size: 1rem;
display: flex;
align-items: center;
}
.theme-label {
text-transform: capitalize;
}
:global(.dropdown-arrow) {
transition: transform 0.2s ease;
}
:global(.dropdown-arrow.open) {
transform: rotate(180deg);
}
.theme-dropdown {
position: absolute;
top: calc(100% + 0.5rem);
right: 0;
min-width: 180px;
background: var(--nav-bg);
border: 1px solid var(--nav-border);
border-radius: 8px;
padding: 0.5rem;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
z-index: 1001;
}
.theme-option {
display: flex;
align-items: center;
gap: 0.75rem;
width: 100%;
padding: 0.625rem 0.75rem;
background: transparent;
border: none;
border-radius: 6px;
color: var(--nav-text);
cursor: pointer;
font-family: inherit;
font-size: 0.85rem;
text-align: left;
transition: all 0.2s ease;
}
.theme-option:hover {
background: color-mix(in srgb, var(--nav-primary) 15%, transparent);
}
.theme-option.active {
background: color-mix(in srgb, var(--nav-primary) 20%, transparent);
color: var(--nav-primary);
}
.option-icon {
font-size: 1.1rem;
display: flex;
align-items: center;
}
.option-label {
flex: 1;
}
:global(.check-mark) {
color: var(--nav-primary);
}
.mode-toggle {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
background: transparent;
border: 1px solid var(--nav-border);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
color: var(--nav-text);
}
.mode-toggle:hover {
border-color: var(--nav-primary);
background: color-mix(in srgb, var(--nav-primary) 10%, transparent);
color: var(--nav-primary);
}
.backdrop {
position: fixed;
inset: 0;
background: transparent;
z-index: 999;
border: none;
cursor: default;
}
@media (max-width: 768px) {
.nav-container {
flex-wrap: wrap;
padding: 0.75rem 1rem;
}
.mobile-toggle {
display: block;
order: 2;
}
.nav-links {
order: 4;
width: 100%;
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease, padding 0.3s ease;
}
.nav-links.expanded {
max-height: 200px;
padding-top: 1rem;
}
.nav-controls {
order: 3;
margin-left: auto;
margin-right: 1rem;
}
.theme-label {
display: none;
}
.theme-button {
padding: 0.5rem;
}
}

View File

@@ -0,0 +1,223 @@
.terminal-page {
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
background: var(--terminal-bg);
border: 1px solid var(--terminal-border);
border-radius: 12px;
overflow: hidden;
box-shadow:
0 25px 50px -12px rgba(0, 0, 0, 0.4),
0 0 0 1px rgba(255, 255, 255, 0.05) inset;
animation: terminalFadeIn 0.5s ease-out;
display: flex;
flex-direction: column;
height: 100%;
}
@keyframes terminalFadeIn {
from {
opacity: 0;
transform: translateY(20px) scale(0.98);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.terminal-header {
display: flex;
align-items: center;
padding: 0.75rem 1rem;
background: var(--terminal-bg-light);
border-bottom: 1px solid var(--terminal-border);
flex-shrink: 0;
}
.terminal-buttons {
display: flex;
gap: 8px;
}
.terminal-buttons .btn {
width: 12px;
height: 12px;
border-radius: 50%;
cursor: pointer;
transition: opacity 0.2s;
}
.terminal-buttons .btn:hover {
opacity: 0.8;
}
.terminal-buttons .close {
background: #ff5f56;
}
.terminal-buttons .minimize {
background: #ffbd2e;
}
.terminal-buttons .maximize {
background: #27ca40;
}
.terminal-title {
flex: 1;
text-align: center;
font-size: 0.8rem;
color: var(--terminal-muted);
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.terminal-icon {
font-size: 1rem;
}
.terminal-spacer {
width: 52px;
}
.terminal-body {
padding: 1rem 1.25rem;
flex: 1;
overflow-y: auto;
font-size: 0.9rem;
line-height: 1.6;
}
.terminal-line {
display: flex;
align-items: flex-start;
flex-wrap: wrap;
margin-bottom: 0.25rem;
animation: lineSlideIn 0.2s ease-out;
min-height: 1.6em;
}
.terminal-line.blank {
min-height: 0.8em;
}
@keyframes lineSlideIn {
from {
opacity: 0;
transform: translateX(-10px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.prompt {
display: inline-flex;
margin-right: 0.5rem;
flex-shrink: 0;
}
.prompt .user {
color: var(--terminal-user);
}
.prompt .separator {
color: var(--terminal-text);
}
.prompt .path {
color: var(--terminal-path);
}
.prompt .symbol {
color: var(--terminal-text);
margin-left: 0.25rem;
}
.content {
color: var(--terminal-text);
word-break: break-word;
white-space: pre-wrap;
}
.terminal-line.output .content {
color: var(--terminal-muted);
}
.terminal-line.error .content {
color: #f38ba8;
}
.terminal-line.success .content {
color: #a6e3a1;
}
.terminal-line.info .content {
color: var(--terminal-primary);
}
.terminal-line.header .content {
color: var(--terminal-accent);
font-weight: 600;
font-size: 1rem;
}
.header-text {
color: var(--terminal-accent);
font-weight: 600;
}
.terminal-image {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 0.5rem 0;
}
.terminal-image img {
border-radius: 8px;
border: 1px solid var(--terminal-border);
background: var(--terminal-bg-light);
object-fit: contain;
}
.image-caption {
color: var(--terminal-muted);
font-size: 0.8rem;
font-style: italic;
}
.cursor {
display: inline-block;
width: 8px;
height: 1em;
background: var(--terminal-primary);
animation: cursorBlink 1s step-end infinite;
margin-left: 2px;
vertical-align: text-bottom;
}
@keyframes cursorBlink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
/* Scrollbar styling */
.terminal-body::-webkit-scrollbar {
width: 8px;
}
.terminal-body::-webkit-scrollbar-track {
background: transparent;
}
.terminal-body::-webkit-scrollbar-thumb {
background: var(--terminal-border);
border-radius: 4px;
}
.terminal-body::-webkit-scrollbar-thumb:hover {
background: var(--terminal-muted);
}

View File

@@ -0,0 +1,73 @@
.tui-terminal {
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
background: var(--terminal-bg);
border: 2px solid var(--terminal-border);
border-radius: 8px;
overflow: hidden;
position: relative;
display: flex;
flex-direction: column;
width: 95%;
margin: 0 auto;
height: calc(100vh - var(--navbar-height) - 80px);
max-height: calc(100vh - var(--navbar-height) - 80px);
animation: tuiFadeIn 0.4s ease-out;
}
.tui-terminal:focus-within .tui-border-glow {
opacity: 1;
}
@keyframes tuiFadeIn {
from {
opacity: 0;
transform: scale(0.98);
}
to {
opacity: 1;
transform: scale(1);
}
}
/* Hyprland-style animated border glow */
.tui-border-glow {
position: absolute;
inset: -2px;
border-radius: 10px;
background: linear-gradient(
45deg,
var(--terminal-primary),
var(--terminal-accent),
var(--terminal-primary),
var(--terminal-accent)
);
background-size: 400% 400%;
animation: borderGlow 8s ease infinite;
opacity: 0.5;
z-index: -1;
filter: blur(4px);
transition: opacity 0.3s ease;
}
@keyframes borderGlow {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
.tui-content {
display: flex;
flex-direction: column;
height: 100%;
background: var(--terminal-bg);
position: relative;
z-index: 1;
}
/* Responsive */
@media (max-width: 768px) {
.tui-terminal {
width: 95%;
height: calc(100vh - var(--navbar-height) - 60px);
max-height: calc(100vh - var(--navbar-height) - 60px);
}
}

View File

@@ -0,0 +1,183 @@
.terminal {
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
background: var(--terminal-bg);
border: 1px solid var(--terminal-border);
border-radius: 12px;
overflow: hidden;
box-shadow:
0 25px 50px -12px rgba(0, 0, 0, 0.4),
0 0 0 1px rgba(255, 255, 255, 0.05) inset;
animation: terminalFadeIn 0.5s ease-out;
}
@keyframes terminalFadeIn {
from {
opacity: 0;
transform: translateY(20px) scale(0.98);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.terminal-header {
display: flex;
align-items: center;
padding: 0.75rem 1rem;
background: color-mix(in srgb, var(--terminal-bg) 80%, black);
border-bottom: 1px solid var(--terminal-border);
}
.terminal-buttons {
display: flex;
gap: 8px;
}
.terminal-buttons .btn {
width: 12px;
height: 12px;
border-radius: 50%;
cursor: pointer;
transition: opacity 0.2s;
}
.terminal-buttons .btn:hover {
opacity: 0.8;
}
.terminal-buttons .close {
background: #ff5f56;
}
.terminal-buttons .minimize {
background: #ffbd2e;
}
.terminal-buttons .maximize {
background: #27ca40;
}
.terminal-title {
flex: 1;
text-align: center;
font-size: 0.8rem;
color: var(--terminal-muted);
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.terminal-icon {
font-size: 1rem;
}
.terminal-spacer {
width: 52px;
}
.terminal-body {
padding: 1rem 1.25rem;
min-height: 200px;
max-height: 400px;
overflow-y: auto;
font-size: 0.9rem;
line-height: 1.6;
}
.terminal-line {
display: flex;
align-items: center;
flex-wrap: wrap;
margin-bottom: 0.25rem;
animation: lineSlideIn 0.2s ease-out;
}
@keyframes lineSlideIn {
from {
opacity: 0;
transform: translateX(-10px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.prompt {
display: inline-flex;
margin-right: 0.5rem;
flex-shrink: 0;
}
.prompt .user {
color: var(--terminal-user);
}
.prompt .separator {
color: var(--terminal-text);
}
.prompt .path {
color: var(--terminal-path);
}
.prompt .symbol {
color: var(--terminal-text);
margin-left: 0.25rem;
}
.content {
color: var(--terminal-text);
word-break: break-word;
}
.terminal-line.output .content {
color: var(--terminal-muted);
}
.terminal-line.error .content {
color: #ff6b6b;
}
.terminal-line.success .content {
color: #51cf66;
}
.terminal-line.info .content {
color: var(--terminal-primary);
}
.cursor {
display: inline-block;
width: 8px;
height: 1em;
background: var(--terminal-primary);
animation: cursorBlink 1s step-end infinite;
margin-left: 2px;
vertical-align: text-bottom;
}
@keyframes cursorBlink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
/* Scrollbar styling */
.terminal-body::-webkit-scrollbar {
width: 8px;
}
.terminal-body::-webkit-scrollbar-track {
background: transparent;
}
.terminal-body::-webkit-scrollbar-thumb {
background: var(--terminal-border);
border-radius: 4px;
}
.terminal-body::-webkit-scrollbar-thumb:hover {
background: var(--terminal-muted);
}

View File

@@ -0,0 +1,69 @@
.tui-accordion {
margin: 0.5rem 0;
border: 1px solid var(--terminal-border);
border-radius: 6px;
overflow: hidden;
}
.accordion-item {
border-bottom: 1px solid var(--terminal-border);
}
.accordion-item:last-child {
border-bottom: none;
}
.accordion-header {
display: flex;
align-items: center;
gap: 0.5rem;
width: 100%;
padding: 0.6rem 0.75rem;
background: transparent;
border: none;
color: var(--terminal-text);
font-family: inherit;
font-size: 0.85rem;
font-weight: 500;
text-align: left;
cursor: pointer;
transition: background 0.15s ease;
}
.accordion-header:hover {
background: rgba(255, 255, 255, 0.03);
}
.accordion-item.open .accordion-header {
background: rgba(0, 0, 0, 0.1);
border-bottom: 1px solid var(--terminal-border);
}
:global(.accordion-icon) {
color: var(--accordion-accent);
transition: transform 0.2s ease;
}
.accordion-title {
flex: 1;
}
.accordion-content {
padding: 0.75rem;
color: var(--terminal-muted);
font-size: 0.85rem;
line-height: 1.6;
white-space: pre-wrap;
animation: slideDown 0.2s ease-out;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

View File

@@ -0,0 +1,227 @@
.tui-body {
flex: 1;
padding: 1rem 1.25rem 2rem 1.25rem;
overflow-y: auto;
overflow-x: hidden;
font-size: 0.9rem;
line-height: 1.7;
min-height: 0;
}
/* Inline group wrapper - groups consecutive inline elements */
.tui-inline-group {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 0.2rem;
animation: lineSlideIn 0.15s ease-out;
}
.inline-content {
display: inline;
white-space: pre-wrap;
}
.inline-content.output {
color: var(--terminal-muted);
}
.inline-content.info {
color: var(--terminal-primary);
}
.inline-content.success {
color: #a6e3a1;
}
.inline-content.error {
color: #f38ba8;
}
.inline-content.warning {
color: #f9e2af;
}
/* Lines */
.tui-line {
display: flex;
align-items: flex-start;
flex-wrap: wrap;
margin-bottom: 0.2rem;
animation: lineSlideIn 0.15s ease-out;
min-height: 1.7em;
}
.tui-line.blank {
min-height: 0.5em;
}
@keyframes lineSlideIn {
from {
opacity: 0;
transform: translateX(-5px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
/* Prompt styling */
.prompt {
display: inline-flex;
margin-right: 0.5rem;
flex-shrink: 0;
}
.prompt .user {
color: var(--terminal-user);
}
.prompt .at {
color: var(--terminal-muted);
}
.prompt .host {
color: var(--terminal-accent);
}
.prompt .separator {
color: var(--terminal-muted);
}
.prompt .path {
color: var(--terminal-path);
}
.prompt .symbol {
color: var(--terminal-muted);
margin-left: 0.25rem;
}
/* Content */
.content {
color: var(--terminal-text);
word-break: break-word;
white-space: pre-wrap;
}
.tui-line.output .content {
color: var(--terminal-muted);
}
.tui-line.error .content {
color: #f38ba8;
}
.tui-line.success .content {
color: #a6e3a1;
}
.tui-line.info .content {
color: var(--terminal-primary);
}
.tui-line.warning .content {
color: #f9e2af;
}
.header-text {
color: var(--terminal-accent);
font-weight: 600;
font-size: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
:global(.header-icon) {
opacity: 0.7;
}
:global(.inline-icon) {
display: inline-block;
vertical-align: middle;
margin: 0 0.15em;
}
/* Divider */
.tui-divider {
display: flex;
align-items: center;
gap: 1rem;
margin: 0.75rem 0;
}
.divider-line {
flex: 1;
height: 1px;
background: linear-gradient(
to right,
transparent,
var(--terminal-border),
transparent
);
}
.divider-text {
color: var(--terminal-muted);
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.1em;
}
/* Images */
.tui-image {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 0.5rem 0;
}
.tui-image img {
border-radius: 6px;
border: 1px solid var(--terminal-border);
background: var(--terminal-bg-light);
object-fit: contain;
}
.image-caption {
color: var(--terminal-muted);
font-size: 0.8rem;
font-style: italic;
}
/* Cursor */
.cursor {
display: inline-block;
width: 8px;
height: 1em;
background: var(--terminal-primary);
animation: cursorBlink 1s step-end infinite;
margin-left: 2px;
vertical-align: text-bottom;
}
@keyframes cursorBlink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
/* Scrollbar */
.tui-body::-webkit-scrollbar {
width: 6px;
}
.tui-body::-webkit-scrollbar-track {
background: transparent;
}
.tui-body::-webkit-scrollbar-thumb {
background: var(--terminal-border);
border-radius: 3px;
}
.tui-body::-webkit-scrollbar-thumb:hover {
background: var(--terminal-muted);
}

View File

@@ -0,0 +1,60 @@
.tui-button {
display: flex;
align-items: center;
gap: 0.5rem;
width: 100%;
padding: 0.5rem 0.75rem;
margin: 0.2rem 0;
background: transparent;
border: 1px solid transparent;
border-radius: 4px;
color: var(--btn-color);
font-family: inherit;
font-size: 0.9rem;
text-align: left;
cursor: pointer;
transition: all 0.15s ease;
}
/* Inline button styles */
.tui-button.inline {
width: auto;
display: inline-flex;
margin: 0;
padding: 0.35rem 0.6rem;
border: 1px solid color-mix(in srgb, var(--btn-color) 40%, transparent);
}
.tui-button.inline .btn-indicator {
display: none;
}
.tui-button:hover,
.tui-button.selected {
background: color-mix(in srgb, var(--btn-color) 15%, transparent);
border-color: var(--btn-color);
}
.btn-indicator {
color: var(--btn-color);
font-size: 0.8rem;
width: 1rem;
}
.btn-text {
flex: 1;
}
:global(.btn-arrow) {
opacity: 0.5;
}
.tui-button.selected :global(.btn-arrow) {
opacity: 1;
}
:global(.inline-icon) {
display: inline-block;
vertical-align: middle;
margin: 0 0.15em;
}

View File

@@ -0,0 +1,222 @@
.tui-card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1rem;
padding: 0.5rem 0;
width: 100%;
}
.tui-card {
background: var(--terminal-bg);
border: 1px solid var(--terminal-border);
border-radius: 8px;
overflow: hidden;
display: flex;
flex-direction: column;
transition: all 0.2s ease;
font-family: 'JetBrains Mono', monospace;
}
.tui-card:hover {
border-color: var(--terminal-primary);
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
}
.tui-card.featured {
border-color: var(--terminal-accent);
}
.card-image {
position: relative;
width: 100%;
height: 140px;
overflow: hidden;
background: var(--terminal-bg-light);
}
.card-image img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.tui-card:hover .card-image img {
transform: scale(1.05);
}
.card-header-badge {
padding: 0.5rem 0.75rem 0;
}
.featured-badge {
position: absolute;
top: 0.5rem;
right: 0.5rem;
background: rgba(0, 0, 0, 0.75);
color: var(--terminal-accent);
padding: 0.15rem 0.4rem;
border-radius: 3px;
font-size: 0.65rem;
font-weight: 600;
backdrop-filter: blur(4px);
}
.card-header-badge .featured-badge {
position: static;
display: inline-block;
}
.card-body {
padding: 0.75rem;
display: flex;
flex-direction: column;
gap: 0.4rem;
flex: 1;
}
.card-title {
margin: 0;
font-size: 0.9rem;
font-weight: 600;
color: var(--terminal-text);
line-height: 1.3;
}
.card-meta {
display: flex;
align-items: center;
gap: 0.3rem;
font-size: 0.7rem;
color: var(--terminal-primary);
}
.meta-icon {
font-size: 0.75rem;
}
.hackathon-name {
font-weight: 500;
}
.year {
color: var(--terminal-muted);
}
.card-location {
display: flex;
align-items: center;
gap: 0.3rem;
font-size: 0.65rem;
color: var(--terminal-muted);
}
.awards {
display: flex;
flex-direction: column;
gap: 0.2rem;
margin: 0.25rem 0;
}
.award {
display: flex;
align-items: center;
gap: 0.3rem;
font-size: 0.7rem;
}
.award-icon {
font-size: 0.75rem;
}
.award-text {
color: #a6e3a1;
font-weight: 500;
}
.card-desc {
margin: 0;
font-size: 0.7rem;
color: var(--terminal-muted);
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.tags {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
margin-top: auto;
}
.tag {
background: color-mix(in srgb, var(--terminal-primary) 15%, transparent);
color: var(--terminal-primary);
padding: 0.1rem 0.35rem;
border-radius: 3px;
font-size: 0.55rem;
font-weight: 500;
text-transform: lowercase;
}
.warning {
font-size: 0.6rem;
color: #f9e2af;
padding: 0.2rem 0.35rem;
background: rgba(249, 226, 175, 0.1);
border-radius: 3px;
width: fit-content;
}
.card-actions {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
margin-top: 0.5rem;
padding-top: 0.5rem;
border-top: 1px solid var(--terminal-border);
}
.action-btn {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
background: transparent;
border: 1px solid var(--terminal-border);
border-radius: 4px;
color: var(--terminal-text);
font-size: 0.65rem;
font-weight: 500;
text-decoration: none;
transition: all 0.15s ease;
font-family: inherit;
}
.action-btn:hover {
background: var(--terminal-primary);
border-color: var(--terminal-primary);
color: var(--terminal-bg);
}
.action-btn.primary {
background: var(--terminal-primary);
border-color: var(--terminal-primary);
color: var(--terminal-bg);
}
.action-btn.primary:hover {
background: var(--terminal-accent);
border-color: var(--terminal-accent);
}
@media (max-width: 600px) {
.tui-card-grid {
grid-template-columns: 1fr;
}
}

View File

@@ -0,0 +1,58 @@
.tui-card {
border: 1px solid var(--terminal-border);
border-radius: 6px;
background: color-mix(in srgb, var(--terminal-bg) 80%, var(--card-accent) 5%);
margin: 0.5rem 0;
overflow: hidden;
transition: border-color 0.2s ease;
}
.tui-card:hover {
border-color: var(--card-accent);
}
.card-header {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.6rem 0.75rem;
border-bottom: 1px solid var(--terminal-border);
background: rgba(0, 0, 0, 0.1);
}
:global(.card-icon) {
color: var(--card-accent);
}
.card-title {
font-weight: 600;
color: var(--terminal-text);
font-size: 0.85rem;
}
.card-body {
padding: 0.75rem;
}
.card-image {
width: 100%;
max-height: 200px;
object-fit: cover;
border-radius: 4px;
margin-bottom: 0.5rem;
}
.card-content {
color: var(--terminal-muted);
font-size: 0.85rem;
line-height: 1.5;
white-space: pre-wrap;
}
.card-footer {
padding: 0.5rem 0.75rem;
border-top: 1px solid var(--terminal-border);
font-size: 0.75rem;
color: var(--terminal-muted);
background: rgba(0, 0, 0, 0.05);
}

View File

@@ -0,0 +1,55 @@
.tui-checkbox {
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0.35rem 0;
padding: 0.35rem 0.5rem;
font-size: 0.9rem;
cursor: pointer;
border-radius: 4px;
transition: all 0.15s ease;
user-select: none;
}
.tui-checkbox.inline {
display: inline-flex;
margin: 0 0.75rem 0 0;
}
.tui-checkbox:hover:not(.disabled) {
background: color-mix(in srgb, var(--checkbox-color) 10%, transparent);
}
.tui-checkbox:focus-visible {
outline: 1px solid var(--checkbox-color);
outline-offset: 2px;
}
.tui-checkbox.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.checkbox-box {
font-family: inherit;
font-weight: bold;
color: var(--terminal-muted);
transition: color 0.15s ease;
}
.tui-checkbox.checked .checkbox-box,
.checkbox-box.indeterminate {
color: var(--checkbox-color);
}
:global(.checkbox-icon) {
color: var(--checkbox-color);
}
.checkbox-label {
color: var(--terminal-text);
}
.tui-checkbox.disabled .checkbox-label {
color: var(--terminal-muted);
}

View File

@@ -0,0 +1,59 @@
.tui-statusbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.4rem 0.75rem;
font-size: 0.75rem;
background: var(--terminal-bg-light);
border-color: var(--terminal-border);
}
.tui-statusbar.bottom {
border-top: 1px solid var(--terminal-border);
}
.status-left, .status-right {
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--terminal-muted);
}
.status-center {
color: var(--terminal-primary);
font-weight: 600;
}
.skip-btn {
display: flex;
align-items: center;
gap: 0.3rem;
padding: 0.15rem 0.5rem;
background: var(--terminal-border);
border: 1px solid transparent;
border-radius: 3px;
color: var(--terminal-muted);
font-family: inherit;
font-size: 0.7rem;
cursor: pointer;
transition: all 0.15s ease;
}
.skip-btn:hover {
background: var(--terminal-primary);
color: var(--terminal-bg);
border-color: var(--terminal-primary);
}
.typing-indicator {
animation: pulse 1s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.line-count {
color: var(--terminal-muted);
}

View File

@@ -0,0 +1,38 @@
.tui-statusbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.4rem 0.75rem;
font-size: 0.75rem;
background: var(--terminal-bg-light);
border-color: var(--terminal-border);
}
.tui-statusbar.top {
border-bottom: 1px solid var(--terminal-border);
}
.status-left, .status-right {
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--terminal-muted);
}
.status-center {
color: var(--terminal-primary);
font-weight: 600;
}
.hint {
padding: 0.1rem 0.4rem;
background: var(--terminal-border);
border-radius: 3px;
font-size: 0.7rem;
}
@media (max-width: 768px) {
.hint {
display: none;
}
}

View File

@@ -0,0 +1,96 @@
.tui-input {
margin: 0.5rem 0;
font-size: 0.9rem;
}
.tui-input.inline {
display: inline-flex;
flex-direction: column;
margin: 0 0.5rem 0 0;
}
.input-label {
display: flex;
align-items: center;
gap: 0.35rem;
margin-bottom: 0.35rem;
color: var(--terminal-muted);
font-size: 0.85rem;
}
:global(.label-icon) {
color: var(--input-color);
}
.input-wrapper {
display: flex;
align-items: center;
gap: 0.35rem;
padding: 0.5rem 0.75rem;
background: color-mix(in srgb, var(--terminal-bg) 80%, black);
border: 1px solid var(--terminal-muted);
border-radius: 4px;
transition: all 0.15s ease;
}
.input-wrapper:focus-within {
border-color: var(--input-color);
box-shadow: 0 0 0 1px color-mix(in srgb, var(--input-color) 30%, transparent);
}
.tui-input.error .input-wrapper {
border-color: #f38ba8;
}
.tui-input.disabled .input-wrapper {
opacity: 0.5;
cursor: not-allowed;
}
.input-prompt {
color: var(--input-color);
font-weight: bold;
user-select: none;
}
.input-affix {
color: var(--terminal-muted);
font-size: 0.85rem;
}
.input-affix.prefix {
margin-right: 0.25rem;
}
.input-affix.suffix {
margin-left: 0.25rem;
}
input {
flex: 1;
min-width: 0;
background: transparent;
border: none;
color: var(--terminal-text);
font-family: inherit;
font-size: inherit;
outline: none;
}
input::placeholder {
color: var(--terminal-muted);
opacity: 0.6;
}
input:disabled {
cursor: not-allowed;
}
.input-error {
display: flex;
align-items: center;
gap: 0.35rem;
margin-top: 0.35rem;
color: #f38ba8;
font-size: 0.8rem;
}

View File

@@ -0,0 +1,45 @@
.tui-link {
display: inline-flex;
align-items: center;
gap: 0.25rem;
}
:global(.link-icon) {
color: var(--link-color);
opacity: 0.8;
}
.link-text {
background: none;
border: none;
padding: 0;
margin: 0;
font-family: inherit;
font-size: inherit;
color: var(--link-color);
text-decoration: underline;
text-decoration-style: dotted;
text-underline-offset: 2px;
cursor: pointer;
transition: all 0.15s ease;
}
.link-text:hover {
text-decoration-style: solid;
filter: brightness(1.2);
}
:global(.link-external) {
color: var(--link-color);
opacity: 0.5;
}
.tui-link:hover :global(.link-external) {
opacity: 0.8;
}
:global(.inline-icon) {
display: inline-block;
vertical-align: middle;
margin: 0 0.15em;
}

View File

@@ -0,0 +1,104 @@
.tui-progress {
margin: 0.5rem 0;
font-size: 0.85rem;
}
/* Inline progress styles */
.tui-progress.inline {
display: inline-flex;
align-items: center;
gap: 0.5rem;
margin: 0;
min-width: 120px;
}
.tui-progress.inline .progress-label {
margin-bottom: 0;
white-space: nowrap;
}
.tui-progress.inline .progress-bar {
flex: 1;
min-width: 80px;
height: 0.8rem;
}
.tui-progress.inline .progress-value {
margin-top: 0;
white-space: nowrap;
}
.progress-label {
color: var(--terminal-text);
margin-bottom: 0.25rem;
}
.progress-bar {
position: relative;
height: 1.2rem;
background: var(--terminal-border);
border-radius: 3px;
overflow: hidden;
}
.progress-fill {
position: absolute;
top: 0;
left: 0;
height: 100%;
background: linear-gradient(90deg, var(--progress-color), color-mix(in srgb, var(--progress-color) 80%, white 20%));
border-radius: 3px;
transition: width 0.3s ease;
}
.progress-glow {
position: absolute;
right: 0;
top: 0;
height: 100%;
width: 20px;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3));
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% { opacity: 0; }
50% { opacity: 1; }
100% { opacity: 0; }
}
.progress-blocks {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
gap: 2px;
padding: 3px;
}
.block {
flex: 1;
background: rgba(0, 0, 0, 0.2);
border-radius: 1px;
transition: background 0.2s;
}
.block.filled {
background: transparent;
}
.progress-value {
text-align: right;
color: var(--progress-color);
font-size: 0.75rem;
margin-top: 0.25rem;
font-weight: 600;
}
:global(.inline-icon) {
display: inline-block;
vertical-align: middle;
margin: 0 0.15em;
}

View File

@@ -0,0 +1,87 @@
.tui-radio-group {
margin: 0.5rem 0;
font-size: 0.9rem;
}
.tui-radio-group.inline {
display: inline-flex;
flex-direction: column;
margin: 0 0.75rem 0 0;
}
.tui-radio-group.disabled {
opacity: 0.5;
}
.radio-label {
display: flex;
align-items: center;
gap: 0.35rem;
margin-bottom: 0.5rem;
color: var(--terminal-muted);
font-size: 0.85rem;
}
:global(.label-icon) {
color: var(--radio-color);
}
.radio-options {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.radio-options.horizontal {
flex-direction: row;
flex-wrap: wrap;
gap: 0.75rem;
}
.radio-option {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.35rem 0.5rem;
cursor: pointer;
border-radius: 4px;
transition: all 0.15s ease;
user-select: none;
}
.radio-option:hover:not(.option-disabled) {
background: color-mix(in srgb, var(--radio-color) 10%, transparent);
}
.radio-option:focus-visible {
outline: 1px solid var(--radio-color);
outline-offset: 2px;
}
.radio-option.option-disabled {
opacity: 0.5;
cursor: not-allowed;
}
.radio-symbol {
font-family: inherit;
font-weight: bold;
color: var(--terminal-muted);
transition: color 0.15s ease;
}
.radio-option.selected .radio-symbol {
color: var(--radio-color);
}
:global(.option-icon) {
color: var(--radio-color);
}
.option-label {
color: var(--terminal-text);
}
.radio-option.option-disabled .option-label {
color: var(--terminal-muted);
}

View File

@@ -0,0 +1,162 @@
.tui-select {
position: relative;
margin: 0.5rem 0;
font-size: 0.9rem;
}
.tui-select.inline {
display: inline-flex;
flex-direction: column;
margin: 0 0.5rem 0 0;
}
.select-label {
display: flex;
align-items: center;
gap: 0.35rem;
margin-bottom: 0.35rem;
color: var(--terminal-muted);
font-size: 0.85rem;
}
:global(.label-icon) {
color: var(--select-color);
}
.select-trigger {
display: flex;
align-items: center;
gap: 0.35rem;
padding: 0.5rem 0.75rem;
background: color-mix(in srgb, var(--terminal-bg) 80%, black);
border: 1px solid var(--terminal-muted);
border-radius: 4px;
cursor: pointer;
transition: all 0.15s ease;
}
.tui-select.open .select-trigger,
.select-trigger:focus-visible {
border-color: var(--select-color);
box-shadow: 0 0 0 1px color-mix(in srgb, var(--select-color) 30%, transparent);
}
.tui-select.error .select-trigger {
border-color: #f38ba8;
}
.tui-select.disabled .select-trigger {
opacity: 0.5;
cursor: not-allowed;
}
.select-prompt {
color: var(--select-color);
font-weight: bold;
}
.select-value {
flex: 1;
color: var(--terminal-text);
}
.select-value.placeholder {
color: var(--terminal-muted);
opacity: 0.6;
}
.select-arrow {
color: var(--terminal-muted);
font-size: 0.7rem;
}
.select-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
margin-top: 0.25rem;
background: var(--terminal-bg);
border: 1px solid var(--select-color);
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
z-index: 100;
overflow: hidden;
}
.select-search {
display: flex;
align-items: center;
gap: 0.35rem;
padding: 0.5rem 0.75rem;
border-bottom: 1px solid var(--terminal-muted);
}
.select-search input {
flex: 1;
background: transparent;
border: none;
color: var(--terminal-text);
font-family: inherit;
font-size: inherit;
outline: none;
}
.select-search input::placeholder {
color: var(--terminal-muted);
}
.select-options {
max-height: 200px;
overflow-y: auto;
}
.select-option {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
cursor: pointer;
transition: background 0.1s ease;
}
.select-option.highlighted {
background: color-mix(in srgb, var(--select-color) 15%, transparent);
}
.select-option.selected {
color: var(--select-color);
}
.select-option.option-disabled {
opacity: 0.5;
cursor: not-allowed;
}
:global(.option-icon) {
color: var(--select-color);
}
.option-label {
flex: 1;
}
:global(.check-icon) {
color: var(--select-color);
}
.select-empty {
padding: 0.75rem;
text-align: center;
color: var(--terminal-muted);
font-style: italic;
}
.select-error {
display: flex;
align-items: center;
gap: 0.35rem;
margin-top: 0.35rem;
color: #f38ba8;
font-size: 0.8rem;
}

View File

@@ -0,0 +1,49 @@
.tui-table-wrapper {
margin: 0.5rem 0;
border: 1px solid var(--terminal-border);
border-radius: 6px;
overflow: hidden;
}
.table-title {
padding: 0.5rem 0.75rem;
font-weight: 600;
font-size: 0.85rem;
color: var(--terminal-text);
background: rgba(0, 0, 0, 0.1);
border-bottom: 1px solid var(--terminal-border);
}
.tui-table {
width: 100%;
border-collapse: collapse;
font-size: 0.8rem;
}
th {
padding: 0.5rem 0.75rem;
text-align: left;
font-weight: 600;
color: var(--table-accent);
background: rgba(0, 0, 0, 0.15);
border-bottom: 1px solid var(--terminal-border);
white-space: nowrap;
}
td {
padding: 0.4rem 0.75rem;
color: var(--terminal-text);
border-bottom: 1px solid var(--terminal-border);
}
tr:last-child td {
border-bottom: none;
}
tr.alt {
background: rgba(0, 0, 0, 0.03);
}
tr:hover {
background: color-mix(in srgb, var(--table-accent) 5%, transparent);
}

View File

@@ -0,0 +1,113 @@
.tui-textarea {
margin: 0.5rem 0;
font-size: 0.9rem;
}
.tui-textarea.inline {
display: inline-flex;
flex-direction: column;
margin: 0 0.5rem 0 0;
}
.textarea-label {
display: flex;
align-items: center;
gap: 0.35rem;
margin-bottom: 0.35rem;
color: var(--terminal-muted);
font-size: 0.85rem;
}
:global(.label-icon) {
color: var(--input-color);
}
.textarea-wrapper {
display: flex;
background: color-mix(in srgb, var(--terminal-bg) 80%, black);
border: 1px solid var(--terminal-muted);
border-radius: 4px;
transition: all 0.15s ease;
overflow: hidden;
}
.tui-textarea.focused .textarea-wrapper {
border-color: var(--input-color);
box-shadow: 0 0 0 1px color-mix(in srgb, var(--input-color) 30%, transparent);
}
.tui-textarea.error .textarea-wrapper {
border-color: #f38ba8;
}
.tui-textarea.disabled .textarea-wrapper {
opacity: 0.5;
cursor: not-allowed;
}
.line-numbers {
display: flex;
flex-direction: column;
padding: 0.5rem 0;
background: color-mix(in srgb, var(--terminal-bg) 60%, black);
border-right: 1px solid var(--terminal-muted);
user-select: none;
}
.line-num {
padding: 0 0.5rem;
color: var(--terminal-muted);
font-size: 0.8rem;
line-height: 1.5;
text-align: right;
min-width: 2rem;
}
textarea {
flex: 1;
min-width: 0;
padding: 0.5rem 0.75rem;
background: transparent;
border: none;
color: var(--terminal-text);
font-family: inherit;
font-size: inherit;
line-height: 1.5;
resize: vertical;
outline: none;
}
textarea::placeholder {
color: var(--terminal-muted);
opacity: 0.6;
}
textarea:disabled {
cursor: not-allowed;
resize: none;
}
.textarea-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 0.35rem;
}
.textarea-error {
display: flex;
align-items: center;
gap: 0.35rem;
color: #f38ba8;
font-size: 0.8rem;
}
.char-count {
margin-left: auto;
color: var(--terminal-muted);
font-size: 0.75rem;
}
.char-count.warning {
color: #f9e2af;
}

View File

@@ -0,0 +1,94 @@
.tui-toggle {
display: flex;
align-items: center;
gap: 0.75rem;
margin: 0.35rem 0;
padding: 0.35rem 0.5rem;
font-size: 0.9rem;
cursor: pointer;
border-radius: 4px;
transition: all 0.15s ease;
user-select: none;
}
.tui-toggle.inline {
display: inline-flex;
margin: 0 0.75rem 0 0;
}
.tui-toggle:hover:not(.disabled) {
background: color-mix(in srgb, var(--toggle-color) 10%, transparent);
}
.tui-toggle:focus-visible {
outline: 1px solid var(--toggle-color);
outline-offset: 2px;
}
.tui-toggle.disabled {
opacity: 0.5;
cursor: not-allowed;
}
:global(.toggle-icon) {
color: var(--toggle-color);
}
.toggle-label {
color: var(--terminal-text);
}
.toggle-track {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
background: color-mix(in srgb, var(--terminal-bg) 80%, black);
border: 1px solid var(--terminal-muted);
border-radius: 4px;
font-size: 0.75rem;
transition: all 0.15s ease;
}
.tui-toggle.checked .toggle-track {
border-color: var(--toggle-color);
background: color-mix(in srgb, var(--toggle-color) 15%, transparent);
}
.toggle-off-label,
.toggle-on-label {
color: var(--terminal-muted);
font-weight: bold;
text-transform: uppercase;
min-width: 2rem;
text-align: center;
transition: color 0.15s ease;
}
.tui-toggle:not(.checked) .toggle-off-label {
color: var(--terminal-text);
}
.tui-toggle.checked .toggle-on-label {
color: var(--toggle-color);
}
.toggle-switch {
display: flex;
align-items: center;
justify-content: center;
width: 2.5rem;
position: relative;
}
.toggle-knob {
font-size: 1rem;
color: var(--terminal-muted);
transition: all 0.2s ease;
transform: translateX(-0.5rem);
}
.tui-toggle.checked .toggle-knob {
color: var(--toggle-color);
transform: translateX(0.5rem);
}

View File

@@ -0,0 +1,78 @@
.tui-tooltip-trigger {
position: relative;
display: inline-flex;
align-items: center;
gap: 0.25rem;
cursor: help;
}
.trigger-text {
border-bottom: 1px dotted var(--tooltip-color);
}
.tooltip-indicator {
font-size: 0.7rem;
color: var(--tooltip-color);
opacity: 0.7;
}
.tooltip {
position: fixed;
z-index: 99999;
padding: 0.5rem 0.75rem;
background: var(--terminal-bg);
border: 1px solid var(--tooltip-color);
border-radius: 4px;
font-size: 0.8rem;
color: var(--terminal-text);
white-space: nowrap;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
animation: tooltipFadeIn 0.15s ease-out;
pointer-events: none;
}
@keyframes tooltipFadeIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
/* Arrow styles - simplified for fixed positioning */
.tooltip-arrow {
position: absolute;
width: 8px;
height: 8px;
background: var(--terminal-bg);
border: 1px solid var(--tooltip-color);
border-top: none;
border-left: none;
}
.tooltip.top .tooltip-arrow {
bottom: -5px;
left: 50%;
transform: translateX(-50%) rotate(45deg);
}
.tooltip.bottom .tooltip-arrow {
top: -5px;
left: 50%;
transform: translateX(-50%) rotate(-135deg);
}
.tooltip.left .tooltip-arrow {
right: -5px;
top: 50%;
transform: translateY(-50%) rotate(-45deg);
}
.tooltip.right .tooltip-arrow {
left: -5px;
top: 50%;
transform: translateY(-50%) rotate(135deg);
}

View File

@@ -4,6 +4,7 @@
import ParticleField from './ParticleField.svelte'; import ParticleField from './ParticleField.svelte';
import { themeColors, mode } from '$lib/stores/theme'; import { themeColors, mode } from '$lib/stores/theme';
import * as THREE from 'three'; import * as THREE from 'three';
import '$lib/assets/css/background-3d.css';
let bgColor = $derived($mode === 'dark' ? $themeColors.background : $themeColors.background); let bgColor = $derived($mode === 'dark' ? $themeColors.background : $themeColors.background);
</script> </script>
@@ -35,19 +36,3 @@
<T.Color args={[bgColor]} attach="background" /> <T.Color args={[bgColor]} attach="background" />
</Canvas> </Canvas>
</div> </div>
<style>
.scene-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
pointer-events: none;
}
.scene-container :global(canvas) {
display: block;
}
</style>

View File

@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { onMount, onDestroy } from 'svelte'; import { onMount, onDestroy } from 'svelte';
import { themeColors } from '$lib/stores/theme'; import { themeColors } from '$lib/stores/theme';
import '$lib/assets/css/model-viewer.css';
import * as THREE from 'three'; import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
@@ -505,178 +506,3 @@
</span> </span>
</div> </div>
</div> </div>
<style>
.model-viewer {
display: flex;
flex-direction: column;
background: var(--viewer-bg);
border: 1px solid var(--viewer-border);
border-radius: 8px;
overflow: hidden;
transition: all 0.3s ease;
}
.model-viewer.fullscreen {
position: fixed;
inset: 60px 0 0 0;
z-index: 100;
border-radius: 0;
border: none;
}
.viewer-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5rem 0.75rem;
background: color-mix(in srgb, var(--viewer-bg) 80%, black);
border-bottom: 1px solid var(--viewer-border);
}
.header-left {
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--viewer-primary);
font-size: 0.85rem;
font-weight: 500;
}
.header-controls {
display: flex;
gap: 0.25rem;
}
.control-btn {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
background: transparent;
border: 1px solid transparent;
border-radius: 4px;
color: var(--viewer-muted);
cursor: pointer;
transition: all 0.2s ease;
}
.control-btn:hover {
background: var(--viewer-border);
color: var(--viewer-text);
border-color: var(--viewer-border);
}
.control-btn.active {
background: var(--viewer-primary);
color: var(--viewer-bg);
border-color: var(--viewer-primary);
}
.control-btn.small {
width: 24px;
height: 24px;
}
/* Extended controls panel */
.controls-panel {
display: flex;
flex-wrap: wrap;
gap: 1rem;
padding: 0.5rem 0.75rem;
background: color-mix(in srgb, var(--viewer-bg) 90%, black);
border-bottom: 1px solid var(--viewer-border);
}
.control-group {
display: flex;
align-items: center;
gap: 0.5rem;
}
.control-label {
font-size: 0.7rem;
color: var(--viewer-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.control-buttons {
display: flex;
align-items: center;
gap: 0.25rem;
}
.control-value {
font-size: 0.7rem;
color: var(--viewer-text);
min-width: 2.5rem;
text-align: center;
font-family: 'JetBrains Mono', monospace;
}
.canvas-container {
flex: 1;
min-height: 300px;
position: relative;
background: radial-gradient(
circle at center,
color-mix(in srgb, var(--viewer-primary) 5%, transparent),
transparent 70%
);
outline: none;
}
.canvas-container:focus {
box-shadow: inset 0 0 0 2px var(--viewer-primary);
}
.canvas-container :global(canvas) {
display: block;
}
.loading-overlay,
.error-overlay {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.75rem;
color: var(--viewer-muted);
font-size: 0.9rem;
}
.error-overlay {
color: #f38ba8;
}
:global(.spin) {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.viewer-footer {
display: flex;
align-items: center;
justify-content: center;
gap: 1.5rem;
padding: 0.5rem;
background: color-mix(in srgb, var(--viewer-bg) 80%, black);
border-top: 1px solid var(--viewer-border);
}
.hint {
display: flex;
align-items: center;
gap: 0.35rem;
color: var(--viewer-muted);
font-size: 0.75rem;
}
</style>

View File

@@ -3,6 +3,7 @@
import { fly, fade } from 'svelte/transition'; import { fly, fade } from 'svelte/transition';
import { user, navigation } from '$lib/config'; import { user, navigation } from '$lib/config';
import Icon from '@iconify/svelte'; import Icon from '@iconify/svelte';
import '$lib/assets/css/navbar.css';
let themeDropdownOpen = $state(false); let themeDropdownOpen = $state(false);
let navExpanded = $state(false); let navExpanded = $state(false);
@@ -141,336 +142,3 @@
></button> ></button>
{/if} {/if}
</nav> </nav>
<style>
.navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
background: var(--nav-bg);
border-bottom: 1px solid var(--nav-border);
backdrop-filter: blur(10px);
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
}
.nav-container {
max-width: 1400px;
margin: 0 auto;
padding: 0.75rem 1.5rem;
display: flex;
align-items: center;
justify-content: space-between;
gap: 2rem;
}
.nav-brand {
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 0.9rem;
font-weight: 600;
}
.terminal-prompt {
display: flex;
align-items: center;
gap: 0;
}
.terminal-prompt .user {
color: var(--nav-accent);
}
.terminal-prompt .separator {
color: var(--nav-text);
}
.terminal-prompt .path {
color: var(--nav-primary);
}
.terminal-prompt .symbol {
color: var(--nav-text);
margin-left: 0.25rem;
}
.cursor {
width: 8px;
height: 1.2em;
background: var(--nav-primary);
animation: blink 1s step-end infinite;
margin-left: 0.25rem;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
.mobile-toggle {
display: none;
background: none;
border: none;
padding: 0.5rem;
cursor: pointer;
}
.hamburger {
display: block;
width: 24px;
height: 2px;
background: var(--nav-text);
position: relative;
transition: all 0.3s ease;
}
.hamburger::before,
.hamburger::after {
content: '';
position: absolute;
width: 24px;
height: 2px;
background: var(--nav-text);
transition: all 0.3s ease;
}
.hamburger::before {
top: -7px;
}
.hamburger::after {
top: 7px;
}
.hamburger.open {
background: transparent;
}
.hamburger.open::before {
top: 0;
transform: rotate(45deg);
}
.hamburger.open::after {
top: 0;
transform: rotate(-45deg);
}
.nav-links {
display: flex;
align-items: center;
gap: 1.5rem;
flex: 1;
}
.nav-link {
color: var(--nav-text);
text-decoration: none;
font-size: 0.85rem;
padding: 0.5rem 0.75rem;
border-radius: 4px;
transition: all 0.2s ease;
display: flex;
align-items: center;
position: relative;
overflow: hidden;
}
.nav-link::before {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 0;
height: 2px;
background: var(--nav-primary);
transition: width 0.3s ease;
}
.nav-link:hover::before {
width: 100%;
}
.nav-link:hover {
color: var(--nav-primary);
}
.link-prefix {
color: var(--nav-muted);
margin-right: 2px;
}
.nav-controls {
display: flex;
align-items: center;
gap: 1rem;
}
.theme-selector {
position: relative;
}
.theme-button {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
background: transparent;
border: 1px solid var(--nav-border);
border-radius: 6px;
color: var(--nav-text);
cursor: pointer;
font-family: inherit;
font-size: 0.8rem;
transition: all 0.2s ease;
}
.theme-button:hover {
border-color: var(--nav-primary);
background: color-mix(in srgb, var(--nav-primary) 10%, transparent);
}
.theme-icon {
font-size: 1rem;
display: flex;
align-items: center;
}
.theme-label {
text-transform: capitalize;
}
:global(.dropdown-arrow) {
transition: transform 0.2s ease;
}
:global(.dropdown-arrow.open) {
transform: rotate(180deg);
}
.theme-dropdown {
position: absolute;
top: calc(100% + 0.5rem);
right: 0;
min-width: 180px;
background: var(--nav-bg);
border: 1px solid var(--nav-border);
border-radius: 8px;
padding: 0.5rem;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
z-index: 1001;
}
.theme-option {
display: flex;
align-items: center;
gap: 0.75rem;
width: 100%;
padding: 0.625rem 0.75rem;
background: transparent;
border: none;
border-radius: 6px;
color: var(--nav-text);
cursor: pointer;
font-family: inherit;
font-size: 0.85rem;
text-align: left;
transition: all 0.2s ease;
}
.theme-option:hover {
background: color-mix(in srgb, var(--nav-primary) 15%, transparent);
}
.theme-option.active {
background: color-mix(in srgb, var(--nav-primary) 20%, transparent);
color: var(--nav-primary);
}
.option-icon {
font-size: 1.1rem;
display: flex;
align-items: center;
}
.option-label {
flex: 1;
}
:global(.check-mark) {
color: var(--nav-primary);
}
.mode-toggle {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
background: transparent;
border: 1px solid var(--nav-border);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
color: var(--nav-text);
}
.mode-toggle:hover {
border-color: var(--nav-primary);
background: color-mix(in srgb, var(--nav-primary) 10%, transparent);
color: var(--nav-primary);
}
.backdrop {
position: fixed;
inset: 0;
background: transparent;
z-index: 999;
border: none;
cursor: default;
}
@media (max-width: 768px) {
.nav-container {
flex-wrap: wrap;
padding: 0.75rem 1rem;
}
.mobile-toggle {
display: block;
order: 2;
}
.nav-links {
order: 4;
width: 100%;
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease, padding 0.3s ease;
}
.nav-links.expanded {
max-height: 200px;
padding-top: 1rem;
}
.nav-controls {
order: 3;
margin-left: auto;
margin-right: 1rem;
}
.theme-label {
display: none;
}
.theme-button {
padding: 0.5rem;
}
}
</style>

View File

@@ -1,13 +1,15 @@
<script lang="ts"> <script lang="ts">
import { mode, colorTheme, toggleMode, setColorTheme, themeOptions, themeColors, type ColorTheme } from '$lib/stores/theme'; import { mode, colorTheme, toggleMode, setColorTheme, themeOptions, themeColors, type ColorTheme } from '$lib/stores/theme';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { fly, fade } from 'svelte/transition'; import { fly, fade, slide } from 'svelte/transition';
import { user, navigation, colorPalette } from '$lib/config'; import { user, navigation, colorPalette } from '$lib/config';
import Icon from '@iconify/svelte'; import Icon from '@iconify/svelte';
import { onMount, onDestroy } from 'svelte'; import { onMount, onDestroy } from 'svelte';
import '$lib/assets/css/navbar-waybar.css';
// State // State
let themeDropdownOpen = $state(false); let themeDropdownOpen = $state(false);
let mobileMenuOpen = $state(false);
let currentTime = $state(new Date()); let currentTime = $state(new Date());
// Derived values (Svelte 5 runes) // Derived values (Svelte 5 runes)
@@ -69,9 +71,14 @@
function handleKeydown(event: KeyboardEvent) { function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') { if (event.key === 'Escape') {
themeDropdownOpen = false; themeDropdownOpen = false;
mobileMenuOpen = false;
} }
} }
function closeMobileMenu() {
mobileMenuOpen = false;
}
function getThemeIcon(theme: ColorTheme): string { function getThemeIcon(theme: ColorTheme): string {
switch (theme) { switch (theme) {
case 'arch': return 'mdi:arch'; case 'arch': return 'mdi:arch';
@@ -105,8 +112,18 @@
<img src="/favicon.png" alt="Blob Icon" width="16" /> <img src="/favicon.png" alt="Blob Icon" width="16" />
</a> </a>
<!-- Workspaces --> <!-- Mobile menu toggle -->
<div class="module workspaces"> <button
class="mobile-menu-toggle"
onclick={() => mobileMenuOpen = !mobileMenuOpen}
aria-expanded={mobileMenuOpen}
aria-label="Toggle navigation menu"
>
<Icon icon={mobileMenuOpen ? 'mdi:close' : 'mdi:menu'} width="20" />
</button>
<!-- Workspaces (desktop) -->
<div class="module workspaces desktop-only">
{#each navigation as nav} {#each navigation as nav}
{@const isActive = currentPath === nav.path || (nav.path !== '/' && currentPath.startsWith(nav.path))} {@const isActive = currentPath === nav.path || (nav.path !== '/' && currentPath.startsWith(nav.path))}
{#if nav.external} {#if nav.external}
@@ -133,8 +150,8 @@
{/each} {/each}
</div> </div>
<!-- Current window title --> <!-- Current window title (desktop) -->
<div class="module window-title"> <div class="module window-title desktop-only">
<span class="title-icon"> <span class="title-icon">
{#if currentPath === '/'} {#if currentPath === '/'}
<Icon icon="mdi:home" width="14" /> <Icon icon="mdi:home" width="14" />
@@ -209,296 +226,89 @@
</div> </div>
<!-- Backdrop --> <!-- Backdrop -->
{#if themeDropdownOpen} {#if themeDropdownOpen || mobileMenuOpen}
<button <button
class="backdrop" class="backdrop"
transition:fade={{ duration: 100 }} transition:fade={{ duration: 100 }}
onclick={() => themeDropdownOpen = false} onclick={() => { themeDropdownOpen = false; mobileMenuOpen = false; }}
aria-label="Close" aria-label="Close"
></button> ></button>
{/if} {/if}
</nav> </nav>
<style> <!-- Mobile menu dropdown -->
.waybar { {#if mobileMenuOpen}
position: fixed; <div
top: 0; class="mobile-menu"
left: 0; transition:slide={{ duration: 200 }}
right: 0; style="
z-index: 1000; --bar-bg: {$themeColors.background};
height: var(--navbar-height, 40px); --bar-bg-module: {$themeColors.backgroundLight};
background: var(--bar-bg); --bar-border: {$themeColors.border};
border-bottom: 1px solid var(--bar-border); --bar-text: {$themeColors.text};
display: flex; --bar-primary: {$themeColors.primary};
align-items: center; --bar-accent: {$themeColors.accent};
justify-content: space-between; --bar-muted: {$themeColors.textMuted};
padding: 0 0.5rem; "
font-family: 'JetBrains Mono', 'Fira Code', monospace; >
font-size: 0.8rem; <div class="mobile-nav-links">
gap: 0.5rem; {#each navigation as nav}
} {@const isActive = currentPath === nav.path || (nav.path !== '/' && currentPath.startsWith(nav.path))}
{#if nav.external}
.bar-left, <a
.bar-center, href={nav.path}
.bar-right { class="mobile-nav-link external"
display: flex; target="_blank"
align-items: center; rel="noopener noreferrer"
gap: 0.5rem; onclick={closeMobileMenu}
} >
<Icon icon="mdi:open-in-new" width="16" />
.bar-left { <span>{nav.name}</span>
flex: 1; </a>
} {:else}
<a
.bar-right { href={nav.path}
flex: 1; class="mobile-nav-link"
justify-content: flex-end; class:active={isActive}
} onclick={closeMobileMenu}
>
/* Modules */ {#if nav.path === '/'}
.module { <Icon icon="mdi:home" width="16" />
display: flex; {:else if nav.path === '/portfolio'}
align-items: center; <Icon icon="mdi:folder-multiple" width="16" />
gap: 0.4rem; {:else if nav.path === '/models'}
padding: 0.35rem 0.6rem; <Icon icon="mdi:cube-outline" width="16" />
background: var(--bar-bg-module); {:else if nav.path === '/projects'}
border-radius: 4px; <Icon icon="mdi:trophy" width="16" />
color: var(--bar-text); {:else}
transition: all 0.15s ease; <Icon icon="mdi:file" width="16" />
} {/if}
<span>{nav.name}</span>
.module:hover { </a>
background: color-mix(in srgb, var(--bar-primary) 20%, var(--bar-bg-module)); {/if}
} {/each}
</div>
<div class="mobile-menu-divider"></div>
/* module-group removed (minimal waybar look uses individual modules) */
<div class="mobile-theme-section">
/* Launcher */ <div class="mobile-section-header">Theme</div>
.launcher { <div class="mobile-theme-options">
color: var(--bar-primary); {#each themeOptions as option}
padding: 0.35rem 0.5rem; <button
} class="mobile-theme-btn"
class:active={$colorTheme === option.value}
.launcher:hover { onclick={() => { handleThemeSelect(option.value); closeMobileMenu(); }}
color: var(--bar-accent); >
} <Icon icon={getThemeIcon(option.value)} width="18" />
<span>{option.label}</span>
/* Workspaces */ </button>
.workspaces { {/each}
display: flex; </div>
gap: 0.25rem; <button class="mobile-mode-btn" onclick={() => { toggleMode(); closeMobileMenu(); }}>
padding: 0.2rem 0.3rem; <Icon icon={$mode === 'dark' ? 'mdi:weather-sunny' : 'mdi:weather-night'} width="18" />
} <span>{$mode === 'dark' ? 'Switch to Light' : 'Switch to Dark'}</span>
</button>
.workspace { </div>
display: flex; </div>
align-items: center; {/if}
justify-content: center;
gap: 0.3rem;
padding: 0.3rem 0.6rem;
background: transparent;
border-radius: 4px;
color: var(--bar-muted);
font-size: 0.75rem;
font-weight: 500;
transition: all 0.15s ease;
text-decoration: none;
white-space: nowrap;
}
.workspace:hover {
background: color-mix(in srgb, var(--bar-primary) 25%, transparent);
color: var(--bar-text);
}
.workspace.active {
background: var(--bar-primary);
color: var(--bar-bg);
font-weight: 600;
}
.workspace .ws-name {
font-size: 0.75rem;
}
.workspace.external {
color: var(--bar-muted);
font-size: 0.7rem;
}
.workspace.external:hover {
color: var(--bar-accent);
}
/* Window title */
.window-title {
color: var(--bar-muted);
font-size: 0.75rem;
max-width: 200px;
overflow: hidden;
}
.title-icon {
display: flex;
color: var(--bar-primary);
}
.title-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Clock */
.clock {
background: var(--bar-bg-module);
padding: 0.35rem 0.75rem;
}
.clock .time {
font-weight: 600;
color: var(--bar-text);
}
.clock .date {
color: var(--bar-muted);
font-size: 0.7rem;
margin-left: 0.5rem;
}
/* System modules removed */
/* Theme selector */
.theme-selector {
position: relative;
padding: 0;
background: transparent;
}
.theme-trigger {
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
background: var(--bar-bg-module);
border: none;
border-radius: 4px;
color: var(--bar-text);
cursor: pointer;
transition: all 0.15s ease;
margin: 5px;
}
.theme-trigger:hover {
background: color-mix(in srgb, var(--bar-primary) 25%, var(--bar-bg-module));
color: var(--bar-primary);
}
.theme-dropdown {
position: absolute;
top: calc(100% + 0.5rem);
right: 0;
min-width: 160px;
background: var(--bar-bg);
border: 1px solid var(--bar-border);
border-radius: 8px;
padding: 0.4rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
z-index: 1001;
}
.dropdown-header {
padding: 0.4rem 0.6rem;
font-size: 0.65rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--bar-muted);
}
.dropdown-divider {
height: 1px;
background: var(--bar-border);
margin: 0.3rem 0;
}
.theme-option {
display: flex;
align-items: center;
gap: 0.5rem;
width: 100%;
padding: 0.5rem 0.6rem;
background: transparent;
border: none;
border-radius: 4px;
color: var(--bar-text);
font-family: inherit;
font-size: 0.75rem;
text-align: left;
cursor: pointer;
transition: all 0.15s ease;
}
.theme-option:hover {
background: color-mix(in srgb, var(--bar-primary) 15%, transparent);
}
.theme-option.active {
color: var(--bar-primary);
}
.theme-option span {
flex: 1;
}
:global(.check) {
color: var(--bar-primary);
}
/* Backdrop */
.backdrop {
position: fixed;
inset: 0;
background: transparent;
z-index: 999;
border: none;
cursor: default;
}
/* Responsive */
@media (max-width: 768px) {
.waybar {
padding: 0 0.25rem;
}
.window-title {
display: none;
}
.clock .date {
display: none;
}
}
@media (max-width: 480px) {
.workspaces {
gap: 0.15rem;
}
.workspace {
padding: 0.25rem 0.4rem;
font-size: 0.65rem;
}
.workspace .ws-name {
font-size: 0.65rem;
}
}
</style>

View File

@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { themeColors } from '$lib/stores/theme'; import { themeColors } from '$lib/stores/theme';
import '$lib/assets/css/terminal.css';
interface Props { interface Props {
lines?: TerminalLine[]; lines?: TerminalLine[];
@@ -162,189 +163,3 @@
{/if} {/if}
</div> </div>
</div> </div>
<style>
.terminal {
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
background: var(--terminal-bg);
border: 1px solid var(--terminal-border);
border-radius: 12px;
overflow: hidden;
box-shadow:
0 25px 50px -12px rgba(0, 0, 0, 0.4),
0 0 0 1px rgba(255, 255, 255, 0.05) inset;
animation: terminalFadeIn 0.5s ease-out;
}
@keyframes terminalFadeIn {
from {
opacity: 0;
transform: translateY(20px) scale(0.98);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.terminal-header {
display: flex;
align-items: center;
padding: 0.75rem 1rem;
background: color-mix(in srgb, var(--terminal-bg) 80%, black);
border-bottom: 1px solid var(--terminal-border);
}
.terminal-buttons {
display: flex;
gap: 8px;
}
.terminal-buttons .btn {
width: 12px;
height: 12px;
border-radius: 50%;
cursor: pointer;
transition: opacity 0.2s;
}
.terminal-buttons .btn:hover {
opacity: 0.8;
}
.terminal-buttons .close {
background: #ff5f56;
}
.terminal-buttons .minimize {
background: #ffbd2e;
}
.terminal-buttons .maximize {
background: #27ca40;
}
.terminal-title {
flex: 1;
text-align: center;
font-size: 0.8rem;
color: var(--terminal-muted);
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.terminal-icon {
font-size: 1rem;
}
.terminal-spacer {
width: 52px;
}
.terminal-body {
padding: 1rem 1.25rem;
min-height: 200px;
max-height: 400px;
overflow-y: auto;
font-size: 0.9rem;
line-height: 1.6;
}
.terminal-line {
display: flex;
align-items: center;
flex-wrap: wrap;
margin-bottom: 0.25rem;
animation: lineSlideIn 0.2s ease-out;
}
@keyframes lineSlideIn {
from {
opacity: 0;
transform: translateX(-10px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.prompt {
display: inline-flex;
margin-right: 0.5rem;
flex-shrink: 0;
}
.prompt .user {
color: var(--terminal-user);
}
.prompt .separator {
color: var(--terminal-text);
}
.prompt .path {
color: var(--terminal-path);
}
.prompt .symbol {
color: var(--terminal-text);
margin-left: 0.25rem;
}
.content {
color: var(--terminal-text);
word-break: break-word;
}
.terminal-line.output .content {
color: var(--terminal-muted);
}
.terminal-line.error .content {
color: #ff6b6b;
}
.terminal-line.success .content {
color: #51cf66;
}
.terminal-line.info .content {
color: var(--terminal-primary);
}
.cursor {
display: inline-block;
width: 8px;
height: 1.2em;
background: var(--terminal-primary);
animation: cursorBlink 1s step-end infinite;
margin-left: 2px;
vertical-align: middle;
}
@keyframes cursorBlink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
/* Scrollbar styling */
.terminal-body::-webkit-scrollbar {
width: 8px;
}
.terminal-body::-webkit-scrollbar-track {
background: transparent;
}
.terminal-body::-webkit-scrollbar-thumb {
background: var(--terminal-border);
border-radius: 4px;
}
.terminal-body::-webkit-scrollbar-thumb:hover {
background: var(--terminal-muted);
}
</style>

View File

@@ -3,6 +3,7 @@
import { themeColors } from '$lib/stores/theme'; import { themeColors } from '$lib/stores/theme';
import { user, terminalSettings } from '$lib/config'; import { user, terminalSettings } from '$lib/config';
import { calculateTypeSpeed } from '$lib'; import { calculateTypeSpeed } from '$lib';
import '$lib/assets/css/terminal-page.css';
export type LineType = 'command' | 'output' | 'prompt' | 'error' | 'success' | 'info' | 'image' | 'blank' | 'header'; export type LineType = 'command' | 'output' | 'prompt' | 'error' | 'success' | 'info' | 'image' | 'blank' | 'header';
@@ -217,229 +218,3 @@
{/if} {/if}
</div> </div>
</div> </div>
<style>
.terminal-page {
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
background: var(--terminal-bg);
border: 1px solid var(--terminal-border);
border-radius: 12px;
overflow: hidden;
box-shadow:
0 25px 50px -12px rgba(0, 0, 0, 0.4),
0 0 0 1px rgba(255, 255, 255, 0.05) inset;
animation: terminalFadeIn 0.5s ease-out;
display: flex;
flex-direction: column;
height: 100%;
}
@keyframes terminalFadeIn {
from {
opacity: 0;
transform: translateY(20px) scale(0.98);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.terminal-header {
display: flex;
align-items: center;
padding: 0.75rem 1rem;
background: var(--terminal-bg-light);
border-bottom: 1px solid var(--terminal-border);
flex-shrink: 0;
}
.terminal-buttons {
display: flex;
gap: 8px;
}
.terminal-buttons .btn {
width: 12px;
height: 12px;
border-radius: 50%;
cursor: pointer;
transition: opacity 0.2s;
}
.terminal-buttons .btn:hover {
opacity: 0.8;
}
.terminal-buttons .close {
background: #ff5f56;
}
.terminal-buttons .minimize {
background: #ffbd2e;
}
.terminal-buttons .maximize {
background: #27ca40;
}
.terminal-title {
flex: 1;
text-align: center;
font-size: 0.8rem;
color: var(--terminal-muted);
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.terminal-icon {
font-size: 1rem;
}
.terminal-spacer {
width: 52px;
}
.terminal-body {
padding: 1rem 1.25rem;
flex: 1;
overflow-y: auto;
font-size: 0.9rem;
line-height: 1.6;
}
.terminal-line {
display: flex;
align-items: flex-start;
flex-wrap: wrap;
margin-bottom: 0.25rem;
animation: lineSlideIn 0.2s ease-out;
min-height: 1.6em;
}
.terminal-line.blank {
min-height: 0.8em;
}
@keyframes lineSlideIn {
from {
opacity: 0;
transform: translateX(-10px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.prompt {
display: inline-flex;
margin-right: 0.5rem;
flex-shrink: 0;
}
.prompt .user {
color: var(--terminal-user);
}
.prompt .separator {
color: var(--terminal-text);
}
.prompt .path {
color: var(--terminal-path);
}
.prompt .symbol {
color: var(--terminal-text);
margin-left: 0.25rem;
}
.content {
color: var(--terminal-text);
word-break: break-word;
white-space: pre-wrap;
}
.terminal-line.output .content {
color: var(--terminal-muted);
}
.terminal-line.error .content {
color: #f38ba8;
}
.terminal-line.success .content {
color: #a6e3a1;
}
.terminal-line.info .content {
color: var(--terminal-primary);
}
.terminal-line.header .content {
color: var(--terminal-accent);
font-weight: 600;
font-size: 1rem;
}
.header-text {
color: var(--terminal-accent);
font-weight: 600;
}
.terminal-image {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 0.5rem 0;
}
.terminal-image img {
border-radius: 8px;
border: 1px solid var(--terminal-border);
background: var(--terminal-bg-light);
object-fit: contain;
}
.image-caption {
color: var(--terminal-muted);
font-size: 0.8rem;
font-style: italic;
}
.cursor {
display: inline-block;
width: 8px;
height: 1.2em;
background: var(--terminal-primary);
animation: cursorBlink 1s step-end infinite;
margin-left: 2px;
vertical-align: middle;
}
@keyframes cursorBlink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
/* Scrollbar styling */
.terminal-body::-webkit-scrollbar {
width: 8px;
}
.terminal-body::-webkit-scrollbar-track {
background: transparent;
}
.terminal-body::-webkit-scrollbar-thumb {
background: var(--terminal-border);
border-radius: 4px;
}
.terminal-body::-webkit-scrollbar-thumb:hover {
background: var(--terminal-muted);
}
</style>

View File

@@ -9,6 +9,7 @@
import TuiBody from './tui/TuiBody.svelte'; import TuiBody from './tui/TuiBody.svelte';
import TuiFooter from './tui/TuiFooter.svelte'; import TuiFooter from './tui/TuiFooter.svelte';
import { parseColorText, getPlainText } from './tui/utils'; import { parseColorText, getPlainText } from './tui/utils';
import '$lib/assets/css/terminal-tui.css';
import type { TerminalLine, ParsedLine, DisplayedLine } from './tui/types'; import type { TerminalLine, ParsedLine, DisplayedLine } from './tui/types';
@@ -365,80 +366,3 @@
<TuiFooter isTyping={isTyping} linesCount={displayedLines.length} skipAnimation={skipAnimation} /> <TuiFooter isTyping={isTyping} linesCount={displayedLines.length} skipAnimation={skipAnimation} />
</div> </div>
</div> </div>
<style>
.tui-terminal {
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
background: var(--terminal-bg);
border: 2px solid var(--terminal-border);
border-radius: 8px;
overflow: hidden;
position: relative;
display: flex;
flex-direction: column;
width: 95%;
margin: 0 auto;
height: calc(100vh - var(--navbar-height) - 80px);
max-height: calc(100vh - var(--navbar-height) - 80px);
animation: tuiFadeIn 0.4s ease-out;
}
.tui-terminal:focus-within .tui-border-glow {
opacity: 1;
}
@keyframes tuiFadeIn {
from {
opacity: 0;
transform: scale(0.98);
}
to {
opacity: 1;
transform: scale(1);
}
}
/* Hyprland-style animated border glow */
.tui-border-glow {
position: absolute;
inset: -2px;
border-radius: 10px;
background: linear-gradient(
45deg,
var(--terminal-primary),
var(--terminal-accent),
var(--terminal-primary),
var(--terminal-accent)
);
background-size: 400% 400%;
animation: borderGlow 8s ease infinite;
opacity: 0.5;
z-index: -1;
filter: blur(4px);
transition: opacity 0.3s ease;
}
@keyframes borderGlow {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
.tui-content {
display: flex;
flex-direction: column;
height: 100%;
background: var(--terminal-bg);
position: relative;
z-index: 1;
}
/* Responsive */
@media (max-width: 768px) {
.tui-terminal {
width: 95%;
height: calc(100vh - var(--navbar-height) - 60px);
max-height: calc(100vh - var(--navbar-height) - 60px);
}
}
</style>

View File

@@ -2,6 +2,7 @@
import Icon from '@iconify/svelte'; import Icon from '@iconify/svelte';
import { getButtonStyle, parseColorText, getSegmentStyle } from './utils'; import { getButtonStyle, parseColorText, getSegmentStyle } from './utils';
import type { TerminalLine } from './types'; import type { TerminalLine } from './types';
import '$lib/assets/css/tui-accordion.css';
export let line: TerminalLine; export let line: TerminalLine;

View File

@@ -10,7 +10,14 @@
import TuiTable from './TuiTable.svelte'; import TuiTable from './TuiTable.svelte';
import TuiTooltip from './TuiTooltip.svelte'; import TuiTooltip from './TuiTooltip.svelte';
import TuiCardGrid from './TuiCardGrid.svelte'; import TuiCardGrid from './TuiCardGrid.svelte';
import TuiInput from './TuiInput.svelte';
import TuiTextarea from './TuiTextarea.svelte';
import TuiCheckbox from './TuiCheckbox.svelte';
import TuiRadio from './TuiRadio.svelte';
import TuiSelect from './TuiSelect.svelte';
import TuiToggle from './TuiToggle.svelte';
import type { DisplayedLine } from './types'; import type { DisplayedLine } from './types';
import '$lib/assets/css/tui-body.css';
export let displayedLines: DisplayedLine[] = []; export let displayedLines: DisplayedLine[] = [];
export let currentLineIndex = 0; export let currentLineIndex = 0;
@@ -71,6 +78,12 @@
<TuiTooltip {line} /> <TuiTooltip {line} />
{:else if line.type === 'progress'} {:else if line.type === 'progress'}
<TuiProgress {line} inline={true} /> <TuiProgress {line} inline={true} />
{:else if line.type === 'input'}
<TuiInput {line} inline={true} />
{:else if line.type === 'checkbox'}
<TuiCheckbox {line} inline={true} />
{:else if line.type === 'toggle'}
<TuiToggle {line} inline={true} />
{:else} {:else}
<span class="inline-content {line.type}"> <span class="inline-content {line.type}">
{getLinePrefix(line.type)}{#each visibleSegments as segment}{#if segment.icon}<Icon icon={segment.icon} width="14" class="inline-icon" />{:else if getSegmentStyle(segment)}<span style={getSegmentStyle(segment)}>{segment.text}</span>{:else}{segment.text}{/if}{/each} {getLinePrefix(line.type)}{#each visibleSegments as segment}{#if segment.icon}<Icon icon={segment.icon} width="14" class="inline-icon" />{:else if getSegmentStyle(segment)}<span style={getSegmentStyle(segment)}>{segment.text}</span>{:else}{segment.text}{/if}{/each}
@@ -113,6 +126,18 @@
<div class="tui-line"> <div class="tui-line">
<TuiTooltip {line} /> <TuiTooltip {line} />
</div> </div>
{:else if line.type === 'input'}
<TuiInput {line} />
{:else if line.type === 'textarea'}
<TuiTextarea {line} />
{:else if line.type === 'checkbox'}
<TuiCheckbox {line} />
{:else if line.type === 'radio'}
<TuiRadio {line} />
{:else if line.type === 'select'}
<TuiSelect {line} />
{:else if line.type === 'toggle'}
<TuiToggle {line} />
{:else} {:else}
<div class="tui-line {line.type}" class:complete id={line.id}> <div class="tui-line {line.type}" class:complete id={line.id}>
{#if line.type === 'command' || line.type === 'prompt'} {#if line.type === 'command' || line.type === 'prompt'}
@@ -167,232 +192,4 @@
{/if} {/if}
</div> </div>
<style>
.tui-body {
flex: 1;
padding: 1rem 1.25rem 2rem 1.25rem;
overflow-y: auto;
overflow-x: hidden;
font-size: 0.9rem;
line-height: 1.7;
min-height: 0;
}
/* Inline group wrapper - groups consecutive inline elements */
.tui-inline-group {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 0.2rem;
animation: lineSlideIn 0.15s ease-out;
}
.inline-content {
display: inline;
white-space: pre-wrap;
}
.inline-content.output {
color: var(--terminal-muted);
}
.inline-content.info {
color: var(--terminal-primary);
}
.inline-content.success {
color: #a6e3a1;
}
.inline-content.error {
color: #f38ba8;
}
.inline-content.warning {
color: #f9e2af;
}
/* Lines */
.tui-line {
display: flex;
align-items: flex-start;
flex-wrap: wrap;
margin-bottom: 0.2rem;
animation: lineSlideIn 0.15s ease-out;
min-height: 1.7em;
}
.tui-line.blank {
min-height: 0.5em;
}
@keyframes lineSlideIn {
from {
opacity: 0;
transform: translateX(-5px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
/* Prompt styling */
.prompt {
display: inline-flex;
margin-right: 0.5rem;
flex-shrink: 0;
}
.prompt .user {
color: var(--terminal-user);
}
.prompt .at {
color: var(--terminal-muted);
}
.prompt .host {
color: var(--terminal-accent);
}
.prompt .separator {
color: var(--terminal-muted);
}
.prompt .path {
color: var(--terminal-path);
}
.prompt .symbol {
color: var(--terminal-muted);
margin-left: 0.25rem;
}
/* Content */
.content {
color: var(--terminal-text);
word-break: break-word;
white-space: pre-wrap;
}
.tui-line.output .content {
color: var(--terminal-muted);
}
.tui-line.error .content {
color: #f38ba8;
}
.tui-line.success .content {
color: #a6e3a1;
}
.tui-line.info .content {
color: var(--terminal-primary);
}
.tui-line.warning .content {
color: #f9e2af;
}
.header-text {
color: var(--terminal-accent);
font-weight: 600;
font-size: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
:global(.header-icon) {
opacity: 0.7;
}
:global(.inline-icon) {
display: inline-block;
vertical-align: middle;
margin: 0 0.15em;
}
/* Divider */
.tui-divider {
display: flex;
align-items: center;
gap: 1rem;
margin: 0.75rem 0;
}
.divider-line {
flex: 1;
height: 1px;
background: linear-gradient(
to right,
transparent,
var(--terminal-border),
transparent
);
}
.divider-text {
color: var(--terminal-muted);
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.1em;
}
/* Images */
.tui-image {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 0.5rem 0;
}
.tui-image img {
border-radius: 6px;
border: 1px solid var(--terminal-border);
background: var(--terminal-bg-light);
object-fit: contain;
}
.image-caption {
color: var(--terminal-muted);
font-size: 0.8rem;
font-style: italic;
}
/* Cursor */
.cursor {
display: inline-block;
width: 8px;
height: 1.2em;
background: var(--terminal-primary);
animation: cursorBlink 1s step-end infinite;
margin-left: 2px;
vertical-align: middle;
}
@keyframes cursorBlink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
/* Scrollbar */
.tui-body::-webkit-scrollbar {
width: 6px;
}
.tui-body::-webkit-scrollbar-track {
background: transparent;
}
.tui-body::-webkit-scrollbar-thumb {
background: var(--terminal-border);
border-radius: 3px;
}
.tui-body::-webkit-scrollbar-thumb:hover {
background: var(--terminal-muted);
}
</style>

View File

@@ -2,6 +2,7 @@
import Icon from '@iconify/svelte'; import Icon from '@iconify/svelte';
import { getButtonStyle, parseColorText, getSegmentStyle } from './utils'; import { getButtonStyle, parseColorText, getSegmentStyle } from './utils';
import type { TerminalLine } from './types'; import type { TerminalLine } from './types';
import '$lib/assets/css/tui-button.css';
export let line: TerminalLine; export let line: TerminalLine;
export let index: number; export let index: number;
@@ -48,66 +49,3 @@
{/if} {/if}
{/if} {/if}
</button> </button>
<style>
.tui-button {
display: flex;
align-items: center;
gap: 0.5rem;
width: 100%;
padding: 0.5rem 0.75rem;
margin: 0.2rem 0;
background: transparent;
border: 1px solid transparent;
border-radius: 4px;
color: var(--btn-color);
font-family: inherit;
font-size: 0.9rem;
text-align: left;
cursor: pointer;
transition: all 0.15s ease;
}
/* Inline button styles */
.tui-button.inline {
width: auto;
display: inline-flex;
margin: 0;
padding: 0.35rem 0.6rem;
border: 1px solid color-mix(in srgb, var(--btn-color) 40%, transparent);
}
.tui-button.inline .btn-indicator {
display: none;
}
.tui-button:hover,
.tui-button.selected {
background: color-mix(in srgb, var(--btn-color) 15%, transparent);
border-color: var(--btn-color);
}
.btn-indicator {
color: var(--btn-color);
font-size: 0.8rem;
width: 1rem;
}
.btn-text {
flex: 1;
}
:global(.btn-arrow) {
opacity: 0.5;
}
.tui-button.selected :global(.btn-arrow) {
opacity: 1;
}
:global(.inline-icon) {
display: inline-block;
vertical-align: middle;
margin: 0 0.15em;
}
</style>

View File

@@ -2,6 +2,7 @@
import Icon from '@iconify/svelte'; import Icon from '@iconify/svelte';
import { getButtonStyle, parseColorText, getSegmentStyle } from './utils'; import { getButtonStyle, parseColorText, getSegmentStyle } from './utils';
import type { TerminalLine } from './types'; import type { TerminalLine } from './types';
import '$lib/assets/css/tui-card.css';
export let line: TerminalLine; export let line: TerminalLine;

View File

@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import Icon from '@iconify/svelte'; import Icon from '@iconify/svelte';
import type { TerminalLine } from './types'; import type { TerminalLine } from './types';
import '$lib/assets/css/tui-card-grid.css';
export let line: TerminalLine; export let line: TerminalLine;
@@ -56,12 +57,12 @@
{#if card.tags && card.tags.length > 0} {#if card.tags && card.tags.length > 0}
<div class="tags"> <div class="tags">
{#each card.tags.slice(0, 5) as tag} {#each card.tags as tag}
<span class="tag">{tag}</span> <span class="tag">{tag}</span>
{/each} {/each}
{#if card.tags.length > 5} <!-- {#if card.tags.length > 5}
<span class="tag more">+{card.tags.length - 5}</span> <span class="tag more">+{card.tags.length - 5}</span>
{/if} {/if} -->
</div> </div>
{/if} {/if}
@@ -261,10 +262,10 @@
text-transform: lowercase; text-transform: lowercase;
} }
.tag.more { /* .tag.more {
background: color-mix(in srgb, var(--terminal-muted) 20%, transparent); background: color-mix(in srgb, var(--terminal-muted) 20%, transparent);
color: var(--terminal-muted); color: var(--terminal-muted);
} } */
.warning { .warning {
font-size: 0.6rem; font-size: 0.6rem;

View File

@@ -0,0 +1,131 @@
<script lang="ts">
import Icon from '@iconify/svelte';
import { getButtonStyle, parseColorText, getSegmentStyle } from './utils';
import type { TerminalLine } from './types';
import { createEventDispatcher } from 'svelte';
import '$lib/assets/css/tui-checkbox.css';
export let line: TerminalLine;
export let inline: boolean = false;
export let checked: boolean = false;
const dispatch = createEventDispatcher<{
change: boolean;
}>();
$: labelSegments = line.content ? parseColorText(line.content) : [];
$: isDisabled = line.inputDisabled || false;
$: indeterminate = line.checkboxIndeterminate || false;
function handleChange() {
if (isDisabled) return;
checked = !checked;
dispatch('change', checked);
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
handleChange();
}
}
function getCheckboxSymbol(checked: boolean, indeterminate: boolean): string {
if (indeterminate) return '[-]';
return checked ? '[✓]' : '[ ]';
}
</script>
<div
class="tui-checkbox"
class:inline={inline}
class:checked={checked}
class:disabled={isDisabled}
style="--checkbox-color: {getButtonStyle(line.style)}"
role="checkbox"
aria-checked={indeterminate ? 'mixed' : checked}
aria-disabled={isDisabled}
tabindex={isDisabled ? -1 : 0}
on:click={handleChange}
on:keydown={handleKeydown}
>
<span class="checkbox-box" class:indeterminate={indeterminate}>
{getCheckboxSymbol(checked, indeterminate)}
</span>
{#if line.icon}
<Icon icon={line.icon} width="14" class="checkbox-icon" />
{/if}
{#if line.content}
<span class="checkbox-label">
{#each labelSegments as segment}
{#if segment.icon}
<Icon icon={segment.icon} width="14" class="inline-icon" />
{:else if getSegmentStyle(segment)}
<span style={getSegmentStyle(segment)}>{segment.text}</span>
{:else}
{segment.text}
{/if}
{/each}
</span>
{/if}
</div>
<style>
.tui-checkbox {
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0.35rem 0;
padding: 0.35rem 0.5rem;
font-size: 0.9rem;
cursor: pointer;
border-radius: 4px;
transition: all 0.15s ease;
user-select: none;
}
.tui-checkbox.inline {
display: inline-flex;
margin: 0 0.75rem 0 0;
}
.tui-checkbox:hover:not(.disabled) {
background: color-mix(in srgb, var(--checkbox-color) 10%, transparent);
}
.tui-checkbox:focus-visible {
outline: 1px solid var(--checkbox-color);
outline-offset: 2px;
}
.tui-checkbox.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.checkbox-box {
font-family: inherit;
font-weight: bold;
color: var(--terminal-muted);
transition: color 0.15s ease;
}
.tui-checkbox.checked .checkbox-box,
.checkbox-box.indeterminate {
color: var(--checkbox-color);
}
:global(.checkbox-icon) {
color: var(--checkbox-color);
}
.checkbox-label {
color: var(--terminal-text);
}
.tui-checkbox.disabled .checkbox-label {
color: var(--terminal-muted);
}
</style>

View File

@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import Icon from '@iconify/svelte'; import Icon from '@iconify/svelte';
import '$lib/assets/css/tui-footer.css';
export let isTyping: boolean; export let isTyping: boolean;
export let linesCount: number; export let linesCount: number;
@@ -30,64 +31,4 @@
</span> </span>
</div> </div>
<style>
.tui-statusbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.4rem 0.75rem;
font-size: 0.75rem;
background: var(--terminal-bg-light);
border-color: var(--terminal-border);
}
.tui-statusbar.bottom {
border-top: 1px solid var(--terminal-border);
}
.status-left, .status-right {
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--terminal-muted);
}
.status-center {
color: var(--terminal-primary);
font-weight: 600;
}
.skip-btn {
display: flex;
align-items: center;
gap: 0.3rem;
padding: 0.15rem 0.5rem;
background: var(--terminal-border);
border: 1px solid transparent;
border-radius: 3px;
color: var(--terminal-muted);
font-family: inherit;
font-size: 0.7rem;
cursor: pointer;
transition: all 0.15s ease;
}
.skip-btn:hover {
background: var(--terminal-primary);
color: var(--terminal-bg);
border-color: var(--terminal-primary);
}
.typing-indicator {
animation: pulse 1s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.line-count {
color: var(--terminal-muted);
}
</style>

View File

@@ -1,16 +1,25 @@
<script lang="ts"> <script lang="ts">
import Icon from '@iconify/svelte'; import Icon from '@iconify/svelte';
import { user } from '$lib/config'; import { user } from '$lib/config';
import type { SpeedPreset } from '$lib/config'; import { colorTheme, type ColorTheme } from '$lib/stores/theme';
import '$lib/assets/css/tui-header.css';
export let title = 'terminal'; export let title = 'terminal';
export let interactive = true; export let interactive = true;
export let hasButtons = false; export let hasButtons = false;
function getThemeIcon(theme: ColorTheme): string {
switch (theme) {
case 'arch': return 'mdi:arch';
case 'catppuccin': return 'solar:cat-bold';
default: return 'mdi:palette';
}
}
</script> </script>
<div class="tui-statusbar top"> <div class="tui-statusbar top">
<span class="status-left"> <span class="status-left">
<Icon icon="mdi:arch" width="14" /> <Icon icon={getThemeIcon($colorTheme)} width="14" />
<span>{user.username}@{user.hostname}</span> <span>{user.username}@{user.hostname}</span>
</span> </span>
<span class="status-center">{title}</span> <span class="status-center">{title}</span>
@@ -22,43 +31,4 @@
</span> </span>
</div> </div>
<style>
.tui-statusbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.4rem 0.75rem;
font-size: 0.75rem;
background: var(--terminal-bg-light);
border-color: var(--terminal-border);
}
.tui-statusbar.top {
border-bottom: 1px solid var(--terminal-border);
}
.status-left, .status-right {
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--terminal-muted);
}
.status-center {
color: var(--terminal-primary);
font-weight: 600;
}
.hint {
padding: 0.1rem 0.4rem;
background: var(--terminal-border);
border-radius: 3px;
font-size: 0.7rem;
}
@media (max-width: 768px) {
.hint {
display: none;
}
}
</style>

View File

@@ -0,0 +1,197 @@
<script lang="ts">
import Icon from '@iconify/svelte';
import { getButtonStyle, parseColorText, getSegmentStyle } from './utils';
import type { TerminalLine } from './types';
import { createEventDispatcher } from 'svelte';
import '$lib/assets/css/tui-input.css';
export let line: TerminalLine;
export let inline: boolean = false;
export let value: string = '';
const dispatch = createEventDispatcher<{
input: string;
change: string;
focus: void;
blur: void;
}>();
$: labelSegments = line.content ? parseColorText(line.content) : [];
$: placeholder = line.inputPlaceholder || '';
$: inputType = line.inputType || 'text';
$: isDisabled = line.inputDisabled || false;
$: hasError = line.inputError;
$: errorMessage = line.inputErrorMessage || '';
$: prefix = line.inputPrefix || '';
$: suffix = line.inputSuffix || '';
function handleInput(e: Event) {
const target = e.target as HTMLInputElement;
value = target.value;
dispatch('input', value);
}
function handleChange(e: Event) {
const target = e.target as HTMLInputElement;
dispatch('change', target.value);
}
function handleFocus() {
dispatch('focus');
}
function handleBlur() {
dispatch('blur');
}
</script>
<div
class="tui-input"
class:inline={inline}
class:error={hasError}
class:disabled={isDisabled}
style="--input-color: {getButtonStyle(line.style)}"
>
{#if line.content}
<label class="input-label">
{#if line.icon}
<Icon icon={line.icon} width="14" class="label-icon" />
{/if}
{#each labelSegments as segment}
{#if segment.icon}
<Icon icon={segment.icon} width="14" class="inline-icon" />
{:else if getSegmentStyle(segment)}
<span style={getSegmentStyle(segment)}>{segment.text}</span>
{:else}
{segment.text}
{/if}
{/each}
</label>
{/if}
<div class="input-wrapper">
<span class="input-prompt"></span>
{#if prefix}
<span class="input-affix prefix">{prefix}</span>
{/if}
<input
type={inputType}
{placeholder}
{value}
disabled={isDisabled}
on:input={handleInput}
on:change={handleChange}
on:focus={handleFocus}
on:blur={handleBlur}
/>
{#if suffix}
<span class="input-affix suffix">{suffix}</span>
{/if}
</div>
{#if hasError && errorMessage}
<div class="input-error">
<Icon icon="mdi:alert-circle" width="12" />
<span>{errorMessage}</span>
</div>
{/if}
</div>
<style>
.tui-input {
margin: 0.5rem 0;
font-size: 0.9rem;
}
.tui-input.inline {
display: inline-flex;
flex-direction: column;
margin: 0 0.5rem 0 0;
}
.input-label {
display: flex;
align-items: center;
gap: 0.35rem;
margin-bottom: 0.35rem;
color: var(--terminal-muted);
font-size: 0.85rem;
}
:global(.label-icon) {
color: var(--input-color);
}
.input-wrapper {
display: flex;
align-items: center;
gap: 0.35rem;
padding: 0.5rem 0.75rem;
background: color-mix(in srgb, var(--terminal-bg) 80%, black);
border: 1px solid var(--terminal-muted);
border-radius: 4px;
transition: all 0.15s ease;
}
.input-wrapper:focus-within {
border-color: var(--input-color);
box-shadow: 0 0 0 1px color-mix(in srgb, var(--input-color) 30%, transparent);
}
.tui-input.error .input-wrapper {
border-color: #f38ba8;
}
.tui-input.disabled .input-wrapper {
opacity: 0.5;
cursor: not-allowed;
}
.input-prompt {
color: var(--input-color);
font-weight: bold;
user-select: none;
}
.input-affix {
color: var(--terminal-muted);
font-size: 0.85rem;
}
.input-affix.prefix {
margin-right: 0.25rem;
}
.input-affix.suffix {
margin-left: 0.25rem;
}
input {
flex: 1;
min-width: 0;
background: transparent;
border: none;
color: var(--terminal-text);
font-family: inherit;
font-size: inherit;
outline: none;
}
input::placeholder {
color: var(--terminal-muted);
opacity: 0.6;
}
input:disabled {
cursor: not-allowed;
}
.input-error {
display: flex;
align-items: center;
gap: 0.35rem;
margin-top: 0.35rem;
color: #f38ba8;
font-size: 0.8rem;
}
</style>

View File

@@ -2,6 +2,7 @@
import Icon from '@iconify/svelte'; import Icon from '@iconify/svelte';
import { getButtonStyle, parseColorText, getSegmentStyle } from './utils'; import { getButtonStyle, parseColorText, getSegmentStyle } from './utils';
import type { TerminalLine } from './types'; import type { TerminalLine } from './types';
import '$lib/assets/css/tui-link.css';
export let line: TerminalLine; export let line: TerminalLine;
export let onClick: () => void; export let onClick: () => void;

View File

@@ -2,6 +2,7 @@
import Icon from '@iconify/svelte'; import Icon from '@iconify/svelte';
import { getButtonStyle, parseColorText, getSegmentStyle } from './utils'; import { getButtonStyle, parseColorText, getSegmentStyle } from './utils';
import type { TerminalLine } from './types'; import type { TerminalLine } from './types';
import '$lib/assets/css/tui-progress.css';
export let line: TerminalLine; export let line: TerminalLine;
export let inline: boolean = false; export let inline: boolean = false;

View File

@@ -0,0 +1,188 @@
<script lang="ts">
import Icon from '@iconify/svelte';
import { getButtonStyle, parseColorText, getSegmentStyle } from './utils';
import type { TerminalLine, FormOption } from './types';
import { createEventDispatcher } from 'svelte';
import '$lib/assets/css/tui-radio.css';
export let line: TerminalLine;
export let inline: boolean = false;
export let value: string = '';
const dispatch = createEventDispatcher<{
change: string;
}>();
$: labelSegments = line.content ? parseColorText(line.content) : [];
$: options = line.radioOptions || [];
$: isDisabled = line.inputDisabled || false;
$: isHorizontal = line.radioHorizontal || false;
function handleSelect(optionValue: string) {
if (isDisabled) return;
value = optionValue;
dispatch('change', value);
}
function handleKeydown(e: KeyboardEvent, optionValue: string) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
handleSelect(optionValue);
}
}
function getRadioSymbol(selected: boolean): string {
return selected ? '(●)' : '( )';
}
</script>
<div
class="tui-radio-group"
class:inline={inline}
class:disabled={isDisabled}
style="--radio-color: {getButtonStyle(line.style)}"
role="radiogroup"
>
{#if line.content}
<div class="radio-label">
{#if line.icon}
<Icon icon={line.icon} width="14" class="label-icon" />
{/if}
{#each labelSegments as segment}
{#if segment.icon}
<Icon icon={segment.icon} width="14" class="inline-icon" />
{:else if getSegmentStyle(segment)}
<span style={getSegmentStyle(segment)}>{segment.text}</span>
{:else}
{segment.text}
{/if}
{/each}
</div>
{/if}
<div class="radio-options" class:horizontal={isHorizontal}>
{#each options as option}
{@const isSelected = value === option.value}
{@const optionSegments = parseColorText(option.label)}
<div
class="radio-option"
class:selected={isSelected}
class:option-disabled={option.disabled}
role="radio"
aria-checked={isSelected}
aria-disabled={isDisabled || option.disabled}
tabindex={isDisabled || option.disabled ? -1 : 0}
on:click={() => !option.disabled && handleSelect(option.value)}
on:keydown={(e) => !option.disabled && handleKeydown(e, option.value)}
>
<span class="radio-symbol">
{getRadioSymbol(isSelected)}
</span>
{#if option.icon}
<Icon icon={option.icon} width="14" class="option-icon" />
{/if}
<span class="option-label">
{#each optionSegments as segment}
{#if segment.icon}
<Icon icon={segment.icon} width="14" class="inline-icon" />
{:else if getSegmentStyle(segment)}
<span style={getSegmentStyle(segment)}>{segment.text}</span>
{:else}
{segment.text}
{/if}
{/each}
</span>
</div>
{/each}
</div>
</div>
<style>
.tui-radio-group {
margin: 0.5rem 0;
font-size: 0.9rem;
}
.tui-radio-group.inline {
display: inline-flex;
flex-direction: column;
margin: 0 0.75rem 0 0;
}
.tui-radio-group.disabled {
opacity: 0.5;
}
.radio-label {
display: flex;
align-items: center;
gap: 0.35rem;
margin-bottom: 0.5rem;
color: var(--terminal-muted);
font-size: 0.85rem;
}
:global(.label-icon) {
color: var(--radio-color);
}
.radio-options {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.radio-options.horizontal {
flex-direction: row;
flex-wrap: wrap;
gap: 0.75rem;
}
.radio-option {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.35rem 0.5rem;
cursor: pointer;
border-radius: 4px;
transition: all 0.15s ease;
user-select: none;
}
.radio-option:hover:not(.option-disabled) {
background: color-mix(in srgb, var(--radio-color) 10%, transparent);
}
.radio-option:focus-visible {
outline: 1px solid var(--radio-color);
outline-offset: 2px;
}
.radio-option.option-disabled {
opacity: 0.5;
cursor: not-allowed;
}
.radio-symbol {
font-family: inherit;
font-weight: bold;
color: var(--terminal-muted);
transition: color 0.15s ease;
}
.radio-option.selected .radio-symbol {
color: var(--radio-color);
}
:global(.option-icon) {
color: var(--radio-color);
}
.option-label {
color: var(--terminal-text);
}
.radio-option.option-disabled .option-label {
color: var(--terminal-muted);
}
</style>

View File

@@ -0,0 +1,393 @@
<script lang="ts">
import Icon from '@iconify/svelte';
import { getButtonStyle, parseColorText, getSegmentStyle } from './utils';
import type { TerminalLine, FormOption } from './types';
import { createEventDispatcher } from 'svelte';
import '$lib/assets/css/tui-select.css';
export let line: TerminalLine;
export let inline: boolean = false;
export let value: string = '';
const dispatch = createEventDispatcher<{
change: string;
focus: void;
blur: void;
}>();
$: labelSegments = line.content ? parseColorText(line.content) : [];
$: options = line.selectOptions || [];
$: placeholder = line.inputPlaceholder || 'Select an option...';
$: isDisabled = line.inputDisabled || false;
$: hasError = line.inputError;
$: errorMessage = line.inputErrorMessage || '';
$: searchable = line.selectSearchable || false;
let isOpen = false;
let searchQuery = '';
let highlightedIndex = 0;
let selectRef: HTMLDivElement;
$: filteredOptions = searchable && searchQuery
? options.filter(opt =>
opt.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
opt.value.toLowerCase().includes(searchQuery.toLowerCase())
)
: options;
$: selectedOption = options.find(opt => opt.value === value);
$: displayValue = selectedOption?.label || '';
function handleToggle() {
if (isDisabled) return;
isOpen = !isOpen;
if (isOpen) {
highlightedIndex = 0;
searchQuery = '';
dispatch('focus');
} else {
dispatch('blur');
}
}
function handleSelect(optionValue: string) {
if (isDisabled) return;
value = optionValue;
isOpen = false;
searchQuery = '';
dispatch('change', value);
dispatch('blur');
}
function handleKeydown(e: KeyboardEvent) {
if (isDisabled) return;
switch (e.key) {
case 'Enter':
case ' ':
e.preventDefault();
if (isOpen && filteredOptions[highlightedIndex]) {
handleSelect(filteredOptions[highlightedIndex].value);
} else {
handleToggle();
}
break;
case 'ArrowDown':
e.preventDefault();
if (!isOpen) {
isOpen = true;
} else {
highlightedIndex = Math.min(highlightedIndex + 1, filteredOptions.length - 1);
}
break;
case 'ArrowUp':
e.preventDefault();
if (isOpen) {
highlightedIndex = Math.max(highlightedIndex - 1, 0);
}
break;
case 'Escape':
isOpen = false;
searchQuery = '';
break;
case 'Home':
if (isOpen) {
e.preventDefault();
highlightedIndex = 0;
}
break;
case 'End':
if (isOpen) {
e.preventDefault();
highlightedIndex = filteredOptions.length - 1;
}
break;
}
}
function handleClickOutside(e: MouseEvent) {
if (selectRef && !selectRef.contains(e.target as Node)) {
isOpen = false;
searchQuery = '';
}
}
function handleSearchInput(e: Event) {
searchQuery = (e.target as HTMLInputElement).value;
highlightedIndex = 0;
}
</script>
<svelte:window on:click={handleClickOutside} />
<div
class="tui-select"
class:inline={inline}
class:open={isOpen}
class:error={hasError}
class:disabled={isDisabled}
style="--select-color: {getButtonStyle(line.style)}"
bind:this={selectRef}
>
{#if line.content}
<label class="select-label">
{#if line.icon}
<Icon icon={line.icon} width="14" class="label-icon" />
{/if}
{#each labelSegments as segment}
{#if segment.icon}
<Icon icon={segment.icon} width="14" class="inline-icon" />
{:else if getSegmentStyle(segment)}
<span style={getSegmentStyle(segment)}>{segment.text}</span>
{:else}
{segment.text}
{/if}
{/each}
</label>
{/if}
<div
class="select-trigger"
role="combobox"
aria-expanded={isOpen}
aria-controls="select-listbox"
aria-haspopup="listbox"
aria-disabled={isDisabled}
tabindex={isDisabled ? -1 : 0}
on:click={handleToggle}
on:keydown={handleKeydown}
>
<span class="select-prompt"></span>
<span class="select-value" class:placeholder={!selectedOption}>
{displayValue || placeholder}
</span>
<span class="select-arrow">{isOpen ? '▲' : '▼'}</span>
</div>
{#if isOpen}
<div class="select-dropdown" role="listbox" id="select-listbox">
{#if searchable}
<div class="select-search">
<Icon icon="mdi:magnify" width="14" />
<input
type="text"
placeholder="Search..."
value={searchQuery}
on:input={handleSearchInput}
on:click|stopPropagation
/>
</div>
{/if}
<div class="select-options">
{#each filteredOptions as option, i}
{@const optionSegments = parseColorText(option.label)}
<div
class="select-option"
class:selected={value === option.value}
class:highlighted={i === highlightedIndex}
class:option-disabled={option.disabled}
role="option"
aria-selected={value === option.value}
aria-disabled={option.disabled}
tabindex={option.disabled ? -1 : 0}
on:click={() => !option.disabled && handleSelect(option.value)}
on:keydown={(e) => e.key === 'Enter' && !option.disabled && handleSelect(option.value)}
on:mouseenter={() => highlightedIndex = i}
>
{#if option.icon}
<Icon icon={option.icon} width="14" class="option-icon" />
{/if}
<span class="option-label">
{#each optionSegments as segment}
{#if segment.icon}
<Icon icon={segment.icon} width="14" class="inline-icon" />
{:else if getSegmentStyle(segment)}
<span style={getSegmentStyle(segment)}>{segment.text}</span>
{:else}
{segment.text}
{/if}
{/each}
</span>
{#if value === option.value}
<Icon icon="mdi:check" width="14" class="check-icon" />
{/if}
</div>
{:else}
<div class="select-empty">No options found</div>
{/each}
</div>
</div>
{/if}
{#if hasError && errorMessage}
<div class="select-error">
<Icon icon="mdi:alert-circle" width="12" />
<span>{errorMessage}</span>
</div>
{/if}
</div>
<style>
.tui-select {
position: relative;
margin: 0.5rem 0;
font-size: 0.9rem;
}
.tui-select.inline {
display: inline-flex;
flex-direction: column;
margin: 0 0.5rem 0 0;
}
.select-label {
display: flex;
align-items: center;
gap: 0.35rem;
margin-bottom: 0.35rem;
color: var(--terminal-muted);
font-size: 0.85rem;
}
:global(.label-icon) {
color: var(--select-color);
}
.select-trigger {
display: flex;
align-items: center;
gap: 0.35rem;
padding: 0.5rem 0.75rem;
background: color-mix(in srgb, var(--terminal-bg) 80%, black);
border: 1px solid var(--terminal-muted);
border-radius: 4px;
cursor: pointer;
transition: all 0.15s ease;
}
.tui-select.open .select-trigger,
.select-trigger:focus-visible {
border-color: var(--select-color);
box-shadow: 0 0 0 1px color-mix(in srgb, var(--select-color) 30%, transparent);
}
.tui-select.error .select-trigger {
border-color: #f38ba8;
}
.tui-select.disabled .select-trigger {
opacity: 0.5;
cursor: not-allowed;
}
.select-prompt {
color: var(--select-color);
font-weight: bold;
}
.select-value {
flex: 1;
color: var(--terminal-text);
}
.select-value.placeholder {
color: var(--terminal-muted);
opacity: 0.6;
}
.select-arrow {
color: var(--terminal-muted);
font-size: 0.7rem;
}
.select-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
margin-top: 0.25rem;
background: var(--terminal-bg);
border: 1px solid var(--select-color);
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
z-index: 100;
overflow: hidden;
}
.select-search {
display: flex;
align-items: center;
gap: 0.35rem;
padding: 0.5rem 0.75rem;
border-bottom: 1px solid var(--terminal-muted);
}
.select-search input {
flex: 1;
background: transparent;
border: none;
color: var(--terminal-text);
font-family: inherit;
font-size: inherit;
outline: none;
}
.select-search input::placeholder {
color: var(--terminal-muted);
}
.select-options {
max-height: 200px;
overflow-y: auto;
}
.select-option {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
cursor: pointer;
transition: background 0.1s ease;
}
.select-option.highlighted {
background: color-mix(in srgb, var(--select-color) 15%, transparent);
}
.select-option.selected {
color: var(--select-color);
}
.select-option.option-disabled {
opacity: 0.5;
cursor: not-allowed;
}
:global(.option-icon) {
color: var(--select-color);
}
.option-label {
flex: 1;
}
:global(.check-icon) {
color: var(--select-color);
}
.select-empty {
padding: 0.75rem;
text-align: center;
color: var(--terminal-muted);
font-style: italic;
}
.select-error {
display: flex;
align-items: center;
gap: 0.35rem;
margin-top: 0.35rem;
color: #f38ba8;
font-size: 0.8rem;
}
</style>

View File

@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { getButtonStyle, parseColorText, getSegmentStyle } from './utils'; import { getButtonStyle, parseColorText, getSegmentStyle } from './utils';
import type { TerminalLine } from './types'; import type { TerminalLine } from './types';
import '$lib/assets/css/tui-table.css';
export let line: TerminalLine; export let line: TerminalLine;

View File

@@ -0,0 +1,226 @@
<script lang="ts">
import Icon from '@iconify/svelte';
import { getButtonStyle, parseColorText, getSegmentStyle } from './utils';
import type { TerminalLine } from './types';
import { createEventDispatcher } from 'svelte';
import '$lib/assets/css/tui-textarea.css';
export let line: TerminalLine;
export let inline: boolean = false;
export let value: string = '';
const dispatch = createEventDispatcher<{
input: string;
change: string;
focus: void;
blur: void;
}>();
$: labelSegments = line.content ? parseColorText(line.content) : [];
$: placeholder = line.inputPlaceholder || '';
$: isDisabled = line.inputDisabled || false;
$: hasError = line.inputError;
$: errorMessage = line.inputErrorMessage || '';
$: rows = line.textareaRows || 4;
$: maxLength = line.textareaMaxLength;
let isFocused = false;
let charCount = 0;
function handleInput(e: Event) {
const target = e.target as HTMLTextAreaElement;
value = target.value;
charCount = value.length;
dispatch('input', value);
}
function handleChange(e: Event) {
const target = e.target as HTMLTextAreaElement;
dispatch('change', target.value);
}
function handleFocus() {
isFocused = true;
dispatch('focus');
}
function handleBlur() {
isFocused = false;
dispatch('blur');
}
</script>
<div
class="tui-textarea"
class:inline={inline}
class:focused={isFocused}
class:error={hasError}
class:disabled={isDisabled}
style="--input-color: {getButtonStyle(line.style)}"
>
{#if line.content}
<label class="textarea-label">
{#if line.icon}
<Icon icon={line.icon} width="14" class="label-icon" />
{/if}
{#each labelSegments as segment}
{#if segment.icon}
<Icon icon={segment.icon} width="14" class="inline-icon" />
{:else if getSegmentStyle(segment)}
<span style={getSegmentStyle(segment)}>{segment.text}</span>
{:else}
{segment.text}
{/if}
{/each}
</label>
{/if}
<div class="textarea-wrapper">
<div class="line-numbers">
{#each Array(Math.max(rows, value.split('\n').length)) as _, i}
<span class="line-num">{i + 1}</span>
{/each}
</div>
<textarea
{placeholder}
{rows}
maxlength={maxLength}
disabled={isDisabled}
bind:value
on:input={handleInput}
on:change={handleChange}
on:focus={handleFocus}
on:blur={handleBlur}
></textarea>
</div>
<div class="textarea-footer">
{#if hasError && errorMessage}
<div class="textarea-error">
<Icon icon="mdi:alert-circle" width="12" />
<span>{errorMessage}</span>
</div>
{/if}
{#if maxLength}
<div class="char-count" class:warning={charCount > maxLength * 0.9}>
{charCount}/{maxLength}
</div>
{/if}
</div>
</div>
<style>
.tui-textarea {
margin: 0.5rem 0;
font-size: 0.9rem;
}
.tui-textarea.inline {
display: inline-flex;
flex-direction: column;
margin: 0 0.5rem 0 0;
}
.textarea-label {
display: flex;
align-items: center;
gap: 0.35rem;
margin-bottom: 0.35rem;
color: var(--terminal-muted);
font-size: 0.85rem;
}
:global(.label-icon) {
color: var(--input-color);
}
.textarea-wrapper {
display: flex;
background: color-mix(in srgb, var(--terminal-bg) 80%, black);
border: 1px solid var(--terminal-muted);
border-radius: 4px;
transition: all 0.15s ease;
overflow: hidden;
}
.tui-textarea.focused .textarea-wrapper {
border-color: var(--input-color);
box-shadow: 0 0 0 1px color-mix(in srgb, var(--input-color) 30%, transparent);
}
.tui-textarea.error .textarea-wrapper {
border-color: #f38ba8;
}
.tui-textarea.disabled .textarea-wrapper {
opacity: 0.5;
cursor: not-allowed;
}
.line-numbers {
display: flex;
flex-direction: column;
padding: 0.5rem 0;
background: color-mix(in srgb, var(--terminal-bg) 60%, black);
border-right: 1px solid var(--terminal-muted);
user-select: none;
}
.line-num {
padding: 0 0.5rem;
color: var(--terminal-muted);
font-size: 0.8rem;
line-height: 1.5;
text-align: right;
min-width: 2rem;
}
textarea {
flex: 1;
min-width: 0;
padding: 0.5rem 0.75rem;
background: transparent;
border: none;
color: var(--terminal-text);
font-family: inherit;
font-size: inherit;
line-height: 1.5;
resize: vertical;
outline: none;
}
textarea::placeholder {
color: var(--terminal-muted);
opacity: 0.6;
}
textarea:disabled {
cursor: not-allowed;
resize: none;
}
.textarea-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 0.35rem;
}
.textarea-error {
display: flex;
align-items: center;
gap: 0.35rem;
color: #f38ba8;
font-size: 0.8rem;
}
.char-count {
margin-left: auto;
color: var(--terminal-muted);
font-size: 0.75rem;
}
.char-count.warning {
color: #f9e2af;
}
</style>

View File

@@ -0,0 +1,175 @@
<script lang="ts">
import Icon from '@iconify/svelte';
import { getButtonStyle, parseColorText, getSegmentStyle } from './utils';
import type { TerminalLine } from './types';
import { createEventDispatcher } from 'svelte';
import '$lib/assets/css/tui-toggle.css';
export let line: TerminalLine;
export let inline: boolean = false;
export let checked: boolean = false;
const dispatch = createEventDispatcher<{
change: boolean;
}>();
$: labelSegments = line.content ? parseColorText(line.content) : [];
$: isDisabled = line.inputDisabled || false;
$: onLabel = line.toggleOnLabel || 'ON';
$: offLabel = line.toggleOffLabel || 'OFF';
$: showLabels = line.toggleShowLabels !== false;
function handleToggle() {
if (isDisabled) return;
checked = !checked;
dispatch('change', checked);
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
handleToggle();
}
}
</script>
<div
class="tui-toggle"
class:inline={inline}
class:checked={checked}
class:disabled={isDisabled}
style="--toggle-color: {getButtonStyle(line.style)}"
role="switch"
aria-checked={checked}
aria-disabled={isDisabled}
tabindex={isDisabled ? -1 : 0}
on:click={handleToggle}
on:keydown={handleKeydown}
>
{#if line.icon}
<Icon icon={line.icon} width="14" class="toggle-icon" />
{/if}
{#if line.content}
<span class="toggle-label">
{#each labelSegments as segment}
{#if segment.icon}
<Icon icon={segment.icon} width="14" class="inline-icon" />
{:else if getSegmentStyle(segment)}
<span style={getSegmentStyle(segment)}>{segment.text}</span>
{:else}
{segment.text}
{/if}
{/each}
</span>
{/if}
<div class="toggle-track">
{#if showLabels}
<span class="toggle-off-label">{offLabel}</span>
{/if}
<span class="toggle-switch">
<span class="toggle-knob">{checked ? '●' : '○'}</span>
</span>
{#if showLabels}
<span class="toggle-on-label">{onLabel}</span>
{/if}
</div>
</div>
<style>
.tui-toggle {
display: flex;
align-items: center;
gap: 0.75rem;
margin: 0.35rem 0;
padding: 0.35rem 0.5rem;
font-size: 0.9rem;
cursor: pointer;
border-radius: 4px;
transition: all 0.15s ease;
user-select: none;
}
.tui-toggle.inline {
display: inline-flex;
margin: 0 0.75rem 0 0;
}
.tui-toggle:hover:not(.disabled) {
background: color-mix(in srgb, var(--toggle-color) 10%, transparent);
}
.tui-toggle:focus-visible {
outline: 1px solid var(--toggle-color);
outline-offset: 2px;
}
.tui-toggle.disabled {
opacity: 0.5;
cursor: not-allowed;
}
:global(.toggle-icon) {
color: var(--toggle-color);
}
.toggle-label {
color: var(--terminal-text);
}
.toggle-track {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
background: color-mix(in srgb, var(--terminal-bg) 80%, black);
border: 1px solid var(--terminal-muted);
border-radius: 4px;
font-size: 0.75rem;
transition: all 0.15s ease;
}
.tui-toggle.checked .toggle-track {
border-color: var(--toggle-color);
background: color-mix(in srgb, var(--toggle-color) 15%, transparent);
}
.toggle-off-label,
.toggle-on-label {
color: var(--terminal-muted);
font-weight: bold;
text-transform: uppercase;
min-width: 2rem;
text-align: center;
transition: color 0.15s ease;
}
.tui-toggle:not(.checked) .toggle-off-label {
color: var(--terminal-text);
}
.tui-toggle.checked .toggle-on-label {
color: var(--toggle-color);
}
.toggle-switch {
display: flex;
align-items: center;
justify-content: center;
width: 2.5rem;
position: relative;
}
.toggle-knob {
font-size: 1rem;
color: var(--terminal-muted);
transition: all 0.2s ease;
transform: translateX(-0.5rem);
}
.tui-toggle.checked .toggle-knob {
color: var(--toggle-color);
transform: translateX(0.5rem);
}
</style>

View File

@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { getButtonStyle, parseColorText, getSegmentStyle } from './utils'; import { getButtonStyle, parseColorText, getSegmentStyle } from './utils';
import type { TerminalLine } from './types'; import type { TerminalLine } from './types';
import '$lib/assets/css/tui-tooltip.css';
export let line: TerminalLine; export let line: TerminalLine;

View File

@@ -4,7 +4,16 @@ import type { Card } from '$lib/config';
export type LineType = export type LineType =
| 'command' | 'output' | 'prompt' | 'error' | 'success' | 'info' | 'warning' | 'command' | 'output' | 'prompt' | 'error' | 'success' | 'info' | 'warning'
| 'image' | 'blank' | 'header' | 'button' | 'divider' | 'link' | 'image' | 'blank' | 'header' | 'button' | 'divider' | 'link'
| 'card' | 'progress' | 'accordion' | 'table' | 'tooltip' | 'cardgrid'; | 'card' | 'progress' | 'accordion' | 'table' | 'tooltip' | 'cardgrid'
| 'input' | 'textarea' | 'checkbox' | 'radio' | 'select' | 'toggle';
// Option type for radio and select components
export interface FormOption {
value: string;
label: string;
icon?: string;
disabled?: boolean;
}
export interface TerminalLine { export interface TerminalLine {
type: LineType; type: LineType;
@@ -41,6 +50,29 @@ export interface TerminalLine {
tooltipPosition?: 'top' | 'bottom' | 'left' | 'right'; tooltipPosition?: 'top' | 'bottom' | 'left' | 'right';
// For cardgrid type // For cardgrid type
cards?: Card[]; cards?: Card[];
// For form input types (input, textarea, checkbox, radio, select, toggle)
inputPlaceholder?: string;
inputType?: 'text' | 'email' | 'password' | 'number' | 'url' | 'tel' | 'search';
inputDisabled?: boolean;
inputError?: boolean;
inputErrorMessage?: string;
inputPrefix?: string;
inputSuffix?: string;
// For textarea type
textareaRows?: number;
textareaMaxLength?: number;
// For checkbox type
checkboxIndeterminate?: boolean;
// For radio type
radioOptions?: FormOption[];
radioHorizontal?: boolean;
// For select type
selectOptions?: FormOption[];
selectSearchable?: boolean;
// For toggle type
toggleOnLabel?: string;
toggleOffLabel?: string;
toggleShowLabels?: boolean;
} }
// Pre-parsed line with segments ready for rendering // Pre-parsed line with segments ready for rendering

View File

@@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores'; import { page } from "$app/stores";
import { goto } from '$app/navigation'; import { goto } from "$app/navigation";
import TerminalTUI from '$lib/components/TerminalTUI.svelte'; import TerminalTUI from "$lib/components/TerminalTUI.svelte";
import type { TerminalLine } from '$lib/components/tui/types'; import type { TerminalLine } from "$lib/components/tui/types";
import { user, site } from '$lib/config'; import { user, site } from "$lib/config";
// Fun 404 messages // Fun 404 messages
const notFoundMessages = [ const notFoundMessages = [
@@ -38,13 +38,15 @@
}; };
const status = $derived($page.status); const status = $derived($page.status);
const errorMessage = $derived($page.error?.message || 'Something went wrong'); const errorMessage = $derived($page.error?.message || "Something went wrong");
const pathname = $derived($page.url.pathname); const pathname = $derived($page.url.pathname);
// Pick a random fun message based on status // Pick a random fun message based on status
function getFunMessage(): string { function getFunMessage(): string {
if (status === 404) { if (status === 404) {
return notFoundMessages[Math.floor(Math.random() * notFoundMessages.length)]; return notFoundMessages[
Math.floor(Math.random() * notFoundMessages.length)
];
} }
const msgs = errorMessages[status]; const msgs = errorMessages[status];
if (msgs) { if (msgs) {
@@ -55,33 +57,35 @@
// ASCII art for different errors (use ███ block characters) // ASCII art for different errors (use ███ block characters)
function getAsciiArt(): string[] { function getAsciiArt(): string[] {
// Cat-shaped block art for 404 // 404: Page Not Found (Pink)
if (status === 404) { if (status === 404) {
return [ return [
'(&pink) ███ ███ (&)', "(&pink) █ █ ███ █ (&)",
'(&pink) █ █ █ █ █ █ (&)', "(&pink) █ █ █ █ █ █ (&)",
'(&pink) █ █ █ █ █ █ █ (&)', "(&pink) ███ █ █ (&)",
'(&pink) ███████████ (&)', "(&pink) █ █ █ █ (&)",
'(&pink) █ █ (&)', "(&pink) █ ███ █ (&)",
]; ];
} }
// Blocky error box for 5xx // 500: Server Error (Red/Error)
if (status >= 500) { if (status >= 500) {
return [ return [
'(&error) █████ █████ (&)', "(&error) ███ █████ (&)",
'(&error) █ █ █ █ █ (&)', "(&error) █ █ █ █ (&)",
'(&error) ██ █ █ █ █ (&)', "(&error) ██ █ █ █ █ (&)",
'(&error) █████████████ (&)', "(&error) █ █ █ █ █ (&)",
"(&error) ███ ███ ███ (&)",
]; ];
} }
// Generic small block banner // Generic ERROR (Pink)
return [ return [
'(&pink) ███ ███ ███ (&)', "(&pink) ███ ███ ███ ███ ███ (&)",
'(&pink) █ █ █ (&)', "(&pink) █ █ █ █ █ █ █ █ █ (&)",
'(&pink) █ █ █ █ █ (&)', "(&pink) ███ ███ ███ █ █ ███ (&)",
'(&pink) █ (&)', "(&pink)█ █ █ █ █ █ █ (&)",
"(&pink) ███ █ █ █ █ ███ █ █ (&)",
]; ];
} }
@@ -91,48 +95,51 @@
// Build the terminal lines for the error page (derived to capture reactive values) // Build the terminal lines for the error page (derived to capture reactive values)
const lines = $derived<TerminalLine[]>([ const lines = $derived<TerminalLine[]>([
// Command that caused the error // Command that caused the error
{ type: 'command', content: `curl ${pathname}` }, { type: "command", content: `curl ${pathname}` },
{ type: 'blank', content: '' }, { type: "blank", content: "" },
// ASCII art // ASCII art
...asciiLines.map(line => ({ type: 'output' as const, content: line })), ...asciiLines.map((line) => ({ type: "output" as const, content: line })),
{ type: 'blank', content: '' }, { type: "blank", content: "" },
// Error status (styled with leading X like the screenshot) // Error status (styled with leading X like the screenshot)
{ type: 'error', content: `(&error,bold)X Error ${status}: ${errorMessage}(&)` }, {
{ type: 'blank', content: '' }, type: "error",
content: `(&error,bold)X Error ${status}: ${errorMessage}(&)`,
},
{ type: "blank", content: "" },
// Fun message as a comment // Fun message as a comment
{ type: 'output', content: `(&muted,italic)# ${funMessage}(&)` }, { type: "output", content: `(&muted,italic)# ${funMessage}(&)` },
{ type: 'blank', content: '' }, { type: "blank", content: "" },
{ type: 'divider', content: 'SUGGESTIONS' }, { type: "divider", content: "SUGGESTIONS" },
{ type: 'blank', content: '' }, { type: "blank", content: "" },
// Suggestions // Suggestions
{ type: 'command', content: 'cat suggestions.txt' }, { type: "command", content: "cat suggestions.txt" },
{ type: 'info', content: 'Check if the URL is correct' }, { type: "info", content: "Check if the URL is correct" },
{ type: 'info', content: 'Try refreshing the page' }, { type: "info", content: "Try refreshing the page" },
{ type: 'info', content: 'Contact me if the problem persists' }, { type: "info", content: "Contact me if the problem persists" },
{ type: 'blank', content: '' }, { type: "blank", content: "" },
{ type: 'divider', content: 'ACTIONS' }, { type: "divider", content: "ACTIONS" },
{ type: 'blank', content: '' }, { type: "blank", content: "" },
// Navigation buttons // Navigation buttons
{ {
type: 'button', type: "button",
content: 'Go Home', content: "Go Home",
icon: 'mdi:home', icon: "mdi:home",
style: 'primary', style: "primary",
href: '/' href: "/",
}, },
{ {
type: 'button', type: "button",
content: 'Go Back', content: "Go Back",
icon: 'mdi:arrow-left', icon: "mdi:arrow-left",
style: 'accent', style: "accent",
action: () => history.back() action: () => history.back(),
}, },
]); ]);
</script> </script>
@@ -143,12 +150,7 @@
</svelte:head> </svelte:head>
<div class="error-container"> <div class="error-container">
<TerminalTUI <TerminalTUI {lines} title="error" interactive={true} speed="fast" />
{lines}
title="error"
interactive={true}
speed="fast"
/>
</div> </div>
<style> <style>

View File

@@ -15,8 +15,8 @@
// Color palette (moved below the art/stats block) // Color palette (moved below the art/stats block)
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{ type: 'output', content: '(&red)███(&)(&orange)███(&)(&yellow)███(&)(&green)███(&)(&cyan)███(&)(&blue)███(&)(&magenta)███(&)(&pink)███(&)' }, { type: 'output', content: '(&red)███(&)(&orange)███(&)(&yellow)███(&)(&green)███(&)(&cyan)███(&)(&blue)███(&)(&magenta)███(&)(&pink)███(&)', inline: true },
{ type: 'output', content: '(&dim,red)███(&)(&dim,orange)███(&)(&dim,yellow)███(&)(&dim,green)███(&)(&dim,cyan)███(&)(&dim,blue)███(&)(&dim,magenta)███(&)(&dim,pink)███(&)' }, { type: 'output', content: '(&dim,red)███(&)(&dim,orange)███(&)(&dim,yellow)███(&)(&dim,green)███(&)(&dim,cyan)███(&)(&dim,blue)███(&)(&dim,magenta)███(&)(&dim,pink)███(&)', inline: true },
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{ type: 'header', content: `Welcome to ${user.displayname}'s Portfolio` }, { type: 'header', content: `Welcome to ${user.displayname}'s Portfolio` },
@@ -33,8 +33,10 @@
content: nav.name, content: nav.name,
icon: nav.icon === '📁' ? 'mdi:folder' : nav.icon === '🎨' ? 'mdi:palette' : 'mdi:trophy', icon: nav.icon === '📁' ? 'mdi:folder' : nav.icon === '🎨' ? 'mdi:palette' : 'mdi:trophy',
style: 'primary' as const, style: 'primary' as const,
href: nav.path href: nav.path,
inline: true
})), })),
{ type: 'blank', content: '' },
]; ];
</script> </script>

View File

@@ -276,6 +276,228 @@
{ type: 'output', content: '(&muted)Empty divider above (no text)(&)' }, { type: 'output', content: '(&muted)Empty divider above (no text)(&)' },
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
// ═══════════════════════════════════════════════════════════════
// FORM INPUTS
// ═══════════════════════════════════════════════════════════════
{ type: 'divider', content: 'FORM INPUTS' },
{ type: 'blank', content: '' },
{ type: 'info', content: '(&blue,bold)Text Input(&)' },
{
type: 'input',
content: 'Username:',
icon: 'mdi:account',
inputPlaceholder: 'Enter your username',
style: 'primary'
},
{
type: 'input',
content: 'Email:',
icon: 'mdi:email',
inputPlaceholder: 'you@example.com',
inputType: 'email',
style: 'accent'
},
{
type: 'input',
content: 'With prefix/suffix:',
inputPlaceholder: '100',
inputPrefix: '$',
inputSuffix: '.00',
inputType: 'number'
},
{
type: 'input',
content: 'Error state:',
inputPlaceholder: 'Invalid input',
inputError: true,
inputErrorMessage: 'This field is required',
style: 'error'
},
{
type: 'input',
content: 'Disabled:',
inputPlaceholder: 'Cannot edit',
inputDisabled: true
},
{ type: 'blank', content: '' },
// ═══════════════════════════════════════════════════════════════
// TEXTAREA
// ═══════════════════════════════════════════════════════════════
{ type: 'divider', content: 'TEXTAREA' },
{ type: 'blank', content: '' },
{ type: 'info', content: '(&blue,bold)Multi-line Text Area(&)' },
{
type: 'textarea',
content: 'Message:',
icon: 'mdi:message-text',
inputPlaceholder: 'Type your message here...',
textareaRows: 4,
style: 'primary'
},
{
type: 'textarea',
content: 'With character limit:',
inputPlaceholder: 'Limited to 100 characters',
textareaRows: 3,
textareaMaxLength: 100,
style: 'accent'
},
{ type: 'blank', content: '' },
// ═══════════════════════════════════════════════════════════════
// CHECKBOX
// ═══════════════════════════════════════════════════════════════
{ type: 'divider', content: 'CHECKBOXES' },
{ type: 'blank', content: '' },
{ type: 'info', content: '(&blue,bold)Checkbox Options(&)' },
{
type: 'checkbox',
content: 'Enable notifications',
icon: 'mdi:bell',
style: 'primary'
},
{
type: 'checkbox',
content: 'Accept terms and conditions',
style: 'accent'
},
{
type: 'checkbox',
content: 'Indeterminate state',
checkboxIndeterminate: true,
style: 'warning'
},
{
type: 'checkbox',
content: 'Disabled checkbox',
inputDisabled: true
},
{ type: 'blank', content: '' },
// ═══════════════════════════════════════════════════════════════
// RADIO BUTTONS
// ═══════════════════════════════════════════════════════════════
{ type: 'divider', content: 'RADIO BUTTONS' },
{ type: 'blank', content: '' },
{ type: 'info', content: '(&blue,bold)Radio Group (Vertical)(&)' },
{
type: 'radio',
content: 'Select theme:',
icon: 'mdi:palette',
style: 'primary',
radioOptions: [
{ value: 'dark', label: 'Dark Mode', icon: 'mdi:weather-night' },
{ value: 'light', label: 'Light Mode', icon: 'mdi:weather-sunny' },
{ value: 'system', label: 'System Default', icon: 'mdi:desktop-mac' }
]
},
{ type: 'blank', content: '' },
{ type: 'info', content: '(&blue,bold)Radio Group (Horizontal)(&)' },
{
type: 'radio',
content: 'Size:',
style: 'accent',
radioHorizontal: true,
radioOptions: [
{ value: 'sm', label: 'Small' },
{ value: 'md', label: 'Medium' },
{ value: 'lg', label: 'Large' },
{ value: 'xl', label: 'Extra Large', disabled: true }
]
},
{ type: 'blank', content: '' },
// ═══════════════════════════════════════════════════════════════
// SELECT DROPDOWN
// ═══════════════════════════════════════════════════════════════
{ type: 'divider', content: 'SELECT DROPDOWN' },
{ type: 'blank', content: '' },
{ type: 'info', content: '(&blue,bold)Basic Select(&)' },
{
type: 'select',
content: 'Country:',
icon: 'mdi:earth',
inputPlaceholder: 'Select a country...',
style: 'primary',
selectOptions: [
{ value: 'us', label: 'United States', icon: 'emojione-v1:flag-for-united-states' },
{ value: 'uk', label: 'United Kingdom', icon: 'emojione-v1:flag-for-united-kingdom' },
{ value: 'de', label: 'Germany', icon: 'emojione-v1:flag-for-germany' },
{ value: 'fr', label: 'France', icon: 'emojione-v1:flag-for-france' },
{ value: 'jp', label: 'Japan', icon: 'emojione-v1:flag-for-japan' }
]
},
{ type: 'blank', content: '' },
{ type: 'info', content: '(&blue,bold)Searchable Select(&)' },
{
type: 'select',
content: 'Programming Language:',
icon: 'mdi:code-braces',
inputPlaceholder: 'Search languages...',
style: 'accent',
selectSearchable: true,
selectOptions: [
{ value: 'ts', label: 'TypeScript', icon: 'mdi:language-typescript' },
{ value: 'js', label: 'JavaScript', icon: 'mdi:language-javascript' },
{ value: 'py', label: 'Python', icon: 'mdi:language-python' },
{ value: 'rs', label: 'Rust', icon: 'mdi:language-rust' },
{ value: 'go', label: 'Go', icon: 'mdi:language-go' },
{ value: 'cpp', label: 'C++', icon: 'mdi:language-cpp' },
{ value: 'java', label: 'Java', icon: 'mdi:language-java' }
]
},
{ type: 'blank', content: '' },
// ═══════════════════════════════════════════════════════════════
// TOGGLE SWITCH
// ═══════════════════════════════════════════════════════════════
{ type: 'divider', content: 'TOGGLE SWITCHES' },
{ type: 'blank', content: '' },
{ type: 'info', content: '(&blue,bold)Toggle Options(&)' },
{
type: 'toggle',
content: 'Dark Mode',
icon: 'mdi:theme-light-dark',
style: 'primary'
},
{
type: 'toggle',
content: 'Airplane Mode',
icon: 'mdi:airplane',
style: 'accent',
toggleOnLabel: 'ON',
toggleOffLabel: 'OFF'
},
{
type: 'toggle',
content: 'Custom Labels',
icon: 'mdi:toggle-switch',
style: 'warning',
toggleOnLabel: 'YES',
toggleOffLabel: 'NO'
},
{
type: 'toggle',
content: 'No Labels',
toggleShowLabels: false,
style: 'accent'
},
{
type: 'toggle',
content: 'Disabled Toggle',
inputDisabled: true
},
{ type: 'blank', content: '' },
// ═══════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════
// USAGE EXAMPLES // USAGE EXAMPLES
// ═══════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════
@@ -289,8 +511,14 @@
{ type: 'output', content: "(&muted)// Button with action(&)" }, { type: 'output', content: "(&muted)// Button with action(&)" },
{ type: 'output', content: "{ type: 'button', content: 'Click', icon: 'mdi:check', style: 'primary', action: () => {} }" }, { type: 'output', content: "{ type: 'button', content: 'Click', icon: 'mdi:check', style: 'primary', action: () => {} }" },
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{ type: 'output', content: "(&muted)// Progress bar(&)" }, { type: 'output', content: "(&muted)// Form input(&)" },
{ type: 'output', content: "{ type: 'progress', progress: 75, progressLabel: 'Loading...' }" }, { type: 'output', content: "{ type: 'input', content: 'Label:', inputPlaceholder: 'Enter value...', style: 'primary' }" },
{ type: 'blank', content: '' },
{ type: 'output', content: "(&muted)// Checkbox(&)" },
{ type: 'output', content: "{ type: 'checkbox', content: 'Enable option', style: 'accent' }" },
{ type: 'blank', content: '' },
{ type: 'output', content: "(&muted)// Select dropdown(&)" },
{ type: 'output', content: "{ type: 'select', content: 'Choose:', selectOptions: [{ value: 'a', label: 'Option A' }] }" },
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
// End // End

View File

@@ -21,6 +21,7 @@
{ type: 'header', content: `(&primary,bold)${user.name}(&)` }, { type: 'header', content: `(&primary,bold)${user.name}(&)` },
{ type: 'info', content: `(&accent)${user.title}(&)` }, { type: 'info', content: `(&accent)${user.title}(&)` },
{ type: 'output', content: `(&muted)${user.bio}(&)` }, { type: 'output', content: `(&muted)${user.bio}(&)` },
{ type: 'output', content: `(&muted)Location:(&) (&primary)${user.location}(&)` },
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{ type: 'output', content: `(&primary, bold)Links >(&)`, inline: true }, { type: 'output', content: `(&primary, bold)Links >(&)`, inline: true },
{ type: 'link', href: "/portfolio#contact", content: `(&bg-blue,black)Contact(&)`, inline: true }, { type: 'link', href: "/portfolio#contact", content: `(&bg-blue,black)Contact(&)`, inline: true },
@@ -76,7 +77,7 @@
...projects.filter(p => p.featured).flatMap(project => [ ...projects.filter(p => p.featured).flatMap(project => [
{ type: 'header' as const, content: `(&primary,bold)${project.name}(&)` }, { type: 'header' as const, content: `(&primary,bold)${project.name}(&)` },
{ type: 'output' as const, content: `(&muted)${project.description}(&)` }, { type: 'output' as const, content: `(&muted)${project.description}(&)` },
{ type: 'info' as const, content: `(&info)Tech: (&primary)${project.tech.join(', ')}(&)` }, { type: 'info' as const, content: `(&info)TechStack: (&primary)${project.tech.join(', ')}(&)` },
...(project.github ? [{ ...(project.github ? [{
type: 'button' as const, type: 'button' as const,
content: 'View on GitHub', content: 'View on GitHub',
@@ -98,7 +99,7 @@
...projects.filter(p => !p.featured).flatMap(project => [ ...projects.filter(p => !p.featured).flatMap(project => [
{ type: 'success' as const, content: `(&success)${project.name}(&)` }, { type: 'success' as const, content: `(&success)${project.name}(&)` },
{ type: 'output' as const, content: `(&muted)${project.description}(&)` }, { type: 'output' as const, content: `(&muted)${project.description}(&)` },
{ type: 'info' as const, content: `(&info)Tech:(&) (&primary)${project.tech.join(', ')}(&)` }, { type: 'info' as const, content: `(&info)TechStack:(&) (&primary)${project.tech.join(', ')}(&)` },
...(project.github ? [{ ...(project.github ? [{
type: 'button' as const, type: 'button' as const,
content: 'View on GitHub', content: 'View on GitHub',