@@ -3,7 +3,9 @@ import { getRxStorageMemory } from "rxdb/plugins/storage-memory";
33import { RxDBDevModePlugin } from "rxdb/plugins/dev-mode" ;
44import { html , render } from "lit-html" ;
55import { 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
810addRxPlugin ( 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
54233const initial = {
@@ -67,55 +246,114 @@ const initial = {
67246
68247type 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