feat: streamline content channel tabs
This commit is contained in:
@@ -15,12 +15,12 @@ The editor should use one shared content body for every selected target channel,
|
||||
- Keep content item editing in `frontend/src/features/content/views/ContentItemDetailView.vue`.
|
||||
- Replace per-channel caption editing with a shared caption and shared hashtags.
|
||||
- Let configured workspace channels be selected or unselected as targets.
|
||||
- Show selected targets in a vertical rail and render preview cards that look closer to social platform previews.
|
||||
- Show target channels as a single vertical tab rail and render preview cards that look closer to social platform previews.
|
||||
- Convert shared hashtags into chip-style entry with suggestions from already used hashtags.
|
||||
- Show a workspace hashtag feed so authors can reuse existing tags.
|
||||
- Keep the editor focused on post text and target channels by removing confusing title, calendar, change summary, and base-caption fields.
|
||||
- Make target preview tabs compact.
|
||||
- Move the create content entry point into the main app menu.
|
||||
- Move the create content entry point into the top app menu bar.
|
||||
- Preserve existing save payloads and backend contracts.
|
||||
|
||||
## Validation
|
||||
@@ -40,5 +40,5 @@ npm run build
|
||||
- [x] Already used hashtags are available as suggestions.
|
||||
- [x] The editor shows a workspace hashtag feed with usage counts.
|
||||
- [x] The editor no longer shows title, calendar, change summary, or base-caption fields.
|
||||
- [x] Target channel preview tabs use compact buttons.
|
||||
- [x] Create content is available from the main app menu.
|
||||
- [x] Target channel tabs use compact icon-only buttons with channel names as accessible labels and tooltips.
|
||||
- [x] Create content is available from the top app menu bar.
|
||||
|
||||
@@ -283,11 +283,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
function toggleChannel(channel) {
|
||||
function selectTargetChannel(channel) {
|
||||
const existing = form.placements.find(placement => placement.channelId === channel.id);
|
||||
|
||||
if (existing) {
|
||||
removePlacement(existing.id);
|
||||
activePlacementId.value = existing.id;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -298,10 +298,6 @@
|
||||
return form.placements.some(placement => placement.channelId === channelId);
|
||||
}
|
||||
|
||||
function setActivePlacement(placement) {
|
||||
activePlacementId.value = placement.id;
|
||||
}
|
||||
|
||||
function addMedia(placement) {
|
||||
placement.mediaItems.push(blankMedia());
|
||||
}
|
||||
@@ -1104,7 +1100,7 @@
|
||||
</div>
|
||||
|
||||
<div class="preview-editor">
|
||||
<aside class="target-rail">
|
||||
<div class="target-tabs" role="tablist" aria-label="Target channels">
|
||||
<template v-if="groupedChannels.length">
|
||||
<div
|
||||
v-for="group in groupedChannels"
|
||||
@@ -1122,7 +1118,11 @@
|
||||
active: activePlacement?.channelId === channel.id,
|
||||
}"
|
||||
type="button"
|
||||
@click="toggleChannel(channel)"
|
||||
role="tab"
|
||||
:aria-selected="activePlacement?.channelId === channel.id"
|
||||
:aria-label="channel.name"
|
||||
:title="channel.name"
|
||||
@click="selectTargetChannel(channel)"
|
||||
>
|
||||
<v-icon
|
||||
class="target-check"
|
||||
@@ -1132,10 +1132,6 @@
|
||||
class="target-network"
|
||||
:icon="channelIcon(channel.network)"
|
||||
/>
|
||||
<span>
|
||||
<strong>{{ channel.name }}</strong>
|
||||
<small>{{ channel.handle || channel.network }}</small>
|
||||
</span>
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1146,26 +1142,9 @@
|
||||
>
|
||||
Add workspace channels before choosing publication targets.
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<div class="preview-stage">
|
||||
<div
|
||||
v-if="selectedPlacements.length"
|
||||
class="selected-preview-tabs"
|
||||
>
|
||||
<v-btn variant="text" :ripple="false"
|
||||
v-for="placement in selectedPlacements"
|
||||
:key="placement.id"
|
||||
class="selected-preview-tab"
|
||||
:class="{ active: activePlacement?.id === placement.id }"
|
||||
type="button"
|
||||
@click="setActivePlacement(placement)"
|
||||
>
|
||||
<v-icon :icon="channelIcon(placement.network)" />
|
||||
<span>{{ placement.channelName || placement.network }}</span>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<article
|
||||
v-if="activePlacement"
|
||||
class="social-preview-card"
|
||||
@@ -1827,26 +1806,26 @@
|
||||
}
|
||||
|
||||
.preview-editor {
|
||||
@apply grid gap-4 lg:grid-cols-[18rem_minmax(0,1fr)];
|
||||
@apply grid gap-4 lg:grid-cols-[6rem_minmax(0,1fr)];
|
||||
}
|
||||
|
||||
.target-rail {
|
||||
@apply flex min-w-0 flex-col gap-4 rounded-[1rem] border p-3;
|
||||
.target-tabs {
|
||||
@apply flex min-w-0 flex-col gap-4 rounded-[1rem] border p-2;
|
||||
background: var(--app-color-on-primary);
|
||||
border-color: var(--app-border-subtle);
|
||||
}
|
||||
|
||||
.target-group {
|
||||
@apply flex flex-col gap-2;
|
||||
@apply flex flex-col items-center gap-2;
|
||||
}
|
||||
|
||||
.target-group > span {
|
||||
@apply px-2 text-xs font-bold uppercase tracking-[0.16em];
|
||||
@apply text-[0.62rem] font-bold uppercase tracking-[0.12em];
|
||||
color: var(--app-text-muted);
|
||||
}
|
||||
|
||||
.target-channel {
|
||||
@apply grid min-h-11 w-full grid-cols-[1rem_1.75rem_minmax(0,1fr)] items-center gap-2 rounded-[0.75rem] border px-2.5 py-1.5 text-left transition;
|
||||
@apply relative grid h-10 w-10 place-items-center rounded-[0.75rem] border p-0 transition;
|
||||
background: var(--app-control-subtle);
|
||||
border-color: transparent;
|
||||
color: var(--app-color-on-surface);
|
||||
@@ -1864,7 +1843,7 @@
|
||||
}
|
||||
|
||||
.target-check {
|
||||
@apply text-lg;
|
||||
@apply absolute -right-1 -top-1 text-base;
|
||||
color: var(--app-color-on-tertiary);
|
||||
}
|
||||
|
||||
@@ -1884,53 +1863,10 @@
|
||||
color: var(--app-color-on-primary);
|
||||
}
|
||||
|
||||
.target-channel span {
|
||||
@apply flex min-w-0 flex-col gap-0.5;
|
||||
}
|
||||
|
||||
.target-channel strong,
|
||||
.target-channel small {
|
||||
@apply block truncate;
|
||||
}
|
||||
|
||||
.target-channel strong {
|
||||
@apply text-xs font-bold;
|
||||
}
|
||||
|
||||
.target-channel small {
|
||||
@apply text-[0.68rem];
|
||||
color: var(--app-text-muted);
|
||||
}
|
||||
|
||||
.target-channel.active small {
|
||||
color: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
.preview-stage {
|
||||
@apply flex min-w-0 flex-col gap-3;
|
||||
}
|
||||
|
||||
.selected-preview-tabs {
|
||||
@apply flex gap-2 overflow-x-auto pb-1;
|
||||
}
|
||||
|
||||
.selected-preview-tab {
|
||||
@apply inline-flex min-h-8 max-w-36 shrink-0 items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-bold transition;
|
||||
background: var(--app-color-on-primary);
|
||||
border-color: var(--app-border-subtle);
|
||||
color: var(--app-color-on-surface);
|
||||
}
|
||||
|
||||
.selected-preview-tab span {
|
||||
@apply truncate;
|
||||
}
|
||||
|
||||
.selected-preview-tab.active {
|
||||
background: var(--app-color-on-surface);
|
||||
border-color: var(--app-color-on-surface);
|
||||
color: var(--app-color-on-primary);
|
||||
}
|
||||
|
||||
.social-preview-card {
|
||||
@apply flex min-h-[28rem] flex-col gap-4 rounded-[1.25rem] border p-4 shadow-sm;
|
||||
background: var(--app-color-on-primary);
|
||||
|
||||
@@ -97,6 +97,17 @@
|
||||
}
|
||||
|
||||
switch (route.name) {
|
||||
case 'workspace-dashboard':
|
||||
case 'content-items':
|
||||
case 'content-item-detail':
|
||||
return authStore.isManager || authStore.isProvider
|
||||
? [{
|
||||
key: 'create-content',
|
||||
label: t('contentItems.newItem'),
|
||||
icon: mdiPlus,
|
||||
route: { name: 'content-item-create' },
|
||||
}]
|
||||
: [];
|
||||
case 'campaigns':
|
||||
return [{
|
||||
key: 'create-campaign',
|
||||
|
||||
@@ -57,7 +57,6 @@
|
||||
|
||||
const primaryLinks = [
|
||||
{ to: '/app/dashboard', labelKey: 'nav.overview', icon: mdiHomeOutline },
|
||||
{ to: { name: 'content-item-create' }, key: 'create-content', labelKey: 'contentItems.newItem', icon: mdiPlus },
|
||||
{ to: '/app/media-library', labelKey: 'nav.mediaLibrary', icon: mdiImageMultipleOutline },
|
||||
{ to: '/app/developer/release-notes', labelKey: 'nav.releaseNotes', icon: mdiSourceCommit, roles: ['developer'], badge: 'commits' },
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user