built basic ui structure in /hierarchy

This commit is contained in:
2026-02-14 18:03:13 -06:00
parent d645bda942
commit e4e0775472
24 changed files with 1174 additions and 106 deletions
+47
View File
@@ -5,6 +5,7 @@
"": {
"name": "river_connect_assets",
"dependencies": {
"@kobalte/core": "^0.13.11",
"phoenix": "^1.8.3",
"phoenix_html": "^4.1.1",
"phoenix_live_view": "^1.1.0",
@@ -74,6 +75,8 @@
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
"@corvu/utils": ["@corvu/utils@0.4.2", "", { "dependencies": { "@floating-ui/dom": "^1.6.11" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-Ox2kYyxy7NoXdKWdHeDEjZxClwzO4SKM8plAaVwmAJPxHMqA0rLOoAsa+hBDwRLpctf+ZRnAd/ykguuJidnaTA=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
@@ -126,6 +129,16 @@
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
"@floating-ui/core": ["@floating-ui/core@1.7.4", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg=="],
"@floating-ui/dom": ["@floating-ui/dom@1.7.5", "", { "dependencies": { "@floating-ui/core": "^1.7.4", "@floating-ui/utils": "^0.2.10" } }, "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg=="],
"@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
"@internationalized/date": ["@internationalized/date@3.11.0", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-BOx5huLAWhicM9/ZFs84CzP+V3gBW6vlpM02yzsdYC7TGlZJX1OJiEEHcSayF00Z+3jLlm4w79amvSt6RqKN3Q=="],
"@internationalized/number": ["@internationalized/number@3.6.5", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
@@ -136,6 +149,34 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@kobalte/core": ["@kobalte/core@0.13.11", "", { "dependencies": { "@floating-ui/dom": "^1.5.1", "@internationalized/date": "^3.4.0", "@internationalized/number": "^3.2.1", "@kobalte/utils": "^0.9.1", "@solid-primitives/props": "^3.1.8", "@solid-primitives/resize-observer": "^2.0.26", "solid-presence": "^0.1.8", "solid-prevent-scroll": "^0.1.4" }, "peerDependencies": { "solid-js": "^1.8.15" } }, "sha512-hK7TYpdib/XDb/r/4XDBFaO9O+3ZHz4ZWryV4/3BfES+tSQVgg2IJupDnztKXB0BqbSRy/aWlHKw1SPtNPYCFQ=="],
"@kobalte/utils": ["@kobalte/utils@0.9.1", "", { "dependencies": { "@solid-primitives/event-listener": "^2.2.14", "@solid-primitives/keyed": "^1.2.0", "@solid-primitives/map": "^0.4.7", "@solid-primitives/media": "^2.2.4", "@solid-primitives/props": "^3.1.8", "@solid-primitives/refs": "^1.0.5", "@solid-primitives/utils": "^6.2.1" }, "peerDependencies": { "solid-js": "^1.8.8" } }, "sha512-eeU60A3kprIiBDAfv9gUJX1tXGLuZiKMajUfSQURAF2pk4ZoMYiqIzmrMBvzcxP39xnYttgTyQEVLwiTZnrV4w=="],
"@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.3", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-h4VqkYFv6Gf+L7SQj+Y6puigL/5DIi7x5q07VZET7AWcS+9/G3WfIE9WheniHWJs51OEkRB43w6lDys5YeFceg=="],
"@solid-primitives/keyed": ["@solid-primitives/keyed@1.5.3", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zNadtyYBhJSOjXtogkGHmRxjGdz9KHc8sGGVAGlUABkE8BED2tbIZoxkwSqzOwde8OcUEH0bb5DLZUWIMvyBSA=="],
"@solid-primitives/map": ["@solid-primitives/map@0.4.13", "", { "dependencies": { "@solid-primitives/trigger": "^1.1.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-B1zyFbsiTQvqPr+cuPCXO72sRuczG9Swncqk5P74NCGw1VE8qa/Ry9GlfI1e/VdeQYHjan+XkbE3rO2GW/qKew=="],
"@solid-primitives/media": ["@solid-primitives/media@2.3.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-hQ4hLOGvfbugQi5Eu1BFWAIJGIAzztq9x0h02xgBGl2l0Jaa3h7tg6bz5tV1NSuNYVGio4rPoa7zVQQLkkx9dA=="],
"@solid-primitives/props": ["@solid-primitives/props@3.2.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-lZOTwFJajBrshSyg14nBMEP0h8MXzPowGO0s3OeiR3z6nXHTfj0FhzDtJMv+VYoRJKQHG2QRnJTgCzK6erARAw=="],
"@solid-primitives/refs": ["@solid-primitives/refs@1.1.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-K7tf2thy7L+YJjdqXspXOg5xvNEOH8tgEWsp0+1mQk3obHBRD6hEjYZk7p7FlJphSZImS35je3UfmWuD7MhDfg=="],
"@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="],
"@solid-primitives/rootless": ["@solid-primitives/rootless@1.5.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-9HULb0QAzL2r47CCad0M+NKFtQ+LrGGNHZfteX/ThdGvKIg2o2GYhBooZubTCd/RTu2l2+Nw4s+dEfiDGvdrrQ=="],
"@solid-primitives/static-store": ["@solid-primitives/static-store@0.1.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-ReK+5O38lJ7fT+L6mUFvUr6igFwHBESZF+2Ug842s7fvlVeBdIVEdTCErygff6w7uR6+jrr7J8jQo+cYrEq4Iw=="],
"@solid-primitives/trigger": ["@solid-primitives/trigger@1.2.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-IWoptVc0SWYgmpBPpCMehS5b07+tpFcvw15tOQ3QbXedSYn6KP8zCjPkHNzMxcOvOicTneleeZDP7lqmz+PQ6g=="],
"@solid-primitives/utils": ["@solid-primitives/utils@6.3.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-hZ/M/qr25QOCcwDPOHtGjxTD8w2mNyVAYvcfgwzBHq2RwNqHNdDNsMZYap20+ruRwW4A3Cdkczyoz0TSxLCAPQ=="],
"@swc/helpers": ["@swc/helpers@0.5.18", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ=="],
"babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.3", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-5HOwwt0BYiv/zxl7j8Pf2bGL6rDXfV6nUhLs8ygBX+EFJXzBPHM/euj9j/6deMZ6wa52Wb2PBaAV5U/jKwIY1w=="],
"babel-preset-solid": ["babel-preset-solid@1.9.10", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.3" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.10" }, "optionalPeers": ["solid-js"] }, "sha512-HCelrgua/Y+kqO8RyL04JBWS/cVdrtUv/h45GntgQY+cJl4eBcKkCDV3TdMjtKx1nXwRaR9QXslM/Npm1dxdZQ=="],
@@ -198,8 +239,14 @@
"solid-js": ["solid-js@1.9.11", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-WEJtcc5mkh/BnHA6Yrg4whlF8g6QwpmXXRg4P2ztPmcKeHHlH4+djYecBLhSpecZY2RRECXYUwIc/C2r3yzQ4Q=="],
"solid-presence": ["solid-presence@0.1.8", "", { "dependencies": { "@corvu/utils": "~0.4.0" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-pWGtXUFWYYUZNbg5YpG5vkQJyOtzn2KXhxYaMx/4I+lylTLYkITOLevaCwMRN+liCVk0pqB6EayLWojNqBFECA=="],
"solid-prevent-scroll": ["solid-prevent-scroll@0.1.10", "", { "dependencies": { "@corvu/utils": "~0.4.1" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-KplGPX2GHiWJLZ6AXYRql4M127PdYzfwvLJJXMkO+CMb8Np4VxqDAg5S8jLdwlEuBis/ia9DKw2M8dFx5u8Mhw=="],
"topbar": ["topbar@2.0.2", "", {}, "sha512-hCKoSaWxXqGIgjag8rIVajysE41as7ti5z9GDO5rcx2zmII1/rY5zvO9IgKwbf50HL82EzlimL6OmPYPUgbpEw=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
+23
View File
@@ -103,3 +103,26 @@
[data-phx-session], [data-phx-teleported-src] { display: contents }
/* This file is for your main application CSS */
/* Custom Animations for Audio UI */
@keyframes pulse-glow {
0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); transform: scale(1); }
70% { box-shadow: 0 0 0 20px rgba(239, 68, 68, 0); transform: scale(1.05); }
100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); transform: scale(1); }
}
.animate-pulse-glow {
animation: pulse-glow 2s infinite;
}
.glass {
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.3);
}
.dark .glass {
background: rgba(15, 23, 42, 0.7);
border: 1px solid rgba(255, 255, 255, 0.1);
}
+2 -1
View File
@@ -27,12 +27,13 @@ import topbar from "../vendor/topbar"
import { AudioRecorder } from "./hooks/audio_recorder"
import { SolidBridge } from "./hooks/SolidBridge"
import { HierarchyBridge } from "./hooks/HierarchyBridge"
const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
const liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 2500,
params: { _csrf_token: csrfToken },
hooks: { ...colocatedHooks, AudioRecorder, SolidBridge },
hooks: { ...colocatedHooks, AudioRecorder, SolidBridge, HierarchyBridge },
})
// Show progress bar on live navigation and form submits
@@ -0,0 +1,168 @@
import { createSignal, createEffect, For, Show } from "solid-js";
export function HierarchyApp(props) {
// props.state contains springs, rivers, streams, trenches, etc.
// props.push is the function to push events to LiveView
// props.setState can be used for local updates if needed,
// but better to just use props.state and fire events.
const handleSpringClick = (springId) => {
// Optimistic update or just wait for server?
// Let's fire event to server to fetch rivers
props.push("fetch_rivers", { spring_id: springId });
// We can also update local state to show selection immediately?
// But sending `setState` from parent is cleaner.
// Wait, the parent hook has `setState`.
// We can use a local signal for UI selection state content,
// but the data (rivers) must come from server.
// Actually, let's just trigger the push.
// And ideally we also set activeSpringId in the store.
props.setState("activeSpringId", springId);
props.setState("activeRiverId", null);
props.setState("activeStreamId", null);
};
const handleRiverClick = (riverId) => {
props.push("fetch_streams", { river_id: riverId });
props.setState("activeRiverId", riverId);
// Maybe toggle expansion?
};
const handleStreamClick = (streamId) => {
props.push("fetch_trenches", { stream_id: streamId });
props.setState("activeStreamId", streamId);
};
return (
<div class="flex h-screen bg-gray-900 text-white font-sans">
{/* 1. Springs (Servers) Column */}
<div class="w-20 flex flex-col items-center py-4 bg-gray-950 border-r border-gray-800 space-y-4">
<For each={props.state.springs}>
{(spring) => (
<button
onClick={() => handleSpringClick(spring.id)}
class={`w-12 h-12 rounded-full flex items-center justify-center transition-all duration-200 hover:rounded-2xl ${props.state.activeSpringId === spring.id
? "bg-indigo-500 rounded-2xl shadow-lg ring-2 ring-indigo-400"
: "bg-gray-800 hover:bg-indigo-600"
}`}
title={spring.name}
>
{spring.name.substring(0, 2).toUpperCase()}
</button>
)}
</For>
<button class="w-12 h-12 rounded-full bg-gray-800 hover:bg-green-600 flex items-center justify-center text-green-400 hover:text-white transition-all duration-200 hover:rounded-2xl">
<span class="text-2xl">+</span>
</button>
</div>
{/* 2. Rivers & Streams (Channels) Column */}
<Show when={props.state.activeSpringId}>
<div class="w-64 bg-gray-900 flex flex-col border-r border-gray-800">
{/* Header */}
<div class="h-12 border-b border-gray-800 flex items-center px-4 font-bold shadow-sm">
{props.state.springs.find(s => s.id === props.state.activeSpringId)?.name || "Spring"}
</div>
{/* Rivers List */}
<div class="flex-1 overflow-y-auto p-2 space-y-2">
<Show when={props.state.rivers[props.state.activeSpringId]} fallback={<div class="p-4 text-gray-500 text-sm">Loading rivers...</div>}>
<For each={props.state.rivers[props.state.activeSpringId]}>
{(river) => (
<div class="mb-2">
<div
class="px-2 py-1 text-xs font-semibold text-gray-400 uppercase tracking-wider hover:text-gray-200 cursor-pointer flex items-center justify-between group"
onClick={() => handleRiverClick(river.id)}
>
<span>v {river.name}</span>
<span class="opacity-0 group-hover:opacity-100 text-gray-500 hover:text-white">+</span>
</div>
{/* Streams in this river */}
<Show when={props.state.streams[river.id]}>
<div class="mt-1 space-y-0.5">
<For each={props.state.streams[river.id]}>
{(stream) => (
<div
onClick={() => handleStreamClick(stream.id)}
class={`px-2 py-1 rounded mx-2 text-gray-400 hover:bg-gray-800 hover:text-gray-100 cursor-pointer flex items-center ${props.state.activeStreamId === stream.id ? "bg-gray-800 text-white font-medium" : ""
}`}
>
<span class="mr-1 text-gray-500">#</span>
{stream.name}
</div>
)}
</For>
</div>
</Show>
</div>
)}
</For>
</Show>
</div>
</div>
</Show>
{/* 3. Main Stream (Chat) Area */}
<div class="flex-1 flex flex-col bg-gray-800">
<Show when={props.state.activeStreamId} fallback={
<div class="flex-1 flex items-center justify-center text-gray-500 flex-col">
<div class="text-4xl mb-4">👋</div>
<p>Select a stream to start chatting</p>
</div>
}>
{/* Header */}
<div class="h-12 border-b border-gray-700 flex items-center px-4 shadow-sm bg-gray-900 justify-between">
<div class="flex items-center">
<span class="text-2xl text-gray-500 mr-2">#</span>
<span class="font-bold">
{/* Need to find stream name across all rivers? inefficient.
Better to store activeStream details? Or just find it. */}
Stream Name
</span>
</div>
</div>
{/* Messages */}
<div class="flex-1 overflow-y-auto p-4 space-y-4">
{/* Placeholder messages */}
<div class="flex items-start">
<div class="w-10 h-10 rounded-full bg-gray-600 mr-3"></div>
<div>
<div class="flex items-baseline">
<span class="font-bold mr-2 text-white">User A</span>
<span class="text-xs text-gray-400">10:30 AM</span>
</div>
<p class="text-gray-300">Welcome to the stream!</p>
</div>
</div>
<Show when={props.state.trenches[props.state.activeStreamId]}>
<div class="pl-12 pt-2">
<div class="text-xs font-bold text-gray-400 mb-1">Trenches (Threads):</div>
<For each={props.state.trenches[props.state.activeStreamId]}>
{(trench) => (
<div class="text-sm text-indigo-400 hover:underline cursor-pointer">
{trench.name}
</div>
)}
</For>
</div>
</Show>
</div>
{/* Input */}
<div class="p-4 bg-gray-900">
<input
type="text"
class="w-full bg-gray-800 text-white rounded-lg px-4 py-3 focus:outline-none focus:ring-2 focus:ring-indigo-500"
placeholder="Message #stream"
/>
</div>
</Show>
</div>
</div>
);
}
+42 -13
View File
@@ -1,4 +1,5 @@
import { createSignal } from "solid-js";
import { Popover } from "@kobalte/core";
export function SolidApp(props) {
// Local state for optimistic updates or other UI-only logic
@@ -14,33 +15,61 @@ export function SolidApp(props) {
props.push("decrement", {});
};
// Reset updating flag when state changes from server
// (In a real app, you might use a more sophisticated approach)
return (
<div class="p-6 border-2 border-blue-500 rounded-lg bg-white shadow-md">
<h2 class="text-xl font-semibold text-blue-700 mb-4">Solid.js Component</h2>
<div class="p-6 border-2 border-blue-500 rounded-2xl bg-white shadow-xl max-w-sm mx-auto">
<h2 class="text-2xl font-black text-blue-700 mb-6 tracking-tight">Solid + Kobalte</h2>
<div class="flex items-center justify-between mb-8 p-4 bg-slate-50 rounded-xl border border-slate-100">
<div class="flex flex-col">
<span class="text-xs font-bold text-slate-400 uppercase tracking-widest">Current Count</span>
<span class="text-5xl font-black text-slate-900">{props.state.count}</span>
</div>
<div class="flex items-center space-x-4 mb-6">
<span class="text-4xl font-bold">{props.state.count}</span>
<div class="flex flex-col space-y-2">
<button
onClick={increment}
class="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 transition-colors"
class="w-12 h-12 bg-indigo-600 text-white rounded-full flex items-center justify-center hover:bg-indigo-700 transition-all active:scale-90 shadow-lg shadow-indigo-200"
>
Increment
<span class="text-2xl">+</span>
</button>
<button
onClick={decrement}
class="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600 transition-colors"
class="w-12 h-12 bg-slate-200 text-slate-600 rounded-full flex items-center justify-center hover:bg-slate-300 transition-all active:scale-90"
>
Decrement
<span class="text-2xl"></span>
</button>
</div>
</div>
<p class="text-sm text-gray-500 italic">
This UI is rendered by Solid.js. Changes are synced back to Elixir via push_event.
<div class="flex justify-center">
<Popover.Root>
<Popover.Trigger class="px-6 py-2.5 bg-slate-900 text-white text-sm font-bold rounded-full hover:bg-slate-800 transition-colors flex items-center space-x-2">
<span>Kobalte Info</span>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</Popover.Trigger>
<Popover.Portal>
<Popover.Content class="z-50 w-64 p-4 bg-white rounded-xl shadow-2xl border border-slate-100 animate-in fade-in zoom-in-95 duration-200 origin-top">
<Popover.Arrow class="text-white fill-current" />
<div class="flex flex-col">
<Popover.Title class="text-sm font-bold text-slate-900 mb-1">Kobalte Logic</Popover.Title>
<Popover.Description class="text-xs text-slate-500 leading-relaxed">
Kobalte is a headless UI toolkit for Solid.js. It provides the logic while you provide the styles!
</Popover.Description>
<Popover.CloseButton class="absolute top-2 right-2 text-slate-400 hover:text-slate-600">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</Popover.CloseButton>
</div>
</Popover.Content>
</Popover.Portal>
</Popover.Root>
</div>
<p class="mt-8 text-[10px] text-slate-400 font-bold uppercase tracking-tighter text-center">
Bridge: Phoenix LiveView &harr; Solid.js
</p>
</div>
);
@@ -0,0 +1,48 @@
import { render } from "solid-js/web";
import { createStore } from "solid-js/store";
import { HierarchyApp } from "../components/HierarchyApp";
export const HierarchyBridge = {
mounted() {
console.log("HierarchyBridge mounted");
// 1. Initialize store
const initialSprings = JSON.parse(this.el.dataset.initialSprings || "[]");
const [state, setState] = createStore({
springs: initialSprings,
rivers: {}, // Map spring_id -> rivers
streams: {}, // Map river_id -> streams
trenches: {}, // Map stream_id -> trenches
activeSpringId: null,
activeRiverId: null,
activeStreamId: null
});
// 2. Render parameters
this._cleanup = render(() => (
<HierarchyApp
state={state}
push={(e, p) => this.pushEvent(e, p)}
setState={setState}
/>
), this.el);
// 3. Listen for server assignments
this.handleEvent("update_rivers", ({ spring_id, rivers }) => {
setState("rivers", spring_id, rivers);
});
this.handleEvent("update_streams", ({ river_id, streams }) => {
setState("streams", river_id, streams);
});
this.handleEvent("update_trenches", ({ stream_id, trenches }) => {
setState("trenches", stream_id, trenches);
});
},
destroyed() {
if (this._cleanup) this._cleanup();
}
};
+1
View File
@@ -15,6 +15,7 @@
"author": "",
"license": "ISC",
"dependencies": {
"@kobalte/core": "^0.13.11",
"phoenix": "^1.8.3",
"phoenix_html": "^4.1.1",
"phoenix_live_view": "^1.1.0",