mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-10-30 08:40:52 +08:00
178 lines
8.0 KiB
Svelte
178 lines
8.0 KiB
Svelte
<script lang="ts" module>
|
|
import type { GitStatusEntry } from "@pierre/trees";
|
|
|
|
import { FileTree } from "@pierre/trees";
|
|
|
|
// App file glyph (path lifted from Icon.svelte, viewBox 0 0 16 16) exposed as
|
|
// a sprite so the tree uses our file icon instead of Pierre's. Folders are
|
|
// handled separately via CSS masks (see `unsafeCSS` below), because Pierre's
|
|
// folder icon slot holds an expand chevron whose open/closed state is only
|
|
// reachable through the `aria-expanded` attribute in CSS.
|
|
const iconSpriteSheet = `
|
|
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" style="display: none">
|
|
<symbol id="app-tree-file" viewBox="0 0 16 16">
|
|
<path fill="currentColor" d="M13.5 14.5L2.5 14.5L2.5 1.5L9.20703 1.5L13.5 5.79297L13.5 14.5ZM8.5 6.5L8.5 2.5L3.5 2.5L3.5 13.5L12.5 13.5L12.5 6.5L8.5 6.5ZM9.5 3.20703L9.5 5.5L11.793 5.5L9.5 3.20703Z" />
|
|
</symbol>
|
|
</svg>
|
|
`;
|
|
|
|
// Folder glyphs as CSS mask data URIs (open + closed), swapped by
|
|
// `aria-expanded` on the row. `fill` is irrelevant — masks use alpha only;
|
|
// the visible color comes from `background-color`.
|
|
const folderClosedMask =
|
|
"url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='black' d='M7.20703 2.5L9.20703 4.5H14.5V14.5H1.5V2.5H7.20703ZM2.5 13.5H13.5V5.5H2.5V13.5ZM2.5 4.5H7.79297L6.79297 3.5H2.5V4.5Z'/%3E%3C/svg%3E\")";
|
|
const folderOpenMask =
|
|
"url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='black' d='M7.20703 2.5L9.20703 4.5H14.5V6.5H15.6631L13.377 14.5H1.5V2.5H7.20703ZM2.70898 13.5H12.623L14.3369 7.5H4.85254L2.70898 13.5ZM2.5 11.1133L4.0293 6.83203L4.14746 6.5H13.5V5.5H8.79297L6.79297 3.5H2.5V11.1133Z'/%3E%3C/svg%3E\")";
|
|
</script>
|
|
|
|
<script lang="ts">
|
|
import { theme } from "@app/components/ThemeSwitch.svelte";
|
|
|
|
interface Props {
|
|
paths: string[];
|
|
gitStatus?: GitStatusEntry[];
|
|
// Fires only for leaf files; folder selections are ignored.
|
|
onSelect: (path: string) => void;
|
|
}
|
|
|
|
const { paths, gitStatus = undefined, onSelect }: Props = $props();
|
|
|
|
let container = $state<HTMLElement>();
|
|
|
|
$effect(() => {
|
|
const el = container;
|
|
if (!el) {
|
|
return;
|
|
}
|
|
const p = paths;
|
|
const status = gitStatus;
|
|
// The tree renders in a shadow root and picks light/dark via CSS
|
|
// `light-dark()`, which follows `color-scheme`. Its base `:host` hard-sets
|
|
// `light dark` (so it would follow the OS), so force the app's theme via
|
|
// `unsafeCSS` (injected into the winning `@layer unsafe`). Reading `$theme`
|
|
// here also rebuilds the tree when the theme changes.
|
|
const unsafeCSS = `
|
|
:host { color-scheme: ${$theme}; }
|
|
/* Force monochrome file icons: override Pierre's per-file-type colored
|
|
rules (which win on specificity in the base layer) from the highest
|
|
@layer unsafe. */
|
|
[data-item-section="icon"] [data-icon-token] {
|
|
color: var(--trees-fg-muted);
|
|
}
|
|
/* Folders: Pierre puts an expand chevron in the icon slot. Hide it and
|
|
paint a folder glyph in its place via a mask, so the icon keeps its
|
|
natural position and indent. Open/closed follows the row's
|
|
\`aria-expanded\` state. */
|
|
[data-item-type="folder"] > [data-item-section="icon"] > [data-icon-name="file-tree-icon-chevron"] {
|
|
display: none;
|
|
}
|
|
[data-item-type="folder"] > [data-item-section="icon"]::after {
|
|
content: "";
|
|
width: var(--trees-icon-width);
|
|
height: var(--trees-icon-width);
|
|
background-color: var(--trees-fg-muted);
|
|
-webkit-mask: center / contain no-repeat ${folderClosedMask};
|
|
mask: center / contain no-repeat ${folderClosedMask};
|
|
}
|
|
[aria-expanded="true"][data-item-type="folder"] > [data-item-section="icon"]::after {
|
|
-webkit-mask-image: ${folderOpenMask};
|
|
mask-image: ${folderOpenMask};
|
|
}
|
|
/* The directory rollup dot is nudged by \`translateY(.65ex - 50%)\`, an
|
|
x-height offset tuned for system-ui. Our Booton override has a
|
|
different x-height, so it lands too high; the row already flex-centers,
|
|
so drop the nudge and let centering handle it. */
|
|
[data-item-section="git"] [data-icon-name="file-tree-icon-dot"] {
|
|
transform: none;
|
|
}
|
|
/* Flattened folder segments (e.g. ".github / workflows") underline on
|
|
hover by default; the app doesn't underline these, so drop it. */
|
|
[data-item-flattened-subitem]:hover {
|
|
text-decoration: none;
|
|
}
|
|
`;
|
|
// Only the leaf files are selectable targets; folder selections are ignored.
|
|
const files = new Set(p);
|
|
const tree = new FileTree({
|
|
paths: p,
|
|
gitStatus: status,
|
|
initialExpansion: "open",
|
|
flattenEmptyDirectories: true,
|
|
unsafeCSS,
|
|
// `set: "none"` drops Pierre's per-file-type glyphs; files use the app's
|
|
// file glyph via the custom sprite. Folders are handled by the CSS mask
|
|
// in `unsafeCSS` above (the folder icon slot is really an expand chevron).
|
|
icons: {
|
|
set: "none",
|
|
spriteSheet: iconSpriteSheet,
|
|
remap: {
|
|
"file-tree-icon-file": "app-tree-file",
|
|
},
|
|
},
|
|
onSelectionChange: selected => {
|
|
const path = selected.at(-1);
|
|
if (path !== undefined && files.has(path)) {
|
|
onSelect(path);
|
|
}
|
|
},
|
|
});
|
|
tree.render({ containerWrapper: el });
|
|
return () => tree.cleanUp();
|
|
});
|
|
</script>
|
|
|
|
<style>
|
|
.pierre-tree {
|
|
height: 100%;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
/* Own the inline gap on the host (not a scroll container) so both sides
|
|
are an even 0.5rem. Pierre's own inline padding is zeroed below because
|
|
its `scrollbar-gutter: stable` eats the right padding, leaving the two
|
|
sides uneven. */
|
|
box-sizing: border-box;
|
|
padding: 0 0.5rem;
|
|
|
|
/* Match the tree to the app's design tokens. These `--trees-*-override`
|
|
hooks are the top of Pierre's fallback chain (override → theme → default)
|
|
and inherit across the shadow boundary. */
|
|
/* Surfaces & text */
|
|
--trees-bg-override: var(--color-surface-canvas);
|
|
--trees-fg-override: var(--color-text-primary);
|
|
--trees-fg-muted-override: var(--color-text-secondary);
|
|
--trees-bg-muted-override: var(--color-surface-subtle);
|
|
--trees-selected-bg-override: var(--color-surface-mid);
|
|
--trees-selected-fg-override: var(--color-text-primary);
|
|
--trees-border-color-override: var(--color-border-subtle);
|
|
--trees-indent-guide-bg-override: var(--color-border-subtle);
|
|
/* Accent uses our brand (not Pierre's default blue) for any non-border
|
|
accents (e.g. search matches). */
|
|
--trees-accent-override: var(--color-border-brand);
|
|
/* But no blue border around the active/selected row — the app indicates
|
|
selection with a background, not a focus/selection ring. */
|
|
--trees-selected-focused-border-color-override: transparent;
|
|
--trees-focus-ring-color-override: transparent;
|
|
/* Search input */
|
|
--trees-input-bg-override: var(--color-surface-base);
|
|
--trees-search-bg-override: var(--color-surface-base);
|
|
--trees-search-fg-override: var(--color-text-primary);
|
|
/* Git status indicators */
|
|
--trees-git-added-color-override: var(--color-feedback-success-text);
|
|
--trees-git-deleted-color-override: var(--color-feedback-error-text);
|
|
--trees-git-modified-color-override: var(--color-text-brand);
|
|
--trees-git-renamed-color-override: var(--color-feedback-warning-text);
|
|
--trees-git-untracked-color-override: var(--color-feedback-success-text);
|
|
--trees-git-ignored-color-override: var(--color-text-tertiary);
|
|
/* Typography & shape */
|
|
--trees-font-family-override: var(--font-family-ui);
|
|
--trees-font-size-override: 0.875rem;
|
|
--trees-border-radius-override: var(--border-radius-sm);
|
|
/* Inline gap is handled by the host's padding (above); zero Pierre's own so
|
|
the two aren't added together and the right side isn't eaten by its
|
|
scrollbar gutter. */
|
|
--trees-padding-inline-override: 0;
|
|
}
|
|
</style>
|
|
|
|
<div bind:this={container} class="pierre-tree"></div>
|