Skip to content

Commit 946171b

Browse files
committed
Recursive navigation stack
1 parent 9cb1990 commit 946171b

File tree

2 files changed

+288
-49
lines changed

2 files changed

+288
-49
lines changed

typescript/packages/lookslike-gems/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
}
3434
},
3535
"dependencies": {
36+
"lit-element": "^4.0.6",
3637
"lit-html": "^3.1.4",
3738
"rxdb": "^15.24.0",
3839
"rxjs": "^7.8.1"

typescript/packages/lookslike-gems/src/main.ts

Lines changed: 287 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import { getRxStorageMemory } from "rxdb/plugins/storage-memory";
33
import { RxDBDevModePlugin } from "rxdb/plugins/dev-mode";
44
import { html, render } from "lit-html";
55
import { RxDBStatePlugin } from "rxdb/plugins/state";
6-
import { Observable } from "rxjs";
6+
import { Observable, Subscription } from "rxjs";
7+
import { LitElement, css } from "lit-element";
8+
import { customElement, property, state } from "lit-element/decorators.js";
79

810
addRxPlugin(RxDBStatePlugin);
911
// addRxPlugin(RxDBDevModePlugin);
@@ -41,14 +43,191 @@ async function createDatabase() {
4143
return db;
4244
}
4345

44-
// Create a data orb component
45-
function DataOrb(props: { id: string; value: any }) {
46-
return html`
47-
<div class="data-orb">
48-
<h3>${props.id}</h3>
49-
<p>${JSON.stringify(props.value)}</p>
50-
</div>
46+
@customElement("data-gem")
47+
class DataGem extends LitElement {
48+
@property({ type: String }) key!: string;
49+
@property({ type: String }) path!: string;
50+
51+
@state() private value: any;
52+
@state() private wobble: boolean = false;
53+
@state() private showTooltip: boolean = false;
54+
@state() private tooltipX: number = 0;
55+
@state() private tooltipY: number = 0;
56+
57+
static styles = css`
58+
:host {
59+
display: block;
60+
position: relative;
61+
aspect-ratio: 1 / 1;
62+
}
63+
.data-orb {
64+
background-color: rgba(0, 100, 200, 0.7);
65+
border-radius: 50%;
66+
padding: 20px;
67+
text-align: center;
68+
color: white;
69+
transition: transform 0.3s ease;
70+
width: 100%;
71+
height: 100%;
72+
display: flex;
73+
flex-direction: column;
74+
justify-content: center;
75+
align-items: center;
76+
box-sizing: border-box;
77+
}
78+
.data-orb.navigable {
79+
cursor: pointer;
80+
}
81+
.data-orb:hover {
82+
transform: scale(1.1);
83+
}
84+
.data-orb.animate {
85+
animation: wobble 0.3s ease-in-out;
86+
}
87+
@keyframes wobble {
88+
0% {
89+
transform: scale(1);
90+
}
91+
50% {
92+
transform: scale(1.1);
93+
}
94+
100% {
95+
transform: scale(1);
96+
}
97+
}
98+
.tooltip {
99+
position: fixed;
100+
display: block;
101+
background-color: rgba(0, 0, 0, 0.8);
102+
color: white;
103+
padding: 10px;
104+
border-radius: 5px;
105+
font-family: monospace;
106+
font-size: 12px;
107+
white-space: pre-wrap;
108+
z-index: 1000;
109+
max-width: 300px;
110+
pointer-events: none;
111+
text-align: left;
112+
}
113+
114+
.tooltip-content {
115+
margin: 0;
116+
padding: 0;
117+
}
118+
119+
.navigate {
120+
cursor: pointer;
121+
text-decoration: underline;
122+
color: blue;
123+
}
51124
`;
125+
subscription: Subscription | null = null;
126+
127+
private bindValue() {
128+
if (this.subscription) {
129+
this.subscription.unsubscribe();
130+
this.subscription = null;
131+
}
132+
133+
const value$ = appState.get$(this.path);
134+
this.subscription = value$.subscribe((newValue) => {
135+
const path = `${this.path}`;
136+
console.log("New value for", path, newValue);
137+
this.value = newValue;
138+
this.wobble = true;
139+
this.requestUpdate();
140+
setTimeout(() => {
141+
this.wobble = false;
142+
this.requestUpdate();
143+
}, 300);
144+
});
145+
}
146+
147+
override connectedCallback() {
148+
super.connectedCallback();
149+
this.bindValue();
150+
}
151+
152+
override updated(changedProperties: Map<string | number | symbol, unknown>) {
153+
if (changedProperties.has("path")) {
154+
this.bindValue();
155+
}
156+
}
157+
158+
override render() {
159+
return html`
160+
<div
161+
class="data-orb ${this.wobble ? "animate" : ""} ${this.isNavigable()
162+
? "navigable"
163+
: ""}"
164+
@mousemove="${this.handleMouseMove}"
165+
@mouseenter="${this.handleMouseEnter}"
166+
@mouseleave="${this.handleMouseLeave}"
167+
@click="${this.handleNavigate}"
168+
>
169+
<h3>${this.key}</h3>
170+
<p>${this.getShortValue()}</p>
171+
</div>
172+
${this.showTooltip ? this.renderTooltip() : ""}
173+
`;
174+
}
175+
176+
isNavigable() {
177+
return typeof this.value === "object" && this.value !== null;
178+
}
179+
180+
handleNavigate() {
181+
if (!this.isNavigable()) {
182+
return;
183+
}
184+
185+
this.dispatchEvent(
186+
new CustomEvent("navigate", {
187+
detail: { key: this.key, value: this.value },
188+
bubbles: true,
189+
composed: true,
190+
}),
191+
);
192+
}
193+
194+
renderTooltip() {
195+
return html`
196+
<div
197+
class="tooltip"
198+
style="left: ${this.tooltipX}px; top: ${this.tooltipY}px"
199+
>
200+
<div class="tooltip-content">${this.getPrettyPrintedValue()}</div>
201+
</div>
202+
`;
203+
}
204+
205+
handleMouseMove(e: MouseEvent) {
206+
this.tooltipX = e.clientX + 10; // Offset from cursor
207+
this.tooltipY = e.clientY + 10;
208+
this.requestUpdate();
209+
}
210+
211+
handleMouseEnter() {
212+
this.showTooltip = true;
213+
}
214+
215+
handleMouseLeave() {
216+
this.showTooltip = false;
217+
}
218+
219+
getShortValue(): string {
220+
if (typeof this.value === "object" && this.value !== null) {
221+
return Array.isArray(this.value)
222+
? `[${this.value.length} items]`
223+
: "{...}";
224+
}
225+
return String(this.value);
226+
}
227+
228+
getPrettyPrintedValue(): string {
229+
return JSON.stringify(this.value, null, 2).trim();
230+
}
52231
}
53232

54233
const initial = {
@@ -67,55 +246,114 @@ const initial = {
67246

68247
type Inventory = typeof initial;
69248

70-
// Main application
71-
async function main() {
72-
const db = await createDatabase();
73-
const state = await db.addState();
249+
type NavigationItem = {
250+
key: string;
251+
};
74252

75-
// Insert some initial data
76-
await state.set("inventory", (_) => initial);
77-
const inventory = state.get$("inventory") as Observable<Inventory | null>;
78-
79-
// Subscribe to changes
80-
inventory.subscribe((stateData) => {
81-
const app = html`
82-
<style>
83-
.inventory-grid {
84-
display: grid;
85-
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
86-
gap: 20px;
87-
padding: 20px;
88-
}
89-
.data-orb {
90-
background-color: rgba(0, 100, 200, 0.7);
91-
border-radius: 50%;
92-
padding: 20px;
93-
text-align: center;
94-
color: white;
95-
transition: transform 0.3s ease;
96-
}
97-
.data-orb:hover {
98-
transform: scale(1.1);
99-
}
100-
</style>
101-
<h1>Inventory Data Orbs</h1>
253+
@customElement("inventory-view")
254+
class InventoryView extends LitElement {
255+
@state() private navigationStack: NavigationItem[] = [];
256+
257+
static styles = css`
258+
.inventory-grid {
259+
display: grid;
260+
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
261+
gap: 20px;
262+
padding: 20px;
263+
}
264+
.breadcrumb {
265+
margin-bottom: 10px;
266+
}
267+
.breadcrumb-item {
268+
cursor: pointer;
269+
color: blue;
270+
text-decoration: underline;
271+
}
272+
`;
273+
274+
override connectedCallback() {
275+
super.connectedCallback();
276+
this.navigateTo({ key: "inventory" });
277+
}
278+
279+
navigateTo(item: NavigationItem) {
280+
this.navigationStack = [...this.navigationStack, item];
281+
}
282+
283+
navigateBack(index: number) {
284+
this.navigationStack = this.navigationStack.slice(0, index + 1);
285+
}
286+
287+
renderBreadcrumbs() {
288+
return html`
289+
<div class="breadcrumb">
290+
${this.navigationStack.map(
291+
(item, index) => html`
292+
<span
293+
class="breadcrumb-item"
294+
@click=${() => this.navigateBack(index)}
295+
>
296+
${item.key}
297+
</span>
298+
${index < this.navigationStack.length - 1 ? " > " : ""}
299+
`,
300+
)}
301+
</div>
302+
`;
303+
}
304+
305+
override render() {
306+
const currentItem = this.navigationStack[this.navigationStack.length - 1];
307+
const path = this.navigationStack.map((item) => item.key).join(".");
308+
const currentValue = appState.get(path);
309+
310+
return html`
311+
${this.renderBreadcrumbs()}
102312
<div class="inventory-grid">
103-
${!stateData
104-
? html`<p>Loading...</p>`
105-
: Object.entries(stateData).map(([key, value]) =>
106-
DataOrb({ id: key, value }),
107-
)}
313+
${Object.entries(currentValue).map(([key, value]) => {
314+
const fullPath = isNaN(key) ? `${path}.${key}` : `${path}[${key}]`;
315+
return html`
316+
<data-gem
317+
.key=${key}
318+
.path=${fullPath}
319+
@navigate=${(e: CustomEvent) => this.navigateTo(e.detail)}
320+
></data-gem>
321+
`;
322+
})}
108323
</div>
109324
`;
110-
render(app, document.body);
111-
});
325+
}
326+
}
327+
328+
// Main application
329+
async function main(state: any) {
330+
// Initial render
331+
render(html`<inventory-view></inventory-view>`, document.body);
112332

113333
// Example of updating state
114334
setInterval(() => {
115-
state.set("inventory.health", (v) => v - 10);
335+
state.set("inventory.health", (v) => Math.max(0, v - 10));
116336
}, 1000);
337+
338+
setInterval(() => {
339+
state.set("inventory.gold", (v) => v + 50);
340+
}, 2000);
341+
342+
setInterval(() => {
343+
state.set("inventory.skills.intelligence", (v) =>
344+
Math.round(Math.random() * 20),
345+
);
346+
}, 500);
117347
}
118348

119-
document.addEventListener("DOMContentLoaded", () => {
120-
main().catch(console.error);
349+
let appState = null;
350+
351+
document.addEventListener("DOMContentLoaded", async () => {
352+
const db = await createDatabase();
353+
appState = await db.addState();
354+
355+
// Insert some initial data
356+
await appState.set("inventory", (_) => initial);
357+
358+
main(appState).catch(console.error);
121359
});

0 commit comments

Comments
 (0)