feat(dashboard): auto-slug app names and infer route host kind from input
Two related polish passes on forms the operator hits most. App create form: the slug field used to come before the name field and demanded the operator hand-roll a valid slug. Now the name field comes first and the slug is derived from it live, GitLab-style — Unicode NFKD-decomposed, combining marks stripped (so `Café` → `cafe`), `ß` mapped to `ss`, non-`[a-z0-9]` runs collapsed to `-`, trimmed and capped at the backend's 63-char limit. The auto-sync releases as soon as the operator edits the slug manually, and re-engages if they clear it. The slug input itself runs every keystroke and paste through the same normalizer, so dirty input never reaches the form state. Route create form: the three-way host-kind `<select>` plus a sometimes- disabled input was confusing — operators routinely picked the wrong kind, typed a host the app didn't claim, and only saw the error after hitting Create. Replace with a single text input that infers the kind from what's there (`*` → any, `*.foo.com` → wildcard, `foo.com` → strict), shows the detected kind as a colored chip beside the field, and suggests the app's existing domain claims via a `<datalist>`. The same matching logic the backend runs in `validate_route_host_against_app` now lives in `route-utils.ts` so the form can surface a soft "not covered by any claim" warning *before* submit. Path also pre-fills to `/` so the most common case is one click away. Lockfile drift from `npm install` (pre-existing 0.5.0 → 0.5.1 version sync, npm metadata cleanup) is folded in here since it surfaced during this work. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { api, ApiError, type App } from '$lib/api';
|
||||
import { slugify, SLUG_MAX } from '$lib/slugify';
|
||||
|
||||
let apps = $state<App[] | null>(null);
|
||||
let listError = $state<string | null>(null);
|
||||
@@ -10,10 +11,34 @@
|
||||
let createSlug = $state('');
|
||||
let createName = $state('');
|
||||
let createDescription = $state('');
|
||||
// Auto-derive slug from name until the user takes manual control of
|
||||
// the slug field. Clearing the slug input releases the lock so the
|
||||
// auto-derive resumes — matches the GitLab project-create UX.
|
||||
let slugTouched = $state(false);
|
||||
let creating = $state(false);
|
||||
let createError = $state<string | null>(null);
|
||||
let createHistoricalConflict = $state<App | null>(null);
|
||||
|
||||
function onNameInput(event: Event) {
|
||||
const value = (event.target as HTMLInputElement).value;
|
||||
createName = value;
|
||||
if (!slugTouched) {
|
||||
createSlug = slugify(value);
|
||||
}
|
||||
}
|
||||
|
||||
function onSlugInput(event: Event) {
|
||||
const raw = (event.target as HTMLInputElement).value;
|
||||
const normalized = slugify(raw);
|
||||
createSlug = normalized;
|
||||
// Re-sync the input element so a paste of "Hello World!" shows
|
||||
// "hello-world" immediately, not the raw value.
|
||||
if (raw !== normalized) {
|
||||
(event.target as HTMLInputElement).value = normalized;
|
||||
}
|
||||
slugTouched = normalized.length > 0;
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading = true;
|
||||
listError = null;
|
||||
@@ -33,6 +58,7 @@
|
||||
createDescription = '';
|
||||
createError = null;
|
||||
createHistoricalConflict = null;
|
||||
slugTouched = false;
|
||||
}
|
||||
|
||||
async function submitCreate(event: Event, forceTakeover = false) {
|
||||
@@ -88,17 +114,28 @@
|
||||
<form class="create-form" onsubmit={(e) => submitCreate(e)}>
|
||||
<div class="row">
|
||||
<label>
|
||||
<span>Slug</span>
|
||||
<span>Name</span>
|
||||
<input
|
||||
bind:value={createSlug}
|
||||
value={createName}
|
||||
oninput={onNameInput}
|
||||
required
|
||||
pattern="[a-z0-9][a-z0-9-]*"
|
||||
placeholder="my-app"
|
||||
placeholder="My App"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>Name</span>
|
||||
<input bind:value={createName} required placeholder="My App" />
|
||||
<span>Slug</span>
|
||||
<input
|
||||
value={createSlug}
|
||||
oninput={onSlugInput}
|
||||
required
|
||||
pattern="[a-z0-9][a-z0-9-]*"
|
||||
maxlength={SLUG_MAX}
|
||||
placeholder="my-app"
|
||||
autocomplete="off"
|
||||
autocapitalize="off"
|
||||
autocorrect="off"
|
||||
spellcheck="false"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<label>
|
||||
|
||||
Reference in New Issue
Block a user