You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
738 lines
20 KiB
738 lines
20 KiB
<script lang="ts">
|
|
import { onDestroy } from 'svelte';
|
|
import { createActiveApp } from '$active-app';
|
|
import { cookieAdapter, localAdapter } from '$storage';
|
|
import type { SupportedLocale } from '$lang';
|
|
import { siumLangs, type Issue } from '$sium';
|
|
import { LogLevel, type LogEntry, type Transport } from '$logger';
|
|
import { object, pipe, string, email, min, meta } from '$sium/core';
|
|
import { translations } from './schema';
|
|
|
|
// ── Logger UI capture ──────────────────────────────────────────────────────
|
|
const captured: LogEntry[] = $state([]);
|
|
const captureTransport: Transport = {
|
|
name: 'ui-capture',
|
|
write(entry) {
|
|
captured.push(entry);
|
|
}
|
|
};
|
|
|
|
// ── App composition ────────────────────────────────────────────────────────
|
|
// Mount lang's catalogue plus sium's catalogue under the `sium.*` namespace
|
|
// so EngineSium's `#?sium.errors.*` references resolve through Lang.t().
|
|
const langSchema = { ...translations, sium: siumLangs };
|
|
|
|
let appRoot = $state<HTMLElement | null>(null);
|
|
|
|
const App = createActiveApp({
|
|
lang: { schema: langSchema, defaultLocale: 'es', fallbackChain: ['en'] },
|
|
logger: {
|
|
level: LogLevel.TRACE,
|
|
globalContext: { surface: '/test/aapp' },
|
|
transports: [captureTransport]
|
|
},
|
|
storage: { adapter: localAdapter, namespace: 'test-aapp' },
|
|
cache: {
|
|
defaultMemoryAdapter: { suppressProductionWarning: true }
|
|
},
|
|
frontend: {
|
|
theme: 'base',
|
|
mode: 'auto',
|
|
density: 'normal',
|
|
target: () => appRoot,
|
|
persist: {
|
|
// Theme via cookie (server-readable for SSR demos), the rest in
|
|
// localStorage with the App's namespace.
|
|
overrides: {
|
|
theme: {
|
|
adapter: cookieAdapter({ path: '/', sameSite: 'lax', maxAge: 31_536_000 }),
|
|
namespace: false,
|
|
raw: true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Sium is page-scoped — `App.createSiumEngine()` wires App.lang +
|
|
// App.Logger so issues translate live and validation diagnostics flow
|
|
// through the same bus.
|
|
const sium = App.createSiumEngine();
|
|
|
|
onDestroy(() => App.dispose());
|
|
|
|
// ── Locale switcher ────────────────────────────────────────────────────────
|
|
type LocaleOption = { code: SupportedLocale; label: string; flag: string };
|
|
const LOCALES: LocaleOption[] = [
|
|
{ code: 'es', label: 'Español (base)', flag: '🌐' },
|
|
{ code: 'es-MX', label: 'Español (MX)', flag: '🇲🇽' },
|
|
{ code: 'es-AR', label: 'Español (AR — base fallback)', flag: '🇦🇷' },
|
|
{ code: 'en', label: 'English (base)', flag: '🌐' },
|
|
{ code: 'en-GB', label: 'English (GB)', flag: '🇬🇧' },
|
|
{ code: 'en-US', label: 'English (US — base fallback)', flag: '🇺🇸' },
|
|
{ code: 'ar', label: 'العربية', flag: '🇸🇦' },
|
|
{ code: 'pt-BR', label: 'Português (BR)', flag: '🇧🇷' },
|
|
{ code: 'fr-CA', label: 'Français (CA — chain fallback)', flag: '🇨🇦' }
|
|
];
|
|
|
|
function setLocale(locale: SupportedLocale): void {
|
|
App.setLocale(locale);
|
|
App.Logger.info('locale', `Locale switched to ${locale}`, {
|
|
context: { locale }
|
|
});
|
|
}
|
|
|
|
// ── Greeting + name input ──────────────────────────────────────────────────
|
|
let userName = $state('Ada');
|
|
let cartCount = $state(1);
|
|
|
|
// ── Region-only sibling demo ───────────────────────────────────────────────
|
|
// Reading 'app.regionOnly' with bare 'es' triggers the ambiguous warning
|
|
// because there are two siblings (es-MX, es-AR) and no canonical 'es'.
|
|
function probeRegionOnly(): void {
|
|
const text = App.lang.t('app.regionOnly', undefined, 'es');
|
|
App.Logger.info('demo', `regionOnly with bare 'es' → ${text}`);
|
|
}
|
|
|
|
// ── Sium login schema ──────────────────────────────────────────────────────
|
|
const loginSchema = object({
|
|
email: pipe(string(), email(), meta({ label: 'Email' })),
|
|
password: pipe(string(), min(8), meta({ label: 'Password' }))
|
|
});
|
|
|
|
let loginInput = $state({ email: 'not-an-email', password: '123' });
|
|
let loginIssues = $state<readonly Issue[]>([]);
|
|
let loginOk = $state(false);
|
|
|
|
async function runLogin(): Promise<void> {
|
|
const result = await sium.validate(loginSchema, loginInput);
|
|
if (result.ok) {
|
|
loginOk = true;
|
|
loginIssues = [];
|
|
} else {
|
|
loginOk = false;
|
|
loginIssues = result.issues;
|
|
}
|
|
}
|
|
|
|
// Re-translates issues whenever the locale changes — same `Issue[]`, new
|
|
// strings — proving Sium's resolveIssue routes through App.lang.
|
|
const translatedIssues = $derived(
|
|
loginIssues.map((entry) => ({
|
|
path: entry.path.length === 0 ? '<root>' : entry.path.join('.'),
|
|
code: entry.code,
|
|
message: sium.resolveIssue(entry, App.getLocale())
|
|
}))
|
|
);
|
|
|
|
// ── Format demo ───────────────────────────────────────────────────────────
|
|
const sampleNumber = 1234567.89;
|
|
const sampleAmount = 1500.5;
|
|
const sampleDate = new Date(2026, 3, 25, 14, 30);
|
|
|
|
const formatsView = $derived.by(() => {
|
|
// Touch the locale so $derived picks it up reactively.
|
|
void App.lang.getLocale();
|
|
return {
|
|
locale: App.format.getLocale(),
|
|
number: App.format.numbers.format(sampleNumber),
|
|
currency: App.format.currency.format(sampleAmount),
|
|
date: App.format.dates.formatDateTime(sampleDate)
|
|
};
|
|
});
|
|
|
|
// ── Frontend view ──────────────────────────────────────────────────────────
|
|
const frontendView = $derived.by(() => {
|
|
void App.lang.getLocale();
|
|
// Re-read on preference change (theme/density). onPreferenceChange would
|
|
// be cleaner but for a demo this is enough.
|
|
return {
|
|
locale: App.frontend.getLocale(),
|
|
dir: App.frontend.getDir(),
|
|
theme: App.frontend.getTheme(),
|
|
mode: App.frontend.getMode(),
|
|
density: App.frontend.getDensity()
|
|
};
|
|
});
|
|
|
|
const THEMES = ['base', 'forest', 'sunset', 'ink'] as const;
|
|
function setTheme(t: string): void {
|
|
App.frontend.setTheme(t);
|
|
}
|
|
|
|
// ── Storage demo ──────────────────────────────────────────────────────────
|
|
// Demo entry on App.storage (localStorage with namespace 'test-aapp').
|
|
const visitCounter = App.storage.entry('visit-counter', 0);
|
|
if (typeof window !== 'undefined') visitCounter.update((n) => n + 1);
|
|
|
|
// ── Logger view ────────────────────────────────────────────────────────────
|
|
function clearHistory(): void {
|
|
captured.length = 0;
|
|
App.Logger.clear();
|
|
}
|
|
|
|
function levelName(l: LogLevel): string {
|
|
return LogLevel[l] ?? String(l);
|
|
}
|
|
|
|
const LEVEL_COLORS: Record<number, string> = {
|
|
[LogLevel.TRACE]: '#8a8f96',
|
|
[LogLevel.DEBUG]: '#6a737d',
|
|
[LogLevel.INFO]: '#0366d6',
|
|
[LogLevel.WARN]: '#b08800',
|
|
[LogLevel.ERROR]: '#d73a49',
|
|
[LogLevel.FATAL]: '#6f42c1'
|
|
};
|
|
</script>
|
|
|
|
<svelte:head>
|
|
<title>/test/aapp</title>
|
|
</svelte:head>
|
|
|
|
<main bind:this={appRoot} class:rtl={frontendView.dir === 'rtl'}>
|
|
<header>
|
|
<p class="eyebrow">src/arts/aapp</p>
|
|
<h1>{App.lang.t('app.title')}</h1>
|
|
<p class="subtitle">{App.lang.t('app.subtitle')}</p>
|
|
</header>
|
|
|
|
<section class="locale" dir="ltr">
|
|
<h2>1. Locale (single source of truth)</h2>
|
|
<p class="hint">
|
|
<code>App.setLocale(...)</code> propagates to <strong>Lang</strong>,
|
|
<strong>Format</strong> and <strong>Frontend</strong> through a shared
|
|
<code>LocaleSource</code>. BCP 47 resolution runs per record on every
|
|
<code>Lang.t()</code> call.
|
|
</p>
|
|
<div class="grid">
|
|
{#each LOCALES as { code, label, flag } (code)}
|
|
<button
|
|
type="button"
|
|
class:active={App.getLocale() === code}
|
|
onclick={() => setLocale(code)}
|
|
>
|
|
<span aria-hidden="true">{flag}</span>
|
|
<strong>{code}</strong>
|
|
<small>{label}</small>
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
<p class="active-line">
|
|
<strong>Active:</strong> <code>{App.getLocale()}</code>
|
|
</p>
|
|
</section>
|
|
|
|
<section class="lang">
|
|
<h2>2. Lang — BCP 47 resolution in action</h2>
|
|
|
|
<div class="row">
|
|
<label>
|
|
<span>Name</span>
|
|
<input type="text" bind:value={userName} />
|
|
</label>
|
|
<p class="result">
|
|
<code>greeting('{userName}')</code> →
|
|
<strong>{App.lang.t('app.greeting', { name: userName })}</strong>
|
|
</p>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<label>
|
|
<span>Cart count</span>
|
|
<input type="number" min="0" max="20" bind:value={cartCount} />
|
|
</label>
|
|
<p class="result">
|
|
<code>cart({'{count: ' + cartCount + '}'})</code> →
|
|
<strong>{App.lang.t('app.cart', { count: cartCount })}</strong>
|
|
</p>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<button type="button" onclick={probeRegionOnly}>
|
|
Probe <code>app.regionOnly</code> with bare <code>'es'</code>
|
|
</button>
|
|
<p class="hint">
|
|
Has only <code>es-MX</code> and <code>es-AR</code> — bare <code>'es'</code> picks the first
|
|
sibling and emits <code>AMBIGUOUS_SIBLING_MATCH</code> in DEV. Watch the logger table below.
|
|
</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="formats">
|
|
<h2>3. Format — locale flows through</h2>
|
|
<p class="hint">
|
|
Same locale string reaches <code>Intl.NumberFormat</code> and
|
|
<code>Intl.DateTimeFormat</code> via the shared <code>LocaleSource</code>.
|
|
</p>
|
|
<table class="kv">
|
|
<tbody>
|
|
<tr>
|
|
<td><code>Format.getLocale()</code></td>
|
|
<td><code>{formatsView.locale}</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>numbers.format({sampleNumber})</code></td>
|
|
<td><strong>{formatsView.number}</strong></td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>currency.format({sampleAmount})</code></td>
|
|
<td><strong>{formatsView.currency}</strong></td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>dates.formatDateTime(...)</code></td>
|
|
<td><strong>{formatsView.date}</strong></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</section>
|
|
|
|
<section class="frontend">
|
|
<h2>4. Frontend — direction + theme (with persistence)</h2>
|
|
<p class="hint">
|
|
<code>dir</code> is auto-derived from the locale (rtl for <code>ar</code>). Theme persists in
|
|
a <strong>cookie</strong> via <code>frontend.persist.overrides.theme</code>; mode/density/dir
|
|
persist in localStorage with the App's namespace. Recargá la página para verificar el
|
|
round-trip.
|
|
</p>
|
|
<div class="row">
|
|
<strong>Theme:</strong>
|
|
{#each THEMES as t (t)}
|
|
<button type="button" class:active={frontendView.theme === t} onclick={() => setTheme(t)}>
|
|
{t}
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
<table class="kv">
|
|
<tbody>
|
|
<tr><td><code>locale</code></td><td><code>{frontendView.locale}</code></td></tr>
|
|
<tr><td><code>dir</code></td><td><strong>{frontendView.dir}</strong></td></tr>
|
|
<tr><td><code>theme</code></td><td><code>{frontendView.theme}</code></td></tr>
|
|
<tr><td><code>mode</code></td><td><code>{frontendView.mode}</code></td></tr>
|
|
<tr><td><code>density</code></td><td><code>{frontendView.density}</code></td></tr>
|
|
</tbody>
|
|
</table>
|
|
</section>
|
|
|
|
<section class="storage">
|
|
<h2>5. Storage — App.storage + page-scoped entries</h2>
|
|
<p class="hint">
|
|
<code>App.storage</code> usa <code>localAdapter</code> con namespace <code>test-aapp</code>.
|
|
El contador se incrementa en cada montaje de la página y persiste entre recargas (mientras no
|
|
lo borres).
|
|
</p>
|
|
<table class="kv">
|
|
<tbody>
|
|
<tr>
|
|
<td><code>visit-counter</code></td>
|
|
<td>
|
|
<strong>{visitCounter.current}</strong>
|
|
<button type="button" onclick={() => visitCounter.reset()}>reset</button>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>fullKey</code></td>
|
|
<td><code>{visitCounter.fullKey}</code></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<p class="hint">
|
|
Para más adapters, raw mode, TTL, version+migrate y errores, mirá la página dedicada
|
|
<a href="/test/stor">/test/stor</a>.
|
|
</p>
|
|
</section>
|
|
|
|
<section class="sium">
|
|
<h2>6. Sium — page-scoped, fed with App's Lang + Logger</h2>
|
|
<p class="hint">
|
|
<strong>Sium is not part of <code>aapp</code></strong> — this page constructs
|
|
<code>{'createEngineSium({ lang: App.lang, logger: App.Logger })'}</code> locally because
|
|
validation is page-scoped. Issues stay as <code>{'{ code, params }'}</code> data; switching locale
|
|
re-translates them without re-validating.
|
|
</p>
|
|
<form onsubmit={(e) => e.preventDefault()}>
|
|
<label>
|
|
<span>Email</span>
|
|
<input type="text" bind:value={loginInput.email} />
|
|
</label>
|
|
<label>
|
|
<span>Password</span>
|
|
<input type="text" bind:value={loginInput.password} />
|
|
</label>
|
|
<button type="button" class="primary" onclick={runLogin}>Validate</button>
|
|
</form>
|
|
{#if loginOk}
|
|
<p class="success">✓ Valid input.</p>
|
|
{:else if translatedIssues.length > 0}
|
|
<ul class="issues">
|
|
{#each translatedIssues as issue (issue.path + issue.code)}
|
|
<li>
|
|
<code class="path">{issue.path}</code>
|
|
<code class="code">{issue.code}</code>
|
|
<span>{issue.message}</span>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
{/if}
|
|
</section>
|
|
|
|
<section class="logger">
|
|
<div class="logger-head">
|
|
<h2>7. Logger — shared sink</h2>
|
|
<button type="button" onclick={clearHistory}>Clear</button>
|
|
</div>
|
|
<p class="hint">
|
|
Lang warnings (missing translations, ambiguous siblings) and Sium debug entries (validation
|
|
failures) all reach <code>App.Logger</code>. Below are entries captured by a UI transport.
|
|
</p>
|
|
{#if captured.length === 0}
|
|
<p class="empty">No entries yet.</p>
|
|
{:else}
|
|
<table class="logs">
|
|
<thead>
|
|
<tr>
|
|
<th>#</th>
|
|
<th>Level</th>
|
|
<th>Cat</th>
|
|
<th>Message</th>
|
|
<th>Context</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{#each captured as entry, i (entry.id)}
|
|
<tr>
|
|
<td>{i + 1}</td>
|
|
<td>
|
|
<span class="level" style:background={LEVEL_COLORS[entry.level]}>
|
|
{levelName(entry.level)}
|
|
</span>
|
|
</td>
|
|
<td><code>{entry.category}</code></td>
|
|
<td>{entry.message}</td>
|
|
<td>
|
|
{#if entry.context}
|
|
<code class="ctx">{JSON.stringify(entry.context)}</code>
|
|
{:else}
|
|
<span class="muted">—</span>
|
|
{/if}
|
|
</td>
|
|
</tr>
|
|
{/each}
|
|
</tbody>
|
|
</table>
|
|
{/if}
|
|
</section>
|
|
</main>
|
|
|
|
<style>
|
|
main {
|
|
max-width: 980px;
|
|
margin: 2rem auto;
|
|
padding: 0 1.5rem 4rem;
|
|
font-family:
|
|
system-ui,
|
|
-apple-system,
|
|
sans-serif;
|
|
line-height: 1.55;
|
|
color: #222;
|
|
}
|
|
main.rtl {
|
|
font-family: 'Segoe UI', 'Tahoma', system-ui, sans-serif;
|
|
}
|
|
header {
|
|
border-bottom: 1px solid #e1e4e8;
|
|
padding-bottom: 1rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
.eyebrow {
|
|
margin: 0 0 0.4rem;
|
|
font-size: 0.7rem;
|
|
font-weight: 800;
|
|
letter-spacing: 0.16em;
|
|
text-transform: uppercase;
|
|
color: #586069;
|
|
}
|
|
h1 {
|
|
margin: 0 0 0.4rem;
|
|
font-size: clamp(1.8rem, 4vw, 2.4rem);
|
|
}
|
|
.subtitle {
|
|
color: #586069;
|
|
margin: 0;
|
|
}
|
|
h2 {
|
|
margin: 0 0 0.6rem;
|
|
font-size: 1rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.08em;
|
|
color: #3f6f52;
|
|
border-bottom: 1px solid #eee;
|
|
padding-bottom: 0.3rem;
|
|
}
|
|
section {
|
|
margin-top: 2rem;
|
|
padding: 1.2rem;
|
|
background: rgba(255, 255, 255, 0.85);
|
|
border: 1px solid #e1e4e8;
|
|
border-radius: 12px;
|
|
}
|
|
:global(main[data-theme='forest']) section {
|
|
background: rgba(245, 252, 246, 0.9);
|
|
border-color: #c8e0cb;
|
|
}
|
|
:global(main[data-theme='sunset']) section {
|
|
background: rgba(255, 248, 235, 0.9);
|
|
border-color: #f0d9b3;
|
|
}
|
|
:global(main[data-theme='ink']) section {
|
|
background: rgba(35, 38, 50, 0.9);
|
|
border-color: #4a5066;
|
|
color: #f3f7ff;
|
|
}
|
|
:global(main[data-theme='ink']) h2 {
|
|
color: #9fc6ff;
|
|
}
|
|
.hint {
|
|
font-size: 0.85rem;
|
|
color: #586069;
|
|
margin: 0 0 0.8rem;
|
|
}
|
|
:global(main[data-theme='ink']) .hint {
|
|
color: #aab2c8;
|
|
}
|
|
.grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
gap: 0.5rem;
|
|
}
|
|
.grid button {
|
|
display: grid;
|
|
grid-template-columns: auto 1fr;
|
|
gap: 0.4rem 0.5rem;
|
|
padding: 0.6rem 0.8rem;
|
|
border: 1px solid #d1d5da;
|
|
border-radius: 8px;
|
|
background: #fff;
|
|
cursor: pointer;
|
|
text-align: left;
|
|
font: inherit;
|
|
}
|
|
.grid button strong {
|
|
grid-column: 2;
|
|
font-family: 'SF Mono', Menlo, monospace;
|
|
font-size: 0.85rem;
|
|
}
|
|
.grid button small {
|
|
grid-column: 2;
|
|
color: #586069;
|
|
font-size: 0.7rem;
|
|
}
|
|
.grid button.active {
|
|
background: #1f3b2a;
|
|
color: #f4ffe7;
|
|
border-color: #1f3b2a;
|
|
}
|
|
.grid button.active small {
|
|
color: rgba(244, 255, 231, 0.7);
|
|
}
|
|
.active-line {
|
|
margin: 0.8rem 0 0;
|
|
font-size: 0.9rem;
|
|
}
|
|
.row {
|
|
display: flex;
|
|
gap: 0.8rem;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
margin: 0.5rem 0;
|
|
}
|
|
.row + .row {
|
|
border-top: 1px dashed #eee;
|
|
padding-top: 0.6rem;
|
|
}
|
|
label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.4rem;
|
|
}
|
|
label span {
|
|
font-size: 0.85rem;
|
|
font-weight: 500;
|
|
min-width: 90px;
|
|
}
|
|
input[type='text'],
|
|
input[type='number'] {
|
|
padding: 0.35rem 0.6rem;
|
|
border: 1px solid #d1d5da;
|
|
border-radius: 4px;
|
|
font-family: inherit;
|
|
font-size: 0.9rem;
|
|
}
|
|
.result {
|
|
margin: 0;
|
|
font-size: 0.95rem;
|
|
}
|
|
button.primary {
|
|
background: #2ea44f;
|
|
color: #fff;
|
|
border: 1px solid #2a8644;
|
|
padding: 0.5rem 1rem;
|
|
border-radius: 5px;
|
|
cursor: pointer;
|
|
font: inherit;
|
|
}
|
|
.row > button {
|
|
padding: 0.4rem 0.8rem;
|
|
border: 1px solid #d1d5da;
|
|
border-radius: 5px;
|
|
background: #fafbfc;
|
|
cursor: pointer;
|
|
font: inherit;
|
|
font-size: 0.85rem;
|
|
}
|
|
.row > button.active {
|
|
background: #0366d6;
|
|
color: #fff;
|
|
border-color: #0366d6;
|
|
}
|
|
.kv {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: 0.9rem;
|
|
}
|
|
.kv td {
|
|
padding: 0.4rem 0.6rem;
|
|
border-bottom: 1px solid #eee;
|
|
}
|
|
.kv td:first-child {
|
|
color: #586069;
|
|
width: 40%;
|
|
}
|
|
.success {
|
|
background: #f0fff4;
|
|
color: #22863a;
|
|
padding: 0.5rem 0.8rem;
|
|
border-radius: 6px;
|
|
border-left: 3px solid #34d058;
|
|
margin: 0.8rem 0 0;
|
|
}
|
|
.issues {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0.8rem 0 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.3rem;
|
|
}
|
|
.issues li {
|
|
background: #ffeef0;
|
|
color: #86181d;
|
|
padding: 0.4rem 0.7rem;
|
|
border-radius: 4px;
|
|
border-left: 3px solid #d73a49;
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
align-items: baseline;
|
|
flex-wrap: wrap;
|
|
font-size: 0.85rem;
|
|
}
|
|
.issues .path {
|
|
background: #fff;
|
|
color: #24292e;
|
|
padding: 0.05em 0.4em;
|
|
border-radius: 3px;
|
|
font-size: 0.75rem;
|
|
}
|
|
.issues .code {
|
|
background: #fff;
|
|
color: #d73a49;
|
|
padding: 0.05em 0.4em;
|
|
border-radius: 3px;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
}
|
|
.logger-head {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
}
|
|
.logger-head button {
|
|
padding: 0.3rem 0.6rem;
|
|
border: 1px solid #d1d5da;
|
|
background: #fafbfc;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 0.8rem;
|
|
}
|
|
.empty {
|
|
color: #586069;
|
|
font-style: italic;
|
|
font-size: 0.85rem;
|
|
}
|
|
.logs {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: 0.82rem;
|
|
}
|
|
.logs th,
|
|
.logs td {
|
|
padding: 0.4rem 0.5rem;
|
|
text-align: left;
|
|
border-bottom: 1px solid #eee;
|
|
vertical-align: top;
|
|
}
|
|
.logs th {
|
|
background: #f6f8fa;
|
|
font-size: 0.7rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
color: #586069;
|
|
}
|
|
:global(main[data-theme='ink']) .logs th {
|
|
background: #2c3144;
|
|
color: #aab2c8;
|
|
}
|
|
.level {
|
|
display: inline-block;
|
|
padding: 0.05em 0.5em;
|
|
border-radius: 3px;
|
|
color: #fff;
|
|
font-size: 0.7rem;
|
|
font-weight: 600;
|
|
}
|
|
.muted {
|
|
color: #bbb;
|
|
}
|
|
.ctx {
|
|
font-size: 0.7rem;
|
|
max-width: 320px;
|
|
display: inline-block;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
vertical-align: middle;
|
|
}
|
|
code {
|
|
background: rgba(31, 59, 42, 0.08);
|
|
padding: 0.05em 0.35em;
|
|
border-radius: 4px;
|
|
font-family: 'SF Mono', Menlo, monospace;
|
|
font-size: 0.85em;
|
|
}
|
|
:global(main[data-theme='ink']) code {
|
|
background: rgba(255, 255, 255, 0.12);
|
|
color: #c9d6ff;
|
|
}
|
|
form {
|
|
background: #f6f8fa;
|
|
padding: 0.8rem 1rem;
|
|
border-radius: 8px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
}
|
|
:global(main[data-theme='ink']) form {
|
|
background: rgba(255, 255, 255, 0.06);
|
|
}
|
|
</style>
|