Compare commits

...

7 Commits

Author SHA1 Message Date
Trisha Lim
facde10b6d set inspector widget height 2025-03-18 16:25:20 +07:00
Trisha Lim
f84a8e9ffb fix button exports 2025-03-18 16:22:17 +07:00
Trisha Lim
122e7e2b15 clean up 2025-03-18 16:21:39 +07:00
Trisha Lim
031276fe71 fit more data onscreen 2025-03-18 16:21:37 +07:00
Trisha Lim
8ee43423d6 set default cursor 2025-03-18 16:21:16 +07:00
Trisha Lim
6d8e3b6e1e use Input component for all inputs 2025-03-18 16:21:16 +07:00
Trisha Lim
d662fddcf5 use Button element for all buttons 2025-03-18 16:21:14 +07:00
11 changed files with 270 additions and 96 deletions

View File

@@ -0,0 +1,65 @@
import { clsx } from "clsx";
import { forwardRef } from "react";
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary" | "tertiary" | "destructive" | "plain";
size?: "sm" | "md" | "lg";
children?: React.ReactNode;
className?: string;
disabled?: boolean;
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{
className,
children,
size = "md",
variant = "primary",
disabled,
type = "button",
...buttonProps
},
ref,
) => {
const sizeClasses = {
sm: "text-sm py-1 px-2",
md: "py-1.5 px-3",
lg: "md:text-lg py-2 px-3 md:px-8 md:py-3",
};
const variantClasses = {
primary:
"bg-blue border-blue text-white font-medium bg-blue hover:bg-blue-800 hover:border-blue-800",
secondary:
"text-stone-900 border font-medium hover:border-stone-300 hover:dark:border-stone-700 dark:text-white",
tertiary: "text-blue underline underline-offset-4",
destructive:
"bg-red-600 border-red-600 text-white font-medium hover:bg-red-700 hover:border-red-700",
};
const classNames =
variant === "plain"
? clsx(className, "text-left")
: clsx(
className,
"inline-flex items-center justify-center gap-2 rounded-lg text-center transition-colors",
"disabled:pointer-events-none disabled:opacity-70",
sizeClasses[size],
variantClasses[variant],
disabled && "opacity-50 cursor-not-allowed pointer-events-none",
);
return (
<button
ref={ref}
{...buttonProps}
disabled={disabled}
className={classNames}
type={type}
>
{children}
</button>
);
},
);

View File

@@ -1,8 +1,4 @@
import { install, tw } from "@twind/core";
import { install } from "@twind/core";
import config from "./twind.config";
// Install Twind globally
install(config);
// Export the tw function for use in components
export { tw };

View File

@@ -1,4 +1,5 @@
import React from "react";
import { Button } from "./button.js";
import { PageInfo } from "./types.js";
interface BreadcrumbsProps {
@@ -11,14 +12,10 @@ export const Breadcrumbs: React.FC<BreadcrumbsProps> = ({
onBreadcrumbClick,
}) => {
return (
<div className="relative z-20 bg-blue-400/10 backdrop-blur-sm rounded-lg inline-flex px-2 py-1 whitespace-pre transition-all items-center gap-1 min-h-[2.5rem]">
<button
onClick={() => onBreadcrumbClick(-1)}
className="flex items-center justify-center p-1 rounded-sm transition-colors"
aria-label="Go to home"
>
Start
</button>
<div className="relative z-20 flex-1 flex gap-2 items-center">
<Button variant="plain" onClick={() => onBreadcrumbClick(-1)}>
Home
</Button>
{path.map((page, index) => {
return (
<span
@@ -30,12 +27,9 @@ export const Breadcrumbs: React.FC<BreadcrumbsProps> = ({
{index === 0 ? null : (
<span className="text-blue-600/30">{" / "}</span>
)}
<button
onClick={() => onBreadcrumbClick(index)}
className="text-blue hover:underline dark:text-blue-400"
>
<Button variant="tertiary" onClick={() => onBreadcrumbClick(index)}>
{index === 0 ? page.name || "Root" : page.name}
</button>
</Button>
</span>
);
})}

View File

@@ -0,0 +1,65 @@
import { clsx } from "clsx";
import { forwardRef } from "react";
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary" | "tertiary" | "destructive" | "plain";
size?: "sm" | "md" | "lg";
children?: React.ReactNode;
className?: string;
disabled?: boolean;
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{
className,
children,
size = "md",
variant = "primary",
disabled,
type = "button",
...buttonProps
},
ref,
) => {
const sizeClasses = {
sm: "text-sm py-1 px-2",
md: "py-1.5 px-3",
lg: "md:text-lg py-2 px-3 md:px-8 md:py-3",
};
const variantClasses = {
primary:
"bg-blue border-blue text-white font-medium bg-blue hover:bg-blue-800 hover:border-blue-800",
secondary:
"text-stone-900 border font-medium hover:border-stone-300 hover:dark:border-stone-700 dark:text-white",
tertiary: "text-blue underline underline-offset-4",
destructive:
"bg-red-600 border-red-600 text-white font-medium hover:bg-red-700 hover:border-red-700",
};
const classNames =
variant === "plain"
? className
: clsx(
className,
"inline-flex items-center justify-center gap-2 rounded-lg text-center transition-colors",
"disabled:pointer-events-none disabled:opacity-70",
sizeClasses[size],
variantClasses[variant],
disabled && "opacity-50 cursor-not-allowed pointer-events-none",
);
return (
<button
ref={ref}
{...buttonProps}
disabled={disabled}
className={classNames}
type={type}
>
{children}
</button>
);
},
);

View File

@@ -9,6 +9,7 @@ import { base64URLtoBytes } from "cojson";
import { BinaryStreamItem, BinaryStreamStart, CoStreamItem } from "cojson";
import type { JsonObject, JsonValue } from "cojson";
import { useEffect, useState } from "react";
import { Button } from "./button.js";
import { PageInfo } from "./types.js";
import { AccountOrGroupPreview } from "./value-renderer.js";
@@ -146,10 +147,10 @@ const BinaryDownloadButton = ({
};
return (
<button onClick={downloadFile}>
<Button variant="secondary" onClick={downloadFile}>
{label}
{/* Download {mimeType === "application/pdf" ? "PDF" : "File"} */}
</button>
</Button>
);
};
@@ -243,7 +244,7 @@ function RenderCoBinaryStream({
label={
mimeType === "application/pdf"
? "Download PDF"
: "Download File"
: "Download file"
}
/>
}

View File

@@ -1,5 +1,6 @@
import { CoID, LocalNode, RawCoValue } from "cojson";
import { JsonObject } from "cojson";
import { Button } from "./button.js";
import { ResolveIcon } from "./type-icon.js";
import { PageInfo, isCoId } from "./types.js";
import { CoMapPreview, ValueRenderer } from "./value-renderer.js";
@@ -16,14 +17,15 @@ export function GridView({
const entries = Object.entries(data);
return (
<div className="grid grid-cols-1 gap-4 p-2">
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
{entries.map(([key, child], childIndex) => (
<div
<Button
variant="plain"
key={childIndex}
className={`p-3 rounded-lg overflow-hidden transition-colors ${
className={`p-3 text-left rounded-lg overflow-hidden transition-colors ${
isCoId(child)
? " border border-gray-200 cursor-pointer shadow-sm hover:bg-gray-100/5"
: "bg-gray-50 dark:bg-gray-925"
? "border border-gray-200 shadow-sm hover:bg-gray-100/5"
: "bg-gray-50 dark:bg-gray-925 cursor-default"
}`}
onClick={() =>
isCoId(child) &&
@@ -56,7 +58,7 @@ export function GridView({
/>
)}
</div>
</div>
</Button>
))}
</div>
);

View File

@@ -0,0 +1,40 @@
import { clsx } from "clsx";
import { forwardRef, useId } from "react";
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
// label can be hidden with a "label:sr-only" className
label: string;
className?: string;
id?: string;
hideLabel?: boolean;
}
export const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, className, hideLabel, id: customId, ...inputProps }, ref) => {
const generatedId = useId();
const id = customId || generatedId;
const inputClassName = clsx(
"w-full rounded-md border px-3.5 py-2 shadow-sm",
"font-medium text-stone-900",
"dark:text-white dark:bg-stone-925",
);
const containerClassName = clsx("grid gap-1", className);
return (
<div className={containerClassName}>
<label
htmlFor={id}
className={clsx(
"text-stone-600 dark:text-stone-300",
hideLabel && "sr-only",
)}
>
{label}
</label>
<input ref={ref} {...inputProps} id={id} className={inputClassName} />
</div>
);
},
);

View File

@@ -2,6 +2,8 @@ import { CoID, RawCoValue } from "cojson";
import { useAccount } from "jazz-react-core";
import React, { useState } from "react";
import { Breadcrumbs } from "./breadcrumbs.js";
import { Button } from "./button.js";
import { Input } from "./input.js";
import { PageStack } from "./page-stack.js";
import { usePagePath } from "./use-page-path.js";
@@ -42,9 +44,11 @@ export function JazzInspector({ position = "right" }: { position?: Position }) {
if (!open) {
return (
<button
<Button
variant="secondary"
size="sm"
onClick={() => setOpen(true)}
className={`fixed w-10 h-10 inline-block bottom-0 right-0 m-4 bg-white border rounded-md shadow-md p-1.5 ${positionClasses[position]}`}
className={`fixed w-10 h-10 bg-white shadow-sm bottom-0 right-0 m-4 p-1.5 ${positionClasses[position]}`}
>
<svg
className="w-full h-auto relative -left-px text-blue"
@@ -62,35 +66,29 @@ export function JazzInspector({ position = "right" }: { position?: Position }) {
/>
</svg>
<span className="sr-only">Open Jazz Inspector</span>
</button>
</Button>
);
}
return (
<div className="fixed h-[50vh] flex flex-col bottom-0 left-0 w-full bg-white border-t border-gray-200 p-4 dark:border-stone-900 dark:bg-stone-925">
<div className="fixed h-[calc(100%-12rem)] flex flex-col bottom-0 left-0 w-full bg-white border-t border-gray-200 p-4 dark:border-stone-900 dark:bg-stone-925">
<div className="flex items-center gap-4 mb-4">
<Breadcrumbs path={path} onBreadcrumbClick={goToIndex} />
<div className="flex-1">
<form onSubmit={handleCoValueIdSubmit}>
{path.length !== 0 && (
<input
className="border p-2 rounded-lg min-w-[21rem] font-mono"
placeholder="co_z1234567890abcdef123456789"
value={coValueId}
onChange={(e) =>
setCoValueId(e.target.value as CoID<RawCoValue>)
}
/>
)}
</form>
</div>
<button
className="ml-auto"
type="button"
onClick={() => setOpen(false)}
>
<form onSubmit={handleCoValueIdSubmit} className="w-[21rem]">
{path.length !== 0 && (
<Input
label="CoValue ID"
className="font-mono"
hideLabel
placeholder="co_z1234567890abcdef123456789"
value={coValueId}
onChange={(e) => setCoValueId(e.target.value as CoID<RawCoValue>)}
/>
)}
</form>
<Button variant="plain" type="button" onClick={() => setOpen(false)}>
Close
</button>
</Button>
</div>
<PageStack
@@ -108,33 +106,33 @@ export function JazzInspector({ position = "right" }: { position?: Position }) {
: "opacity-100"
}`}
>
<fieldset className="flex flex-col gap-2 text-sm">
<h2 className="text-lg font-medium mb-4 text-stone-900 dark:text-white">
<fieldset className="flex flex-col gap-2">
<h2 className="text-lg text-center font-medium mb-4 text-stone-900 dark:text-white">
Jazz CoValue Inspector
</h2>
<input
className="border border-gray-200 p-4 rounded-lg min-w-[21rem] font-mono"
<Input
label="CoValue ID"
className="min-w-[21rem] font-mono"
hideLabel
placeholder="co_z1234567890abcdef123456789"
value={coValueId}
onChange={(e) => setCoValueId(e.target.value as CoID<RawCoValue>)}
/>
<button
type="submit"
className="bg-blue text-white py-2 px-4 rounded-md hover:bg-blue-800"
>
Inspect
</button>
<hr />
<button
type="button"
className="border border-gray-200 inline-block py-1.5 px-2 rounded-md"
<Button type="submit" variant="primary">
Inspect CoValue
</Button>
<p className="text-center">or</p>
<Button
variant="secondary"
onClick={() => {
setCoValueId(me._raw.id);
setPage(me._raw.id);
}}
>
Inspect My Account
</button>
Inspect my account
</Button>
</fieldset>
</form>
</PageStack>

View File

@@ -56,8 +56,7 @@ export function Page({
<div
style={style}
className={
className +
" absolute z-10 inset-0 border rounded-xl shadow-lg p-6 w-full h-full bg-clip-padding"
className + " absolute z-10 inset-0 w-full h-full bg-clip-padding"
}
>
{!isTopLevel && (
@@ -71,7 +70,7 @@ export function Page({
></div>
)}
<div className="flex justify-between items-center mb-4">
<div className="flex flex-col gap-2">
<div className="flex items-center gap-3">
<h2 className="text-2xl font-bold flex flex-col items-start gap-1">
<span>
{name}
@@ -83,14 +82,12 @@ export function Page({
) : null}
</span>
</h2>
<div className="flex items-center gap-2">
<span className="text-xs text-gray-700 font-medium py-0.5 px-1 -ml-0.5 rounded bg-gray-700/5 inline-block font-mono">
{type && <TypeIcon type={type} extendedType={extendedType} />}
</span>
<span className="text-xs text-gray-700 font-medium py-0.5 px-1 -ml-0.5 rounded bg-gray-700/5 inline-block font-mono">
{coId}
</span>
</div>
<span className="text-xs text-gray-700 font-medium py-0.5 px-1 -ml-0.5 rounded bg-gray-700/5 inline-block font-mono">
{type && <TypeIcon type={type} extendedType={extendedType} />}
</span>
<span className="text-xs text-gray-700 font-medium py-0.5 px-1 -ml-0.5 rounded bg-gray-700/5 inline-block font-mono">
{coId}
</span>
</div>
</div>
<div className="overflow-auto max-h-[calc(100%-4rem)]">

View File

@@ -2,6 +2,7 @@ import { CoID, LocalNode, RawCoValue } from "cojson";
import type { JsonObject } from "cojson";
import { useMemo, useState } from "react";
import { LinkIcon } from "../link-icon.js";
import { Button } from "./button.js";
import { PageInfo } from "./types.js";
import { useResolvedCoValues } from "./use-resolve-covalue.js";
import { ValueRenderer } from "./value-renderer.js";
@@ -68,7 +69,8 @@ export function TableView({
{resolvedRows.slice(0, visibleRowsCount).map((item, index) => (
<tr key={index}>
<td className="p-1">
<button
<Button
variant="tertiary"
onClick={() =>
onNavigate([
{
@@ -77,10 +79,9 @@ export function TableView({
},
])
}
className="p-4 whitespace-nowrap text-sm text-gray-500 rounded hover:bg-gray-100 hover:text-blue-500"
>
<LinkIcon />
</button>
</Button>
</td>
{keys.map((key) => (
<td
@@ -119,12 +120,13 @@ export function TableView({
</span>
{hasMore && (
<div className="text-center">
<button
<Button
variant="plain"
onClick={loadMore}
className="px-4 py-2 bg-blue text-white rounded hover:bg-blue-800"
>
Load more
</button>
</Button>
</div>
)}
</div>

View File

@@ -2,6 +2,7 @@ import clsx from "clsx";
import { CoID, JsonValue, LocalNode, RawCoValue } from "cojson";
import React, { useEffect, useState } from "react";
import { LinkIcon } from "../link-icon.js";
import { Button } from "./button.js";
import {
isBrowserImage,
resolveCoValue,
@@ -29,20 +30,32 @@ export function ValueRenderer({
}
if (typeof json === "string" && json.startsWith("co_")) {
return (
<span
className={clsx(
"inline-flex gap-1 items-center",
onCoIDClick && "text-blue-500 cursor-pointer hover:underline",
)}
onClick={() => {
onCoIDClick?.(json as CoID<RawCoValue>);
}}
>
const linkClasses = onCoIDClick
? "text-blue cursor-pointer inline-flex gap-1 items-center dark:text-blue-400"
: "inline-flex gap-1 items-center";
const content = (
<>
{json}
{onCoIDClick && <LinkIcon />}
</span>
</>
);
if (onCoIDClick) {
return (
<Button
className={linkClasses}
onClick={() => {
onCoIDClick?.(json as CoID<RawCoValue>);
}}
variant="plain"
>
{content}
</Button>
);
}
return <span className={linkClasses}>{content}</span>;
}
if (typeof json === "string") {
@@ -101,12 +114,13 @@ export function ValueRenderer({
.slice(0, 3)
.join("\n") + (Object.keys(json).length > 2 ? "\n..." : "")}
</pre>
<button
<Button
variant="plain"
onClick={() => setIsExpanded(!isExpanded)}
className="text-xs text-gray-500 hover:text-gray-700"
>
{isExpanded ? "Show less" : "Show more"}
</button>
</Button>
</span>
) : (
<pre className="whitespace-pre-wrap">