839 lines
21 KiB
Vue
839 lines
21 KiB
Vue
<script setup lang="ts">
|
||
import type { TagProps } from 'element-plus';
|
||
|
||
import { computed } from 'vue';
|
||
|
||
import { useAccess } from '@easyflow/access';
|
||
import { IconifyIcon } from '@easyflow/icons';
|
||
|
||
import { Loading, MoreFilled } from '@element-plus/icons-vue';
|
||
import {
|
||
ElAvatar,
|
||
ElButton,
|
||
ElCard,
|
||
ElDropdown,
|
||
ElDropdownItem,
|
||
ElDropdownMenu,
|
||
ElIcon,
|
||
ElTag,
|
||
ElText,
|
||
ElTooltip,
|
||
} from 'element-plus';
|
||
|
||
import { $t } from '#/locales';
|
||
|
||
export type ActionPlacement = 'inline' | 'menu';
|
||
export type ActionTone = 'danger' | 'default';
|
||
export type CardDensity = 'compact' | 'default';
|
||
export type MetaLayout = 'compact' | 'stacked';
|
||
export type TagLayout = 'inline' | 'stacked';
|
||
export type TagPlacement = 'details' | 'title';
|
||
|
||
export interface ActionButton {
|
||
icon?: any;
|
||
text: ((row: any) => string) | string;
|
||
className?: string;
|
||
disabled?: ((row: any) => boolean) | boolean;
|
||
loading?: ((row: any) => boolean) | boolean;
|
||
permission?: string;
|
||
placement?: ActionPlacement;
|
||
tone?: ActionTone;
|
||
visible?: ((row: any) => boolean) | boolean;
|
||
onClick: (row: any) => void;
|
||
}
|
||
|
||
export interface CardPrimaryAction {
|
||
icon?: any;
|
||
iconOnly?: boolean;
|
||
showIndicator?: boolean;
|
||
text: string;
|
||
permission?: string;
|
||
onClick: (row: any) => void;
|
||
}
|
||
|
||
interface ResolvedActionButton extends ActionButton {
|
||
placement: ActionPlacement;
|
||
tone: ActionTone;
|
||
}
|
||
|
||
export interface CardListProps {
|
||
iconField?: string;
|
||
titleField?: string;
|
||
descField?: string;
|
||
actions?: ActionButton[];
|
||
cornerTagField?: string;
|
||
cornerTagMap?: Record<string, string>;
|
||
cornerTagTypeMap?: Record<string, TagProps['type']>;
|
||
density?: CardDensity;
|
||
defaultIcon: any;
|
||
data: any[];
|
||
metaLayout?: MetaLayout;
|
||
primaryAction?: CardPrimaryAction;
|
||
tagField?: string;
|
||
tagLayout?: TagLayout;
|
||
tagMap?: Record<string, string>;
|
||
tagPlacement?: TagPlacement;
|
||
titleLineClamp?: number;
|
||
}
|
||
|
||
const props = withDefaults(defineProps<CardListProps>(), {
|
||
iconField: 'icon',
|
||
titleField: 'title',
|
||
descField: 'description',
|
||
actions: () => [],
|
||
cornerTagField: '',
|
||
cornerTagMap: () => ({}),
|
||
cornerTagTypeMap: () => ({}),
|
||
density: 'default',
|
||
metaLayout: 'stacked',
|
||
primaryAction: undefined,
|
||
tagField: '',
|
||
tagLayout: 'inline',
|
||
tagMap: () => ({}),
|
||
tagPlacement: 'title',
|
||
titleLineClamp: 1,
|
||
});
|
||
|
||
const { hasAccessByCodes } = useAccess();
|
||
|
||
function hasPermission(permission?: string) {
|
||
return !permission || hasAccessByCodes([permission]);
|
||
}
|
||
|
||
function isActionVisible(action: ActionButton, row: any) {
|
||
if (typeof action.visible === 'function') {
|
||
return action.visible(row);
|
||
}
|
||
return action.visible !== false;
|
||
}
|
||
|
||
function resolveActionState(
|
||
state: ((row: any) => boolean) | boolean | undefined,
|
||
row: any,
|
||
) {
|
||
return typeof state === 'function' ? state(row) : state === true;
|
||
}
|
||
|
||
const resolvedPrimaryAction = computed(() => {
|
||
if (!props.primaryAction || !hasPermission(props.primaryAction.permission)) {
|
||
return undefined;
|
||
}
|
||
return props.primaryAction;
|
||
});
|
||
|
||
const showPrimaryActionIndicator = computed(
|
||
() => resolvedPrimaryAction.value?.showIndicator !== false,
|
||
);
|
||
|
||
function resolveActionPlacement(
|
||
action: ActionButton,
|
||
index: number,
|
||
): ActionPlacement {
|
||
if (action.placement) {
|
||
return action.placement;
|
||
}
|
||
if (resolvedPrimaryAction.value) {
|
||
return 'menu';
|
||
}
|
||
return index < 3 ? 'inline' : 'menu';
|
||
}
|
||
|
||
const resolvedActions = computed<ResolvedActionButton[]>(() => {
|
||
return props.actions
|
||
.filter((action) => hasPermission(action.permission))
|
||
.map((action, index) => ({
|
||
...action,
|
||
placement: resolveActionPlacement(action, index),
|
||
tone:
|
||
action.tone ||
|
||
(action.className?.includes('danger') ? 'danger' : 'default'),
|
||
}));
|
||
});
|
||
|
||
function handlePrimaryAction(item: any) {
|
||
resolvedPrimaryAction.value?.onClick(item);
|
||
}
|
||
|
||
function handleActionClick(event: Event, action: ActionButton, item: any) {
|
||
event.stopPropagation();
|
||
if (
|
||
resolveActionState(action.disabled, item) ||
|
||
resolveActionState(action.loading, item)
|
||
) {
|
||
return;
|
||
}
|
||
action.onClick(item);
|
||
}
|
||
|
||
function resolveActionText(action: ActionButton, item: any) {
|
||
return typeof action.text === 'function' ? action.text(item) : action.text;
|
||
}
|
||
|
||
function resolveInlineActions(item: any) {
|
||
return resolvedActions.value.filter(
|
||
(action) => action.placement === 'inline' && isActionVisible(action, item),
|
||
);
|
||
}
|
||
|
||
function resolveMenuActions(item: any) {
|
||
return resolvedActions.value.filter(
|
||
(action) => action.placement === 'menu' && isActionVisible(action, item),
|
||
);
|
||
}
|
||
|
||
function hasVisibleActions(item: any) {
|
||
return (
|
||
resolveInlineActions(item).length > 0 || resolveMenuActions(item).length > 0
|
||
);
|
||
}
|
||
|
||
function resolveMetaItems(item: any) {
|
||
const metaItems: Array<{ label: string; value: string }> = [];
|
||
if (item.createdByName) {
|
||
metaItems.push({
|
||
label: $t('aiResource.createdBy'),
|
||
value: String(item.createdByName),
|
||
});
|
||
}
|
||
if (item.created) {
|
||
metaItems.push({
|
||
label: $t('aiResource.created'),
|
||
value: String(item.created),
|
||
});
|
||
}
|
||
return metaItems;
|
||
}
|
||
|
||
function resolveMetaAriaLabel(item: any) {
|
||
return resolveMetaItems(item)
|
||
.map((meta) => `${meta.label}:${meta.value}`)
|
||
.join(',');
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="card-grid">
|
||
<ElCard
|
||
v-for="(item, index) in props.data"
|
||
:key="item.id ?? index"
|
||
shadow="never"
|
||
class="card-item"
|
||
:class="[
|
||
`card-item--${density}`,
|
||
{ 'card-item--interactive': resolvedPrimaryAction },
|
||
]"
|
||
:role="resolvedPrimaryAction ? 'button' : undefined"
|
||
:tabindex="resolvedPrimaryAction ? 0 : undefined"
|
||
@click="handlePrimaryAction(item)"
|
||
@keydown.enter.prevent="handlePrimaryAction(item)"
|
||
@keydown.space.prevent="handlePrimaryAction(item)"
|
||
>
|
||
<div class="card-content">
|
||
<div class="card-header">
|
||
<ElAvatar
|
||
class="card-avatar shrink-0"
|
||
:src="item[iconField] || defaultIcon"
|
||
:size="44"
|
||
/>
|
||
<div class="card-meta">
|
||
<div
|
||
class="title-row"
|
||
:class="{ 'title-row--stacked': tagLayout === 'stacked' }"
|
||
>
|
||
<ElText
|
||
:truncated="titleLineClamp === 1"
|
||
:line-clamp="titleLineClamp > 1 ? titleLineClamp : undefined"
|
||
size="large"
|
||
class="card-title"
|
||
:class="{ 'card-title--wrap': titleLineClamp === 0 }"
|
||
>
|
||
{{ item[titleField] }}
|
||
</ElText>
|
||
<ElTag
|
||
v-if="tagPlacement === 'title' && tagField && item[tagField]"
|
||
size="small"
|
||
effect="plain"
|
||
type="info"
|
||
>
|
||
{{ tagMap[item[tagField]] || item[tagField] }}
|
||
</ElTag>
|
||
</div>
|
||
<ElText line-clamp="2" class="item-desc w-full">
|
||
{{ item[descField] }}
|
||
</ElText>
|
||
<div
|
||
v-if="
|
||
(tagPlacement === 'details' && tagField && item[tagField]) ||
|
||
$slots.details
|
||
"
|
||
class="card-detail-row"
|
||
>
|
||
<ElTag
|
||
v-if="tagPlacement === 'details' && tagField && item[tagField]"
|
||
size="small"
|
||
effect="plain"
|
||
type="info"
|
||
round
|
||
>
|
||
{{ tagMap[item[tagField]] || item[tagField] }}
|
||
</ElTag>
|
||
<slot name="details" :item="item"></slot>
|
||
</div>
|
||
</div>
|
||
<div
|
||
v-if="$slots.corner || (cornerTagField && item[cornerTagField])"
|
||
class="card-corner-tag"
|
||
>
|
||
<slot name="corner" :item="item">
|
||
<ElTag
|
||
size="small"
|
||
effect="plain"
|
||
:type="cornerTagTypeMap[item[cornerTagField]] || 'info'"
|
||
round
|
||
>
|
||
{{ cornerTagMap[item[cornerTagField]] || item[cornerTagField] }}
|
||
</ElTag>
|
||
</slot>
|
||
</div>
|
||
<div
|
||
v-if="$slots.meta || resolveMetaItems(item).length > 0"
|
||
class="card-meta-row"
|
||
>
|
||
<slot name="meta" :item="item">
|
||
<div
|
||
v-if="metaLayout === 'compact'"
|
||
class="card-meta-compact"
|
||
:aria-label="resolveMetaAriaLabel(item)"
|
||
>
|
||
<template
|
||
v-for="(meta, metaIndex) in resolveMetaItems(item)"
|
||
:key="meta.label"
|
||
>
|
||
<span
|
||
v-if="metaIndex > 0"
|
||
class="card-meta-separator"
|
||
aria-hidden="true"
|
||
>
|
||
·
|
||
</span>
|
||
<span class="card-meta-value">{{ meta.value }}</span>
|
||
</template>
|
||
</div>
|
||
<template v-else>
|
||
<span
|
||
v-for="meta in resolveMetaItems(item)"
|
||
:key="meta.label"
|
||
class="card-meta-item"
|
||
>
|
||
<span class="card-meta-label">{{ meta.label }}:</span>
|
||
<span class="card-meta-value">{{ meta.value }}</span>
|
||
</span>
|
||
</template>
|
||
</slot>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<template
|
||
v-if="
|
||
(resolvedPrimaryAction && showPrimaryActionIndicator) ||
|
||
hasVisibleActions(item)
|
||
"
|
||
#footer
|
||
>
|
||
<div class="card-footer">
|
||
<div
|
||
v-if="
|
||
(resolvedPrimaryAction && showPrimaryActionIndicator) ||
|
||
resolveInlineActions(item).length > 0
|
||
"
|
||
class="card-main-actions"
|
||
>
|
||
<div
|
||
v-if="resolvedPrimaryAction && showPrimaryActionIndicator"
|
||
class="card-primary-hint"
|
||
>
|
||
<ElTooltip
|
||
v-if="resolvedPrimaryAction.iconOnly"
|
||
:content="resolvedPrimaryAction.text"
|
||
placement="top"
|
||
:show-after="300"
|
||
>
|
||
<ElButton
|
||
class="card-primary-button"
|
||
:aria-label="resolvedPrimaryAction.text"
|
||
circle
|
||
@click.stop="handlePrimaryAction(item)"
|
||
>
|
||
<ElIcon v-if="resolvedPrimaryAction.icon">
|
||
<IconifyIcon
|
||
v-if="typeof resolvedPrimaryAction.icon === 'string'"
|
||
:icon="resolvedPrimaryAction.icon"
|
||
/>
|
||
<component v-else :is="resolvedPrimaryAction.icon" />
|
||
</ElIcon>
|
||
</ElButton>
|
||
</ElTooltip>
|
||
<div v-else class="primary-label">
|
||
<ElIcon v-if="resolvedPrimaryAction.icon" class="primary-icon">
|
||
<IconifyIcon
|
||
v-if="typeof resolvedPrimaryAction.icon === 'string'"
|
||
:icon="resolvedPrimaryAction.icon"
|
||
/>
|
||
<component v-else :is="resolvedPrimaryAction.icon" />
|
||
</ElIcon>
|
||
<span class="primary-text">
|
||
{{ resolvedPrimaryAction.text }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
v-if="resolveInlineActions(item).length > 0"
|
||
class="card-actions"
|
||
@click.stop
|
||
>
|
||
<ElButton
|
||
v-for="(action, actionIndex) in resolveInlineActions(item)"
|
||
:key="`${item.id ?? index}-inline-${actionIndex}`"
|
||
:icon="
|
||
typeof action.icon === 'string' ? undefined : action.icon
|
||
"
|
||
size="small"
|
||
class="card-action-btn"
|
||
:class="{
|
||
'card-action-btn--danger': action.tone === 'danger',
|
||
}"
|
||
:disabled="resolveActionState(action.disabled, item)"
|
||
:loading="resolveActionState(action.loading, item)"
|
||
link
|
||
@click.stop="handleActionClick($event, action, item)"
|
||
>
|
||
<template v-if="typeof action.icon === 'string'" #icon>
|
||
<IconifyIcon :icon="action.icon" />
|
||
</template>
|
||
{{ resolveActionText(action, item) }}
|
||
</ElButton>
|
||
</div>
|
||
</div>
|
||
|
||
<ElDropdown
|
||
v-if="resolveMenuActions(item).length > 0"
|
||
class="card-overflow-action"
|
||
trigger="click"
|
||
placement="bottom-end"
|
||
>
|
||
<ElButton
|
||
class="card-action-btn card-action-btn--menu"
|
||
:icon="MoreFilled"
|
||
:aria-label="$t('button.more')"
|
||
link
|
||
@click.stop
|
||
/>
|
||
<template #dropdown>
|
||
<ElDropdownMenu>
|
||
<ElDropdownItem
|
||
v-for="(action, actionIndex) in resolveMenuActions(item)"
|
||
:key="`${item.id ?? index}-menu-${actionIndex}`"
|
||
:class="{
|
||
'card-menu-item--danger': action.tone === 'danger',
|
||
}"
|
||
:disabled="
|
||
resolveActionState(action.disabled, item) ||
|
||
resolveActionState(action.loading, item)
|
||
"
|
||
@click="handleActionClick($event, action, item)"
|
||
>
|
||
<div class="menu-action-content">
|
||
<ElIcon
|
||
v-if="resolveActionState(action.loading, item)"
|
||
class="is-loading"
|
||
>
|
||
<Loading />
|
||
</ElIcon>
|
||
<ElIcon v-else-if="action.icon">
|
||
<IconifyIcon
|
||
v-if="typeof action.icon === 'string'"
|
||
:icon="action.icon"
|
||
/>
|
||
<component v-else :is="action.icon" />
|
||
</ElIcon>
|
||
<span>{{ resolveActionText(action, item) }}</span>
|
||
</div>
|
||
</ElDropdownItem>
|
||
</ElDropdownMenu>
|
||
</template>
|
||
</ElDropdown>
|
||
</div>
|
||
</template>
|
||
</ElCard>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
@media (max-width: 1024px) {
|
||
.card-grid {
|
||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.card-grid {
|
||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||
min-width: 100%;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
.card-grid {
|
||
grid-template-columns: minmax(0, 1fr);
|
||
}
|
||
}
|
||
|
||
.card-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||
gap: 20px;
|
||
min-width: max(100%, 600px);
|
||
}
|
||
|
||
.card-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: hsl(var(--card));
|
||
border: 1px solid hsl(var(--border));
|
||
border-radius: var(--radius-panel);
|
||
transition:
|
||
transform 0.18s ease,
|
||
box-shadow 0.18s ease,
|
||
border-color 0.18s ease,
|
||
background-color 0.18s ease;
|
||
}
|
||
|
||
.card-item--interactive {
|
||
cursor: pointer;
|
||
}
|
||
|
||
.card-item--interactive:hover {
|
||
border-color: hsl(var(--primary) / 20%);
|
||
box-shadow: var(--shadow-subtle);
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.card-item--interactive:focus-visible {
|
||
outline: none;
|
||
border-color: hsl(var(--primary) / 32%);
|
||
box-shadow:
|
||
0 0 0 4px hsl(var(--primary) / 12%),
|
||
var(--shadow-subtle);
|
||
}
|
||
|
||
.card-item--compact :deep(.el-card__body) {
|
||
padding: var(--space-4) var(--space-5);
|
||
}
|
||
|
||
.card-item--compact :deep(.el-card__footer) {
|
||
padding: 0 var(--space-5) var(--space-4);
|
||
}
|
||
|
||
:deep(.el-card__body) {
|
||
flex: 1;
|
||
padding: 18px;
|
||
}
|
||
|
||
:deep(.el-card__footer) {
|
||
padding: 0 18px 18px;
|
||
border-top: none;
|
||
}
|
||
|
||
.card-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-height: 116px;
|
||
}
|
||
|
||
.card-item--compact .card-content {
|
||
min-height: 0;
|
||
}
|
||
|
||
.card-header {
|
||
display: grid;
|
||
grid-template-columns: 44px minmax(0, 1fr) auto;
|
||
column-gap: 14px;
|
||
row-gap: 10px;
|
||
align-items: flex-start;
|
||
width: 100%;
|
||
}
|
||
|
||
.card-avatar {
|
||
grid-column: 1;
|
||
grid-row: 1;
|
||
background: hsl(var(--surface-subtle));
|
||
border: 1px solid hsl(var(--line-subtle));
|
||
}
|
||
|
||
.card-meta {
|
||
display: flex;
|
||
flex: 1;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
grid-column: 2;
|
||
grid-row: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.card-item--compact .card-meta {
|
||
gap: var(--space-2);
|
||
}
|
||
|
||
.title-row {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
align-items: center;
|
||
min-width: 0;
|
||
}
|
||
|
||
.title-row--stacked {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.title-row--stacked .card-title {
|
||
align-self: flex-start;
|
||
max-width: 100%;
|
||
}
|
||
|
||
.card-title {
|
||
font-weight: 600;
|
||
color: hsl(var(--text-strong));
|
||
}
|
||
|
||
.card-title--wrap {
|
||
overflow: visible;
|
||
overflow-wrap: anywhere;
|
||
text-overflow: clip;
|
||
white-space: normal;
|
||
}
|
||
|
||
.item-desc {
|
||
min-height: 44px;
|
||
font-size: 13px;
|
||
line-height: 22px;
|
||
color: hsl(var(--text-muted));
|
||
}
|
||
|
||
.card-item--compact .item-desc {
|
||
min-height: 20px;
|
||
line-height: 20px;
|
||
}
|
||
|
||
.card-detail-row {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
align-items: center;
|
||
min-height: 22px;
|
||
}
|
||
|
||
.card-detail-row :deep(.el-tag) {
|
||
--el-tag-bg-color: hsl(var(--surface-subtle));
|
||
--el-tag-border-color: hsl(var(--line-subtle));
|
||
--el-tag-text-color: hsl(var(--text-muted));
|
||
--el-tag-border-radius: var(--radius-pill);
|
||
--el-tag-font-size: 11px;
|
||
|
||
height: 22px;
|
||
padding: 0 8px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.card-meta-row {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
align-items: flex-start;
|
||
grid-column: 1 / -1;
|
||
width: 100%;
|
||
font-size: 12px;
|
||
line-height: 18px;
|
||
color: hsl(var(--text-muted));
|
||
}
|
||
|
||
.card-meta-item {
|
||
display: flex;
|
||
gap: 4px;
|
||
align-items: flex-start;
|
||
justify-content: flex-start;
|
||
width: 100%;
|
||
min-width: 0;
|
||
}
|
||
|
||
.card-meta-compact {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
align-items: center;
|
||
min-width: 0;
|
||
}
|
||
|
||
.card-meta-separator {
|
||
color: hsl(var(--divider-faint));
|
||
}
|
||
|
||
.card-meta-label {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.card-meta-value {
|
||
min-width: 0;
|
||
text-align: left;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.card-corner-tag {
|
||
display: flex;
|
||
grid-column: 3;
|
||
grid-row: 1;
|
||
flex-shrink: 0;
|
||
align-items: flex-start;
|
||
justify-content: flex-end;
|
||
min-height: 28px;
|
||
margin-left: auto;
|
||
}
|
||
|
||
.card-corner-tag :deep(.el-tag) {
|
||
--el-tag-border-radius: 999px;
|
||
--el-tag-font-size: 12px;
|
||
--el-tag-border-color: transparent;
|
||
|
||
padding: 0 10px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.01em;
|
||
backdrop-filter: blur(6px);
|
||
}
|
||
|
||
.card-footer {
|
||
display: flex;
|
||
gap: var(--space-2);
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
min-height: var(--space-8);
|
||
padding-top: var(--space-4);
|
||
border-top: 1px solid hsl(var(--divider-faint) / 85%);
|
||
}
|
||
|
||
.card-main-actions {
|
||
display: flex;
|
||
gap: var(--space-2);
|
||
align-items: center;
|
||
min-width: 0;
|
||
}
|
||
|
||
.card-primary-hint {
|
||
display: flex;
|
||
flex: 0 0 auto;
|
||
align-items: center;
|
||
min-width: 0;
|
||
}
|
||
|
||
.card-primary-button {
|
||
--el-button-bg-color: hsl(var(--primary) / 8%);
|
||
--el-button-border-color: hsl(var(--primary) / 14%);
|
||
--el-button-hover-bg-color: hsl(var(--primary) / 14%);
|
||
--el-button-hover-border-color: hsl(var(--primary) / 20%);
|
||
--el-button-active-bg-color: hsl(var(--primary) / 18%);
|
||
--el-button-active-border-color: hsl(var(--primary) / 24%);
|
||
--el-button-text-color: hsl(var(--primary));
|
||
--el-button-hover-text-color: hsl(var(--primary));
|
||
--el-button-active-text-color: hsl(var(--primary));
|
||
|
||
width: 32px;
|
||
height: 32px;
|
||
transition:
|
||
color var(--motion-duration-base) var(--motion-ease-standard),
|
||
background-color var(--motion-duration-base) var(--motion-ease-standard),
|
||
transform var(--motion-duration-fast) var(--motion-ease-standard);
|
||
}
|
||
|
||
.card-primary-button:active {
|
||
transform: scale(0.96);
|
||
}
|
||
|
||
.card-primary-button:focus-visible {
|
||
outline: none;
|
||
box-shadow: 0 0 0 4px hsl(var(--primary) / 12%);
|
||
}
|
||
|
||
.primary-label {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
height: var(--space-8);
|
||
min-width: 0;
|
||
padding: 0 var(--space-1);
|
||
}
|
||
|
||
.primary-icon {
|
||
color: hsl(var(--primary));
|
||
}
|
||
|
||
.primary-text {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: hsl(var(--text-strong));
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.card-actions {
|
||
display: flex;
|
||
flex: 0 1 auto;
|
||
gap: var(--space-2);
|
||
align-items: center;
|
||
min-width: 0;
|
||
}
|
||
|
||
.card-actions :deep(.el-button + .el-button) {
|
||
margin-left: 0;
|
||
}
|
||
|
||
.card-overflow-action {
|
||
flex-shrink: 0;
|
||
margin-left: auto;
|
||
}
|
||
|
||
.card-action-btn {
|
||
--el-button-font-weight: 500;
|
||
--el-button-text-color: hsl(var(--text-muted));
|
||
--el-button-hover-text-color: hsl(var(--text-strong));
|
||
--el-button-active-text-color: hsl(var(--text-strong));
|
||
|
||
height: var(--space-8);
|
||
padding: 0 var(--space-1);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.card-action-btn--danger {
|
||
--el-button-text-color: hsl(var(--destructive));
|
||
--el-button-hover-text-color: hsl(var(--destructive));
|
||
--el-button-active-text-color: hsl(var(--destructive));
|
||
}
|
||
|
||
.card-action-btn--menu {
|
||
width: var(--space-8);
|
||
padding: 0;
|
||
}
|
||
|
||
.menu-action-content {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
}
|
||
|
||
:deep(.card-menu-item--danger) {
|
||
color: hsl(var(--destructive));
|
||
}
|
||
</style>
|