fix(ui): truncate long header subtitles, show a dash for missing project folders, remove unused badge wrappers
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
0b145af parent: 9d90581 deleted
packages/ui/app/components/AgentBadge.vue +0 -10 | deleted file mode 100644 | ||
| @@ -1,10 +0,0 @@ | ||
| 1 | -<script setup lang="ts"> | |
| 2 | -import type { AgentId } from '@skilly/core' | |
| 3 | - | |
| 4 | -// Legacy name kept for existing screens; agents are neutral in the new design (see SkAgentBadge). | |
| 5 | -withDefaults(defineProps<{ agent: AgentId, compact?: boolean }>(), { compact: false }) | |
| 6 | -</script> | |
| 7 | - | |
| 8 | -<template> | |
| 9 | - <SkAgentBadge :agent="agent" :compact="compact" variant="tag" /> | |
| 10 | -</template> | |
| deleted file mode 100644 | |||
| @@ -1,10 +0,0 @@ | |||
| 1 | -<script setup lang="ts"> | ||
| 2 | -import type { AgentId } from '@skilly/core' | ||
| 3 | - | ||
| 4 | -// Legacy name kept for existing screens; agents are neutral in the new design (see SkAgentBadge). | ||
| 5 | -withDefaults(defineProps<{ agent: AgentId, compact?: boolean }>(), { compact: false }) | ||
| 6 | -</script> | ||
| 7 | - | ||
| 8 | -<template> | ||
| 9 | - <SkAgentBadge :agent="agent" :compact="compact" variant="tag" /> | ||
| 10 | -</template> | ||
modified
packages/ui/app/components/CatalogHeader.vue +1 -1 | @@ -28,7 +28,7 @@ const menu = computed(() => [CAT_ORDER.map(t => ({ | ||
| 28 | 28 | </script> |
| 29 | 29 | |
| 30 | 30 | <template> |
| 31 | - <SkPageHeader title="Catalog"> | |
| 31 | + <SkPageHeader title="Catalog" truncate-subtitle :subtitle-title="where ? `Your shared library. ${where}` : undefined"> | |
| 32 | 32 | <template #subtitle> |
| 33 | 33 | Your shared library. |
| 34 | 34 | <span v-if="where" class="font-mono text-xs" :title="where">{{ where }}</span> |
| @@ -28,7 +28,7 @@ const menu = computed(() => [CAT_ORDER.map(t => ({ | |||
| 28 | </script> | 28 | </script> |
| 29 | 29 | ||
| 30 | <template> | 30 | <template> |
| 31 | - <SkPageHeader title="Catalog"> | 31 | + <SkPageHeader title="Catalog" truncate-subtitle :subtitle-title="where ? `Your shared library. ${where}` : undefined"> |
| 32 | <template #subtitle> | 32 | <template #subtitle> |
| 33 | Your shared library. | 33 | Your shared library. |
| 34 | <span v-if="where" class="font-mono text-xs" :title="where">{{ where }}</span> | 34 | <span v-if="where" class="font-mono text-xs" :title="where">{{ where }}</span> |
deleted
packages/ui/app/components/StatusBadge.vue +0 -14 | deleted file mode 100644 | ||
| @@ -1,14 +0,0 @@ | ||
| 1 | -<script setup lang="ts"> | |
| 2 | -import type { ItemStatus } from '@skilly/core' | |
| 3 | - | |
| 4 | -// Legacy wrapper kept for existing screens; new screens use SkStatus directly. | |
| 5 | -defineProps<{ status?: ItemStatus }>() | |
| 6 | -</script> | |
| 7 | - | |
| 8 | -<template> | |
| 9 | - <div v-if="status" class="flex flex-wrap items-center gap-1.5"> | |
| 10 | - <SkStatus :state="status.state" /> | |
| 11 | - <SkTag v-if="status.managed" mono title="Written by skilly: it keeps this file up to date.">managed</SkTag> | |
| 12 | - </div> | |
| 13 | - <span v-else class="text-xs text-sk-faint">-</span> | |
| 14 | -</template> | |
| deleted file mode 100644 | |||
| @@ -1,14 +0,0 @@ | |||
| 1 | -<script setup lang="ts"> | ||
| 2 | -import type { ItemStatus } from '@skilly/core' | ||
| 3 | - | ||
| 4 | -// Legacy wrapper kept for existing screens; new screens use SkStatus directly. | ||
| 5 | -defineProps<{ status?: ItemStatus }>() | ||
| 6 | -</script> | ||
| 7 | - | ||
| 8 | -<template> | ||
| 9 | - <div v-if="status" class="flex flex-wrap items-center gap-1.5"> | ||
| 10 | - <SkStatus :state="status.state" /> | ||
| 11 | - <SkTag v-if="status.managed" mono title="Written by skilly: it keeps this file up to date.">managed</SkTag> | ||
| 12 | - </div> | ||
| 13 | - <span v-else class="text-xs text-sk-faint">-</span> | ||
| 14 | -</template> | ||
modified
packages/ui/app/components/projects/ProjectsTable.vue +1 -1 | @@ -72,7 +72,7 @@ const rowStatus = (p: ProjectSummary): { tone: Tone, label: string } | null => { | ||
| 72 | 72 | } |
| 73 | 73 | const agentsOf = (p: ProjectSummary) => summaries.value[p.id]?.agents |
| 74 | 74 | ?? (p.agentsDetected.length ? p.agentsDetected.map(a => AGENT_LABELS[a]).join(', ') : '—') |
| 75 | -const countOf = (p: ProjectSummary) => summaries.value[p.id]?.items ?? Object.entries(p.counts).filter(([t]) => t !== 'instructions').reduce((n, [, c]) => n + c, 0) | |
| 75 | +const countOf = (p: ProjectSummary) => !p.exists ? '—' : summaries.value[p.id]?.items ?? Object.entries(p.counts).filter(([t]) => t !== 'instructions').reduce((n, [, c]) => n + c, 0) | |
| 76 | 76 | </script> |
| 77 | 77 | |
| 78 | 78 | <template> |
| @@ -72,7 +72,7 @@ const rowStatus = (p: ProjectSummary): { tone: Tone, label: string } | null => { | |||
| 72 | } | 72 | } |
| 73 | const agentsOf = (p: ProjectSummary) => summaries.value[p.id]?.agents | 73 | const agentsOf = (p: ProjectSummary) => summaries.value[p.id]?.agents |
| 74 | ?? (p.agentsDetected.length ? p.agentsDetected.map(a => AGENT_LABELS[a]).join(', ') : '—') | 74 | ?? (p.agentsDetected.length ? p.agentsDetected.map(a => AGENT_LABELS[a]).join(', ') : '—') |
| 75 | -const countOf = (p: ProjectSummary) => summaries.value[p.id]?.items ?? Object.entries(p.counts).filter(([t]) => t !== 'instructions').reduce((n, [, c]) => n + c, 0) | 75 | +const countOf = (p: ProjectSummary) => !p.exists ? '—' : summaries.value[p.id]?.items ?? Object.entries(p.counts).filter(([t]) => t !== 'instructions').reduce((n, [, c]) => n + c, 0) |
| 76 | </script> | 76 | </script> |
| 77 | 77 | ||
| 78 | <template> | 78 | <template> |
modified
packages/ui/app/components/scope/ScopePage.vue +3 -3 | @@ -87,12 +87,12 @@ const homeInstructionCount = computed(() => (inventory.value?.items ?? []).filte | ||
| 87 | 87 | |
| 88 | 88 | <template> |
| 89 | 89 | <SkPage> |
| 90 | - <SkPageHeader :title="home ? 'Home · user-level config' : title" :subtitle="home ? 'Everything here is visible in every project.' : undefined" :aside="home"> | |
| 90 | + <SkPageHeader :title="home ? 'Home · user-level config' : title" :subtitle="home ? 'Everything here is visible in every project.' : undefined" :aside="home" truncate-subtitle :subtitle-title="rootPath"> | |
| 91 | 91 | <template v-if="home" #lead><SkAvatar /></template> |
| 92 | 92 | <template v-else #breadcrumb><SkBreadcrumb :items="[{ label: 'Projects', to: '/projects' }]" /></template> |
| 93 | 93 | <template v-if="!home" #subtitle> |
| 94 | - <span class="flex flex-wrap items-center gap-x-2 gap-y-1 font-mono text-xs text-sk-faint"> | |
| 95 | - <span class="max-w-[360px] truncate" :title="rootPath">{{ rootPath }}</span> | |
| 94 | + <span class="flex items-center gap-x-2 font-mono text-xs text-sk-faint"> | |
| 95 | + <span class="min-w-0 max-w-[360px] shrink truncate" :title="rootPath">{{ rootPath }}</span> | |
| 96 | 96 | <template v-if="inventory"> |
| 97 | 97 | <span aria-hidden="true">·</span> |
| 98 | 98 | <ScopeTargets v-if="config" /> |
| @@ -87,12 +87,12 @@ const homeInstructionCount = computed(() => (inventory.value?.items ?? []).filte | |||
| 87 | 87 | ||
| 88 | <template> | 88 | <template> |
| 89 | <SkPage> | 89 | <SkPage> |
| 90 | - <SkPageHeader :title="home ? 'Home · user-level config' : title" :subtitle="home ? 'Everything here is visible in every project.' : undefined" :aside="home"> | 90 | + <SkPageHeader :title="home ? 'Home · user-level config' : title" :subtitle="home ? 'Everything here is visible in every project.' : undefined" :aside="home" truncate-subtitle :subtitle-title="rootPath"> |
| 91 | <template v-if="home" #lead><SkAvatar /></template> | 91 | <template v-if="home" #lead><SkAvatar /></template> |
| 92 | <template v-else #breadcrumb><SkBreadcrumb :items="[{ label: 'Projects', to: '/projects' }]" /></template> | 92 | <template v-else #breadcrumb><SkBreadcrumb :items="[{ label: 'Projects', to: '/projects' }]" /></template> |
| 93 | <template v-if="!home" #subtitle> | 93 | <template v-if="!home" #subtitle> |
| 94 | - <span class="flex flex-wrap items-center gap-x-2 gap-y-1 font-mono text-xs text-sk-faint"> | 94 | + <span class="flex items-center gap-x-2 font-mono text-xs text-sk-faint"> |
| 95 | - <span class="max-w-[360px] truncate" :title="rootPath">{{ rootPath }}</span> | 95 | + <span class="min-w-0 max-w-[360px] shrink truncate" :title="rootPath">{{ rootPath }}</span> |
| 96 | <template v-if="inventory"> | 96 | <template v-if="inventory"> |
| 97 | <span aria-hidden="true">·</span> | 97 | <span aria-hidden="true">·</span> |
| 98 | <ScopeTargets v-if="config" /> | 98 | <ScopeTargets v-if="config" /> |
modified
packages/ui/app/components/sk/SkPageHeader.vue +9 -3 | @@ -4,22 +4,28 @@ | ||
| 4 | 4 | * - `#lead`: something before the title (e.g. the `~` SkAvatar on Home) |
| 5 | 5 | * - `#breadcrumb`: above the title (SkBreadcrumb) |
| 6 | 6 | * - `#subtitle` or `subtitle` prop; `aside` puts the subtitle on the right of the title row (Home style) |
| 7 | + * - `truncateSubtitle`: single-line subtitle with an ellipsis; the full text is exposed as a tooltip (`subtitleTitle` or the subtitle itself) | |
| 7 | 8 | * - `#actions`: buttons / SkSeg tabs on the right |
| 8 | 9 | */ |
| 9 | -withDefaults(defineProps<{ title: string, subtitle?: string, aside?: boolean }>(), { aside: false }) | |
| 10 | +withDefaults(defineProps<{ title: string, subtitle?: string, aside?: boolean, truncateSubtitle?: boolean, subtitleTitle?: string }>(), { aside: false, truncateSubtitle: false }) | |
| 10 | 11 | </script> |
| 11 | 12 | |
| 12 | 13 | <template> |
| 13 | 14 | <header class="flex flex-wrap items-center gap-x-3 gap-y-2"> |
| 14 | 15 | <slot name="lead" /> |
| 15 | - <div class="min-w-0"> | |
| 16 | + <div class="min-w-0" :class="truncateSubtitle && 'max-w-full'"> | |
| 16 | 17 | <div v-if="$slots.breadcrumb" class="mb-1.5 text-sk-muted"> |
| 17 | 18 | <slot name="breadcrumb" /> |
| 18 | 19 | </div> |
| 19 | 20 | <h1 class="m-0 truncate font-mono text-[22px] leading-[1.25] font-normal tracking-[-0.01em] text-sk-text"> |
| 20 | 21 | {{ title }} |
| 21 | 22 | </h1> |
| 22 | - <p v-if="!aside && (subtitle || $slots.subtitle)" class="mt-1 mb-0 text-pretty text-sk-muted"> | |
| 23 | + <p | |
| 24 | + v-if="!aside && (subtitle || $slots.subtitle)" | |
| 25 | + class="mt-1 mb-0 text-sk-muted" | |
| 26 | + :class="truncateSubtitle ? 'truncate' : 'text-pretty'" | |
| 27 | + :title="truncateSubtitle ? (subtitleTitle ?? subtitle) : undefined" | |
| 28 | + > | |
| 23 | 29 | <slot name="subtitle">{{ subtitle }}</slot> |
| 24 | 30 | </p> |
| 25 | 31 | </div> |
| @@ -4,22 +4,28 @@ | |||
| 4 | * - `#lead`: something before the title (e.g. the `~` SkAvatar on Home) | 4 | * - `#lead`: something before the title (e.g. the `~` SkAvatar on Home) |
| 5 | * - `#breadcrumb`: above the title (SkBreadcrumb) | 5 | * - `#breadcrumb`: above the title (SkBreadcrumb) |
| 6 | * - `#subtitle` or `subtitle` prop; `aside` puts the subtitle on the right of the title row (Home style) | 6 | * - `#subtitle` or `subtitle` prop; `aside` puts the subtitle on the right of the title row (Home style) |
| 7 | + * - `truncateSubtitle`: single-line subtitle with an ellipsis; the full text is exposed as a tooltip (`subtitleTitle` or the subtitle itself) | ||
| 7 | * - `#actions`: buttons / SkSeg tabs on the right | 8 | * - `#actions`: buttons / SkSeg tabs on the right |
| 8 | */ | 9 | */ |
| 9 | -withDefaults(defineProps<{ title: string, subtitle?: string, aside?: boolean }>(), { aside: false }) | 10 | +withDefaults(defineProps<{ title: string, subtitle?: string, aside?: boolean, truncateSubtitle?: boolean, subtitleTitle?: string }>(), { aside: false, truncateSubtitle: false }) |
| 10 | </script> | 11 | </script> |
| 11 | 12 | ||
| 12 | <template> | 13 | <template> |
| 13 | <header class="flex flex-wrap items-center gap-x-3 gap-y-2"> | 14 | <header class="flex flex-wrap items-center gap-x-3 gap-y-2"> |
| 14 | <slot name="lead" /> | 15 | <slot name="lead" /> |
| 15 | - <div class="min-w-0"> | 16 | + <div class="min-w-0" :class="truncateSubtitle && 'max-w-full'"> |
| 16 | <div v-if="$slots.breadcrumb" class="mb-1.5 text-sk-muted"> | 17 | <div v-if="$slots.breadcrumb" class="mb-1.5 text-sk-muted"> |
| 17 | <slot name="breadcrumb" /> | 18 | <slot name="breadcrumb" /> |
| 18 | </div> | 19 | </div> |
| 19 | <h1 class="m-0 truncate font-mono text-[22px] leading-[1.25] font-normal tracking-[-0.01em] text-sk-text"> | 20 | <h1 class="m-0 truncate font-mono text-[22px] leading-[1.25] font-normal tracking-[-0.01em] text-sk-text"> |
| 20 | {{ title }} | 21 | {{ title }} |
| 21 | </h1> | 22 | </h1> |
| 22 | - <p v-if="!aside && (subtitle || $slots.subtitle)" class="mt-1 mb-0 text-pretty text-sk-muted"> | 23 | + <p |
| 24 | + v-if="!aside && (subtitle || $slots.subtitle)" | ||
| 25 | + class="mt-1 mb-0 text-sk-muted" | ||
| 26 | + :class="truncateSubtitle ? 'truncate' : 'text-pretty'" | ||
| 27 | + :title="truncateSubtitle ? (subtitleTitle ?? subtitle) : undefined" | ||
| 28 | + > | ||
| 23 | <slot name="subtitle">{{ subtitle }}</slot> | 29 | <slot name="subtitle">{{ subtitle }}</slot> |
| 24 | </p> | 30 | </p> |
| 25 | </div> | 31 | </div> |
modified
packages/ui/app/pages/sync.vue +1 -1 | @@ -121,7 +121,7 @@ const noRemoteHint = 'Set a remote first' | ||
| 121 | 121 | |
| 122 | 122 | <template> |
| 123 | 123 | <SkPage> |
| 124 | - <SkPageHeader title="Sync" :subtitle="subtitle"> | |
| 124 | + <SkPageHeader title="Sync" :subtitle="subtitle" truncate-subtitle> | |
| 125 | 125 | <template #actions> |
| 126 | 126 | <SkButton variant="ghost" icon="i-lucide-refresh-cw" label="Refresh" :disabled="working" @click="load" /> |
| 127 | 127 | </template> |
| @@ -121,7 +121,7 @@ const noRemoteHint = 'Set a remote first' | |||
| 121 | 121 | ||
| 122 | <template> | 122 | <template> |
| 123 | <SkPage> | 123 | <SkPage> |
| 124 | - <SkPageHeader title="Sync" :subtitle="subtitle"> | 124 | + <SkPageHeader title="Sync" :subtitle="subtitle" truncate-subtitle> |
| 125 | <template #actions> | 125 | <template #actions> |
| 126 | <SkButton variant="ghost" icon="i-lucide-refresh-cw" label="Refresh" :disabled="working" @click="load" /> | 126 | <SkButton variant="ghost" icon="i-lucide-refresh-cw" label="Refresh" :disabled="working" @click="load" /> |
| 127 | </template> | 127 | </template> |