refactor: organize frontend by feature
This commit is contained in:
255
frontend/src/features/content/stores/contentItemDetailStore.js
Normal file
255
frontend/src/features/content/stores/contentItemDetailStore.js
Normal file
@@ -0,0 +1,255 @@
|
||||
import { reactive, ref } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import { useWorkspaceStore } from '@/features/workspaces/stores/workspaceStore.js';
|
||||
import { useClient } from '@/plugins/api.js';
|
||||
|
||||
export const useContentItemDetailStore = defineStore('content-item-detail', () => {
|
||||
const workspaceStore = useWorkspaceStore();
|
||||
const client = useClient();
|
||||
|
||||
const item = ref(null);
|
||||
const revisions = ref([]);
|
||||
const assets = ref([]);
|
||||
const comments = ref([]);
|
||||
const approvals = ref([]);
|
||||
const notifications = ref([]);
|
||||
const isLoading = ref(false);
|
||||
const error = ref(null);
|
||||
const actions = reactive({
|
||||
revision: false,
|
||||
asset: false,
|
||||
assetRevision: false,
|
||||
comment: false,
|
||||
approval: false,
|
||||
decision: false,
|
||||
status: false,
|
||||
});
|
||||
|
||||
function reset() {
|
||||
item.value = null;
|
||||
revisions.value = [];
|
||||
assets.value = [];
|
||||
comments.value = [];
|
||||
approvals.value = [];
|
||||
notifications.value = [];
|
||||
error.value = null;
|
||||
}
|
||||
|
||||
async function fetchContentItemDetail(contentItemId) {
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const [
|
||||
itemResponse,
|
||||
revisionsResponse,
|
||||
assetsResponse,
|
||||
commentsResponse,
|
||||
approvalsResponse,
|
||||
notificationsResponse,
|
||||
] = await Promise.all([
|
||||
client.get(`/api/content-items/${contentItemId}`),
|
||||
client.get(`/api/content-items/${contentItemId}/revisions`),
|
||||
client.get('/api/assets', { params: { contentItemId } }),
|
||||
client.get('/api/comments', { params: { contentItemId } }),
|
||||
client.get('/api/approvals', { params: { contentItemId } }),
|
||||
client.get('/api/notifications', {
|
||||
params: {
|
||||
workspaceId: workspaceStore.activeWorkspaceId,
|
||||
contentItemId,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
item.value = itemResponse.data;
|
||||
revisions.value = revisionsResponse.data ?? [];
|
||||
assets.value = assetsResponse.data ?? [];
|
||||
comments.value = commentsResponse.data ?? [];
|
||||
approvals.value = approvalsResponse.data ?? [];
|
||||
notifications.value = notificationsResponse.data ?? [];
|
||||
} catch (fetchError) {
|
||||
console.error('Failed to load content item detail:', fetchError);
|
||||
reset();
|
||||
error.value = 'Failed to load the content item detail.';
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function createRevision(contentItemId, payload) {
|
||||
actions.revision = true;
|
||||
|
||||
try {
|
||||
const response = await client.post(`/api/content-items/${contentItemId}/revisions`, payload);
|
||||
if (response.data) {
|
||||
revisions.value = [response.data, ...revisions.value];
|
||||
await fetchContentItemDetail(contentItemId);
|
||||
}
|
||||
return response.data;
|
||||
} finally {
|
||||
actions.revision = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function addGoogleDriveAsset(contentItemId, payload) {
|
||||
actions.asset = true;
|
||||
|
||||
try {
|
||||
const response = await client.post('/api/assets/google-drive', {
|
||||
...payload,
|
||||
contentItemId,
|
||||
workspaceId: workspaceStore.activeWorkspaceId,
|
||||
});
|
||||
if (response.data) {
|
||||
assets.value = [...assets.value, response.data];
|
||||
await fetchNotifications(contentItemId);
|
||||
}
|
||||
return response.data;
|
||||
} finally {
|
||||
actions.asset = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function addAssetRevision(contentItemId, assetId, payload) {
|
||||
actions.assetRevision = true;
|
||||
|
||||
try {
|
||||
const response = await client.post(`/api/assets/${assetId}/revisions`, payload);
|
||||
if (response.data) {
|
||||
await fetchAssets(contentItemId);
|
||||
await fetchNotifications(contentItemId);
|
||||
}
|
||||
return response.data;
|
||||
} finally {
|
||||
actions.assetRevision = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function addComment(contentItemId, payload) {
|
||||
actions.comment = true;
|
||||
|
||||
try {
|
||||
const response = await client.post('/api/comments', {
|
||||
...payload,
|
||||
contentItemId,
|
||||
workspaceId: workspaceStore.activeWorkspaceId,
|
||||
});
|
||||
if (response.data) {
|
||||
comments.value = [...comments.value, response.data];
|
||||
await fetchNotifications(contentItemId);
|
||||
}
|
||||
return response.data;
|
||||
} finally {
|
||||
actions.comment = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveComment(contentItemId, commentId) {
|
||||
actions.comment = true;
|
||||
|
||||
try {
|
||||
const response = await client.post(`/api/comments/${commentId}/resolve`);
|
||||
if (response.data) {
|
||||
comments.value = comments.value.map(comment => comment.id === commentId ? response.data : comment);
|
||||
await fetchNotifications(contentItemId);
|
||||
}
|
||||
return response.data;
|
||||
} finally {
|
||||
actions.comment = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function createApproval(contentItemId, payload) {
|
||||
actions.approval = true;
|
||||
|
||||
try {
|
||||
const response = await client.post('/api/approvals', {
|
||||
...payload,
|
||||
contentItemId,
|
||||
workspaceId: workspaceStore.activeWorkspaceId,
|
||||
});
|
||||
if (response.data) {
|
||||
approvals.value = [response.data, ...approvals.value];
|
||||
await fetchContentItem(contentItemId);
|
||||
await fetchNotifications(contentItemId);
|
||||
}
|
||||
return response.data;
|
||||
} finally {
|
||||
actions.approval = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function submitDecision(contentItemId, approvalId, payload) {
|
||||
actions.decision = true;
|
||||
|
||||
try {
|
||||
const response = await client.post(`/api/approvals/${approvalId}/decisions`, payload);
|
||||
if (response.data) {
|
||||
approvals.value = approvals.value.map(approval => approval.id === approvalId ? response.data : approval);
|
||||
await fetchContentItem(contentItemId);
|
||||
await fetchNotifications(contentItemId);
|
||||
}
|
||||
return response.data;
|
||||
} finally {
|
||||
actions.decision = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateStatus(contentItemId, status) {
|
||||
actions.status = true;
|
||||
|
||||
try {
|
||||
const response = await client.post(`/api/content-items/${contentItemId}/status`, { status });
|
||||
item.value = response.data;
|
||||
await fetchNotifications(contentItemId);
|
||||
return response.data;
|
||||
} finally {
|
||||
actions.status = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchContentItem(contentItemId) {
|
||||
const response = await client.get(`/api/content-items/${contentItemId}`);
|
||||
item.value = response.data;
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function fetchAssets(contentItemId) {
|
||||
const response = await client.get('/api/assets', { params: { contentItemId } });
|
||||
assets.value = response.data ?? [];
|
||||
return assets.value;
|
||||
}
|
||||
|
||||
async function fetchNotifications(contentItemId) {
|
||||
const response = await client.get('/api/notifications', {
|
||||
params: {
|
||||
workspaceId: workspaceStore.activeWorkspaceId,
|
||||
contentItemId,
|
||||
},
|
||||
});
|
||||
notifications.value = response.data ?? [];
|
||||
return notifications.value;
|
||||
}
|
||||
|
||||
return {
|
||||
item,
|
||||
revisions,
|
||||
assets,
|
||||
comments,
|
||||
approvals,
|
||||
notifications,
|
||||
isLoading,
|
||||
error,
|
||||
actions,
|
||||
reset,
|
||||
fetchContentItemDetail,
|
||||
createRevision,
|
||||
addGoogleDriveAsset,
|
||||
addAssetRevision,
|
||||
addComment,
|
||||
resolveComment,
|
||||
createApproval,
|
||||
submitDecision,
|
||||
updateStatus,
|
||||
};
|
||||
});
|
||||
112
frontend/src/features/content/stores/contentItemsStore.js
Normal file
112
frontend/src/features/content/stores/contentItemsStore.js
Normal file
@@ -0,0 +1,112 @@
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import { useAuthStore } from '@/features/auth/stores/authStore.js';
|
||||
import { useWorkspaceStore } from '@/features/workspaces/stores/workspaceStore.js';
|
||||
import { useClient } from '@/plugins/api.js';
|
||||
|
||||
export const useContentItemsStore = defineStore('content-items', () => {
|
||||
const authStore = useAuthStore();
|
||||
const workspaceStore = useWorkspaceStore();
|
||||
const client = useClient();
|
||||
|
||||
const items = ref([]);
|
||||
const isLoading = ref(false);
|
||||
const isCreating = ref(false);
|
||||
const error = ref(null);
|
||||
|
||||
const activeCount = computed(() =>
|
||||
items.value.filter(item => item.status !== 'Approved' && item.status !== 'Published' && item.status !== 'Archived')
|
||||
.length
|
||||
);
|
||||
|
||||
async function fetchContentItems(filters = {}) {
|
||||
if (!authStore.isAuthenticated || !workspaceStore.activeWorkspaceId) {
|
||||
items.value = [];
|
||||
error.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const response = await client.get('/api/content-items', {
|
||||
params: {
|
||||
workspaceId: workspaceStore.activeWorkspaceId,
|
||||
clientId: filters.clientId,
|
||||
projectId: filters.projectId,
|
||||
},
|
||||
});
|
||||
|
||||
items.value = response.data ?? [];
|
||||
} catch (fetchError) {
|
||||
console.error('Failed to fetch content items:', fetchError);
|
||||
items.value = [];
|
||||
error.value = 'Failed to load content items.';
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function createContentItem(payload) {
|
||||
if (!authStore.isAuthenticated || !workspaceStore.activeWorkspaceId) {
|
||||
throw new Error('You must be authenticated to create a content item.');
|
||||
}
|
||||
|
||||
if (isCreating.value) {
|
||||
throw new Error('A content item creation request is already in progress.');
|
||||
}
|
||||
|
||||
isCreating.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const response = await client.post('/api/content-items', {
|
||||
...payload,
|
||||
workspaceId: workspaceStore.activeWorkspaceId,
|
||||
});
|
||||
|
||||
if (response.data) {
|
||||
items.value = [response.data, ...items.value];
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch (createError) {
|
||||
console.error('Failed to create content item:', createError);
|
||||
error.value = 'Failed to create content item.';
|
||||
throw createError;
|
||||
} finally {
|
||||
isCreating.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchContentItem(id) {
|
||||
const response = await client.get(`/api/content-items/${id}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [authStore.isAuthenticated, workspaceStore.activeWorkspaceId],
|
||||
async ([isAuthenticated, workspaceId]) => {
|
||||
if (!isAuthenticated || !workspaceId) {
|
||||
items.value = [];
|
||||
error.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchContentItems();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
return {
|
||||
items,
|
||||
isLoading,
|
||||
isCreating,
|
||||
error,
|
||||
activeCount,
|
||||
fetchContentItems,
|
||||
fetchContentItem,
|
||||
createContentItem,
|
||||
};
|
||||
});
|
||||
1201
frontend/src/features/content/views/ContentItemDetailView.vue
Normal file
1201
frontend/src/features/content/views/ContentItemDetailView.vue
Normal file
File diff suppressed because it is too large
Load Diff
146
frontend/src/features/content/views/ContentItemsView.vue
Normal file
146
frontend/src/features/content/views/ContentItemsView.vue
Normal file
@@ -0,0 +1,146 @@
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useAuthStore } from '@/features/auth/stores/authStore.js';
|
||||
import { useContentItemsStore } from '@/features/content/stores/contentItemsStore.js';
|
||||
|
||||
const { t } = useI18n();
|
||||
const authStore = useAuthStore();
|
||||
const contentItemsStore = useContentItemsStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page-shell">
|
||||
<div class="header">
|
||||
<div>
|
||||
<div class="eyebrow">{{ t('contentItems.eyebrow') }}</div>
|
||||
<h1>{{ t('contentItems.title') }}</h1>
|
||||
<p>{{ t('contentItems.description') }}</p>
|
||||
</div>
|
||||
|
||||
<router-link
|
||||
v-if="authStore.isManager || authStore.isProvider"
|
||||
:to="{ name: 'content-item-create' }"
|
||||
class="create-button"
|
||||
>
|
||||
{{ t('contentItems.newItem') }}
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="contentItemsStore.isLoading"
|
||||
class="page-message"
|
||||
>
|
||||
{{ t('contentItems.loading') }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="contentItemsStore.error"
|
||||
class="page-message error"
|
||||
>
|
||||
{{ contentItemsStore.error }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="contentItemsStore.items.length"
|
||||
class="item-grid"
|
||||
>
|
||||
<router-link
|
||||
v-for="item in contentItemsStore.items"
|
||||
:key="item.id"
|
||||
:to="{ name: 'content-item-detail', params: { id: item.id } }"
|
||||
class="item-card"
|
||||
>
|
||||
<div class="version-chip">{{ item.currentRevisionLabel }}</div>
|
||||
<strong>{{ item.title }}</strong>
|
||||
<span>{{ item.publicationTargets }}</span>
|
||||
<div class="status-row">
|
||||
<em>{{ item.status }}</em>
|
||||
<small>{{ item.dueDate ? new Date(item.dueDate).toLocaleDateString() : t('contentItems.noDueDate') }}</small>
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="page-message"
|
||||
>
|
||||
{{ t('contentItems.empty') }}
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-shell {
|
||||
@apply mx-auto flex w-full max-w-7xl flex-col gap-6 px-5 py-8 md:px-8;
|
||||
}
|
||||
|
||||
.header {
|
||||
@apply flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
@apply text-xs font-bold uppercase tracking-[0.24em];
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
@apply mt-2 text-4xl font-black;
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
.header p,
|
||||
.item-card span,
|
||||
.status-row em,
|
||||
.status-row small {
|
||||
@apply text-sm leading-6 not-italic;
|
||||
color: #526178;
|
||||
}
|
||||
|
||||
.create-button {
|
||||
@apply inline-flex items-center justify-center rounded-full px-5 py-3 text-sm font-bold no-underline transition;
|
||||
background: #172033;
|
||||
color: #fffaf2;
|
||||
}
|
||||
|
||||
.page-message,
|
||||
.item-card {
|
||||
@apply rounded-[1.5rem] border;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-color: rgba(23, 32, 51, 0.08);
|
||||
}
|
||||
|
||||
.page-message {
|
||||
@apply p-5 text-sm;
|
||||
color: #526178;
|
||||
}
|
||||
|
||||
.page-message.error {
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.item-grid {
|
||||
@apply grid gap-4 md:grid-cols-2 xl:grid-cols-3;
|
||||
}
|
||||
|
||||
.item-card {
|
||||
@apply flex flex-col gap-4 p-5 no-underline transition;
|
||||
}
|
||||
|
||||
.item-card:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.item-card strong {
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
.version-chip {
|
||||
@apply w-fit rounded-full px-4 py-2 text-xs font-bold uppercase tracking-[0.16em];
|
||||
background: rgba(23, 32, 51, 0.08);
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
.status-row {
|
||||
@apply flex items-center justify-between gap-3;
|
||||
}
|
||||
</style>
|
||||
222
frontend/src/features/content/views/MediaLibraryView.vue
Normal file
222
frontend/src/features/content/views/MediaLibraryView.vue
Normal file
@@ -0,0 +1,222 @@
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import {
|
||||
mdiCheckCircleOutline,
|
||||
mdiCloudSyncOutline,
|
||||
mdiFolderGoogleDrive,
|
||||
mdiImageMultipleOutline,
|
||||
mdiVideoOutline,
|
||||
} from '@mdi/js';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const mediaTypes = [
|
||||
{ label: t('mediaLibrary.mediaTypes.images'), icon: mdiImageMultipleOutline },
|
||||
{ label: t('mediaLibrary.mediaTypes.videos'), icon: mdiVideoOutline },
|
||||
];
|
||||
|
||||
const workflowSteps = [
|
||||
t('mediaLibrary.workflow.connectDrive'),
|
||||
t('mediaLibrary.workflow.syncAssets'),
|
||||
t('mediaLibrary.workflow.organizeLibrary'),
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page-shell">
|
||||
<div class="hero">
|
||||
<div class="hero-copy">
|
||||
<div class="eyebrow">{{ t('mediaLibrary.eyebrow') }}</div>
|
||||
<h1>{{ t('mediaLibrary.title') }}</h1>
|
||||
<p>{{ t('mediaLibrary.description') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="hero-card">
|
||||
<div class="hero-card-icon">
|
||||
<v-icon :icon="mdiFolderGoogleDrive" />
|
||||
</div>
|
||||
<strong>{{ t('mediaLibrary.syncCard.title') }}</strong>
|
||||
<span>{{ t('mediaLibrary.syncCard.description') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-grid">
|
||||
<article class="panel">
|
||||
<div class="panel-header">
|
||||
<strong>{{ t('mediaLibrary.mediaTypesTitle') }}</strong>
|
||||
<span>{{ t('mediaLibrary.mediaTypesDescription') }}</span>
|
||||
</div>
|
||||
|
||||
<div class="media-type-list">
|
||||
<div
|
||||
v-for="type in mediaTypes"
|
||||
:key="type.label"
|
||||
class="media-type-item"
|
||||
>
|
||||
<v-icon :icon="type.icon" />
|
||||
<span>{{ type.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel">
|
||||
<div class="panel-header">
|
||||
<strong>{{ t('mediaLibrary.workflowTitle') }}</strong>
|
||||
<span>{{ t('mediaLibrary.workflowDescription') }}</span>
|
||||
</div>
|
||||
|
||||
<div class="workflow-list">
|
||||
<div
|
||||
v-for="step in workflowSteps"
|
||||
:key="step"
|
||||
class="workflow-item"
|
||||
>
|
||||
<v-icon
|
||||
:icon="mdiCheckCircleOutline"
|
||||
class="workflow-icon"
|
||||
/>
|
||||
<span>{{ step }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<article class="status-panel">
|
||||
<div class="status-copy">
|
||||
<div class="status-label">
|
||||
<v-icon :icon="mdiCloudSyncOutline" />
|
||||
<span>{{ t('mediaLibrary.statusLabel') }}</span>
|
||||
</div>
|
||||
<strong>{{ t('mediaLibrary.pendingTitle') }}</strong>
|
||||
<p>{{ t('mediaLibrary.pendingDescription') }}</p>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-shell {
|
||||
@apply mx-auto flex w-full max-w-7xl flex-col gap-6 px-5 py-8 md:px-8;
|
||||
}
|
||||
|
||||
.hero {
|
||||
@apply grid gap-4 lg:grid-cols-[minmax(0,1.45fr)_minmax(18rem,0.8fr)];
|
||||
}
|
||||
|
||||
.hero-copy,
|
||||
.hero-card,
|
||||
.panel,
|
||||
.status-panel {
|
||||
@apply rounded-[1.75rem] border;
|
||||
border-color: rgba(23, 32, 51, 0.08);
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.hero-copy {
|
||||
@apply p-6 md:p-8;
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(14, 165, 164, 0.18), transparent 45%),
|
||||
linear-gradient(135deg, rgba(255, 255, 255, 0.98), rgba(240, 249, 255, 0.92));
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
@apply text-xs font-bold uppercase tracking-[0.24em];
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.hero-copy h1 {
|
||||
@apply mt-3 text-4xl font-black;
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
.hero-copy p,
|
||||
.hero-card span,
|
||||
.panel-header span,
|
||||
.media-type-item span,
|
||||
.workflow-item span,
|
||||
.status-copy p,
|
||||
.status-label span {
|
||||
@apply text-sm leading-6;
|
||||
color: #526178;
|
||||
}
|
||||
|
||||
.hero-card {
|
||||
@apply flex flex-col justify-between gap-5 p-6;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 250, 242, 0.96), rgba(255, 255, 255, 0.96));
|
||||
}
|
||||
|
||||
.hero-card-icon,
|
||||
.media-type-item,
|
||||
.workflow-item,
|
||||
.status-label {
|
||||
@apply inline-flex items-center gap-3;
|
||||
}
|
||||
|
||||
.hero-card-icon,
|
||||
.media-type-item {
|
||||
@apply w-fit rounded-full px-3 py-2;
|
||||
background: rgba(15, 118, 110, 0.08);
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.hero-card strong,
|
||||
.panel-header strong,
|
||||
.status-copy strong {
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
.hero-card strong {
|
||||
@apply text-2xl font-black;
|
||||
}
|
||||
|
||||
.content-grid {
|
||||
@apply grid gap-6 lg:grid-cols-2;
|
||||
}
|
||||
|
||||
.panel,
|
||||
.status-panel {
|
||||
@apply flex flex-col gap-5 p-6;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
@apply flex flex-col gap-2;
|
||||
}
|
||||
|
||||
.panel-header strong {
|
||||
@apply text-xl font-black;
|
||||
}
|
||||
|
||||
.media-type-list,
|
||||
.workflow-list {
|
||||
@apply flex flex-col gap-3;
|
||||
}
|
||||
|
||||
.media-type-item,
|
||||
.workflow-item {
|
||||
@apply rounded-[1.1rem] border px-4 py-3;
|
||||
border-color: rgba(23, 32, 51, 0.08);
|
||||
background: rgba(248, 250, 252, 0.9);
|
||||
}
|
||||
|
||||
.workflow-icon {
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.status-panel {
|
||||
background: linear-gradient(135deg, rgba(255, 247, 237, 0.95), rgba(255, 255, 255, 0.98));
|
||||
}
|
||||
|
||||
.status-copy {
|
||||
@apply flex flex-col gap-3;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
@apply text-xs font-bold uppercase tracking-[0.2em];
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.status-copy strong {
|
||||
@apply text-2xl font-black;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user