diff --git a/AGENTS.md b/AGENTS.md index 69ad6ae..2070468 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -62,6 +62,10 @@ All ISAC data-management editors must follow the same successful-save behavior: 4. Server actions must return the add/edit mode and a unique result token; client-side reactive handling must consume each token only once to avoid repeated state updates. +5. When a successful add keeps an icon-enabled editor open, increment the + shared upload reset token so the native file input, generated preview, + filename, drag/paste state, validation message, and removal state are fresh + for the next record. Apply this convention whenever an ISAC editor is added or materially changed. diff --git a/docs/DESIGN_SYSTEM.md b/docs/DESIGN_SYSTEM.md index 593226b..21509c9 100644 --- a/docs/DESIGN_SYSTEM.md +++ b/docs/DESIGN_SYSTEM.md @@ -772,6 +772,12 @@ fixed column to `IconUploadField`, give the record name the flexible dominant column, and stack both controls at mobile widths. Do not enlarge an upload drop-zone to fill spare identity-row space. +Icon-enabled consecutive-entry editors pass a successful-add result token into +the shared upload control. Advancing that token clears the native file input, +temporary preview URL, filename, drag/paste feedback, and validation state. +This reset occurs only after a validated add succeeds; failed saves preserve +the selected image, and successful edits close the editor normally. + The primary `name` field in every ISAC data editor is a header-level input: 46 pixels tall with larger, heavier text. This creates a consistent first reading point for long registries while keeping secondary metadata compact. diff --git a/src/lib/components/IconUploadField.svelte b/src/lib/components/IconUploadField.svelte index 99280ba..ef2c1a3 100644 --- a/src/lib/components/IconUploadField.svelte +++ b/src/lib/components/IconUploadField.svelte @@ -2,13 +2,14 @@ import ImageUploadInput from '$lib/components/ImageUploadInput.svelte'; let { currentUrl = '', - label = 'ICON' - }: { currentUrl?: string | null; label?: string } = $props(); + label = 'ICON', + resetToken = 0 + }: { currentUrl?: string | null; label?: string; resetToken?: string | number } = $props();
{label} - + {#if currentUrl}{/if} TRANSPARENT SVG, PNG, WEBP, OR AVIF · 2 MiB MAX
diff --git a/src/lib/components/ImageUploadInput.svelte b/src/lib/components/ImageUploadInput.svelte index 6b2e41c..6fb10a9 100644 --- a/src/lib/components/ImageUploadInput.svelte +++ b/src/lib/components/ImageUploadInput.svelte @@ -7,7 +7,8 @@ required = false, currentUrl = '', emptyLabel = 'NO IMAGE', - browseLabel = 'CHOOSE FILE' + browseLabel = 'CHOOSE FILE', + resetToken = 0 }: { name: string; accept: string; @@ -15,16 +16,38 @@ currentUrl?: string | null; emptyLabel?: string; browseLabel?: string; + resetToken?: string | number; } = $props(); - let input: HTMLInputElement; + let input: HTMLInputElement | undefined; let previewUrl = $state(''); let fileName = $state(''); let dragging = $state(false); let error = $state(''); + let handledResetToken: string | number | null = null; + + function resetUpload() { + if (previewUrl) URL.revokeObjectURL(previewUrl); + previewUrl = ''; + fileName = ''; + dragging = false; + error = ''; + if (input) input.value = ''; + } + + $effect(() => { + if (handledResetToken === null) { + handledResetToken = resetToken; + return; + } + if (resetToken !== handledResetToken) { + handledResetToken = resetToken; + resetUpload(); + } + }); function setFile(file: File | undefined) { - if (!file) return; + if (!file || !input) return; if (!file.type.startsWith('image/')) { error = 'Clipboard or dropped content must be an image.'; return; @@ -108,7 +131,7 @@ {fileName || 'OR USE AN UPLOAD METHOD'}
- +
diff --git a/src/routes/admin/brands/+page.svelte b/src/routes/admin/brands/+page.svelte index cea769b..71dc587 100644 --- a/src/routes/admin/brands/+page.svelte +++ b/src/routes/admin/brands/+page.svelte @@ -10,6 +10,7 @@ const blank = (): Editor => ({ id: '', name: '', iconUrl: '', active: 1, effects: [2, 3, 4].map((pieces) => ({ pieces_required: pieces, stat_attribute_id: '', value: 0 })) }); let editing = $state(null); let handledResultToken = $state(''); + let iconResetToken = $state(0); const effectFor = (brand: Editor, pieces: number) => brand.effects.find((effect) => Number(effect.pieces_required) === pieces) ?? { pieces_required: pieces, stat_attribute_id: '', value: 0 }; const statFor = (id: string) => data.stats.find((stat) => stat.id === id); const formatEffect = (effect: Effect) => { const stat = statFor(effect.stat_attribute_id); return `${effect.value}${stat?.unit ?? ''} ${stat?.name ?? 'Unknown attribute'}`; }; @@ -22,6 +23,7 @@ } if (form?.brandSaved && form.resultToken && form.resultToken !== handledResultToken) { handledResultToken = form.resultToken; + if (form.brandMode === 'add') iconResetToken += 1; editing = form.brandMode === 'add' ? blank() : null; } }); @@ -36,7 +38,7 @@
- +
diff --git a/src/routes/admin/protocols/+page.svelte b/src/routes/admin/protocols/+page.svelte index 41a78ec..3e13b8b 100644 --- a/src/routes/admin/protocols/+page.svelte +++ b/src/routes/admin/protocols/+page.svelte @@ -25,6 +25,7 @@ let talentUpgrade = $state(0); let protocolLevel = $state(1); let handledResultToken = $state(''); + let iconResetToken = $state(0); const qualityClass = (quality: string) => `quality-${quality.toLowerCase().replace('-', '')}`; const iconName = (type: string): GameIconName => type.toLowerCase() as GameIconName; const qualityOptions = qualities.map((quality) => ({ value: quality, label: quality, className: qualityClass(quality) })); @@ -80,6 +81,7 @@ } if (form?.protocolSaved && form.resultToken && form.resultToken !== handledResultToken) { handledResultToken = form.resultToken; + if (form.protocolMode === 'add') iconResetToken += 1; editing = form.protocolMode === 'add' ? blank() : null; talentUpgrade = 0; protocolLevel = 1; @@ -132,7 +134,7 @@ ({ statId: effect.stat_attribute_id, baseValue: effect.base_value })))} />
-
+
diff --git a/src/routes/admin/skillchips/+page.svelte b/src/routes/admin/skillchips/+page.svelte index 23c07e9..5e7a35f 100644 --- a/src/routes/admin/skillchips/+page.svelte +++ b/src/routes/admin/skillchips/+page.svelte @@ -13,6 +13,7 @@ let editing = $state(null); let highEndCount = $state('0'); let handledResultToken = $state(''); + let iconResetToken = $state(0); const qualitySteps = [ { value: '0', label: '0 HIGH-END' }, { value: '1', label: '1 HIGH-END' }, @@ -51,6 +52,7 @@ $effect(() => { if (form?.skillChipSaved && form.resultToken && form.resultToken !== handledResultToken) { handledResultToken = form.resultToken; + if (form.skillChipMode === 'add') iconResetToken += 1; editing = form.skillChipMode === 'add' ? blank() : null; highEndCount = '0'; } @@ -73,7 +75,7 @@ ({ statId: effect.stat_attribute_id, minValue: effect.min_value, maxValue: effect.max_value })))}/> -
SUPPORTED QUALITY
SUPERIORHIGH-END
Only these qualities participate in chip scaling.
+
SUPPORTED QUALITY
SUPERIORHIGH-END
Only these qualities participate in chip scaling.
2-PIECE BONUSMINIMUM AT ZERO HIGH-END · LINEAR TO MAXIMUM AT THREE HIGH-END
{format(scaled(editing.twoPiece.min_value, editing.twoPiece.max_value), editing.twoPiece.stat_attribute_id)}
diff --git a/src/routes/admin/talents/+page.svelte b/src/routes/admin/talents/+page.svelte index fe32c59..86b0f90 100644 --- a/src/routes/admin/talents/+page.svelte +++ b/src/routes/admin/talents/+page.svelte @@ -8,6 +8,7 @@ type Effect = { statId: string; valueI: string; valueII: string }; let talent = $state(null), trigger = $state(null), uptime = $state(false); let handledResultToken = $state(''); + let iconResetToken = $state(0); const blankTalent = () => ({ id: '', name: '', iconUrl: '', category: 'Weapon Talent 1', descriptionTemplate: '', tierIDescription: '', tierIIDescription: '', triggerIds: [], effects: [{ statId: '', valueI: '', valueII: '' }] }); const editTalent = (row: any) => { const tier = (name: string) => row.tiers.find((item: any) => item.tier === name) ?? { description: '', triggerIds: [], effects: [] }; @@ -49,7 +50,10 @@ $effect(() => { if (!form?.resultToken || form.resultToken === handledResultToken) return; handledResultToken = form.resultToken; - if (form.talentSaved) talent = form.talentMode === 'add' ? blankTalent() : null; + if (form.talentSaved) { + if (form.talentMode === 'add') iconResetToken += 1; + talent = form.talentMode === 'add' ? blankTalent() : null; + } if (form.triggerSaved) { if (form.triggerMode === 'add') blankTrigger(); else trigger = null; @@ -70,7 +74,7 @@

{talent.id ? 'MODIFY TALENT' : 'NEW TALENT'}

{talent.id ? talent.name : 'TALENT DEFINITION'}

-
+
TIER I PREVIEW

{preview('I') || 'Description preview'}

TIER II PREVIEW

{preview('II') || 'Description preview'}

SHARED ACTIVATION TRIGGERS

diff --git a/src/routes/admin/weapons/+page.svelte b/src/routes/admin/weapons/+page.svelte index 3fe0119..5bbace8 100644 --- a/src/routes/admin/weapons/+page.svelte +++ b/src/routes/admin/weapons/+page.svelte @@ -16,6 +16,7 @@ const blank = () => ({ id: '', name: '', category: 'AR', is_exotic: 0, native_tier: 0, damage_type: 'Pierce', rpm: 0, magazine: 0, reload_speed: 0, accuracy: 0, stability: 0, exotic_talent_id: '', exoticTalent: blankExoticTalent(), active: 1, tiers: [blankTier(0), blankTier(1), blankTier(2)], talent1Ids: [], talent2Ids: [], augmentEffects: [{ stat_attribute_id: '', value: 0, sort_order: 1 }], baseBonuses: [{ stat_attribute_id: '', value: 0, sort_order: 1 }] }); let editing = $state(null); let handledResultToken = $state(''); + let iconResetToken = $state(0); let openedRequestedWeapon = $state(false); const edit = (weapon: any) => { const tiers = [0, 1, 2].map((tier) => ({ ...blankTier(tier), ...(weapon.tiers.find((item: any) => Number(item.tier) === tier) ?? {}) })); @@ -67,6 +68,7 @@ } if (form?.weaponSaved && form.resultToken && form.resultToken !== handledResultToken) { handledResultToken = form.resultToken; + if (form.weaponMode === 'add') iconResetToken += 1; editing = form.weaponMode === 'add' ? blank() : null; } }); @@ -93,7 +95,7 @@
DEDICATED EXOTIC TALENTOWNED EXCLUSIVELY BY THIS WEAPON · TIER II VALUES REQUIRED
{editing.exoticTalent.id ? 'LINKED RECORD' : 'NEW RECORD'}
-
+
TIER I PREVIEW

{exoticPreview('I') || 'Description preview'}

TIER II PREVIEW

{exoticPreview('II') || 'Description preview'}

ACTIVATION TRIGGERSSHARED BETWEEN BOTH TIERS
{editing.exoticTalent.triggerIds.length}