mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
admin ui: started color centralization + made sidebar resizable
This commit is contained in:
@@ -18,13 +18,7 @@ const Base: React.FC<AlertProps> = ({
|
|||||||
...props
|
...props
|
||||||
}) =>
|
}) =>
|
||||||
visible ? (
|
visible ? (
|
||||||
<div
|
<div {...props} className={twMerge("flex flex-row items-center p-4", className)}>
|
||||||
{...props}
|
|
||||||
className={twMerge(
|
|
||||||
"flex flex-row items-center dark:bg-amber-300/20 bg-amber-200 p-4",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<p>
|
<p>
|
||||||
{title && <b>{title}: </b>}
|
{title && <b>{title}: </b>}
|
||||||
{message || children}
|
{message || children}
|
||||||
@@ -33,19 +27,19 @@ const Base: React.FC<AlertProps> = ({
|
|||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
const Warning: React.FC<AlertProps> = ({ className, ...props }) => (
|
const Warning: React.FC<AlertProps> = ({ className, ...props }) => (
|
||||||
<Base {...props} className={twMerge("dark:bg-amber-300/20 bg-amber-200", className)} />
|
<Base {...props} className={twMerge("bg-warning text-warning-foreground", className)} />
|
||||||
);
|
);
|
||||||
|
|
||||||
const Exception: React.FC<AlertProps> = ({ className, ...props }) => (
|
const Exception: React.FC<AlertProps> = ({ className, ...props }) => (
|
||||||
<Base {...props} className={twMerge("dark:bg-red-950 bg-red-100", className)} />
|
<Base {...props} className={twMerge("bg-error text-error-foreground", className)} />
|
||||||
);
|
);
|
||||||
|
|
||||||
const Success: React.FC<AlertProps> = ({ className, ...props }) => (
|
const Success: React.FC<AlertProps> = ({ className, ...props }) => (
|
||||||
<Base {...props} className={twMerge("dark:bg-green-950 bg-green-100", className)} />
|
<Base {...props} className={twMerge("bg-success text-success-foreground", className)} />
|
||||||
);
|
);
|
||||||
|
|
||||||
const Info: React.FC<AlertProps> = ({ className, ...props }) => (
|
const Info: React.FC<AlertProps> = ({ className, ...props }) => (
|
||||||
<Base {...props} className={twMerge("dark:bg-blue-950 bg-blue-100", className)} />
|
<Base {...props} className={twMerge("bg-info text-info-foreground", className)} />
|
||||||
);
|
);
|
||||||
|
|
||||||
export const Alert = {
|
export const Alert = {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { getBrowser } from "core/utils";
|
|||||||
import type { Field } from "data";
|
import type { Field } from "data";
|
||||||
import { Switch as RadixSwitch } from "radix-ui";
|
import { Switch as RadixSwitch } from "radix-ui";
|
||||||
import {
|
import {
|
||||||
type ChangeEventHandler,
|
|
||||||
type ComponentPropsWithoutRef,
|
type ComponentPropsWithoutRef,
|
||||||
type ElementType,
|
type ElementType,
|
||||||
forwardRef,
|
forwardRef,
|
||||||
@@ -12,7 +11,7 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { TbCalendar, TbChevronDown, TbInfoCircle } from "react-icons/tb";
|
import { TbCalendar, TbChevronDown, TbEye, TbEyeOff, TbInfoCircle } from "react-icons/tb";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import { IconButton } from "ui/components/buttons/IconButton";
|
import { IconButton } from "ui/components/buttons/IconButton";
|
||||||
import { useEvent } from "ui/hooks/use-event";
|
import { useEvent } from "ui/hooks/use-event";
|
||||||
@@ -89,7 +88,7 @@ export const Input = forwardRef<HTMLInputElement, React.ComponentProps<"input">>
|
|||||||
{...props}
|
{...props}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"bg-muted/40 h-11 rounded-md py-2.5 px-4 outline-none",
|
"bg-muted/40 h-11 rounded-md py-2.5 px-4 outline-none w-full",
|
||||||
disabledOrReadonly && "bg-muted/50 text-primary/50",
|
disabledOrReadonly && "bg-muted/50 text-primary/50",
|
||||||
!disabledOrReadonly &&
|
!disabledOrReadonly &&
|
||||||
"focus:bg-muted focus:outline-none focus:ring-2 focus:ring-zinc-500 focus:border-transparent transition-all",
|
"focus:bg-muted focus:outline-none focus:ring-2 focus:ring-zinc-500 focus:border-transparent transition-all",
|
||||||
@@ -99,6 +98,40 @@ export const Input = forwardRef<HTMLInputElement, React.ComponentProps<"input">>
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const TypeAwareInput = forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||||
|
(props, ref) => {
|
||||||
|
if (props.type === "password") {
|
||||||
|
return <Password {...props} ref={ref} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Input {...props} ref={ref} />;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Password = forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||||
|
(props, ref) => {
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
|
||||||
|
function handleToggle() {
|
||||||
|
setVisible((v) => !v);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative w-full">
|
||||||
|
<Input {...props} type={visible ? "text" : "password"} className="w-full" ref={ref} />
|
||||||
|
<div className="absolute right-3 top-0 bottom-0 flex items-center">
|
||||||
|
<IconButton
|
||||||
|
Icon={visible ? TbEyeOff : TbEye}
|
||||||
|
onClick={handleToggle}
|
||||||
|
variant="ghost"
|
||||||
|
className="opacity-70"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export const Textarea = forwardRef<HTMLTextAreaElement, React.ComponentProps<"textarea">>(
|
export const Textarea = forwardRef<HTMLTextAreaElement, React.ComponentProps<"textarea">>(
|
||||||
(props, ref) => {
|
(props, ref) => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { JsonSchema } from "json-schema-library";
|
import type { JsonSchema } from "json-schema-library";
|
||||||
import type { ChangeEvent, ComponentPropsWithoutRef } from "react";
|
import type { ChangeEvent, ComponentPropsWithoutRef, ReactNode } from "react";
|
||||||
import ErrorBoundary from "ui/components/display/ErrorBoundary";
|
import ErrorBoundary from "ui/components/display/ErrorBoundary";
|
||||||
import * as Formy from "ui/components/form/Formy";
|
import * as Formy from "ui/components/form/Formy";
|
||||||
import { useEvent } from "ui/hooks/use-event";
|
import { useEvent } from "ui/hooks/use-event";
|
||||||
@@ -13,6 +13,7 @@ export type FieldProps = {
|
|||||||
onChange?: (e: ChangeEvent<any>) => void;
|
onChange?: (e: ChangeEvent<any>) => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
inputProps?: Partial<FieldComponentProps>;
|
||||||
} & Omit<FieldwrapperProps, "children" | "schema">;
|
} & Omit<FieldwrapperProps, "children" | "schema">;
|
||||||
|
|
||||||
export const Field = (props: FieldProps) => {
|
export const Field = (props: FieldProps) => {
|
||||||
@@ -31,7 +32,14 @@ const fieldErrorBoundary =
|
|||||||
</Pre>
|
</Pre>
|
||||||
);
|
);
|
||||||
|
|
||||||
const FieldImpl = ({ name, onChange, placeholder, required: _required, ...props }: FieldProps) => {
|
const FieldImpl = ({
|
||||||
|
name,
|
||||||
|
onChange,
|
||||||
|
placeholder,
|
||||||
|
required: _required,
|
||||||
|
inputProps,
|
||||||
|
...props
|
||||||
|
}: FieldProps) => {
|
||||||
const { path, setValue, schema, ...ctx } = useDerivedFieldContext(name);
|
const { path, setValue, schema, ...ctx } = useDerivedFieldContext(name);
|
||||||
const required = typeof _required === "boolean" ? _required : ctx.required;
|
const required = typeof _required === "boolean" ? _required : ctx.required;
|
||||||
//console.log("Field", { name, path, schema });
|
//console.log("Field", { name, path, schema });
|
||||||
@@ -64,6 +72,7 @@ const FieldImpl = ({ name, onChange, placeholder, required: _required, ...props
|
|||||||
return (
|
return (
|
||||||
<FieldWrapper name={name} required={required} schema={schema} {...props}>
|
<FieldWrapper name={name} required={required} schema={schema} {...props}>
|
||||||
<FieldComponent
|
<FieldComponent
|
||||||
|
{...inputProps}
|
||||||
schema={schema}
|
schema={schema}
|
||||||
name={name}
|
name={name}
|
||||||
required={required}
|
required={required}
|
||||||
@@ -81,10 +90,12 @@ export const Pre = ({ children }) => (
|
|||||||
</pre>
|
</pre>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const FieldComponent = ({
|
export type FieldComponentProps = {
|
||||||
schema,
|
schema: JsonSchema;
|
||||||
..._props
|
render?: (props: Omit<FieldComponentProps, "render">) => ReactNode;
|
||||||
}: { schema: JsonSchema } & ComponentPropsWithoutRef<"input">) => {
|
} & ComponentPropsWithoutRef<"input">;
|
||||||
|
|
||||||
|
export const FieldComponent = ({ schema, render, ..._props }: FieldComponentProps) => {
|
||||||
const { value } = useFormValue(_props.name!, { strict: true });
|
const { value } = useFormValue(_props.name!, { strict: true });
|
||||||
if (!isTypeSchema(schema)) return null;
|
if (!isTypeSchema(schema)) return null;
|
||||||
const props = {
|
const props = {
|
||||||
@@ -97,6 +108,8 @@ export const FieldComponent = ({
|
|||||||
: "",
|
: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (render) return render({ schema, ...props });
|
||||||
|
|
||||||
if (schema.enum) {
|
if (schema.enum) {
|
||||||
return <Formy.Select id={props.name} options={schema.enum} {...(props as any)} />;
|
return <Formy.Select id={props.name} options={schema.enum} {...(props as any)} />;
|
||||||
}
|
}
|
||||||
@@ -158,5 +171,7 @@ export const FieldComponent = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Formy.Input id={props.name} {...props} value={props.value ?? ""} {...additional} />;
|
return (
|
||||||
|
<Formy.TypeAwareInput id={props.name} {...props} value={props.value ?? ""} {...additional} />
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useClickOutside, useHotkeys } from "@mantine/hooks";
|
import { useClickOutside, useHotkeys } from "@mantine/hooks";
|
||||||
import { IconChevronDown, IconChevronUp } from "@tabler/icons-react";
|
import { IconChevronDown, IconChevronUp } from "@tabler/icons-react";
|
||||||
|
import { clampNumber } from "core/utils/numbers";
|
||||||
import { throttle } from "lodash-es";
|
import { throttle } from "lodash-es";
|
||||||
import { ScrollArea } from "radix-ui";
|
import { ScrollArea } from "radix-ui";
|
||||||
import {
|
import {
|
||||||
@@ -12,13 +13,20 @@ import {
|
|||||||
import type { IconType } from "react-icons";
|
import type { IconType } from "react-icons";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import { IconButton } from "ui/components/buttons/IconButton";
|
import { IconButton } from "ui/components/buttons/IconButton";
|
||||||
import { useEvent } from "ui/hooks/use-event";
|
|
||||||
import { AppShellProvider, useAppShell } from "ui/layouts/AppShell/use-appshell";
|
import { AppShellProvider, useAppShell } from "ui/layouts/AppShell/use-appshell";
|
||||||
|
import { appShellStore } from "ui/store";
|
||||||
|
import { useLocation } from "wouter";
|
||||||
|
|
||||||
export function Root({ children }) {
|
export function Root({ children }: { children: React.ReactNode }) {
|
||||||
|
const sidebarWidth = appShellStore((store) => store.sidebarWidth);
|
||||||
return (
|
return (
|
||||||
<AppShellProvider>
|
<AppShellProvider>
|
||||||
<div data-shell="root" className="flex flex-1 flex-col select-none h-dvh">
|
<div
|
||||||
|
id="app-shell"
|
||||||
|
data-shell="root"
|
||||||
|
className="flex flex-1 flex-col select-none h-dvh"
|
||||||
|
style={{ "--sidebar-width": `${sidebarWidth}px` } as React.CSSProperties}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</AppShellProvider>
|
</AppShellProvider>
|
||||||
@@ -80,7 +88,7 @@ export function Main({ children }) {
|
|||||||
data-shell="main"
|
data-shell="main"
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"flex flex-col flex-grow w-1 flex-shrink-1",
|
"flex flex-col flex-grow w-1 flex-shrink-1",
|
||||||
sidebar.open && "md:max-w-[calc(100%-350px)]",
|
sidebar.open && "md:max-w-[calc(100%-var(--sidebar-width))]",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -89,47 +97,38 @@ export function Main({ children }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Sidebar({ children }) {
|
export function Sidebar({ children }) {
|
||||||
const ctx = useAppShell();
|
const open = appShellStore((store) => store.sidebarOpen);
|
||||||
|
const close = appShellStore((store) => store.closeSidebar);
|
||||||
|
const ref = useClickOutside(close, null, [document.getElementById("header")]);
|
||||||
|
const [location] = useLocation();
|
||||||
|
|
||||||
const ref = useClickOutside(ctx.sidebar?.handler?.close);
|
const closeHandler = () => {
|
||||||
|
open && close();
|
||||||
|
};
|
||||||
|
|
||||||
const onClickBackdrop = useEvent((e: React.MouseEvent) => {
|
// listen for window location change
|
||||||
e.preventDefault();
|
useEffect(closeHandler, [location]);
|
||||||
e.stopPropagation();
|
|
||||||
ctx?.sidebar?.handler.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
const onEscape = useEvent(() => {
|
|
||||||
if (ctx?.sidebar?.open) {
|
|
||||||
ctx?.sidebar?.handler.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// @todo: potentially has to be added to the root, as modals could be opened
|
// @todo: potentially has to be added to the root, as modals could be opened
|
||||||
useHotkeys([["Escape", onEscape]]);
|
useHotkeys([["Escape", closeHandler]]);
|
||||||
|
|
||||||
if (!ctx) {
|
|
||||||
console.warn("AppShell.Sidebar: missing AppShellContext");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<aside
|
<aside
|
||||||
data-shell="sidebar"
|
data-shell="sidebar"
|
||||||
className="hidden md:flex flex-col basis-[350px] flex-shrink-0 flex-grow-0 h-full border-muted border-r bg-muted/10"
|
className="hidden md:flex flex-col basis-[var(--sidebar-width)] flex-shrink-0 flex-grow-0 h-full bg-muted/10"
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</aside>
|
</aside>
|
||||||
|
<SidebarResize />
|
||||||
<div
|
<div
|
||||||
data-open={ctx?.sidebar?.open}
|
data-open={open}
|
||||||
className="absolute w-full md:hidden data-[open=true]:translate-x-0 translate-x-[-100%] transition-transform z-10 backdrop-blur-sm"
|
className="absolute w-full md:hidden data-[open=true]:translate-x-0 translate-x-[-100%] transition-transform z-10 backdrop-blur-sm"
|
||||||
onClick={onClickBackdrop}
|
|
||||||
>
|
>
|
||||||
<aside
|
<aside
|
||||||
/*ref={ref}*/
|
ref={ref}
|
||||||
data-shell="sidebar"
|
data-shell="sidebar"
|
||||||
className="flex-col w-[350px] flex-shrink-0 flex-grow-0 h-full border-muted border-r bg-background"
|
className="flex-col w-[var(--sidebar-width)] flex-shrink-0 flex-grow-0 h-full border-muted border-r bg-background"
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</aside>
|
</aside>
|
||||||
@@ -138,6 +137,59 @@ export function Sidebar({ children }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SidebarResize = () => {
|
||||||
|
const setSidebarWidth = appShellStore((store) => store.setSidebarWidth);
|
||||||
|
const [isResizing, setIsResizing] = useState(false);
|
||||||
|
const [startX, setStartX] = useState(0);
|
||||||
|
const [startWidth, setStartWidth] = useState(0);
|
||||||
|
|
||||||
|
const handleMouseDown = (e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsResizing(true);
|
||||||
|
setStartX(e.clientX);
|
||||||
|
setStartWidth(
|
||||||
|
Number.parseInt(
|
||||||
|
getComputedStyle(document.getElementById("app-shell")!)
|
||||||
|
.getPropertyValue("--sidebar-width")
|
||||||
|
.replace("px", ""),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
|
if (!isResizing) return;
|
||||||
|
|
||||||
|
const diff = e.clientX - startX;
|
||||||
|
const newWidth = clampNumber(startWidth + diff, 250, window.innerWidth * 0.5);
|
||||||
|
setSidebarWidth(newWidth);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
setIsResizing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isResizing) {
|
||||||
|
window.addEventListener("mousemove", handleMouseMove);
|
||||||
|
window.addEventListener("mouseup", handleMouseUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("mousemove", handleMouseMove);
|
||||||
|
window.removeEventListener("mouseup", handleMouseUp);
|
||||||
|
};
|
||||||
|
}, [isResizing, startX, startWidth]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-active={isResizing ? 1 : undefined}
|
||||||
|
className="w-px h-full hidden md:flex bg-muted after:transition-colors relative after:absolute after:inset-0 after:-left-px after:w-[2px] select-none data-[active]:after:bg-sky-400 data-[active]:cursor-col-resize hover:after:bg-sky-400 hover:cursor-col-resize after:z-2"
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
style={{ touchAction: "none" }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export function SectionHeaderTitle({ children, className, ...props }: ComponentProps<"h2">) {
|
export function SectionHeaderTitle({ children, className, ...props }: ComponentProps<"h2">) {
|
||||||
return (
|
return (
|
||||||
<h2
|
<h2
|
||||||
|
|||||||
@@ -19,11 +19,11 @@ import { Logo } from "ui/components/display/Logo";
|
|||||||
import { Dropdown, type DropdownItem } from "ui/components/overlay/Dropdown";
|
import { Dropdown, type DropdownItem } from "ui/components/overlay/Dropdown";
|
||||||
import { Link } from "ui/components/wouter/Link";
|
import { Link } from "ui/components/wouter/Link";
|
||||||
import { useEvent } from "ui/hooks/use-event";
|
import { useEvent } from "ui/hooks/use-event";
|
||||||
import { useAppShell } from "ui/layouts/AppShell/use-appshell";
|
|
||||||
import { useNavigate } from "ui/lib/routes";
|
import { useNavigate } from "ui/lib/routes";
|
||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
import { NavLink } from "./AppShell";
|
import { NavLink } from "./AppShell";
|
||||||
import { autoFormatString } from "core/utils";
|
import { autoFormatString } from "core/utils";
|
||||||
|
import { appShellStore } from "ui/store";
|
||||||
|
|
||||||
export function HeaderNavigation() {
|
export function HeaderNavigation() {
|
||||||
const [location, navigate] = useLocation();
|
const [location, navigate] = useLocation();
|
||||||
@@ -105,10 +105,9 @@ export function HeaderNavigation() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function SidebarToggler() {
|
function SidebarToggler() {
|
||||||
const { sidebar } = useAppShell();
|
const toggle = appShellStore((store) => store.toggleSidebar);
|
||||||
return (
|
const open = appShellStore((store) => store.sidebarOpen);
|
||||||
<IconButton size="lg" Icon={sidebar.open ? TbX : TbMenu2} onClick={sidebar.handler.toggle} />
|
return <IconButton size="lg" Icon={open ? TbX : TbMenu2} onClick={toggle} />;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Header({ hasSidebar = true }) {
|
export function Header({ hasSidebar = true }) {
|
||||||
@@ -118,6 +117,7 @@ export function Header({ hasSidebar = true }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
|
id="header"
|
||||||
data-shell="header"
|
data-shell="header"
|
||||||
className="flex flex-row w-full h-16 gap-2.5 border-muted border-b justify-start bg-muted/10"
|
className="flex flex-row w-full h-16 gap-2.5 border-muted border-b justify-start bg-muted/10"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -4,17 +4,28 @@
|
|||||||
|
|
||||||
#bknd-admin,
|
#bknd-admin,
|
||||||
.bknd-admin {
|
.bknd-admin {
|
||||||
--color-primary: rgb(9 9 11); /* zinc-950 */
|
--color-primary: var(--color-zinc-950);
|
||||||
--color-background: rgb(250 250 250); /* zinc-50 */
|
--color-background: var(--color-zinc-50);
|
||||||
--color-muted: rgb(228 228 231); /* ? */
|
--color-muted: var(--color-zinc-200);
|
||||||
--color-darkest: rgb(0 0 0); /* black */
|
--color-darkest: var(--color-black);
|
||||||
--color-lightest: rgb(255 255 255); /* white */
|
--color-lightest: var(--color-white);
|
||||||
|
|
||||||
|
--color-warning: var(--color-amber-100);
|
||||||
|
--color-warning-foreground: var(--color-amber-800);
|
||||||
|
--color-error: var(--color-red-100);
|
||||||
|
--color-error-foreground: var(--color-red-800);
|
||||||
|
--color-success: var(--color-green-100);
|
||||||
|
--color-success-foreground: var(--color-green-800);
|
||||||
|
--color-info: var(--color-blue-100);
|
||||||
|
--color-info-foreground: var(--color-blue-800);
|
||||||
|
|
||||||
|
--color-resize: var(--color-blue-300);
|
||||||
|
|
||||||
@mixin light {
|
@mixin light {
|
||||||
--mantine-color-body: rgb(250 250 250);
|
--mantine-color-body: var(--color-zinc-50);
|
||||||
}
|
}
|
||||||
@mixin dark {
|
@mixin dark {
|
||||||
--mantine-color-body: rgb(9 9 11);
|
--mantine-color-body: var(--color-zinc-950);
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
@@ -26,11 +37,20 @@
|
|||||||
.dark .bknd-admin /* currently used for elements, drop after making headless */,
|
.dark .bknd-admin /* currently used for elements, drop after making headless */,
|
||||||
#bknd-admin.dark,
|
#bknd-admin.dark,
|
||||||
.bknd-admin.dark {
|
.bknd-admin.dark {
|
||||||
--color-primary: rgb(250 250 250); /* zinc-50 */
|
--color-primary: var(--color-zinc-50);
|
||||||
--color-background: rgb(30 31 34);
|
--color-background: rgb(30 31 34);
|
||||||
--color-muted: rgb(47 47 52);
|
--color-muted: rgb(47 47 52);
|
||||||
--color-darkest: rgb(255 255 255); /* white */
|
--color-darkest: var(--color-white);
|
||||||
--color-lightest: rgb(24 24 27); /* black */
|
--color-lightest: rgb(24 24 27);
|
||||||
|
|
||||||
|
--color-warning: var(--color-yellow-900);
|
||||||
|
--color-warning-foreground: var(--color-yellow-200);
|
||||||
|
--color-error: var(--color-red-950);
|
||||||
|
--color-error-foreground: var(--color-red-200);
|
||||||
|
--color-success: var(--color-green-950);
|
||||||
|
--color-success-foreground: var(--color-green-200);
|
||||||
|
--color-info: var(--color-blue-950);
|
||||||
|
--color-info-foreground: var(--color-blue-200);
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
@@ -39,6 +59,15 @@
|
|||||||
--color-muted: var(--color-muted);
|
--color-muted: var(--color-muted);
|
||||||
--color-darkest: var(--color-darkest);
|
--color-darkest: var(--color-darkest);
|
||||||
--color-lightest: var(--color-lightest);
|
--color-lightest: var(--color-lightest);
|
||||||
|
|
||||||
|
--color-warning: var(--color-warning);
|
||||||
|
--color-warning-foreground: var(--color-warning-foreground);
|
||||||
|
--color-error: var(--color-error);
|
||||||
|
--color-error-foreground: var(--color-error-foreground);
|
||||||
|
--color-success: var(--color-success);
|
||||||
|
--color-success-foreground: var(--color-success-foreground);
|
||||||
|
--color-info: var(--color-info);
|
||||||
|
--color-info-foreground: var(--color-info-foreground);
|
||||||
}
|
}
|
||||||
|
|
||||||
#bknd-admin {
|
#bknd-admin {
|
||||||
|
|||||||
@@ -85,8 +85,6 @@ export function DataSchemaCanvas() {
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
console.log("-", data, { nodes, edges });
|
|
||||||
|
|
||||||
const nodeLayout = layoutWithDagre({
|
const nodeLayout = layoutWithDagre({
|
||||||
nodes: nodes.map((n) => ({
|
nodes: nodes.map((n) => ({
|
||||||
id: n.id,
|
id: n.id,
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ function AuthSettingsInternal() {
|
|||||||
name="jwt.secret"
|
name="jwt.secret"
|
||||||
description="The secret used to sign the JWT token. If not set, a random key will be generated after enabling authentication."
|
description="The secret used to sign the JWT token. If not set, a random key will be generated after enabling authentication."
|
||||||
advanced="jwt"
|
advanced="jwt"
|
||||||
|
inputProps={{ type: "password" }}
|
||||||
/>
|
/>
|
||||||
<AuthField name="jwt.alg" advanced="jwt" />
|
<AuthField name="jwt.alg" advanced="jwt" />
|
||||||
<AuthField name="jwt.expires" advanced="jwt" />
|
<AuthField name="jwt.expires" advanced="jwt" />
|
||||||
|
|||||||
23
app/src/ui/store/appshell.ts
Normal file
23
app/src/ui/store/appshell.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
import { combine, persist } from "zustand/middleware";
|
||||||
|
|
||||||
|
export const appShellStore = create(
|
||||||
|
persist(
|
||||||
|
combine(
|
||||||
|
{
|
||||||
|
sidebarOpen: false as boolean,
|
||||||
|
sidebarWidth: 350 as number,
|
||||||
|
},
|
||||||
|
(set) => ({
|
||||||
|
toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
|
||||||
|
closeSidebar: () => set({ sidebarOpen: false }),
|
||||||
|
openSidebar: () => set({ sidebarOpen: true }),
|
||||||
|
setSidebarWidth: (width: number) => set({ sidebarWidth: width }),
|
||||||
|
resetSidebarWidth: () => set({ sidebarWidth: 350 }),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
{
|
||||||
|
name: "appshell",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
1
app/src/ui/store/index.ts
Normal file
1
app/src/ui/store/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { appShellStore } from "./appshell";
|
||||||
0
app/src/ui/store/utils.ts
Normal file
0
app/src/ui/store/utils.ts
Normal file
@@ -1,14 +1,15 @@
|
|||||||
import { createContext, lazy, useEffect, useState, Suspense, Fragment } from "react";
|
import { lazy, Suspense, useEffect, useState } from "react";
|
||||||
import { App } from "bknd";
|
import { App } from "bknd";
|
||||||
import { checksum, secureRandomString } from "bknd/utils";
|
import { checksum } from "bknd/utils";
|
||||||
import { boolean, em, entity, text } from "bknd/data";
|
import { boolean, em, entity, text } from "bknd/data";
|
||||||
import { SQLocalConnection } from "@bknd/sqlocal";
|
import { SQLocalConnection } from "@bknd/sqlocal";
|
||||||
import { Route, Router, Switch } from "wouter";
|
import { Route, Router, Switch } from "wouter";
|
||||||
import IndexPage from "~/routes/_index";
|
import IndexPage from "~/routes/_index";
|
||||||
const Admin = lazy(() => import("~/routes/admin"));
|
|
||||||
import { Center } from "~/components/Center";
|
import { Center } from "~/components/Center";
|
||||||
import { ClientProvider } from "bknd/client";
|
import { ClientProvider } from "bknd/client";
|
||||||
|
|
||||||
|
const Admin = lazy(() => import("~/routes/admin"));
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
const [app, setApp] = useState<App | undefined>(undefined);
|
const [app, setApp] = useState<App | undefined>(undefined);
|
||||||
const [hash, setHash] = useState<string>("");
|
const [hash, setHash] = useState<string>("");
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import { Center } from "~/components/Center";
|
import { Center } from "~/components/Center";
|
||||||
import type { App } from "bknd";
|
import type { App } from "bknd";
|
||||||
import { useEntityQuery } from "bknd/client";
|
import { useEntityQuery } from "bknd/client";
|
||||||
|
import type { SQLocalConnection } from "@bknd/sqlocal/src";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export default function IndexPage({ app }: { app: App }) {
|
export default function IndexPage({ app }: { app: App }) {
|
||||||
const user = app.getApi().getUser();
|
//const user = app.getApi().getUser();
|
||||||
const limit = 5;
|
const limit = 5;
|
||||||
const { data: todos, ...$q } = useEntityQuery("todos", undefined, {
|
const { data: todos, ...$q } = useEntityQuery("todos", undefined, {
|
||||||
limit,
|
limit,
|
||||||
|
sort: "-id",
|
||||||
});
|
});
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const total = todos?.body.meta.total || 0;
|
const total = todos?.body.meta.total || 0;
|
||||||
@@ -29,7 +32,8 @@ export default function IndexPage({ app }: { app: App }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
{todos?.reverse().map((todo) => (
|
{todos &&
|
||||||
|
[...todos].reverse().map((todo) => (
|
||||||
<div className="flex flex-row" key={String(todo.id)}>
|
<div className="flex flex-row" key={String(todo.id)}>
|
||||||
<div className="flex flex-row flex-grow items-center gap-3 ml-1">
|
<div className="flex flex-row flex-grow items-center gap-3 ml-1">
|
||||||
<input
|
<input
|
||||||
@@ -87,6 +91,49 @@ export default function IndexPage({ app }: { app: App }) {
|
|||||||
)}
|
)}
|
||||||
</div>*/}
|
</div>*/}
|
||||||
</div>
|
</div>
|
||||||
|
<Debug app={app} />
|
||||||
</Center>
|
</Center>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Debug({ app }: { app: App }) {
|
||||||
|
const [info, setInfo] = useState<any>();
|
||||||
|
const connection = app.em.connection as SQLocalConnection;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
setInfo(await connection.client.getDatabaseInfo());
|
||||||
|
app.emgr.onAny(
|
||||||
|
async () => {
|
||||||
|
setInfo(await connection.client.getDatabaseInfo());
|
||||||
|
},
|
||||||
|
{ mode: "sync", id: "debug" },
|
||||||
|
);
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function download() {
|
||||||
|
const databaseFile = await connection.client.getDatabaseFile();
|
||||||
|
const fileUrl = URL.createObjectURL(databaseFile);
|
||||||
|
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = fileUrl;
|
||||||
|
a.download = "database.sqlite3";
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
|
||||||
|
URL.revokeObjectURL(fileUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2 items-center">
|
||||||
|
<button
|
||||||
|
className="bg-foreground/20 leading-none py-2 px-3.5 rounded-lg text-sm hover:bg-foreground/30 transition-colors cursor-pointer"
|
||||||
|
onClick={download}
|
||||||
|
>
|
||||||
|
Download Database
|
||||||
|
</button>
|
||||||
|
<pre className="text-xs">{JSON.stringify(info, null, 2)}</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user