mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-04 17:38:10 +00:00
tabs and app mode signals (#450)
Usage example - Conditional Rendering Based on App Mode:
```typescript
import { appMode } from "~/signals";
function ConditionalComponent() {
return (
<div className="app-container">
{/* Show/hide components based on current mode */}
{appMode.value === 'image' && (
<div className="image-tools">
<h3>Image Mode Tools</h3>
</div>
)}
{appMode.value === 'edit' && (
<div className="edit-tools">
<h3>Edit Mode Tools</h3>
</div>
)}
);
}
```
This commit is contained in:
1 parent
727eead5a7
commit
883e32220c
5 files changed
+153
-1
No files matched your search
@@ -1,17 +1,43 @@
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { TabSelector, TabItem } from "~/components/ui/TabSelector";
|
||||
import { useComputed } from "@preact/signals-react";
|
||||
import { appMode, dispatchUiEvents } from "~/signals";
|
||||
|
||||
// import { paperWrapperStyles } from "~/components/styles";
|
||||
// import { faPlus, faQuestion } from "@fortawesome/pro-solid-svg-icons";
|
||||
// import { ToolbarButton } from "~/components/features/ToolbarButton";
|
||||
|
||||
export const ToolbarTopLeft = () => {
|
||||
// Use the appMode signal directly with useComputed for reactivity - active tab sets the app mode
|
||||
const activeTab = useComputed(() => appMode.value);
|
||||
|
||||
const tabs: TabItem[] = [
|
||||
{ id: "image", label: "Image" },
|
||||
{ id: "edit", label: "Edit" },
|
||||
// { id: "video", label: "Video" },
|
||||
];
|
||||
|
||||
const handleTabChange = (tabId: string) => {
|
||||
// Update the global signal
|
||||
dispatchUiEvents.changeAppMode(tabId as "image" | "edit");
|
||||
};
|
||||
|
||||
console.log(appMode.value);
|
||||
|
||||
return (
|
||||
<div className={twMerge("z-20 flex h-fit w-fit items-center gap-1")}>
|
||||
<div className={twMerge("z-20 flex h-fit w-fit items-center gap-8 px-4")}>
|
||||
<img
|
||||
src="/brand/mira-logo.png"
|
||||
alt="logo"
|
||||
className="h-[30px] select-none pb-1"
|
||||
/>
|
||||
|
||||
<TabSelector
|
||||
tabs={tabs}
|
||||
activeTab={activeTab.value}
|
||||
onTabChange={handleTabChange}
|
||||
/>
|
||||
|
||||
{/* <ToolbarButton icon={faPlus}>
|
||||
<span className="font-semibold">New Board</span>
|
||||
</ToolbarButton>
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import React, { useRef, useEffect, useState } from "react";
|
||||
import { Tab, TabGroup, TabList } from "@headlessui/react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export interface TabItem {
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface TabSelectorProps {
|
||||
tabs: TabItem[];
|
||||
activeTab: string;
|
||||
onTabChange: (tabId: string) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const TabSelector: React.FC<TabSelectorProps> = ({
|
||||
tabs,
|
||||
activeTab,
|
||||
onTabChange,
|
||||
className,
|
||||
}) => {
|
||||
// Find the index of the active tab
|
||||
const selectedIndex = tabs.findIndex((tab) => tab.id === activeTab);
|
||||
const tabsRef = useRef<(HTMLElement | null)[]>([]);
|
||||
const [indicatorStyle, setIndicatorStyle] = useState({
|
||||
left: 0,
|
||||
width: 0,
|
||||
});
|
||||
|
||||
// Handle tab change
|
||||
const handleTabChange = (index: number) => {
|
||||
onTabChange(tabs[index].id);
|
||||
};
|
||||
|
||||
// Update the indicator position when the selected tab changes
|
||||
useEffect(() => {
|
||||
if (selectedIndex >= 0 && tabsRef.current[selectedIndex]) {
|
||||
const currentTab = tabsRef.current[selectedIndex];
|
||||
if (currentTab) {
|
||||
setIndicatorStyle({
|
||||
left: currentTab.offsetLeft,
|
||||
width: currentTab.offsetWidth,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [selectedIndex, tabs]);
|
||||
|
||||
return (
|
||||
<TabGroup
|
||||
selectedIndex={selectedIndex}
|
||||
onChange={handleTabChange}
|
||||
className={twMerge("w-full", className)}
|
||||
>
|
||||
<TabList className="glass relative inline-flex min-w-fit overflow-x-auto rounded-lg p-1 shadow-lg">
|
||||
{/* Animated indicator */}
|
||||
<div
|
||||
className="absolute top-1 z-10 h-[calc(100%-8px)] rounded-md border-2 border-primary bg-primary/30 transition-all duration-300 ease-in-out"
|
||||
style={{
|
||||
left: indicatorStyle.left,
|
||||
width: indicatorStyle.width,
|
||||
}}
|
||||
/>
|
||||
|
||||
{tabs.map((tab, index) => (
|
||||
<Tab
|
||||
key={tab.id}
|
||||
ref={(el) => (tabsRef.current[index] = el)}
|
||||
className={({ selected }) =>
|
||||
twMerge(
|
||||
"relative z-20 mx-0.5 min-w-max rounded-md border-2 border-transparent px-4 py-1 text-center font-semibold transition-all duration-300 ease-in-out",
|
||||
selected ? "text-white" : "text-gray-300 hover:text-white",
|
||||
)
|
||||
}
|
||||
>
|
||||
{tab.label}
|
||||
</Tab>
|
||||
))}
|
||||
</TabList>
|
||||
</TabGroup>
|
||||
);
|
||||
};
|
||||
@@ -8,6 +8,7 @@ export * from "./PanelRight";
|
||||
export * from "./Slider";
|
||||
export * from "./Spinner";
|
||||
export * from "./Switches";
|
||||
export * from "./TabSelector";
|
||||
export * from "./TextArea";
|
||||
export * from "./TextEditor";
|
||||
export * from "./Tooltip";
|
||||
@@ -0,0 +1,38 @@
|
||||
import { signal, effect } from "@preact/signals-core";
|
||||
|
||||
// this is to control the app mode - BFlat
|
||||
|
||||
// Define the app mode types
|
||||
export type AppModeType = "image" | "edit";
|
||||
|
||||
// Create a signal for the current app mode
|
||||
export const appMode = signal<AppModeType>("image");
|
||||
|
||||
// Create a function to change the app mode
|
||||
export const changeAppMode = (mode: AppModeType) => {
|
||||
appMode.value = mode;
|
||||
};
|
||||
|
||||
// Store callbacks for mode changes
|
||||
const callbacks: ((mode: AppModeType) => void)[] = [];
|
||||
|
||||
// Set up an effect to notify callbacks when the mode changes
|
||||
effect(() => {
|
||||
const currentMode = appMode.value;
|
||||
callbacks.forEach((callback) => callback(currentMode));
|
||||
});
|
||||
|
||||
// Export an event handler for components to use
|
||||
export const appModeEvents = {
|
||||
onChange: (callback: (mode: AppModeType) => void) => {
|
||||
callbacks.push(callback);
|
||||
|
||||
// Return a function to unsubscribe
|
||||
return () => {
|
||||
const index = callbacks.indexOf(callback);
|
||||
if (index !== -1) {
|
||||
callbacks.splice(index, 1);
|
||||
}
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
dispatchers as modelSelectionDispatchers,
|
||||
events as modelSelectionEvents,
|
||||
} from "./modelSelection";
|
||||
import { appMode, changeAppMode, appModeEvents } from "./appMode";
|
||||
|
||||
export const uiEvents = {
|
||||
...addMediaEvents,
|
||||
@@ -49,6 +50,7 @@ export const uiEvents = {
|
||||
promptEvents: promptEvents,
|
||||
loadingIndicator: loadingIndicatorEvents,
|
||||
modelSelection: modelSelectionEvents,
|
||||
appMode: appModeEvents,
|
||||
};
|
||||
export const dispatchUiEvents = {
|
||||
...addMediaDispatchers,
|
||||
@@ -62,4 +64,7 @@ export const dispatchUiEvents = {
|
||||
promptSettings: promptDispatchers,
|
||||
loadingIndicator: loadingIndicatorDispatchers,
|
||||
modelSelection: modelSelectionDispatchers,
|
||||
changeAppMode,
|
||||
};
|
||||
|
||||
export { appMode };
|
||||
Reference in new issue
Block a user