Skip to content

Commit 9b9df4c

Browse files
committed
fix terminal preview - accurate tab bar styles without overcomplication
1 parent 04e64fd commit 9b9df4c

1 file changed

Lines changed: 90 additions & 150 deletions

File tree

src/components/live-preview/terminal-window.component.ts

Lines changed: 90 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { FontPresetsService } from '../../services/font-presets.service';
88
imports: [CommonModule],
99
template: `
1010
<div class="flex-1 overflow-hidden flex flex-col rounded-lg" [ngStyle]="windowStyles()">
11-
<!-- Title bar -->
1211
@if (!hideDecorations()) {
1312
<div class="flex items-center px-3 py-2 select-none border-b"
1413
[style.background]="titleBarBg()"
@@ -20,30 +19,16 @@ import { FontPresetsService } from '../../services/font-presets.service';
2019
</div>
2120
<span class="flex-1 text-center text-xs select-none opacity-50"
2221
[style.font-family]="terminalFont()"
23-
[style.color]="colors().foreground">
24-
bash
25-
</span>
22+
[style.color]="colors().foreground">bash</span>
2623
</div>
2724
}
2825
29-
<!-- Tab bar (top) -->
3026
@if (tabBarVisible() && tabBarEdge() === 'top') {
31-
<div [ngStyle]="tabBarStyles()">
32-
@for (tab of tabs; track tab.name; let i = $index) {
33-
<div class="px-3 py-1 text-xs whitespace-nowrap"
34-
[ngStyle]="tabStyles(i === 0)">
35-
{{ tab.name }}
36-
</div>
37-
}
38-
</div>
27+
<ng-container *ngTemplateOutlet="tabBar"></ng-container>
3928
}
4029
41-
<!-- Terminal content -->
4230
<div class="flex-1 overflow-hidden relative p-3" [ngStyle]="terminalStyles()">
43-
<!-- Background with opacity -->
4431
<div class="absolute inset-0 -z-10" [ngStyle]="bgStyles()"></div>
45-
46-
<!-- Content lines -->
4732
<div class="space-y-0.5" [style.line-height]="'1.4'">
4833
<div class="whitespace-nowrap">
4934
<span [style.color]="c('color2')">user</span>
@@ -69,68 +54,90 @@ import { FontPresetsService } from '../../services/font-presets.service';
6954
<span [style.color]="c('foreground')">cat </span>
7055
<span class="url-text" [style.color]="urlColor()" [style.text-decoration]="urlUnderline()">https://sw.kovidgoyal.net/kitty/</span>
7156
</div>
72-
73-
<!-- Cursor -->
7457
<div class="whitespace-nowrap mt-2">
7558
<span [style.color]="c('color2')">user</span>
7659
<span [style.color]="c('foreground')" class="opacity-60">@</span>
7760
<span [style.color]="c('color4')">arch</span>
7861
<span [style.color]="c('foreground')" class="opacity-60">:~$ </span>
7962
@if (cursorShape() === 'block') {
8063
<span class="inline-block w-2 h-4 align-text-bottom"
81-
[class.animate-pulse]="cursorBlinking()"
64+
[class.animate-blink]="cursorBlinking()"
8265
[style.background]="cursorColor()"
8366
[style.color]="cursorTextColor()">&nbsp;</span>
8467
} @else if (cursorShape() === 'beam') {
8568
<span class="inline-block w-0.5 h-4 align-text-bottom border-l-2"
86-
[class.animate-pulse]="cursorBlinking()"
69+
[class.animate-blink]="cursorBlinking()"
8770
[style.border-color]="cursorColor()"></span>
8871
} @else {
8972
<span class="inline-block w-2 h-4 align-text-bottom border-b-2"
90-
[class.animate-pulse]="cursorBlinking()"
73+
[class.animate-blink]="cursorBlinking()"
9174
[style.border-color]="cursorColor()">&nbsp;</span>
9275
}
9376
</div>
9477
</div>
9578
</div>
9679
97-
<!-- Tab bar (bottom) -->
9880
@if (tabBarVisible() && tabBarEdge() === 'bottom') {
99-
<div [ngStyle]="tabBarStyles()">
100-
@for (tab of tabs; track tab.name; let i = $index) {
101-
<div class="px-3 py-1 text-xs whitespace-nowrap"
102-
[ngStyle]="tabStyles(i === 0)">
103-
{{ tab.name }}
104-
</div>
105-
}
106-
</div>
81+
<ng-container *ngTemplateOutlet="tabBar"></ng-container>
10782
}
10883
</div>
84+
85+
<ng-template #tabBar>
86+
<div class="flex items-stretch overflow-hidden select-none" [ngStyle]="tabBarContainerStyles()">
87+
@switch (tabBarStyle()) {
88+
@case ('fade') {
89+
@for (tab of tabs; track tab.name; let i = $index; let first = $first) {
90+
@if (!first) { <div class="w-px" [style.background]="tabBarBg()"></div> }
91+
<div class="px-3 py-1.5 text-xs whitespace-nowrap flex items-center" [ngStyle]="fadeTabStyles(i === 0, i)">{{ tab.name }}</div>
92+
}
93+
<div class="flex-1" [style.background]="tabBarBg()"></div>
94+
}
95+
@case ('separator') {
96+
@for (tab of tabs; track tab.name; let i = $index; let first = $first) {
97+
@if (!first) {
98+
<div class="px-1 py-1.5 text-xs flex items-center opacity-50" [style.color]="inactiveTabColor()">{{ tabSeparator() }}</div>
99+
}
100+
<div class="px-3 py-1.5 text-xs whitespace-nowrap flex items-center" [ngStyle]="simpleTabStyles(i === 0)">{{ tab.name }}</div>
101+
}
102+
<div class="flex-1" [style.background]="tabBarBg()"></div>
103+
}
104+
@case ('slant') {
105+
@for (tab of tabs; track tab.name; let i = $index) {
106+
<div class="relative px-4 py-1.5 text-xs whitespace-nowrap flex items-center slanted-tab" [ngStyle]="slantTabStyles(i === 0)">{{ tab.name }}</div>
107+
}
108+
<div class="flex-1" [style.background]="tabBarBg()"></div>
109+
}
110+
@case ('powerline') {
111+
@for (tab of tabs; track tab.name; let i = $index) {
112+
<div class="relative px-3 py-1.5 text-xs whitespace-nowrap flex items-center powerline-tab" [ngStyle]="powerlineTabStyles(i === 0)">{{ tab.name }}</div>
113+
}
114+
<div class="flex-1" [style.background]="tabBarBg()"></div>
115+
}
116+
@default {
117+
@for (tab of tabs; track tab.name; let i = $index) {
118+
<div class="px-3 py-1.5 text-xs whitespace-nowrap flex items-center" [ngStyle]="simpleTabStyles(i === 0)">{{ tab.name }}</div>
119+
}
120+
<div class="flex-1" [style.background]="tabBarBg()"></div>
121+
}
122+
}
123+
</div>
124+
</ng-template>
109125
`,
110126
styles: [`
111-
.url-text {
112-
text-decoration: underline;
113-
cursor: pointer;
114-
}
115-
@keyframes blink {
116-
0%, 50% { opacity: 1; }
117-
51%, 100% { opacity: 0; }
118-
}
119-
.animate-pulse {
120-
animation: blink 1s step-end infinite;
121-
}
127+
.url-text { text-decoration: underline; cursor: pointer; }
128+
@keyframes blink { 0%, 50% { opacity: 1; } 51%, 100% { opacity: 0; } }
129+
.animate-blink { animation: blink 1s step-end infinite; }
130+
.slanted-tab { clip-path: polygon(8px 0%, 100% 0%, calc(100% - 8px) 100%, 0% 100%); padding-left: 16px; padding-right: 16px; margin-right: -4px; }
131+
.slanted-tab:first-child { clip-path: polygon(0% 0%, 100% 0%, calc(100% - 8px) 100%, 0% 100%); padding-left: 12px; }
132+
.powerline-tab { padding-right: 16px; }
133+
.powerline-tab::after { content: ''; position: absolute; right: -8px; top: 0; width: 0; height: 0; border-top: 12px solid transparent; border-bottom: 12px solid transparent; border-left: 8px solid var(--tab-bg); z-index: 1; }
122134
`]
123135
})
124136
export class TerminalWindowComponent {
125137
private readonly store = inject(ConfigStoreService);
126138
private readonly fontPresets = inject(FontPresetsService);
127139

128-
// Fixed 3 tabs for preview
129-
readonly tabs = [
130-
{ name: 'bash' },
131-
{ name: 'nvim' },
132-
{ name: 'git' }
133-
];
140+
readonly tabs = [{ name: 'bash' }, { name: 'nvim' }, { name: 'git' }];
134141

135142
constructor() {
136143
effect(() => {
@@ -146,137 +153,70 @@ export class TerminalWindowComponent {
146153
windowCfg = computed(() => this.store.configState().window_layout);
147154
mouseCfg = computed(() => this.store.configState().mouse);
148155

149-
c(key: string): string {
150-
return (this.colors() as unknown as Record<string, string>)[key] || '#888888';
151-
}
156+
c(key: string): string { return (this.colors() as unknown as Record<string, string>)[key] || '#888888'; }
152157

153-
terminalFont = computed(() => {
154-
const f = this.fonts().font_family;
155-
return `"${f}", "JetBrains Mono", monospace`;
156-
});
158+
terminalFont = computed(() => { const f = this.fonts().font_family; return `"${f}", "JetBrains Mono", monospace`; });
157159

158160
windowStyles = computed(() => {
159161
const cfg = this.windowCfg();
160162
const borderWidth = this.parseBorderWidth(cfg.window_border_width);
161-
const styles: Record<string, string> = {
162-
background: this.colors().background,
163-
border: borderWidth === '0px' ? '1px solid ' + this.borderColor() : `${borderWidth} solid ${cfg.active_border_color}`,
164-
};
165-
return styles;
163+
return { background: this.colors().background, border: borderWidth === '0px' ? '1px solid ' + this.borderColor() : `${borderWidth} solid ${cfg.active_border_color}` };
166164
});
167165

168-
titleBarBg = computed(() => {
169-
const bg = this.colors().background;
170-
return this.darken(bg, 10);
171-
});
172-
173-
borderColor = computed(() => {
174-
const bg = this.colors().background;
175-
return this.darken(bg, 20);
176-
});
166+
titleBarBg = computed(() => this.darken(this.colors().background, 10));
167+
borderColor = computed(() => this.darken(this.colors().background, 20));
168+
tabBarBg = computed(() => { const cfg = this.tabBarCfg(); return (cfg.tab_bar_background && cfg.tab_bar_background !== 'none') ? cfg.tab_bar_background : this.darken(this.colors().background, 8); });
177169

178170
hideDecorations = computed(() => this.windowCfg().hide_window_decorations);
179171

180172
terminalStyles = computed(() => {
181173
const cfg = this.windowCfg();
182174
const padding = Math.min(Math.max(cfg.window_padding_width || 0, 0), 32);
183175
const fontSize = Math.max(6, Math.min(this.fonts().font_size, 24));
184-
185-
return {
186-
fontFamily: this.terminalFont(),
187-
fontSize: `${fontSize}px`,
188-
color: this.colors().foreground,
189-
padding: `${padding}px`,
190-
};
176+
return { fontFamily: this.terminalFont(), fontSize: `${fontSize}px`, color: this.colors().foreground, padding: `${padding}px` };
191177
});
192178

193-
bgStyles = computed(() => {
194-
const bg = this.colors().background;
195-
const opacity = this.colors().background_opacity;
196-
const clamped = typeof opacity === 'number' ? Math.max(0.05, Math.min(1, opacity)) : 1;
179+
bgStyles = computed(() => { const bg = this.colors().background; const opacity = this.colors().background_opacity; const clamped = typeof opacity === 'number' ? Math.max(0.05, Math.min(1, opacity)) : 1; return { background: bg, opacity: String(clamped) }; });
197180

198-
return {
199-
background: bg,
200-
opacity: String(clamped),
201-
};
202-
});
181+
tabBarVisible = computed(() => { const cfg = this.tabBarCfg(); const minTabs = cfg.tab_bar_min_tabs ?? 2; return cfg.tab_bar_style !== 'hidden' && 3 >= minTabs; });
182+
tabBarEdge = computed(() => this.tabBarCfg().tab_bar_edge);
183+
tabBarStyle = computed(() => this.tabBarCfg().tab_bar_style);
184+
tabSeparator = computed(() => this.tabBarCfg().tab_separator || ' ┇');
203185

204-
tabBarVisible = computed(() => {
205-
const cfg = this.tabBarCfg();
206-
const minTabs = cfg.tab_bar_min_tabs ?? 2;
207-
return cfg.tab_bar_style !== 'hidden' && 3 >= minTabs;
208-
});
186+
tabBarContainerStyles = computed(() => ({ background: this.tabBarBg(), borderTop: this.tabBarEdge() === 'bottom' ? `1px solid ${this.borderColor()}` : 'none', borderBottom: this.tabBarEdge() === 'top' ? `1px solid ${this.borderColor()}` : 'none' }));
187+
inactiveTabColor = computed(() => this.tabBarCfg().inactive_tab_foreground);
209188

210-
tabBarEdge = computed(() => this.tabBarCfg().tab_bar_edge);
189+
simpleTabStyles(isActive: boolean): Record<string, string> {
190+
const cfg = this.tabBarCfg();
191+
return isActive ? { background: cfg.active_tab_background, color: cfg.active_tab_foreground, fontWeight: '600' } : { background: cfg.inactive_tab_background, color: cfg.inactive_tab_foreground };
192+
}
211193

212-
tabBarStyles = computed(() => {
194+
fadeTabStyles(isActive: boolean, index: number): Record<string, string> {
213195
const cfg = this.tabBarCfg();
214-
const bg = cfg.tab_bar_background === 'none' || !cfg.tab_bar_background
215-
? this.darken(this.colors().background, 8)
216-
: cfg.tab_bar_background;
196+
const fade = cfg.tab_fade || [0.25, 0.5, 0.75, 1];
197+
const fadeVal = isActive ? 1 : (fade[Math.min(index, fade.length - 1)] ?? 1);
198+
return isActive ? { background: cfg.active_tab_background, color: cfg.active_tab_foreground, fontWeight: '600' } : { background: cfg.inactive_tab_background, color: cfg.inactive_tab_foreground, opacity: String(fadeVal) };
199+
}
217200

218-
return {
219-
display: 'flex',
220-
background: bg,
221-
borderTop: this.tabBarEdge() === 'bottom' ? 'none' : `1px solid ${this.borderColor()}`,
222-
borderBottom: this.tabBarEdge() === 'top' ? 'none' : `1px solid ${this.borderColor()}`,
223-
};
224-
});
201+
slantTabStyles(isActive: boolean): Record<string, string> {
202+
const cfg = this.tabBarCfg();
203+
return isActive ? { background: cfg.active_tab_background, color: cfg.active_tab_foreground, fontWeight: '600' } : { background: cfg.inactive_tab_background, color: cfg.inactive_tab_foreground };
204+
}
225205

226-
tabStyles(isActive: boolean): Record<string, string> {
206+
powerlineTabStyles(isActive: boolean): Record<string, string> {
227207
const cfg = this.tabBarCfg();
228-
if (isActive) {
229-
return {
230-
background: cfg.active_tab_background,
231-
color: cfg.active_tab_foreground,
232-
fontWeight: '600',
233-
};
234-
}
235-
return {
236-
background: cfg.inactive_tab_background,
237-
color: cfg.inactive_tab_foreground,
238-
opacity: '0.85',
239-
};
208+
const bg = isActive ? cfg.active_tab_background : cfg.inactive_tab_background;
209+
return { background: bg, color: isActive ? cfg.active_tab_foreground : cfg.inactive_tab_foreground, fontWeight: isActive ? '600' : '400', '--tab-bg': bg } as Record<string, string>;
240210
}
241211

242212
cursorShape = computed(() => this.cursorCfg().cursor_shape);
243-
cursorColor = computed(() => {
244-
const c = this.cursorCfg().cursor;
245-
return c === 'none' || !c?.startsWith('#') ? this.colors().foreground : c;
246-
});
247-
cursorTextColor = computed(() => {
248-
const c = this.cursorCfg().cursor_text_color;
249-
return c === 'background' ? this.colors().background : (c?.startsWith('#') ? c : this.colors().background);
250-
});
213+
cursorColor = computed(() => { const c = this.cursorCfg().cursor; return c === 'none' || !c?.startsWith('#') ? this.colors().foreground : c; });
214+
cursorTextColor = computed(() => { const c = this.cursorCfg().cursor_text_color; return c === 'background' ? this.colors().background : (c?.startsWith('#') ? c : this.colors().background); });
251215
cursorBlinking = computed(() => this.cursorCfg().cursor_blink_interval !== 0);
252216

253217
urlColor = computed(() => this.mouseCfg().url_color || this.colors().color4 || '#0087bd');
254-
urlUnderline = computed(() => {
255-
const style = this.mouseCfg().url_style;
256-
if (style === 'none') return 'none';
257-
if (style === 'double') return 'underline double';
258-
if (style === 'curly') return 'underline wavy';
259-
if (style === 'dashed') return 'underline dashed';
260-
if (style === 'dotted') return 'underline dotted';
261-
return 'underline';
262-
});
218+
urlUnderline = computed(() => { const style = this.mouseCfg().url_style; if (style === 'none') return 'none'; if (style === 'double') return 'underline double'; if (style === 'curly') return 'underline wavy'; if (style === 'dashed') return 'underline dashed'; if (style === 'dotted') return 'underline dotted'; return 'underline'; });
263219

264-
private parseBorderWidth(val: string): string {
265-
if (!val || val === '0' || val === '0pt' || val === '0px') return '0px';
266-
if (val.endsWith('pt')) {
267-
const n = Number.parseFloat(val);
268-
return `${Math.max(0, Math.round(n * 1.333))}px`;
269-
}
270-
return val.endsWith('px') ? val : '1px';
271-
}
272-
273-
private darken(hex: string, amount: number): string {
274-
if (!hex?.startsWith('#')) return '#333';
275-
const c = hex.slice(1);
276-
if (c.length !== 6) return hex;
277-
const r = Math.max(0, Math.min(255, Number.parseInt(c.slice(0, 2), 16) - amount));
278-
const g = Math.max(0, Math.min(255, Number.parseInt(c.slice(2, 4), 16) - amount));
279-
const b = Math.max(0, Math.min(255, Number.parseInt(c.slice(4, 6), 16) - amount));
280-
return `#${r.toString(16).padStart(2,'0')}${g.toString(16).padStart(2,'0')}${b.toString(16).padStart(2,'0')}`;
281-
}
220+
private parseBorderWidth(val: string): string { if (!val || val === '0' || val === '0pt' || val === '0px') return '0px'; if (val.endsWith('pt')) { const n = Number.parseFloat(val); return `${Math.max(0, Math.round(n * 1.333))}px`; } return val.endsWith('px') ? val : '1px'; }
221+
private darken(hex: string, amount: number): string { if (!hex?.startsWith('#')) return '#333'; const c = hex.slice(1); if (c.length !== 6) return hex; const r = Math.max(0, Math.min(255, Number.parseInt(c.slice(0, 2), 16) - amount)); const g = Math.max(0, Math.min(255, Number.parseInt(c.slice(2, 4), 16) - amount)); const b = Math.max(0, Math.min(255, Number.parseInt(c.slice(4, 6), 16) - amount)); return `#${r.toString(16).padStart(2,'0')}${g.toString(16).padStart(2,'0')}${b.toString(16).padStart(2,'0')}`; }
282222
}

0 commit comments

Comments
 (0)