Skip to content

Commit 92bc6ca

Browse files
authored
fix(registry): keep file tree names on one line (#532)
1 parent a4a57af commit 92bc6ca

3 files changed

Lines changed: 93 additions & 26 deletions

File tree

‎layers/registry/app/components/SkillFileTreeNode.vue‎

Lines changed: 54 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script setup lang="ts">
22
import type { SkillFileTreeNode as TreeNode } from '../utils/skill-file-tree'
33
import { formatByteSize } from '../utils/skill-context-cost'
4-
import { fileIcon, isInlineRenderable } from '../utils/skill-file-tree'
4+
import { fileIcon, isInlineRenderable, splitFileLabel } from '../utils/skill-file-tree'
55
66
const props = defineProps<{
77
node: TreeNode
@@ -55,6 +55,8 @@ function onFileClick(event: MouseEvent) {
5555
emit('select', props.node.path)
5656
}
5757
58+
const label = computed(() => splitFileLabel(props.node.name))
59+
5860
const inlineRenderable = computed(() =>
5961
props.node.kind === 'file' && !!props.node.asset && isInlineRenderable(props.node.asset.type),
6062
)
@@ -68,22 +70,29 @@ const inlineRenderable = computed(() =>
6870
class="tree-row"
6971
:aria-expanded="open"
7072
:aria-controls="`${idPrefix}-${node.path}`"
73+
:title="node.path"
7174
@click="open = !open"
7275
>
73-
<UIcon
74-
name="i-lucide-chevron-right"
75-
class="size-3 text-muted transition-transform shrink-0"
76-
:class="{ 'rotate-90': open }"
77-
aria-hidden="true"
78-
/>
7976
<SkillFileIcon
8077
:name="open ? 'default-folder-opened' : 'default-folder'"
8178
class="size-4 shrink-0"
8279
/>
83-
<span class="tree-label">{{ node.name }}</span>
80+
<span
81+
class="tree-label"
82+
:style="{ '--label-tail': `${label.tail.length}ch` }"
83+
>
84+
<span class="tree-label-head">{{ label.head }}</span>
85+
<span v-if="label.tail">{{ label.tail }}</span>
86+
</span>
8487
<span
8588
class="tree-size"
8689
>{{ folderFileCount }} {{ folderFileCount === 1 ? 'file' : 'files' }}</span>
90+
<UIcon
91+
name="i-lucide-chevron-right"
92+
class="size-3 text-muted transition-transform shrink-0"
93+
:class="{ 'rotate-90': open }"
94+
aria-hidden="true"
95+
/>
8796
</button>
8897
<ul
8998
v-show="open"
@@ -122,12 +131,17 @@ const inlineRenderable = computed(() =>
122131
:title="node.path"
123132
@click="onFileClick"
124133
>
125-
<span class="tree-spacer" aria-hidden="true" />
126134
<SkillFileIcon
127135
:name="fileIcon(node.name)"
128136
class="size-4 shrink-0"
129137
/>
130-
<span class="tree-label">{{ node.name }}</span>
138+
<span
139+
class="tree-label"
140+
:style="{ '--label-tail': `${label.tail.length}ch` }"
141+
>
142+
<span class="tree-label-head">{{ label.head }}</span>
143+
<span v-if="label.tail">{{ label.tail }}</span>
144+
</span>
131145
<span
132146
v-if="node.asset?.size"
133147
class="tree-size"
@@ -142,12 +156,17 @@ const inlineRenderable = computed(() =>
142156
:title="node.path"
143157
@click="onFileClick"
144158
>
145-
<span class="tree-spacer" aria-hidden="true" />
146159
<SkillFileIcon
147160
:name="fileIcon(node.name)"
148161
class="size-4 shrink-0"
149162
/>
150-
<span class="tree-label">{{ node.name }}</span>
163+
<span
164+
class="tree-label"
165+
:style="{ '--label-tail': `${label.tail.length}ch` }"
166+
>
167+
<span class="tree-label-head">{{ label.head }}</span>
168+
<span v-if="label.tail">{{ label.tail }}</span>
169+
</span>
151170
<span
152171
v-if="node.asset?.size"
153172
class="tree-size"
@@ -161,12 +180,17 @@ const inlineRenderable = computed(() =>
161180
class="tree-row file"
162181
:title="node.path"
163182
>
164-
<span class="tree-spacer" aria-hidden="true" />
165183
<SkillFileIcon
166184
:name="fileIcon(node.name)"
167185
class="size-4 shrink-0"
168186
/>
169-
<span class="tree-label">{{ node.name }}</span>
187+
<span
188+
class="tree-label"
189+
:style="{ '--label-tail': `${label.tail.length}ch` }"
190+
>
191+
<span class="tree-label-head">{{ label.head }}</span>
192+
<span v-if="label.tail">{{ label.tail }}</span>
193+
</span>
170194
<span
171195
v-if="node.asset?.size"
172196
class="tree-size"
@@ -185,13 +209,14 @@ const inlineRenderable = computed(() =>
185209
.tree-children {
186210
list-style: none;
187211
margin: 0;
188-
padding-left: 0.875rem;
212+
padding-left: 0.375rem;
189213
border-left: 1px dashed var(--ui-border);
190-
margin-left: 0.4375rem;
214+
/* Puts the guide line under the parent folder icon. */
215+
margin-left: 0.9375rem;
191216
}
192217
.tree-row {
193218
display: flex;
194-
align-items: flex-start;
219+
align-items: center;
195220
gap: 0.375rem;
196221
width: 100%;
197222
padding: 0.25rem 0.5rem;
@@ -217,21 +242,25 @@ const inlineRenderable = computed(() =>
217242
background: var(--ui-bg-muted);
218243
font-weight: 500;
219244
}
220-
.tree-spacer {
221-
width: 0.75rem;
222-
flex-shrink: 0;
223-
}
224245
.tree-size {
225246
flex-shrink: 0;
226247
font-size: 0.6875rem;
227248
font-variant-numeric: tabular-nums;
228249
color: var(--ui-text-muted);
229250
}
230-
/* The explorer column is narrow, so long names wrap instead of hiding the
231-
part that tells two files apart. */
232251
.tree-label {
233-
min-width: 0;
252+
display: flex;
234253
flex: 1;
235-
overflow-wrap: anywhere;
254+
min-width: 0;
255+
white-space: nowrap;
256+
container-type: inline-size;
257+
}
258+
/* The tree is monospace. Rounding the head down to whole characters puts the
259+
ellipsis flush against the tail, with no part-character gap between them. */
260+
.tree-label-head {
261+
min-width: 0;
262+
max-width: round(down, calc(100cqw - var(--label-tail)), 1ch);
263+
overflow: hidden;
264+
text-overflow: ellipsis;
236265
}
237266
</style>

‎layers/registry/app/utils/skill-file-tree.ts‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,3 +173,26 @@ export function highlightLangFromPath(path: string): string {
173173
export function isInlineRenderable(type: 'markdown' | 'code' | 'image' | 'data' | 'other'): boolean {
174174
return type === 'markdown' || type === 'code' || type === 'data'
175175
}
176+
177+
const LABEL_SPLIT_MIN = 17
178+
const LABEL_TAIL_MIN = 6
179+
const LABEL_TAIL_MAX = 12
180+
const LABEL_TAIL_FALLBACK = 8
181+
const LABEL_BOUNDARY = /[-_. ]/g
182+
183+
/**
184+
* Splits a long file name so the tree can truncate its middle and keep its
185+
* end. Names like `setup-v1.md` and `setup-v2.md` then stay apart in a narrow
186+
* column. The tail starts after a separator when one gives a tail of 6 to 12
187+
* characters, so a word is not cut in half.
188+
*/
189+
export function splitFileLabel(name: string): { head: string, tail: string } {
190+
if (name.length < LABEL_SPLIT_MIN)
191+
return { head: name, tail: '' }
192+
const cut = [...name.matchAll(LABEL_BOUNDARY)]
193+
.map(match => match.index + 1)
194+
.reverse()
195+
.find(index => name.length - index >= LABEL_TAIL_MIN && name.length - index <= LABEL_TAIL_MAX)
196+
?? name.length - LABEL_TAIL_FALLBACK
197+
return { head: name.slice(0, cut), tail: name.slice(cut) }
198+
}

‎test/unit/skill-file-tree.test.ts‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { SkillFileTreeNode } from '../../layers/registry/app/utils/skill-file-tree'
22
import { describe, expect, it } from 'vitest'
3-
import { buildSkillFileTree } from '../../layers/registry/app/utils/skill-file-tree'
3+
import { buildSkillFileTree, splitFileLabel } from '../../layers/registry/app/utils/skill-file-tree'
44

55
function file(path: string, type: 'markdown' | 'code' = 'markdown') {
66
return { path, size: 100, type }
@@ -55,3 +55,18 @@ describe('skill file tree', () => {
5555
expect(tree.at(-1)).toMatchObject({ kind: 'file', asset: { path: 'SKILL.md', size: 7900 } })
5656
})
5757
})
58+
59+
describe('splitFileLabel', () => {
60+
it('keeps a short name whole', () => {
61+
expect(splitFileLabel('SKILL.md')).toEqual({ head: 'SKILL.md', tail: '' })
62+
})
63+
64+
it('starts the kept tail at a word boundary', () => {
65+
expect(splitFileLabel('package-lock.json')).toEqual({ head: 'package-', tail: 'lock.json' })
66+
expect(splitFileLabel('shipreel.example.yml')).toEqual({ head: 'shipreel.', tail: 'example.yml' })
67+
})
68+
69+
it('keeps the last eight characters when no boundary fits', () => {
70+
expect(splitFileLabel('abcdefghijklmnopqrstuvwxyz.md')).toEqual({ head: 'abcdefghijklmnopqrstu', tail: 'vwxyz.md' })
71+
})
72+
})

0 commit comments

Comments
 (0)