No al cierre de webs
ShareCode
Permalink: http://www.treeweb.es/u/974/ 01/02/2011

ShareCode

1 <!DOCTYPE html>2 <html lang="es" class="h-full">3 <head>4  <meta charset="utf-8" />5  <meta name="viewport" content="width=device-width, initial-scale=1" />6  <title>InceptionDB • UI (SPA)</title>7  <!-- TailwindCSS CDN -->8  <script src="https://cdn.tailwindcss.com"></script>9  <script>10  tailwind.config = {11  darkMode: 'class',12  theme: {13  extend: {14  colors: {15  primary: {16  50: '#eef2ff', 100: '#e0e7ff', 200: '#c7d2fe', 300: '#a5b4fc', 400: '#818cf8', 500: '#6366f1',17  600: '#4f46e5', 700: '#4338ca', 800: '#3730a3', 900: '#312e81'18  }19  }20  }21  }22  }23  </script>24  <!-- Vue 3 CDN -->25  <script src="https://unpkg.com/vue@3.5.12/dist/vue.global.prod.js"></script>26  <style>27  html, body, #app { height: 100%; }28  .scrollbar-thin::-webkit-scrollbar { height: 8px; width: 8px; }29  .scrollbar-thin::-webkit-scrollbar-thumb { background: #c7d2fe; border-radius: 9999px; }30  .scrollbar-thin::-webkit-scrollbar-track { background: transparent; }31  </style>32 </head>33 <body class="h-full bg-gray-50 text-gray-900 dark:bg-gray-950 dark:text-gray-100">34 <div id="app" class="h-full">35  <!-- Shell -->36  <div class="flex h-full">37  <!-- Sidebar -->38  <aside class="hidden md:flex md:w-72 flex-col gap-2 p-4 border-r border-gray-200 dark:border-gray-800 bg-white/70 backdrop-blur dark:bg-gray-900/50">39  <div class="flex items-center gap-2">40  <div class="h-9 w-9 rounded-2xl bg-primary-600 grid place-content-center text-white font-bold">IDB</div>41  <div>42  <div class="font-semibold">InceptionDB</div>43  <div class="text-xs text-gray-500">UI • SPA</div>44  </div>45  </div>46 47  <nav class="mt-4 text-sm">48  <p class="px-2 text-xs uppercase tracking-wider text-gray-500">Navegación</p>49  <ul class="mt-2 grid gap-1">50  <li><a :class="navClass('/')" href="#/">🏠 <span class="ml-2">Inicio</span></a></li>51  <li><a :class="navClass('/dbs')" href="#/dbs">🗄️ <span class="ml-2">Bases de datos</span></a></li>52  <li><a :class="navClass('/queries')" href="#/queries">🧪 <span class="ml-2">Consultas</span></a></li>53  <li><a :class="navClass('/release')" href="#/release">🏷️ <span class="ml-2">Release</span></a></li>54  <li><a :class="navClass('/docs')" href="#/docs">📖 <span class="ml-2">Docs</span></a></li>55  <li><button @click="toggleTheme" class="w-full text-left rounded-xl px-3 py-2 hover:bg-primary-50 dark:hover:bg-primary-900/40">🌓 <span class="ml-2">Tema</span></button></li>56  </ul>57  </nav>58 59  <div class="mt-auto text-xs text-gray-500">60  <div>Servidor por defecto:</div>61  <div class="truncate">{{ servers[0] }}</div>62  </div>63  </aside>64 65  <!-- Main -->66  <section class="flex-1 flex flex-col min-w-0">67  <!-- Topbar -->68  <header class="sticky top-0 z-10 bg-white/70 backdrop-blur border-b border-gray-200 dark:bg-gray-900/50 dark:border-gray-800">69  <div class="px-4 py-3 flex items-center gap-3">70  <button class="md:hidden rounded-lg px-3 py-2 border border-gray-200 dark:border-gray-700" @click="mobileOpen = !mobileOpen">☰</button>71  <h1 class="text-lg font-semibold">{{ pageTitle }}</h1>72  <div class="ml-auto flex items-center gap-2 text-xs">73  <span class="hidden sm:block text-gray-500">Entorno</span>74  <select v-model="activeServer" class="rounded-lg border-gray-300 dark:border-gray-700 bg-white/70 dark:bg-gray-800/70">75  <option v-for="s in servers" :key="s" :value="s">{{ s }}</option>76  </select>77  </div>78  </div>79  </header>80 81  <!-- Mobile drawer -->82  <transition name="fade">83  <div v-if="mobileOpen" class="md:hidden p-4 border-b border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900">84  <nav class="grid gap-1 text-sm">85  <a :class="navClass('/')" href="#/" @click="mobileOpen=false">🏠 Inicio</a>86  <a :class="navClass('/dbs')" href="#/dbs" @click="mobileOpen=false">🗄️ Bases de datos</a>87  <a :class="navClass('/queries')" href="#/queries" @click="mobileOpen=false">🧪 Consultas</a>88  <a :class="navClass('/release')" href="#/release" @click="mobileOpen=false">🏷️ Release</a>89  <a :class="navClass('/docs')" href="#/docs" @click="mobileOpen=false">📖 Docs</a>90  </nav>91  </div>92  </transition>93 94  <!-- Content -->95  <main class="flex-1 overflow-auto p-4 md:p-6">96  <component :is="currentView" :state="state" @goto="goto" />97  </main>98 99  <!-- Footer -->100  <footer class="border-t border-gray-200 dark:border-gray-800 px-4 py-3 text-xs text-gray-500">101  API: OpenAPI 3.1 • Datos fake, wiring real más adelante.102  </footer>103  </section>104  </div>105 </div>106 107 <script>108 const Dashboard = {109  props: ['state'],110  template: `111  <div class="grid gap-6">112  <div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-4">113  <StatCard title="Bases de datos" :value="state.dbs.length" subtitle="/v1/databases" />114  <StatCard title="Colecciones (demo)" :value="totalCollections" subtitle="Colecciones simuladas" />115  <StatCard title="API Keys (demo)" :value="totalApiKeys" subtitle="Por base de datos" />116  <StatCard title="Owners (demo)" :value="totalOwners" subtitle="Acumulado" />117  </div>118  <div class="grid lg:grid-cols-2 gap-6">119  <Panel title="Bases de datos recientes">120  <table class="w-full text-sm">121  <thead class="text-left text-gray-500">122  <tr><th class="py-2">Nombre</th><th>Id</th><th class="text-right">Owners</th></tr>123  </thead>124  <tbody>125  <tr v-for="db in state.dbs.slice(0,6)" :key="db.id" class="border-t border-gray-200 dark:border-gray-800">126  <td class="py-2"><a class="text-primary-600 hover:underline" :href="'#/db/'+db.id">{{ db.name }}</a></td>127  <td class="truncate max-w-[12rem] text-xs">{{ db.id }}</td>128  <td class="text-right">{{ db.owners_length }}</td>129  </tr>130  </tbody>131  </table>132  </Panel>133  <Panel title="Servidores">134  <ul class="text-sm list-disc pl-5">135  <li v-for="s in state.servers" :key="s">{{ s }}</li>136  </ul>137  </Panel>138  </div>139  </div>140  `,141  computed: {142  totalCollections(){143  return this.state.collectionsDemo.reduce((a, c) => a + c.items.length, 0)144  },145  totalApiKeys(){146  return this.state.dbDetails.reduce((a, d) => a + d.api_keys.length, 0)147  },148  totalOwners(){149  return this.state.dbDetails.reduce((a, d) => a + d.owners.length, 0)150  },151  }152 }153 154 const Databases = {155  props: ['state'],156  emits: ['goto'],157  data(){ return { q: '' } },158  computed: {159  filtered(){160  const q = this.q.toLowerCase();161  return this.state.dbs.filter(d => d.name.toLowerCase().includes(q) || d.id.includes(this.q));162  }163  },164  template: `165  <div class="grid gap-4">166  <div class="flex flex-wrap gap-2 items-center">167  <input v-model="q" placeholder="Buscar..." class="px-3 py-2 rounded-xl border border-gray-300 dark:border-gray-700 bg-white/70 dark:bg-gray-800/70"/>168  <button class="px-3 py-2 rounded-xl bg-primary-600 text-white" @click="$emit('goto','/db/new')">+ Crear DB</button>169  </div>170  <div class="grid gap-3">171  <div v-for="db in filtered" :key="db.id" class="rounded-2xl border border-gray-200 dark:border-gray-800 p-4 bg-white/70 dark:bg-gray-900/50">172  <div class="flex gap-3 items-start">173  <div class="h-10 w-10 rounded-xl bg-primary-600/10 grid place-content-center">🗄️</div>174  <div class="min-w-0 flex-1">175  <div class="flex items-center gap-2">176  <a class="font-semibold hover:underline" :href="'#/db/'+db.id">{{ db.name }}</a>177  <span class="text-xs text-gray-500">{{ db.id }}</span>178  </div>179  <div class="text-sm text-gray-500">Owners: {{ db.owners_length }}</div>180  </div>181  <div class="flex gap-2">182  <a :href="'#/db/'+db.id" class="px-3 py-1.5 text-sm rounded-lg border border-gray-300 dark:border-gray-700">Abrir</a>183  <button disabled class="px-3 py-1.5 text-sm rounded-lg bg-red-600/10 text-red-700 dark:text-red-300">Eliminar</button>184  </div>185  </div>186  </div>187  </div>188  </div>189  `190 }191 192 const DatabaseDetail = {193  props: ['state'],194  computed:{195  db(){ return this.state.dbDetails.find(d=>d.id===this.state.route.params.id) },196  collections(){ return this.state.collectionsDemo.find(c=>c.dbId===this.state.route.params.id)?.items || [] },197  },198  template: `199  <div v-if="db" class="grid gap-6">200  <div class="flex flex-wrap items-center gap-3">201  <h2 class="text-xl font-semibold">{{ db.name }}</h2>202  <span class="text-xs text-gray-500">{{ db.id }}</span>203  <span class="ml-auto text-xs text-gray-500">Creada: {{ db.creation_date }}</span>204  </div>205 206  <div class="grid md:grid-cols-3 gap-4">207  <Panel title="Resumen">208  <ul class="text-sm grid gap-1">209  <li>Owners: <b>{{ db.owners.length }}</b></li>210  <li>API Keys: <b>{{ db.api_keys.length }}</b></li>211  <li>Colecciones (demo): <b>{{ collections.length }}</b></li>212  </ul>213  </Panel>214  <Panel title="Owners">215  <ul class="text-sm list-disc pl-5">216  <li v-for="o in db.owners" :key="o">{{ o }}</li>217  </ul>218  </Panel>219  <Panel title="API Keys">220  <div class="grid gap-2 text-sm">221  <div v-for="k in db.api_keys" :key="k.key" class="rounded-lg border border-gray-200 dark:border-gray-800 p-2">222  <div class="font-mono text-xs truncate">{{ k.key }}</div>223  <div class="text-xs text-gray-500 flex justify-between"><span>{{ k.name }}</span><span>{{ k.creation_date }}</span></div>224  </div>225  <button disabled class="mt-1 px-3 py-2 rounded-xl bg-primary-600 text-white">+ Crear API Key</button>226  </div>227  </Panel>228  </div>229 230  <Panel title="Colecciones">231  <div class="grid gap-2">232  <table class="w-full text-sm">233  <thead class="text-left text-gray-500">234  <tr><th class="py-2">Nombre</th><th>Total</th><th>Índices</th><th>Acciones</th></tr>235  </thead>236  <tbody>237  <tr v-for="c in collections" :key="c.name" class="border-t border-gray-200 dark:border-gray-800">238  <td class="py-2">{{ c.name }}</td>239  <td>{{ c.total }}</td>240  <td>{{ c.indexes }}</td>241  <td>242  <a :href="'#/db/'+db.id+'/col/'+c.name" class="px-2 py-1 rounded-lg border text-xs">Abrir</a>243  </td>244  </tr>245  </tbody>246  </table>247  <div class="pt-2">248  <button disabled class="px-3 py-2 rounded-xl bg-primary-600 text-white">+ Crear colección</button>249  </div>250  </div>251  </Panel>252  </div>253  <div v-else class="text-sm text-gray-500">DB no encontrada</div>254  `255 }256 257 const CollectionDetail = {258  props: ['state'],259  computed: {260  db(){ return this.state.dbDetails.find(d=>d.id===this.state.route.params.id) },261  col(){262  const list = this.state.collectionsDemo.find(c=>c.dbId===this.state.route.params.id)?.items || []263  return list.find(x=>x.name===this.state.route.params.col)264  },265  indexes(){266  const key = `${this.state.route.params.id}/${this.state.route.params.col}`267  return this.state.indexesDemo[key] || []268  }269  },270  template: `271  <div v-if="db && col" class="grid gap-6">272  <div class="flex flex-wrap items-center gap-3">273  <h2 class="text-xl font-semibold">{{ db.name }} / {{ col.name }}</h2>274  <span class="ml-auto text-xs text-gray-500">Docs: {{ col.total }}</span>275  </div>276 277  <div class="grid md:grid-cols-3 gap-4">278  <Panel title="Defaults"><pre class="text-xs">{{ JSON.stringify(col.defaults || {}, null, 2) }}</pre></Panel>279  <Panel title="Índices">280  <ul class="text-sm grid gap-1">281  <li v-for="i in indexes" :key="i.name" class="flex justify-between">282  <span class="font-mono">{{ i.name }}</span>283  <span class="text-xs text-gray-500">{{ i.type }}</span>284  </li>285  </ul>286  </Panel>287  <Panel title="Acciones">288  <div class="grid gap-2 text-sm">289  <button disabled class="px-3 py-2 rounded-xl border">Crear índice</button>290  <button disabled class="px-3 py-2 rounded-xl border">Drop índice</button>291  <button disabled class="px-3 py-2 rounded-xl border">Insertar documento</button>292  </div>293  </Panel>294  </div>295 296  <Panel title="Explorar (demo)">297  <div class="text-sm text-gray-500">Aquí irá /find con filtros. Por ahora mostramos datos fake.</div>298  <div class="mt-3 grid gap-2">299  <div v-for="n in 5" :key="n" class="rounded-xl border border-gray-200 dark:border-gray-800 p-3 bg-white/70 dark:bg-gray-900/50">300  <div class="text-xs text-gray-500">doc_{{n}}</div>301  <pre class="text-xs overflow-auto">{{ JSON.stringify({ _id: 'doc_'+n, value: Math.floor(Math.random()*1000), updated_at: new Date().toISOString() }, null, 2) }}</pre>302  </div>303  </div>304  </Panel>305  </div>306  <div v-else class="text-sm text-gray-500">Colección no encontrada</div>307  `308 }309 310 const Queries = {311  props: ['state'],312  data(){ return { mode: 'find', payload: '{"limit": 10}', response: null } },313  template: `314  <div class="grid gap-4">315  <div class="grid md:grid-cols-4 gap-3">316  <Panel title="Contexto">317  <div class="grid gap-2 text-sm">318  <label class="text-xs text-gray-500">Base de datos</label>319  <select class="rounded-xl border-gray-300 dark:border-gray-700 bg-white/70 dark:bg-gray-800/70">320  <option v-for="d in state.dbs" :key="d.id">{{ d.name }}</option>321  </select>322  <label class="text-xs text-gray-500">Colección</label>323  <input placeholder="orders" class="rounded-xl border-gray-300 dark:border-gray-700 bg-white/70 dark:bg-gray-800/70"/>324  </div>325  </Panel>326  <Panel class="md:col-span-3" title="Constructor (demo)">327  <div class="flex gap-2 text-sm">328  <select v-model="mode" class="rounded-xl border-gray-300 dark:border-gray-700 bg-white/70 dark:bg-gray-800/70">329  <option value="find">/find</option>330  <option value="insert">/insert</option>331  <option value="remove">/remove</option>332  <option value="patch">/patch</option>333  <option value="size">/size</option>334  </select>335  <button disabled class="px-3 py-2 rounded-xl bg-primary-600 text-white">Ejecutar</button>336  </div>337  <div class="mt-3 grid gap-2">338  <label class="text-xs text-gray-500">Payload JSON</label>339  <textarea v-model="payload" rows="8" class="font-mono text-xs rounded-xl border-gray-300 dark:border-gray-700 bg-white/70 dark:bg-gray-800/70"></textarea>340  <label class="text-xs text-gray-500">Respuesta</label>341  <pre class="text-xs rounded-xl border border-gray-200 dark:border-gray-800 p-3 bg-white/70 dark:bg-gray-900/50">{{ response || '{ /* respuesta fake */ }' }}</pre>342  </div>343  </Panel>344  </div>345  </div>346  `347 }348 349 const Release = { props:['state'], template:`350  <Panel title="Release">351  <div class="text-sm">Endpoint <code class='font-mono'>/release</code> — aquí mostraremos la versión real más adelante.</div>352  <div class="mt-4 rounded-xl border border-gray-200 dark:border-gray-800 p-3 bg-white/70 dark:bg-gray-900/50">353  <div class="text-xs text-gray-500">Versión (fake)</div>354  <div class="font-mono">"1.2.3-demo"</div>355  </div>356  </Panel>357 `}358 359 const Docs = { props:['state'], template:`360  <div class="grid gap-4">361  <Panel title="Documentación">362  <ul class="list-disc pl-5 text-sm">363  <li>OpenAPI 3.1</li>364  <li><code class='font-mono'>/doc</code> (documentación integrada)</li>365  <li>Servidores:366  <ul class="list-disc pl-5">367  <li v-for="s in state.servers" :key="s">{{ s }}</li>368  </ul>369  </li>370  </ul>371  </Panel>372  <Panel title="Siguientes pasos">373  <ol class="list-decimal pl-5 text-sm grid gap-1">374  <li>Conectar acciones a <code class='font-mono'>fetch</code> con la cabecera <code class='font-mono'>X-Api-Key</code> (cuando aplique).</li>375  <li>Gestionar estados de carga/errores.</li>376  <li>Paginar listados grandes.</li>377  <li>Añadir formularios para crear DB/colecciones, índices, etc.</li>378  </ol>379  </Panel>380  </div>381 `}382 383 // UI atoms384 const Panel = {385  props: ['title'],386  template: `387  <div class="rounded-2xl border border-gray-200 dark:border-gray-800 p-4 bg-white/70 dark:bg-gray-900/50">388  <div class="font-semibold mb-2">{{ title }}</div>389  <div><slot /></div>390  </div>391  `392 }393 394 const StatCard = {395  props: ['title','value','subtitle'],396  template: `397  <div class="rounded-2xl border border-gray-200 dark:border-gray-800 p-4 bg-white/70 dark:bg-gray-900/50">398  <div class="text-sm text-gray-500">{{ title }}</div>399  <div class="text-2xl font-semibold">{{ value }}</div>400  <div class="text-xs text-gray-500 mt-1">{{ subtitle }}</div>401  </div>402  `403 }404 405 // Minimal hash router406 function parseHash(){407  const h = location.hash.slice(1) || '/'408  const parts = h.split('/').filter(Boolean)409  const match = (arr)=>arr.every((a,i)=>a===parts[i])410  if (h==='/' || parts.length===0) return { view:'Dashboard', params:{} }411  if (match(['dbs'])) return { view:'Databases', params:{} }412  if (match(['queries'])) return { view:'Queries', params:{} }413  if (match(['release'])) return { view:'Release', params:{} }414  if (match(['docs'])) return { view:'Docs', params:{} }415  if (parts[0]==='db' && parts[1]){416  if (parts[2]==='col' && parts[3]) return { view:'CollectionDetail', params:{ id: parts[1], col: parts[3] } }417  return { view:'DatabaseDetail', params:{ id: parts[1] } }418  }419  return { view:'Dashboard', params:{} }420 }421 422 const App = {423  components: { Panel, StatCard, Dashboard, Databases, DatabaseDetail, CollectionDetail, Queries, Release, Docs },424  data(){425  return {426  servers: [427  'https://inceptiondb.hola.cloud',428  'http://inceptiondb.hola.cloud'429  ],430  activeServer: 'https://inceptiondb.hola.cloud',431  mobileOpen: false,432  state: {433  servers: [434  'https://inceptiondb.hola.cloud',435  'http://inceptiondb.hola.cloud'436  ],437  route: parseHash(),438  // Fake data según el esquema OpenAPI439  dbs: [440  { id: 'db_9f7a12', name: 'production', owners_length: 3 },441  { id: 'db_a13c55', name: 'staging', owners_length: 2 },442  { id: 'db_55aa01', name: 'analytics', owners_length: 1 },443  { id: 'db_f00baa', name: 'playground', owners_length: 1 },444  { id: 'db_999999', name: 'demo', owners_length: 2 },445  { id: 'db_beef00', name: 'events', owners_length: 2 },446  ],447  dbDetails: [448  {449  id: 'db_9f7a12', name: 'production', creation_date: '2025-09-01T10:00:00Z',450  api_keys: [451  { key: 'prod_pk_01', name: 'backend', creation_date: '2025-08-01T10:00:00Z' },452  { key: 'prod_pk_02', name: 'etl', creation_date: '2025-08-15T10:00:00Z' },453  ],454  owners: ['alice@hola.cloud','bob@hola.cloud','carol@hola.cloud']455  },456  {457  id: 'db_a13c55', name: 'staging', creation_date: '2025-08-21T09:00:00Z',458  api_keys: [ { key: 'stg_pk_01', name: 'web', creation_date: '2025-08-21T09:00:00Z' } ],459  owners: ['alice@hola.cloud','qa@hola.cloud']460  },461  { id: 'db_55aa01', name: 'analytics', creation_date: '2025-08-05T12:00:00Z', api_keys: [], owners: ['data@hola.cloud'] },462  { id: 'db_f00baa', name: 'playground', creation_date: '2025-07-10T08:00:00Z', api_keys: [], owners: ['dev@hola.cloud'] },463  { id: 'db_999999', name: 'demo', creation_date: '2025-06-01T08:00:00Z', api_keys: [], owners: ['gerardo@hola.cloud','demo@hola.cloud'] },464  { id: 'db_beef00', name: 'events', creation_date: '2025-08-30T08:00:00Z', api_keys: [], owners: ['stream@hola.cloud','ops@hola.cloud'] },465  ],466  collectionsDemo: [467  { dbId: 'db_9f7a12', items: [468  { name: 'users', total: 120034, indexes: 3, defaults: { plan: 'free' } },469  { name: 'orders', total: 41234, indexes: 4, defaults: { status: 'new' } },470  { name: 'invoices', total: 876, indexes: 2, defaults: {} },471  ]},472  { dbId: 'db_a13c55', items: [473  { name: 'users', total: 4521, indexes: 3, defaults: {} },474  { name: 'orders', total: 1280, indexes: 2, defaults: {} },475  ]},476  { dbId: 'db_55aa01', items: [477  { name: 'events', total: 2500000, indexes: 2, defaults: {} },478  ]},479  { dbId: 'db_f00baa', items: [ { name: 'play', total: 42, indexes: 1, defaults: {} } ] },480  { dbId: 'db_999999', items: [ { name: 'samples', total: 12, indexes: 1, defaults: {} } ] },481  { dbId: 'db_beef00', items: [ { name: 'kafka', total: 999999, indexes: 2, defaults: {} } ] },482  ],483  indexesDemo: {484  'db_9f7a12/users': [ { name: 'pk_id', type: 'hash' }, { name: 'email_idx', type: 'btree' } ],485  'db_9f7a12/orders': [ { name: 'pk_id', type: 'hash' }, { name: 'created_at', type: 'btree' }, { name: 'user_id', type: 'btree' } ],486  'db_a13c55/users': [ { name: 'pk_id', type: 'hash' } ],487  'db_55aa01/events': [ { name: 'pk_id', type: 'hash' }, { name: 'ts', type: 'btree' } ],488  },489  }490  }491  },492  computed: {493  pageTitle(){494  const r = this.state.route495  if (r.view==='Dashboard') return 'Inicio'496  if (r.view==='Databases') return 'Bases de datos'497  if (r.view==='Queries') return 'Consultas'498  if (r.view==='Release') return 'Release'499  if (r.view==='Docs') return 'Documentación'500  if (r.view==='DatabaseDetail') return `Base de datos: ${r.params.id}`501  if (r.view==='CollectionDetail') return `Colección: ${r.params.col}`502  return 'InceptionDB'503  }504  },505  mounted(){506  window.addEventListener('hashchange', ()=>{ this.state.route = parseHash() })507  // Dark mode, respeta preferencia previa508  if (localStorage.getItem('theme')==='dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {509  document.documentElement.classList.add('dark')510  }511  },512  methods: {513  goto(path){ location.hash = '#' + path },514  navClass(path){515  const active = location.hash.startsWith('#'+path)516  return [517  'flex items-center rounded-xl px-3 py-2 hover:bg-primary-50 dark:hover:bg-primary-900/40',518  active ? 'bg-primary-50 dark:bg-primary-900/40 text-primary-700 dark:text-primary-200' : 'text-gray-700 dark:text-gray-200'519  ]520  },521  toggleTheme(){522  const isDark = document.documentElement.classList.toggle('dark')523  localStorage.setItem('theme', isDark ? 'dark' : 'light')524  }525  }526 }527 528 Vue.createApp(App).mount('#app')529 </script>530 </body>531 </html>532 
Enlace
El enlace para compartir es: