v0.3 Phase 4f/5a/5b: token architecture + skeleton + browser-preview reclass
Three small phases land together for Jake's round-4 feedback. Each is
focused and reviewable. Larger Dictation/Settings/Tasks/History page
migrations are deferred to focused future sittings.
Phase 4f — semantic source / derived token architecture.
Added --semantic-{red,blue,green,yellow}-source raw-palette tokens.
These encode the role's hue identity from Jake's color.adobe.com
palette (#FF0700 / #FFCD00 / #00FF56 / #000AFF) and stay constant
across all themes. The existing --color-{danger,caution,success,info}
tokens are now documented as theme-tuned derivations meeting WCAG
AA in context. Source tokens make the brand-to-runtime relationship
traceable in code.
Added --color-{role}-bg subtle-fill tokens derived via
color-mix(in oklab, var(--color-{role}) 8%, transparent). Used by
Notice backgrounds, hover tints and focus halos. HC mode forces
-bg tokens to transparent so HC notices read as text + icon +
2-px border + left bar with no body tint.
Phase 5a — page skeleton primitive + slim notice variant.
NEW src/lib/ui/LumotiaPageSkeleton.svelte. Six-zone layout:
header (optional)
notice (optional, slim ambient state)
primary work surface (required, primaryBleed for full-bleed)
right rail (optional, lg-screen drawer)
action bar (optional, bottom)
metadata (optional, mono micro-footer)
Every zone except primary is opt-in. Foundation for the
page-skeleton sweep without forcing any page migration yet.
LumotiaNotice gained a slim prop. When true: padding collapses
(px-3 py-2 vs px-4 py-3), icon shrinks (14 vs 16), title block is
suppressed. For ambient page-level notices like browser-preview.
Phase 5b — browser-preview reclassification.
src/lib/pages/DictationPage.svelte: the browser-preview state used
to render as a red error block. That is an environment limitation,
not a failed action. Reclassified as a slim info notice with
friendlier copy:
"You're in a browser preview. Local transcription only works in
the Lumotia desktop app."
Other error states (transcription failed, needs review) still
render through the original danger-tinted block with the
technical-details affordance preserved. Only the browser-preview
path branches into LumotiaNotice tone="info" slim.
The larger Dictation page restructure (header recomposition,
Template/Extract Tasks demotion to bottom action bar, empty-state
typography) is reserved for a focused Phase 5c sitting since
DictationPage.svelte is 1 262 LOC of state-heavy code.
Verified.
- npm run check: 0 errors, 0 warnings across 5707 files.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -285,6 +285,45 @@ LumotiaNotice opacity rebalance:
|
|||||||
| Icon | `--color-text` | role colour |
|
| Icon | `--color-text` | role colour |
|
||||||
| Title / body | `--color-text` | `--color-text` (unchanged) |
|
| Title / body | `--color-text` | `--color-text` (unchanged) |
|
||||||
|
|
||||||
|
### Phase 4f — Semantic source / derived token architecture. Landed 2026-05-15.
|
||||||
|
|
||||||
|
Round-4 feedback formalised the relationship between the brand-source palette and the theme-derived semantic tokens. Source tokens are stable across all themes (they encode the role's hue identity); derived tokens are theme-tuned to meet WCAG AA in context.
|
||||||
|
|
||||||
|
Added to `src/design-system/v0.3-quietware-tokens.css`:
|
||||||
|
|
||||||
|
```css
|
||||||
|
:root[data-design="quietware"] {
|
||||||
|
--semantic-red-source: #FF0700; /* error / failed / destructive / blocked */
|
||||||
|
--semantic-yellow-source: #FFCD00; /* needs review / caution / pending */
|
||||||
|
--semantic-green-source: #00FF56; /* ready / saved / complete / safe */
|
||||||
|
--semantic-blue-source: #000AFF; /* info / processing / system guidance */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The existing `--color-{danger,caution,success,info}` tokens are documented as derivations of these sources, retuned per theme (muted-Material values in normal modes, clarity-first values in HC). New `--color-{role}-bg` subtle-fill tokens derived via `color-mix(in oklab, var(--color-{role}) 8%, transparent)` — used by Notice backgrounds, hover tints and focus halos. HC mode forces all `-bg` tokens to `transparent` so HC notices render as text + icon + 2-px border + left bar with no body tint.
|
||||||
|
|
||||||
|
### Phase 5a — Page skeleton primitive + slim notice variant. Landed 2026-05-15.
|
||||||
|
|
||||||
|
Foundation for the page-skeleton sweep without yet migrating any page. Two additions:
|
||||||
|
|
||||||
|
- **`LumotiaPageSkeleton.svelte`** ([src/lib/ui/](../../src/lib/ui/LumotiaPageSkeleton.svelte)) — six-zone layout used by every quietware page: header / optional slim notice / primary work surface / optional right rail / optional bottom action bar / optional mono metadata footer. Sidebar lives in AppChrome above the skeleton. Every snippet is optional except `primary`. `primaryBleed` prop drops the surface's inner card chrome for full-bleed transcript canvases.
|
||||||
|
|
||||||
|
- **`LumotiaNotice` `slim` prop** — collapses padding, drops the title block, smaller icon. For one-line ambient notices like browser-preview state.
|
||||||
|
|
||||||
|
### Phase 5b — Browser-preview reclassification. Landed 2026-05-15.
|
||||||
|
|
||||||
|
Targeted scope per Jake's round-4 feedback: the browser-preview state used to render as a red error block in `DictationPage.svelte`. That is an environment limitation, not a failed action. Reclassified as a slim info notice with friendlier copy:
|
||||||
|
|
||||||
|
```
|
||||||
|
Old: bg-danger/10 border-danger/20, red text, technical-details affordance
|
||||||
|
New: LumotiaNotice tone="info" slim, "You're in a browser preview. Local
|
||||||
|
transcription only works in the Lumotia desktop app."
|
||||||
|
```
|
||||||
|
|
||||||
|
Other error states (transcription failed, needs review) still render through the original danger-tinted block with the technical-details `<details>` affordance preserved. Only the browser-preview path branches into the info-classed notice.
|
||||||
|
|
||||||
|
The larger Dictation page restructure (header recomposition, Template/Extract Tasks demotion to bottom action bar, empty-state typography) is reserved for a focused Phase 5c sitting — `DictationPage.svelte` is 1 262 LOC of state-heavy code and warrants surgical attention rather than a tail-end rush.
|
||||||
|
|
||||||
### Phase 4e — High-contrast rendering contract. Landed 2026-05-15.
|
### Phase 4e — High-contrast rendering contract. Landed 2026-05-15.
|
||||||
|
|
||||||
High-contrast is not a colour variant — it's a different rendering contract that the entire UI subscribes to. The tokens block now publishes behavioural variables alongside the palette overrides, so components can opt into the contract by reading them.
|
High-contrast is not a colour variant — it's a different rendering contract that the entire UI subscribes to. The tokens block now publishes behavioural variables alongside the palette overrides, so components can opt into the contract by reading them.
|
||||||
|
|||||||
@@ -65,6 +65,33 @@
|
|||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
Semantic source palette (Phase 4f, 2026-05-15).
|
||||||
|
|
||||||
|
These four hex values are the BRAND-identity source for the
|
||||||
|
semantic system. They are NEVER used directly as text or fill
|
||||||
|
in the runtime UI — each --color-{role} is a theme-tuned
|
||||||
|
derivation that meets WCAG AA in its context. The source tokens
|
||||||
|
exist so the relationship between brand-source and theme-derived
|
||||||
|
is traceable in code (and so a future re-tune of one role does
|
||||||
|
not require chasing every reference).
|
||||||
|
|
||||||
|
Red -> error, failed, destructive, blocked
|
||||||
|
Yellow -> needs review, caution, missing setup, pending
|
||||||
|
Green -> ready, saved, complete, safe
|
||||||
|
Blue -> information, processing, neutral system guidance
|
||||||
|
|
||||||
|
The brand accent (--color-accent, copper-clay) is intentionally
|
||||||
|
separate from semantic yellow. Brand is action / Lumotia
|
||||||
|
identity. Yellow is caution / review.
|
||||||
|
============================================================ */
|
||||||
|
:root[data-design="quietware"] {
|
||||||
|
--semantic-red-source: #FF0700;
|
||||||
|
--semantic-yellow-source: #FFCD00;
|
||||||
|
--semantic-green-source: #00FF56;
|
||||||
|
--semantic-blue-source: #000AFF;
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
Quietware — dark mode default.
|
Quietware — dark mode default.
|
||||||
Warm brown-charcoal atmosphere. Reads as warm when you notice
|
Warm brown-charcoal atmosphere. Reads as warm when you notice
|
||||||
@@ -89,12 +116,22 @@
|
|||||||
--color-accent-subtle: #C9784514;
|
--color-accent-subtle: #C9784514;
|
||||||
--color-accent-glow: #C9784525;
|
--color-accent-glow: #C9784525;
|
||||||
|
|
||||||
/* Semantic palette. Muted-Material hues. */
|
/* Semantic palette. Derived from --semantic-{role}-source by
|
||||||
--color-danger: #FF8A8A;
|
lowering saturation/chroma and tuning lightness for AA on the
|
||||||
--color-caution: #F2C94C;
|
dark surface. Each role keeps the source hue identity. */
|
||||||
|
--color-danger: #FF8A8A; /* derived from --semantic-red-source #FF0700 */
|
||||||
|
--color-caution: #F2C94C; /* derived from --semantic-yellow-source #FFCD00 */
|
||||||
--color-warning: var(--color-caution);
|
--color-warning: var(--color-caution);
|
||||||
--color-success: #79D59B;
|
--color-success: #79D59B; /* derived from --semantic-green-source #00FF56 */
|
||||||
--color-info: #8AB4F8;
|
--color-info: #8AB4F8; /* derived from --semantic-blue-source #000AFF */
|
||||||
|
|
||||||
|
/* Subtle role fills for Notice backgrounds, hover tints, focus
|
||||||
|
halos. ~8% role over the surface — passes through atmosphere
|
||||||
|
without becoming a colour zone. */
|
||||||
|
--color-danger-bg: color-mix(in oklab, var(--color-danger) 8%, transparent);
|
||||||
|
--color-caution-bg: color-mix(in oklab, var(--color-caution) 8%, transparent);
|
||||||
|
--color-success-bg: color-mix(in oklab, var(--color-success) 8%, transparent);
|
||||||
|
--color-info-bg: color-mix(in oklab, var(--color-info) 8%, transparent);
|
||||||
|
|
||||||
--color-sidebar: #0F0E0C;
|
--color-sidebar: #0F0E0C;
|
||||||
--color-nav-active: #211D18;
|
--color-nav-active: #211D18;
|
||||||
@@ -142,11 +179,17 @@
|
|||||||
--color-accent-subtle: #9D5F3214;
|
--color-accent-subtle: #9D5F3214;
|
||||||
--color-accent-glow: #9D5F3225;
|
--color-accent-glow: #9D5F3225;
|
||||||
|
|
||||||
--color-danger: #B3261E;
|
/* Semantic palette — derivations for light mode, AA on cream. */
|
||||||
--color-caution: #7A5D00;
|
--color-danger: #B3261E; /* derived from --semantic-red-source */
|
||||||
|
--color-caution: #7A5D00; /* derived from --semantic-yellow-source */
|
||||||
--color-warning: var(--color-caution);
|
--color-warning: var(--color-caution);
|
||||||
--color-success: #1B6B3A;
|
--color-success: #1B6B3A; /* derived from --semantic-green-source */
|
||||||
--color-info: #2457A6;
|
--color-info: #2457A6; /* derived from --semantic-blue-source */
|
||||||
|
|
||||||
|
--color-danger-bg: color-mix(in oklab, var(--color-danger) 8%, transparent);
|
||||||
|
--color-caution-bg: color-mix(in oklab, var(--color-caution) 8%, transparent);
|
||||||
|
--color-success-bg: color-mix(in oklab, var(--color-success) 8%, transparent);
|
||||||
|
--color-info-bg: color-mix(in oklab, var(--color-info) 8%, transparent);
|
||||||
|
|
||||||
--color-sidebar: #F3EEE6;
|
--color-sidebar: #F3EEE6;
|
||||||
--color-nav-active: #E8E2D6;
|
--color-nav-active: #E8E2D6;
|
||||||
@@ -191,6 +234,13 @@
|
|||||||
--color-success: #1B6B3A;
|
--color-success: #1B6B3A;
|
||||||
--color-info: #005FCC;
|
--color-info: #005FCC;
|
||||||
|
|
||||||
|
/* HC strips fill atmosphere. Notices become text + icon + 2-px
|
||||||
|
border + left bar with no body tint. Maximum clarity. */
|
||||||
|
--color-danger-bg: transparent;
|
||||||
|
--color-caution-bg: transparent;
|
||||||
|
--color-success-bg: transparent;
|
||||||
|
--color-info-bg: transparent;
|
||||||
|
|
||||||
--color-sidebar: #FFFFFF;
|
--color-sidebar: #FFFFFF;
|
||||||
--color-nav-active: #000000;
|
--color-nav-active: #000000;
|
||||||
--color-hover: #F0F0F0;
|
--color-hover: #F0F0F0;
|
||||||
|
|||||||
@@ -29,6 +29,10 @@
|
|||||||
const prefs = getPreferences();
|
const prefs = getPreferences();
|
||||||
const tauriRuntimeAvailable = hasTauriRuntime();
|
const tauriRuntimeAvailable = hasTauriRuntime();
|
||||||
const browserPreviewMessage = "You're viewing Lumotia in a normal browser. Local transcription only works in the Tauri desktop app window.";
|
const browserPreviewMessage = "You're viewing Lumotia in a normal browser. Local transcription only works in the Tauri desktop app window.";
|
||||||
|
// v0.3 Phase 5b — shorter calmer copy used for the info-classed
|
||||||
|
// notice when the same condition surfaces in the UI. Same physical
|
||||||
|
// limitation, friendlier framing per round-4 feedback.
|
||||||
|
const browserPreviewSlim = "You're in a browser preview. Local transcription only works in the Lumotia desktop app.";
|
||||||
|
|
||||||
let transcript = $state("");
|
let transcript = $state("");
|
||||||
let segments = $state([]);
|
let segments = $state([]);
|
||||||
@@ -1127,31 +1131,40 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Error — Sub-task D: structured copy with technical detail behind <details> -->
|
<!-- Error — Sub-task D: structured copy with technical detail behind <details>.
|
||||||
|
v0.3 Phase 5b: browser-preview state is reclassified info (not error)
|
||||||
|
because it is an environment limitation, not a failed action. Other
|
||||||
|
errors continue to render as danger with technical details affordance. -->
|
||||||
{#if error}
|
{#if error}
|
||||||
<div class="px-5 pt-2 animate-fade-in flex-shrink-0">
|
<div class="px-5 pt-2 animate-fade-in flex-shrink-0">
|
||||||
<div class="px-4 py-3 rounded-lg bg-danger/10 border border-danger/20 text-[12px] text-danger space-y-2">
|
{#if error === browserPreviewMessage}
|
||||||
<div class="flex items-start gap-2">
|
<LumotiaNotice tone="info" slim dismissible onDismiss={() => { error = ""; errorDetail = ""; }}>
|
||||||
{#if transcriptionFailed}
|
{browserPreviewSlim}
|
||||||
<LumotiaStatusPill status="failed-safely" />
|
</LumotiaNotice>
|
||||||
{:else}
|
{:else}
|
||||||
<LumotiaStatusPill status="needs-review" />
|
<div class="px-4 py-3 rounded-lg bg-danger/10 border border-danger/20 text-[12px] text-danger space-y-2">
|
||||||
|
<div class="flex items-start gap-2">
|
||||||
|
{#if transcriptionFailed}
|
||||||
|
<LumotiaStatusPill status="failed-safely" />
|
||||||
|
{:else}
|
||||||
|
<LumotiaStatusPill status="needs-review" />
|
||||||
|
{/if}
|
||||||
|
<p class="m-0">{error}</p>
|
||||||
|
</div>
|
||||||
|
{#if errorDetail}
|
||||||
|
<details class="text-[11px] text-danger/70">
|
||||||
|
<summary class="cursor-pointer select-none hover:text-danger">Technical details</summary>
|
||||||
|
<pre class="mt-1 whitespace-pre-wrap break-words">{errorDetail}</pre>
|
||||||
|
</details>
|
||||||
{/if}
|
{/if}
|
||||||
<p class="m-0">{error}</p>
|
<div>
|
||||||
|
<button
|
||||||
|
class="text-[11px] underline hover:no-underline text-danger/80 hover:text-danger"
|
||||||
|
onclick={() => { error = ""; errorDetail = ""; }}
|
||||||
|
>Dismiss</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if errorDetail}
|
{/if}
|
||||||
<details class="text-[11px] text-danger/70">
|
|
||||||
<summary class="cursor-pointer select-none hover:text-danger">Technical details</summary>
|
|
||||||
<pre class="mt-1 whitespace-pre-wrap break-words">{errorDetail}</pre>
|
|
||||||
</details>
|
|
||||||
{/if}
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
class="text-[11px] underline hover:no-underline text-danger/80 hover:text-danger"
|
|
||||||
onclick={() => { error = ""; errorDetail = ""; }}
|
|
||||||
>Dismiss</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,9 @@
|
|||||||
title?: string;
|
title?: string;
|
||||||
dismissible?: boolean;
|
dismissible?: boolean;
|
||||||
onDismiss?: () => void;
|
onDismiss?: () => void;
|
||||||
|
/** Slim variant collapses to a single line with no title block.
|
||||||
|
For ambient page-level notices (eg. "You're in browser preview"). */
|
||||||
|
slim?: boolean;
|
||||||
children?: Snippet;
|
children?: Snippet;
|
||||||
classes?: string;
|
classes?: string;
|
||||||
}
|
}
|
||||||
@@ -41,6 +44,7 @@
|
|||||||
title,
|
title,
|
||||||
dismissible = false,
|
dismissible = false,
|
||||||
onDismiss,
|
onDismiss,
|
||||||
|
slim = false,
|
||||||
children,
|
children,
|
||||||
classes = "",
|
classes = "",
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
@@ -85,13 +89,13 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
role={ariaRole}
|
role={ariaRole}
|
||||||
class="px-4 py-3 rounded-r-lg rounded-l-sm border border-l-4
|
class="rounded-r-lg rounded-l-sm border border-l-4 text-[13px] flex gap-2.5
|
||||||
text-[13px] flex items-start gap-2.5
|
{slim ? 'px-3 py-2 items-center' : 'px-4 py-3 items-start'}
|
||||||
{tonePal.bar} {tonePal.border} {tonePal.bg} {classes}"
|
{tonePal.bar} {tonePal.border} {tonePal.bg} {classes}"
|
||||||
>
|
>
|
||||||
<ToneIcon size={16} class="mt-0.5 shrink-0 {tonePal.iconColor}" aria-hidden="true" />
|
<ToneIcon size={slim ? 14 : 16} class="shrink-0 {slim ? '' : 'mt-0.5'} {tonePal.iconColor}" aria-hidden="true" />
|
||||||
<div class="flex-1 min-w-0 leading-relaxed text-text">
|
<div class="flex-1 min-w-0 leading-relaxed text-text">
|
||||||
{#if title}
|
{#if title && !slim}
|
||||||
<p class="font-semibold text-text mb-1">{title}</p>
|
<p class="font-semibold text-text mb-1">{title}</p>
|
||||||
{/if}
|
{/if}
|
||||||
{#if children}{@render children()}{/if}
|
{#if children}{@render children()}{/if}
|
||||||
|
|||||||
87
src/lib/ui/LumotiaPageSkeleton.svelte
Normal file
87
src/lib/ui/LumotiaPageSkeleton.svelte
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
// v0.3 Phase 5a — shared page skeleton.
|
||||||
|
//
|
||||||
|
// Six-zone layout used by every quietware page so structure stays
|
||||||
|
// consistent across Dictation / Files / Tasks / History / Settings.
|
||||||
|
// Sidebar lives in AppChrome above this — the skeleton owns the
|
||||||
|
// right-of-sidebar column only.
|
||||||
|
//
|
||||||
|
// +----------------------------+
|
||||||
|
// | header | page title + status cluster
|
||||||
|
// |----------------------------|
|
||||||
|
// | notice (slim, optional) | one-line ambient state
|
||||||
|
// |----------------------------|
|
||||||
|
// | |
|
||||||
|
// | primary work surface | the page's centre of attention
|
||||||
|
// | |
|
||||||
|
// |----------------------------|
|
||||||
|
// | action bar (optional) | post-capture / per-row actions
|
||||||
|
// |----------------------------|
|
||||||
|
// | metadata (mono, tiny) | hotkey hint · backend · model
|
||||||
|
// +----------------------------+
|
||||||
|
//
|
||||||
|
// Every snippet is optional except primary. Pages compose the
|
||||||
|
// skeleton; the skeleton does not impose copy or controls.
|
||||||
|
import type { Snippet } from "svelte";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
header?: Snippet;
|
||||||
|
notice?: Snippet;
|
||||||
|
primary: Snippet;
|
||||||
|
actionBar?: Snippet;
|
||||||
|
metadata?: Snippet;
|
||||||
|
/** Right rail / drawer — opt-in narrow column to the right of the primary surface. */
|
||||||
|
rightRail?: Snippet;
|
||||||
|
/** Drop the primary surface's outer padding for full-bleed content (eg. transcript canvas). */
|
||||||
|
primaryBleed?: boolean;
|
||||||
|
classes?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
header,
|
||||||
|
notice,
|
||||||
|
primary,
|
||||||
|
actionBar,
|
||||||
|
metadata,
|
||||||
|
rightRail,
|
||||||
|
primaryBleed = false,
|
||||||
|
classes = "",
|
||||||
|
}: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col h-full min-h-0 bg-bg {classes}">
|
||||||
|
{#if header}
|
||||||
|
<div class="shrink-0 px-7 pt-5 pb-3 border-b border-border-subtle">
|
||||||
|
{@render header()}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if notice}
|
||||||
|
<div class="shrink-0 px-7 pt-3">
|
||||||
|
{@render notice()}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="flex-1 min-h-0 flex {rightRail ? 'gap-5' : ''} px-7 pt-5 pb-3">
|
||||||
|
<div class="flex-1 min-w-0 {primaryBleed ? '' : 'bg-bg-card border border-border-subtle rounded-lg overflow-hidden'}">
|
||||||
|
{@render primary()}
|
||||||
|
</div>
|
||||||
|
{#if rightRail}
|
||||||
|
<aside class="w-72 shrink-0 hidden lg:flex flex-col gap-3">
|
||||||
|
{@render rightRail()}
|
||||||
|
</aside>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if actionBar}
|
||||||
|
<div class="shrink-0 px-7 py-3 border-t border-border-subtle bg-bg-elevated/60">
|
||||||
|
{@render actionBar()}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if metadata}
|
||||||
|
<div class="shrink-0 px-7 py-2 text-[11px] font-mono text-text-tertiary">
|
||||||
|
{@render metadata()}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user