built basic ui structure in /hierarchy
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 ↔ 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();
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user