rspace-online/lib/folk-destination.ts

281 lines
6.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { FolkShape } from "./folk-shape";
import { css, html } from "./tags";
const styles = css`
:host {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
min-width: 260px;
min-height: 180px;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background: #059669;
color: white;
border-radius: 8px 8px 0 0;
font-size: 12px;
font-weight: 600;
cursor: move;
}
.header-title {
display: flex;
align-items: center;
gap: 6px;
}
.header-actions button {
background: transparent;
border: none;
color: white;
cursor: pointer;
padding: 2px 6px;
border-radius: 4px;
font-size: 14px;
}
.header-actions button:hover {
background: rgba(255, 255, 255, 0.2);
}
.dest-body {
padding: 12px;
}
.dest-name {
font-size: 16px;
font-weight: 700;
color: #1e293b;
margin-bottom: 2px;
}
.dest-country {
font-size: 12px;
color: #64748b;
margin-bottom: 8px;
}
.dest-dates {
display: flex;
gap: 12px;
font-size: 11px;
color: #475569;
margin-bottom: 10px;
padding: 6px 8px;
background: #f1f5f9;
border-radius: 4px;
}
.dest-dates span {
display: flex;
align-items: center;
gap: 4px;
}
.dest-dates .label {
color: #94a3b8;
font-weight: 500;
}
.dest-notes {
font-size: 12px;
color: #475569;
line-height: 1.5;
padding: 8px;
background: #fafaf9;
border-radius: 4px;
border: 1px solid #e2e8f0;
min-height: 40px;
white-space: pre-wrap;
}
.dest-notes[contenteditable]:focus {
outline: none;
border-color: #059669;
}
.coords {
font-size: 10px;
color: #94a3b8;
margin-top: 6px;
}
.empty-name {
color: #94a3b8;
font-style: italic;
}
`;
declare global {
interface HTMLElementTagNameMap {
"folk-destination": FolkDestination;
}
}
export class FolkDestination extends FolkShape {
static override tagName = "folk-destination";
static {
const sheet = new CSSStyleSheet();
const parentRules = Array.from(FolkShape.styles.cssRules)
.map((r) => r.cssText)
.join("\n");
const childRules = Array.from(styles.cssRules)
.map((r) => r.cssText)
.join("\n");
sheet.replaceSync(`${parentRules}\n${childRules}`);
this.styles = sheet;
}
#destName = "";
#country = "";
#lat: number | null = null;
#lng: number | null = null;
#arrivalDate = "";
#departureDate = "";
#notes = "";
#nameEl: HTMLElement | null = null;
#countryEl: HTMLElement | null = null;
#notesEl: HTMLElement | null = null;
#datesEl: HTMLElement | null = null;
get destName() { return this.#destName; }
set destName(v: string) {
this.#destName = v;
if (this.#nameEl) this.#nameEl.textContent = v || "Unnamed destination";
this.dispatchEvent(new CustomEvent("content-change"));
}
get country() { return this.#country; }
set country(v: string) {
this.#country = v;
if (this.#countryEl) this.#countryEl.textContent = v;
this.dispatchEvent(new CustomEvent("content-change"));
}
get lat() { return this.#lat; }
set lat(v: number | null) { this.#lat = v; }
get lng() { return this.#lng; }
set lng(v: number | null) { this.#lng = v; }
get arrivalDate() { return this.#arrivalDate; }
set arrivalDate(v: string) {
this.#arrivalDate = v;
this.#renderDates();
this.dispatchEvent(new CustomEvent("content-change"));
}
get departureDate() { return this.#departureDate; }
set departureDate(v: string) {
this.#departureDate = v;
this.#renderDates();
this.dispatchEvent(new CustomEvent("content-change"));
}
get notes() { return this.#notes; }
set notes(v: string) {
this.#notes = v;
if (this.#notesEl && this.#notesEl.textContent !== v) {
this.#notesEl.textContent = v;
}
this.dispatchEvent(new CustomEvent("content-change"));
}
override createRenderRoot() {
const root = super.createRenderRoot();
const wrapper = document.createElement("div");
wrapper.innerHTML = html`
<div class="header">
<span class="header-title">
<span>📍</span>
<span>Destination</span>
</span>
<div class="header-actions">
<button class="close-btn" title="Close">×</button>
</div>
</div>
<div class="dest-body">
<div class="dest-name"></div>
<div class="dest-country"></div>
<div class="dest-dates"></div>
<div class="dest-notes" contenteditable="true"></div>
</div>
`;
const slot = root.querySelector("slot");
const containerDiv = slot?.parentElement as HTMLElement;
if (containerDiv) {
containerDiv.replaceWith(wrapper);
}
this.#nameEl = wrapper.querySelector(".dest-name");
this.#countryEl = wrapper.querySelector(".dest-country");
this.#datesEl = wrapper.querySelector(".dest-dates");
this.#notesEl = wrapper.querySelector(".dest-notes");
if (this.#nameEl) {
this.#nameEl.textContent = this.#destName || "Unnamed destination";
if (!this.#destName) this.#nameEl.classList.add("empty-name");
}
if (this.#countryEl) this.#countryEl.textContent = this.#country;
const closeBtn = wrapper.querySelector(".close-btn") as HTMLButtonElement;
closeBtn.addEventListener("click", (e) => {
e.stopPropagation();
this.dispatchEvent(new CustomEvent("close"));
});
if (this.#notesEl) {
this.#notesEl.textContent = this.#notes;
this.#notesEl.addEventListener("input", () => {
this.#notes = this.#notesEl!.textContent || "";
this.dispatchEvent(new CustomEvent("content-change"));
});
this.#notesEl.addEventListener("click", (e) => e.stopPropagation());
}
this.#renderDates();
return root;
}
#renderDates() {
if (!this.#datesEl) return;
if (!this.#arrivalDate && !this.#departureDate) {
this.#datesEl.style.display = "none";
return;
}
this.#datesEl.style.display = "flex";
let result = "";
if (this.#arrivalDate) {
const d = new Date(this.#arrivalDate);
result += `<span><span class="label">Arrive:</span> ${d.toLocaleDateString("en-US", { month: "short", day: "numeric" })}</span>`;
}
if (this.#departureDate) {
const d = new Date(this.#departureDate);
result += `<span><span class="label">Depart:</span> ${d.toLocaleDateString("en-US", { month: "short", day: "numeric" })}</span>`;
}
this.#datesEl.innerHTML = result;
}
override toJSON() {
return {
...super.toJSON(),
type: "folk-destination",
destName: this.#destName,
country: this.#country,
lat: this.#lat,
lng: this.#lng,
arrivalDate: this.#arrivalDate,
departureDate: this.#departureDate,
notes: this.#notes,
};
}
}