Author SHA1 Message Date
jb 904cb073a2 prototype: compact variant switcher 2026-08-23 19:50:18 -04:00
jb fe06b90117 prototype: isolate model status fixtures 2026-08-23 18:30:02 -04:00
jb e5213248a8 prototype: explore public model status layouts 2026-08-23 14:46:30 -04:00
10 changed files with 414 additions and 1 deletions
+10
View File
@@ -1,3 +1,4 @@
import { dev } from '$app/environment';
import { json, type Handle, type HandleServerError } from '@sveltejs/kit';
import { stringify } from 'devalue';
import { randomUUID } from 'node:crypto';
@@ -15,6 +16,15 @@ import {
} from '$lib/server/data-moderation';
export const handle: Handle = async ({ event, resolve }) => {
// THROWAWAY PROTOTYPE: fixture-only review must not open the shared database.
if (
dev &&
event.url.pathname === '/database' &&
event.url.searchParams.get('prototype') === 'model-status'
) {
return resolve(event);
}
if (
event.url.pathname === '/admin' ||
event.url.pathname.startsWith('/admin/')
+17 -1
View File
@@ -1,10 +1,26 @@
import { dev } from '$app/environment';
import { getSessionUser } from '$lib/server/auth/session';
import { hasPermission } from '$lib/server/auth/authorization';
import type { LayoutServerLoad } from './$types';
import { notificationSummary } from '$lib/server/notifications';
import { getSiteAccessSettings } from '$lib/server/site-access';
export const load: LayoutServerLoad = async ({ cookies }) => {
export const load: LayoutServerLoad = async ({ cookies, url }) => {
// THROWAWAY PROTOTYPE: fixture-only review must not open the shared database.
if (dev && url.pathname === '/database' && url.searchParams.get('prototype') === 'model-status') {
return {
user: null,
isacAccess: false,
notifications: { unread: 0 },
siteAccess: {
comingSoonEnabled: false,
accountCreationEnabled: true,
sections: { news: true, database: true, builds: true, guides: true, builder: true },
updatedAt: 'prototype'
}
};
}
const user = await getSessionUser(cookies);
return {
user,
+6
View File
@@ -1,3 +1,4 @@
import { dev } from '$app/environment';
import { getDatabase } from '$lib/server/database';
import { getRegistrySearch } from '$lib/server/pagination';
import { databaseCategories } from '$lib/database/categories';
@@ -16,6 +17,11 @@ const tables = {
} as const;
export const load: PageServerLoad = async ({ url }) => {
// THROWAWAY PROTOTYPE: avoid opening the shared database for fixture-only review.
if (dev && url.searchParams.get('prototype') === 'model-status') {
return { registries: [], total: 0, search: '', searchResults: [] };
}
const database = await getDatabase();
const { search, pattern } = getRegistrySearch(url);
const counts = await Promise.all(
+14
View File
@@ -1,6 +1,16 @@
<script lang="ts">
import { dev } from '$app/environment';
import { page } from '$app/state';
import ModelStatusPrototype from './ModelStatusPrototype.prototype.svelte';
import type { PrototypeVariant } from './model-status.prototype';
let { data } = $props();
const onlineRegistries = $derived(data.registries.filter((registry) => registry.count > 0).length);
const prototypeVariant = $derived.by<PrototypeVariant | null>(() => {
if (!dev || page.url.searchParams.get('prototype') !== 'model-status') return null;
const variant = page.url.searchParams.get('variant');
return variant === 'A' || variant === 'B' || variant === 'C' ? variant : null;
});
</script>
<svelte:head>
@@ -8,6 +18,9 @@
<meta name="description" content="Community-maintained The Division Resurgence attributes, talents, brands, gear, weapons, mods, protocols, skill chips, and specializations data."/>
</svelte:head>
{#if prototypeVariant}
<ModelStatusPrototype variant={prototypeVariant} />
{:else}
<main class="page community-index-page">
<header class="catalogue-hero community-index-hero">
<div><p class="eyebrow"><span></span> ISAC PUBLIC ARCHIVE</p><h1>RESURGENCE <em>DATABASE.</em></h1><p>Explore equipment, talents, modifications, and gameplay effects from The Division Resurgence.</p></div>
@@ -54,3 +67,4 @@
{/each}
</section>
</main>
{/if}
@@ -0,0 +1,21 @@
<script lang="ts">
import ModelStatusPrototypeSwitcher from './ModelStatusPrototypeSwitcher.prototype.svelte';
import ModelStatusVariantA from './ModelStatusVariantA.prototype.svelte';
import ModelStatusVariantB from './ModelStatusVariantB.prototype.svelte';
import ModelStatusVariantC from './ModelStatusVariantC.prototype.svelte';
import type { PrototypeVariant } from './model-status.prototype';
let { variant }: { variant: PrototypeVariant } = $props();
</script>
<!-- THROWAWAY PROTOTYPE: three issue #5 variants on the existing /database route. -->
<main class="page model-status-prototype">
{#if variant === 'A'}
<ModelStatusVariantA />
{:else if variant === 'B'}
<ModelStatusVariantB />
{:else}
<ModelStatusVariantC />
{/if}
</main>
<ModelStatusPrototypeSwitcher current={variant} />
@@ -0,0 +1,45 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/state';
import { onMount } from 'svelte';
import { prototypeVariantNames, type PrototypeVariant } from './model-status.prototype';
let { current }: { current: PrototypeVariant } = $props();
const variants: PrototypeVariant[] = ['A', 'B', 'C'];
const cycle = (direction: -1 | 1) => {
const currentIndex = variants.indexOf(current);
const next = variants[(currentIndex + direction + variants.length) % variants.length];
const params = new URLSearchParams(page.url.searchParams);
params.set('prototype', 'model-status');
params.set('variant', next);
void goto(`${page.url.pathname}?${params.toString()}${page.url.hash}`, {
keepFocus: true,
noScroll: true,
replaceState: true
});
};
onMount(() => {
const handleKeydown = (event: KeyboardEvent) => {
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return;
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;
if (event.target instanceof HTMLElement && event.target.closest('input, textarea, select, [contenteditable]:not([contenteditable="false"])')) return;
event.preventDefault();
cycle(event.key === 'ArrowLeft' ? -1 : 1);
};
window.addEventListener('keydown', handleKeydown);
return () => window.removeEventListener('keydown', handleKeydown);
});
</script>
<!-- THROWAWAY PROTOTYPE: development-only variant switcher for issue #5. -->
<nav class="prototype-switcher" aria-label="Model status prototype variants">
<button type="button" onclick={() => cycle(-1)} aria-label="Show previous prototype variant">←</button>
<strong aria-live="polite" title={prototypeVariantNames[current]} aria-label={`Variant ${current}: ${prototypeVariantNames[current]}`}>{current}</strong>
<button type="button" onclick={() => cycle(1)} aria-label="Show next prototype variant">→</button>
</nav>
<style>
.prototype-switcher{position:fixed;z-index:1000;right:14px;bottom:14px;display:grid;grid-template-columns:36px 34px 36px;height:36px;border:1px solid var(--shd-border-strong);box-shadow:0 7px 20px #000a;background:var(--shd-bg);color:var(--shd-text)}.prototype-switcher button{display:grid;place-items:center;border:0;background:var(--shd-surface-raised);color:var(--shd-orange);font:800 17px/1 var(--shd-font-mono);cursor:pointer}.prototype-switcher button:first-child{border-right:1px solid var(--shd-border)}.prototype-switcher button:last-child{border-left:1px solid var(--shd-border)}.prototype-switcher button:hover{background:var(--shd-surface-active);color:var(--shd-orange-hover)}.prototype-switcher button:active{transform:translateY(1px);filter:brightness(.82)}.prototype-switcher button:focus-visible{position:relative;z-index:1;outline:2px solid var(--shd-focus-ring);outline-offset:2px}.prototype-switcher>strong{display:grid;place-items:center;color:var(--shd-text);font:900 12px/1 var(--shd-font-mono);letter-spacing:.6px}@media(max-width:520px){.prototype-switcher{right:8px;bottom:8px}}@media(prefers-reduced-motion:reduce){.prototype-switcher button{transition:none}}
</style>
@@ -0,0 +1,78 @@
<script lang="ts">
import {
modelStatusFixtures,
publicBehavior,
publicStatus
} from './model-status.prototype';
const quickViewFixtures = [modelStatusFixtures[0], modelStatusFixtures[3]];
</script>
<!-- THROWAWAY PROTOTYPE: issue #5, variant A. -->
<div class="prototype-a">
<header class="prototype-heading">
<div>
<p><span></span> DEVELOPMENT PROTOTYPE · ISSUE #5 · VARIANT A</p>
<h1>MODELING <em>STATUS.</em></h1>
</div>
<p>Integrated card hierarchy keeps the public state in the identity header and gives behavior one primary reading block.</p>
</header>
<section aria-labelledby="a-card-heading">
<header class="surface-heading">
<div><span>01 / CARD PRESENTATION</span><h2 id="a-card-heading">DATABASE CARDS</h2></div>
<small>ALL MECHANIC OWNERS · PUBLIC STATE ONLY</small>
</header>
<div class="card-grid">
{#each modelStatusFixtures as fixture}
<article class="model-card">
<header>
<div><small>{fixture.owner}</small><h3>{fixture.name}</h3><span>{fixture.context}</span></div>
<strong class:not-modeled={publicStatus(fixture) === 'NOT MODELED'}>{publicStatus(fixture)}</strong>
</header>
<div class="behavior-block">
<small>{publicStatus(fixture) === 'MODELED' ? 'MODELED BEHAVIOR' : 'AUTHORED DESCRIPTION'}</small>
<p>{publicBehavior(fixture)}</p>
</div>
<footer><span>READ-ONLY FIXTURE</span><b>VIEW DETAILS →</b></footer>
</article>
{/each}
</div>
</section>
<section aria-labelledby="a-detail-heading">
<header class="surface-heading">
<div><span>02 / DETAIL PRESENTATION</span><h2 id="a-detail-heading">QUICK VIEW</h2></div>
<small>MODELED TEXT FIRST · AUTHORED FALLBACK WHEN NOT MODELED</small>
</header>
<div class="quick-view">
{#each quickViewFixtures as fixture}
<article>
<header><div><small>{fixture.owner}</small><h3>{fixture.name}</h3></div><strong class:not-modeled={publicStatus(fixture) === 'NOT MODELED'}>{publicStatus(fixture)}</strong></header>
<div><span>BEHAVIOR</span><p>{publicBehavior(fixture)}</p></div>
<dl><div><dt>RECORD</dt><dd>{fixture.context}</dd></div><div><dt>DISPLAY SOURCE</dt><dd>{publicStatus(fixture) === 'MODELED' ? 'Modeled behavior' : 'Authored description'}</dd></div></dl>
</article>
{/each}
</div>
</section>
<section aria-labelledby="a-search-heading">
<header class="surface-heading">
<div><span>03 / GLOBAL SEARCH PRESENTATION</span><h2 id="a-search-heading">SEARCH RESULTS</h2></div>
<small>{modelStatusFixtures.length} FIXTURE MATCHES</small>
</header>
<div class="search-results">
{#each modelStatusFixtures as fixture}
<div class="search-result">
<small>{fixture.owner}</small><strong>{fixture.name}</strong><span>{publicBehavior(fixture)}</span><b class:not-modeled={publicStatus(fixture) === 'NOT MODELED'}>{publicStatus(fixture)}</b>
</div>
{/each}
</div>
</section>
</div>
<style>
.prototype-a{display:grid;gap:28px;padding:20px 0 110px}.prototype-heading{min-height:150px;display:grid;grid-template-columns:minmax(0,1fr) minmax(280px,420px);align-items:end;gap:24px;padding:24px;border:1px solid var(--shd-border);border-top:2px solid var(--shd-orange);background:var(--shd-surface)}.prototype-heading p{margin:0;color:var(--shd-text-muted);font:var(--shd-type-supporting)/1.5 var(--shd-font-editorial)}.prototype-heading>div>p{color:var(--shd-orange);font:900 var(--shd-type-telemetry)/1 var(--shd-font-sans);letter-spacing:1px}.prototype-heading>div>p span{display:inline-block;width:22px;border-top:2px solid currentColor;vertical-align:middle}.prototype-heading h1{margin:10px 0 0;font-size:clamp(34px,5vw,64px);line-height:.9;letter-spacing:-1px}.prototype-heading h1 em{color:var(--shd-orange);font-style:normal}.prototype-a>section{display:grid;gap:10px}.surface-heading{min-height:46px;display:flex;align-items:end;justify-content:space-between;gap:16px;padding-bottom:9px;border-bottom:1px solid var(--shd-border)}.surface-heading>div{display:flex;align-items:baseline;gap:12px}.surface-heading span,.surface-heading small{color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);font-weight:800;letter-spacing:.8px}.surface-heading h2{margin:0;font-size:20px;letter-spacing:.6px}.card-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.model-card{min-width:0;display:flex;flex-direction:column;border:1px solid var(--shd-border);border-top:2px solid var(--shd-border-strong);background:var(--shd-surface)}.model-card>header{min-height:92px;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:start;gap:10px;padding:14px;border-bottom:1px solid var(--shd-border)}.model-card header small,.quick-view header small{display:block;margin-bottom:6px;color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.8px}.model-card h3,.quick-view h3{margin:0;font-size:18px;line-height:1.05}.model-card header span{display:block;margin-top:7px;color:var(--shd-text-dim);font-size:var(--shd-type-telemetry)}.model-card header strong,.quick-view header strong,.search-result b{padding:5px 7px;border:1px solid var(--shd-success);color:var(--shd-success);font-size:10px;line-height:1;letter-spacing:.7px;white-space:nowrap}.model-card header strong.not-modeled,.quick-view header strong.not-modeled,.search-result b.not-modeled{border-color:var(--shd-border-strong);color:var(--shd-text-muted)}.behavior-block{flex:1;margin:12px;padding:12px;border-left:2px solid var(--shd-success);background:var(--shd-model-surface)}.model-card:has(.not-modeled) .behavior-block{border-left-color:var(--shd-text-dim)}.behavior-block small{color:var(--shd-text-dim);font-size:10px;font-weight:900;letter-spacing:.9px}.behavior-block p{margin:7px 0 0;color:var(--shd-text);font:var(--shd-type-supporting)/1.5 var(--shd-font-editorial)}.model-card>footer{display:flex;justify-content:space-between;gap:10px;padding:10px 12px;border-top:1px solid var(--shd-border-soft);color:var(--shd-text-dim);font-size:10px;font-weight:900;letter-spacing:.6px}.model-card>footer b{color:var(--shd-orange)}.quick-view{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.quick-view>article{border:1px solid var(--shd-border);background:var(--shd-surface-raised)}.quick-view article>header{display:flex;align-items:start;justify-content:space-between;gap:16px;padding:16px;border-bottom:1px solid var(--shd-border)}.quick-view article>div{padding:18px 16px}.quick-view article>div span{color:var(--shd-text-muted);font-size:10px;font-weight:900;letter-spacing:1px}.quick-view article>div p{max-width:62ch;margin:8px 0 0;font:18px/1.55 var(--shd-font-editorial)}.quick-view dl{display:grid;grid-template-columns:1fr 1fr;margin:0;border-top:1px solid var(--shd-border)}.quick-view dl>div{padding:10px 16px;border-left:1px solid var(--shd-border)}.quick-view dl>div:first-child{border-left:0}.quick-view dt{color:var(--shd-text-dim);font-size:10px;font-weight:900;letter-spacing:.8px}.quick-view dd{margin:4px 0 0;color:var(--shd-text-muted);font-size:var(--shd-type-telemetry)}.search-results{border:1px solid var(--shd-border);background:var(--shd-model-surface)}.search-result{display:grid;grid-template-columns:190px minmax(150px,.7fr) minmax(260px,1.6fr) auto;align-items:center;gap:12px;min-height:58px;padding:8px 12px;border-top:1px solid var(--shd-border-soft)}.search-result:first-child{border-top:0}.search-result>small{color:var(--shd-orange);font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.5px}.search-result>strong{font-size:var(--shd-type-supporting)}.search-result>span{color:var(--shd-text-muted);font:var(--shd-type-telemetry)/1.4 var(--shd-font-editorial)}
@media(max-width:1000px){.card-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.search-result{grid-template-columns:minmax(140px,.7fr) minmax(0,1fr) auto}.search-result>small{grid-column:1/-1}.search-result>span{grid-column:2}}
@media(max-width:700px){.prototype-heading,.quick-view,.card-grid{grid-template-columns:1fr}.prototype-heading{align-items:start}.surface-heading{align-items:start}.surface-heading>div{display:grid;gap:4px}.surface-heading>small{display:none}.search-result{grid-template-columns:1fr auto}.search-result>small,.search-result>span{grid-column:1/-1}}
</style>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,93 @@
// THROWAWAY PROTOTYPE for issue #5. Fixture lifecycle data must never become a public API.
export type PrototypeVariant = 'A' | 'B' | 'C';
export type PublicModelStatus = 'MODELED' | 'NOT MODELED';
type MechanicLifecycle =
'ready' | 'requires_assumptions' | 'captured_only' | 'absent';
export type ModelStatusFixture = {
id: string;
owner:
| 'Talent'
| 'OS Protocol Talent'
| 'Skill Chip Talent'
| 'Specialization tree-rank effect';
name: string;
context: string;
internal: {
lifecycle: MechanicLifecycle;
current: boolean;
};
modeledText?: string;
authoredText: string;
};
export const modelStatusFixtures: ModelStatusFixture[] = [
{
id: 'talent-ready',
owner: 'Talent',
name: 'Predatory Advance',
context: 'Weapon Talent · Tier I',
internal: { lifecycle: 'ready', current: true },
modeledText:
'On enemy kill: gain +15% Weapon Damage for 10 s. Refreshes and stacks up to 3 times.',
authoredText:
'Killing an enemy grants 15% weapon damage for 10s. Stacks up to 3 times.',
},
{
id: 'protocol-assumptions',
owner: 'OS Protocol Talent',
name: 'Emergency Weave',
context: 'Superior OS Protocol · Talent',
internal: { lifecycle: 'requires_assumptions', current: true },
modeledText:
'When armor breaks: restore 20% Armor over 5 s. Cooldown: 20 s.',
authoredText:
'When armor is broken, restore 20% armor over 5s. Cooldown: 20s.',
},
{
id: 'chip-captured',
owner: 'Skill Chip Talent',
name: 'Rapid Instruction',
context: 'Tactical Skill Chip Set · 3-piece talent',
internal: { lifecycle: 'captured_only', current: true },
modeledText: 'After skill use: gain +8% Skill Haste for 12 s.',
authoredText: 'Using a skill grants 8% Skill Haste for 12s.',
},
{
id: 'specialization-absent',
owner: 'Specialization tree-rank effect',
name: 'Perfect Cadence · Rank 3',
context: 'Sharpshooter · Precision branch',
internal: { lifecycle: 'absent', current: true },
authoredText:
'After reaching 5 hits without missing, your next shot deals 25% increased damage.',
},
{
id: 'talent-stale',
owner: 'Talent',
name: 'Measured Focus',
context: 'Gear Talent · Tier II',
internal: { lifecycle: 'ready', current: false },
modeledText:
'After a headshot: gain +8% Critical Hit Damage for 8 s, stacking up to 3 times.',
authoredText:
'Headshots grant 10% Critical Hit Damage for 8s. Stacks up to 4 times.',
},
];
export const publicStatus = (fixture: ModelStatusFixture): PublicModelStatus =>
fixture.internal.current && fixture.internal.lifecycle !== 'absent'
? 'MODELED'
: 'NOT MODELED';
export const publicBehavior = (fixture: ModelStatusFixture): string =>
publicStatus(fixture) === 'MODELED' && fixture.modeledText
? fixture.modeledText
: fixture.authoredText;
export const prototypeVariantNames = {
A: 'Integrated card hierarchy',
B: 'Evidence rail',
C: 'Registry rows',
} satisfies Record<PrototypeVariant, string>;