From a434cbe60cf1a3e1461fde0b873a41bf65398514 Mon Sep 17 00:00:00 2001 From: JB Date: Tue, 28 Jul 2026 00:11:40 -0400 Subject: [PATCH] feat: expand profiles and weapon configuration --- database/schema.sql | 19 +- docs/DESIGN_SYSTEM.md | 171 ++++++-- docs/architecture.md | 29 +- scripts/db-migrate.mjs | 48 +++ scripts/isac-data-tables.mjs | 2 +- src/app.css | 32 ++ src/lib/components/AccountControl.svelte | 188 +++++++++ src/lib/components/AgentIdentityLink.svelte | 34 +- src/lib/components/CommentComposer.svelte | 51 +++ src/lib/components/ContentReport.svelte | 68 ++-- src/lib/components/InfoPopover.svelte | 48 +++ src/lib/components/MarkdownContent.svelte | 2 + .../components/ModerationDisclosure.svelte | 7 +- src/lib/components/OverflowMenu.svelte | 89 +++++ .../components/SearchableAssignment.svelte | 54 ++- src/lib/server/account-control-data.ts | 67 ++++ src/lib/server/profile-comment-follows.ts | 72 ++++ src/lib/server/profile-comment-thread.test.ts | 86 +++++ src/lib/server/profile-comment-thread.ts | 106 +++++ src/lib/server/profile-settings.ts | 73 ++++ src/lib/server/shared-build-combat.ts | 8 +- src/lib/weapons/category-bonuses.test.ts | 29 ++ src/lib/weapons/category-bonuses.ts | 13 + src/lib/weapons/mod-slots.test.ts | 77 ++++ src/lib/weapons/mod-slots.ts | 100 +++++ src/routes/+layout.svelte | 2 + src/routes/account/+page.server.ts | 178 ++------- src/routes/account/+page.svelte | 304 --------------- src/routes/agents/[id]/+page.server.ts | 365 +++++++++++++++--- src/routes/agents/[id]/+page.svelte | 363 ++++++++++++----- src/routes/builder/+page.server.ts | 10 +- .../database/[category]/+page.server.ts | 2 +- src/routes/isac/community/+page.server.ts | 74 +++- src/routes/isac/gear/+page.server.ts | 19 +- src/routes/isac/gear/+page.svelte | 2 +- src/routes/isac/weapons/+page.server.ts | 117 ++++-- src/routes/isac/weapons/+page.svelte | 100 +++-- 37 files changed, 2209 insertions(+), 800 deletions(-) create mode 100644 src/lib/components/AccountControl.svelte create mode 100644 src/lib/components/CommentComposer.svelte create mode 100644 src/lib/components/InfoPopover.svelte create mode 100644 src/lib/components/OverflowMenu.svelte create mode 100644 src/lib/server/account-control-data.ts create mode 100644 src/lib/server/profile-comment-follows.ts create mode 100644 src/lib/server/profile-comment-thread.test.ts create mode 100644 src/lib/server/profile-comment-thread.ts create mode 100644 src/lib/server/profile-settings.ts create mode 100644 src/lib/weapons/category-bonuses.test.ts create mode 100644 src/lib/weapons/category-bonuses.ts create mode 100644 src/lib/weapons/mod-slots.test.ts create mode 100644 src/lib/weapons/mod-slots.ts delete mode 100644 src/routes/account/+page.svelte diff --git a/database/schema.sql b/database/schema.sql index 40a7509..d7e7d8e 100644 --- a/database/schema.sql +++ b/database/schema.sql @@ -164,6 +164,14 @@ CREATE TABLE IF NOT EXISTS comments ( ); CREATE INDEX IF NOT EXISTS comments_thread_status_created_idx ON comments(thread_id, status, created_at DESC); CREATE INDEX IF NOT EXISTS comments_author_status_created_idx ON comments(author_id, status, created_at DESC); +CREATE TABLE IF NOT EXISTS comment_thread_follows ( + user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + root_comment_id TEXT NOT NULL REFERENCES comments(id) ON DELETE CASCADE, + created_at TEXT NOT NULL, + PRIMARY KEY(user_id, root_comment_id) +); +CREATE INDEX IF NOT EXISTS comment_thread_follows_root_idx + ON comment_thread_follows(root_comment_id, user_id); CREATE TABLE IF NOT EXISTS community_moderation_settings ( id TEXT PRIMARY KEY CHECK(id='primary'), automated_enabled INTEGER NOT NULL DEFAULT 0 CHECK(automated_enabled IN (0,1)), @@ -859,14 +867,15 @@ CREATE TABLE IF NOT EXISTS weapon_augmentation_effects ( UNIQUE(weapon_id, sort_order), UNIQUE(weapon_id, stat_attribute_id) ); -CREATE TABLE IF NOT EXISTS weapon_base_attribute_bonuses ( +CREATE TABLE IF NOT EXISTS weapon_category_attribute_bonuses ( id TEXT PRIMARY KEY, - weapon_id TEXT NOT NULL REFERENCES weapons(id) ON DELETE CASCADE, + weapon_category TEXT NOT NULL CHECK(weapon_category IN ('Shotgun','Rifle','MMR','AR','LMG','SMG')), stat_attribute_id TEXT NOT NULL REFERENCES stat_attributes(id), value REAL NOT NULL CHECK(value > 0), - sort_order INTEGER NOT NULL CHECK(sort_order >= 1), - UNIQUE(weapon_id, stat_attribute_id), - UNIQUE(weapon_id, sort_order) + sort_order INTEGER NOT NULL CHECK(sort_order BETWEEN 1 AND 3), + updated_at TEXT NOT NULL, + UNIQUE(weapon_category, stat_attribute_id), + UNIQUE(weapon_category, sort_order) ); CREATE TABLE IF NOT EXISTS os_protocols ( id TEXT PRIMARY KEY, diff --git a/docs/DESIGN_SYSTEM.md b/docs/DESIGN_SYSTEM.md index 62aa092..36a705d 100644 --- a/docs/DESIGN_SYSTEM.md +++ b/docs/DESIGN_SYSTEM.md @@ -389,9 +389,13 @@ direct URL. Do not present authentication as a way around a disabled section. ### Agent and authentication surfaces -- Account pages should read as an agent dossier: identity and access at the top, - activity metrics beside it, archives in the primary column, and security - controls in a narrower console column. +- The canonical signed-in account surface is the owner’s public Agent profile at + `/agents/{slug}`. `/account` is a compatibility route that redirects there; + the full notification inbox remains at `/account/notifications`. +- Owners see the same dossier and conversation that visitors see, plus inline + bio editing and a compact Account Control widget in the right rail. Do not + create a separate profile-editing dashboard or remove community context while + the owner manages their profile. - Dense account consoles use a tabbed control surface for profile, credentials, linked identities, and account lifecycle actions. Only one panel is shown at a time; validation failures reopen the panel containing the failed action. @@ -446,8 +450,10 @@ direct URL. Do not present authentication as a way around a disabled section. - ISAC user dossiers reuse the same linked-identity node. Administrative unlinking requires a scoped identity-management capability, produces an audit event, and must never remove the user's only available sign-in method. -- On small screens, the split frame becomes a single column and account controls - move before archival content. +- On small screens, the split frame becomes one column. Show the public dossier + and archives first, followed by Account Control, notifications, and Recent + Discussion. Place Profile Messages at the bottom. Give owners a mobile-only + header link to Account Control. - Public display names and private login usernames are distinct. Display names support normalized Unicode and relaxed visible characters. They are unique under the product's normalized identity key and generate readable, @@ -458,38 +464,89 @@ direct URL. Do not present authentication as a way around a disabled section. ### Public Agent profiles -Public Agent profiles are community dossiers, not account-control screens. Lead -with the shared `AgentIdentityLink` in its large state: circular portrait, -display name, explicit `SHD.BUILD AGENT PROFILE` context, and a directional -link cue. Place assigned site-role badges beneath the identity as a labeled -semantic list. Show role names only, in alphabetical order, and omit the list -when the Agent has no assigned roles. Badges use the shared raised surface, -explicit role text, and a small orange diamond; they wrap without truncating -long or multiple role names. Never expose role descriptions, permissions, -direct grants, or access scopes on public profiles. The profile owner may -receive an `EDIT MY PROFILE` action in the header. Keep public content totals -in a compact telemetry strip below the identity header, then place the field -bio, public builds, published guides, and recent discussion in the primary -column. Profile messages occupy the narrower communications column and may -remain sticky where the viewport has room. +Public Agent profiles are community dossiers. The owner view extends the same +page with account controls without changing its public hierarchy. Lead with the +shared `AgentIdentityLink` in its large state: circular portrait, display name, +and assigned role badges. Place role badges to the right of the display name on +desktop and below the name on mobile. Show role names only, in alphabetical +order, and omit the list when the Agent has no assigned roles. Badges use the +shared raised surface, explicit role text, and a small orange +diamond; they wrap without truncating long or multiple role names. Never expose +role descriptions, permissions, direct grants, or access scopes on public +profiles. The owner header contains the visibility switch, its information +control, and a mobile-only Account Control link. Present the primary identity +and its actions directly on the page canvas +without a card surface, border, or decorative accent rule. Give the identity and +actions content-sized responsive tracks so long names, multiple roles, and +mobile actions cannot overlap the public content totals. Keep those totals in a +compact telemetry strip below the identity header. Use four columns on desktop +and two columns on mobile. Place the field bio in the primary column and keep +its mobile Edit Bio action compact within the section header. Put Public Builds +and Published Guides side by side directly +beneath the bio; stack them on narrow screens. Their cards stay within their own +half of the primary column. Profile Messages follow the archive pair within the +same column and must not inherit height from the right rail. + +Recent Discussion occupies the narrower activity rail. Owner-only Account +Control and notification-preview widgets appear above it. The profile preview +shows unread notifications only; the full inbox retains notification history. +Recent-discussion +entries use compact destination cards because each entry links to content +elsewhere on the site. Show only the discussion type, destination title, comment +date, and open action; do not repeat the message body in this rail. Reset nested +card headers to content height and align the destination identity with its date; +do not inherit the application header's fixed height or surface treatment. +Profile messages continue directly +beneath the biography in the primary column so the conversation begins without +dead space. Use `AgentIdentityLink` for author attribution on builds, guides, comments, and other community records. Its compact and default states combine the circular -portrait, public display name, optional contextual metadata, and a clear link -cue. Hover and focus-visible states may turn the name and cue orange. The -portrait remains circular at every size; selectors may use square tactical -thumbnails only where the avatar rules explicitly allow them. +portrait, public display name, assigned public role badges, and optional +contextual metadata. Role badges remain beside the display name in comment +headers and other author attributions; an empty role list renders no badge. The +display name supplies the link affordance through its hover and focus-visible +states; do not add a trailing arrow. Use the primary orange token for the +portrait border, with a two-pixel edge that remains legible against the page +canvas. The default author portrait is 44 pixels; compact identities may use 30 +pixels. The portrait remains circular at every size; selectors may use square +tactical thumbnails only where the avatar rules explicitly allow them. Public-profile links use the stored readable slug, URL-encoded as one path segment. A display-name change updates the canonical slug. Legacy UUID routes remain valid and redirect to the canonical address; UUIDs remain the internal identity for mutations, relationships, and audit evidence. -Public biographies and community comments use the shared `MarkdownEditor` and -`MarkdownContent` pair. The editor supplies a small formatting toolbar, -remaining-character count, concise help, a readable editorial typeface, and a -minimum 44px touch target for toolbar actions on narrow screens. It supports -headings, lists, quotes, links, emphasis, and inline code. Raw HTML is escaped. +Public biographies use the shared `MarkdownEditor` and `MarkdownContent` pair. +The editor supplies formatting controls, a remaining-character count, concise +help, and a readable editorial typeface. Profile messages and replies use the +compact `CommentComposer`: one textarea, a Markdown notice, a character count, +and submit or cancel actions. Rendered Markdown supports headings, lists, +quotes, links, bold, italics, and inline code. Load the required editorial font +weights and italic face because the global type system disables synthetic font +styles. Raw HTML is escaped. + +Profile conversations begin with a collapsed `LEAVE A MESSAGE` action. A reply +opens the same editor beneath the selected message, and only one editor remains +open. Comments sit on the page canvas without cards, section surfaces, or +detached author headers. Align each body and action row with the username rather +than the avatar gutter. Keep Reply visible below the message. Place Follow or +Unfollow, Report, and the author-only Remove action in a shared ellipsis menu +beside Reply. The menu uses a 44-pixel trigger, returns focus when +closed, supports Escape and directional-key navigation, and becomes a fixed +bottom panel on narrow screens. Keep destructive actions neutral at rest and use +the danger color on hover or keyboard focus. Render conversations +in depth-first reading order: newest root conversations first and each branch +oldest-first. Use one-pixel guide lines and restrained indentation for replies, +then cap visual indentation on narrow screens while retaining the logical +parent chain. If an author or moderator removes a message that has replies, show +a bodyless `MESSAGE REMOVED` placeholder so readers can follow the remaining +conversation. Following applies to the top-level conversation and sends inbox +notifications for visible descendant replies, excluding the reply author and +any recipient already receiving the direct-reply notification. Send reply +notifications to the direct parent author and link to the specific reply. +Recent-discussion links target the exact comment anchor instead of the +containing profile or content page. Public user-generated content uses the shared `ContentReport` control when the viewer is authenticated and does not own the content. Keep the control visually @@ -538,8 +595,10 @@ record is absent without implying an error. Profile visibility is an explicit public or hidden account setting. A hidden profile is excluded from public discovery and cannot receive new profile messages. Hiding it does not remove the Agent's display name or portrait from -public builds, guides, or comments they authored. State this consequence beside -the visibility control so the setting does not imply retroactive anonymity. +public builds, guides, or comments they authored. Put the compact textual +`PUBLIC` or `HIDDEN` switch in the owner’s identity header and explain the +consequence in an adjacent information popover. Display-name restrictions use +the same popover pattern instead of persistent instructional copy. Community forms use `PendingForm` to block duplicate submissions. Pending actions retain their label context, validation errors preserve entered text, @@ -808,8 +867,10 @@ Repository agents and contributors should follow the maintenance checklist in ### Agent avatars Agent portraits use a circular crop in identity headers and square tactical -thumbnails in selectors. Preset choices appear in a compact four-column grid; -the active choice uses the orange action border and an explicit pressed state. +thumbnails in selectors. Account Control shows the current portrait first and +keeps preset and upload controls behind an explicit Edit Avatar action. Preset +choices appear in a compact four-column grid; the active choice uses the orange +action border and an explicit pressed state. Custom images are normalized server-side to a metadata-free 256 × 256 WebP. Selectors must retain visible hover, focus-visible, pressed, pending, and disabled states, and collapse naturally with their containing account console. @@ -826,13 +887,12 @@ navigation links use the same height, typography, and interaction motion. Complex progression definitions use a stacked control-surface editor: identity and fixed values first, then parallel tier cards, mutually exclusive talent -pools, fixed base bonuses, and the augmentation sequence. Tier cards retain +pools, and the augmentation sequence. Tier cards retain identical row ordering so values can be compared horizontally on wide screens and stack without changing meaning on narrow screens. System-derived augment steps appear as read-only numbered cells; editable effects use the shared attribute/value row pattern. Exotic is a status layered onto a weapon family, -never a replacement for the family classification. Fixed base bonuses remain -visually separate from tactical augmentation effects. Exotic definitions add a +never a replacement for the family classification. Exotic definitions add a dedicated exotic-only talent after the two standard talent pools; their sixth augmentation cell communicates that this talent advances to Tier II. The exotic talent is owned by exactly one exotic weapon and is authored inline @@ -842,6 +902,14 @@ Tier I/II previews, and the shared source-owned mechanic program editor. The Tal Registry may display an owned exotic talent for discovery, but routes editing back to its weapon rather than opening the shared-talent editor. +Place shared weapon-category bonuses above the weapon registry in one collapsed +profile console. Its six category buttons show the three inherited Attribute +values and open one category editor. The editor keeps Attribute identities +fixed and exposes only their positive numeric values. Saving closes the editor; +validation preserves its values and error state. Standard and Exotic weapons +inherit the profile for their underlying Shotgun, Rifle, MMR, AR, LMG, or SMG +category. Do not repeat these controls inside individual weapon editors. + Large relationship sets use the searchable assignment control instead of checkbox walls. It keeps assigned records visible as removable chips, filters available records by identity, renders at most thirty results at once, and @@ -1426,6 +1494,25 @@ large enough that scanning a dropdown is inefficient. The selected record, pending-contribution state, keyboard focus, and remove interaction must remain explicit in both modes. +Records with established visual identity may supply an optional square icon. +Standard assignment lists show it at 32 pixels in result rows and 24 pixels in +selected chips. An icon-first multiselect may use the `visual-grid` presentation +for a finite visual registry. Its compact tiles use a 38-pixel icon without +persistent tile text. Hover and keyboard focus reveal the record name, while +the accessible label includes the name and assignment state. The orange inset +edge marks selected tiles. Weapon Talent 1 and Talent 2 pools and the Backpack +and Body Armor talent pools use this grid. Use the shared missing-item artwork +when a record lacks an icon. Render the full eligible set without a search, +result-count row, item cap, or internal scroll region; the section header owns +the selected count. Reflow tiles without changing keyboard order. + +Use the `toggle-grid` presentation for a small fixed registry whose text labels +are the primary recognition aid. Each 44-pixel-or-taller option uses checkbox +semantics and shows only the record name. The orange action edge marks the +selected state, and the accessible label includes that state. Weapon mod slot +selectors use this presentation so an operator can toggle each slot by clicking +its name. Keep all options visible and preserve DOM order as the grid reflows. + Every ISAC relationship to an Attribute uses `SearchableAssignment`; native attribute dropdowns are not permitted because the registry is expected to contain hundreds of records. Use full mode for primary editor sections and @@ -1679,10 +1766,16 @@ persistence controls and shared-loadout columns stack vertically. When compatibility depends on two independent dimensions, keep those dimensions explicit rather than presenting a flattened list. Weapon -definitions configure their available Weapon Mod slots per tier. T0 requires -four distinct slots; T1 and T2 require five. Weapon Mod definitions then -configure one slot plus one or more compatible weapon categories. The UI and -validation must require both dimensions to match. +definitions configure one shared set of five Weapon Mod slots for T1 and T2. +T0 uses the same set minus one explicitly selected slot. A T2-native weapon +shows only its applicable shared-slot control, and tiers below the native tier +remain unavailable. Operators may save an unverified shared set or T0 exclusion +when they cannot inspect the weapon in-game. Mark an active weapon `CONFIG +REQUIRED` until every available tier has its full slot count, and keep verified +assignments editable. Reject unknown slot types and counts above the tier limit. +Weapon Mod definitions then configure one slot plus one or more compatible +weapon categories. The UI and validation must require both dimensions to match +once the weapon definition is complete. Weapon Mod definitions use the shared segmented-choice control for the bounded eight-slot vocabulary, with a full-width row on desktop and two columns on diff --git a/docs/architecture.md b/docs/architecture.md index 6f87573..199f7aa 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -255,6 +255,26 @@ active Administrator. A later critical transition may alert again; repeated scheduler runs within the same episode do not. Provider errors preserve the last known balance and never expose the API key or raw response body. +# Weapon category bonuses + +`weapon_category_attribute_bonuses` stores three ordered fixed Attribute +contributions for each Shotgun, Rifle, MMR, AR, LMG, and SMG category. Each +profile grants 5% Weapon Critical Hit Chance and 50% Weapon Critical Hit Damage. +Shotgun, Rifle, AR, LMG, and SMG grant 40% Headshot Damage; MMR grants 70%. + +Weapon records do not own copies of these rows. Both builder catalog loaders +attach the matching category profile when they hydrate a weapon. Exotic weapons +retain an underlying category and inherit the same profile as a standard weapon +of that type. The combat summary preserves the existing equipped-weapon +semantics: each equipped weapon contributes its inherited profile to the +Agent's accumulated Attributes. + +ISAC exposes one category-profile editor above the weapon registry. Operators +may change the three positive values, while the Attribute identities and order +remain fixed. Each successful save appends a +`game_data/weapon_bonus_profile.updated` audit event with the complete previous +and resulting profile. + # Weapon-owned exotic talents Standard weapon talent pools remain reusable across weapons and use Talent 1 / @@ -291,9 +311,12 @@ the existing `game_data` audit history. ### Weapon Mods and tiered weapon slots `weapon_mod_slots` records each weapon's available slot vocabulary per tier. -Canonical validation requires four unique slots for T0 and five for T1/T2 for -every tier at or above the weapon's native tier. Regular Weapon Mods declare a -single slot type and one or more compatible base weapon categories through +The editor captures one shared set for T1/T2 and the single shared slot omitted +at T0, then expands that input into the normalized per-tier rows. Operators may +save partial rows when the in-game configuration is unavailable. ISAC derives +`CONFIG REQUIRED` until every available tier contains four unique slots at T0 +or five at T1/T2. Regular Weapon Mods declare a single slot type and one or more +compatible base weapon categories through `weapon_mod_compatible_categories`; eligibility requires both a category match and the slot to exist on the equipped weapon at its selected tier. diff --git a/scripts/db-migrate.mjs b/scripts/db-migrate.mjs index 49871b6..1722691 100644 --- a/scripts/db-migrate.mjs +++ b/scripts/db-migrate.mjs @@ -2,6 +2,7 @@ import { connect } from '@tursodatabase/database'; import { access, mkdir, readFile } from 'node:fs/promises'; import { dirname, resolve } from 'node:path'; import process from 'node:process'; +import { randomUUID } from 'node:crypto'; import { initializeDefaultUsers } from './default-users.mjs'; const gearSlots = ['Holster', 'Gloves', 'Backpack', 'Knee Pads', 'Body Armor', 'Mask']; @@ -386,6 +387,53 @@ for (const statement of statements) { await database.exec(statement); } +const weaponBonusAttributes = await ( + await database.prepare(`SELECT id,name FROM stat_attributes + WHERE name IN ('Weapon Critical Hit Chance','Weapon Critical Hit Damage','Headshot Damage')`) +).all(); +if (weaponBonusAttributes.length && weaponBonusAttributes.length !== 3) { + throw new Error('The shared weapon-category bonus migration requires all three fixed weapon Attributes.'); +} +if (weaponBonusAttributes.length === 3) { + const attributeId = (name) => String(weaponBonusAttributes.find((attribute) => attribute.name === name)?.id ?? ''); + const defaults = [ + ['Weapon Critical Hit Chance', 5], + ['Weapon Critical Hit Damage', 50], + ['Headshot Damage', 40] + ]; + await database.exec('BEGIN'); + try { + for (const category of ['Shotgun', 'Rifle', 'MMR', 'AR', 'LMG', 'SMG']) { + for (let index = 0; index < defaults.length; index += 1) { + const [name, defaultValue] = defaults[index]; + const value = category === 'MMR' && name === 'Headshot Damage' ? 70 : defaultValue; + await (await database.prepare(`INSERT OR IGNORE INTO weapon_category_attribute_bonuses( + id,weapon_category,stat_attribute_id,value,sort_order,updated_at + ) VALUES(?,?,?,?,?,?)`)).run( + randomUUID(), category, attributeId(name), value, index + 1, new Date().toISOString() + ); + } + } + const configured = await (await database.prepare( + 'SELECT COUNT(*) AS count FROM weapon_category_attribute_bonuses' + )).get(); + if (Number(configured?.count) !== 18) { + throw new Error('Every weapon category must have exactly three shared base Attribute bonuses.'); + } + const legacyWeaponBonuses = await (await database.prepare( + "SELECT 1 FROM sqlite_master WHERE type='table' AND name='weapon_base_attribute_bonuses'" + )).get(); + if (legacyWeaponBonuses) { + console.log('Replacing per-weapon base Attribute bonuses with shared weapon-category profiles.'); + await database.exec('DROP TABLE weapon_base_attribute_bonuses'); + } + await database.exec('COMMIT'); + } catch (error) { + await database.exec('ROLLBACK'); + throw error; + } +} + const deletedRoleTable = await (await database.prepare( "SELECT 1 FROM sqlite_master WHERE type='table' AND name='deleted_roles'" )).get(); diff --git a/scripts/isac-data-tables.mjs b/scripts/isac-data-tables.mjs index f7fc31a..29f66e9 100644 --- a/scripts/isac-data-tables.mjs +++ b/scripts/isac-data-tables.mjs @@ -22,7 +22,7 @@ export const ISAC_DATA_TABLES = [ 'weapon_tier_ranges', 'weapon_talent_options', 'weapon_augmentation_effects', - 'weapon_base_attribute_bonuses', + 'weapon_category_attribute_bonuses', 'os_protocols', 'os_protocol_primary_effects', 'os_protocol_secondary_profiles', diff --git a/src/app.css b/src/app.css index 9667f2e..3095942 100644 --- a/src/app.css +++ b/src/app.css @@ -2409,3 +2409,35 @@ .modeling-content{padding:10px} @media(max-width:560px){.modeling-disclosure>summary{min-height:56px}.modeling-content{padding:7px}} @media(prefers-reduced-motion:reduce){.modeling-disclosure>summary{transition:none}} + +/* Optional visual identity in shared assignment options. */ +.assignment-item-icon{flex:0 0 auto;object-fit:contain;background:var(--shd-bg)} +.assignment-chip-icon{width:24px;height:24px} +.assignment-results>button.with-icon{min-height:44px;display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:9px} +.assignment-result-icon{width:32px;height:32px} +.assignment-results>button.with-icon>span{min-width:0;overflow-wrap:anywhere} +.assignment-picker.visual-grid .assignment-results{display:grid;grid-template-columns:repeat(auto-fill,minmax(52px,1fr));grid-auto-rows:52px;gap:2px;max-height:none;padding:2px;overflow:visible;background:var(--shd-border-soft)} +.assignment-picker.visual-grid .assignment-results>button.visual-grid-option{position:relative;min-width:0;min-height:44px;display:grid;place-items:center;padding:5px;border:0;background:var(--shd-surface)} +.assignment-grid-icon{width:38px;height:38px} +.assignment-picker.visual-grid .assignment-results>button.visual-grid-option::after{content:attr(data-tooltip);position:absolute;left:50%;bottom:calc(100% + 6px);z-index:30;width:max-content;max-width:210px;padding:5px 7px;border:1px solid var(--shd-border-strong);background:var(--shd-bg);color:var(--shd-text);font-size:var(--shd-type-telemetry);font-weight:700;line-height:1.25;white-space:normal;opacity:0;pointer-events:none;transform:translateX(-50%)} +.assignment-picker.visual-grid .assignment-results>button.visual-grid-option:hover::after,.assignment-picker.visual-grid .assignment-results>button.visual-grid-option:focus-visible::after{opacity:1} +.assignment-picker.visual-grid .assignment-results>button.visual-grid-option:hover{background:var(--shd-surface-active);box-shadow:inset 0 0 0 1px var(--shd-border-strong)} +.assignment-picker.visual-grid .assignment-results>button.visual-grid-option:focus-visible{outline:2px solid var(--shd-focus-ring);outline-offset:-2px} +.assignment-picker.visual-grid .assignment-results>button.visual-grid-option.selected{background:var(--shd-surface-active);box-shadow:inset 0 0 0 2px var(--shd-orange)} +.assignment-picker.toggle-grid{border:0;background:transparent} +.assignment-picker.toggle-grid .assignment-results{display:grid;grid-template-columns:repeat(auto-fit,minmax(94px,1fr));gap:5px;max-height:none;overflow:visible} +.assignment-picker.toggle-grid .assignment-results>button.toggle-grid-option{min-width:0;min-height:44px;display:grid;grid-template-columns:minmax(0,1fr);padding:7px 9px;border:1px solid var(--shd-border);background:var(--shd-surface);color:var(--shd-text-muted)} +.assignment-picker.toggle-grid .assignment-results>button.toggle-grid-option>span{min-width:0;font-size:var(--shd-type-supporting);font-weight:700;overflow-wrap:anywhere} +.assignment-picker.toggle-grid .assignment-results>button.toggle-grid-option:hover{border-color:var(--shd-border-strong);background:var(--shd-surface-active);color:var(--shd-text)} +.assignment-picker.toggle-grid .assignment-results>button.toggle-grid-option:focus-visible{outline:2px solid var(--shd-focus-ring);outline-offset:2px} +.assignment-picker.toggle-grid .assignment-results>button.toggle-grid-option.selected{border-color:var(--shd-orange);background:var(--shd-surface-active);color:var(--shd-orange);box-shadow:inset 2px 0 var(--shd-orange)} +@media(max-width:560px){.assignment-picker.visual-grid .assignment-results{grid-template-columns:repeat(auto-fill,minmax(48px,1fr))}.assignment-picker.toggle-grid .assignment-results{grid-template-columns:repeat(2,minmax(0,1fr))}} + +/* Shared weapon-category bonus profiles. */ +.weapon-bonus-registry{margin-top:10px} +.weapon-bonus-profile-grid>button header strong{display:flex;align-items:center;gap:6px} +.weapon-bonus-editor{border-top-color:var(--shd-success)} +.weapon-bonus-fields{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:7px;margin-top:9px} +.weapon-mod-slot-note{display:flex;align-items:center;gap:10px;margin:8px 0;padding:8px 10px;border:1px solid #74552a;background:#18150d;color:#a89568;font-size:var(--shd-type-telemetry);line-height:1.45}.weapon-mod-slot-note b{flex:0 0 auto;color:var(--shd-orange);letter-spacing:.8px}.weapon-mod-slot-config{margin-top:8px;padding:9px;border:1px solid var(--shd-border);background:#0d1011}.weapon-mod-slot-config b.config-required,.state-dot.config-required{color:var(--shd-orange)}.weapon-t0-exclusion{margin-top:10px;padding-top:9px;border-top:1px solid var(--shd-border)}.weapon-t0-exclusion>.segmented-choice{margin-top:7px}.weapon-t0-exclusion>.segmented-choice>div{height:auto;min-height:44px}@media(max-width:700px){.weapon-mod-slot-note{align-items:flex-start;flex-direction:column;gap:4px}.weapon-t0-exclusion>.segmented-choice>div{grid-template-columns:repeat(2,minmax(0,1fr))!important}} +.weapon-bonus-fields .field{margin:0} +@media(max-width:700px){.weapon-bonus-fields{grid-template-columns:1fr}} diff --git a/src/lib/components/AccountControl.svelte b/src/lib/components/AccountControl.svelte new file mode 100644 index 0000000..797bfce --- /dev/null +++ b/src/lib/components/AccountControl.svelte @@ -0,0 +1,188 @@ + + +
+
ACCOUNT CONTROL
+ + + {#if accountPanel === 'identity'} + + {:else if accountPanel === 'access'} + + {:else} + + {/if} +
+ + diff --git a/src/lib/components/AgentIdentityLink.svelte b/src/lib/components/AgentIdentityLink.svelte index b58a504..c2e24a8 100644 --- a/src/lib/components/AgentIdentityLink.svelte +++ b/src/lib/components/AgentIdentityLink.svelte @@ -1,29 +1,42 @@ -{#if linked} +{#if linked && roles !== null} +
+ +
+ + {#if meta}{meta}{:else if showId}AGENT {agent.id.slice(0,8).toUpperCase()}{/if} +
+
+{:else if linked} {agent.displayName} {#if meta}{meta}{:else if showId}AGENT {agent.id.slice(0,8).toUpperCase()}{/if} - {:else}
@@ -33,14 +46,15 @@ {/if} diff --git a/src/lib/components/CommentComposer.svelte b/src/lib/components/CommentComposer.svelte new file mode 100644 index 0000000..4f3078a --- /dev/null +++ b/src/lib/components/CommentComposer.svelte @@ -0,0 +1,51 @@ + + +
+ + +
+ MARKDOWN SUPPORTED + {remaining.toLocaleString()} REMAINING +
+
+ + diff --git a/src/lib/components/ContentReport.svelte b/src/lib/components/ContentReport.svelte index 2d4bed4..4e1bf39 100644 --- a/src/lib/components/ContentReport.svelte +++ b/src/lib/components/ContentReport.svelte @@ -5,12 +5,16 @@ contentType, contentId, label = 'REPORT', - result + result, + plain = false, + menuItem = false }: { contentType: 'comment' | 'profile_bio' | 'guide' | 'avatar'; contentId: string; label?: string; result?: Record | null; + plain?: boolean; + menuItem?: boolean; } = $props(); const categories = [ @@ -27,37 +31,45 @@ String(result?.userReportContentId ?? '') === contentId && String(result?.userReportContentType ?? '') === contentType ); + let reportOpen = $state(false); + + function closeReport() { + reportOpen = false; + } -
- {label} -
- REQUEST OVERSIGHT REVIEW -

Send this content to Community Oversight. A report does not remove it automatically.

- - {#snippet children(pending)} - - - - - {#if matchesResult && result?.userReportError}

{String(result.userReportError)}

{/if} - {#if matchesResult && result?.userReportSaved}

Report sent to Community Oversight.

{/if} -
- -
- {/snippet} -
-
+
+ {label} + {#if reportOpen} +
+ REQUEST OVERSIGHT REVIEW +

Send this content to Community Oversight. A report does not remove it automatically.

+ + {#snippet children(pending)} + + + + + {#if matchesResult && result?.userReportError}

{String(result.userReportError)}

{/if} + {#if matchesResult && result?.userReportSaved}

Report sent to Community Oversight.

{/if} +
+ + +
+ {/snippet} +
+
+ {/if}
diff --git a/src/lib/components/InfoPopover.svelte b/src/lib/components/InfoPopover.svelte new file mode 100644 index 0000000..5924db8 --- /dev/null +++ b/src/lib/components/InfoPopover.svelte @@ -0,0 +1,48 @@ + + +
+ i +
+ {title} +

{text}

+
+
+ + diff --git a/src/lib/components/MarkdownContent.svelte b/src/lib/components/MarkdownContent.svelte index 93d5ed1..d683c07 100644 --- a/src/lib/components/MarkdownContent.svelte +++ b/src/lib/components/MarkdownContent.svelte @@ -27,6 +27,8 @@ .markdown-content :global(li+li){margin-top:.3em} .markdown-content :global(a){color:var(--shd-orange);font-family:var(--shd-font-sans);font-weight:700} .markdown-content :global(a:hover),.markdown-content :global(a:focus-visible){color:var(--shd-orange-hover)} + .markdown-content :global(strong){font-weight:700} + .markdown-content :global(em){font-style:italic} .markdown-content :global(blockquote){padding:.2em 0 .2em 1em;border-left:2px solid var(--shd-orange);color:var(--shd-text-muted)} .markdown-content :global(code){padding:2px 4px;border:1px solid var(--shd-border);background:#0d0f10;color:var(--shd-text);font-family:var(--shd-font-mono);font-size:.9em} .markdown-content :global(:first-child){margin-top:0} diff --git a/src/lib/components/ModerationDisclosure.svelte b/src/lib/components/ModerationDisclosure.svelte index 085c309..2bbd7b2 100644 --- a/src/lib/components/ModerationDisclosure.svelte +++ b/src/lib/components/ModerationDisclosure.svelte @@ -1,4 +1,8 @@ -