add filtering to magic item market
This commit is contained in:
parent
49754b1394
commit
4e37f2b0ee
95
app.js
95
app.js
@ -319,6 +319,8 @@ function getStockTransitions(rarity) {
|
||||
// Global market data and sort state
|
||||
let marketData = null;
|
||||
let currentSort = { column: 'name', direction: 'asc' };
|
||||
|
||||
|
||||
|
||||
// Calculate price multiplier based on reputation and rarity
|
||||
function getPriceMultiplier(reputation, rarity) {
|
||||
@ -435,6 +437,7 @@ document.addEventListener('click', (event) => {
|
||||
function renderMarket(marketData, reputationData) {
|
||||
const marketContainer = document.getElementById('market-display');
|
||||
if (!marketContainer || !marketData || !marketData.items) return;
|
||||
captureMarketTableScroll();
|
||||
|
||||
// Filter to only available items (stock > unavailable AND reputation high enough)
|
||||
const availableItems = [];
|
||||
@ -478,46 +481,45 @@ function renderMarket(marketData, reputationData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filterValues = getMarketFilterValues(availableItems);
|
||||
const filteredItems = applyMarketFilters(availableItems);
|
||||
|
||||
if (filteredItems.length === 0) {
|
||||
marketContainer.innerHTML = `
|
||||
<div class="market-table-wrapper">
|
||||
<table class="market-table">
|
||||
<thead>
|
||||
${renderMarketHeader()}
|
||||
${renderMarketFilters(filterValues)}
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="no-items" colspan="6">No items match the current filters.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
restoreMarketFilterFocus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort items
|
||||
sortItems(availableItems, currentSort.column, currentSort.direction);
|
||||
sortItems(filteredItems, currentSort.column, currentSort.direction);
|
||||
|
||||
// Build table HTML with scrollable wrapper
|
||||
let html = `
|
||||
<div class="market-table-wrapper">
|
||||
<table class="market-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="sortable ${currentSort.column === 'name' ? 'sorted-' + currentSort.direction : ''}"
|
||||
onclick="sortMarket('name')">
|
||||
Name ${getSortIndicator('name')}
|
||||
</th>
|
||||
<th class="sortable ${currentSort.column === 'faction' ? 'sorted-' + currentSort.direction : ''}"
|
||||
onclick="sortMarket('faction')">
|
||||
Faction ${getSortIndicator('faction')}
|
||||
</th>
|
||||
<th class="sortable ${currentSort.column === 'rarity' ? 'sorted-' + currentSort.direction : ''}"
|
||||
onclick="sortMarket('rarity')">
|
||||
Rarity ${getSortIndicator('rarity')}
|
||||
</th>
|
||||
<th class="sortable ${currentSort.column === 'stock' ? 'sorted-' + currentSort.direction : ''}"
|
||||
onclick="sortMarket('stock')">
|
||||
Stock ${getSortIndicator('stock')}
|
||||
</th>
|
||||
<th class="sortable ${currentSort.column === 'cost' ? 'sorted-' + currentSort.direction : ''}"
|
||||
onclick="sortMarket('cost')">
|
||||
Cost ${getSortIndicator('cost')}
|
||||
</th>
|
||||
<th class="sortable ${currentSort.column === 'attunement' ? 'sorted-' + currentSort.direction : ''}"
|
||||
onclick="sortMarket('attunement')">
|
||||
Attunement ${getSortIndicator('attunement')}
|
||||
</th>
|
||||
</tr>
|
||||
${renderMarketHeader()}
|
||||
${renderMarketFilters(filterValues)}
|
||||
</thead>
|
||||
<tbody>
|
||||
`;
|
||||
|
||||
// Render each item as table row
|
||||
availableItems.forEach(item => {
|
||||
filteredItems.forEach(item => {
|
||||
const stockClass = item.stock.replace(' ', '-');
|
||||
const factionClass = item.faction.toLowerCase();
|
||||
const rarityClass = item.rarity.toLowerCase().replace(' ', '-');
|
||||
@ -570,9 +572,41 @@ function renderMarket(marketData, reputationData) {
|
||||
`;
|
||||
|
||||
marketContainer.innerHTML = html;
|
||||
restoreMarketFilterFocus();
|
||||
restoreMarketTableScroll();
|
||||
}
|
||||
|
||||
function renderMarketHeader() {
|
||||
return `
|
||||
<tr>
|
||||
<th class="sortable ${currentSort.column === 'name' ? 'sorted-' + currentSort.direction : ''}"
|
||||
onclick="sortMarket('name')">
|
||||
Name ${getSortIndicator('name')}
|
||||
</th>
|
||||
<th class="sortable ${currentSort.column === 'faction' ? 'sorted-' + currentSort.direction : ''}"
|
||||
onclick="sortMarket('faction')">
|
||||
Faction ${getSortIndicator('faction')}
|
||||
</th>
|
||||
<th class="sortable ${currentSort.column === 'rarity' ? 'sorted-' + currentSort.direction : ''}"
|
||||
onclick="sortMarket('rarity')">
|
||||
Rarity ${getSortIndicator('rarity')}
|
||||
</th>
|
||||
<th class="sortable ${currentSort.column === 'stock' ? 'sorted-' + currentSort.direction : ''}"
|
||||
onclick="sortMarket('stock')">
|
||||
Stock ${getSortIndicator('stock')}
|
||||
</th>
|
||||
<th class="sortable cost-col ${currentSort.column === 'cost' ? 'sorted-' + currentSort.direction : ''}"
|
||||
onclick="sortMarket('cost')">
|
||||
Cost ${getSortIndicator('cost')}
|
||||
</th>
|
||||
<th class="sortable ${currentSort.column === 'attunement' ? 'sorted-' + currentSort.direction : ''}"
|
||||
onclick="sortMarket('attunement')">
|
||||
Attunement ${getSortIndicator('attunement')}
|
||||
</th>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
// Sort items by column
|
||||
function sortItems(items, column, direction) {
|
||||
const multiplier = direction === 'asc' ? 1 : -1;
|
||||
|
||||
@ -841,6 +875,9 @@ document.addEventListener('click', (event) => {
|
||||
tooltip.classList.remove('show');
|
||||
});
|
||||
}
|
||||
if (!event.target.closest('.market-filter-dropdown')) {
|
||||
closeMarketFilterDropdown();
|
||||
}
|
||||
});
|
||||
|
||||
function renderQuestBoard(quests, currentTime) {
|
||||
|
||||
393
filtering.js
Normal file
393
filtering.js
Normal file
@ -0,0 +1,393 @@
|
||||
// Magic Item Market filtering state and helpers
|
||||
let marketFilters = {
|
||||
name: '',
|
||||
faction: [],
|
||||
rarity: [],
|
||||
stock: [],
|
||||
attunement: [],
|
||||
costMin: '',
|
||||
costMax: ''
|
||||
};
|
||||
let marketFilterFocus = null;
|
||||
let marketFilterOpenKey = null;
|
||||
let marketFilterSearch = {
|
||||
faction: '',
|
||||
rarity: '',
|
||||
stock: '',
|
||||
attunement: ''
|
||||
};
|
||||
let marketTableScrollLeft = null;
|
||||
let marketFilterAllValues = {
|
||||
faction: [],
|
||||
rarity: [],
|
||||
stock: [],
|
||||
attunement: []
|
||||
};
|
||||
let marketFiltersInitialized = false;
|
||||
|
||||
function renderMarketFilters(values) {
|
||||
// Build the filter row markup for the market table.
|
||||
const { factions, rarities, stocks, attunements } = values;
|
||||
return `
|
||||
<tr class="market-filter-row">
|
||||
<th>
|
||||
<div class="market-filter-search">
|
||||
<input class="market-filter-input market-filter-search-input" type="text" placeholder="Search"
|
||||
value="${escapeAttribute(marketFilters.name)}"
|
||||
data-filter-key="name"
|
||||
oninput="updateMarketFilter('name', this.value)" />
|
||||
<button class="market-filter-clear${marketFilters.name ? ' active' : ''}"
|
||||
type="button"
|
||||
aria-label="Clear name filter"
|
||||
onclick="clearMarketFilter('name')">×</button>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
${renderFilterDropdown('faction', 'Faction', factions, marketFilters.faction)}
|
||||
</th>
|
||||
<th>
|
||||
${renderFilterDropdown('rarity', 'Rarity', rarities, marketFilters.rarity)}
|
||||
</th>
|
||||
<th>
|
||||
${renderFilterDropdown('stock', 'Stock', stocks, marketFilters.stock, toTitleCase)}
|
||||
</th>
|
||||
<th class="cost-col">
|
||||
<div class="market-filter-range">
|
||||
<div class="market-filter-search">
|
||||
<input class="market-filter-input cost-filter" type="text" inputmode="numeric" pattern="[0-9]*" placeholder="Min"
|
||||
value="${escapeAttribute(marketFilters.costMin)}"
|
||||
data-filter-key="costMin"
|
||||
oninput="updateMarketFilter('costMin', this.value)" />
|
||||
<button class="market-filter-clear${marketFilters.costMin ? ' active' : ''}"
|
||||
type="button"
|
||||
aria-label="Clear minimum cost"
|
||||
onclick="clearMarketFilter('costMin')">×</button>
|
||||
</div>
|
||||
<div class="market-filter-search">
|
||||
<input class="market-filter-input cost-filter" type="text" inputmode="numeric" pattern="[0-9]*" placeholder="Max"
|
||||
value="${escapeAttribute(marketFilters.costMax)}"
|
||||
data-filter-key="costMax"
|
||||
oninput="updateMarketFilter('costMax', this.value)" />
|
||||
<button class="market-filter-clear${marketFilters.costMax ? ' active' : ''}"
|
||||
type="button"
|
||||
aria-label="Clear maximum cost"
|
||||
onclick="clearMarketFilter('costMax')">×</button>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
${renderFilterDropdown('attunement', 'Attunement', attunements, marketFilters.attunement)}
|
||||
</th>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
function getMarketFilterValues(items) {
|
||||
// Derive filter options once per render, seeding defaults on first load.
|
||||
const factions = getUniqueValues(items, item => item.faction);
|
||||
const rarities = getRarityValues();
|
||||
const stocks = getUniqueValues(items, item => item.stock, stockSort);
|
||||
const attunements = getUniqueValues(items, item => item.attunementRequired);
|
||||
marketFilterAllValues = {
|
||||
faction: factions,
|
||||
rarity: rarities,
|
||||
stock: stocks,
|
||||
attunement: attunements
|
||||
};
|
||||
if (!marketFiltersInitialized) {
|
||||
marketFilters.faction = [...factions];
|
||||
marketFilters.rarity = [...rarities];
|
||||
marketFilters.stock = [...stocks];
|
||||
marketFilters.attunement = [...attunements];
|
||||
marketFiltersInitialized = true;
|
||||
}
|
||||
return { factions, rarities, stocks, attunements };
|
||||
}
|
||||
|
||||
function getUniqueValues(items, getter, sorter) {
|
||||
// Collect unique values and apply optional custom sort.
|
||||
const values = new Set();
|
||||
items.forEach(item => {
|
||||
const value = getter(item);
|
||||
if (value) values.add(value);
|
||||
});
|
||||
const list = Array.from(values);
|
||||
if (sorter) {
|
||||
return list.sort(sorter);
|
||||
}
|
||||
return list.sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
function renderFilterDropdown(key, label, values, selected, labelFormatter) {
|
||||
// Compact filter UI with search and multi-select checkboxes.
|
||||
const formatted = labelFormatter || (value => value);
|
||||
const search = marketFilterSearch[key] || '';
|
||||
const filteredValues = values.filter(value =>
|
||||
value.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
// "All" is shown only when every value is selected; "NONE" means zero selected.
|
||||
const isAllSelected = selected.length === values.length && values.length > 0;
|
||||
const isNoneSelected = selected.length === 0;
|
||||
const summary = isAllSelected
|
||||
? 'All'
|
||||
: isNoneSelected
|
||||
? 'NONE'
|
||||
: selected.length === 1
|
||||
? formatted(selected[0])
|
||||
: `${selected.length} selected`;
|
||||
const options = filteredValues.map(value => {
|
||||
const isChecked = selected.includes(value) ? ' checked' : '';
|
||||
const display = escapeAttribute(formatted(value));
|
||||
return `
|
||||
<label class="market-filter-option">
|
||||
<input type="checkbox" value="${escapeAttribute(value)}"${isChecked}
|
||||
onchange="toggleMarketFilterValue('${key}', this.value, this.checked)" />
|
||||
<span>${display}</span>
|
||||
</label>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
const openClass = marketFilterOpenKey === key ? ' show' : '';
|
||||
return `
|
||||
<div class="market-filter-dropdown" data-filter-key="${key}">
|
||||
<button type="button" class="market-filter-trigger${isNoneSelected ? ' is-empty' : ''}" onclick="toggleMarketFilterDropdown('${key}')">
|
||||
${label}: <span class="market-filter-summary">${escapeAttribute(summary)}</span>
|
||||
</button>
|
||||
<div class="market-filter-popover${openClass}" onclick="event.stopPropagation()">
|
||||
<div class="market-filter-popover-header">
|
||||
<input class="market-filter-input market-filter-popover-search" type="text" placeholder="Search"
|
||||
value="${escapeAttribute(search)}"
|
||||
data-filter-search-key="${key}"
|
||||
oninput="updateMarketFilterSearch('${key}', this.value)" />
|
||||
<div class="market-filter-popover-actions">
|
||||
<button type="button" onclick="selectAllMarketFilter('${key}')">All</button>
|
||||
<button type="button" onclick="clearMarketFilter('${key}')">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="market-filter-options">
|
||||
${options || '<div class="market-filter-empty">No matches</div>'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function toTitleCase(value) {
|
||||
// Title-case display helper for stock labels.
|
||||
return value.replace(/\b\w/g, (char) => char.toUpperCase());
|
||||
}
|
||||
|
||||
function stockSort(a, b) {
|
||||
// Enforce stock order from most scarce to most available.
|
||||
const order = ["Very Limited", "Limited", "Modest", "Abundant", "Very Abundant"];
|
||||
return order.indexOf(toTitleCase(a)) - order.indexOf(toTitleCase(b));
|
||||
}
|
||||
|
||||
function raritySort(a, b) {
|
||||
// Enforce rarity order from rarest to most common.
|
||||
const order = ["Unique", "Artifact", "Legendary", "Very Rare", "Rare", "Uncommon", "Common"];
|
||||
const indexA = order.indexOf(a);
|
||||
const indexB = order.indexOf(b);
|
||||
if (indexA === -1 && indexB === -1) return a.localeCompare(b);
|
||||
if (indexA === -1) return 1;
|
||||
if (indexB === -1) return -1;
|
||||
return indexA - indexB;
|
||||
}
|
||||
|
||||
function getRarityValues() {
|
||||
// Use full dataset so rarities aren't limited by current visibility.
|
||||
if (typeof MAGIC_ITEMS_DATA === 'undefined') return [];
|
||||
const values = new Set();
|
||||
MAGIC_ITEMS_DATA.forEach(item => {
|
||||
if (item.rarity) values.add(item.rarity);
|
||||
});
|
||||
const list = Array.from(values);
|
||||
return list.sort(raritySort);
|
||||
}
|
||||
|
||||
function applyMarketFilters(items) {
|
||||
// Empty selections mean "show none" for that filter (intentional for clear state).
|
||||
const name = marketFilters.name.trim().toLowerCase();
|
||||
const faction = marketFilters.faction;
|
||||
const rarity = marketFilters.rarity;
|
||||
const stock = marketFilters.stock;
|
||||
const attunement = marketFilters.attunement;
|
||||
const costMin = marketFilters.costMin === '' ? null : Number(marketFilters.costMin);
|
||||
const costMax = marketFilters.costMax === '' ? null : Number(marketFilters.costMax);
|
||||
|
||||
return items.filter(item => {
|
||||
if (name && !item.name.toLowerCase().includes(name)) return false;
|
||||
if (faction.length === 0 || !faction.includes(item.faction)) return false;
|
||||
if (rarity.length === 0 || !rarity.includes(item.rarity)) return false;
|
||||
if (stock.length === 0 || !stock.includes(item.stock)) return false;
|
||||
if (attunement.length === 0 || !attunement.includes(item.attunementRequired)) return false;
|
||||
if (costMin !== null && item.adjustedPrice < costMin) return false;
|
||||
if (costMax !== null && item.adjustedPrice > costMax) return false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function updateMarketFilter(key, value) {
|
||||
// Preserve focus while re-rendering.
|
||||
captureMarketFilterFocus();
|
||||
marketFilters[key] = value;
|
||||
if (marketData && campaignData) {
|
||||
renderMarket(marketData, campaignData.reputation);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleMarketFilterValue(key, value, isChecked) {
|
||||
// Toggle a checkbox value within the active selection array.
|
||||
const values = marketFilters[key];
|
||||
if (!Array.isArray(values)) return;
|
||||
const index = values.indexOf(value);
|
||||
if (isChecked && index === -1) {
|
||||
values.push(value);
|
||||
} else if (!isChecked && index >= 0) {
|
||||
values.splice(index, 1);
|
||||
}
|
||||
if (marketData && campaignData) {
|
||||
renderMarket(marketData, campaignData.reputation);
|
||||
}
|
||||
}
|
||||
|
||||
function clearMarketFilter(key) {
|
||||
// Clear selection or input for the given filter key.
|
||||
captureMarketFilterFocus();
|
||||
if (Array.isArray(marketFilters[key])) {
|
||||
marketFilters[key] = [];
|
||||
} else {
|
||||
marketFilters[key] = '';
|
||||
}
|
||||
if (key === 'costMin' || key === 'costMax' || key === 'name') {
|
||||
marketFilterFocus = {
|
||||
key,
|
||||
selectionStart: 0,
|
||||
selectionEnd: 0
|
||||
};
|
||||
}
|
||||
if (marketData && campaignData) {
|
||||
renderMarket(marketData, campaignData.reputation);
|
||||
}
|
||||
}
|
||||
|
||||
function selectAllMarketFilter(key) {
|
||||
// Select all values for the given filter.
|
||||
const values = marketFilters[key];
|
||||
if (!Array.isArray(values)) return;
|
||||
marketFilters[key] = [...(marketFilterAllValues[key] || [])];
|
||||
if (marketData && campaignData) {
|
||||
renderMarket(marketData, campaignData.reputation);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleMarketFilterDropdown(key) {
|
||||
// Open/close the filter popover for the given column.
|
||||
marketFilterOpenKey = marketFilterOpenKey === key ? null : key;
|
||||
if (marketData && campaignData) {
|
||||
renderMarket(marketData, campaignData.reputation);
|
||||
}
|
||||
if (marketFilterOpenKey) {
|
||||
requestAnimationFrame(() => positionMarketFilterPopover(marketFilterOpenKey));
|
||||
}
|
||||
}
|
||||
|
||||
function updateMarketFilterSearch(key, value) {
|
||||
// Update popover search text and refresh the list.
|
||||
captureMarketFilterFocus();
|
||||
marketFilterSearch[key] = value;
|
||||
if (marketData && campaignData) {
|
||||
renderMarket(marketData, campaignData.reputation);
|
||||
}
|
||||
}
|
||||
|
||||
function closeMarketFilterDropdown() {
|
||||
// Close any open popover when clicking outside.
|
||||
if (marketFilterOpenKey !== null) {
|
||||
marketFilterOpenKey = null;
|
||||
if (marketData && campaignData) {
|
||||
renderMarket(marketData, campaignData.reputation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function positionMarketFilterPopover(key) {
|
||||
// Keep the popover within the viewport on narrow screens.
|
||||
const dropdown = document.querySelector(`.market-filter-dropdown[data-filter-key="${key}"]`);
|
||||
if (!dropdown) return;
|
||||
const trigger = dropdown.querySelector('.market-filter-trigger');
|
||||
const popover = dropdown.querySelector('.market-filter-popover');
|
||||
if (!popover || !trigger) return;
|
||||
|
||||
const margin = 12;
|
||||
const viewportWidth = window.innerWidth;
|
||||
const triggerRect = trigger.getBoundingClientRect();
|
||||
|
||||
popover.style.position = 'absolute';
|
||||
popover.style.left = '0';
|
||||
popover.style.right = 'auto';
|
||||
popover.style.transform = 'none';
|
||||
|
||||
const popoverRect = popover.getBoundingClientRect();
|
||||
const popoverWidth = popoverRect.width;
|
||||
|
||||
const maxLeft = Math.max(margin, viewportWidth - popoverWidth - margin);
|
||||
const left = Math.min(Math.max(triggerRect.left, margin), maxLeft);
|
||||
const shiftLeft = triggerRect.left - left;
|
||||
if (shiftLeft !== 0) {
|
||||
popover.style.transform = `translateX(-${shiftLeft}px)`;
|
||||
}
|
||||
}
|
||||
|
||||
function captureMarketFilterFocus() {
|
||||
// Track cursor placement to avoid input blur on re-render.
|
||||
const el = document.activeElement;
|
||||
if (!el || !el.dataset) return;
|
||||
if (el.dataset.filterKey) {
|
||||
marketFilterFocus = {
|
||||
type: 'filter',
|
||||
key: el.dataset.filterKey,
|
||||
selectionStart: el.selectionStart,
|
||||
selectionEnd: el.selectionEnd
|
||||
};
|
||||
} else if (el.dataset.filterSearchKey) {
|
||||
marketFilterFocus = {
|
||||
type: 'search',
|
||||
key: el.dataset.filterSearchKey,
|
||||
selectionStart: el.selectionStart,
|
||||
selectionEnd: el.selectionEnd
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function restoreMarketFilterFocus() {
|
||||
// Restore focus and caret to the active filter input.
|
||||
if (!marketFilterFocus) return;
|
||||
const selector = marketFilterFocus.type === 'search'
|
||||
? `[data-filter-search-key="${marketFilterFocus.key}"]`
|
||||
: `[data-filter-key="${marketFilterFocus.key}"]`;
|
||||
const el = document.querySelector(selector);
|
||||
if (!el) return;
|
||||
el.focus();
|
||||
if (typeof el.selectionStart === 'number') {
|
||||
el.setSelectionRange(marketFilterFocus.selectionStart, marketFilterFocus.selectionEnd);
|
||||
}
|
||||
marketFilterFocus = null;
|
||||
}
|
||||
|
||||
function captureMarketTableScroll() {
|
||||
// Preserve horizontal scroll when re-rendering on mobile.
|
||||
const wrapper = document.querySelector('.market-table-wrapper');
|
||||
if (!wrapper) return;
|
||||
marketTableScrollLeft = wrapper.scrollLeft;
|
||||
}
|
||||
|
||||
function restoreMarketTableScroll() {
|
||||
// Restore horizontal scroll position after re-render.
|
||||
if (marketTableScrollLeft === null) return;
|
||||
const wrapper = document.querySelector('.market-table-wrapper');
|
||||
if (!wrapper) return;
|
||||
wrapper.scrollLeft = marketTableScrollLeft;
|
||||
marketTableScrollLeft = null;
|
||||
}
|
||||
|
||||
@ -75,7 +75,11 @@
|
||||
<!-- Magic Items Data -->
|
||||
<script src="magic-items-data.js"></script>
|
||||
|
||||
<!-- Market Filtering -->
|
||||
<script src="filtering.js"></script>
|
||||
|
||||
<!-- App Logic -->
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
174
styles.css
174
styles.css
@ -979,6 +979,7 @@ button {
|
||||
|
||||
.no-items {
|
||||
text-align: center;
|
||||
vertical-align: top;
|
||||
padding: 2rem;
|
||||
color: #b8a992;
|
||||
font-style: italic;
|
||||
@ -1017,6 +1018,177 @@ button {
|
||||
background: rgba(139, 115, 85, 0.5);
|
||||
}
|
||||
|
||||
.market-table thead tr:first-child th {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.market-filter-row th {
|
||||
background: rgba(245, 222, 179, 0.5);
|
||||
padding: 0.5rem 0.75rem;
|
||||
vertical-align: top;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.market-table tbody {
|
||||
height: 40vh;
|
||||
}
|
||||
|
||||
.market-filter-input,
|
||||
.market-filter-select {
|
||||
width: 100%;
|
||||
padding: 0.35rem 0.5rem;
|
||||
border: 1px solid rgba(139, 115, 85, 0.5);
|
||||
border-radius: 4px;
|
||||
font-size: 0.85rem;
|
||||
background: #fff;
|
||||
color: #2c1810;
|
||||
}
|
||||
|
||||
.market-filter-input::placeholder {
|
||||
color: #8b7355;
|
||||
}
|
||||
|
||||
.market-filter-search {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.market-filter-search-input {
|
||||
padding-right: 1.6rem;
|
||||
}
|
||||
|
||||
.market-filter-clear {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 6px;
|
||||
transform: translateY(-50%);
|
||||
border: none;
|
||||
padding: 0 0.5vw;
|
||||
background: transparent;
|
||||
color: #8b7355;
|
||||
cursor: pointer;
|
||||
font-size: 1.5rem;
|
||||
line-height: 1;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.market-filter-clear.active {
|
||||
opacity: 0.9;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.market-filter-range {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.cost-col {
|
||||
width: 150px;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.market-filter-input.cost-filter {
|
||||
flex: 1 1 70px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.market-filter-dropdown {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.market-filter-trigger {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
border: 1px solid rgba(139, 115, 85, 0.5);
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
color: #2c1810;
|
||||
padding: 0.35rem 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.market-filter-trigger.is-empty {
|
||||
border-color: #b22222;
|
||||
color: #b22222;
|
||||
}
|
||||
|
||||
.market-filter-summary {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.market-filter-popover {
|
||||
position: absolute;
|
||||
top: calc(100% + 6px);
|
||||
left: 0;
|
||||
z-index: 20;
|
||||
width: 125px;
|
||||
max-width: 80vw;
|
||||
background: #fff;
|
||||
border: 1px solid rgba(139, 115, 85, 0.5);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 8px 18px rgba(0,0,0,0.2);
|
||||
padding: 0.5rem;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.market-filter-popover.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.market-filter-popover-header {
|
||||
display: grid;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.market-filter-popover-search {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.market-filter-popover-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.market-filter-popover-actions button {
|
||||
border: 1px solid rgba(139, 115, 85, 0.4);
|
||||
background: rgba(245, 222, 179, 0.6);
|
||||
color: #2c1810;
|
||||
border-radius: 4px;
|
||||
padding: 0.2rem 0.4rem;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.market-filter-options {
|
||||
margin-top: 0.5rem;
|
||||
max-height: 180px;
|
||||
overflow-y: auto;
|
||||
display: grid;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.market-filter-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
font-size: 0.8rem;
|
||||
color: #2c1810;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.market-filter-option input {
|
||||
accent-color: #8b4513;
|
||||
}
|
||||
|
||||
.market-filter-empty {
|
||||
font-size: 0.75rem;
|
||||
color: #8b7355;
|
||||
}
|
||||
|
||||
.market-table tbody tr {
|
||||
border-bottom: 1px solid rgba(139, 115, 85, 0.2);
|
||||
transition: background 0.2s ease;
|
||||
@ -1159,10 +1331,12 @@ button {
|
||||
/* Market table wrapper for horizontal scroll */
|
||||
.market-table-wrapper {
|
||||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
margin: 0 -10px;
|
||||
padding: 0 10px;
|
||||
position: relative;
|
||||
min-height: 320px;
|
||||
}
|
||||
|
||||
/* Scroll hint shadow on mobile */
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user