@commonpub/layer 0.21.22 → 0.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/admin/theme/AdminThemeFamilyCard.vue +277 -0
- package/components/admin/theme/AdminThemeOverridesPanel.vue +222 -0
- package/components/admin/theme/AdminThemePreviewPane.vue +218 -0
- package/components/admin/theme/AdminThemeSceneAdmin.vue +189 -0
- package/components/admin/theme/AdminThemeSceneGallery.vue +353 -0
- package/components/admin/theme/AdminThemeSceneProse.vue +140 -0
- package/components/admin/theme/AdminThemeTokenGroup.vue +98 -0
- package/components/admin/theme/AdminThemeTokenInput.vue +278 -0
- package/composables/useTheme.ts +24 -14
- package/composables/useThemeAdmin.ts +167 -0
- package/package.json +7 -7
- package/pages/admin/theme/edit/[id].vue +595 -0
- package/pages/admin/theme/index.vue +449 -0
- package/plugins/theme.ts +25 -7
- package/server/api/admin/themes/[id].delete.ts +40 -0
- package/server/api/admin/themes/[id].get.ts +20 -0
- package/server/api/admin/themes/[id].put.ts +45 -0
- package/server/api/admin/themes/discover.get.ts +22 -0
- package/server/api/admin/themes/index.get.ts +40 -0
- package/server/api/admin/themes/index.post.ts +46 -0
- package/server/api/profile/theme.put.ts +2 -1
- package/server/middleware/theme.ts +23 -9
- package/server/utils/instanceTheme.ts +145 -25
- package/types/theme.ts +54 -0
- package/utils/themeDiscovery.ts +67 -0
- package/utils/themeIO.ts +79 -0
- package/utils/themeIds.ts +25 -0
- package/pages/admin/theme.vue +0 -502
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Live preview pane for the theme editor. Hosts the scene picker, the
|
|
4
|
+
* light/dark mode toggle, and the scrollable scene surface that renders
|
|
5
|
+
* the in-progress theme tokens applied to representative components.
|
|
6
|
+
*
|
|
7
|
+
* The scene system is pluggable: each scene is a Vue component rendered
|
|
8
|
+
* inside the token-scoped wrapper. Add scenes by registering them in
|
|
9
|
+
* PREVIEW_SCENES and dropping an `AdminThemeScene*.vue` in this directory.
|
|
10
|
+
*
|
|
11
|
+
* Future scenes the architecture is built to absorb:
|
|
12
|
+
* - 'iframe-route' — render an actual site route with the in-progress theme
|
|
13
|
+
* - 'page-layout' — full landing-page mockup with editable section list
|
|
14
|
+
* - 'layout-builder' — drag-and-drop section composer
|
|
15
|
+
*/
|
|
16
|
+
import { computed, ref } from 'vue';
|
|
17
|
+
|
|
18
|
+
const props = defineProps<{
|
|
19
|
+
tokens: Record<string, string>;
|
|
20
|
+
/** The base theme whose CSS file provides inherited defaults (via data-theme). */
|
|
21
|
+
parentTheme: string;
|
|
22
|
+
isDark: boolean;
|
|
23
|
+
}>();
|
|
24
|
+
|
|
25
|
+
interface SceneOption {
|
|
26
|
+
id: 'gallery' | 'prose' | 'admin';
|
|
27
|
+
label: string;
|
|
28
|
+
description: string;
|
|
29
|
+
icon: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const PREVIEW_SCENES: SceneOption[] = [
|
|
33
|
+
{ id: 'gallery', label: 'Components', description: 'Buttons, cards, forms, badges, prose, code', icon: 'fa-th-large' },
|
|
34
|
+
{ id: 'prose', label: 'Article', description: 'Headings, paragraphs, quote, code block, list', icon: 'fa-file-lines' },
|
|
35
|
+
{ id: 'admin', label: 'Admin shell', description: 'Topbar, sidebar, table, stat cards', icon: 'fa-gauge' },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const activeScene = ref<SceneOption['id']>('gallery');
|
|
39
|
+
const previewMode = ref<'light' | 'dark'>(props.isDark ? 'dark' : 'light');
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Map every parent-theme id to its family's light + dark variant. Mirrors
|
|
43
|
+
* `layers/base/utils/themeConfig.ts` THEME_TO_FAMILY + FAMILY_VARIANTS,
|
|
44
|
+
* inlined here so the preview pane doesn't need to import the SSR-side
|
|
45
|
+
* utils. Custom-theme parents (`cpub-custom-*`) and any unknown id fall
|
|
46
|
+
* back to the classic family — the user's tokens override on top regardless.
|
|
47
|
+
*/
|
|
48
|
+
const FAMILY_VARIANT_OF: Record<string, { light: string; dark: string }> = {
|
|
49
|
+
base: { light: 'base', dark: 'dark' },
|
|
50
|
+
dark: { light: 'base', dark: 'dark' },
|
|
51
|
+
agora: { light: 'agora', dark: 'agora-dark' },
|
|
52
|
+
'agora-dark': { light: 'agora', dark: 'agora-dark' },
|
|
53
|
+
generics: { light: 'generics', dark: 'generics' },
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The actual `data-theme` attribute applied to the preview surface,
|
|
58
|
+
* resolved from `parentTheme` + `previewMode`. Returns `undefined` for the
|
|
59
|
+
* base/light case (no attribute = `:root` rules apply natively, matching
|
|
60
|
+
* the convention used by `applyThemeToElement` elsewhere).
|
|
61
|
+
*
|
|
62
|
+
* **Bug fix from 0.22.0**: previously `:data-theme="parentTheme"` was
|
|
63
|
+
* hardcoded, so the Light/Dark toggle updated a ref but never re-rendered
|
|
64
|
+
* the preview. Now the toggle actually swaps the rendered theme.
|
|
65
|
+
*/
|
|
66
|
+
const effectiveDataTheme = computed<string | undefined>(() => {
|
|
67
|
+
const variants = FAMILY_VARIANT_OF[props.parentTheme] ?? FAMILY_VARIANT_OF.base!;
|
|
68
|
+
const v = previewMode.value === 'dark' ? variants.dark : variants.light;
|
|
69
|
+
return v === 'base' ? undefined : v;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Build the inline style string scoped to the preview surface. We apply
|
|
74
|
+
* tokens to the wrapper element only — that scopes the in-progress theme
|
|
75
|
+
* to the preview and avoids leaking it into the surrounding admin UI.
|
|
76
|
+
*/
|
|
77
|
+
const previewStyle = computed(() => {
|
|
78
|
+
const lines: string[] = [];
|
|
79
|
+
for (const [k, v] of Object.entries(props.tokens)) {
|
|
80
|
+
if (typeof v !== 'string') continue;
|
|
81
|
+
const safeKey = k.replace(/[^a-zA-Z0-9_-]/g, '');
|
|
82
|
+
const safeVal = v.replace(/[\r\n;]/g, ' ');
|
|
83
|
+
if (!safeKey) continue;
|
|
84
|
+
lines.push(`--${safeKey}: ${safeVal}`);
|
|
85
|
+
}
|
|
86
|
+
return lines.join('; ');
|
|
87
|
+
});
|
|
88
|
+
</script>
|
|
89
|
+
|
|
90
|
+
<template>
|
|
91
|
+
<div class="theme-preview-pane">
|
|
92
|
+
<header class="theme-preview-header">
|
|
93
|
+
<div class="theme-preview-scene-picker" role="tablist" aria-label="Preview scene">
|
|
94
|
+
<button
|
|
95
|
+
v-for="scene in PREVIEW_SCENES"
|
|
96
|
+
:key="scene.id"
|
|
97
|
+
type="button"
|
|
98
|
+
role="tab"
|
|
99
|
+
:aria-selected="activeScene === scene.id"
|
|
100
|
+
class="theme-preview-scene-tab"
|
|
101
|
+
:class="{ active: activeScene === scene.id }"
|
|
102
|
+
:title="scene.description"
|
|
103
|
+
@click="activeScene = scene.id"
|
|
104
|
+
>
|
|
105
|
+
<i :class="['fa-solid', scene.icon]" aria-hidden="true" />
|
|
106
|
+
<span>{{ scene.label }}</span>
|
|
107
|
+
</button>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<div class="theme-preview-mode-toggle" role="radiogroup" aria-label="Preview mode">
|
|
111
|
+
<button
|
|
112
|
+
type="button"
|
|
113
|
+
role="radio"
|
|
114
|
+
:aria-checked="previewMode === 'light'"
|
|
115
|
+
class="theme-preview-mode-btn"
|
|
116
|
+
:class="{ active: previewMode === 'light' }"
|
|
117
|
+
@click="previewMode = 'light'"
|
|
118
|
+
>
|
|
119
|
+
<i class="fa-solid fa-sun" aria-hidden="true" /> Light
|
|
120
|
+
</button>
|
|
121
|
+
<button
|
|
122
|
+
type="button"
|
|
123
|
+
role="radio"
|
|
124
|
+
:aria-checked="previewMode === 'dark'"
|
|
125
|
+
class="theme-preview-mode-btn"
|
|
126
|
+
:class="{ active: previewMode === 'dark' }"
|
|
127
|
+
@click="previewMode = 'dark'"
|
|
128
|
+
>
|
|
129
|
+
<i class="fa-solid fa-moon" aria-hidden="true" /> Dark
|
|
130
|
+
</button>
|
|
131
|
+
</div>
|
|
132
|
+
</header>
|
|
133
|
+
|
|
134
|
+
<div
|
|
135
|
+
class="theme-preview-surface"
|
|
136
|
+
:data-theme="effectiveDataTheme"
|
|
137
|
+
:style="previewStyle"
|
|
138
|
+
:data-preview-mode="previewMode"
|
|
139
|
+
>
|
|
140
|
+
<AdminThemeSceneGallery v-if="activeScene === 'gallery'" />
|
|
141
|
+
<AdminThemeSceneProse v-else-if="activeScene === 'prose'" />
|
|
142
|
+
<AdminThemeSceneAdmin v-else-if="activeScene === 'admin'" />
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
</template>
|
|
146
|
+
|
|
147
|
+
<style scoped>
|
|
148
|
+
.theme-preview-pane {
|
|
149
|
+
display: flex;
|
|
150
|
+
flex-direction: column;
|
|
151
|
+
height: 100%;
|
|
152
|
+
min-height: 0;
|
|
153
|
+
background: var(--surface2);
|
|
154
|
+
border-left: var(--border-width-default) solid var(--border);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.theme-preview-header {
|
|
158
|
+
display: flex;
|
|
159
|
+
justify-content: space-between;
|
|
160
|
+
align-items: center;
|
|
161
|
+
padding: var(--space-2) var(--space-3);
|
|
162
|
+
background: var(--surface);
|
|
163
|
+
border-bottom: var(--border-width-default) solid var(--border);
|
|
164
|
+
flex-wrap: wrap;
|
|
165
|
+
gap: var(--space-2);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.theme-preview-scene-picker,
|
|
169
|
+
.theme-preview-mode-toggle {
|
|
170
|
+
display: flex;
|
|
171
|
+
gap: 2px;
|
|
172
|
+
background: var(--surface2);
|
|
173
|
+
padding: 2px;
|
|
174
|
+
border: var(--border-width-thin) solid var(--border2);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.theme-preview-scene-tab,
|
|
178
|
+
.theme-preview-mode-btn {
|
|
179
|
+
display: inline-flex;
|
|
180
|
+
align-items: center;
|
|
181
|
+
gap: 6px;
|
|
182
|
+
padding: 6px 10px;
|
|
183
|
+
background: none;
|
|
184
|
+
border: 0;
|
|
185
|
+
font-family: var(--font-mono);
|
|
186
|
+
font-size: 11px;
|
|
187
|
+
letter-spacing: var(--tracking-wide);
|
|
188
|
+
text-transform: uppercase;
|
|
189
|
+
color: var(--text-dim);
|
|
190
|
+
cursor: pointer;
|
|
191
|
+
border-radius: 0;
|
|
192
|
+
}
|
|
193
|
+
.theme-preview-scene-tab:hover,
|
|
194
|
+
.theme-preview-mode-btn:hover { color: var(--text); }
|
|
195
|
+
.theme-preview-scene-tab.active {
|
|
196
|
+
background: var(--surface);
|
|
197
|
+
color: var(--text);
|
|
198
|
+
box-shadow: inset 0 -2px 0 var(--accent);
|
|
199
|
+
}
|
|
200
|
+
.theme-preview-mode-btn.active {
|
|
201
|
+
background: var(--surface);
|
|
202
|
+
color: var(--accent);
|
|
203
|
+
}
|
|
204
|
+
.theme-preview-scene-tab i { font-size: 10px; }
|
|
205
|
+
|
|
206
|
+
.theme-preview-surface {
|
|
207
|
+
flex: 1;
|
|
208
|
+
overflow: auto;
|
|
209
|
+
padding: var(--space-4);
|
|
210
|
+
background-color: var(--bg);
|
|
211
|
+
color: var(--text);
|
|
212
|
+
font-family: var(--font-body);
|
|
213
|
+
/* Token re-application happens via inline style on this element. The
|
|
214
|
+
`data-theme` attr seeds inherited defaults; inline style overrides
|
|
215
|
+
each token the editor has changed. */
|
|
216
|
+
min-height: 0;
|
|
217
|
+
}
|
|
218
|
+
</style>
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Admin-shell preview. Renders a fake topbar + sidebar + stat dashboard
|
|
4
|
+
* + table so admins can preview what the panel they're using right now
|
|
5
|
+
* will look like with the in-progress theme applied.
|
|
6
|
+
*/
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<template>
|
|
10
|
+
<div class="scene-admin">
|
|
11
|
+
<header class="scene-admin-top">
|
|
12
|
+
<span class="scene-admin-brand">{{ '{ Instance Name }' }}</span>
|
|
13
|
+
<span class="scene-admin-badge">Admin</span>
|
|
14
|
+
<span class="scene-admin-back">← Back to site</span>
|
|
15
|
+
</header>
|
|
16
|
+
|
|
17
|
+
<div class="scene-admin-body">
|
|
18
|
+
<nav class="scene-admin-side">
|
|
19
|
+
<a class="scene-admin-link"><i class="fa-solid fa-gauge" aria-hidden="true" />Dashboard</a>
|
|
20
|
+
<a class="scene-admin-link"><i class="fa-solid fa-users" aria-hidden="true" />Users</a>
|
|
21
|
+
<a class="scene-admin-link"><i class="fa-solid fa-newspaper" aria-hidden="true" />Content</a>
|
|
22
|
+
<a class="scene-admin-link active"><i class="fa-solid fa-palette" aria-hidden="true" />Theme</a>
|
|
23
|
+
<a class="scene-admin-link"><i class="fa-solid fa-globe" aria-hidden="true" />Federation</a>
|
|
24
|
+
<a class="scene-admin-link"><i class="fa-solid fa-toggle-on" aria-hidden="true" />Features</a>
|
|
25
|
+
</nav>
|
|
26
|
+
|
|
27
|
+
<main class="scene-admin-main">
|
|
28
|
+
<h1 class="scene-admin-title">Instance overview</h1>
|
|
29
|
+
<p class="scene-admin-sub">Snapshot of activity across this CommonPub instance.</p>
|
|
30
|
+
|
|
31
|
+
<div class="scene-admin-stats">
|
|
32
|
+
<div class="scene-admin-stat">
|
|
33
|
+
<span class="scene-admin-stat-label">Members</span>
|
|
34
|
+
<span class="scene-admin-stat-value">3,124</span>
|
|
35
|
+
<span class="scene-admin-stat-delta">+18 this week</span>
|
|
36
|
+
</div>
|
|
37
|
+
<div class="scene-admin-stat">
|
|
38
|
+
<span class="scene-admin-stat-label">Published</span>
|
|
39
|
+
<span class="scene-admin-stat-value">512</span>
|
|
40
|
+
<span class="scene-admin-stat-delta">+24 this week</span>
|
|
41
|
+
</div>
|
|
42
|
+
<div class="scene-admin-stat">
|
|
43
|
+
<span class="scene-admin-stat-label">Federated</span>
|
|
44
|
+
<span class="scene-admin-stat-value">87 peers</span>
|
|
45
|
+
<span class="scene-admin-stat-delta tone-warn">2 retrying</span>
|
|
46
|
+
</div>
|
|
47
|
+
<div class="scene-admin-stat">
|
|
48
|
+
<span class="scene-admin-stat-label">Storage</span>
|
|
49
|
+
<span class="scene-admin-stat-value">8.2 GB</span>
|
|
50
|
+
<span class="scene-admin-stat-delta">of 50 GB</span>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<h2 class="scene-admin-h2">Recent reports</h2>
|
|
55
|
+
<div class="scene-admin-table-wrap">
|
|
56
|
+
<table class="scene-admin-table">
|
|
57
|
+
<thead>
|
|
58
|
+
<tr><th>Target</th><th>Reason</th><th>Reporter</th><th>Status</th></tr>
|
|
59
|
+
</thead>
|
|
60
|
+
<tbody>
|
|
61
|
+
<tr>
|
|
62
|
+
<td><strong>@spam-actor@example.fed</strong></td>
|
|
63
|
+
<td>Spam</td>
|
|
64
|
+
<td>moheeb</td>
|
|
65
|
+
<td><span class="scene-admin-pill tone-yellow">Open</span></td>
|
|
66
|
+
</tr>
|
|
67
|
+
<tr>
|
|
68
|
+
<td>Project: "Buy followers fast"</td>
|
|
69
|
+
<td>Spam</td>
|
|
70
|
+
<td>aly</td>
|
|
71
|
+
<td><span class="scene-admin-pill tone-green">Resolved</span></td>
|
|
72
|
+
</tr>
|
|
73
|
+
<tr>
|
|
74
|
+
<td>Comment on "Edge AI nodes"</td>
|
|
75
|
+
<td>Harassment</td>
|
|
76
|
+
<td>kim</td>
|
|
77
|
+
<td><span class="scene-admin-pill tone-red">Escalated</span></td>
|
|
78
|
+
</tr>
|
|
79
|
+
</tbody>
|
|
80
|
+
</table>
|
|
81
|
+
</div>
|
|
82
|
+
</main>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</template>
|
|
86
|
+
|
|
87
|
+
<style scoped>
|
|
88
|
+
.scene-admin {
|
|
89
|
+
background: var(--bg);
|
|
90
|
+
border: var(--border-width-default) solid var(--border);
|
|
91
|
+
min-height: 480px;
|
|
92
|
+
overflow: hidden;
|
|
93
|
+
display: flex;
|
|
94
|
+
flex-direction: column;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.scene-admin-top {
|
|
98
|
+
height: 44px;
|
|
99
|
+
display: flex;
|
|
100
|
+
align-items: center;
|
|
101
|
+
gap: var(--space-3);
|
|
102
|
+
padding: 0 var(--space-4);
|
|
103
|
+
background: var(--surface);
|
|
104
|
+
border-bottom: var(--border-width-default) solid var(--border);
|
|
105
|
+
}
|
|
106
|
+
.scene-admin-brand { font-weight: var(--font-weight-bold); color: var(--text); }
|
|
107
|
+
.scene-admin-badge {
|
|
108
|
+
padding: 2px 8px;
|
|
109
|
+
background: var(--accent);
|
|
110
|
+
color: var(--color-on-accent);
|
|
111
|
+
font-family: var(--font-mono);
|
|
112
|
+
font-size: 10px;
|
|
113
|
+
letter-spacing: var(--tracking-wide);
|
|
114
|
+
text-transform: uppercase;
|
|
115
|
+
}
|
|
116
|
+
.scene-admin-back { margin-left: auto; color: var(--text-dim); font-size: var(--text-sm); }
|
|
117
|
+
|
|
118
|
+
.scene-admin-body { display: flex; flex: 1; min-height: 0; }
|
|
119
|
+
.scene-admin-side {
|
|
120
|
+
width: 180px;
|
|
121
|
+
padding: var(--space-3) var(--space-2);
|
|
122
|
+
background: var(--surface);
|
|
123
|
+
border-right: var(--border-width-default) solid var(--border);
|
|
124
|
+
display: flex;
|
|
125
|
+
flex-direction: column;
|
|
126
|
+
gap: 2px;
|
|
127
|
+
}
|
|
128
|
+
.scene-admin-link {
|
|
129
|
+
display: flex;
|
|
130
|
+
align-items: center;
|
|
131
|
+
gap: 10px;
|
|
132
|
+
padding: 6px var(--space-3);
|
|
133
|
+
color: var(--text-dim);
|
|
134
|
+
font-size: var(--text-sm);
|
|
135
|
+
cursor: pointer;
|
|
136
|
+
text-decoration: none;
|
|
137
|
+
}
|
|
138
|
+
.scene-admin-link i { width: 14px; text-align: center; font-size: 11px; }
|
|
139
|
+
.scene-admin-link:hover { color: var(--text); background: var(--surface2); }
|
|
140
|
+
.scene-admin-link.active { color: var(--accent); background: var(--accent-bg); font-weight: var(--font-weight-semibold); }
|
|
141
|
+
|
|
142
|
+
.scene-admin-main { flex: 1; padding: var(--space-5); min-width: 0; }
|
|
143
|
+
.scene-admin-title { font-size: var(--text-xl); font-weight: var(--font-weight-bold); color: var(--text); margin: 0 0 var(--space-1); }
|
|
144
|
+
.scene-admin-sub { color: var(--text-dim); margin: 0 0 var(--space-5); }
|
|
145
|
+
|
|
146
|
+
.scene-admin-stats {
|
|
147
|
+
display: grid;
|
|
148
|
+
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
|
149
|
+
gap: var(--space-3);
|
|
150
|
+
margin-bottom: var(--space-6);
|
|
151
|
+
}
|
|
152
|
+
.scene-admin-stat {
|
|
153
|
+
background: var(--surface);
|
|
154
|
+
border: var(--border-width-default) solid var(--border);
|
|
155
|
+
padding: var(--space-3) var(--space-4);
|
|
156
|
+
display: flex;
|
|
157
|
+
flex-direction: column;
|
|
158
|
+
gap: 2px;
|
|
159
|
+
box-shadow: var(--shadow-sm);
|
|
160
|
+
}
|
|
161
|
+
.scene-admin-stat-label { font-family: var(--font-mono); font-size: var(--text-label); letter-spacing: var(--tracking-wide); text-transform: uppercase; color: var(--text-dim); }
|
|
162
|
+
.scene-admin-stat-value { font-size: var(--text-xl); font-weight: var(--font-weight-bold); color: var(--text); }
|
|
163
|
+
.scene-admin-stat-delta { font-size: var(--text-sm); color: var(--green); }
|
|
164
|
+
.scene-admin-stat-delta.tone-warn { color: var(--yellow); }
|
|
165
|
+
|
|
166
|
+
.scene-admin-h2 { font-size: var(--text-md); font-weight: var(--font-weight-semibold); color: var(--text); margin: 0 0 var(--space-2); }
|
|
167
|
+
.scene-admin-table-wrap { background: var(--surface); border: var(--border-width-default) solid var(--border); overflow: hidden; }
|
|
168
|
+
.scene-admin-table { width: 100%; border-collapse: collapse; }
|
|
169
|
+
.scene-admin-table th, .scene-admin-table td { padding: var(--space-2) var(--space-3); border-bottom: var(--border-width-thin) solid var(--border2); text-align: left; font-size: var(--text-sm); color: var(--text); }
|
|
170
|
+
.scene-admin-table th { font-family: var(--font-mono); font-size: var(--text-label); letter-spacing: var(--tracking-wide); text-transform: uppercase; color: var(--text-dim); background: var(--surface2); }
|
|
171
|
+
.scene-admin-table tr:last-child td { border-bottom: 0; }
|
|
172
|
+
.scene-admin-pill {
|
|
173
|
+
display: inline-block;
|
|
174
|
+
padding: 1px 8px;
|
|
175
|
+
font-family: var(--font-mono);
|
|
176
|
+
font-size: 10px;
|
|
177
|
+
letter-spacing: var(--tracking-wide);
|
|
178
|
+
text-transform: uppercase;
|
|
179
|
+
border: var(--border-width-thin) solid var(--border2);
|
|
180
|
+
}
|
|
181
|
+
.scene-admin-pill.tone-green { color: var(--green); background: var(--green-bg); border-color: var(--green-border); }
|
|
182
|
+
.scene-admin-pill.tone-yellow { color: var(--yellow); background: var(--yellow-bg); border-color: var(--yellow-border); }
|
|
183
|
+
.scene-admin-pill.tone-red { color: var(--red); background: var(--red-bg); border-color: var(--red-border); }
|
|
184
|
+
|
|
185
|
+
@media (max-width: 540px) {
|
|
186
|
+
.scene-admin-side { width: 50px; }
|
|
187
|
+
.scene-admin-link span { display: none; }
|
|
188
|
+
}
|
|
189
|
+
</style>
|