Admin panel (warp-admin) and the apps/ui design system

apps/ui: Warp's design system for Vue 3 + Tailwind v4. Tokens, fonts,
icons and logo come from the brand package, with a Tailwind theme on the
tokens and Vue components on the brand component styles.

apps/admin: the admin panel as a separate, optional service. warp-admin
embeds the built Vue app and proxies only the login endpoints and
/_warp/admin/v1 to the homeserver. Pages: dashboard, users, rooms,
federation, registration and tokens, server notices, application
services, audit log.

Also: pnpm workspace, go.work entry, Dockerfile, an `admin` compose
profile, Makefile targets, a CI job with a proxy smoke test, and docs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
tajniak81andClaude Opus 5.5 committed 2026-10-18 15:05:16 +02:00
1 parent 487e0f410b
commit 7e5e38c921
137 files changed
+6895 -15

No files matched your search

+37
View File
@@ -0,0 +1,37 @@
<script setup lang="ts">
import { computed } from 'vue'
const props = withDefaults(
defineProps<{
/** Display name or Matrix ID; the initial is taken from it. */
name: string
src?: string
/** px */
size?: number
presence?: 'online' | 'away' | 'busy' | 'offline'
}>(),
{ src: undefined, size: 40, presence: undefined },
)
const initial = computed(() => {
const n = props.name.replace(/^[@#!+]/, '').trim()
return (Array.from(n)[0] ?? '?').toUpperCase()
})
const dot = computed(() => Math.max(8, Math.round(props.size / 4)))
</script>
<template>
<span
class="wp-avatar"
:style="{ width: `${size}px`, height: `${size}px`, fontSize: `${Math.round(size * 0.42)}px` }"
:title="name"
>
<img v-if="src" :src="src" alt="" />
<span v-else aria-hidden="true">{{ initial }}</span>
<span
v-if="presence"
:class="['wp-presence', `wp-presence-${presence}`]"
:style="{ width: `${dot}px`, height: `${dot}px` }"
/>
</span>
</template>
+24
View File
@@ -0,0 +1,24 @@
<script setup lang="ts">
import WIcon from './WIcon.vue'
import type { IconName } from '../icons'
withDefaults(
defineProps<{
tone?: 'neutral' | 'brand' | 'success' | 'warning' | 'danger' | 'info'
/** Unread count; renders a round counter, 99+ above 99. */
count?: number
icon?: IconName
}>(),
{ tone: 'neutral', count: undefined, icon: undefined },
)
</script>
<template>
<span :class="['wp-badge', `wp-badge-${tone}`, count !== undefined && 'wp-badge-count']">
<template v-if="count !== undefined">{{ count > 99 ? '99+' : count }}</template>
<template v-else>
<WIcon v-if="icon" :name="icon" :size="14" />
<slot />
</template>
</span>
</template>
+34
View File
@@ -0,0 +1,34 @@
<script setup lang="ts">
import { computed } from 'vue'
import WIcon from './WIcon.vue'
import type { IconName } from '../icons'
const props = withDefaults(
defineProps<{
tone?: 'info' | 'success' | 'warning' | 'danger'
title?: string
/** Defaults to the tone's own icon: status is never colour alone. */
icon?: IconName
}>(),
{ tone: 'info', title: undefined, icon: undefined },
)
const toneIcon: Record<'info' | 'success' | 'warning' | 'danger', IconName> = {
info: 'server',
success: 'shield-check',
warning: 'shield-alert',
danger: 'shield-alert',
}
const shown = computed(() => props.icon ?? toneIcon[props.tone])
</script>
<template>
<div :class="['wp-banner', `wp-banner-${tone}`]" :role="tone === 'danger' ? 'alert' : 'status'">
<WIcon :name="shown" />
<div class="wp-banner-body">
<p v-if="title" class="wp-banner-title">{{ title }}</p>
<p v-if="$slots.default" class="wp-banner-text"><slot /></p>
</div>
<div v-if="$slots.action" class="wp-banner-action"><slot name="action" /></div>
</div>
</template>
+33
View File
@@ -0,0 +1,33 @@
<script setup lang="ts">
import WIcon from './WIcon.vue'
import WSpinner from './WSpinner.vue'
import type { IconName } from '../icons'
withDefaults(
defineProps<{
/** One primary action per view. `danger` is the soft destructive style,
* `danger-solid` the final confirmation inside a dialog. */
variant?: 'primary' | 'secondary' | 'ghost' | 'danger' | 'danger-solid'
size?: 'md' | 'sm'
icon?: IconName
type?: 'button' | 'submit' | 'reset'
disabled?: boolean
/** Shows a spinner and disables the button while an action runs. */
loading?: boolean
}>(),
{ variant: 'secondary', size: 'md', icon: undefined, type: 'button', disabled: false, loading: false },
)
</script>
<template>
<button
:type="type"
:class="['wp-btn', `wp-btn-${size}`, `wp-btn-${variant}`]"
:disabled="disabled || loading"
:aria-busy="loading || undefined"
>
<WSpinner v-if="loading" :size="size === 'sm' ? 14 : 16" />
<WIcon v-else-if="icon" :name="icon" :size="size === 'sm' ? 16 : 18" />
<slot />
</button>
</template>
+12
View File
@@ -0,0 +1,12 @@
<script setup lang="ts">
defineProps<{ label: string; disabled?: boolean }>()
const model = defineModel<boolean>({ default: false })
</script>
<template>
<label class="wp-check">
<input v-model="model" type="checkbox" :disabled="disabled" />
<span>{{ label }}</span>
</label>
</template>
+69
View File
@@ -0,0 +1,69 @@
<script setup lang="ts">
import { nextTick, onBeforeUnmount, ref, useId, watch } from 'vue'
import WIconButton from './WIconButton.vue'
// A modal on the native <dialog>: the browser traps focus, closes it on
// Escape and gives focus back to whatever opened it.
const props = withDefaults(
defineProps<{
title: string
/** Wider body for tables and long forms. */
wide?: boolean
/** Keep the dialog open on Escape, e.g. while a request is running. */
persistent?: boolean
}>(),
{ wide: false, persistent: false },
)
const open = defineModel<boolean>('open', { default: false })
const el = ref<HTMLDialogElement>()
const titleId = useId()
watch(
open,
async (value) => {
await nextTick()
const d = el.value
if (!d) return
if (value && !d.open) d.showModal()
if (!value && d.open) d.close()
},
{ immediate: true },
)
function onCancel(e: Event) {
if (props.persistent) e.preventDefault()
else open.value = false
}
function onClose() {
open.value = false
}
onBeforeUnmount(() => {
if (el.value?.open) el.value.close()
})
</script>
<template>
<dialog
ref="el"
class="wp-dialog"
:style="wide ? { maxWidth: '720px' } : undefined"
:aria-labelledby="titleId"
@cancel="onCancel"
@close="onClose"
>
<template v-if="open">
<div class="wp-dialog-head">
<h2 :id="titleId" class="wp-dialog-title">{{ title }}</h2>
<WIconButton icon="close" label="Close" :disabled="persistent" @click="open = false" />
</div>
<slot />
<div v-if="$slots.actions" class="wp-dialog-actions">
<slot name="actions" />
</div>
</template>
</dialog>
</template>
+37
View File
@@ -0,0 +1,37 @@
<script setup lang="ts">
import { computed } from 'vue'
import { iconBody, type IconName } from '../icons'
const props = withDefaults(
defineProps<{
name: IconName
/** px */
size?: number
/** Accessible name; leave out for a decorative icon. */
label?: string
}>(),
{ size: 20, label: undefined },
)
const body = computed(() => iconBody(props.name))
</script>
<template>
<svg
class="wp-icon"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
:width="size"
:height="size"
fill="none"
stroke="currentColor"
stroke-width="1.75"
stroke-linecap="round"
stroke-linejoin="round"
:role="label ? 'img' : undefined"
:aria-label="label"
:aria-hidden="label ? undefined : 'true'"
focusable="false"
v-html="body"
/>
</template>
+28
View File
@@ -0,0 +1,28 @@
<script setup lang="ts">
import WIcon from './WIcon.vue'
import type { IconName } from '../icons'
withDefaults(
defineProps<{
icon: IconName
/** Required accessible name, also the tooltip. */
label: string
variant?: 'ghost' | 'solid'
type?: 'button' | 'submit'
disabled?: boolean
}>(),
{ variant: 'ghost', type: 'button', disabled: false },
)
</script>
<template>
<button
:type="type"
:class="['wp-iconbtn', variant === 'solid' && 'wp-iconbtn-solid']"
:aria-label="label"
:title="label"
:disabled="disabled"
>
<WIcon :name="icon" />
</button>
</template>
+36
View File
@@ -0,0 +1,36 @@
<script setup lang="ts">
import { computed } from 'vue'
import { symbolPath, symbolSmallPath, wordmarkPath, wordmarkTransform } from '../logos'
const props = withDefaults(
defineProps<{
/** "horizontal" (symbol + wordmark, the default lockup) or "symbol". */
variant?: 'horizontal' | 'symbol'
/** Rendered height in px. */
height?: number
/** One colour: the symbol takes CSS `color` too. */
mono?: boolean
}>(),
{ variant: 'horizontal', height: 32, mono: false },
)
const width = computed(() => (props.variant === 'horizontal' ? props.height * 3 : props.height))
// Under 32px the small cut keeps the gap between the frames open.
const symbol = computed(() => (props.variant === 'symbol' && props.height < 32 ? symbolSmallPath : symbolPath))
const symbolFill = computed(() => (props.mono ? 'currentColor' : 'var(--teal)'))
</script>
<template>
<svg
class="wp-logo"
xmlns="http://www.w3.org/2000/svg"
:viewBox="variant === 'horizontal' ? '0 0 768 256' : '0 0 256 256'"
:width="width"
:height="height"
role="img"
aria-label="Warp"
>
<path :fill="symbolFill" fill-rule="evenodd" :d="symbol" />
<path v-if="variant === 'horizontal'" fill="currentColor" :transform="wordmarkTransform" :d="wordmarkPath" />
</svg>
</template>
+24
View File
@@ -0,0 +1,24 @@
<script setup lang="ts">
import { useId } from 'vue'
defineOptions({ inheritAttrs: false })
defineProps<{
label: string
options: { value: string; label: string }[]
hint?: string
}>()
const model = defineModel<string>({ default: '' })
const id = useId()
</script>
<template>
<div class="wp-field">
<label class="wp-field-label" :for="id">{{ label }}</label>
<select :id="id" v-model="model" class="wp-select" v-bind="$attrs">
<option v-for="o in options" :key="o.value" :value="o.value">{{ o.label }}</option>
</select>
<p v-if="hint" class="wp-field-hint">{{ hint }}</p>
</div>
</template>
+13
View File
@@ -0,0 +1,13 @@
<script setup lang="ts">
withDefaults(defineProps<{ size?: number; label?: string }>(), { size: 20, label: undefined })
</script>
<template>
<span
class="wp-spinner"
:style="{ width: `${size}px`, height: `${size}px` }"
:role="label ? 'status' : undefined"
:aria-label="label"
:aria-hidden="label ? undefined : 'true'"
/>
</template>
+21
View File
@@ -0,0 +1,21 @@
<script setup lang="ts">
defineProps<{
/** Accessible name when no visible label is tied to the switch. */
label?: string
disabled?: boolean
}>()
const model = defineModel<boolean>({ default: false })
</script>
<template>
<button
type="button"
role="switch"
class="wp-switch"
:aria-checked="model ? 'true' : 'false'"
:aria-label="label"
:disabled="disabled"
@click="model = !model"
/>
</template>
+37
View File
@@ -0,0 +1,37 @@
<script setup lang="ts">
import { useId } from 'vue'
import WIcon from './WIcon.vue'
defineOptions({ inheritAttrs: false })
withDefaults(
defineProps<{
label: string
hint?: string
error?: string
mono?: boolean
}>(),
{ hint: undefined, error: undefined, mono: false },
)
const model = defineModel<string>({ default: '' })
const id = useId()
</script>
<template>
<div :class="['wp-field wp-field-wide', error && 'wp-field-error']">
<label class="wp-field-label" :for="id">{{ label }}</label>
<textarea
:id="id"
v-model="model"
:class="['wp-textarea', mono && 'wp-input-mono']"
:aria-invalid="error ? 'true' : undefined"
:aria-describedby="error || hint ? `${id}-hint` : undefined"
v-bind="$attrs"
/>
<p v-if="error || hint" :id="`${id}-hint`" class="wp-field-hint">
<WIcon v-if="error" name="shield-alert" :size="14" />
{{ error || hint }}
</p>
</div>
</template>
+40
View File
@@ -0,0 +1,40 @@
<script setup lang="ts">
import { useId } from 'vue'
import WIcon from './WIcon.vue'
defineOptions({ inheritAttrs: false })
withDefaults(
defineProps<{
label: string
hint?: string
error?: string
/** Monospace input for Matrix IDs, server names, keys. */
mono?: boolean
/** Fill the container instead of stopping at 360px. */
wide?: boolean
}>(),
{ hint: undefined, error: undefined, mono: false, wide: false },
)
const model = defineModel<string | number>({ default: '' })
const id = useId()
</script>
<template>
<div :class="['wp-field', error && 'wp-field-error', wide && 'wp-field-wide']">
<label class="wp-field-label" :for="id">{{ label }}</label>
<input
:id="id"
v-model="model"
:class="['wp-input', mono && 'wp-input-mono']"
:aria-invalid="error ? 'true' : undefined"
:aria-describedby="error || hint ? `${id}-hint` : undefined"
v-bind="$attrs"
/>
<p v-if="error || hint" :id="`${id}-hint`" class="wp-field-hint">
<WIcon v-if="error" name="shield-alert" :size="14" />
{{ error || hint }}
</p>
</div>
</template>