aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/ParkMap.astro152
-rw-r--r--src/components/ParkMapHelp.astro15
-rw-r--r--src/components/ParksFilters.astro173
-rw-r--r--src/components/ParksTable.astro558
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:
'&copy; <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,
),