<template>
<div class="catalog-panel">
<header class="catalog-header">
<h2 class="catalog-title">{{ title }}</h2>
<p class="catalog-hint">{{ hint }}</p>
</header>
<GnAlert v-if="loadError" variant="error" class="catalog-alert">{{ loadError }}</GnAlert>
<div class="catalog-layout" :class="{ 'is-detail-open': detailOpen }">
<aside class="catalog-pane catalog-list-pane" :aria-label="t('settings.skillsCatalog')">
<!-- The controls live with the list they act on, so what they filter
is never in doubt. -->
<div class="catalog-list-head">
<GnSearchField
v-model="query"
:placeholder="t('settings.skillsSearchHint')"
:aria-label="t('settings.skillsSearchLabel')"
/>
<div class="catalog-filters">
<GnSelect
v-model="areaFilter"
class="catalog-area"
:aria-label="t('settings.skillsFilterArea')"
:options="areaOptions"
/>
<GnButton
variant="primary"
size="sm"
icon="ph ph-plus"
class="catalog-add"
@click="startCreate"
>
{{ addLabel }}
</GnButton>
</div>
<p v-if="!loading" class="catalog-count">{{ countLabel }}</p>
</div>
<div class="catalog-list">
<div v-if="loading" class="catalog-loading">
<GnLoader circle :label="t('settings.skillsLoading')" />
</div>
<div v-else-if="!items.length" class="catalog-empty">
<i class="ph ph-notebook" aria-hidden="true"></i>
<p>{{ emptyLabel }}</p>
</div>
<button
v-for="item in items"
:key="item.id"
type="button"
class="catalog-row"
:class="{ 'is-active': item.id === selectedId }"
@click="select(item.id)"
>
<span class="catalog-row-main">
<span class="catalog-row-head">
<span class="catalog-row-name">{{ item.name }}</span>
<span class="catalog-row-version">v{{ item.version }}</span>
</span>
<span class="catalog-row-meta">
{{ areaLabel(item.area) }}
<template v-if="item.visibility === 'shared'">
· {{ t('settings.skillsShared') }}
</template>
</span>
<span v-if="item.description" class="catalog-row-description">
{{ item.description }}
</span>
</span>
<i class="ph ph-caret-right catalog-row-open" aria-hidden="true"></i>
</button>
</div>
</aside>
<section class="catalog-pane catalog-detail">
<button type="button" class="catalog-back" @click="reset">
<i class="ph ph-arrow-left" aria-hidden="true"></i>
<span>{{ t('settings.skillsBack') }}</span>
</button>
<!-- Editing: the document is YAML front matter plus markdown, so the
only honest editor is the source itself. -->
<template v-if="editing">
<header class="catalog-detail-head">
<div class="catalog-detail-title">
<h3>{{ entry ? entry.name : newTitle }}</h3>
<span v-if="entry" class="catalog-detail-version">v{{ entry.version }}</span>
</div>
</header>
<div class="catalog-detail-body">
<GnAlert v-if="saveError" variant="error" class="catalog-save-error">
{{ saveError }}
</GnAlert>
<GnTextarea
v-model="draft"
class="catalog-textarea"
:rows="18"
:disabled="!canEdit || saving"
:placeholder="t('settings.skillsEditorPlaceholder')"
:aria-label="t('settings.skillsEditorLabel')"
/>
<div class="catalog-editor-actions">
<div v-if="authStore.isAdmin && canEdit" class="catalog-visibility">
<GnSelect
v-model="visibility"
:aria-label="t('settings.skillsVisibility')"
:options="visibilityOptions"
/>
</div>
<div class="catalog-editor-buttons">
<GnButton
variant="primary"
:disabled="!canEdit || saving || !draft.trim()"
@click="save"
>
{{ t('common.save') }}
</GnButton>
<GnButton variant="ghost" :disabled="saving" @click="cancel">
{{ t('common.cancel') }}
</GnButton>
<GnButton
v-if="entry && entry.can_edit"
variant="ghost"
icon="ph ph-trash"
:disabled="saving"
@click="remove"
>
{{ t('common.delete') }}
</GnButton>
</div>
</div>
</div>
</template>
<!-- Reading: the parsed document, the way the agent loads it. -->
<template v-else-if="entry">
<header class="catalog-detail-head">
<div class="catalog-detail-title">
<h3>{{ entry.name }}</h3>
<span class="catalog-detail-version">v{{ entry.version }}</span>
</div>
<div class="catalog-detail-actions">
<GnButton
v-if="entry.can_edit"
variant="secondary"
size="sm"
icon="ph ph-pencil-simple"
class="catalog-edit-btn"
@click="beginEdit"
>
{{ t('settings.skillsEdit') }}
</GnButton>
<GnIconButton
v-if="entry.can_edit"
icon="ph ph-trash"
:label="t('common.delete')"
size="sm"
class="catalog-delete-btn"
@click="remove"
/>
</div>
</header>
<div class="catalog-detail-body">
<p v-if="entry.description" class="catalog-description">{{ entry.description }}</p>
<div class="catalog-meta">
<span>{{ areaLabel(entry.area) }}</span>
<span>
{{ entry.visibility === 'shared' ? t('settings.skillsShared') : t('settings.skillsPersonal') }}
</span>
<span v-if="!entry.can_edit" class="catalog-readonly">
{{ t('settings.skillsReadOnly') }}
</span>
<span v-if="entry.updated_at">
{{ t('settings.skillsUpdated', { when: formatWhen(entry.updated_at) }) }}
</span>
<span v-if="entry.tags?.length" class="catalog-tags">
<i class="ph ph-tag" aria-hidden="true"></i>
{{ entry.tags.join(', ') }}
</span>
</div>
<SkillDocument v-if="entry.document" :doc="entry.document" class="catalog-document" />
<template v-else>
<GnAlert variant="warning" class="catalog-parse-error">
{{ t('settings.skillsParseFailed') }}
</GnAlert>
<pre class="catalog-raw">{{ entry.content }}</pre>
</template>
<div class="catalog-history">
<button type="button" class="catalog-history-toggle" @click="toggleHistory">
<i class="ph" :class="showHistory ? 'ph-caret-down' : 'ph-caret-right'" aria-hidden="true"></i>
{{ t('settings.skillsHistory') }}
</button>
<div v-if="showHistory" class="catalog-history-list">
<div v-if="!versions.length" class="catalog-history-empty">
{{ t('settings.skillsHistoryEmpty') }}
</div>
<div v-for="v in versions" :key="v.version" class="catalog-history-item">
<div class="catalog-history-meta">
v{{ v.version }} · {{ v.edited_by || t('settings.skillsUnknownEditor') }}
· {{ formatWhen(v.created_at) }}
<template v-if="v.reason"> · {{ v.reason }}</template>
</div>
<pre class="catalog-history-text">{{ v.content }}</pre>
</div>
</div>
</div>
</div>
</template>
<div v-else-if="detailLoading" class="catalog-detail-body catalog-detail-loading">
<GnLoader circle :label="t('settings.skillsLoading')" />
</div>
<div v-else class="catalog-detail-body">
<div class="catalog-placeholder">
<i class="ph ph-notebook" aria-hidden="true"></i>
<p>{{ t('settings.skillsPlaceholder') }}</p>
</div>
</div>
</section>
</div>
</div>
</template>
<script setup>
import { onMounted, watch } from 'vue'
import {
GnAlert,
GnButton,
GnIconButton,
GnLoader,
GnSearchField,
GnSelect,
GnTextarea,
} from 'gnexus-ui-kit/vue'
import { t } from '@/i18n/index.js'
import SkillDocument from '@/components/skills/SkillDocument.vue'
import { areaLabel } from './skills/constants.js'
import { useCatalogEntry } from './skills/composables/useCatalogEntry.js'
import { useCatalogLabels } from './skills/composables/useCatalogLabels.js'
import { useCatalogList } from './skills/composables/useCatalogList.js'
// One catalog, two sections: Settings shows skills and protocols as separate
// tabs and mounts this panel with the kind it owns.
const props = defineProps({
kind: {
type: String,
default: 'skill',
validator: value => value === 'skill' || value === 'protocol',
},
})
const getKind = () => props.kind
const {
items,
loading,
loadError,
query,
areaFilter,
areaOptions,
load,
} = useCatalogList({ getKind })
const {
title,
hint,
addLabel,
emptyLabel,
countLabel,
newTitle,
} = useCatalogLabels({ getKind, items })
const {
authStore,
entry,
selectedId,
detailLoading,
editing,
draft,
visibility,
saving,
saveError,
versions,
showHistory,
detailOpen,
canEdit,
visibilityOptions,
select,
startCreate,
beginEdit,
cancel,
reset,
save,
remove,
toggleHistory,
formatWhen,
} = useCatalogEntry({ getKind, reload: load, loadError })
watch(() => props.kind, () => {
reset()
load()
})
onMounted(load)
</script>
<style scoped lang="scss">
@use 'kit-deps' as *;
@use './skills/styles/panel' as *;
@use './skills/styles/layout' as *;
@use './skills/styles/rows' as *;
@use './skills/styles/detail' as *;
@use './skills/styles/editor' as *;
@use './skills/styles/history' as *;
@use './skills/styles/placeholder' as *;
</style>