Stage 2: file panels become a real 7.css ListView
The file list was a CSS grid of <div>s, so 7.css's listview component never matched a single selector and Tailwind styled the whole thing. It is now the plain <table> 7.css documents, which is what makes the Aero treatment appear. - The column header row moved OUT of its own div and INTO <thead>: 7.css makes thead position:sticky, so it stays put while the body scrolls, and it has to be inside the same table to do that. - Column widths moved from a computed grid-template-columns to <colgroup>, because 7.css sets table-layout: fixed. `gridTemplate()` is deleted. - Selection is now `tr.highlighted` (7.css's gradient band + #aaddfa border) instead of bg-primary/15. - The sort arrow was the text glyph ↑/↓ from `sortIndicator()`, which is also deleted; the sorted column is now `th.highlighted.indicator[.up]` and 7.css draws the real clip-path triangle in the header. - Row height, padding, borders and fonts now come from 7.css, so --row-h and the per-cell text/color/font utilities are gone. Two things 7.css does not cover, added in `skinfix`: - Dark mode. listview.css hardcodes light literals (#fff, #c0c1cd, #eee and a white header gradient) rather than --w7-* vars, so it does NOT follow the hand-authored dark palette — the header labels rendered invisible, dark on dark. The dark arms restate those rules; light keeps 7.css's own values. - The keyboard cursor. 7.css models selection only; Win7 drew the focused row with a dotted focus rectangle over it, which is what tells "cursor" apart from "selected" when they differ. That is `tr.is-cursor`. Verified visually this time, not just by build log: harness screenshots in both light and dark show the header gradient, sort triangle, cell separators, selection band and cursor rectangle matching 7.css's reference markup. Backend `tsc --noEmit` clean, `bun run build:ui` clean, bundle 489.83 kB (from 490.50). Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -191,26 +191,8 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- column headers (right-click to add/remove columns) -->
|
||||
<div
|
||||
(contextmenu)="headerContextMenu($event)"
|
||||
[style.grid-template-columns]="gridTemplate()"
|
||||
class="grid border-b border-border text-[11px] font-medium text-muted-foreground"
|
||||
>
|
||||
<button class="px-2 py-1 text-left hover:text-foreground" (click)="panel().toggleSort('name')">
|
||||
{{ i18n.t('columns.name') }} {{ sortIndicator('name') }}
|
||||
</button>
|
||||
@for (col of visibleColumns(); track col.key) {
|
||||
<button
|
||||
class="px-2 py-1 hover:text-foreground"
|
||||
[class.text-right]="col.key === 'size'"
|
||||
[class.text-left]="col.key !== 'size'"
|
||||
(click)="panel().toggleSort(col.key)"
|
||||
>
|
||||
{{ i18n.t('columns.' + col.key) }} {{ sortIndicator(col.key) }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<!-- Column headers live INSIDE the table now (7.css makes thead sticky), so
|
||||
there is no separate header row here — see the ListView below. -->
|
||||
|
||||
<!-- type-to-filter bar (appears while a query is active) -->
|
||||
@if (panel().filter(); as query) {
|
||||
@@ -256,67 +238,96 @@
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
@for (entry of panel().sorted(); track entry.name; let i = $index) {
|
||||
<div
|
||||
[attr.data-index]="i"
|
||||
(click)="rowClick($event, i)"
|
||||
(dblclick)="openAt(i)"
|
||||
(contextmenu)="rowContextMenu($event, i)"
|
||||
[style.grid-template-columns]="gridTemplate()"
|
||||
class="grid h-(--row-h) items-center text-(length:--list-font)"
|
||||
[class.bg-cursor-tint]="i === panel().cursor() && !isSelected(entry)"
|
||||
[class.bg-primary/15]="isSelected(entry)"
|
||||
[class.ring-1]="i === panel().cursor() && isActive()"
|
||||
[class.ring-inset]="i === panel().cursor() && isActive()"
|
||||
[class.ring-ring]="i === panel().cursor() && isActive()"
|
||||
>
|
||||
<div class="flex min-w-0 items-center gap-1.5 px-2">
|
||||
<ng-icon [name]="iconFor(entry)" size="0.95rem" [class]="iconClass(entry)" />
|
||||
<span class="truncate" [class.italic]="entry.type === 'symlink'">{{ entry.name }}</span>
|
||||
</div>
|
||||
<!-- 7.css ListView: a plain <table>. The header is <thead> (7.css makes it
|
||||
position:sticky, which is why it moved in here), the sorted column is
|
||||
.highlighted.indicator[.up], and the selection band is tr.highlighted.
|
||||
Widths come from <colgroup> because 7.css sets table-layout:fixed. -->
|
||||
<table>
|
||||
<colgroup>
|
||||
<col />
|
||||
@for (col of visibleColumns(); track col.key) {
|
||||
@switch (col.key) {
|
||||
@case ('size') {
|
||||
<span class="px-2 text-right font-mono text-(length:--list-meta-font) tabular-nums text-muted-foreground">
|
||||
{{ entry.type === 'dir' ? '—' : formatSize(entry.size) }}
|
||||
</span>
|
||||
}
|
||||
@case ('mtime') {
|
||||
<span class="px-2 font-mono text-(length:--list-meta-font) tabular-nums text-muted-foreground">
|
||||
{{ formatDate(entry.mtime) }}
|
||||
</span>
|
||||
}
|
||||
@case ('kind') {
|
||||
<span class="truncate px-2 text-(length:--list-meta-font) text-muted-foreground">
|
||||
{{ fileKind(entry) }}
|
||||
</span>
|
||||
<!-- fileKind is localized in file-panel.ts via i18n -->
|
||||
|
||||
}
|
||||
@case ('rights') {
|
||||
<span class="px-2 font-mono text-(length:--list-meta-font) text-muted-foreground">
|
||||
{{ entry.name === '..' ? '' : formatMode(entry.mode) }}
|
||||
</span>
|
||||
}
|
||||
@case ('owner') {
|
||||
<span class="truncate px-2 font-mono text-(length:--list-meta-font) text-muted-foreground">
|
||||
{{ entry.name === '..' ? '' : (entry.owner ?? '—') }}
|
||||
</span>
|
||||
}
|
||||
@case ('group') {
|
||||
<span class="truncate px-2 font-mono text-(length:--list-meta-font) text-muted-foreground">
|
||||
{{ entry.name === '..' ? '' : (entry.group ?? '—') }}
|
||||
</span>
|
||||
}
|
||||
}
|
||||
<col [style.width]="col.width" />
|
||||
}
|
||||
</div>
|
||||
} @empty {
|
||||
<div class="p-6 text-center text-xs text-muted-foreground">{{ i18n.t('panel.emptyDir') }}</div>
|
||||
}
|
||||
@if (panel().filter() && filterMatches() === 0) {
|
||||
<div class="p-6 text-center text-xs text-muted-foreground">{{ i18n.t('panel.noMatches') }}</div>
|
||||
}
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr (contextmenu)="headerContextMenu($event)">
|
||||
<th
|
||||
(click)="panel().toggleSort('name')"
|
||||
[class.highlighted]="panel().sortKey() === 'name'"
|
||||
[class.indicator]="panel().sortKey() === 'name'"
|
||||
[class.up]="panel().sortKey() === 'name' && panel().sortAsc()"
|
||||
>
|
||||
{{ i18n.t('columns.name') }}
|
||||
</th>
|
||||
@for (col of visibleColumns(); track col.key) {
|
||||
<th
|
||||
(click)="panel().toggleSort(col.key)"
|
||||
[class.highlighted]="panel().sortKey() === col.key"
|
||||
[class.indicator]="panel().sortKey() === col.key"
|
||||
[class.up]="panel().sortKey() === col.key && panel().sortAsc()"
|
||||
[style.text-align]="col.key === 'size' ? 'right' : 'left'"
|
||||
>
|
||||
{{ i18n.t('columns.' + col.key) }}
|
||||
</th>
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (entry of panel().sorted(); track entry.name; let i = $index) {
|
||||
<tr
|
||||
[attr.data-index]="i"
|
||||
(click)="rowClick($event, i)"
|
||||
(dblclick)="openAt(i)"
|
||||
(contextmenu)="rowContextMenu($event, i)"
|
||||
[class.highlighted]="isSelected(entry)"
|
||||
[class.is-cursor]="i === panel().cursor() && isActive()"
|
||||
>
|
||||
<td>
|
||||
<span class="flex min-w-0 items-center gap-1.5">
|
||||
<ng-icon [name]="iconFor(entry)" size="0.95rem" [class]="iconClass(entry)" />
|
||||
<span class="truncate" [class.italic]="entry.type === 'symlink'">{{ entry.name }}</span>
|
||||
</span>
|
||||
</td>
|
||||
@for (col of visibleColumns(); track col.key) {
|
||||
@switch (col.key) {
|
||||
@case ('size') {
|
||||
<td style="text-align: right">{{ entry.type === 'dir' ? '—' : formatSize(entry.size) }}</td>
|
||||
}
|
||||
@case ('mtime') {
|
||||
<td>{{ formatDate(entry.mtime) }}</td>
|
||||
}
|
||||
@case ('kind') {
|
||||
<!-- fileKind is localized in file-panel.ts via i18n -->
|
||||
<td>{{ fileKind(entry) }}</td>
|
||||
}
|
||||
@case ('rights') {
|
||||
<td>{{ entry.name === '..' ? '' : formatMode(entry.mode) }}</td>
|
||||
}
|
||||
@case ('owner') {
|
||||
<td>{{ entry.name === '..' ? '' : (entry.owner ?? '—') }}</td>
|
||||
}
|
||||
@case ('group') {
|
||||
<td>{{ entry.name === '..' ? '' : (entry.group ?? '—') }}</td>
|
||||
}
|
||||
}
|
||||
}
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td [attr.colspan]="visibleColumns().length + 1" style="text-align: center; padding: 24px">
|
||||
{{ i18n.t('panel.emptyDir') }}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (panel().filter() && filterMatches() === 0) {
|
||||
<tr>
|
||||
<td [attr.colspan]="visibleColumns().length + 1" style="text-align: center; padding: 24px">
|
||||
{{ i18n.t('panel.noMatches') }}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
@if (panel().slowLoad() && !panel().connecting()) {
|
||||
|
||||
@@ -84,9 +84,6 @@ export class FilePanel {
|
||||
const visible = this.settings.columns();
|
||||
return COLUMN_DEFS.filter((c) => visible.includes(c.key));
|
||||
});
|
||||
readonly gridTemplate = computed(() =>
|
||||
['minmax(0,1fr)', ...this.visibleColumns().map((c) => c.width)].join(' '),
|
||||
);
|
||||
|
||||
headerContextMenu(event: MouseEvent): void {
|
||||
event.preventDefault();
|
||||
@@ -197,11 +194,6 @@ export class FilePanel {
|
||||
.filter((e) => e.name !== '..').length;
|
||||
}
|
||||
|
||||
sortIndicator(key: SortKey): string {
|
||||
if (this.panel().sortKey() !== key) return '';
|
||||
return this.panel().sortAsc() ? '↑' : '↓';
|
||||
}
|
||||
|
||||
commitPath(): void {
|
||||
if (this.blocked()) return;
|
||||
const value = this.pathDraft().trim();
|
||||
|
||||
Reference in New Issue
Block a user