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`.
|
- Keep content item editing in `frontend/src/features/content/views/ContentItemDetailView.vue`.
|
||||||
- Replace per-channel caption editing with a shared caption and shared hashtags.
|
- Replace per-channel caption editing with a shared caption and shared hashtags.
|
||||||
- Let configured workspace channels be selected or unselected as targets.
|
- 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.
|
- Convert shared hashtags into chip-style entry with suggestions from already used hashtags.
|
||||||
- Show a workspace hashtag feed so authors can reuse existing tags.
|
- 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.
|
- 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.
|
- 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.
|
- Preserve existing save payloads and backend contracts.
|
||||||
|
|
||||||
## Validation
|
## Validation
|
||||||
@@ -40,5 +40,5 @@ npm run build
|
|||||||
- [x] Already used hashtags are available as suggestions.
|
- [x] Already used hashtags are available as suggestions.
|
||||||
- [x] The editor shows a workspace hashtag feed with usage counts.
|
- [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] The editor no longer shows title, calendar, change summary, or base-caption fields.
|
||||||
- [x] Target channel preview tabs use compact buttons.
|
- [x] Target channel tabs use compact icon-only buttons with channel names as accessible labels and tooltips.
|
||||||
- [x] Create content is available from the main app menu.
|
- [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);
|
const existing = form.placements.find(placement => placement.channelId === channel.id);
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
removePlacement(existing.id);
|
activePlacementId.value = existing.id;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,10 +298,6 @@
|
|||||||
return form.placements.some(placement => placement.channelId === channelId);
|
return form.placements.some(placement => placement.channelId === channelId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setActivePlacement(placement) {
|
|
||||||
activePlacementId.value = placement.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
function addMedia(placement) {
|
function addMedia(placement) {
|
||||||
placement.mediaItems.push(blankMedia());
|
placement.mediaItems.push(blankMedia());
|
||||||
}
|
}
|
||||||
@@ -1104,7 +1100,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="preview-editor">
|
<div class="preview-editor">
|
||||||
<aside class="target-rail">
|
<div class="target-tabs" role="tablist" aria-label="Target channels">
|
||||||
<template v-if="groupedChannels.length">
|
<template v-if="groupedChannels.length">
|
||||||
<div
|
<div
|
||||||
v-for="group in groupedChannels"
|
v-for="group in groupedChannels"
|
||||||
@@ -1122,7 +1118,11 @@
|
|||||||
active: activePlacement?.channelId === channel.id,
|
active: activePlacement?.channelId === channel.id,
|
||||||
}"
|
}"
|
||||||
type="button"
|
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
|
<v-icon
|
||||||
class="target-check"
|
class="target-check"
|
||||||
@@ -1132,10 +1132,6 @@
|
|||||||
class="target-network"
|
class="target-network"
|
||||||
:icon="channelIcon(channel.network)"
|
:icon="channelIcon(channel.network)"
|
||||||
/>
|
/>
|
||||||
<span>
|
|
||||||
<strong>{{ channel.name }}</strong>
|
|
||||||
<small>{{ channel.handle || channel.network }}</small>
|
|
||||||
</span>
|
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -1146,26 +1142,9 @@
|
|||||||
>
|
>
|
||||||
Add workspace channels before choosing publication targets.
|
Add workspace channels before choosing publication targets.
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</div>
|
||||||
|
|
||||||
<div class="preview-stage">
|
<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
|
<article
|
||||||
v-if="activePlacement"
|
v-if="activePlacement"
|
||||||
class="social-preview-card"
|
class="social-preview-card"
|
||||||
@@ -1827,26 +1806,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.preview-editor {
|
.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 {
|
.target-tabs {
|
||||||
@apply flex min-w-0 flex-col gap-4 rounded-[1rem] border p-3;
|
@apply flex min-w-0 flex-col gap-4 rounded-[1rem] border p-2;
|
||||||
background: var(--app-color-on-primary);
|
background: var(--app-color-on-primary);
|
||||||
border-color: var(--app-border-subtle);
|
border-color: var(--app-border-subtle);
|
||||||
}
|
}
|
||||||
|
|
||||||
.target-group {
|
.target-group {
|
||||||
@apply flex flex-col gap-2;
|
@apply flex flex-col items-center gap-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.target-group > span {
|
.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);
|
color: var(--app-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.target-channel {
|
.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);
|
background: var(--app-control-subtle);
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
color: var(--app-color-on-surface);
|
color: var(--app-color-on-surface);
|
||||||
@@ -1864,7 +1843,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.target-check {
|
.target-check {
|
||||||
@apply text-lg;
|
@apply absolute -right-1 -top-1 text-base;
|
||||||
color: var(--app-color-on-tertiary);
|
color: var(--app-color-on-tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1884,53 +1863,10 @@
|
|||||||
color: var(--app-color-on-primary);
|
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 {
|
.preview-stage {
|
||||||
@apply flex min-w-0 flex-col gap-3;
|
@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 {
|
.social-preview-card {
|
||||||
@apply flex min-h-[28rem] flex-col gap-4 rounded-[1.25rem] border p-4 shadow-sm;
|
@apply flex min-h-[28rem] flex-col gap-4 rounded-[1.25rem] border p-4 shadow-sm;
|
||||||
background: var(--app-color-on-primary);
|
background: var(--app-color-on-primary);
|
||||||
|
|||||||
@@ -97,6 +97,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (route.name) {
|
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':
|
case 'campaigns':
|
||||||
return [{
|
return [{
|
||||||
key: 'create-campaign',
|
key: 'create-campaign',
|
||||||
|
|||||||
@@ -57,7 +57,6 @@
|
|||||||
|
|
||||||
const primaryLinks = [
|
const primaryLinks = [
|
||||||
{ to: '/app/dashboard', labelKey: 'nav.overview', icon: mdiHomeOutline },
|
{ 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/media-library', labelKey: 'nav.mediaLibrary', icon: mdiImageMultipleOutline },
|
||||||
{ to: '/app/developer/release-notes', labelKey: 'nav.releaseNotes', icon: mdiSourceCommit, roles: ['developer'], badge: 'commits' },
|
{ to: '/app/developer/release-notes', labelKey: 'nav.releaseNotes', icon: mdiSourceCommit, roles: ['developer'], badge: 'commits' },
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user