popilot 0.6.0 → 0.7.0
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/bin/cli.mjs +204 -2
- package/lib/doctor.mjs +38 -1
- package/lib/hydrate.mjs +15 -0
- package/lib/scaffold.mjs +5 -0
- package/lib/setup-wizard.mjs +35 -2
- package/package.json +1 -1
- package/scaffold/.context/project.yaml.example +19 -0
- package/scaffold/mcp-pm/package.json +19 -0
- package/scaffold/mcp-pm/src/api-client.ts +69 -0
- package/scaffold/mcp-pm/src/index.ts +660 -0
- package/scaffold/mcp-pm/tsconfig.json +14 -0
- package/scaffold/pm-api/package.json +21 -0
- package/scaffold/pm-api/sql/schema-core.sql +331 -0
- package/scaffold/pm-api/sql/schema-docs.sql +25 -0
- package/scaffold/pm-api/sql/schema-meetings.sql +17 -0
- package/scaffold/pm-api/sql/schema-rewards.sql +16 -0
- package/scaffold/pm-api/src/auth.ts +28 -0
- package/scaffold/pm-api/src/blockchain/adapter.ts +20 -0
- package/scaffold/pm-api/src/blockchain/tron.ts +62 -0
- package/scaffold/pm-api/src/db/adapter.ts +36 -0
- package/scaffold/pm-api/src/db/turso.ts +147 -0
- package/scaffold/pm-api/src/index.ts +114 -0
- package/scaffold/pm-api/src/mcp-tools/dashboard.ts +40 -0
- package/scaffold/pm-api/src/mcp-tools/epic.ts +67 -0
- package/scaffold/pm-api/src/mcp-tools/event.ts +89 -0
- package/scaffold/pm-api/src/mcp-tools/index.ts +11 -0
- package/scaffold/pm-api/src/mcp-tools/initiative.ts +51 -0
- package/scaffold/pm-api/src/mcp-tools/memo.ts +164 -0
- package/scaffold/pm-api/src/mcp-tools/notification.ts +37 -0
- package/scaffold/pm-api/src/mcp-tools/retro.ts +183 -0
- package/scaffold/pm-api/src/mcp-tools/sprint.ts +204 -0
- package/scaffold/pm-api/src/mcp-tools/standup.ts +136 -0
- package/scaffold/pm-api/src/mcp-tools/story.ts +230 -0
- package/scaffold/pm-api/src/mcp-tools/task.ts +187 -0
- package/scaffold/pm-api/src/mcp-tools/utils.ts +83 -0
- package/scaffold/pm-api/src/mcp.ts +871 -0
- package/scaffold/pm-api/src/nudge.ts +283 -0
- package/scaffold/pm-api/src/routes/auth.ts +32 -0
- package/scaffold/pm-api/src/routes/v2-activity.ts +27 -0
- package/scaffold/pm-api/src/routes/v2-admin.ts +165 -0
- package/scaffold/pm-api/src/routes/v2-dashboard.ts +189 -0
- package/scaffold/pm-api/src/routes/v2-docs.ts +34 -0
- package/scaffold/pm-api/src/routes/v2-initiatives.ts +118 -0
- package/scaffold/pm-api/src/routes/v2-kickoff.ts +265 -0
- package/scaffold/pm-api/src/routes/v2-meetings.ts +324 -0
- package/scaffold/pm-api/src/routes/v2-memos.ts +257 -0
- package/scaffold/pm-api/src/routes/v2-nav.ts +260 -0
- package/scaffold/pm-api/src/routes/v2-notifications.ts +79 -0
- package/scaffold/pm-api/src/routes/v2-page-content.ts +35 -0
- package/scaffold/pm-api/src/routes/v2-pm.ts +380 -0
- package/scaffold/pm-api/src/routes/v2-policy.ts +58 -0
- package/scaffold/pm-api/src/routes/v2-retro.ts +221 -0
- package/scaffold/pm-api/src/routes/v2-rewards.ts +132 -0
- package/scaffold/pm-api/src/routes/v2-scenarios.ts +48 -0
- package/scaffold/pm-api/src/routes/v2-search.ts +32 -0
- package/scaffold/pm-api/src/routes/v2-standup.ts +127 -0
- package/scaffold/pm-api/src/routes/v2-user.ts +38 -0
- package/scaffold/pm-api/src/types.ts +11 -0
- package/scaffold/pm-api/src/utils/activity.ts +22 -0
- package/scaffold/pm-api/src/utils/admin.ts +9 -0
- package/scaffold/pm-api/src/utils/agent-notify.ts +62 -0
- package/scaffold/pm-api/src/utils/assignee.ts +69 -0
- package/scaffold/pm-api/src/utils/db.ts +45 -0
- package/scaffold/pm-api/src/utils/initiative.ts +23 -0
- package/scaffold/pm-api/src/utils/sprint-lifecycle.ts +96 -0
- package/scaffold/pm-api/tsconfig.json +15 -0
- package/scaffold/pm-api/wrangler.toml.hbs +11 -0
- package/scaffold/spec-site/package-lock.json +40 -0
- package/scaffold/spec-site/package.json +4 -1
- package/scaffold/spec-site/src/api/types.ts +6 -0
- package/scaffold/spec-site/src/components/AppHeader.vue +429 -55
- package/scaffold/spec-site/src/components/MemberSelect.vue +48 -0
- package/scaffold/spec-site/src/components/NotificationDropdown.vue +116 -0
- package/scaffold/spec-site/src/components/SearchModal.vue +102 -0
- package/scaffold/spec-site/src/components/VelocityChart.vue +77 -0
- package/scaffold/spec-site/src/composables/pmTypes.ts +15 -2
- package/scaffold/spec-site/src/composables/useDashboard.ts +221 -0
- package/scaffold/spec-site/src/composables/useMediaQuery.ts +28 -0
- package/scaffold/spec-site/src/composables/useNotification.ts +200 -0
- package/scaffold/spec-site/src/composables/usePmStore.ts +48 -1
- package/scaffold/spec-site/src/composables/useRetro.ts +6 -0
- package/scaffold/spec-site/src/composables/useStandup.ts +201 -0
- package/scaffold/spec-site/src/composables/useTheme.ts +37 -0
- package/scaffold/spec-site/src/composables/useUser.ts +19 -1
- package/scaffold/spec-site/src/features.ts +108 -0
- package/scaffold/spec-site/src/pages/AdminPage.vue +299 -0
- package/scaffold/spec-site/src/pages/DashboardPage.vue +650 -0
- package/scaffold/spec-site/src/pages/DocsHub.vue +157 -0
- package/scaffold/spec-site/src/pages/InboxPage.vue +156 -0
- package/scaffold/spec-site/src/pages/MeetingsPage.vue +294 -0
- package/scaffold/spec-site/src/pages/MyPage.vue +343 -0
- package/scaffold/spec-site/src/pages/RewardsPage.vue +266 -0
- package/scaffold/spec-site/src/pages/board/BoardAdmin.vue +422 -0
- package/scaffold/spec-site/src/pages/board/BoardEpicSection.vue +54 -0
- package/scaffold/spec-site/src/pages/board/BoardPage.vue +884 -0
- package/scaffold/spec-site/src/pages/board/BoardStoryCard.vue +67 -0
- package/scaffold/spec-site/src/pages/board/BoardTaskItem.vue +52 -0
- package/scaffold/spec-site/src/pages/board/MyTasksPage.vue +202 -0
- package/scaffold/spec-site/src/pages/board/SprintClose.vue +167 -0
- package/scaffold/spec-site/src/pages/board/SprintColumn.vue +49 -0
- package/scaffold/spec-site/src/pages/board/SprintKickoff.vue +389 -0
- package/scaffold/spec-site/src/pages/board/StatusBadge.vue +52 -0
- package/scaffold/spec-site/src/pages/board/StoryDetailPanel.vue +495 -0
- package/scaffold/spec-site/src/pages/board/TaskCard.vue +42 -0
- package/scaffold/spec-site/src/pages/retro/RetroCard.vue +36 -2
- package/scaffold/spec-site/src/pages/retro/RetroHeader.vue +82 -66
- package/scaffold/spec-site/src/pages/retro/RetroPage.vue +47 -18
- package/scaffold/spec-site/src/pages/standup/StandupEntryCard.vue +551 -0
- package/scaffold/spec-site/src/pages/standup/StandupForm.vue +68 -0
- package/scaffold/spec-site/src/pages/standup/StandupList.vue +71 -0
- package/scaffold/spec-site/src/pages/standup/StandupPage.vue +225 -0
- package/scaffold/spec-site/src/router.ts +141 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, onMounted, computed } from 'vue'
|
|
3
|
+
import { apiGet, apiPost, apiPatch, apiDelete, apiPut } from '@/api/client'
|
|
4
|
+
|
|
5
|
+
interface MemberRow {
|
|
6
|
+
id: number
|
|
7
|
+
display_name: string
|
|
8
|
+
email: string | null
|
|
9
|
+
role: string
|
|
10
|
+
is_active: number
|
|
11
|
+
created_at: string
|
|
12
|
+
updated_at: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const members = ref<MemberRow[]>([])
|
|
16
|
+
const loading = ref(true)
|
|
17
|
+
const error = ref('')
|
|
18
|
+
|
|
19
|
+
// New member form
|
|
20
|
+
const newName = ref('')
|
|
21
|
+
const newEmail = ref('')
|
|
22
|
+
const newTtlDays = ref<number | null>(null)
|
|
23
|
+
|
|
24
|
+
// Status message
|
|
25
|
+
const statusMsg = ref('')
|
|
26
|
+
|
|
27
|
+
// LLM settings
|
|
28
|
+
const llmApiKey = ref('')
|
|
29
|
+
const llmProvider = ref('openai')
|
|
30
|
+
const llmModel = ref('')
|
|
31
|
+
const settingsSaved = ref(false)
|
|
32
|
+
|
|
33
|
+
async function loadSettings() {
|
|
34
|
+
const { data } = await apiGet<{ settings: Record<string, string> }>('/api/v2/admin/settings')
|
|
35
|
+
if (data?.settings) {
|
|
36
|
+
llmApiKey.value = data.settings.llm_api_key ?? ''
|
|
37
|
+
llmProvider.value = data.settings.llm_provider ?? 'openai'
|
|
38
|
+
llmModel.value = data.settings.llm_model ?? ''
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function saveSettings() {
|
|
43
|
+
settingsSaved.value = false
|
|
44
|
+
await apiPut('/api/v2/admin/settings/llm_api_key', { value: llmApiKey.value || null })
|
|
45
|
+
await apiPut('/api/v2/admin/settings/llm_provider', { value: llmProvider.value || null })
|
|
46
|
+
await apiPut('/api/v2/admin/settings/llm_model', { value: llmModel.value || null })
|
|
47
|
+
settingsSaved.value = true
|
|
48
|
+
setTimeout(() => { settingsSaved.value = false }, 3000)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function clearApiKey() {
|
|
52
|
+
await apiPut('/api/v2/admin/settings/llm_api_key', { value: null })
|
|
53
|
+
llmApiKey.value = ''
|
|
54
|
+
settingsSaved.value = true
|
|
55
|
+
setTimeout(() => { settingsSaved.value = false }, 3000)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
loadSettings()
|
|
59
|
+
|
|
60
|
+
async function loadMembers() {
|
|
61
|
+
loading.value = true
|
|
62
|
+
const { data, error: apiError } = await apiGet<{ members: MemberRow[] }>('/api/v2/admin/members')
|
|
63
|
+
if (apiError) {
|
|
64
|
+
error.value = apiError
|
|
65
|
+
} else if (data) {
|
|
66
|
+
members.value = data.members
|
|
67
|
+
}
|
|
68
|
+
loading.value = false
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function generateToken(): string {
|
|
72
|
+
return crypto.randomUUID()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function addMember() {
|
|
76
|
+
const name = newName.value.trim()
|
|
77
|
+
if (!name) return
|
|
78
|
+
const token = generateToken()
|
|
79
|
+
const email = newEmail.value.trim() || null
|
|
80
|
+
const ttl = newTtlDays.value
|
|
81
|
+
const body: Record<string, unknown> = { token, userName: name, userEmail: email }
|
|
82
|
+
if (ttl && Number.isInteger(ttl) && ttl > 0 && ttl <= 3650) body.ttlDays = ttl
|
|
83
|
+
const { error: apiError } = await apiPost('/api/v2/admin/members', body)
|
|
84
|
+
if (apiError) {
|
|
85
|
+
statusMsg.value = `Error: ${apiError}`
|
|
86
|
+
} else {
|
|
87
|
+
statusMsg.value = `${name} added`
|
|
88
|
+
newName.value = ''
|
|
89
|
+
newEmail.value = ''
|
|
90
|
+
newTtlDays.value = null
|
|
91
|
+
await loadMembers()
|
|
92
|
+
}
|
|
93
|
+
clearStatus()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function revokeToken(id: string, name: string) {
|
|
97
|
+
if (!confirm(`Revoke token for ${name}?`)) return
|
|
98
|
+
const { error: apiError } = await apiPatch(`/api/v2/admin/members/${id}/revoke`, {})
|
|
99
|
+
if (!apiError) { statusMsg.value = `${name} token revoked`; await loadMembers() }
|
|
100
|
+
clearStatus()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function reactivateToken(id: string, name: string) {
|
|
104
|
+
const { error: apiError } = await apiPatch(`/api/v2/admin/members/${id}/activate`, {})
|
|
105
|
+
if (!apiError) { statusMsg.value = `${name} token reactivated`; await loadMembers() }
|
|
106
|
+
clearStatus()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function regenerateToken(oldToken: string, name: string) {
|
|
110
|
+
if (!confirm(`Regenerate token for ${name}? The old token will be invalidated.`)) return
|
|
111
|
+
const newToken = generateToken()
|
|
112
|
+
const { error: apiError } = await apiPost(`/api/v2/admin/members/${oldToken}/regenerate`, { newToken })
|
|
113
|
+
if (!apiError) { statusMsg.value = `${name} token regenerated`; await loadMembers() }
|
|
114
|
+
clearStatus()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function deleteMember(id: string, name: string) {
|
|
118
|
+
if (!confirm(`Permanently delete ${name}? This cannot be undone.`)) return
|
|
119
|
+
const { error: apiError } = await apiDelete(`/api/v2/admin/members/${id}`)
|
|
120
|
+
if (!apiError) { statusMsg.value = `${name} deleted`; await loadMembers() }
|
|
121
|
+
clearStatus()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function clearStatus() {
|
|
125
|
+
setTimeout(() => { statusMsg.value = '' }, 3000)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function formatDate(d: string | null): string {
|
|
129
|
+
if (!d) return '-'
|
|
130
|
+
return d.replace('T', ' ').substring(0, 16)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const activeCount = computed(() => members.value.filter(m => m.is_active).length)
|
|
134
|
+
const totalCount = computed(() => members.value.length)
|
|
135
|
+
|
|
136
|
+
onMounted(loadMembers)
|
|
137
|
+
</script>
|
|
138
|
+
|
|
139
|
+
<template>
|
|
140
|
+
<div class="admin">
|
|
141
|
+
<div class="admin-header">
|
|
142
|
+
<div class="admin-header-row">
|
|
143
|
+
<div>
|
|
144
|
+
<h1>Team Token Management</h1>
|
|
145
|
+
<p class="admin-subtitle">This page is accessible only to administrators</p>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<!-- Status -->
|
|
151
|
+
<Transition name="fade">
|
|
152
|
+
<div v-if="statusMsg" class="admin-status">{{ statusMsg }}</div>
|
|
153
|
+
</Transition>
|
|
154
|
+
|
|
155
|
+
<!-- Stats -->
|
|
156
|
+
<div class="admin-stats">
|
|
157
|
+
<div class="stat">
|
|
158
|
+
<span class="stat-num">{{ activeCount }}</span>
|
|
159
|
+
<span class="stat-label">Active</span>
|
|
160
|
+
</div>
|
|
161
|
+
<div class="stat">
|
|
162
|
+
<span class="stat-num">{{ totalCount }}</span>
|
|
163
|
+
<span class="stat-label">Total</span>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<!-- Add Member -->
|
|
168
|
+
<div class="admin-card">
|
|
169
|
+
<h2>Add Member</h2>
|
|
170
|
+
<div class="add-form">
|
|
171
|
+
<input v-model="newName" class="input" placeholder="Name" />
|
|
172
|
+
<input v-model="newEmail" class="input" placeholder="Email (optional)" />
|
|
173
|
+
<input v-model.number="newTtlDays" class="input input--sm" type="number" placeholder="TTL (days)" min="1" />
|
|
174
|
+
<button class="btn btn--primary" @click="addMember" :disabled="!newName.trim()">Add</button>
|
|
175
|
+
</div>
|
|
176
|
+
<p class="add-hint">Leave TTL empty for unlimited</p>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<!-- Members Table -->
|
|
180
|
+
<div class="admin-card">
|
|
181
|
+
<h2>Members</h2>
|
|
182
|
+
<div v-if="loading" class="admin-loading">Loading...</div>
|
|
183
|
+
<div v-else-if="error" class="admin-error">{{ error }}</div>
|
|
184
|
+
<div v-else class="table-wrap">
|
|
185
|
+
<table>
|
|
186
|
+
<thead>
|
|
187
|
+
<tr>
|
|
188
|
+
<th>Status</th>
|
|
189
|
+
<th>Name</th>
|
|
190
|
+
<th>Email</th>
|
|
191
|
+
<th>Role</th>
|
|
192
|
+
<th>Created</th>
|
|
193
|
+
<th>Actions</th>
|
|
194
|
+
</tr>
|
|
195
|
+
</thead>
|
|
196
|
+
<tbody>
|
|
197
|
+
<tr v-for="m in members" :key="String(m.id)" :class="{ inactive: !m.is_active }">
|
|
198
|
+
<td>
|
|
199
|
+
<span class="badge" :class="m.is_active ? 'badge--active' : 'badge--revoked'">
|
|
200
|
+
{{ m.is_active ? 'active' : 'revoked' }}
|
|
201
|
+
</span>
|
|
202
|
+
</td>
|
|
203
|
+
<td class="td-name">{{ m.display_name }}</td>
|
|
204
|
+
<td class="td-email">{{ m.email || '-' }}</td>
|
|
205
|
+
<td>{{ m.role }}</td>
|
|
206
|
+
<td class="td-date">{{ formatDate(m.created_at) }}</td>
|
|
207
|
+
<td class="td-actions">
|
|
208
|
+
<button v-if="m.is_active" class="btn btn--sm btn--warn" @click="revokeToken(String(m.id), m.display_name)">Revoke</button>
|
|
209
|
+
<button v-else class="btn btn--sm btn--ok" @click="reactivateToken(String(m.id), m.display_name)">Activate</button>
|
|
210
|
+
<button class="btn btn--sm" @click="regenerateToken(String(m.id), m.display_name)">Regen</button>
|
|
211
|
+
<button class="btn btn--sm btn--danger" @click="deleteMember(String(m.id), m.display_name)">Delete</button>
|
|
212
|
+
</td>
|
|
213
|
+
</tr>
|
|
214
|
+
</tbody>
|
|
215
|
+
</table>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
<!-- AI Settings -->
|
|
220
|
+
<div class="admin-section">
|
|
221
|
+
<h2>AI Settings (BYOM)</h2>
|
|
222
|
+
<div class="setting-row">
|
|
223
|
+
<label>API Key</label>
|
|
224
|
+
<input v-model="llmApiKey" type="password" class="setting-input" placeholder="sk-..." />
|
|
225
|
+
</div>
|
|
226
|
+
<div class="setting-row">
|
|
227
|
+
<label>Provider</label>
|
|
228
|
+
<select v-model="llmProvider" class="setting-input">
|
|
229
|
+
<option value="openai">OpenAI</option>
|
|
230
|
+
<option value="anthropic">Anthropic</option>
|
|
231
|
+
<option value="gemini">Gemini</option>
|
|
232
|
+
</select>
|
|
233
|
+
</div>
|
|
234
|
+
<div class="setting-row">
|
|
235
|
+
<label>Model</label>
|
|
236
|
+
<input v-model="llmModel" class="setting-input" placeholder="gpt-4o-mini" />
|
|
237
|
+
</div>
|
|
238
|
+
<div class="setting-actions">
|
|
239
|
+
<button class="btn btn--primary" @click="saveSettings">Save</button>
|
|
240
|
+
<button class="btn btn--danger" @click="clearApiKey">Clear API Key</button>
|
|
241
|
+
<span v-if="settingsSaved" class="save-ok">Saved</span>
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
</template>
|
|
246
|
+
|
|
247
|
+
<style scoped>
|
|
248
|
+
.admin { max-width: 1000px; margin: 0 auto; padding: 32px 24px; }
|
|
249
|
+
.admin-header { margin-bottom: 24px; }
|
|
250
|
+
.admin-header-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
|
|
251
|
+
.admin-header h1 { font-size: 24px; font-weight: 700; color: #1e293b; margin-bottom: 4px; }
|
|
252
|
+
.admin-subtitle { font-size: 13px; color: #94a3b8; }
|
|
253
|
+
.admin-status { background: #ecfdf5; border: 1px solid #a7f3d0; color: #065f46; padding: 10px 16px; border-radius: 8px; font-size: 13px; font-weight: 500; margin-bottom: 16px; }
|
|
254
|
+
.admin-stats { display: flex; gap: 16px; margin-bottom: 24px; }
|
|
255
|
+
.stat { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 10px; padding: 16px 24px; display: flex; flex-direction: column; align-items: center; min-width: 80px; }
|
|
256
|
+
.stat-num { font-size: 28px; font-weight: 700; color: #1e293b; }
|
|
257
|
+
.stat-label { font-size: 12px; color: #94a3b8; margin-top: 2px; }
|
|
258
|
+
.admin-card { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; padding: 20px 24px; margin-bottom: 20px; }
|
|
259
|
+
.admin-card h2 { font-size: 16px; font-weight: 600; color: #1e293b; margin-bottom: 16px; }
|
|
260
|
+
.add-form { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
261
|
+
.input { padding: 8px 12px; border: 1px solid #e2e8f0; border-radius: 6px; font-size: 13px; flex: 1; min-width: 120px; }
|
|
262
|
+
.input--sm { max-width: 100px; flex: none; }
|
|
263
|
+
.input:focus { outline: none; border-color: #3b82f6; }
|
|
264
|
+
.add-hint { font-size: 11px; color: #94a3b8; margin-top: 8px; }
|
|
265
|
+
.btn { padding: 8px 16px; border: 1px solid #e2e8f0; border-radius: 6px; font-size: 13px; font-weight: 500; cursor: pointer; background: #fff; color: #475569; white-space: nowrap; transition: all 0.15s; }
|
|
266
|
+
.btn:hover { background: #f1f5f9; }
|
|
267
|
+
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
268
|
+
.btn--primary { background: #1e293b; color: #fff; border-color: #1e293b; }
|
|
269
|
+
.btn--primary:hover { background: #334155; }
|
|
270
|
+
.btn--sm { padding: 4px 10px; font-size: 11px; }
|
|
271
|
+
.btn--warn { color: #f59e0b; border-color: #fcd34d; }
|
|
272
|
+
.btn--ok { color: #22c55e; border-color: #86efac; }
|
|
273
|
+
.btn--danger { color: #ef4444; border-color: #fca5a5; }
|
|
274
|
+
.table-wrap { overflow-x: auto; }
|
|
275
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
276
|
+
th { text-align: left; padding: 8px 10px; font-size: 11px; font-weight: 600; color: #64748b; text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 2px solid #e2e8f0; }
|
|
277
|
+
td { padding: 10px; border-bottom: 1px solid #f1f5f9; color: #475569; vertical-align: middle; }
|
|
278
|
+
tr.inactive td { opacity: 0.5; }
|
|
279
|
+
tr:hover td { background: #f8fafc; }
|
|
280
|
+
.td-name { font-weight: 600; color: #1e293b; }
|
|
281
|
+
.td-email { font-size: 12px; }
|
|
282
|
+
.td-date { font-size: 11px; color: #94a3b8; white-space: nowrap; }
|
|
283
|
+
.td-actions { display: flex; gap: 4px; flex-wrap: nowrap; }
|
|
284
|
+
.badge { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 10px; font-weight: 600; text-transform: uppercase; }
|
|
285
|
+
.badge--active { background: #ecfdf5; color: #059669; }
|
|
286
|
+
.badge--revoked { background: #fef2f2; color: #dc2626; }
|
|
287
|
+
.admin-loading, .admin-error { padding: 20px; text-align: center; color: #94a3b8; font-size: 14px; }
|
|
288
|
+
.admin-error { color: #ef4444; }
|
|
289
|
+
.admin-section { margin-top: 32px; padding: 20px; background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; }
|
|
290
|
+
.admin-section h2 { font-size: 16px; font-weight: 600; margin-bottom: 16px; }
|
|
291
|
+
.setting-row { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; }
|
|
292
|
+
.setting-row label { width: 100px; font-size: 13px; font-weight: 500; color: #64748b; flex-shrink: 0; }
|
|
293
|
+
.setting-input { flex: 1; padding: 8px 12px; border: 1px solid rgba(0,0,0,0.08); border-radius: 8px; font-size: 13px; }
|
|
294
|
+
.setting-actions { display: flex; gap: 8px; align-items: center; }
|
|
295
|
+
.save-ok { font-size: 12px; color: #16a34a; }
|
|
296
|
+
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s; }
|
|
297
|
+
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
|
298
|
+
@media (max-width: 767px) { .admin { padding: 16px; } .add-form { flex-direction: column; } .td-actions { flex-wrap: wrap; } }
|
|
299
|
+
</style>
|