@@ -110,7 +110,7 @@
{#if job.masterLevel || job.ultimateMastery}
{#if job.masterLevel}
-
ML{job.masterLevel}
+
ML
{/if}
{#if job.ultimateMastery}
UM
diff --git a/src/lib/components/job/JobSkillItem.svelte b/src/lib/components/job/JobSkillItem.svelte
index cc635499..19171f66 100644
--- a/src/lib/components/job/JobSkillItem.svelte
+++ b/src/lib/components/job/JobSkillItem.svelte
@@ -23,9 +23,8 @@
function getSkillColorClass(skill: JobSkill): string {
if (skill.main) return 'skill-main'
if (skill.sub) return 'skill-sub'
- // Use category for additional classification
- if (skill.category === 2) return 'skill-emp'
- if (skill.category === 1) return 'skill-base'
+ if (skill.emp) return 'skill-emp'
+ if (skill.base) return 'skill-base'
return ''
}
diff --git a/src/lib/components/job/JobSkillSlot.svelte b/src/lib/components/job/JobSkillSlot.svelte
index 342fa33a..d2576995 100644
--- a/src/lib/components/job/JobSkillSlot.svelte
+++ b/src/lib/components/job/JobSkillSlot.svelte
@@ -112,8 +112,10 @@
{#snippet EmptyState({ slot })}
-
-
Slot {slot + 1}
+
+
+
+
Select a skill
{/snippet}
@@ -154,12 +156,19 @@
}
&.empty {
- border-style: dashed;
- background: var(--placeholder-bg);
+ background: transparent;
&.editable:hover {
- background: var(--button-contained-bg-hover);
- border-style: solid;
+ background: var(--button-bg-hover);
+
+ .placeholder-icon {
+ border-color: var(--border-medium);
+ box-shadow: var(--hover-shadow);
+ }
+
+ .placeholder-text {
+ color: var(--text-tertiary-hover);
+ }
}
}
@@ -222,16 +231,30 @@
.empty-content {
display: flex;
- flex-direction: column;
+ flex-direction: row;
align-items: center;
- justify-content: center;
- gap: 4px;
+ gap: $unit;
padding: $unit;
- color: var(--text-tertiary);
- height: 60px;
+ height: 100%;
- span {
- font-size: 12px;
+ .placeholder-icon {
+ width: 32px;
+ height: 32px;
+ background: var(--card-bg);
+ border: 1px solid transparent;
+ border-radius: $unit-half;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ color: var(--icon-secondary);
+ transition: all 0.15s ease;
+ }
+
+ .placeholder-text {
+ font-size: 14px;
+ color: var(--text-tertiary);
+ transition: color 0.15s ease;
}
}
diff --git a/src/lib/components/sidebar/JobSkillSelectionSidebar.svelte b/src/lib/components/sidebar/JobSkillSelectionSidebar.svelte
index 43a33a57..6394f3e6 100644
--- a/src/lib/components/sidebar/JobSkillSelectionSidebar.svelte
+++ b/src/lib/components/sidebar/JobSkillSelectionSidebar.svelte
@@ -8,6 +8,7 @@
import JobSkillItem from '../job/JobSkillItem.svelte'
import Button from '../ui/Button.svelte'
import Input from '../ui/Input.svelte'
+ import Select from '../ui/Select.svelte'
import Icon from '../Icon.svelte'
import * as m from '$lib/paraglide/messages'
@@ -27,13 +28,28 @@
onRemoveSkill
}: Props = $props()
+ // Skill category filter options
+ // Values match Rails search_controller expectations:
+ // -1: All, 0-3: Color categories, 4: EMP, 5: Base
+ const skillCategoryOptions = [
+ { value: -1, label: 'All Skills' },
+ { value: 2, label: 'Damaging' },
+ { value: 0, label: 'Buffing' },
+ { value: 1, label: 'Debuffing' },
+ { value: 3, label: 'Healing' },
+ { value: 4, label: 'EMP' },
+ { value: 5, label: 'Base' }
+ ]
+
// State
let searchQuery = $state('')
+ let skillCategory = $state(-1) // -1 = All
let error = $state
()
let sentinelEl = $state()
let skillsResource = $state> | null>(null)
let lastSearchQuery = ''
let lastJobId: string | undefined
+ let lastCategory = -1
// Check if slot is locked
const slotLocked = $derived(targetSlot === 0)
@@ -44,9 +60,10 @@
// Manage resource creation and search updates
let debounceTimer: ReturnType | undefined
- function updateSearch() {
+ function updateSearch(forceRebuild = false) {
const jobId = job?.id
const locked = slotLocked
+ const category = skillCategory
// Clean up if no job or locked
if (!jobId || locked) {
@@ -56,21 +73,29 @@
}
lastJobId = undefined
lastSearchQuery = ''
+ lastCategory = -1
return
}
- // Create new resource if job changed
- if (jobId !== lastJobId) {
+ // Rebuild resource if job or category changed
+ const needsRebuild = forceRebuild || jobId !== lastJobId || category !== lastCategory
+
+ if (needsRebuild) {
if (skillsResource) {
skillsResource.destroy()
}
+ // Capture current values for the closure
+ const currentQuery = searchQuery
+ const currentCategory = category
+
const resource = createInfiniteScrollResource({
fetcher: async (page) => {
const response = await jobAdapter.searchSkills({
- query: lastSearchQuery,
+ query: currentQuery,
jobId,
- page
+ page,
+ ...(currentCategory >= 0 ? { filters: { group: currentCategory } } : {})
})
return response
},
@@ -81,19 +106,21 @@
skillsResource = resource
lastJobId = jobId
lastSearchQuery = searchQuery
+ lastCategory = category
resource.load()
}
- // Reload if search query changed
+ // Reload if only search query changed
else if (searchQuery !== lastSearchQuery && skillsResource) {
lastSearchQuery = searchQuery
- skillsResource.reset()
- skillsResource.load()
+ // Need to rebuild to capture new query in closure
+ updateSearch(true)
}
}
- // Watch for job changes
+ // Watch for job and category changes
$effect(() => {
job // Track job
+ skillCategory // Track category
updateSearch()
})
@@ -198,6 +225,16 @@
disabled={!canSearch}
fullWidth={true}
/>
+
+
+
@@ -235,10 +272,19 @@
No skills found
- {#if searchQuery}
-
+ {#if searchQuery || skillCategory >= 0}
+
+ {#if searchQuery}
+
+ {/if}
+ {#if skillCategory >= 0}
+
+ {/if}
+
{/if}
{/if}
@@ -341,11 +387,19 @@
}
.search-section {
+ display: flex;
+ flex-direction: column;
+ gap: $unit;
padding: $unit-2x 0;
border-bottom: 1px solid var(--border-subtle);
flex-shrink: 0;
}
+ .filter-row {
+ display: flex;
+ gap: $unit;
+ }
+
.skills-container {
flex: 1;
overflow-y: auto;
@@ -372,6 +426,12 @@
margin: 0;
font-size: 14px;
}
+
+ .clear-filters {
+ display: flex;
+ gap: $unit;
+ margin-top: $unit;
+ }
}
.loading-state :global(svg) {
diff --git a/src/lib/types/api/entities.ts b/src/lib/types/api/entities.ts
index f0ac6ded..a0854289 100644
--- a/src/lib/types/api/entities.ts
+++ b/src/lib/types/api/entities.ts
@@ -91,20 +91,24 @@ export interface Job {
row: number
order: number
proficiency: [number, number]
- masterLevel?: number
- ultimateMastery?: number
+ masterLevel?: boolean // Whether this job supports master level
+ ultimateMastery?: boolean // Whether this job supports ultimate mastery
accessory?: boolean
accessoryType?: number
}
-// JobSkill entity
+// JobSkill entity from JobSkillBlueprint
export interface JobSkill {
id: string
name: LocalizedName
slug: string
- category: number
- main: boolean
- sub: boolean
+ color: number // Skill category (0-3 for colors, relates to skill type)
+ main: boolean // Primary job skill
+ sub: boolean // Sub-skill (transferable)
+ emp: boolean // EMP skill
+ base: boolean // Base skill (for advanced jobs)
+ order: number // Display order
+ job: Job // Associated job
}
// JobAccessory entity
diff --git a/src/lib/utils/jobUtils.ts b/src/lib/utils/jobUtils.ts
index feb1e9d2..ac9e6e68 100644
--- a/src/lib/utils/jobUtils.ts
+++ b/src/lib/utils/jobUtils.ts
@@ -154,9 +154,8 @@ export function isSkillSlotLocked(
export function getSkillCategoryName(skill: JobSkill): string {
if (skill.main) return 'Main'
if (skill.sub) return 'Subskill'
- // Use category field for additional classification
- if (skill.category === 2) return 'EMP'
- if (skill.category === 1) return 'Base'
+ if (skill.emp) return 'EMP'
+ if (skill.base) return 'Base'
return 'Unknown'
}
@@ -167,9 +166,8 @@ export function getSkillCategoryName(skill: JobSkill): string {
export function getSkillCategoryColor(skill: JobSkill): string {
if (skill.main) return 'var(--skill-main)'
if (skill.sub) return 'var(--skill-sub)'
- // Use category field for additional classification
- if (skill.category === 2) return 'var(--skill-emp)'
- if (skill.category === 1) return 'var(--skill-base)'
+ if (skill.emp) return 'var(--skill-emp)'
+ if (skill.base) return 'var(--skill-base)'
return 'var(--skill-default)'
}
@@ -226,9 +224,8 @@ export function countSkillsByType(skills: JobSkillList): {
if (skill) {
if (skill.main) counts.main++
else if (skill.sub) counts.sub++
- // Use category field for additional classification
- else if (skill.category === 2) counts.emp++
- else if (skill.category === 1) counts.base++
+ else if (skill.emp) counts.emp++
+ else if (skill.base) counts.base++
}
}