refactor(web): 收口管理域运营台组件

This commit is contained in:
root
2026-07-15 18:36:28 +02:00
parent ff4bc3c9f3
commit 99e0418fa0
9 changed files with 104 additions and 107 deletions
@@ -5,6 +5,7 @@
<script setup lang="ts">
import { List, Workflow } from "lucide-vue-next";
import EmptyState from "@/components/common/EmptyState.vue";
import ResourceCollection from "@/components/common/ResourceCollection.vue";
import StatusBadge from "@/components/common/StatusBadge.vue";
import FactPath, { type FactPathStep } from "@/components/console/FactPath.vue";
import ViewModeSwitch from "@/components/console/ViewModeSwitch.vue";
@@ -55,21 +56,11 @@ function displayDate(value: string): string {
description="同一 ExternalSecret DTO 的紧凑字段视图。"
:body-padding="false"
>
<div class="secret-compact-list">
<button
v-for="row in rows"
:key="rowKey(row)"
type="button"
:data-selected="rowKey(row) === rowKey(selected)"
@click="emit('select', row)"
>
<strong>{{ row.namespace }}/{{ row.name }}</strong
><StatusBadge :status="row.status" :label="row.status" /><code
>{{ row.storeKind }}/{{ row.storeName }}</code
><code>{{ row.targetSecretName }}</code
><span>{{ row.ageText }}</span>
</button>
</div>
<ResourceCollection class="secret-compact-list" :items="rows" :item-key="rowKey" view="list" density="compact" :selected-id="rowKey(selected)" :show-controls="false" empty-text="暂无 ExternalSecret" aria-label="ExternalSecret 库存清单">
<template #list="{ item: row }"><button type="button" :data-selected="rowKey(row) === rowKey(selected)" @click="emit('select', row)">
<strong>{{ row.namespace }}/{{ row.name }}</strong><StatusBadge :status="row.status" :label="row.status" /><code>{{ row.storeKind }}/{{ row.storeName }}</code><code>{{ row.targetSecretName }}</code><span>{{ row.ageText }}</span>
</button></template>
</ResourceCollection>
</SectionFrame>
<SectionFrame
title="ExternalSecret 事实"
@@ -126,7 +117,7 @@ function displayDate(value: string): string {
type="button"
@click="emit('inspect', selected)"
>
打开详情 Inspector
打开脱敏详情
</button></SectionFrame
>
</div>
@@ -2,6 +2,7 @@
<!-- Implementation reference: draft-2026-07-13-p0-cloud-console. -->
<script setup lang="ts">
import InspectorPanel from "@/components/console/InspectorPanel.vue";
import AuditTimeline from "@/components/console/AuditTimeline.vue";
import type { AdminBillingUserSummary, ApiKeyRecord, AuthUser, BillingPlanDetail, ResourceEntitlementSummary, UsageLedgerRow, UsageReservationRow } from "@/types";
type DetailTab = "profile" | "credits" | "entitlements" | "keys" | "reservations" | "ledger";
@@ -13,7 +14,7 @@ const emit = defineEmits<{ close: []; tab: [tab: string]; save: []; adjust: [] }
const formatNumber = (value: unknown): string => Number(value ?? 0).toLocaleString();
function formatDate(value: string | null | undefined): string { if (!value) return "-"; const date = new Date(value); return Number.isNaN(date.getTime()) ? value : date.toLocaleString(); }
const formatSigned = (value: unknown): string => { const number = Number(value ?? 0); return `${number > 0 ? "+" : ""}${number.toLocaleString()}`; };
const auditKey = (item: UsageLedgerRow): string => String(item.id || item.idempotencyKey || item.createdAt || Math.random());
const auditKey = (item: UsageLedgerRow): string => item.id;
</script>
<template>
@@ -25,6 +26,6 @@ const auditKey = (item: UsageLedgerRow): string => String(item.id || item.idempo
<div v-else-if="tab === 'entitlements'" class="inspector-fact-list"><article v-for="item in entitlements" :key="item.id || item.resourceType"><strong>{{ item.resourceType }}</strong><small>{{ item.enabled === false ? "disabled" : "enabled" }} · monthly {{ formatNumber(item.monthlyUsageCredits) }} / {{ item.monthlyQuotaUnlimited ? "unlimited" : formatNumber(item.monthlyQuotaCredits) }}</small><small>concurrent {{ formatNumber(item.activeReservations) }} / {{ item.concurrencyUnlimited ? "unlimited" : formatNumber(item.maxConcurrentReservations) }} · RPM {{ formatNumber(item.rpmUsed) }} / {{ item.rpmUnlimited ? "unlimited" : formatNumber(item.rpmLimit) }}</small></article><p v-if="!entitlements.length" class="muted-box">暂无 entitlement</p></div>
<div v-else-if="tab === 'keys'" class="inspector-fact-list"><article v-for="item in apiKeys" :key="item.id"><strong>{{ item.name || item.id }}</strong><small>{{ item.prefix || "-" }} · {{ item.revokedAt ? "revoked" : "active" }}</small><small>created {{ formatDate(item.createdAt) }} · last used {{ formatDate(item.lastUsedAt) }}</small></article><p v-if="!apiKeys.length" class="muted-box">暂无 API key</p></div>
<div v-else-if="tab === 'reservations'" class="inspector-fact-list"><article v-for="item in reservations" :key="item.reservationId"><strong>{{ item.reservationId }}</strong><small>{{ item.serviceId }} · {{ item.resourceType || "generic" }} · {{ item.status }}</small><small>{{ formatNumber(item.estimatedCredits) }} credits · expires {{ formatDate(item.expiresAt) }}</small></article><p v-if="!reservations.length" class="muted-box">没有 active reservation</p></div>
<div v-else class="inspector-fact-list"><article v-for="item in ledgerRows" :key="auditKey(item)"><strong>{{ item.kind }} · {{ formatSigned(item.deltaCredits) }}</strong><small>{{ formatDate(item.createdAt) }} · {{ item.reason }} · {{ item.status }}</small><small>{{ formatNumber(item.balanceBefore) }} → {{ formatNumber(item.balanceAfter) }} · {{ item.idempotencyKey || "-" }}</small></article><p v-if="!ledgerRows.length" class="muted-box">暂无用户 ledger</p></div>
<AuditTimeline v-else :items="ledgerRows" :item-key="auditKey" label="用户 ledger 时间线" empty-text="暂无用户 ledger"><template #item="{ item }"><strong>{{ item.kind }} · {{ formatSigned(item.deltaCredits) }}</strong><small>{{ formatDate(item.createdAt) }} · {{ item.reason }} · {{ item.status }}</small><small>{{ formatNumber(item.balanceBefore) }} {{ formatNumber(item.balanceAfter) }} · {{ item.idempotencyKey || "-" }}</small></template></AuditTimeline>
</InspectorPanel>
</template>
@@ -5,10 +5,11 @@
<script setup lang="ts">
import { Users } from "lucide-vue-next";
import DataGrid, { type DataGridColumn } from "@/components/common/DataGrid.vue";
import ResourceCollection from "@/components/common/ResourceCollection.vue";
import SectionFrame from "@/components/layout/SectionFrame.vue";
import type { AdminBillingUserSummary } from "@/types";
defineProps<{ users: AdminBillingUserSummary[]; selectedId: string | null; view: "list" | "grid"; pendingId: string | null }>();
defineProps<{ users: AdminBillingUserSummary[]; selectedId: string | null; view: "list" | "grid"; density: "compact" | "comfortable"; pendingId: string | null }>();
const emit = defineEmits<{ select: [userId: string]; status: [user: AdminBillingUserSummary] }>();
const columns: DataGridColumn[] = [{ key: "user", label: "User" }, { key: "status", label: "Status" }, { key: "credits", label: "Credits" }, { key: "usage", label: "Usage" }, { key: "keys", label: "Keys" }, { key: "actions", label: "Action", align: "right" }];
const userKey = (item: AdminBillingUserSummary): string => String(item.user?.id || "");
@@ -21,37 +22,36 @@ const formatNumber = (value: unknown): string => Number(value ?? 0).toLocaleStri
description="选择动作与状态写操作使用同级控件,避免交互元素嵌套。"
:body-padding="false"
>
<div v-if="view === 'grid'" class="user-entity-grid">
<article
v-for="item in users"
:key="userKey(item)"
class="user-entity-card"
:data-selected="userKey(item) === selectedId"
>
<button
class="user-entity-select"
type="button"
@click="emit('select', item.user?.id || '')"
>
<Users :size="20" aria-hidden="true" />
<strong>{{ item.user?.displayName || item.user?.username }}</strong>
<small>{{ item.user?.email || item.user?.id }}</small>
<dl>
<div><dt>Available</dt><dd>{{ formatNumber(item.credits?.available) }}</dd></div>
<div><dt>Usage</dt><dd>{{ formatNumber(item.usage?.totalCredits) }}</dd></div>
<div><dt>Keys</dt><dd>{{ formatNumber(item.apiKeys?.activeCount) }}</dd></div>
</dl>
</button>
<button
class="table-action"
type="button"
:disabled="pendingId === item.user?.id"
@click="emit('status', item)"
>
{{ item.user?.status === "disabled" ? "启用" : "禁用" }}
</button>
</article>
</div>
<ResourceCollection
v-if="view === 'grid'"
class="user-entity-grid"
:items="users"
:item-key="userKey"
view="cards"
:density="density"
:selected-id="selectedId"
:show-controls="false"
empty-text="暂无用户"
aria-label="用户实体集合"
>
<template #card="{ item }">
<article class="user-entity-card" :data-selected="userKey(item) === selectedId">
<button class="user-entity-select" type="button" @click="emit('select', item.user?.id || '')">
<Users :size="20" aria-hidden="true" />
<strong>{{ item.user?.displayName || item.user?.username }}</strong>
<small>{{ item.user?.email || item.user?.id }}</small>
<dl>
<div><dt>Available</dt><dd>{{ formatNumber(item.credits?.available) }}</dd></div>
<div><dt>Usage</dt><dd>{{ formatNumber(item.usage?.totalCredits) }}</dd></div>
<div><dt>Keys</dt><dd>{{ formatNumber(item.apiKeys?.activeCount) }}</dd></div>
</dl>
</button>
<button class="table-action" type="button" :disabled="pendingId === item.user?.id" @click="emit('status', item)">
{{ item.user?.status === "disabled" ? "启用" : "禁用" }}
</button>
</article>
</template>
</ResourceCollection>
<DataGrid
v-else
:columns="columns"
@@ -101,6 +101,14 @@ const viewMode = computed({
false,
),
});
const density = computed({
get: () => viewState.density.value,
set: (value: "compact" | "comfortable") =>
void viewState.update(
{ density: value === "comfortable" ? undefined : value },
false,
),
});
const detailTab = computed({
get: () => viewState.tab.value || "profile",
set: (value: string) =>
@@ -373,7 +381,7 @@ async function adjustCredits(): Promise<void> {
error, mutationError, pending, page, pageSize, total, stateAuthority,
createOpen, filters, createForm, editForm, creditForm, selectedSummary,
selectedUser, selectedEntitlements, ledgerRows, apiKeys, reservations,
viewMode, detailTab, asyncState, statusItems, loadPlans, loadUsers,
viewMode, density, detailTab, asyncState, statusItems, loadPlans, loadUsers,
applyFilters, setPage, selectUser, closeInspector, createUser, saveUser,
setUserStatus, adjustCredits, userColumns, userKey, formatNumber,
...audit, usageColumns, ledgerColumns, auditKey, formatSigned, formatDate,
@@ -22,6 +22,9 @@
font-weight: 800;
}
.billing-summary-grid {
min-width: 0;
}
.billing-summary-grid > .resource-collection-items {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 1px;
@@ -98,7 +101,7 @@
.billing-operations {
min-height: 0;
}
.billing-summary-grid {
.billing-summary-grid > .resource-collection-items {
grid-template-columns: minmax(0, 1fr);
}
.billing-summary-grid dl {
@@ -6,6 +6,7 @@ import AsyncBoundary from "@/components/common/AsyncBoundary.vue";
import BaseDialog from "@/components/common/BaseDialog.vue";
import DataGrid from "@/components/common/DataGrid.vue";
import EmptyState from "@/components/common/EmptyState.vue";
import ResourceCollection from "@/components/common/ResourceCollection.vue";
import BillingCodeIndex from "@/components/billing/BillingCodeIndex.vue";
import BillingCodeInspector from "@/components/billing/BillingCodeInspector.vue";
import OperationsWorkspace from "@/components/console/OperationsWorkspace.vue";
@@ -93,38 +94,12 @@ const {
title="兑换运营摘要"
description="状态、额度、使用率与到期时间可直接扫描。"
:body-padding="false"
><div class="billing-summary-grid">
<button
v-for="code in codes"
:key="codeKey(code)"
type="button"
@click="selectCode(code)"
>
<span class="status-pill" :data-status="code.status">{{
code.status
}}</span
><strong>{{ code.codePrefix }}</strong
><small>{{ code.displayName || code.reason }}</small>
<dl>
<div>
<dt>Credits</dt>
<dd>{{ formatNumber(code.creditAmount) }}</dd>
</div>
<div>
<dt>Usage</dt>
<dd>
{{ formatNumber(code.redeemedCount) }}/{{
formatNumber(code.maxRedemptions)
}}
</dd>
</div>
<div>
<dt>Expires</dt>
<dd>{{ formatDate(code.expiresAt) }}</dd>
</div>
</dl>
</button>
</div></SectionFrame
><ResourceCollection class="billing-summary-grid" :items="codes" :item-key="codeKey" view="cards" density="compact" :selected-id="selectedCodeId" :show-controls="false" empty-text="暂无兑换码" aria-label="兑换码运营摘要" @select="selectCode">
<template #card="{ item: code }"><button type="button" @click="selectCode(code)">
<span class="status-pill" :data-status="code.status">{{ code.status }}</span><strong>{{ code.codePrefix }}</strong><small>{{ code.displayName || code.reason }}</small>
<dl><div><dt>Credits</dt><dd>{{ formatNumber(code.creditAmount) }}</dd></div><div><dt>Usage</dt><dd>{{ formatNumber(code.redeemedCount) }}/{{ formatNumber(code.maxRedemptions) }}</dd></div><div><dt>Expires</dt><dd>{{ formatDate(code.expiresAt) }}</dd></div></dl>
</button></template>
</ResourceCollection></SectionFrame
><SectionFrame
v-else
title="兑换码紧凑清单"
@@ -13,11 +13,22 @@
gap: 8px;
}
.user-entity-grid {
min-width: 0;
}
.user-entity-grid > .resource-collection-items {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
gap: 8px;
padding: 10px;
}
.user-entity-grid > .resource-collection-items[data-density="compact"] {
gap: 5px;
padding: 6px;
}
.user-entity-grid > .resource-collection-items[data-density="compact"] .user-entity-card {
gap: 5px;
padding: 6px;
}
.user-entity-card {
display: grid;
gap: 7px;
@@ -107,7 +118,7 @@
color: #61747c;
}
@media (max-width: 720px) {
.user-entity-grid {
.user-entity-grid > .resource-collection-items {
grid-template-columns: 1fr;
}
.audit-filter-grid {
@@ -1,11 +1,12 @@
<!-- SPEC: PJ2026-010405 云端控制台 / Users. -->
<!-- Implementation reference: draft-2026-07-13-p0-cloud-console. -->
<script setup lang="ts">
import { Grid2X2, List } from "lucide-vue-next";
import { Grid2X2, List, Rows3, Rows4 } from "lucide-vue-next";
import AsyncBoundary from "@/components/common/AsyncBoundary.vue";
import BaseDrawer from "@/components/common/BaseDrawer.vue";
import DataGrid from "@/components/common/DataGrid.vue";
import EmptyState from "@/components/common/EmptyState.vue";
import Pagination from "@/components/common/Pagination.vue";
import OperationsWorkspace from "@/components/console/OperationsWorkspace.vue";
import ViewModeSwitch from "@/components/console/ViewModeSwitch.vue";
import PageCommandBar from "@/components/layout/PageCommandBar.vue";
@@ -20,7 +21,7 @@ const {
users, plans, selectedUserId, loading, detailLoading, error, mutationError,
pending, page, pageSize, total, createOpen, filters, createForm, editForm,
creditForm, selectedSummary, selectedUser, selectedEntitlements, ledgerRows,
apiKeys, reservations, viewMode, detailTab, asyncState, statusItems,
apiKeys, reservations, viewMode, density, detailTab, asyncState, statusItems,
loadUsers, applyFilters, setPage, selectUser, closeInspector, createUser, saveUser,
setUserStatus, adjustCredits, userColumns, userKey, formatNumber,
auditTab, auditLoading, auditError, auditUsageRows, auditLedgerRows,
@@ -44,6 +45,13 @@ const {
{ value: 'list', label: '紧凑列表', icon: List },
{ value: 'grid', label: '实体网格', icon: Grid2X2 },
]"
/><ViewModeSwitch
v-model="density"
:options="[
{ value: 'comfortable', label: '舒适密度', icon: Rows4 },
{ value: 'compact', label: '紧凑密度', icon: Rows3 },
]"
label="用户集合密度"
/><button class="table-action" type="button" @click="createOpen = true">
创建用户
</button></template
@@ -72,7 +80,7 @@ const {
<template #index><UserDirectoryIndex v-model:search="filters.search" v-model:status="filters.status" v-model:role="filters.role" :users="users" :selected-id="selectedUserId" :page="page" :page-size="pageSize" :total="total" @select="selectUser" @apply="applyFilters" @page="setPage" /></template>
<div class="user-main-stack">
<UserDirectoryCollection :users="users" :selected-id="selectedUserId" :view="viewMode" :pending-id="pending?.startsWith('status:') ? pending.slice(7) : null" @select="selectUser" @status="setUserStatus" />
<UserDirectoryCollection :users="users" :selected-id="selectedUserId" :view="viewMode" :density="density" :pending-id="pending?.startsWith('status:') ? pending.slice(7) : null" @select="selectUser" @status="setUserStatus" />
<SectionFrame
title="全局 Usage / Ledger"
@@ -1,15 +1,16 @@
<!-- SPEC: PJ2026-010405 云端控制台. -->
<!-- Implementation reference: draft-2026-07-13-p0-cloud-console. -->
<script setup lang="ts">
import { Grid2X2, List, Rows3, Rows4, X } from "lucide-vue-next";
import { Grid2X2, List, Rows3, Rows4 } from "lucide-vue-next";
import { computed, onMounted, ref } from "vue";
import { RouterLink, useRoute, useRouter } from "vue-router";
import { useRoute, useRouter } from "vue-router";
import { apiKeysAPI } from "@/api";
import AsyncBoundary from "@/components/common/AsyncBoundary.vue";
import BaseDialog from "@/components/common/BaseDialog.vue";
import ConfirmDialog from "@/components/common/ConfirmDialog.vue";
import EmptyState from "@/components/common/EmptyState.vue";
import ResourceCollection from "@/components/common/ResourceCollection.vue";
import InspectorPanel from "@/components/console/InspectorPanel.vue";
import ViewModeSwitch from "@/components/console/ViewModeSwitch.vue";
import ApiKeyResourceCard from "@/components/api-keys/ApiKeyResourceCard.vue";
import BoundedWorkspace from "@/components/layout/BoundedWorkspace.vue";
@@ -136,6 +137,12 @@ function selectKey(key: ApiKeyRecord): void {
void router.push({ name: "ApiKeyDetail", params: { keyId: key.id }, query: route.query });
}
function closeInspector(): void {
editingId.value = null;
editingName.value = "";
void router.push({ name: "ApiKeys", query: route.query });
}
function startRename(key: ApiKeyRecord): void {
editingId.value = key.id;
editingName.value = key.name || "";
@@ -204,9 +211,8 @@ function formatDate(value: unknown): string {
</section>
<template #inspector>
<section v-if="selectedKey" class="api-key-inspector">
<header><div><span>KEY DETAIL</span><h2>{{ selectedKey.name || selectedKey.id }}</h2></div><RouterLink :to="{ name: 'ApiKeys', query: route.query }" title="关闭详情" aria-label="关闭详情"><X :size="16" aria-hidden="true" /></RouterLink></header>
<dl>
<InspectorPanel v-if="selectedKey" :title="selectedKey.name || selectedKey.id" :subtitle="selectedKey.id" @close="closeInspector">
<dl class="api-key-inspector-facts">
<div><dt>ID</dt><dd><code>{{ selectedKey.id }}</code></dd></div>
<div><dt>Prefix</dt><dd><code>{{ selectedKey.prefix || '-' }}</code></dd></div>
<div><dt>状态</dt><dd>{{ selectedKey.revokedAt ? 'revoked' : 'active' }}</dd></div>
@@ -220,7 +226,7 @@ function formatDate(value: unknown): string {
<button class="danger" type="button" :disabled="pending === `revoke:${selectedKey.id}`" @click="revokeTarget = selectedKey">撤销 Key</button>
</section>
<p class="api-key-fact-note">当前 API 未声明 scope rotation 合同因此详情不会展示或模拟这些能力</p>
</section>
</InspectorPanel>
</template>
</BoundedWorkspace>
@@ -251,27 +257,22 @@ function formatDate(value: unknown): string {
.api-key-summary-strip { display: grid; grid-template-columns: 120px 120px 120px minmax(0, 1fr); border: 1px solid #cbd5dc; border-radius: 8px; overflow: hidden; background: #fff; }
.api-key-summary-strip div { display: grid; gap: 3px; padding: 10px 13px; border-right: 1px solid #dbe3e7; }
.api-key-summary-strip span,
.api-key-collection-panel > header span,
.api-key-inspector > header span { color: #16807d; font: 850 9px/1 ui-monospace, monospace; letter-spacing: 0; }
.api-key-collection-panel > header span { color: #16807d; font: 850 9px/1 ui-monospace, monospace; letter-spacing: 0; }
.api-key-summary-strip strong { color: #19303a; font-size: 20px; }
.api-key-summary-strip p { align-self: center; margin: 0; padding: 0 14px; color: #64757e; font-size: 11px; }
.api-key-workspace { display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; min-height: 0; }
.api-key-workspace[data-inspector-open="true"] { grid-template-columns: minmax(0, 1fr) minmax(290px, 360px); }
.api-key-collection-panel { display: grid; align-content: start; gap: 11px; padding: 13px; }
.api-key-collection-panel > header,
.api-key-inspector > header { display: flex; align-items: start; justify-content: space-between; gap: 12px; border-bottom: 1px solid #e0e7ea; padding-bottom: 10px; }
.api-key-collection-panel h2,
.api-key-inspector h2 { margin: 5px 0 0; color: #172b34; font-size: 17px; }
.api-key-collection-panel > header { display: flex; align-items: start; justify-content: space-between; gap: 12px; border-bottom: 1px solid #e0e7ea; padding-bottom: 10px; }
.api-key-collection-panel h2 { margin: 5px 0 0; color: #172b34; font-size: 17px; }
.api-key-collection-panel > header small { color: #697a83; font-size: 10px; }
.api-key-collection { display: grid; grid-template-columns: repeat(auto-fill, minmax(290px, 1fr)); gap: 9px; }
.api-key-collection[data-view="list"] { grid-template-columns: 1fr; }
.api-key-collection[data-density="compact"] { gap: 5px; }
.api-key-inspector { position: sticky; top: 0; align-self: start; display: grid; gap: 13px; max-height: calc(100dvh - 120px); overflow: auto; padding: 14px; box-shadow: inset 3px 0 #17a39e; }
.api-key-inspector > header a { display: grid; width: 28px; height: 28px; place-items: center; border: 1px solid #c7d1d7; border-radius: 6px; color: #344850; text-decoration: none; }
.api-key-inspector dl { display: grid; gap: 0; margin: 0; }
.api-key-inspector dl div { display: grid; grid-template-columns: 90px minmax(0, 1fr); gap: 10px; border-bottom: 1px solid #e2e8eb; padding: 9px 0; }
.api-key-inspector dt { color: #74828a; font-size: 10px; }
.api-key-inspector dd { min-width: 0; margin: 0; color: #263b44; font-size: 11px; overflow-wrap: anywhere; }
.api-key-inspector-facts { display: grid; gap: 0; margin: 0; }
.api-key-inspector-facts div { display: grid; grid-template-columns: 90px minmax(0, 1fr); gap: 10px; border-bottom: 1px solid #e2e8eb; padding: 9px 0; }
.api-key-inspector-facts dt { color: #74828a; font-size: 10px; }
.api-key-inspector-facts dd { min-width: 0; margin: 0; color: #263b44; font-size: 11px; overflow-wrap: anywhere; }
.api-key-inspector-actions { display: grid; gap: 7px; }
.api-key-inspector-actions button,
.api-key-rename button { min-height: 34px; border: 1px solid #aebdc5; border-radius: 6px; background: #fff; color: #30434c; font-weight: 750; cursor: pointer; }
@@ -285,7 +286,6 @@ function formatDate(value: unknown): string {
.secret-once-box small { color: #7c672f; }
@media (max-width: 1120px) {
.api-key-workspace[data-inspector-open="true"] { grid-template-columns: 1fr; }
.api-key-inspector { position: static; max-height: none; }
}
@media (max-width: 760px) {
.api-key-command-bar { grid-template-columns: 1fr 1fr; }