CSS Files Styling
This commit is contained in:
13
src/lib/assets/css/background-3d.css
Normal file
13
src/lib/assets/css/background-3d.css
Normal 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;
|
||||
}
|
||||
34
src/lib/assets/css/index.css
Normal file
34
src/lib/assets/css/index.css
Normal 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';
|
||||
172
src/lib/assets/css/model-viewer.css
Normal file
172
src/lib/assets/css/model-viewer.css
Normal 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;
|
||||
}
|
||||
377
src/lib/assets/css/navbar-waybar.css
Normal file
377
src/lib/assets/css/navbar-waybar.css
Normal 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);
|
||||
}
|
||||
331
src/lib/assets/css/navbar.css
Normal file
331
src/lib/assets/css/navbar.css
Normal 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;
|
||||
}
|
||||
}
|
||||
223
src/lib/assets/css/terminal-page.css
Normal file
223
src/lib/assets/css/terminal-page.css
Normal 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);
|
||||
}
|
||||
73
src/lib/assets/css/terminal-tui.css
Normal file
73
src/lib/assets/css/terminal-tui.css
Normal 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);
|
||||
}
|
||||
}
|
||||
183
src/lib/assets/css/terminal.css
Normal file
183
src/lib/assets/css/terminal.css
Normal 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);
|
||||
}
|
||||
69
src/lib/assets/css/tui-accordion.css
Normal file
69
src/lib/assets/css/tui-accordion.css
Normal 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);
|
||||
}
|
||||
}
|
||||
227
src/lib/assets/css/tui-body.css
Normal file
227
src/lib/assets/css/tui-body.css
Normal 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);
|
||||
}
|
||||
60
src/lib/assets/css/tui-button.css
Normal file
60
src/lib/assets/css/tui-button.css
Normal 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;
|
||||
}
|
||||
222
src/lib/assets/css/tui-card-grid.css
Normal file
222
src/lib/assets/css/tui-card-grid.css
Normal 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;
|
||||
}
|
||||
}
|
||||
58
src/lib/assets/css/tui-card.css
Normal file
58
src/lib/assets/css/tui-card.css
Normal 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);
|
||||
}
|
||||
55
src/lib/assets/css/tui-checkbox.css
Normal file
55
src/lib/assets/css/tui-checkbox.css
Normal 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);
|
||||
}
|
||||
59
src/lib/assets/css/tui-footer.css
Normal file
59
src/lib/assets/css/tui-footer.css
Normal 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);
|
||||
}
|
||||
38
src/lib/assets/css/tui-header.css
Normal file
38
src/lib/assets/css/tui-header.css
Normal 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;
|
||||
}
|
||||
}
|
||||
96
src/lib/assets/css/tui-input.css
Normal file
96
src/lib/assets/css/tui-input.css
Normal 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;
|
||||
}
|
||||
45
src/lib/assets/css/tui-link.css
Normal file
45
src/lib/assets/css/tui-link.css
Normal 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;
|
||||
}
|
||||
104
src/lib/assets/css/tui-progress.css
Normal file
104
src/lib/assets/css/tui-progress.css
Normal 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;
|
||||
}
|
||||
87
src/lib/assets/css/tui-radio.css
Normal file
87
src/lib/assets/css/tui-radio.css
Normal 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);
|
||||
}
|
||||
162
src/lib/assets/css/tui-select.css
Normal file
162
src/lib/assets/css/tui-select.css
Normal 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;
|
||||
}
|
||||
49
src/lib/assets/css/tui-table.css
Normal file
49
src/lib/assets/css/tui-table.css
Normal 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);
|
||||
}
|
||||
113
src/lib/assets/css/tui-textarea.css
Normal file
113
src/lib/assets/css/tui-textarea.css
Normal 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;
|
||||
}
|
||||
94
src/lib/assets/css/tui-toggle.css
Normal file
94
src/lib/assets/css/tui-toggle.css
Normal 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);
|
||||
}
|
||||
78
src/lib/assets/css/tui-tooltip.css
Normal file
78
src/lib/assets/css/tui-tooltip.css
Normal 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);
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
import ParticleField from './ParticleField.svelte';
|
||||
import { themeColors, mode } from '$lib/stores/theme';
|
||||
import * as THREE from 'three';
|
||||
import '$lib/assets/css/background-3d.css';
|
||||
|
||||
let bgColor = $derived($mode === 'dark' ? $themeColors.background : $themeColors.background);
|
||||
</script>
|
||||
@@ -35,19 +36,3 @@
|
||||
<T.Color args={[bgColor]} attach="background" />
|
||||
</Canvas>
|
||||
</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>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { themeColors } from '$lib/stores/theme';
|
||||
import '$lib/assets/css/model-viewer.css';
|
||||
import * as THREE from 'three';
|
||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
||||
@@ -505,178 +506,3 @@
|
||||
</span>
|
||||
</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>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { fly, fade } from 'svelte/transition';
|
||||
import { user, navigation } from '$lib/config';
|
||||
import Icon from '@iconify/svelte';
|
||||
import '$lib/assets/css/navbar.css';
|
||||
|
||||
let themeDropdownOpen = $state(false);
|
||||
let navExpanded = $state(false);
|
||||
@@ -141,336 +142,3 @@
|
||||
></button>
|
||||
{/if}
|
||||
</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>
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { mode, colorTheme, toggleMode, setColorTheme, themeOptions, themeColors, type ColorTheme } from '$lib/stores/theme';
|
||||
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 Icon from '@iconify/svelte';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import '$lib/assets/css/navbar-waybar.css';
|
||||
|
||||
// State
|
||||
let themeDropdownOpen = $state(false);
|
||||
let mobileMenuOpen = $state(false);
|
||||
let currentTime = $state(new Date());
|
||||
|
||||
// Derived values (Svelte 5 runes)
|
||||
@@ -69,9 +71,14 @@
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') {
|
||||
themeDropdownOpen = false;
|
||||
mobileMenuOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
function closeMobileMenu() {
|
||||
mobileMenuOpen = false;
|
||||
}
|
||||
|
||||
function getThemeIcon(theme: ColorTheme): string {
|
||||
switch (theme) {
|
||||
case 'arch': return 'mdi:arch';
|
||||
@@ -105,8 +112,18 @@
|
||||
<img src="/favicon.png" alt="Blob Icon" width="16" />
|
||||
</a>
|
||||
|
||||
<!-- Workspaces -->
|
||||
<div class="module workspaces">
|
||||
<!-- Mobile menu toggle -->
|
||||
<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}
|
||||
{@const isActive = currentPath === nav.path || (nav.path !== '/' && currentPath.startsWith(nav.path))}
|
||||
{#if nav.external}
|
||||
@@ -133,8 +150,8 @@
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Current window title -->
|
||||
<div class="module window-title">
|
||||
<!-- Current window title (desktop) -->
|
||||
<div class="module window-title desktop-only">
|
||||
<span class="title-icon">
|
||||
{#if currentPath === '/'}
|
||||
<Icon icon="mdi:home" width="14" />
|
||||
@@ -209,296 +226,89 @@
|
||||
</div>
|
||||
|
||||
<!-- Backdrop -->
|
||||
{#if themeDropdownOpen}
|
||||
{#if themeDropdownOpen || mobileMenuOpen}
|
||||
<button
|
||||
class="backdrop"
|
||||
transition:fade={{ duration: 100 }}
|
||||
onclick={() => themeDropdownOpen = false}
|
||||
onclick={() => { themeDropdownOpen = false; mobileMenuOpen = false; }}
|
||||
aria-label="Close"
|
||||
></button>
|
||||
{/if}
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
.waybar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
height: var(--navbar-height, 40px);
|
||||
background: var(--bar-bg);
|
||||
border-bottom: 1px solid var(--bar-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 0.5rem;
|
||||
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
||||
font-size: 0.8rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
/* Modules */
|
||||
.module {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.35rem 0.6rem;
|
||||
background: var(--bar-bg-module);
|
||||
border-radius: 4px;
|
||||
color: var(--bar-text);
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.module:hover {
|
||||
background: color-mix(in srgb, var(--bar-primary) 20%, var(--bar-bg-module));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* module-group removed (minimal waybar look uses individual modules) */
|
||||
|
||||
/* Launcher */
|
||||
.launcher {
|
||||
color: var(--bar-primary);
|
||||
padding: 0.35rem 0.5rem;
|
||||
}
|
||||
|
||||
.launcher:hover {
|
||||
color: var(--bar-accent);
|
||||
}
|
||||
|
||||
/* Workspaces */
|
||||
.workspaces {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
padding: 0.2rem 0.3rem;
|
||||
}
|
||||
|
||||
.workspace {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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>
|
||||
<!-- Mobile menu dropdown -->
|
||||
{#if mobileMenuOpen}
|
||||
<div
|
||||
class="mobile-menu"
|
||||
transition:slide={{ duration: 200 }}
|
||||
style="
|
||||
--bar-bg: {$themeColors.background};
|
||||
--bar-bg-module: {$themeColors.backgroundLight};
|
||||
--bar-border: {$themeColors.border};
|
||||
--bar-text: {$themeColors.text};
|
||||
--bar-primary: {$themeColors.primary};
|
||||
--bar-accent: {$themeColors.accent};
|
||||
--bar-muted: {$themeColors.textMuted};
|
||||
"
|
||||
>
|
||||
<div class="mobile-nav-links">
|
||||
{#each navigation as nav}
|
||||
{@const isActive = currentPath === nav.path || (nav.path !== '/' && currentPath.startsWith(nav.path))}
|
||||
{#if nav.external}
|
||||
<a
|
||||
href={nav.path}
|
||||
class="mobile-nav-link external"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onclick={closeMobileMenu}
|
||||
>
|
||||
<Icon icon="mdi:open-in-new" width="16" />
|
||||
<span>{nav.name}</span>
|
||||
</a>
|
||||
{:else}
|
||||
<a
|
||||
href={nav.path}
|
||||
class="mobile-nav-link"
|
||||
class:active={isActive}
|
||||
onclick={closeMobileMenu}
|
||||
>
|
||||
{#if nav.path === '/'}
|
||||
<Icon icon="mdi:home" width="16" />
|
||||
{:else if nav.path === '/portfolio'}
|
||||
<Icon icon="mdi:folder-multiple" width="16" />
|
||||
{:else if nav.path === '/models'}
|
||||
<Icon icon="mdi:cube-outline" width="16" />
|
||||
{:else if nav.path === '/projects'}
|
||||
<Icon icon="mdi:trophy" width="16" />
|
||||
{:else}
|
||||
<Icon icon="mdi:file" width="16" />
|
||||
{/if}
|
||||
<span>{nav.name}</span>
|
||||
</a>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="mobile-menu-divider"></div>
|
||||
|
||||
<div class="mobile-theme-section">
|
||||
<div class="mobile-section-header">Theme</div>
|
||||
<div class="mobile-theme-options">
|
||||
{#each themeOptions as option}
|
||||
<button
|
||||
class="mobile-theme-btn"
|
||||
class:active={$colorTheme === option.value}
|
||||
onclick={() => { handleThemeSelect(option.value); closeMobileMenu(); }}
|
||||
>
|
||||
<Icon icon={getThemeIcon(option.value)} width="18" />
|
||||
<span>{option.label}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<button class="mobile-mode-btn" onclick={() => { toggleMode(); closeMobileMenu(); }}>
|
||||
<Icon icon={$mode === 'dark' ? 'mdi:weather-sunny' : 'mdi:weather-night'} width="18" />
|
||||
<span>{$mode === 'dark' ? 'Switch to Light' : 'Switch to Dark'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { themeColors } from '$lib/stores/theme';
|
||||
import '$lib/assets/css/terminal.css';
|
||||
|
||||
interface Props {
|
||||
lines?: TerminalLine[];
|
||||
@@ -162,189 +163,3 @@
|
||||
{/if}
|
||||
</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>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { themeColors } from '$lib/stores/theme';
|
||||
import { user, terminalSettings } from '$lib/config';
|
||||
import { calculateTypeSpeed } from '$lib';
|
||||
import '$lib/assets/css/terminal-page.css';
|
||||
|
||||
export type LineType = 'command' | 'output' | 'prompt' | 'error' | 'success' | 'info' | 'image' | 'blank' | 'header';
|
||||
|
||||
@@ -217,229 +218,3 @@
|
||||
{/if}
|
||||
</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>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import TuiBody from './tui/TuiBody.svelte';
|
||||
import TuiFooter from './tui/TuiFooter.svelte';
|
||||
import { parseColorText, getPlainText } from './tui/utils';
|
||||
import '$lib/assets/css/terminal-tui.css';
|
||||
|
||||
import type { TerminalLine, ParsedLine, DisplayedLine } from './tui/types';
|
||||
|
||||
@@ -365,80 +366,3 @@
|
||||
<TuiFooter isTyping={isTyping} linesCount={displayedLines.length} skipAnimation={skipAnimation} />
|
||||
</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>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import Icon from '@iconify/svelte';
|
||||
import { getButtonStyle, parseColorText, getSegmentStyle } from './utils';
|
||||
import type { TerminalLine } from './types';
|
||||
import '$lib/assets/css/tui-accordion.css';
|
||||
|
||||
export let line: TerminalLine;
|
||||
|
||||
|
||||
@@ -10,7 +10,14 @@
|
||||
import TuiTable from './TuiTable.svelte';
|
||||
import TuiTooltip from './TuiTooltip.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 '$lib/assets/css/tui-body.css';
|
||||
|
||||
export let displayedLines: DisplayedLine[] = [];
|
||||
export let currentLineIndex = 0;
|
||||
@@ -71,6 +78,12 @@
|
||||
<TuiTooltip {line} />
|
||||
{:else if line.type === 'progress'}
|
||||
<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}
|
||||
<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}
|
||||
@@ -113,6 +126,18 @@
|
||||
<div class="tui-line">
|
||||
<TuiTooltip {line} />
|
||||
</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}
|
||||
<div class="tui-line {line.type}" class:complete id={line.id}>
|
||||
{#if line.type === 'command' || line.type === 'prompt'}
|
||||
@@ -167,232 +192,4 @@
|
||||
{/if}
|
||||
</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>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import Icon from '@iconify/svelte';
|
||||
import { getButtonStyle, parseColorText, getSegmentStyle } from './utils';
|
||||
import type { TerminalLine } from './types';
|
||||
import '$lib/assets/css/tui-button.css';
|
||||
|
||||
export let line: TerminalLine;
|
||||
export let index: number;
|
||||
@@ -48,66 +49,3 @@
|
||||
{/if}
|
||||
{/if}
|
||||
</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>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import Icon from '@iconify/svelte';
|
||||
import { getButtonStyle, parseColorText, getSegmentStyle } from './utils';
|
||||
import type { TerminalLine } from './types';
|
||||
import '$lib/assets/css/tui-card.css';
|
||||
|
||||
export let line: TerminalLine;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import Icon from '@iconify/svelte';
|
||||
import type { TerminalLine } from './types';
|
||||
import '$lib/assets/css/tui-card-grid.css';
|
||||
|
||||
export let line: TerminalLine;
|
||||
|
||||
@@ -56,12 +57,12 @@
|
||||
|
||||
{#if card.tags && card.tags.length > 0}
|
||||
<div class="tags">
|
||||
{#each card.tags.slice(0, 5) as tag}
|
||||
{#each card.tags as tag}
|
||||
<span class="tag">{tag}</span>
|
||||
{/each}
|
||||
{#if card.tags.length > 5}
|
||||
<!-- {#if card.tags.length > 5}
|
||||
<span class="tag more">+{card.tags.length - 5}</span>
|
||||
{/if}
|
||||
{/if} -->
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -261,10 +262,10 @@
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.tag.more {
|
||||
/* .tag.more {
|
||||
background: color-mix(in srgb, var(--terminal-muted) 20%, transparent);
|
||||
color: var(--terminal-muted);
|
||||
}
|
||||
} */
|
||||
|
||||
.warning {
|
||||
font-size: 0.6rem;
|
||||
|
||||
131
src/lib/components/tui/TuiCheckbox.svelte
Normal file
131
src/lib/components/tui/TuiCheckbox.svelte
Normal 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>
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import Icon from '@iconify/svelte';
|
||||
import '$lib/assets/css/tui-footer.css';
|
||||
|
||||
export let isTyping: boolean;
|
||||
export let linesCount: number;
|
||||
@@ -30,64 +31,4 @@
|
||||
</span>
|
||||
</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>
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
<script lang="ts">
|
||||
import Icon from '@iconify/svelte';
|
||||
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 interactive = true;
|
||||
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>
|
||||
|
||||
<div class="tui-statusbar top">
|
||||
<span class="status-left">
|
||||
<Icon icon="mdi:arch" width="14" />
|
||||
<Icon icon={getThemeIcon($colorTheme)} width="14" />
|
||||
<span>{user.username}@{user.hostname}</span>
|
||||
</span>
|
||||
<span class="status-center">{title}</span>
|
||||
@@ -22,43 +31,4 @@
|
||||
</span>
|
||||
</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>
|
||||
|
||||
197
src/lib/components/tui/TuiInput.svelte
Normal file
197
src/lib/components/tui/TuiInput.svelte
Normal 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>
|
||||
@@ -2,6 +2,7 @@
|
||||
import Icon from '@iconify/svelte';
|
||||
import { getButtonStyle, parseColorText, getSegmentStyle } from './utils';
|
||||
import type { TerminalLine } from './types';
|
||||
import '$lib/assets/css/tui-link.css';
|
||||
|
||||
export let line: TerminalLine;
|
||||
export let onClick: () => void;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import Icon from '@iconify/svelte';
|
||||
import { getButtonStyle, parseColorText, getSegmentStyle } from './utils';
|
||||
import type { TerminalLine } from './types';
|
||||
import '$lib/assets/css/tui-progress.css';
|
||||
|
||||
export let line: TerminalLine;
|
||||
export let inline: boolean = false;
|
||||
|
||||
188
src/lib/components/tui/TuiRadio.svelte
Normal file
188
src/lib/components/tui/TuiRadio.svelte
Normal 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>
|
||||
393
src/lib/components/tui/TuiSelect.svelte
Normal file
393
src/lib/components/tui/TuiSelect.svelte
Normal 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>
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { getButtonStyle, parseColorText, getSegmentStyle } from './utils';
|
||||
import type { TerminalLine } from './types';
|
||||
import '$lib/assets/css/tui-table.css';
|
||||
|
||||
export let line: TerminalLine;
|
||||
|
||||
|
||||
226
src/lib/components/tui/TuiTextarea.svelte
Normal file
226
src/lib/components/tui/TuiTextarea.svelte
Normal 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>
|
||||
175
src/lib/components/tui/TuiToggle.svelte
Normal file
175
src/lib/components/tui/TuiToggle.svelte
Normal 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>
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { getButtonStyle, parseColorText, getSegmentStyle } from './utils';
|
||||
import type { TerminalLine } from './types';
|
||||
import '$lib/assets/css/tui-tooltip.css';
|
||||
|
||||
export let line: TerminalLine;
|
||||
|
||||
|
||||
@@ -4,7 +4,16 @@ import type { Card } from '$lib/config';
|
||||
export type LineType =
|
||||
| 'command' | 'output' | 'prompt' | 'error' | 'success' | 'info' | 'warning'
|
||||
| '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 {
|
||||
type: LineType;
|
||||
@@ -41,6 +50,29 @@ export interface TerminalLine {
|
||||
tooltipPosition?: 'top' | 'bottom' | 'left' | 'right';
|
||||
// For cardgrid type
|
||||
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
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import TerminalTUI from '$lib/components/TerminalTUI.svelte';
|
||||
import type { TerminalLine } from '$lib/components/tui/types';
|
||||
import { user, site } from '$lib/config';
|
||||
import { page } from "$app/stores";
|
||||
import { goto } from "$app/navigation";
|
||||
import TerminalTUI from "$lib/components/TerminalTUI.svelte";
|
||||
import type { TerminalLine } from "$lib/components/tui/types";
|
||||
import { user, site } from "$lib/config";
|
||||
|
||||
// Fun 404 messages
|
||||
const notFoundMessages = [
|
||||
@@ -38,13 +38,15 @@
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
// Pick a random fun message based on status
|
||||
function getFunMessage(): string {
|
||||
if (status === 404) {
|
||||
return notFoundMessages[Math.floor(Math.random() * notFoundMessages.length)];
|
||||
return notFoundMessages[
|
||||
Math.floor(Math.random() * notFoundMessages.length)
|
||||
];
|
||||
}
|
||||
const msgs = errorMessages[status];
|
||||
if (msgs) {
|
||||
@@ -55,33 +57,35 @@
|
||||
|
||||
// ASCII art for different errors (use ███ block characters)
|
||||
function getAsciiArt(): string[] {
|
||||
// Cat-shaped block art for 404
|
||||
// 404: Page Not Found (Pink)
|
||||
if (status === 404) {
|
||||
return [
|
||||
'(&pink) ███ ███ (&)',
|
||||
'(&pink) █ █ █ █ █ █ █ (&)',
|
||||
'(&pink) █ █ █ █ █ █ █ (&)',
|
||||
'(&pink) ███████████ (&)',
|
||||
'(&pink) █ █ █ █ (&)',
|
||||
"(&pink) █ █ ███ █ █ (&)",
|
||||
"(&pink) █ █ █ █ █ █ (&)",
|
||||
"(&pink) ███ █ █ ███ (&)",
|
||||
"(&pink) █ █ █ █ (&)",
|
||||
"(&pink) █ ███ █ (&)",
|
||||
];
|
||||
}
|
||||
|
||||
// Blocky error box for 5xx
|
||||
// 500: Server Error (Red/Error)
|
||||
if (status >= 500) {
|
||||
return [
|
||||
'(&error) █████ █████ (&)',
|
||||
'(&error) █ █ █ █ █ (&)',
|
||||
'(&error) █ █ █ █ █ █ █ (&)',
|
||||
'(&error) █████████████ (&)',
|
||||
"(&error) ███ ███ ███ (&)",
|
||||
"(&error) █ █ █ █ █ (&)",
|
||||
"(&error) ███ █ █ █ █ (&)",
|
||||
"(&error) █ █ █ █ █ (&)",
|
||||
"(&error) ███ ███ ███ (&)",
|
||||
];
|
||||
}
|
||||
|
||||
// Generic small block banner
|
||||
// Generic ERROR (Pink)
|
||||
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)
|
||||
const lines = $derived<TerminalLine[]>([
|
||||
// Command that caused the error
|
||||
{ type: 'command', content: `curl ${pathname}` },
|
||||
{ type: 'blank', content: '' },
|
||||
{ type: "command", content: `curl ${pathname}` },
|
||||
{ type: "blank", content: "" },
|
||||
|
||||
// ASCII art
|
||||
...asciiLines.map(line => ({ type: 'output' as const, content: line })),
|
||||
{ type: 'blank', content: '' },
|
||||
...asciiLines.map((line) => ({ type: "output" as const, content: line })),
|
||||
{ type: "blank", content: "" },
|
||||
|
||||
// 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
|
||||
{ type: 'output', content: `(&muted,italic)# ${funMessage}(&)` },
|
||||
{ type: 'blank', content: '' },
|
||||
{ type: "output", content: `(&muted,italic)# ${funMessage}(&)` },
|
||||
{ type: "blank", content: "" },
|
||||
|
||||
{ type: 'divider', content: 'SUGGESTIONS' },
|
||||
{ type: 'blank', content: '' },
|
||||
{ type: "divider", content: "SUGGESTIONS" },
|
||||
{ type: "blank", content: "" },
|
||||
|
||||
// Suggestions
|
||||
{ type: 'command', content: 'cat suggestions.txt' },
|
||||
{ type: 'info', content: 'Check if the URL is correct' },
|
||||
{ type: 'info', content: 'Try refreshing the page' },
|
||||
{ type: 'info', content: 'Contact me if the problem persists' },
|
||||
{ type: 'blank', content: '' },
|
||||
{ type: "command", content: "cat suggestions.txt" },
|
||||
{ type: "info", content: "Check if the URL is correct" },
|
||||
{ type: "info", content: "Try refreshing the page" },
|
||||
{ type: "info", content: "Contact me if the problem persists" },
|
||||
{ type: "blank", content: "" },
|
||||
|
||||
{ type: 'divider', content: 'ACTIONS' },
|
||||
{ type: 'blank', content: '' },
|
||||
{ type: "divider", content: "ACTIONS" },
|
||||
{ type: "blank", content: "" },
|
||||
|
||||
// Navigation buttons
|
||||
{
|
||||
type: 'button',
|
||||
content: 'Go Home',
|
||||
icon: 'mdi:home',
|
||||
style: 'primary',
|
||||
href: '/'
|
||||
type: "button",
|
||||
content: "Go Home",
|
||||
icon: "mdi:home",
|
||||
style: "primary",
|
||||
href: "/",
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
content: 'Go Back',
|
||||
icon: 'mdi:arrow-left',
|
||||
style: 'accent',
|
||||
action: () => history.back()
|
||||
type: "button",
|
||||
content: "Go Back",
|
||||
icon: "mdi:arrow-left",
|
||||
style: "accent",
|
||||
action: () => history.back(),
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
@@ -143,12 +150,7 @@
|
||||
</svelte:head>
|
||||
|
||||
<div class="error-container">
|
||||
<TerminalTUI
|
||||
{lines}
|
||||
title="error"
|
||||
interactive={true}
|
||||
speed="fast"
|
||||
/>
|
||||
<TerminalTUI {lines} title="error" interactive={true} speed="fast" />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
|
||||
// Color palette (moved below the art/stats block)
|
||||
{ type: 'blank', content: '' },
|
||||
{ type: 'output', content: '(&red)███(&)(&orange)███(&)(&yellow)███(&)(&green)███(&)(&cyan)███(&)(&blue)███(&)(&magenta)███(&)(&pink)███(&)' },
|
||||
{ type: 'output', content: '(&dim,red)███(&)(&dim,orange)███(&)(&dim,yellow)███(&)(&dim,green)███(&)(&dim,cyan)███(&)(&dim,blue)███(&)(&dim,magenta)███(&)(&dim,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)███(&)', inline: true },
|
||||
|
||||
{ type: 'blank', content: '' },
|
||||
{ type: 'header', content: `Welcome to ${user.displayname}'s Portfolio` },
|
||||
@@ -33,8 +33,10 @@
|
||||
content: nav.name,
|
||||
icon: nav.icon === '📁' ? 'mdi:folder' : nav.icon === '🎨' ? 'mdi:palette' : 'mdi:trophy',
|
||||
style: 'primary' as const,
|
||||
href: nav.path
|
||||
href: nav.path,
|
||||
inline: true
|
||||
})),
|
||||
{ type: 'blank', content: '' },
|
||||
];
|
||||
</script>
|
||||
|
||||
|
||||
@@ -276,6 +276,228 @@
|
||||
{ type: 'output', content: '(&muted)Empty divider above (no text)(&)' },
|
||||
{ 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
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
@@ -289,8 +511,14 @@
|
||||
{ type: 'output', content: "(&muted)// Button with action(&)" },
|
||||
{ type: 'output', content: "{ type: 'button', content: 'Click', icon: 'mdi:check', style: 'primary', action: () => {} }" },
|
||||
{ type: 'blank', content: '' },
|
||||
{ type: 'output', content: "(&muted)// Progress bar(&)" },
|
||||
{ type: 'output', content: "{ type: 'progress', progress: 75, progressLabel: 'Loading...' }" },
|
||||
{ type: 'output', content: "(&muted)// Form input(&)" },
|
||||
{ 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: '' },
|
||||
|
||||
// End
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
{ type: 'header', content: `(&primary,bold)${user.name}(&)` },
|
||||
{ type: 'info', content: `(&accent)${user.title}(&)` },
|
||||
{ type: 'output', content: `(&muted)${user.bio}(&)` },
|
||||
{ type: 'output', content: `(&muted)Location:(&) (&primary)${user.location}(&)` },
|
||||
{ type: 'blank', content: '' },
|
||||
{ type: 'output', content: `(&primary, bold)Links >(&)`, 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 => [
|
||||
{ type: 'header' as const, content: `(&primary,bold)${project.name}(&)` },
|
||||
{ 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 ? [{
|
||||
type: 'button' as const,
|
||||
content: 'View on GitHub',
|
||||
@@ -98,7 +99,7 @@
|
||||
...projects.filter(p => !p.featured).flatMap(project => [
|
||||
{ type: 'success' as const, content: `(&success)${project.name}(&)` },
|
||||
{ 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 ? [{
|
||||
type: 'button' as const,
|
||||
content: 'View on GitHub',
|
||||
|
||||
Reference in New Issue
Block a user