diff options
| author | Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> | 2026-11-08 00:48:32 +0900 |
|---|---|---|
| committer | Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> | 2026-11-08 00:48:32 +0900 |
| commit | a81bb98c971bd9470bbba0c415b6c111f5146ad3 (patch) | |
| tree | be5fc4b3280a8892c3d277c19cca588361f868f5 /src/components | |
| parent | f6bdca7dea59e81006a659676a00aae10d2cec58 (diff) | |
公園マップ: フィルタを分類・拡充し、地図の表示不具合を修正
ParksFilters.astro: フィルタを「遊具/広場・休憩/設備/利用ルール」に
分類し、パーゴラ・ボール遊び禁止の表示なし・犬禁止の表示なしを追加。
冒頭にβ版の注意書きを追加
ParkMap.astro: 全画面時のズーム調整と、小数ズーム時のタイル読込の
不具合を修正
ParksTable.astro: 絞り込みトークンを追加し、確認済みの表示を反映
park-editor: 確認チェックの自動同期と文字化けを修正
kodaira-parks.json: 水飲み場表記の統一と確認済みを反映
park-icons.ts: パーゴラのアイコンを追加
parks.mdx: 説明文を更新
Diffstat (limited to 'src/components')
| -rw-r--r-- | src/components/ParkMap.astro | 152 | ||||
| -rw-r--r-- | src/components/ParkMapHelp.astro | 15 | ||||
| -rw-r--r-- | src/components/ParksFilters.astro | 173 | ||||
| -rw-r--r-- | src/components/ParksTable.astro | 558 |
4 files changed, 610 insertions, 288 deletions
diff --git a/src/components/ParkMap.astro b/src/components/ParkMap.astro index 3a208db..b9aa641 100644 --- a/src/components/ParkMap.astro +++ b/src/components/ParkMap.astro @@ -24,6 +24,9 @@ width: 100%; border: 1px solid var(--sl-color-gray-5); border-radius: 8px; + /* タイルが未読込のとき、ダークテーマの背景色(青みのある濃色)が + 地図の領域に見えないよう、地図らしい下地色を敷いておく。 */ + background: #e8e6e1; } .park-map:fullscreen, .park-map:-webkit-full-screen { @@ -97,6 +100,9 @@ font-size: 0.7rem; font-weight: 700; } + :global(.park-map-filter-status[hidden]) { + display: none; + } :global(.park-map-filter-footer) { flex: none; display: flex; @@ -167,6 +173,11 @@ border-color: #e0e0e0; background: #fff; color: #6d4c41; + padding: 0; + } + :global(.park-popup-ico-pet svg) { + width: 100%; + height: 100%; } :global(.park-popup-ico-clock) { border-color: #ffe082; @@ -179,12 +190,6 @@ color: #fff; font-size: 8px; } - /* ボール遊びは可で試合形式だけ禁止のときに「試」バッジを並べる */ - :global(.park-popup-ico-ball) { - width: auto; - gap: 2px; - padding: 0 2px; - } :global(.park-popup-mini) { position: relative; display: inline-flex; @@ -264,6 +269,17 @@ color: #0b57d0; font-weight: 700; } + :global(.google-ico) { + display: inline-flex; + flex: none; + width: 15px; + height: 15px; + } + :global(.google-ico svg) { + display: block; + width: 100%; + height: 100%; + } :global(.park-name-marker) { background: none; @@ -851,7 +867,7 @@ dragging: !isTouch, touchZoom: true, scrollWheelZoom: true, - zoomSnap: 1, + zoomSnap: 0.1, zoomDelta: 1, zoomAnimation: false, fadeAnimation: false, @@ -923,7 +939,7 @@ // 帰属表示の「Leaflet」旗アイコンを消す(OSMの表示は残す)。 map.attributionControl.setPrefix(false); - const tileLayer = L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { + L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { maxZoom: 19, attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors', @@ -955,7 +971,7 @@ if (filterCaret) filterCaret.textContent = "▾"; }); filterFooter.appendChild(filterClose); - filterPanel.append(filterToggle, filterBody, filterFooter); + filterPanel.append(filterToggle, filterBody); L.DomEvent.disableClickPropagation(filterPanel); L.DomEvent.disableScrollPropagation(filterPanel); filterToggle.addEventListener("click", () => { @@ -974,6 +990,7 @@ next: controls.nextSibling, }; filterBody.append(controls); + filterBody.append(filterFooter); map.getContainer().appendChild(filterPanel); filterPanel.hidden = false; filterBody.hidden = false; @@ -999,6 +1016,30 @@ const target = map.getContainer(); const isCssMax = () => target.classList.contains("is-maximized"); + // 全画面の切替前のサイズ・ズーム。Leaflet はコンテナが大きくなっても + // ズームを自動では変えないため、切替後も同じ範囲が画面に収まるよう、 + // 画面の大きさの比に応じてズームを合わせ直す。 + let preSize: { x: number; y: number } | null = null; + let preZoom = 0; + let lastSize: { x: number; y: number } | null = null; + const captureNow = () => { + preSize = map.getSize(); + preZoom = map.getZoom(); + }; + const scaleZoomToFit = () => { + if (!preSize) return; + const now = map.getSize(); + // 面積比の平方根(縦横の拡大率の相乗平均)で拡大・縮小する。 + // 入るときと出るときで同じ倍率になり、行って戻ると元のズームに戻る。 + const ratio = Math.sqrt((now.x * now.y) / (preSize.x * preSize.y)); + if (!Number.isFinite(ratio) || ratio <= 0 || Math.abs(ratio - 1) < 0.02) return; + const z = preZoom + Math.log2(ratio); + map.setView( + map.getCenter(), + Math.max(map.getMinZoom(), Math.min(map.getMaxZoom(), z)), + { animate: false }, + ); + }; const update = () => { const active = document.fullscreenElement === target || isCssMax(); button.innerHTML = active ? "✕" : "⛶"; @@ -1010,8 +1051,24 @@ if (active) map.dragging.enable(); else map.dragging.disable(); } - window.setTimeout(() => map.invalidateSize(), 60); - window.setTimeout(() => map.invalidateSize(), 350); + // Esc などボタン操作以外で切り替わったときのために、直前の状態を控える。 + if (!preSize) { + preSize = lastSize || map.getSize(); + preZoom = map.getZoom(); + } + // 端末によってサイズが変わるタイミングが違うので、2回に分けて合わせる。 + window.setTimeout(() => { + map.invalidateSize(); + scaleZoomToFit(); + }, 60); + window.setTimeout(() => { + map.invalidateSize(); + scaleZoomToFit(); + }, 350); + window.setTimeout(() => { + lastSize = map.getSize(); + preSize = null; + }, 600); }; let maxHome: { parent: Node; next: Node | null } | null = null; const enterCssMax = () => { @@ -1035,6 +1092,8 @@ L.DomEvent.on(button, "click", (ev: Event) => { L.DomEvent.stop(ev); + // 切替後のズーム合わせ用に、切替前のサイズ・ズームを控えておく。 + captureNow(); if (document.fullscreenElement === target) { document.exitFullscreen?.().catch(() => exitCssMax()); return; @@ -1127,24 +1186,35 @@ .filter((s) => s && !s.includes("防火水槽") && !s.includes("消火栓")) .filter((s) => DISASTER_KEEP.test(s)) .join("、"); - const waterRaw = String(rec?.["水飲み場"] || "").trim(); - // 水飲み場は数量ではなく有無だけを、分かるように表示する。 - const water = waterRaw - ? /[??]/.test(waterRaw) - ? "水飲み場あり?" - : "水飲み場あり" - : "水飲み場なし"; + // 水飲み場は「水飲み場x1」のように、手洗いも含めて数を示す。 + const waterText = (value: string) => { + const text = String(value || "").trim(); + if (!text) return "水飲み場なし"; + if (/[??]/.test(text)) return "水飲み場?"; + let total = 0; + for (const part of text.split(/[、,]/)) { + const nums = [...part.matchAll(/[x×](\d+)/g)].map((m) => Number(m[1])); + if (nums.length) total += nums.reduce((a, b) => a * b, 1); + } + return total > 0 ? `水飲み場x${total}` : "水飲み場"; + }; + const water = waterText(String(rec?.["水飲み場"] || "")); // ○/〇 などの表記は「〜あり」にして意味が分かるようにする。 const presence = (label: string, value: unknown) => { const v = String(value ?? "").trim(); if (!v) return ""; return /^[○〇◯あり有1]+$/.test(v) ? `${label}あり` : v; }; - // ボール遊びは、試合形式の可否を括弧書きで添える。 + // ボール遊び禁止なら試合形式の記載は省く。試合形式だけ禁止ならそれだけを示す。 const ballText = String(rec?.["ボール遊び"] || "").trim(); const matchText = String(rec?.["試合形式のボール遊び"] || "").trim(); - const ball = - ballText && matchText ? `${ballText}(試合形式${matchText})` : ballText; + const ballBanned = /禁止/.test(ballText); + const matchBanned = /禁止/.test(matchText); + const ball = ballBanned + ? "ボール遊び禁止" + : matchBanned + ? "試合形式禁止" + : ballText; // 項目名は1文字の文字アイコンで示す(水・ベンチ・池は絵アイコン)。 const rowIcons: Record<string, string> = { 遊具: "遊", @@ -1191,12 +1261,9 @@ const labelText = String(label); const valueText = String(value); const isBall = labelText === "ボール遊び"; - // ボール遊び全体が禁止ならボールに×、試合形式だけ禁止なら「試」に×を付ける。 + // ボール遊び全体が禁止のときだけボールに×を付ける。 const banned = isBall ? /禁止/.test(ballText) : /禁止/.test(valueText); - let iconHtml = rowIcons[labelText] || ""; - if (isBall && !banned && /禁止/.test(matchText)) { - iconHtml += '<span class="park-popup-mini" title="試合形式は禁止">試×</span>'; - } + const iconHtml = rowIcons[labelText] || ""; const icoClass = `park-popup-ico${ROW_MOD[labelText] || ""}${banned ? " is-banned" : ""}${isBall ? " park-popup-ico-ball" : ""}`; return `<div class="park-popup-row" title="${escapePopup(labelText)}"><span class="${icoClass}" role="img" aria-label="${escapePopup(labelText)}">${iconHtml}</span><span class="park-popup-val">${escapePopup(valueText)}</span></div>`; }) @@ -1210,7 +1277,7 @@ rec?.["地図URL"] || `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(query)}`; const route = `https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent(dest)}&travelmode=walking`; - return `<div class="park-popup"><strong>${name}</strong>${owner ? `<span class="park-popup-owner">${escapePopup(owner)}</span>` : ""}${addr || area || type ? `<div class="park-popup-meta">${[addr, area, type].filter(Boolean).join(" ・ ")}</div>` : ""}${detailHtml}<div class="park-popup-links"><a href="${maps}" target="_blank" rel="noopener">🌏Google</a><a href="${route}" target="_blank" rel="noopener">🚶経路</a></div></div>`; + return `<div class="park-popup"><strong>${name}</strong>${owner ? `<span class="park-popup-owner">${escapePopup(owner)}</span>` : ""}${addr || area || type ? `<div class="park-popup-meta">${[addr, area, type].filter(Boolean).join(" ・ ")}</div>` : ""}${detailHtml}<div class="park-popup-links"><a href="${maps}" target="_blank" rel="noopener"><span class="google-ico">${PARK_ICONS.google.svg}</span>Google</a><a href="${route}" target="_blank" rel="noopener">🚶経路</a></div></div>`; }; const filterName = (name: string, scroll = false) => { @@ -1908,14 +1975,14 @@ const nearbyHit = nearbyEntries.find((entry) => entry.key === key); if (nearbyHit) { const group = L.featureGroup([nearbyHit.layer]); - map.fitBounds(group.getBounds(), { padding: [40, 40], maxZoom: 17 }); + map.fitBounds(group.getBounds(), { padding: [40, 40], maxZoom: map.getMaxZoom() }); nearbyHit.layer.openPopup?.(); return true; } } const target = layersByName.get(name); if (target) { - map.fitBounds(target.getBounds(), { padding: [40, 40], maxZoom: 17 }); + map.fitBounds(target.getBounds(), { padding: [40, 40], maxZoom: map.getMaxZoom() }); const targetLayer = typeof target.getLayers === "function" ? target.getLayers()[0] : target; targetLayer?.openPopup?.(); @@ -1926,7 +1993,7 @@ .map((entry) => entry.layer); if (nearbyTargets.length) { const group = L.featureGroup(nearbyTargets); - map.fitBounds(group.getBounds(), { padding: [40, 40], maxZoom: 17 }); + map.fitBounds(group.getBounds(), { padding: [40, 40], maxZoom: map.getMaxZoom() }); nearbyTargets[0].openPopup?.(); return true; } @@ -1950,30 +2017,39 @@ // フィルタの対象(複数ならその範囲)に地図をフォーカスする。 let filterFocusTimer = 0; - const focusOnParks = (names: Set<string> | null) => { + const focusOnParks = (names: Set<string> | null, nearbyNames: Set<string> | null) => { const bounds = L.latLngBounds([]); const add = (lyr: any) => { const b = lyr?.getBounds?.(); if (b && b.isValid()) bounds.extend(b); }; + const addPoint = (ll: any) => { + if (ll) bounds.extend(ll); + }; if (names) { allLayers.forEach((lyr) => { if (names.has(lyr._parkName || "")) add(lyr); }); nearbyEntries.forEach(({ name, layer }) => { - if (names.has(name)) add(layer); + if (nearbyNames && nearbyNames.has(name)) add(layer); + }); + facilityMarkers.forEach(({ name, layer }) => { + if (names.has(name)) addPoint(layer.getLatLng?.()); }); } else { allLayers.forEach(add); + facilityMarkers.forEach(({ layer }) => addPoint(layer.getLatLng?.())); } if (!bounds.isValid()) return; - map.fitBounds(bounds, { padding: [40, 40], maxZoom: 16, animate: true }); + map.fitBounds(bounds, { padding: [40, 40], maxZoom: map.getMaxZoom(), animate: true }); }; // 選択中の公園を強調するためのクラス(グロー表示) document.addEventListener("park:filter", (e: Event) => { const detail = (e as CustomEvent).detail || {}; const names = new Set<string>(detail.names || []); + // 市外の近い公園は小平市内と同名でも別の公園なので、別セットで扱う。 + const nearbyNames = new Set<string>(detail.nearbyNames ?? detail.names ?? []); const active = Boolean(detail.active); if (filterStatus) filterStatus.hidden = !active; const fromMap = Boolean(detail.fromMap); @@ -1997,7 +2073,7 @@ } }); nearbyEntries.forEach(({ name, layer: lyr }) => { - const selected = Boolean(name && names.has(name)); + const selected = Boolean(name && nearbyNames.has(name)); lyr.setStyle(selected ? NEARBY_SELECTED_STYLE : nearbyBaseStyle(lyr)); markSelected(lyr, selected); if (selected) lyr.bringToFront?.(); @@ -2037,7 +2113,7 @@ markBlink(lyr, true); return; } - if (name && names.has(name)) { + if (name && nearbyNames.has(name)) { lyr.setStyle(NEARBY_SELECTED_STYLE); markSelected(lyr, true); lyr.bringToFront?.(); @@ -2056,7 +2132,7 @@ // フィルタの対象にフォーカスする(キーワード入力は少し待ってから)。 if (detail.focus) { - const run = () => focusOnParks(active ? names : null); + const run = () => focusOnParks(active ? names : null, active ? nearbyNames : null); window.clearTimeout(filterFocusTimer); if (detail.focus === "later") { filterFocusTimer = window.setTimeout(run, 800); @@ -2223,7 +2299,9 @@ // レイアウト確定後にサイズを再計算し、タイルを再配置する。 const refresh = () => { map.invalidateSize(); - tileLayer.redraw(); + // redraw() は使わない。Leaflet 1.9 の redraw() は小数ズーム時に + // _tileZoom を小数(例: 12.9)にしてしまい、タイルが読めなくなる。 + // invalidateSize と fitAll だけでタイルは更新される。 fitAll(); }; requestAnimationFrame(refresh); diff --git a/src/components/ParkMapHelp.astro b/src/components/ParkMapHelp.astro index 4402567..5bb0387 100644 --- a/src/components/ParkMapHelp.astro +++ b/src/components/ParkMapHelp.astro @@ -38,7 +38,7 @@ align-items: center; margin-top: 0.5rem; color: var(--sl-color-gray-2); - font-size: 0.8rem; + font-size: 1rem; } .park-map-legend span { display: inline-flex; @@ -47,8 +47,9 @@ } .swatch { display: inline-block; - width: 12px; - height: 12px; + box-sizing: border-box; + width: 18px; + height: 18px; border-radius: 2px; } .swatch-matched { @@ -122,7 +123,7 @@ .park-map-note { margin-top: 0.4rem; color: var(--sl-color-gray-2); - font-size: 0.8rem; + font-size: 1rem; } .park-play-legend { display: flex; @@ -130,7 +131,11 @@ gap: 0.35rem 0.8rem; margin: 0.5rem 0 0; color: var(--sl-color-gray-2); - font-size: 0.78rem; + font-size: 1rem; + } + .park-play-legend :global(.park-play) { + width: 18px; + height: 18px; } .park-play-legend-item { display: inline-flex; diff --git a/src/components/ParksFilters.astro b/src/components/ParksFilters.astro index 576c0ff..e93b4ed 100644 --- a/src/components/ParksFilters.astro +++ b/src/components/ParksFilters.astro @@ -55,57 +55,81 @@ const facilityTypes = [ </label> <fieldset class="parks-facilities"> <legend>設備・遊具で絞る</legend> - <label><input type="checkbox" data-fac-filter value="toilet" /><span class="filter-wc" aria-hidden="true">WC</span>トイレ</label> - <label><input type="checkbox" data-fac-filter value="water" /><span class="filter-icon filter-icon-water" set:html={PARK_ICONS.water.svg} />水飲み</label> - <label> - <input type="checkbox" data-fac-filter value="picnic" /> - <span class="filter-icon filter-icon-grass" set:html={PARK_ICONS.grass.svg} />草地広場 - </label> - <label> - <input type="checkbox" data-fac-filter value="table" /> - <span class="filter-icon" set:html={PARK_ICONS.picnic.svg} />野外卓/縁台 - </label> - <label><input type="checkbox" data-fac-filter value="clock" /><span class="filter-icon filter-icon-clock" set:html={PARK_ICONS.clock.svg} />時計</label> - <label> - <input type="checkbox" data-fac-filter value="sand" /> - <span class="filter-icon" set:html={PARK_ICONS.sand.svg} />砂場 - </label> - <label> - <input type="checkbox" data-fac-filter value="swing" /> - <span class="filter-icon" set:html={PARK_ICONS.swing.svg} />ブランコ - </label> - <label> - <input type="checkbox" data-fac-filter value="seesaw" /> - <span class="filter-icon" set:html={PARK_ICONS.teeter.svg} />シーソー - </label> - <label> - <input type="checkbox" data-fac-filter value="spring" /> - <span class="filter-icon" set:html={PARK_ICONS.spring.svg} />スイング遊具 - </label> - <label> - <input type="checkbox" data-fac-filter value="toddler" /> - <span class="filter-icon" set:html={PARK_ICONS.duck.svg} />幼児用遊具 - </label> - <label> - <input type="checkbox" data-fac-filter value="jungle" /> - <span class="filter-icon" set:html={PARK_ICONS.climbing.svg} />ジャングルジム - </label> - <label> - <input type="checkbox" data-fac-filter value="plane" /> - <span class="filter-icon" set:html={PARK_ICONS.airplane.svg} />ヒコーキ - </label> - <label> - <input type="checkbox" data-fac-filter value="slide" /> - <span class="filter-icon" set:html={PARK_ICONS.slide.svg} />滑り台 - </label> - <label> - <input type="checkbox" data-fac-filter value="complex" /> - <span class="filter-icon" set:html={PARK_ICONS.playground.svg} />複合遊具 - </label> - <label><input type="checkbox" data-fac-filter value="basket" />🏀バスケゴール</label> - <label><input type="checkbox" data-fac-filter value="shelter" /><span class="filter-icon" set:html={PARK_ICONS.shelter.svg} />東屋/シェルター</label> - <label><input type="checkbox" data-fac-filter value="health" /><span class="filter-icon" set:html={PARK_ICONS.health.svg} />健康遊具</label> - <label><input type="checkbox" data-fac-filter value="special" /><span class="filter-icon" set:html={PARK_ICONS.special.svg} />特殊遊具</label> + <div class="fac-group"> + <span class="fac-group-title">遊具</span> + <div class="fac-group-items"> + <label> + <input type="checkbox" data-fac-filter value="sand" /> + <span class="filter-icon" set:html={PARK_ICONS.sand.svg} />砂場 + </label> + <label> + <input type="checkbox" data-fac-filter value="swing" /> + <span class="filter-icon" set:html={PARK_ICONS.swing.svg} />ブランコ + </label> + <label> + <input type="checkbox" data-fac-filter value="seesaw" /> + <span class="filter-icon" set:html={PARK_ICONS.teeter.svg} />シーソー + </label> + <label> + <input type="checkbox" data-fac-filter value="spring" /> + <span class="filter-icon" set:html={PARK_ICONS.spring.svg} />スイング遊具 + </label> + <label> + <input type="checkbox" data-fac-filter value="toddler" /> + <span class="filter-icon" set:html={PARK_ICONS.duck.svg} />幼児用遊具 + </label> + <label> + <input type="checkbox" data-fac-filter value="jungle" /> + <span class="filter-icon" set:html={PARK_ICONS.climbing.svg} />ジャングルジム + </label> + <label> + <input type="checkbox" data-fac-filter value="plane" /> + <span class="filter-icon" set:html={PARK_ICONS.airplane.svg} />ヒコーキ + </label> + <label> + <input type="checkbox" data-fac-filter value="slide" /> + <span class="filter-icon" set:html={PARK_ICONS.slide.svg} />滑り台 + </label> + <label> + <input type="checkbox" data-fac-filter value="complex" /> + <span class="filter-icon" set:html={PARK_ICONS.playground.svg} />複合遊具 + </label> + <label><input type="checkbox" data-fac-filter value="basket" />🏀バスケゴール</label> + <label><input type="checkbox" data-fac-filter value="health" /><span class="filter-icon" set:html={PARK_ICONS.health.svg} />健康遊具</label> + <label><input type="checkbox" data-fac-filter value="special" /><span class="filter-icon" set:html={PARK_ICONS.special.svg} />特殊遊具</label> + </div> + </div> + <div class="fac-group"> + <span class="fac-group-title">広場・休憩</span> + <div class="fac-group-items"> + <label> + <input type="checkbox" data-fac-filter value="picnic" /> + <span class="filter-icon filter-icon-grass" set:html={PARK_ICONS.grass.svg} />草地広場 + </label> + <label> + <input type="checkbox" data-fac-filter value="table" /> + <span class="filter-icon" set:html={PARK_ICONS.picnic.svg} />野外卓/縁台 + </label> + <label><input type="checkbox" data-fac-filter value="shelter" /><span class="filter-icon" set:html={PARK_ICONS.shelter.svg} />東屋/シェルター</label> + <label><input type="checkbox" data-fac-filter value="pergola" /><span class="filter-icon" set:html={PARK_ICONS.pergola.svg} />パーゴラ</label> + </div> + </div> + <div class="fac-group"> + <span class="fac-group-title">設備</span> + <div class="fac-group-items"> + <label><input type="checkbox" data-fac-filter value="toilet" /><span class="filter-wc" aria-hidden="true">WC</span>トイレ</label> + <label><input type="checkbox" data-fac-filter value="water" /><span class="filter-icon filter-icon-water" set:html={PARK_ICONS.water.svg} />水飲み</label> + <label><input type="checkbox" data-fac-filter value="clock" /><span class="filter-icon filter-icon-clock" set:html={PARK_ICONS.clock.svg} />時計</label> + </div> + </div> + <div class="fac-group"> + <span class="fac-group-title">利用ルール</span> + <div class="fac-group-items"> + <label><input type="checkbox" data-fac-filter value="ballok" /><span class="filter-icon" set:html={PARK_ICONS.ball.svg} />ボール遊び禁止の表示なし</label> + <label><input type="checkbox" data-fac-filter value="dogok" /><span class="filter-icon" set:html={PARK_ICONS.dog.svg} />犬禁止の表示なし</label> + <span class="fac-note">利用ルールは実際と異なる場合があります。現地の看板などでご確認ください。</span> + </div> + </div> </fieldset> <label class="parks-fire-toggle" title="消火栓・防火水槽を地図に表示する"> <input type="checkbox" data-show-fire /> @@ -115,7 +139,7 @@ const facilityTypes = [ </div> <p class="parks-disclaimer"> - 情報には誤りが含まれる場合があります。最新の情報は各施設の公式ページでご確認ください。 + 現在β版のテスト中です。情報に誤りが含まれている可能性があります(特に利用ルール)。最新の情報は各施設の公式ページや現地の表示でご確認ください。遊具・設備などの一部のアイコンは、地図を拡大すると表示されます。 </p> <style> @@ -163,6 +187,49 @@ const facilityTypes = [ border: 1px solid var(--sl-color-hairline, #ccc); border-radius: 0.3rem; } + /* フィルタを「遊具 / 広場・休憩 / 設備 / 利用ルール」にまとまりで見せる */ + .fac-group { + display: flex; + flex: 1 0 100%; + align-items: flex-start; + gap: 0.45rem; + } + .fac-group-title { + flex: none; + width: 4rem; + font-size: 0.72rem; + font-weight: 700; + line-height: 1.5rem; + color: var(--sl-color-gray-2, #64748b); + text-align: right; + } + .fac-group-items { + display: flex; + flex: 1 1 auto; + flex-wrap: wrap; + align-items: center; + gap: 0 0.6rem; + } + /* グループの注記(利用ルールの但し書きなど) */ + .fac-note { + flex: 1 0 100%; + padding-top: 0.1rem; + font-size: 0.72rem; + line-height: 1.35; + color: var(--sl-color-gray-2, #64748b); + } + @media (max-width: 700px) { + .fac-group { + flex-direction: column; + align-items: stretch; + gap: 0.1rem; + } + .fac-group-title { + width: auto; + line-height: 1.2; + text-align: left; + } + } .parks-facilities legend { padding: 0 0.2rem; font-size: 0.78em; @@ -174,6 +241,8 @@ const facilityTypes = [ align-items: center; gap: 0.2rem; min-height: 1.5rem; + /* Starlight の「label + label」の上マージンで位置がずれ、縦も伸びるため打ち消す */ + margin: 0; font-size: 0.8rem; line-height: 1.2; } diff --git a/src/components/ParksTable.astro b/src/components/ParksTable.astro index 5b7f5ac..81a93e2 100644 --- a/src/components/ParksTable.astro +++ b/src/components/ParksTable.astro @@ -4,6 +4,7 @@ // DOM を抑えるため、設備は1セルにまとめ、アイコンは CSS の擬似要素で描く。 // 絞り込み・並べ替え・カード表示は利用者の端末で実行する。 import { parks, mappedNames, type Park } from "@/data/parks-rows"; +import { PARK_ICONS } from "@/data/park-icons"; const EMPTY = "—"; @@ -76,33 +77,6 @@ function ownerClass(owner: string): string { return "parks-owner--other"; } -// 設備バッジ(アイコンは CSS の ::before で描く)。 -type Badge = { kind: string; label: string; title: string }; -function facilityBadges(p: Park): Badge[] { - const badges: Badge[] = []; - if (p["トイレ"]) { - badges.push({ kind: "toilet", label: "トイレ", title: `トイレ: ${p["トイレ"]}` }); - } - if (p["水飲み場"]) { - badges.push({ kind: "water", label: "水飲み場", title: `水飲み場: ${p["水飲み場"]}` }); - } - if (p["給水"]) { - badges.push({ kind: "water", label: "給水", title: `給水: ${p["給水"]}` }); - } - if (p["遊具"]) { - badges.push({ kind: "toy", label: "遊具", title: `遊具: ${p["遊具"]}` }); - } - const ball = p["ボール遊び"]; - if (ball && ball !== "禁止") { - const label = ball.includes("一部") ? "ボール遊び一部可" : "ボール遊び可"; - badges.push({ kind: "ball", label, title: `ボール遊び: ${ball}` }); - } - if (p["南西部指定管理"]) { - badges.push({ kind: "cm", label: "指定管理", title: "南西部地域の指定管理公園" }); - } - return badges; -} - // スイング遊具・健康遊具の判定用 const SPRING_TOKENS = [ "スプリング", "スイング", "リトルポップ", "リンクミニ", "ルッキンロッキン", @@ -156,7 +130,11 @@ function facTokens(p: Park): string { if (play.includes("滑り台") || play.includes("滑台")) tokens.push("slide"); if (play.includes("複合遊具")) tokens.push("complex"); if (play.includes("バスケ")) tokens.push("basket"); - if ((p["シェルター等"] || "").trim()) tokens.push("shelter"); + // シェルター等の欄には、シェルター・東屋・パーゴラ・GTシェードが混在している。 + // 「東屋/シェルター」と「パーゴラ」を別々に絞れるよう、キーワードで分ける。 + const shelterText = (p["シェルター等"] || "").trim(); + if (/東屋|シェルター|シェード/.test(shelterText)) tokens.push("shelter"); + if (shelterText.includes("パーゴラ")) tokens.push("pergola"); if (HEALTH_TOKENS.some((token) => play.includes(token))) tokens.push("health"); if (hasSpecialToy(play)) tokens.push("special"); if (play.includes("幼児")) tokens.push("toddler"); @@ -167,26 +145,40 @@ function facTokens(p: Park): string { if (p["草地広場"]) tokens.push("picnic"); // 野外卓・縁台がある公園。 if ((p["野外卓"] || "").trim()) tokens.push("table"); + // 利用ルール(ボール遊び禁止の表示なし・犬禁止の表示なし)。 + // 「禁止」と記録されていない公園を対象にする(未記録も含む)。 + // 公共施設と市外の公園は利用ルールを調べていないので対象外。 + if (p["施設"] !== "1" && p["種別"] !== "市外") { + if (!/禁止/.test(p["ボール遊び"] || "")) tokens.push("ballok"); + if (!/禁止/.test(p["ペット"] || "")) tokens.push("dogok"); + } return tokens.join(" "); } -// 設備セルの補足テキスト(バッジで示した項目の詳細と、その他の項目)。 -// ラベル文字は使わず、アイコンで項目を示す。 -const DETAIL_FIELDS: { key: string; icon: string }[] = [ - { key: "ベンチ・椅子", icon: "🪑" }, - { key: "野外卓", icon: "🍽" }, - { key: "シェルター等", icon: "⛱" }, - { key: "草地広場", icon: "🌿" }, - { key: "多目的広場", icon: "🏟" }, - { key: "築山", icon: "⛰" }, - { key: "じゃぶじゃぶ池", icon: "💦" }, - { key: "防災用具", icon: "🛟" }, - { key: "遊具", icon: "🎠" }, - { key: "時計", icon: "⏰" }, - { key: "駐車場", icon: "🅿" }, - { key: "その他設備", icon: "🔧" }, - { key: "備考", icon: "" }, +// 一覧の施設・備考は、フキダシと同じ「アイコン+値」の行で表示する。 +const FACILITY_KEYS = [ + "ベンチ・椅子", "野外卓", "シェルター等", "草地広場", "多目的広場", + "築山", "じゃぶじゃぶ池", "防災用具", "遊具", "時計", "駐車場", + "その他設備", "備考", ]; +type FacilityRow = { icon: string; cls: string; value: string; banned?: boolean }; +const textOf = (value: unknown) => String(value ?? "").trim(); +const presenceText = (label: string, value: unknown) => { + const v = textOf(value); + if (!v) return ""; + return /^[○〇◯あり有1]+$/.test(v) ? `${label}あり` : v; +}; +const waterText = (value: string) => { + const text = textOf(value); + if (!text) return ""; + if (/[??]/.test(text)) return "水飲み場?"; + let total = 0; + for (const part of text.split(/[、,]/)) { + const nums = [...part.matchAll(/[x×](\d+)/g)].map((m) => Number(m[1])); + if (nums.length) total += nums.reduce((a, b) => a * b, 1); + } + return total > 0 ? `水飲み場x${total}` : "水飲み場"; +}; function disasterText(value: string): string { return value .split(/[、,]/) @@ -195,18 +187,82 @@ function disasterText(value: string): string { .filter((s) => /かまど|カマド|防災収納|レスキュー|防災器具|避難/.test(s)) .join("、"); } -function detailText(p: Park): string { - const parts: string[] = []; - for (const { key, icon } of DETAIL_FIELDS) { - const raw = p[key]; - if (!raw) continue; - const value = key === "防災用具" ? disasterText(raw) : raw; - if (!value) continue; - parts.push(icon ? `${icon} ${value}` : value); +function facilityRows(p: Park): FacilityRow[] { + const rows: FacilityRow[] = []; + // 遊具もフキダシと同じく「遊」の文字アイコンを付ける。 + if (textOf(p["遊具"])) { + rows.push({ icon: '<span class="fac-text">遊</span>', cls: "", value: textOf(p["遊具"]) }); + } + if (textOf(p["トイレ"])) { + rows.push({ + icon: '<span class="fac-text">WC</span>', + cls: "is-toilet", + value: textOf(p["トイレ"]), + }); + } + const water = textOf(p["水飲み場"]); + if (water) { + rows.push({ icon: PARK_ICONS.water.svg, cls: "is-water", value: waterText(water) }); + } + if (textOf(p["給水"])) { + rows.push({ icon: PARK_ICONS.water.svg, cls: "is-water", value: textOf(p["給水"]) }); } - const ball = p["ボール遊び"]; - if (ball && ball !== "禁止" && ball !== "可") parts.push(`⚽ ${ball}`); - return parts.join(" / "); + if (textOf(p["ベンチ・椅子"])) { + rows.push({ icon: PARK_ICONS.bench.svg, cls: "is-bench", value: textOf(p["ベンチ・椅子"]) }); + } + if (textOf(p["野外卓"])) { + rows.push({ icon: PARK_ICONS.picnic.svg, cls: "", value: textOf(p["野外卓"]) }); + } + if (textOf(p["シェルター等"])) { + rows.push({ icon: PARK_ICONS.shelter.svg, cls: "", value: textOf(p["シェルター等"]) }); + } + if (textOf(p["草地広場"])) { + rows.push({ icon: PARK_ICONS.grass.svg, cls: "is-grass", value: presenceText("草地広場", p["草地広場"]) }); + } + if (textOf(p["多目的広場"])) { + rows.push({ icon: '<span class="fac-text">広</span>', cls: "", value: presenceText("多目的広場", p["多目的広場"]) }); + } + if (textOf(p["築山"])) { + rows.push({ icon: PARK_ICONS.mountain.svg, cls: "", value: presenceText("築山", p["築山"]) }); + } + if (textOf(p["じゃぶじゃぶ池"])) { + rows.push({ icon: PARK_ICONS.pond.svg, cls: "is-pond", value: presenceText("じゃぶじゃぶ池", p["じゃぶじゃぶ池"]) }); + } + const ballText = textOf(p["ボール遊び"]); + const matchText = textOf(p["試合形式のボール遊び"]); + const ballBanned = /禁止/.test(ballText); + const ballValue = ballBanned + ? "ボール遊び禁止" + : /禁止/.test(matchText) + ? "試合形式禁止" + : ballText; + if (ballValue) { + rows.push({ icon: PARK_ICONS.ball.svg, cls: "is-ball", value: ballValue, banned: ballBanned }); + } + const pet = textOf(p["ペット"]); + if (pet) { + rows.push({ icon: PARK_ICONS.dog.svg, cls: "is-pet", value: pet, banned: /禁止/.test(pet) }); + } + if (textOf(p["時計"])) { + rows.push({ icon: PARK_ICONS.clock.svg, cls: "is-clock", value: presenceText("時計", p["時計"]) }); + } + const disaster = disasterText(textOf(p["防災用具"])); + if (disaster) { + rows.push({ icon: '<span class="fac-text">災</span>', cls: "", value: disaster }); + } + if (textOf(p["駐車場"])) { + rows.push({ icon: '<span class="fac-text">P</span>', cls: "", value: textOf(p["駐車場"]) }); + } + if (textOf(p["その他設備"])) { + rows.push({ icon: '<span class="fac-text">他</span>', cls: "", value: textOf(p["その他設備"]) }); + } + if (textOf(p["備考"])) { + rows.push({ icon: "", cls: "", value: textOf(p["備考"]) }); + } + if (p["南西部指定管理"]) { + rows.push({ icon: "", cls: "", value: "指定管理" }); + } + return rows; } // 面積(数値) @@ -261,21 +317,18 @@ function searchIndex(p: Park): string { p["築山"] ? "築山" : "", p["じゃぶじゃぶ池"] ? "じゃぶじゃぶ池" : "", p["ボール遊び"], - ...DETAIL_FIELDS.map((f) => p[f.key]), + ...FACILITY_KEYS.map((key) => p[key]), ] .join(" ") .toLowerCase(); } -// 名称・住所などが未確認の公園 +// 編集画面(park-editor)の確認チェックが「済」でないものを未確認とする。 +// 対象は市内公園(市内の公共施設・市外の公園は各タブで管理する)。 function isUnconfirmed(p: Park): boolean { if (p["施設"] === "1") return false; - return ( - (p["備考"] || "").includes("未確認") || - (p["住所"] || "").includes("未確認") || - (p["状態"] || "").includes("未確認") || - (p["公園名"] || "").includes("未確認") - ); + if (p["種別"] === "市外") return false; + return (p["確認"] || "") !== "済"; } --- @@ -301,7 +354,7 @@ function isUnconfirmed(p: Park): boolean { <th scope="col">住所</th> <th scope="col" aria-sort="none"> <button type="button" class="parks-sort" data-sort="area"> - 面積(㎡)<span class="parks-sort-mark" aria-hidden="true"></span> + 面積<span class="parks-sort-mark" aria-hidden="true"></span> </button> </th> <th scope="col">種別</th> @@ -327,7 +380,12 @@ function isUnconfirmed(p: Park): boolean { data-focus-park={park["公園名"]} data-focus-key={park["地図キー"] || ""} title="地図でこの公園の位置を表示" - >{park["公園名"]}</button> + > + {park["公園名"]} + <span class="park-focus-ico" aria-hidden="true"> + <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="6"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3"/></svg> + </span> + </button> ) : park["公式URL"] ? ( <a class="park-name-link" @@ -358,7 +416,7 @@ function isUnconfirmed(p: Park): boolean { target="_blank" rel="noopener" title={`Googleマップで「${park["公園名"]}」を開く`} - >{park["住所"] || EMPTY}</a> + ><span class="addr-google" set:html={PARK_ICONS.google.svg} />{park["住所"] || EMPTY}</a> </td> <td class="is-num" @@ -382,11 +440,22 @@ function isUnconfirmed(p: Park): boolean { )} </td> <td class="is-facility" data-label="施設・備考"> - {facilityBadges(park).map((b) => ( - <span class:list={["fac-badge", `fac-${b.kind}`]} title={b.title} aria-label={b.title}></span> - ))} - {detailText(park) ? <span class="fac-detail">{detailText(park)}</span> : null} - {facilityBadges(park).length === 0 && !detailText(park) ? EMPTY : null} + {facilityRows(park).length ? ( + <span class="fac-detail"> + {facilityRows(park).map((row) => ( + <span class="fac-detail-row"> + {row.icon ? ( + <span class:list={["fac-detail-ico", row.cls, row.banned ? "is-banned" : ""]} set:html={row.icon} /> + ) : ( + <span /> + )} + <span>{row.value}</span> + </span> + ))} + </span> + ) : ( + EMPTY + )} </td> </tr> ))} @@ -463,50 +532,113 @@ function isUnconfirmed(p: Park): boolean { border-radius: 0.3rem; } .parks table { + display: block; width: 100%; - min-width: 760px; - table-layout: fixed; + min-width: 0; + overflow: visible; border-collapse: collapse; font-size: 0.85rem; } - .parks col.col-name { - width: 24%; + .parks colgroup { + display: none; } - .parks col.col-address { - width: 18%; + /* 1行を「公園情報」「施設・備考」の2列で表示する(PC・スマホ共通) */ + .parks tbody, + .parks tbody tr, + .parks td { + display: block; } - .parks col.col-area { - width: 11%; + .parks tbody tr { + display: grid; + grid-template-columns: minmax(0, 0.48fr) minmax(0, 0.26fr) minmax(0, 1.26fr); + grid-template-areas: + "name name fac" + "addr addr fac" + "area type fac"; + gap: 0 0.6rem; + padding: 0.2rem 0.6rem; + border-bottom: 0; } - .parks col.col-type { - width: 7%; + .parks table tbody td { + border: 0; + margin: 0; + padding: 0; + text-align: left; + vertical-align: top; + white-space: normal; + overflow-wrap: anywhere; } - .parks col.col-facility { - width: 40%; + .parks .is-name { + grid-area: name; } - .parks th, - .parks td { - border-bottom: 1px solid var(--sl-color-hairline, #ddd); - border-right: 1px solid var(--sl-color-hairline, #ddd); - padding: 0.35rem 0.55rem; + .parks .is-address { + grid-area: addr; + min-width: 0; + } + .parks .is-num { + grid-area: area; text-align: left; - vertical-align: top; - white-space: nowrap; } - .parks th:last-child, - .parks td:last-child { - border-right: none; + .parks .is-type { + grid-area: type; + text-align: left; } - .parks thead th { + .parks .is-facility { + grid-area: fac; + min-width: 0; + max-width: none; + border-left: 1px solid var(--sl-color-hairline, #ddd); + padding-left: 0.65rem; + } + .parks thead { + display: block; position: sticky; top: 0; - z-index: 1; + z-index: 2; background: var(--sl-color-gray-6, #f2f2f2); + border-bottom: 1px solid var(--sl-color-hairline, #ddd); + } + .parks thead tr { + display: grid; + grid-template-columns: minmax(0, 0.48fr) minmax(0, 0.26fr) minmax(0, 1.26fr); + grid-template-areas: + "h-name h-name h-fac" + "h-area h-type h-fac"; + } + .parks thead th { + position: static; + border: 0; + background: transparent; + padding: 0.35rem 0.65rem; + text-align: left; + vertical-align: bottom; + white-space: normal; color: var(--sl-color-white, inherit); } + .parks thead th:nth-child(1) { + grid-area: h-name; + } + .parks thead th:nth-child(2) { + display: none; + } + .parks thead th:nth-child(3) { + grid-area: h-area; + } + .parks thead th:nth-child(4) { + grid-area: h-type; + } + .parks thead th:nth-child(5) { + grid-area: h-fac; + } .parks tbody tr:nth-child(even) td { + background: transparent; + } + .parks tbody tr:nth-child(even) { background: color-mix(in srgb, var(--sl-color-text, #333) 5%, transparent); } + .parks tbody tr.is-selected td:first-child { + box-shadow: none; + } .parks tr.is-hidden { display: none; } @@ -533,17 +665,22 @@ function isUnconfirmed(p: Park): boolean { font-weight: 600; white-space: normal; overflow-wrap: anywhere; - line-height: 1.5; + line-height: 1.3; + } + .parks .is-address, + .parks .is-num, + .parks .is-type { + line-height: 1.25; } .parks-owner { display: inline-block; - margin-left: 0.35rem; + margin: 0 0.35rem 0 0; padding: 0.02rem 0.35rem; border: 1px solid currentColor; border-radius: 999px; font-size: 0.7rem; font-weight: 600; - line-height: 1.5; + line-height: 1.2; vertical-align: 0.08em; white-space: nowrap; } @@ -561,7 +698,7 @@ function isUnconfirmed(p: Park): boolean { } .parks-status { display: inline-block; - margin-left: 0.35rem; + margin: 0 0.35rem 0 0; padding: 0.02rem 0.35rem; border: 1px solid #ef9a9a; border-radius: 999px; @@ -569,7 +706,7 @@ function isUnconfirmed(p: Park): boolean { color: #b71c1c; font-size: 0.7rem; font-weight: 700; - line-height: 1.5; + line-height: 1.2; vertical-align: 0.08em; white-space: nowrap; } @@ -579,7 +716,7 @@ function isUnconfirmed(p: Park): boolean { color: #424242; } .park-name-link { - display: inline-block; + display: block; max-width: 100%; padding: 0; border: 0; @@ -598,29 +735,56 @@ function isUnconfirmed(p: Park): boolean { .park-name-link:hover { color: var(--sl-color-accent); } - .parks tbody tr.is-highlighted td { + .park-focus-ico { + display: inline-flex; + width: 0.9em; + height: 0.9em; + margin-left: 0.2em; + color: #0277bd; + vertical-align: -0.08em; + } + .park-focus-ico svg { + display: block; + width: 100%; + height: 100%; + } + .parks tbody tr.is-highlighted { background: #fff59d !important; + outline: 2px solid #f9a825; + outline-offset: -2px; } - .parks tbody tr.is-selected td { + .parks tbody tr.is-highlighted td { + background: transparent !important; + } + .parks tbody tr.is-selected { background: #ffe0b2 !important; + outline: 2px solid #e65100; + outline-offset: -2px; } - .parks tbody tr.is-selected td:first-child { - box-shadow: inset 4px 0 0 #e65100; + .parks tbody tr.is-selected td { + background: transparent !important; } .parks .is-address { font-size: 0.8em; white-space: normal; - min-width: 9rem; } .parks .is-address a { color: var(--sl-color-text-accent, #357); } - .parks .is-address a::before { - content: "📍"; - margin-right: 0.2em; + .parks .addr-google { + display: inline-flex; + width: 1em; + height: 1em; + margin-right: 0.15em; + vertical-align: -0.12em; + } + .parks .addr-google :global(svg) { + display: block; + width: 100%; + height: 100%; } .parks .is-num { - text-align: right; + text-align: left; font-variant-numeric: tabular-nums; white-space: nowrap; } @@ -631,7 +795,7 @@ function isUnconfirmed(p: Park): boolean { padding: 0 0.45em; border-radius: 0.7em; font-size: 0.75em; - line-height: 1.7; + line-height: 1.2; vertical-align: middle; background: color-mix(in srgb, currentColor 14%, transparent); } @@ -687,111 +851,111 @@ function isUnconfirmed(p: Park): boolean { } .parks .is-facility { white-space: normal; - min-width: 12rem; - max-width: 26rem; } - .parks .fac-badge { + .parks .fac-detail { + display: grid; + gap: 0.1rem; + font-size: 0.8em; + opacity: 0.9; + } + .parks .fac-detail-row { + display: grid; + grid-template-columns: 18px minmax(0, 1fr); + gap: 0.35rem; + align-items: start; + } + .parks .fac-detail-ico { + position: relative; display: inline-flex; align-items: center; justify-content: center; - margin: 0 0.3em 0.2em 0; - padding: 0 0.4em; - border-radius: 1em; - font-size: 0.85em; - line-height: 1.9; - white-space: nowrap; - background: color-mix(in srgb, currentColor 10%, transparent); - cursor: help; + box-sizing: border-box; + width: 18px; + height: 18px; + border: 1px solid #c8e6c9; + border-radius: 4px; + background: #e8f5e9; + color: #2e7d32; + font-size: 11px; + font-weight: 700; + line-height: 1; } - .parks .fac-toilet::before { - content: "🚻"; + .parks .fac-detail-ico :global(svg) { + display: block; + width: 14px; + height: 14px; } - .parks .fac-water::before { - content: "💧"; + .parks .fac-detail-ico.is-toilet { + border-color: #263238; + background: #455a64; + color: #fff; + font-size: 8px; } - .parks .fac-toy::before { - content: "🎠"; + .parks .fac-detail-ico.is-water { + border-color: #90caf9; + background: #e3f2fd; + color: #0277bd; } - .parks .fac-ball::before { - content: "⚽"; + .parks .fac-detail-ico.is-bench { + border-color: #d7ccc8; + background: #efebe9; + color: #6d4c41; } - .parks .fac-cm::before { - content: "🏛"; + .parks .fac-detail-ico.is-pond { + border-color: #81d4fa; + background: #e1f5fe; + color: #29b6f6; } - .parks .fac-pergola::before { - content: "🌿"; + .parks .fac-detail-ico.is-clock { + border-color: #ffe082; + background: #fff8e1; + color: #f9a825; } - .parks .fac-azumaya::before { - content: "🛖"; + .parks .fac-detail-ico.is-ball { + color: #2e7d32; } - .parks .fac-detail { - display: block; - margin-top: 0.25em; - font-size: 0.8em; - opacity: 0.85; + .parks .fac-detail-ico.is-pet { + padding: 0; + border-color: #e0e0e0; + background: #fff; + color: #6d4c41; + } + .parks .fac-detail-ico.is-pet :global(svg) { + width: 100%; + height: 100%; + } + .parks .fac-detail-ico.is-banned { + overflow: hidden; + } + .parks .fac-detail-ico.is-banned::after { + content: ""; + position: absolute; + inset: 0; + background: + linear-gradient(45deg, transparent 47%, #d32f2f 47%, #d32f2f 53%, transparent 53%), + linear-gradient(-45deg, transparent 47%, #d32f2f 47%, #d32f2f 53%, transparent 53%); + } + .parks .fac-detail-ico :global(.fac-text) { + font-size: inherit; + font-weight: inherit; + line-height: inherit; + white-space: nowrap; } - /* スマートフォンでは表をカード状に組み替え、横スクロールなしで読めるようにする。 */ + /* スマートフォンではヘッダーを省き、余白を詰める(2列レイアウトは共通) */ @media (max-width: 700px) { .parks table { - display: block; - width: 100%; - min-width: 0; font-size: 0.8rem; } .parks thead { display: none; } - .parks tbody, - .parks tbody tr, - .parks td { - display: block; - } .parks tbody tr { - display: grid; - grid-template-columns: 1fr auto auto; - grid-template-areas: - "name name name" - "addr area type" - "fac fac fac"; - gap: 0.15rem 0.5rem; - padding: 0.45rem 0.55rem; - border-bottom: 1px solid var(--sl-color-hairline, #ddd); - } - .parks tbody tr:nth-child(even) td { - background: transparent; - } - .parks tbody tr:nth-child(even) { - background: color-mix(in srgb, var(--sl-color-text, #333) 5%, transparent); - } - .parks td { - border: 0; - padding: 0; - white-space: normal; - overflow-wrap: anywhere; - } - .parks .is-name { - grid-area: name; - } - .parks .is-address { - grid-area: addr; - min-width: 0; - } - .parks .is-num { - grid-area: area; - text-align: left; - } - .parks .is-type { - grid-area: type; - text-align: left; + gap: 0 0.45rem; + padding: 0.2rem 0.5rem; } .parks .is-facility { - grid-area: fac; - min-width: 0; - max-width: none; - } - .parks tbody tr.is-selected td:first-child { - box-shadow: none; + padding-left: 0.5rem; } } </style> @@ -868,8 +1032,13 @@ function isUnconfirmed(p: Park): boolean { if (count) count.textContent = `${shown} / ${total}件`; if (none) none.hidden = shown !== 0; // 地図側に絞り込み結果を通知する。 - const visibleNames = allRows() - .filter((row) => !row.classList.contains("is-hidden")) + const visibleRows = allRows().filter((row) => !row.classList.contains("is-hidden")); + const visibleNames = visibleRows + .map((row) => row.dataset.name || "") + .filter(Boolean); + // 市外の近い公園は名前が小平市内と重複することがあるため、別に渡す。 + const visibleNearbyNames = visibleRows + .filter((row) => row.dataset.town === "市外") .map((row) => row.dataset.name || "") .filter(Boolean); const fromMap = Boolean(selectedName && search?.value === selectedName); @@ -877,6 +1046,7 @@ function isUnconfirmed(p: Park): boolean { new CustomEvent("park:filter", { detail: { names: visibleNames, + nearbyNames: visibleNearbyNames, active: Boolean( kind || town || query || wanted.length || selectedName || onlyUnconfirmed, ), |
