Compare commits

...

32 Commits

Author SHA1 Message Date
Trisha Lim
d746ce8c8d collapse upgrade guides on side nav 2025-03-25 18:11:15 +07:00
Benjamin S. Leveritt
297a2dd92d Fix depth indentation 2025-03-24 08:49:52 +00:00
Trisha Lim
fd17ecb2b0 make framework selector sticky, add gradient to bottom 2025-03-24 08:49:52 +00:00
Trisha Lim
7cd0bd7ebc reduce spacing in TOC to match left nav 2025-03-24 08:49:52 +00:00
Anselm Eickhoff
4263c30753 Merge pull request #1710 from garden-co/feat/hend-showcase
add Hend to showcase
2025-03-21 13:39:35 +00:00
Anselm Eickhoff
416dd79b50 Merge pull request #1712 from garden-co/chore/inspector-changset
add changeset
2025-03-21 13:39:17 +00:00
Trisha Lim
11da4d1366 add changeset 2025-03-21 20:34:24 +07:00
Trisha Lim
6e794c3fed isolate class name hashing to jazz-inspector
Co-authored-by: Giordano Ricci <me@giordanoricci.com>
2025-03-21 20:31:59 +07:00
Trisha Lim
080a718c4d add Hend to showcase 2025-03-21 18:19:40 +07:00
Anselm Eickhoff
e5891f77ca Merge pull request #1703 from garden-co/improvement/docs-nav
Docs nav improvements
2025-03-20 11:20:39 +00:00
Trisha Lim
e141c8779b move AI tools and Inspector in its own section 2025-03-20 18:06:56 +07:00
Trisha Lim
868c49d096 reduce spacing between docs nav items 2025-03-20 18:04:24 +07:00
Trisha Lim
bdc78c55fc remove "coming soon" legend on docs nav 2025-03-20 17:41:46 +07:00
Benjamin S. Leveritt
2dbe990c22 Merge pull request #1688 from garden-co/feat/inspector-json
inspector: collapse JSON field, show content of JSON arrays, UI improvements
2025-03-19 20:29:05 +00:00
James Vickery
60b8dbc33c Merge pull request #1698 from garden-co/jmsv/rss-fix-url
Set canonical URL and fix RSS URLs
2025-03-19 17:31:08 +00:00
James Vickery
5337edf717 fix lint 2025-03-19 17:27:28 +00:00
James Vickery
fe60a88de9 Merge branch 'main' of github.com:garden-co/jazz into jmsv/rss-fix-url 2025-03-19 17:25:57 +00:00
James Vickery
939e1d7a3c fix rss urls and set canonical url 2025-03-19 17:24:20 +00:00
James Vickery
19871690e4 Merge pull request #1696 from garden-co/jmsv/rss
RSS
2025-03-19 16:59:20 +00:00
James Vickery
064900d5f9 rss 2025-03-19 16:49:48 +00:00
Trisha Lim
8e44caaebc install lucide-react on jazz-inspector 2025-03-19 13:56:07 +07:00
Trisha Lim
c95f344c41 remove covalue inspector form from dom if not shown 2025-03-19 13:44:59 +07:00
Trisha Lim
7320adc58d set input length 2025-03-19 13:18:55 +07:00
Trisha Lim
57a92f4aa0 missing key 2025-03-19 13:06:29 +07:00
Trisha Lim
c9b2b01928 move ui components to separate dir 2025-03-19 13:03:01 +07:00
Trisha Lim
5b97ac3b92 account selector UI improvement 2025-03-19 12:53:11 +07:00
Trisha Lim
09f0a98eef add changeset 2025-03-19 12:35:19 +07:00
Trisha Lim
3d8babdbb6 "add account" form improvement 2025-03-19 12:31:50 +07:00
Trisha Lim
f10004c6bc UI fixes 2025-03-19 12:20:39 +07:00
Trisha Lim
33ecca3d10 spacing and typography improvements 2025-03-19 12:00:47 +07:00
Trisha Lim
bc601d809b show array contents in a co.json 2025-03-19 11:13:35 +07:00
Anselm Eickhoff
cc8462f071 Merge pull request #1687 from garden-co/copy/meta-tags
update meta tags to align with new copy
2025-03-18 15:46:49 +00:00
40 changed files with 711 additions and 372 deletions

View File

@@ -0,0 +1,6 @@
---
"jazz-inspector": patch
"jazz-inspector-app": patch
---
UI and JSON display improvements

View File

@@ -0,0 +1,6 @@
---
"jazz-inspector-app": patch
"jazz-inspector": patch
---
isolate class name hashing on inspector

View File

@@ -1,65 +0,0 @@
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

@@ -9,8 +9,14 @@ import {
} from "cojson";
import { createWebSocketPeer } from "cojson-transport-ws";
import { WasmCrypto } from "cojson/crypto/WasmCrypto";
import { Breadcrumbs, PageStack } from "jazz-inspector";
import { Trash2 } from "lucide-react";
import {
Breadcrumbs,
Button,
Icon,
Input,
PageStack,
Select,
} from "jazz-inspector";
import React, { useState, useEffect } from "react";
import { usePagePath } from "./use-page-path";
import { resolveCoValue, useResolvedCoValue } from "./use-resolve-covalue";
@@ -120,15 +126,23 @@ export default function CoJsonViewerApp() {
}
return (
<div className="w-full h-screen bg-white p-4 overflow-hidden flex flex-col">
<div className="flex items-center mb-4 gap-4">
<div
className={clsx(
"h-screen overflow-hidden flex flex-col",
" text-stone-700 bg-white",
"dark:text-stone-300 dark:bg-stone-950",
)}
>
<header className="flex items-center gap-4 p-3">
<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"
<Input
className="min-w-[21rem] font-mono"
placeholder="co_z1234567890abcdef123456789"
label="CoValue ID"
hideLabel
value={coValueId}
onChange={(e) =>
setCoValueId(e.target.value as CoID<RawCoValue>)
@@ -144,7 +158,7 @@ export default function CoJsonViewerApp() {
deleteCurrentAccount={deleteCurrentAccount}
localNode={localNode}
/>
</div>
</header>
<PageStack
path={path}
@@ -152,49 +166,39 @@ export default function CoJsonViewerApp() {
goBack={goBack}
addPages={addPages}
>
{!currentAccount ? (
<AddAccountForm addAccount={addAccount} />
) : (
{!currentAccount && <AddAccountForm addAccount={addAccount} />}
{currentAccount && path.length <= 0 && (
<form
onSubmit={handleCoValueIdSubmit}
aria-hidden={path.length !== 0}
className={clsx(
"flex flex-col justify-center items-center gap-2 h-full w-full mb-20 ",
"transition-all duration-150",
path.length > 0
? "opacity-0 -translate-y-2 scale-95"
: "opacity-100",
)}
className="flex flex-col relative -top-6 justify-center gap-2 h-full w-full max-w-sm mx-auto"
>
<fieldset className="flex flex-col gap-2 text-sm">
<h2 className="text-3xl font-medium text-gray-950 text-center mb-4">
Jazz CoValue Inspector
</h2>
<input
className="border p-4 rounded-lg min-w-[21rem] font-mono"
placeholder="co_z1234567890abcdef123456789"
value={coValueId}
onChange={(e) =>
setCoValueId(e.target.value as CoID<RawCoValue>)
}
/>
<button
type="submit"
className="bg-indigo-500 hover:bg-indigo-500/80 text-white px-4 py-2 rounded-md"
>
Inspect
</button>
<hr />
<button
type="button"
className="border inline-block px-2 py-1.5 text-black rounded"
onClick={() => {
setPage(currentAccount.id);
}}
>
Inspect My Account
</button>
</fieldset>
<h1 className="text-lg text-center font-medium mb-4 text-stone-900 dark:text-white">
Jazz CoValue Inspector
</h1>
<Input
label="CoValue ID"
className="font-mono"
hideLabel
placeholder="co_z1234567890abcdef123456789"
value={coValueId}
onChange={(e) => setCoValueId(e.target.value as CoID<RawCoValue>)}
/>
<Button type="submit" variant="primary">
Inspect CoValue
</Button>
<p className="text-center">or</p>
<Button
variant="secondary"
onClick={() => {
setPage(currentAccount.id);
}}
>
Inspect my account
</Button>
</form>
)}
</PageStack>
@@ -216,8 +220,11 @@ function AccountSwitcher({
localNode: LocalNode | null;
}) {
return (
<div className="relative flex items-center gap-1">
<select
<div className="relative flex items-stretch gap-1">
<Select
label="Account to inspect"
hideLabel
className="label:sr-only max-w-96"
value={currentAccount?.id || "add-account"}
onChange={(e) => {
if (e.target.value === "add-account") {
@@ -227,7 +234,6 @@ function AccountSwitcher({
setCurrentAccount(account || null);
}
}}
className="p-2 px-4 bg-gray-100/50 border border-indigo-500/10 backdrop-blur-sm rounded-md text-indigo-700 appearance-none"
>
{accounts.map((account) => (
<option key={account.id} value={account.id}>
@@ -239,15 +245,16 @@ function AccountSwitcher({
</option>
))}
<option value="add-account">Add account</option>
</select>
</Select>
{currentAccount && (
<button
<Button
variant="secondary"
onClick={deleteCurrentAccount}
className="p-3 rounded hover:bg-gray-200 transition-colors"
title="Delete Account"
className="rounded-md p-2 ml-1"
aria-label="Remove account"
>
<Trash2 size={16} className="text-gray-500" />
</button>
<Icon name="delete" className="text-gray-500" />
</Button>
)}
</div>
);
@@ -271,30 +278,34 @@ function AddAccountForm({
return (
<form
onSubmit={handleSubmit}
className="flex flex-col gap-2 max-w-md mx-auto h-full justify-center"
className="flex flex-col gap-3 max-w-md mx-auto h-full justify-center"
>
<h2 className="text-2xl font-medium text-gray-900 mb-3">
Add an Account to Inspect
<h2 className="text-2xl font-medium text-gray-900 dark:text-white">
Add an account to inspect
</h2>
<input
className="border py-2 px-3 rounded-md"
placeholder="Account ID"
<p className="leading-relaxed mb-5">
Use the{" "}
<code className="whitespace-nowrap text-stone-900 dark:text-white font-semibold">
jazz-logged-in-secret
</code>{" "}
local storage key from within your Jazz app for your account
credentials.
</p>
<Input
label="Account ID"
value={id}
placeholder="co_z1234567890abcdef123456789"
onChange={(e) => setId(e.target.value)}
/>
<input
<Input
label="Account secret"
type="password"
className="border py-2 px-3 rounded-md"
placeholder="Account Secret"
value={secret}
onChange={(e) => setSecret(e.target.value)}
/>
<button
type="submit"
className="bg-indigo-500 text-white px-4 py-2 rounded-md"
>
Add Account
</button>
<Button className="mt-3" type="submit">
Add account
</Button>
</form>
);
}

View File

@@ -1,4 +1,5 @@
import type { Config } from "tailwindcss";
import plugin from "tailwindcss/plugin";
const stonePalette = {
50: "oklch(0.988281 0.002 75)",
@@ -61,6 +62,7 @@ const config: Config = {
},
},
},
plugins: [plugin(({ addVariant }) => addVariant("label", "& :is(label)"))],
};
export default config;

View File

@@ -0,0 +1,37 @@
import { metaTags } from "@/app/layout";
import { posts } from "@/lib/posts";
import { Feed } from "feed";
import { NextResponse } from "next/server";
export async function GET() {
const feed = new Feed({
title: "Garden Computing Blog",
description: "News from Garden Computing",
id: metaTags.url,
link: `${metaTags.url}/news`,
language: "en",
image: `${metaTags.url}/social-image.png`,
favicon: `${metaTags.url}/favicon.ico`,
copyright: `${new Date().getFullYear()} Garden Computing, Inc.`,
});
posts.forEach((post) => {
feed.addItem({
title: post.meta.title,
description: post.meta.subtitle,
id: post.meta.slug,
link: `${metaTags.url}/news/${post.meta.slug}`,
date: new Date(post.meta.date),
author: [{ name: post.meta.author.name }],
guid: post.meta.slug,
image: `${metaTags.url}${post.meta.coverImage}`,
});
});
return new NextResponse(feed.rss2(), {
headers: {
"Content-Type": "application/xml",
"Cache-Control": "public, s-maxage=3600, stale-while-revalidate=86400",
},
});
}

View File

@@ -41,7 +41,7 @@ const commitMono = localFont({
display: "swap",
});
const metaTags = {
export const metaTags = {
title: "garden computing",
description:
"Computers are magic. So why do we put up with so much complexity? We believe just a few new ideas can make all the difference.",
@@ -70,6 +70,16 @@ export const metadata: Metadata = {
},
],
},
alternates: {
canonical: metaTags.url,
types: {
"application/rss+xml": `${
process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}`
: "http://localhost:3000"
}/api/rss`,
},
},
};
export default function RootLayout({

View File

@@ -20,6 +20,7 @@
"@vercel/analytics": "^1.3.1",
"@vercel/speed-insights": "^1.0.12",
"clsx": "^2.1.1",
"feed": "^4.2.2",
"gcmp-design-system": "workspace:*",
"lucide-react": "^0.436.0",
"mdast-util-from-markdown": "^2.0.0",

View File

@@ -10,6 +10,15 @@
:focus-visible {
@apply ring-2 ring-blue/75 dark:ring-blue-400/75;
}
details > summary {
@apply cursor-pointer;
&.list-none::-webkit-details-marker,
&.list-none::marker {
display: none;
}
}
}
@layer components {

View File

@@ -1,7 +1,10 @@
import { SideNavHeader } from "@/components/SideNavHeader";
import { SideNavItem } from "@/components/SideNavItem";
import { Framework } from "@/lib/framework";
import { useFramework } from "@/lib/use-framework";
import { clsx } from "clsx";
import { Icon } from "gcmp-design-system/src/app/components/atoms/Icon";
import { usePathname } from "next/navigation";
import React from "react";
interface SideNavItem {
@@ -13,6 +16,8 @@ interface SideNavItem {
[key in Framework]: number;
};
items?: SideNavItem[];
collapse?: boolean;
prefix?: string;
}
export function SideNav({
items,
@@ -26,51 +31,98 @@ export function SideNav({
footer?: React.ReactNode;
}) {
return (
<div className={clsx(className, "text-sm space-y-5 px-2")}>
<div
className={clsx(
className,
"text-sm h-full pt-3 md:pt-8 flex flex-col gap-4 px-2",
)}
>
{children}
<div className="flex items-center gap-2">
<span className="inline-block size-2 rounded-full bg-yellow-400"></span>{" "}
Documentation coming soon
<div className="flex-1 relative overflow-y-auto px-2 -mx-2">
{items.map((item) => (
<SideNavSection item={item} key={item.name} />
))}
{footer}
<div
aria-hidden
className={clsx(
"h-12 right-0 sticky bottom-0 left-0",
"bg-gradient-to-t from-white to-transparent",
"dark:from-stone-950",
"hidden md:block",
)}
/>
</div>
{items.map(({ name, href, items }) => (
<div key={name}>
<SideNavHeader href={href}>{name}</SideNavHeader>
{items &&
items.map(({ name, href, items, done }) => (
<ul key={name}>
<li>
<SideNavItem href={href}>
{done == 0 && (
<span className="mr-1.5 inline-block size-2 rounded-full bg-yellow-400"></span>
)}
<span
className={
done === 0 ? "text-stone-400 dark:text-stone-600" : ""
}
>
{name}
</span>
</SideNavItem>
</li>
{items && items?.length > 0 && (
<ul className="pl-4">
{items.map(({ name, href }) => (
<li key={href}>
<SideNavItem href={href}>{name}</SideNavItem>
</li>
))}
</ul>
)}
</ul>
))}
</div>
))}
{footer}
</div>
);
}
export function SideNavSection({
item: { name, href, collapse, items, prefix },
}: { item: SideNavItem }) {
const path = usePathname();
const framework = useFramework();
if (!collapse) {
return (
<>
<SideNavHeader href={href}>{name}</SideNavHeader>
<SideNavSectionList items={items} />
</>
);
}
return (
<>
<details
className="group [&:not(:first-child)]:mt-4"
open={
prefix
? path.startsWith(
prefix.replace("/docs/", "/docs/" + framework + "/"),
)
: true
}
>
<summary className="list-none">
<SideNavHeader href={href}>
{name}
{collapse && (
<Icon
className="group-open:rotate-180 transition-transform group-hover:text-stone-500 text-stone-400 dark:text-stone-600"
name="chevronDown"
size="xs"
/>
)}
</SideNavHeader>
</summary>
<SideNavSectionList items={items} />
</details>
</>
);
}
export function SideNavSectionList({ items }: { items?: SideNavItem[] }) {
return (
!!items?.length && (
<ul>
{items.map(({ name, href, items, done }) => (
<li key={name}>
<SideNavItem href={href}>
<span
className={
done === 0 ? "text-stone-400 dark:text-stone-600" : ""
}
>
{name}
</span>
</SideNavItem>
</li>
))}
</ul>
)
);
}

View File

@@ -1,22 +1,26 @@
import { clsx } from "clsx";
import Link from "next/link";
export function SideNavHeader({
href,
children,
className,
}: {
href?: string;
children: React.ReactNode;
className?: string;
}) {
const className =
"block font-medium text-stone-900 py-1 dark:text-white mb-1";
const classes = clsx(
className,
"flex items-center gap-2 justify-between font-medium text-stone-900 py-1 dark:text-white mb-1 [&:not(:first-child)]:mt-4",
);
if (href) {
return (
<Link className={className} href={href}>
<Link className={classes} href={href}>
{children}
</Link>
);
}
return <p className={className}>{children}</p>;
return <p className={classes}>{children}</p>;
}

View File

@@ -17,7 +17,7 @@ export function SideNavItem({
}) {
const classes = clsx(
className,
"py-1.5 px-2 -mx-2 group rounded-md flex items-center transition-colors",
"py-1 px-2 -mx-2 group rounded-md flex items-center transition-colors",
);
const path = usePathname();

View File

@@ -40,9 +40,8 @@ export default function DocsLayout({
<div className="container relative md:grid md:grid-cols-12 md:gap-12">
<div
className={clsx(
"py-8",
"pr-3 md:col-span-4 lg:col-span-3",
"sticky align-start top-[72px] h-[calc(100vh-72px)] overflow-y-auto",
"sticky align-start top-[61px] h-[calc(100vh-61px)] overflow-y-auto",
"hidden md:block",
)}
>
@@ -53,7 +52,7 @@ export default function DocsLayout({
{tocItems?.length && (
<>
<TableOfContents
className="pl-3 py-6 shrink-0 text-sm sticky align-start top-[72px] w-[16rem] h-[calc(100vh-72px)] overflow-y-auto hidden lg:block"
className="pl-3 py-6 shrink-0 text-sm sticky align-start top-[61px] w-[16rem] h-[calc(100vh-61px)] overflow-y-auto hidden lg:block"
items={tocItems}
/>
</>

View File

@@ -20,9 +20,9 @@ const TocList = ({
};
return (
<ul className="space-y-3" style={{ paddingLeft: `${level}rem` }}>
<ul className="space-y-2" style={{ paddingLeft: "1rem" }}>
{items.map((item) => (
<li key={item.id} className="space-y-3">
<li key={item.id} className="space-y-2">
{item.id && (
<Link
href={`#${item.id}`}
@@ -35,7 +35,7 @@ const TocList = ({
{item.value}
</Link>
)}
{item.children && (
{!!item.children?.length && (
<TocList
items={item.children}
level={level + 1}

View File

@@ -20,8 +20,7 @@ export function DocNav({ className }: { className?: string }) {
if (!item.href?.startsWith("/docs")) return item;
const frameworkDone = (item.done as any)[framework] ?? 0;
let done =
typeof item.done === "number" ? item.done : frameworkDone;
let done = typeof item.done === "number" ? item.done : frameworkDone;
let href = item.href.replace("/docs", `/docs/${framework}`);
return {

View File

@@ -73,7 +73,7 @@ export function ClassOrInterface({
<div className="relative not-prose">
<div
id={name}
className="peer sticky top-0 mt-4 md:top-[72px] md:pt-8 bg-white dark:bg-stone-950 z-20"
className="peer sticky top-0 mt-4 md:top-[61px] md:pt-8 bg-white dark:bg-stone-950 z-20"
>
<Link
href={"#" + name}

View File

@@ -21,17 +21,7 @@ export const docNavigationItems = [
href: "/examples",
done: 30,
},
{
name: "AI tools",
href: "/docs/ai-tools",
done: 100,
},
{
name: "Inspector",
href: "/docs/inspector",
done: 100,
},
{ name: "FAQ", href: "/docs/faq", done: 100 },
{ name: "FAQs", href: "/docs/faq", done: 100 },
],
},
{
@@ -60,8 +50,25 @@ export const docNavigationItems = [
},
],
},
{
name: "Tools",
items: [
{
name: "AI tools",
href: "/docs/ai-tools",
done: 100,
},
{
name: "Inspector",
href: "/docs/inspector",
done: 100,
},
],
},
{
name: "Upgrade guides",
collapse: true,
prefix: "/docs/upgrade",
items: [
{
// upgrade guides

View File

@@ -12,4 +12,11 @@ export const products = [
url: "https://invoiceradar.com",
description: "Automatically gather invoices from mail and cloud providers.",
},
{
name: "Hend",
description: "Learn languages naturally with interesting, compelling content tailored just for you.",
url: "https://hendapp.com",
imageUrl: "/hend.png",
}
];

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

View File

@@ -102,6 +102,9 @@ importers:
clsx:
specifier: ^2.1.1
version: 2.1.1
feed:
specifier: ^4.2.2
version: 4.2.2
gcmp-design-system:
specifier: workspace:*
version: link:../design-system
@@ -1952,6 +1955,10 @@ packages:
fastq@1.17.1:
resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
feed@4.2.2:
resolution: {integrity: sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==}
engines: {node: '>=0.4.0'}
fenceparser@1.1.1:
resolution: {integrity: sha512-VdkTsK7GWLT0VWMK5S5WTAPn61wJ98WPFwJiRHumhg4ESNUO/tnkU8bzzzc62o6Uk1SVhuZFLnakmDA4SGV7wA==}
engines: {node: '>=12'}
@@ -2793,6 +2800,9 @@ packages:
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
sax@1.4.1:
resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==}
scheduler@0.23.2:
resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
@@ -3165,6 +3175,10 @@ packages:
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
engines: {node: '>=12'}
xml-js@1.6.11:
resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==}
hasBin: true
y18n@4.0.3:
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
@@ -5468,6 +5482,10 @@ snapshots:
dependencies:
reusify: 1.0.4
feed@4.2.2:
dependencies:
xml-js: 1.6.11
fenceparser@1.1.1: {}
fill-range@7.0.1:
@@ -6687,6 +6705,8 @@ snapshots:
safe-buffer@5.2.1: {}
sax@1.4.1: {}
scheduler@0.23.2:
dependencies:
loose-envify: 1.4.0
@@ -7136,6 +7156,10 @@ snapshots:
string-width: 5.1.2
strip-ansi: 7.1.0
xml-js@1.6.11:
dependencies:
sax: 1.4.1
y18n@4.0.3: {}
yaml@2.4.2: {}

View File

@@ -21,7 +21,8 @@
"@twind/preset-tailwind": "^1.1.4",
"cojson": "workspace:*",
"jazz-react-core": "workspace:*",
"jazz-tools": "workspace:*"
"jazz-tools": "workspace:*",
"lucide-react": "^0.274.0"
},
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"

View File

@@ -5,4 +5,9 @@ export { JazzInspector } from "./viewer/new-app.js";
export { PageStack } from "./viewer/page-stack.js";
export { Breadcrumbs } from "./viewer/breadcrumbs.js";
export { Button } from "./ui/button.js";
export { Input } from "./ui/input.js";
export { Select } from "./ui/select.js";
export { Icon } from "./ui/icon.js";
export type { PageInfo } from "./viewer/types.js";

View File

@@ -1,3 +1,5 @@
import { classNames } from "./utils.js";
export function LinkIcon() {
return (
<svg
@@ -6,7 +8,7 @@ export function LinkIcon() {
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-3 h-3"
className={classNames("w-3 h-3")}
>
<path
strokeLinecap="round"

View File

@@ -28,6 +28,7 @@ Object.keys(stonePalette).forEach((key) => {
});
export default defineConfig({
hash: true,
presets: [presetAutoprefix(), presetTailwind()],
theme: {
extend: {

View File

@@ -1,4 +1,8 @@
import { install } from "@twind/core";
import { setup } from "@twind/core";
import config from "./twind.config";
install(config);
export const tw = setup(
config,
undefined,
document.getElementById("__jazz_inspector")!,
);

View File

@@ -1,5 +1,5 @@
import { clsx } from "clsx";
import { forwardRef } from "react";
import { classNames } from "../utils.js";
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary" | "tertiary" | "destructive" | "plain";
@@ -38,10 +38,10 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
"bg-red-600 border-red-600 text-white font-medium hover:bg-red-700 hover:border-red-700",
};
const classNames =
const classes =
variant === "plain"
? className
: clsx(
: classNames(
className,
"inline-flex items-center justify-center gap-2 rounded-lg text-center transition-colors",
"disabled:pointer-events-none disabled:opacity-70",
@@ -55,7 +55,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
ref={ref}
{...buttonProps}
disabled={disabled}
className={classNames}
className={classes}
type={type}
>
{children}

View File

@@ -0,0 +1,89 @@
import {
CheckIcon,
ChevronDown,
ChevronRight,
ClipboardIcon,
LinkIcon,
type LucideIcon,
TrashIcon,
UserIcon,
XIcon,
} from "lucide-react";
import { classNames } from "../utils.js";
const icons = {
auth: UserIcon,
check: CheckIcon,
chevronRight: ChevronRight,
chevronDown: ChevronDown,
close: XIcon,
copy: ClipboardIcon,
delete: TrashIcon,
link: LinkIcon,
};
// copied from tailwind line height https://tailwindcss.com/docs/font-size
const sizes = {
"2xs": 14,
xs: 16,
sm: 20,
md: 24,
lg: 28,
xl: 28,
"2xl": 32,
"3xl": 36,
"4xl": 40,
"5xl": 48,
"6xl": 60,
"7xl": 72,
"8xl": 96,
"9xl": 128,
};
const strokeWidths = {
"2xs": 2.5,
xs: 2,
sm: 2,
md: 1.5,
lg: 1.5,
xl: 1.5,
"2xl": 1.25,
"3xl": 1.25,
"4xl": 1.25,
"5xl": 1,
"6xl": 1,
"7xl": 1,
"8xl": 1,
"9xl": 1,
};
export function Icon({
name,
icon,
size = "md",
className,
...svgProps
}: {
name?: string;
icon?: LucideIcon;
size?: keyof typeof sizes;
className?: string;
} & React.SVGProps<SVGSVGElement>) {
if (!icon && (!name || !icons.hasOwnProperty(name))) {
throw new Error(`Icon not found: ${name}`);
}
// @ts-ignore
const IconComponent = icons?.hasOwnProperty(name) ? icons[name] : icon;
return (
<IconComponent
aria-hidden="true"
size={sizes[size]}
strokeWidth={strokeWidths[size]}
strokeLinecap="round"
className={classNames(className)}
{...svgProps}
/>
);
}

View File

@@ -1,6 +1,6 @@
import { clsx } from "clsx";
import { forwardRef, useId } from "react";
import { classNames } from "../utils.js";
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
// label can be hidden with a "label:sr-only" className
label: string;
@@ -13,19 +13,19 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
const generatedId = useId();
const id = customId || generatedId;
const inputClassName = clsx(
const inputClassName = classNames(
"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);
const containerClassName = classNames("grid gap-1", className);
return (
<div className={containerClassName}>
<label
htmlFor={id}
className={clsx(
className={classNames(
"text-stone-600 dark:text-stone-300",
hideLabel && "sr-only",
)}

View File

@@ -0,0 +1,51 @@
import { useId } from "react";
import { classNames } from "../utils.js";
import { Icon } from "./icon.js";
export function Select(
props: React.SelectHTMLAttributes<HTMLSelectElement> & {
label: string;
hideLabel?: boolean;
},
) {
const { label, hideLabel, id: customId, className } = props;
const generatedId = useId();
const id = customId || generatedId;
const containerClassName = classNames("grid gap-1", className);
const selectClassName = classNames(
"w-full rounded-md border pl-3.5 py-2 pr-8 shadow-sm",
"font-medium text-stone-900",
"dark:text-white dark:bg-stone-925",
"appearance-none",
"truncate",
);
return (
<div className={classNames(containerClassName)}>
<label
htmlFor={id}
className={classNames("text-stone-600 dark:text-stone-300", {
"sr-only": hideLabel,
})}
>
{label}
</label>
<div className={classNames("relative flex items-center")}>
<select {...props} id={id} className={selectClassName}>
{props.children}
</select>
<Icon
name="chevronDown"
className={classNames(
"absolute right-[0.5em] text-stone-400 dark:text-stone-600",
)}
size="sm"
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,6 @@
import { ClassValue, clsx } from "clsx";
import { tw } from "./twind.js";
export const classNames = (...inputs: ClassValue[]) => {
return tw(clsx(inputs));
};

View File

@@ -1,7 +1,9 @@
import React from "react";
import { Button } from "./button.js";
import { Button } from "../ui/button.js";
import { PageInfo } from "./types.js";
import { classNames } from "../utils.js";
interface BreadcrumbsProps {
path: PageInfo[];
onBreadcrumbClick: (index: number) => void;
@@ -12,25 +14,33 @@ export const Breadcrumbs: React.FC<BreadcrumbsProps> = ({
onBreadcrumbClick,
}) => {
return (
<div className="relative z-20 flex-1 flex gap-2 items-center">
<Button variant="plain" onClick={() => onBreadcrumbClick(-1)}>
<div className={classNames("relative z-20 flex-1 flex items-center")}>
<Button
variant="plain"
className={classNames("text-blue px-1 dark:text-blue-400")}
onClick={() => onBreadcrumbClick(-1)}
>
Home
</Button>
{path.map((page, index) => {
return (
<span
key={index}
className={`inline-block ${index === 0 ? "pl-1" : "pl-0"} ${
index === path.length - 1 ? "pr-1" : "pr-0"
}`}
>
{index === 0 ? null : (
<span className="text-blue-600/30">{" / "}</span>
)}
<Button variant="tertiary" onClick={() => onBreadcrumbClick(index)}>
<React.Fragment key={page.coId}>
<span
aria-hidden
className={classNames(
"text-stone-400 dark:text-stone-600 px-0.5",
)}
>
/
</span>
<Button
variant="plain"
className={classNames("text-blue px-1 dark:text-blue-400")}
onClick={() => onBreadcrumbClick(index)}
>
{index === 0 ? page.name || "Root" : page.name}
</Button>
</span>
</React.Fragment>
);
})}
</div>

View File

@@ -9,10 +9,12 @@ 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 { Button } from "../ui/button.js";
import { PageInfo } from "./types.js";
import { AccountOrGroupPreview } from "./value-renderer.js";
import { classNames } from "../utils.js";
// typeguard for BinaryStreamStart
function isBinaryStreamStart(item: unknown): item is BinaryStreamStart {
return (
@@ -162,7 +164,7 @@ const LabelContentPair = ({
content: React.ReactNode;
}) => {
return (
<div className="flex flex-col gap-1.5">
<div className={classNames("flex flex-col gap-1.5")}>
<span>{label}</span>
<span>{content}</span>
</div>
@@ -220,12 +222,16 @@ function RenderCoBinaryStream({
const sizeInKB = (file.totalSize || 0) / 1024;
return (
<div className="mt-8 flex flex-col gap-8">
<div className="grid grid-cols-3 gap-2 max-w-3xl">
<div className={classNames("mt-8 flex flex-col gap-8")}>
<div className={classNames("grid grid-cols-3 gap-2 max-w-3xl")}>
<LabelContentPair
label="Mime Type"
content={
<span className="font-mono bg-gray-100 rounded px-2 py-1 text-sm dark:bg-stone-900">
<span
className={classNames(
"font-mono bg-gray-100 rounded px-2 py-1 text-sm dark:bg-stone-900",
)}
>
{mimeType || "No mime type"}
</span>
}
@@ -254,7 +260,9 @@ function RenderCoBinaryStream({
<LabelContentPair
label="Preview"
content={
<div className="bg-gray-50 dark:bg-gray-925 p-3 rounded">
<div
className={classNames("bg-gray-50 dark:bg-gray-925 p-3 rounded")}
>
<RenderBlobImage blob={blob} />
</div>
}
@@ -275,10 +283,12 @@ function RenderCoStream({
const userCoIds = streamPerUser.map((stream) => stream.split("_session")[0]);
return (
<div className="grid grid-cols-3 gap-2">
<div className={classNames("grid grid-cols-3 gap-2")}>
{userCoIds.map((id, idx) => (
<div
className="p-3 rounded-lg overflow-hidden border border-gray-200 cursor-pointer shadow-sm hover:bg-gray-100/5"
className={classNames(
"p-3 rounded-lg overflow-hidden border border-gray-200 cursor-pointer shadow-sm hover:bg-gray-100/5",
)}
key={id}
>
<AccountOrGroupPreview coId={id as CoID<RawCoValue>} node={node} />

View File

@@ -1,10 +1,12 @@
import { CoID, LocalNode, RawCoValue } from "cojson";
import { JsonObject } from "cojson";
import { Button } from "./button.js";
import { Button } from "../ui/button.js";
import { ResolveIcon } from "./type-icon.js";
import { PageInfo, isCoId } from "./types.js";
import { CoMapPreview, ValueRenderer } from "./value-renderer.js";
import { classNames } from "../utils.js";
export function GridView({
data,
onNavigate,
@@ -17,27 +19,41 @@ export function GridView({
const entries = Object.entries(data);
return (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
<div
className={classNames(
"grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4",
)}
>
{entries.map(([key, child], childIndex) => (
<Button
variant="plain"
key={childIndex}
className={`p-3 text-left rounded-lg overflow-hidden transition-colors ${
isCoId(child)
? "border border-gray-200 shadow-sm hover:bg-gray-100/5"
: "bg-gray-50 dark:bg-gray-925 cursor-default"
}`}
className={classNames(
`p-3 text-left rounded-lg overflow-hidden transition-colors ${
isCoId(child)
? "border border-gray-200 shadow-sm hover:bg-gray-100/5"
: "bg-gray-50 dark:bg-gray-925 cursor-default"
}`,
)}
onClick={() =>
isCoId(child) &&
onNavigate([{ coId: child as CoID<RawCoValue>, name: key }])
}
>
<h3 className="overflow-hidden text-ellipsis whitespace-nowrap">
<h3
className={classNames(
"overflow-hidden text-ellipsis whitespace-nowrap",
)}
>
{isCoId(child) ? (
<span className="font-medium flex justify-between">
<span className={classNames("font-medium flex justify-between")}>
{key}
<div className="py-1 px-2 text-xs bg-gray-100 rounded dark:bg-gray-900">
<div
className={classNames(
"py-1 px-2 text-sm bg-gray-100 rounded dark:bg-gray-900",
)}
>
<ResolveIcon coId={child as CoID<RawCoValue>} node={node} />
</div>
</span>
@@ -45,7 +61,7 @@ export function GridView({
<span>{key}</span>
)}
</h3>
<div className="mt-2 text-sm">
<div className={classNames("mt-2 text-sm")}>
{isCoId(child) ? (
<CoMapPreview coId={child as CoID<RawCoValue>} node={node} />
) : (
@@ -54,7 +70,6 @@ export function GridView({
onCoIDClick={(coId) => {
onNavigate([{ coId, name: key }]);
}}
compact
/>
)}
</div>

View File

@@ -1,12 +1,14 @@
import { CoID, RawCoValue } from "cojson";
import { useAccount } from "jazz-react-core";
import React, { useState } from "react";
import { Button } from "../ui/button.js";
import { Input } from "../ui/input.js";
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";
import { classNames } from "../utils.js";
type Position =
| "bottom right"
| "bottom left"
@@ -43,15 +45,20 @@ export function JazzInspector({ position = "right" }: { position?: Position }) {
};
if (!open) {
// not sure if this will work, probably is better to use inline styles for the button, but please check.
return (
<Button
id="__jazz_inspector"
variant="secondary"
size="sm"
onClick={() => setOpen(true)}
className={`fixed w-10 h-10 bg-white shadow-sm bottom-0 right-0 m-4 p-1.5 ${positionClasses[position]}`}
className={classNames(
`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"
className={classNames("w-full h-auto relative -left-px text-blue")}
xmlns="http://www.w3.org/2000/svg"
width="119"
height="115"
@@ -65,20 +72,25 @@ export function JazzInspector({ position = "right" }: { position?: Position }) {
fill="currentColor"
/>
</svg>
<span className="sr-only">Open Jazz Inspector</span>
<span className={classNames("sr-only")}>Open Jazz Inspector</span>
</Button>
);
}
return (
<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">
<div
className={classNames(
"fixed h-[calc(100%-12rem)] flex flex-col bottom-0 left-0 w-full bg-red-500 border-t border-gray-200 dark:border-stone-900 dark:bg-stone-925",
)}
id="__jazz_inspector"
>
<div className={classNames("flex items-center gap-4 px-3 my-3")}>
<Breadcrumbs path={path} onBreadcrumbClick={goToIndex} />
<form onSubmit={handleCoValueIdSubmit} className="w-[21rem]">
<form onSubmit={handleCoValueIdSubmit} className={classNames("w-96")}>
{path.length !== 0 && (
<Input
label="CoValue ID"
className="font-mono"
className={classNames("font-mono")}
hideLabel
placeholder="co_z1234567890abcdef123456789"
value={coValueId}
@@ -97,22 +109,24 @@ export function JazzInspector({ position = "right" }: { position?: Position }) {
goBack={goBack}
addPages={addPages}
>
<form
onSubmit={handleCoValueIdSubmit}
aria-hidden={path.length !== 0}
className={`flex flex-col justify-center items-center gap-2 h-full w-full mb-20 transition-all duration-150 ${
path.length > 0
? "opacity-0 translate-y-[-0.5rem] scale-95"
: "opacity-100"
}`}
>
<fieldset className="flex flex-col gap-2">
<h2 className="text-lg text-center font-medium mb-4 text-stone-900 dark:text-white">
{path.length <= 0 && (
<form
onSubmit={handleCoValueIdSubmit}
aria-hidden={path.length !== 0}
className={classNames(
"flex flex-col relative -top-6 justify-center gap-2 h-full w-full max-w-sm mx-auto",
)}
>
<h2
className={classNames(
"text-lg text-center font-medium mb-4 text-stone-900 dark:text-white",
)}
>
Jazz CoValue Inspector
</h2>
<Input
label="CoValue ID"
className="min-w-[21rem] font-mono"
className={classNames("min-w-[21rem] font-mono")}
hideLabel
placeholder="co_z1234567890abcdef123456789"
value={coValueId}
@@ -122,7 +136,7 @@ export function JazzInspector({ position = "right" }: { position?: Position }) {
Inspect CoValue
</Button>
<p className="text-center">or</p>
<p className={classNames("text-center")}>or</p>
<Button
variant="secondary"
@@ -133,8 +147,8 @@ export function JazzInspector({ position = "right" }: { position?: Position }) {
>
Inspect my account
</Button>
</fieldset>
</form>
</form>
)}
</PageStack>
</div>
);

View File

@@ -1,6 +1,7 @@
import { CoID, LocalNode, RawCoValue } from "cojson";
import { Page } from "./page.js"; // Assuming you have a Page component
import { classNames } from "../utils.js";
// Define the structure of a page in the path
interface PageInfo {
coId: CoID<RawCoValue>;
@@ -27,8 +28,12 @@ export function PageStack({
const index = path.length - 1;
return (
<div className="relative mt-4 overflow-y-auto flex-1">
{children && <div className="absolute inset-0 pb-20">{children}</div>}
<div
className={classNames(
"relative px-3 overflow-y-auto flex-1 text-stone-700 dark:text-stone-400",
)}
>
{children}
{node && page && (
<Page
coId={page.coId}
@@ -37,14 +42,6 @@ export function PageStack({
onHeaderClick={goBack}
onNavigate={addPages}
isTopLevel={index === path.length - 1}
className="transition-transform transition-opacity duration-300 ease-out"
style={{
transform: `translateZ(${(index - path.length + 1) * 200}px) scale(${
1 - (path.length - index - 1) * 0.05
}) translateY(${-(index - path.length + 1) * -4}%)`,
opacity: 1 - (path.length - index - 1) * 0.05,
zIndex: index,
}}
/>
)}
</div>

View File

@@ -8,6 +8,8 @@ import { PageInfo } from "./types.js";
import { useResolvedCoValue } from "./use-resolve-covalue.js";
import { AccountOrGroupPreview } from "./value-renderer.js";
import { classNames } from "../utils.js";
type PageProps = {
coId: CoID<RawCoValue>;
node: LocalNode;
@@ -15,7 +17,7 @@ type PageProps = {
onNavigate: (newPages: PageInfo[]) => void;
onHeaderClick?: () => void;
isTopLevel?: boolean;
style: React.CSSProperties;
style?: React.CSSProperties;
className?: string;
};
@@ -55,13 +57,11 @@ export function Page({
return (
<div
style={style}
className={
className + " absolute z-10 inset-0 w-full h-full bg-clip-padding"
}
className={className + "absolute z-10 inset-0 w-full h-full px-3"}
>
{!isTopLevel && (
<div
className="absolute left-0 right-0 top-0 h-10"
className={classNames("absolute left-0 right-0 top-0 h-10")}
aria-label="Back"
onClick={() => {
onHeaderClick?.();
@@ -69,28 +69,40 @@ export function Page({
aria-hidden="true"
></div>
)}
<div className="flex justify-between items-center mb-4">
<div className="flex items-center gap-3">
<h2 className="text-2xl font-bold flex flex-col items-start gap-1">
<div className={classNames("flex justify-between items-center mb-4")}>
<div className={classNames("flex items-center gap-3")}>
<h2
className={classNames(
"text-lg font-medium flex flex-col items-start gap-1 text-stone-900 dark:text-white",
)}
>
<span>
{name}
{typeof snapshot === "object" && "name" in snapshot ? (
<span className="text-gray-600 font-medium">
<span className={classNames("text-gray-600 font-medium")}>
{" "}
{(snapshot as { name: string }).name}
</span>
) : null}
</span>
</h2>
<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">
<span
className={classNames(
"text-sm 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">
<span
className={classNames(
"text-sm 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)]">
<div className={classNames("overflow-auto")}>
{type === "costream" ? (
<CoStreamView
data={snapshot}
@@ -104,7 +116,7 @@ export function Page({
<TableView data={snapshot} node={node} onNavigate={onNavigate} />
)}
{extendedType !== "account" && extendedType !== "group" && (
<div className="text-xs text-gray-500 mt-4">
<div className={classNames("text-sm text-gray-500 mt-4")}>
Owned by{" "}
<AccountOrGroupPreview
coId={value.group.id}

View File

@@ -2,11 +2,12 @@ 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 { Button } from "../ui/button.js";
import { PageInfo } from "./types.js";
import { useResolvedCoValues } from "./use-resolve-covalue.js";
import { ValueRenderer } from "./value-renderer.js";
import { classNames } from "../utils.js";
export function TableView({
data,
node,
@@ -52,23 +53,29 @@ export function TableView({
return (
<div>
<table className="min-w-full border-spacing-0 border-collapse">
<thead className="sticky top-0 border-b border-gray-200">
<table
className={classNames(
"min-w-full text-sm border-spacing-0 border-collapse",
)}
>
<thead className={classNames("sticky top-0 border-b border-gray-200")}>
<tr>
{["", ...keys].map((key) => (
<th
key={key}
className="p-3 bg-gray-50 dark:bg-gray-925 text-left text-xs font-medium text-gray-500 rounded"
className={classNames(
"p-3 bg-gray-50 dark:bg-gray-925 text-left font-medium rounded",
)}
>
{key}
</th>
))}
</tr>
</thead>
<tbody className=" border-t border-gray-200">
<tbody className={classNames(" border-t border-gray-200")}>
{resolvedRows.slice(0, visibleRowsCount).map((item, index) => (
<tr key={index}>
<td className="p-1">
<td className={classNames("p-1")}>
<Button
variant="tertiary"
onClick={() =>
@@ -84,10 +91,7 @@ export function TableView({
</Button>
</td>
{keys.map((key) => (
<td
key={key}
className="p-4 whitespace-nowrap text-sm text-gray-500"
>
<td key={key} className={classNames("p-4 whitespace-nowrap")}>
<ValueRenderer
json={(item.snapshot as JsonObject)[key]}
onCoIDClick={(coId) => {
@@ -113,17 +117,23 @@ export function TableView({
))}
</tbody>
</table>
<div className="py-4 text-gray-500 flex items-center justify-between gap-2">
<div
className={classNames(
"py-4 text-gray-500 flex items-center justify-between gap-2",
)}
>
<span>
Showing {Math.min(visibleRowsCount, coIdArray.length)} of{" "}
{coIdArray.length}
</span>
{hasMore && (
<div className="text-center">
<div className={classNames("text-center")}>
<Button
variant="plain"
onClick={loadMore}
className="px-4 py-2 bg-blue text-white rounded hover:bg-blue-800"
className={classNames(
"px-4 py-2 bg-blue text-white rounded hover:bg-blue-800",
)}
>
Load more
</Button>

View File

@@ -5,6 +5,8 @@ import {
useResolvedCoValue,
} from "./use-resolve-covalue.js";
import { classNames } from "../utils.js";
export const TypeIcon = ({
type,
extendedType,
@@ -25,7 +27,7 @@ export const TypeIcon = ({
const iconKey = extendedType || type;
const icon = iconMap[iconKey as keyof typeof iconMap];
return icon ? <span className="font-mono">{icon}</span> : null;
return icon ? <span className={classNames("font-mono")}>{icon}</span> : null;
};
export const ResolveIcon = ({
@@ -38,10 +40,13 @@ export const ResolveIcon = ({
const { type, extendedType, snapshot } = useResolvedCoValue(coId, node);
if (snapshot === "unavailable" && !type) {
return <div className="text-gray-600 font-medium">Unavailable</div>;
return (
<div className={classNames("text-gray-600 font-medium")}>Unavailable</div>
);
}
if (!type) return <div className="whitespace-pre w-14 font-mono"> </div>;
if (!type)
return <div className={classNames("whitespace-pre w-14 font-mono")}> </div>;
return <TypeIcon type={type} extendedType={extendedType} />;
};

View File

@@ -1,8 +1,8 @@
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 { Button } from "../ui/button.js";
import { classNames } from "../utils.js";
import {
isBrowserImage,
resolveCoValue,
@@ -12,21 +12,19 @@ import {
// Is there a chance we can pass the actual CoValue here?
export function ValueRenderer({
json,
compact,
onCoIDClick,
}: {
json: JsonValue | undefined;
compact?: boolean;
onCoIDClick?: (childNode: CoID<RawCoValue>) => void;
}) {
const [isExpanded, setIsExpanded] = useState(false);
if (typeof json === "undefined" || json === undefined) {
return <span className="text-gray-400">undefined</span>;
return <span className={classNames("text-gray-400")}>undefined</span>;
}
if (json === null) {
return <span className="text-gray-400">null</span>;
return <span className={classNames("text-gray-400")}>null</span>;
}
if (typeof json === "string" && json.startsWith("co_")) {
@@ -44,7 +42,7 @@ export function ValueRenderer({
if (onCoIDClick) {
return (
<Button
className={linkClasses}
className={classNames(linkClasses)}
onClick={() => {
onCoIDClick?.(json as CoID<RawCoValue>);
}}
@@ -55,27 +53,31 @@ export function ValueRenderer({
);
}
return <span className={linkClasses}>{content}</span>;
return <span className={classNames(linkClasses)}>{content}</span>;
}
if (typeof json === "string") {
return (
<span className="text-green-900 font-mono">
{/* <span className="select-none opacity-70">{'"'}</span> */}
<span
className={classNames("text-green-700 font-mono dark:text-green-400")}
>
{json}
{/* <span className="select-none opacity-70">{'"'}</span> */}
</span>
);
}
if (typeof json === "number") {
return <span className="text-purple-500">{json}</span>;
return (
<span className={classNames("text-purple-700 dark:text-purple-400")}>
{json}
</span>
);
}
if (typeof json === "boolean") {
return (
<span
className={clsx(
className={classNames(
json
? "text-green-700 bg-green-700/5"
: "text-amber-700 bg-amber-500/5",
@@ -88,45 +90,30 @@ export function ValueRenderer({
);
}
if (Array.isArray(json)) {
return (
<span title={JSON.stringify(json)}>
Array <span className="text-gray-500">({json.length})</span>
</span>
);
}
if (typeof json === "object") {
return (
<span
title={JSON.stringify(json, null, 2)}
className="inline-block max-w-64"
className={classNames("inline-block max-w-64")}
>
{compact ? (
<span>
Object{" "}
<span className="text-gray-500">({Object.keys(json).length})</span>
<pre className="mt-1 text-sm whitespace-pre-wrap">
{isExpanded
? JSON.stringify(json, null, 2)
: JSON.stringify(json, null, 2)
.split("\n")
.slice(0, 3)
.join("\n") + (Object.keys(json).length > 2 ? "\n..." : "")}
</pre>
<Button
variant="plain"
onClick={() => setIsExpanded(!isExpanded)}
className="text-xs text-gray-500 hover:text-gray-700"
>
{isExpanded ? "Show less" : "Show more"}
</Button>
</span>
) : (
<pre className="whitespace-pre-wrap">
{JSON.stringify(json, null, 2)}
</pre>
)}
<span className={classNames("text-gray-600")}>
{Array.isArray(json) ? <>Array ({json.length})</> : <>Object</>}
</span>
<pre className={classNames("mt-1.5 text-sm whitespace-pre-wrap")}>
{isExpanded
? JSON.stringify(json, null, 2)
: JSON.stringify(json, null, 2).split("\n").slice(0, 3).join("\n") +
(Object.keys(json).length > 2 ? "\n..." : "")}
</pre>
<Button
variant="plain"
onClick={() => setIsExpanded(!isExpanded)}
className={classNames(
"mt-1.5 text-sm text-gray-600 hover:text-gray-700",
)}
>
{isExpanded ? "Show less" : "Show more"}
</Button>
</span>
);
}
@@ -150,14 +137,18 @@ export const CoMapPreview = ({
if (!snapshot) {
return (
<div className="rounded bg-gray-100 animate-pulse whitespace-pre w-24">
<div
className={classNames(
"rounded bg-gray-100 animate-pulse whitespace-pre w-24",
)}
>
{" "}
</div>
);
}
if (snapshot === "unavailable" && !value) {
return <div className="text-gray-500">Unavailable</div>;
return <div className={classNames("text-gray-500")}>Unavailable</div>;
}
if (extendedType === "image" && isBrowserImage(snapshot)) {
@@ -165,9 +156,11 @@ export const CoMapPreview = ({
<div>
<img
src={snapshot.placeholderDataURL}
className="size-8 border-2 border-white drop-shadow-md my-2"
className={classNames(
"size-8 border-2 border-white drop-shadow-md my-2",
)}
/>
<span className="text-gray-500 text-sm">
<span className={classNames("text-gray-500 text-sm")}>
{snapshot.originalSize[0]} x {snapshot.originalSize[1]}
</span>
@@ -183,7 +176,9 @@ export const CoMapPreview = ({
return (
<div>
Record{" "}
<span className="text-gray-500">({Object.keys(snapshot).length})</span>
<span className={classNames("text-gray-500")}>
({Object.keys(snapshot).length})
</span>
</div>
);
}
@@ -192,7 +187,7 @@ export const CoMapPreview = ({
return (
<div>
List{" "}
<span className="text-gray-500">
<span className={classNames("text-gray-500")}>
({(snapshot as unknown as []).length})
</span>
</div>
@@ -200,13 +195,13 @@ export const CoMapPreview = ({
}
return (
<div className="text-sm flex flex-col gap-2 items-start">
<div className="grid grid-cols-[auto_1fr] gap-2">
<div className={classNames("text-sm flex flex-col gap-2 items-start")}>
<div className={classNames("grid grid-cols-[auto_1fr] gap-2")}>
{Object.entries(snapshot)
.slice(0, limit)
.map(([key, value]) => (
<React.Fragment key={key}>
<span className="font-medium">{key}: </span>
<span className={classNames("font-medium")}>{key}: </span>
<span>
<ValueRenderer json={value} />
</span>
@@ -214,7 +209,7 @@ export const CoMapPreview = ({
))}
</div>
{Object.entries(snapshot).length > limit && (
<div className="text-left text-xs text-gray-500 mt-2">
<div className={classNames("text-left text-sm text-gray-500 mt-2")}>
{Object.entries(snapshot).length - limit} more
</div>
)}
@@ -264,10 +259,10 @@ export function AccountOrGroupPreview({
const props = onClick
? {
onClick: () => onClick(displayName),
className: "text-blue-500 cursor-pointer hover:underline",
className: classNames("text-blue-500 cursor-pointer hover:underline"),
}
: {
className: "text-gray-500",
className: classNames("text-gray-500"),
};
return <span {...props}>{displayText}</span>;

3
pnpm-lock.yaml generated
View File

@@ -1709,6 +1709,9 @@ importers:
jazz-tools:
specifier: workspace:*
version: link:../jazz-tools
lucide-react:
specifier: ^0.274.0
version: 0.274.0(react@18.3.1)
react:
specifier: 18.3.1
version: 18.3.1