feat(web): adopt Pierre theme palette and reorder sign-in tab stops (#8287)

This commit is contained in:
ᴊᴏᴇ ᴄʜᴇɴ
2026-05-22 11:19:38 -04:00
committed by GitHub
parent e7d0cb646d
commit d54f98f5a4
6 changed files with 93 additions and 45 deletions
+2
View File
@@ -3,6 +3,8 @@
dist/
.moon/cache/
node_modules/
.agents/skills/
.claude/skills
# Runtime data
log/
+23
View File
@@ -71,6 +71,11 @@ func Toggle(options *ToggleOptions) macaron.Handler {
return
}
if isWebPath(c.Req.URL.Path) {
c.ServeWeb()
return
}
c.SetCookie("redirect_to", url.QueryEscape(conf.Server.Subpath+c.Req.RequestURI), 0, conf.Server.Subpath)
c.RedirectSubpath("/user/sign-in")
return
@@ -84,6 +89,10 @@ func Toggle(options *ToggleOptions) macaron.Handler {
// Redirect to log in page if auto-signin info is provided and has not signed in.
if !options.SignOutRequired && !c.IsLogged && !isAPIPath(c.Req.URL.Path) &&
len(c.GetCookie(conf.Security.CookieUsername)) > 0 {
if isWebPath(c.Req.URL.Path) {
c.ServeWeb()
return
}
c.SetCookie("redirect_to", url.QueryEscape(conf.Server.Subpath+c.Req.RequestURI), 0, conf.Server.Subpath)
c.RedirectSubpath("/user/sign-in")
return
@@ -103,6 +112,20 @@ func isAPIPath(url string) bool {
return strings.HasPrefix(url, "/api/")
}
func isWebPath(p string) bool {
p = strings.TrimPrefix(p, conf.Server.Subpath)
switch {
case p == "/user/sign-in",
strings.HasPrefix(p, "/assets/"),
strings.HasPrefix(p, "/src/"),
strings.HasPrefix(p, "/node_modules/"),
strings.HasPrefix(p, "/@"),
strings.HasPrefix(p, "/img/"):
return true
}
return false
}
type AuthStore interface {
// GetAccessTokenBySHA1 returns the access token with given SHA1. It returns
// database.ErrAccessTokenNotExist when not found.
+11
View File
@@ -0,0 +1,11 @@
{
"version": 1,
"skills": {
"shadcn": {
"source": "shadcn/ui",
"sourceType": "github",
"skillPath": "skills/shadcn/SKILL.md",
"computedHash": "80a6226e78f6d1fe464214ae0ef449d49d8ffaa3e7704f011e9b418c678ad4d1"
}
}
}
+3 -3
View File
@@ -17,7 +17,7 @@ Don't mix sans and mono within the same UI surface for arbitrary reasons. If a c
## Color hierarchy
Palettes are derived from [Happy Hues](https://www.happyhues.co): palette 6 for light, palette 13 for dark. Dark mode is opt-in via the `.dark` class on `:root` (see `lib/theme.ts`), not media-query driven, so the user's stored preference always wins. The `@custom-variant dark` rule in `index.css` lets utilities like `dark:...` target the same class.
Palettes are adapted from [Pierre Theme](https://github.com/pierrecomputer/theme)'s "Light" and "Dark" (non-soft) variants. The dark-mode input background is bumped slightly above Pierre's value (`#262626` instead of `#1d1d1d`) so form fields read as edged elements outside an IDE panel context. Dark mode is opt-in via the `.dark` class on `:root` (see `lib/theme.ts`), not media-query driven, so the user's stored preference always wins. The `@custom-variant dark` rule in `index.css` lets utilities like `dark:...` target the same class.
Use these tokens. Don't introduce raw hex values in components.
@@ -32,8 +32,8 @@ Use these tokens. Don't introduce raw hex values in components.
**Accents and state**
- `--color-primary` / `--color-primary-foreground`: brand purple. Reserved for genuine brand emphasis. Don't use it to mean "primary action" between two peer links (see the peer-item rule below).
- `--color-secondary` / `--color-secondary-foreground`: muted brand support. Available for chips, tags, low-emphasis fills.
- `--color-primary` / `--color-primary-foreground`: brand blue (`#009fff` in both modes). Reserved for genuine brand emphasis. Don't use it to mean "primary action" between two peer links (see the peer-item rule below). Note: white-on-primary contrast is 2.84:1, which is below WCAG AA in both modes since the token is identical light and dark. Avoid using primary as a fill for body-sized text. Use it for chrome accents, ring/focus, and large CTAs only.
- `--color-secondary` / `--color-secondary-foreground`: neutral support fill. Available for chips, tags, low-emphasis fills.
- `--color-destructive` / `--color-destructive-foreground`: error and danger. The 404 page uses `text-(--color-destructive)` on the `fatal:` token, always paired with the word itself (color is never the sole signal).
- `--color-ring`: keyboard focus ring color. Don't override per-component. If a default ring looks wrong, fix it at the token level.
+36 -36
View File
@@ -32,50 +32,50 @@
--radius: 0.5rem;
}
/* Theme palettes are derived from Happy Hues (https://www.happyhues.co):
light mode = palette 6, dark mode = palette 13. */
/* Theme palettes are adapted from Pierre Theme's "Light" and "Dark"
(non-soft) variants (https://github.com/pierrecomputer/theme). */
:root {
color-scheme: light dark;
--color-background: #fffffe;
--color-foreground: #2b2c34;
--color-card: #fffffe;
--color-card-foreground: #2b2c34;
--color-popover: #fffffe;
--color-popover-foreground: #2b2c34;
--color-primary: #059669;
--color-primary-foreground: #fffffe;
--color-secondary: #d1d1e9;
--color-secondary-foreground: #2b2c34;
--color-surface: #ececf6;
--color-muted-foreground: #5f6172;
--color-destructive: #c2392f;
--color-destructive-foreground: #fffffe;
--color-border: #d1d1e9;
--color-input: #c8c8d8;
--color-ring: #059669;
--color-background: #ffffff;
--color-foreground: #0a0a0a;
--color-card: #ffffff;
--color-card-foreground: #0a0a0a;
--color-popover: #ffffff;
--color-popover-foreground: #0a0a0a;
--color-primary: #009fff;
--color-primary-foreground: #ffffff;
--color-secondary: #f5f5f5;
--color-secondary-foreground: #0a0a0a;
--color-surface: #f5f5f5;
--color-muted-foreground: #737373;
--color-destructive: #d52c36;
--color-destructive-foreground: #ffffff;
--color-border: #e5e5e5;
--color-input: #d4d4d4;
--color-ring: #009fff;
}
:root.dark {
color-scheme: dark;
--color-background: #16161a;
--color-foreground: #fffffe;
--color-card: #242629;
--color-card-foreground: #fffffe;
--color-popover: #242629;
--color-popover-foreground: #fffffe;
--color-primary: #059669;
--color-primary-foreground: #fffffe;
--color-secondary: #5c5f68;
--color-secondary-foreground: #fffffe;
--color-surface: #33363c;
--color-muted-foreground: #94a1b2;
--color-destructive: #d63653;
--color-destructive-foreground: #fffffe;
--color-border: #5c5f68;
--color-input: #5c5f68;
--color-ring: #059669;
--color-background: #0a0a0a;
--color-foreground: #fafafa;
--color-card: #171717;
--color-card-foreground: #fafafa;
--color-popover: #171717;
--color-popover-foreground: #fafafa;
--color-primary: #009fff;
--color-primary-foreground: #ffffff;
--color-secondary: #1d1d1d;
--color-secondary-foreground: #fafafa;
--color-surface: #1d1d1d;
--color-muted-foreground: #8a8a8a;
--color-destructive: #ff6762;
--color-destructive-foreground: #ffffff;
--color-border: #1d1d1d;
--color-input: #262626;
--color-ring: #009fff;
}
@layer base {
+18 -6
View File
@@ -81,6 +81,7 @@ export function SignIn() {
if (!body.error && !body.fields) {
setFormError(t("sign_in_failed"));
}
setSubmitting(false);
return;
}
const data = (await res.json()) as SignInResponse;
@@ -91,7 +92,6 @@ export function SignIn() {
window.location.assign(data.redirectTo || subUrl("/"));
} catch {
setFormError(t("sign_in_failed"));
} finally {
setSubmitting(false);
}
})();
@@ -124,6 +124,7 @@ export function SignIn() {
autoComplete="username"
required
autoFocus
tabIndex={1}
placeholder={t("username_placeholder")}
value={username}
onChange={(e) => setUsername(e.target.value)}
@@ -141,7 +142,9 @@ export function SignIn() {
<div className="flex items-center justify-between gap-3">
<Label htmlFor="password">{t("password")}</Label>
<Button variant="link" size="inline" asChild>
<a href={subUrl("/user/forget_password")}>{t("forget_password")}</a>
<a href={subUrl("/user/forget_password")} tabIndex={7}>
{t("forget_password")}
</a>
</Button>
</div>
<div className="relative">
@@ -152,6 +155,7 @@ export function SignIn() {
type={showPassword ? "text" : "password"}
autoComplete="current-password"
required
tabIndex={2}
placeholder={t("password_placeholder")}
value={password}
onChange={(e) => setPassword(e.target.value)}
@@ -161,6 +165,7 @@ export function SignIn() {
/>
<button
type="button"
tabIndex={3}
onClick={() => setShowPassword((v) => !v)}
aria-label={showPassword ? t("hide_password") : t("show_password")}
aria-pressed={showPassword}
@@ -180,7 +185,7 @@ export function SignIn() {
<div className="flex flex-col gap-1.5">
<Label htmlFor="login_source">{t("auth_source")}</Label>
<Select value={String(loginSource)} onValueChange={(v) => setLoginSource(Number(v))}>
<SelectTrigger id="login_source">
<SelectTrigger id="login_source" tabIndex={4}>
<SelectValue />
</SelectTrigger>
<SelectContent>
@@ -196,18 +201,25 @@ export function SignIn() {
)}
<div className="flex items-center gap-2">
<Checkbox id="remember" checked={remember} onCheckedChange={(v) => setRemember(v === true)} />
<Checkbox
id="remember"
tabIndex={5}
checked={remember}
onCheckedChange={(v) => setRemember(v === true)}
/>
<Label htmlFor="remember" className="cursor-pointer font-normal">
{t("remember_me")}
</Label>
</div>
<div className="mt-2 flex flex-col gap-3">
<Button type="submit" disabled={submitting} className="w-full">
<Button type="submit" disabled={submitting} tabIndex={6} className="w-full">
{submitting ? t("sign_in_submitting") : t("sign_in")}
</Button>
<Button variant="link" size="inline" asChild className="self-center">
<a href={subUrl("/user/sign_up")}>{t("sign_up_now")}</a>
<a href={subUrl("/user/sign_up")} tabIndex={8}>
{t("sign_up_now")}
</a>
</Button>
</div>
</form>