Compare commits

...

6 Commits

Author SHA1 Message Date
Trisha Lim
a0dbc2b555 add changeset 2025-04-02 19:51:54 +07:00
Trisha Lim
050e007d1e refactor css to use goober 2025-04-02 19:07:36 +07:00
Trisha Lim
b06650e52b move inspector button to separate component 2025-04-02 19:07:20 +07:00
Trisha Lim
c7f6f69c6b add table components 2025-04-02 19:07:20 +07:00
Trisha Lim
9554402bbe use button component for links 2025-04-02 19:07:20 +07:00
Trisha Lim
91dea6e813 add text and badge component 2025-03-28 11:52:33 +07:00
29 changed files with 1087 additions and 612 deletions

View File

@@ -0,0 +1,6 @@
---
"jazz-inspector": patch
"jazz-inspector-app": patch
---
use goober for css

4
.gitignore vendored
View File

@@ -24,4 +24,6 @@ test-results
.vscode/settings.json
.svelte-kit
.svelte-kit
.idea

View File

@@ -12,6 +12,7 @@ import { WasmCrypto } from "cojson/crypto/WasmCrypto";
import {
Breadcrumbs,
Button,
GlobalStyles,
Icon,
Input,
PageStack,
@@ -126,7 +127,7 @@ export default function CoJsonViewerApp() {
}
return (
<div
<GlobalStyles
className={clsx(
"h-screen overflow-hidden flex flex-col",
" text-stone-700 bg-white",
@@ -202,7 +203,7 @@ export default function CoJsonViewerApp() {
</form>
)}
</PageStack>
</div>
</GlobalStyles>
);
}

View File

@@ -16,11 +16,9 @@
"preview": "vite preview"
},
"dependencies": {
"@twind/core": "^1.1.3",
"@twind/preset-autoprefix": "^1.0.7",
"@twind/preset-tailwind": "^1.1.4",
"clsx": "^2.0.0",
"cojson": "workspace:*",
"goober": "^2.1.16",
"jazz-react-core": "workspace:*",
"jazz-tools": "workspace:*"
},

View File

@@ -1,5 +1,4 @@
// Import Twind setup
import "./twind.js";
import React from "react";
export { JazzInspector } from "./viewer/new-app.js";
export { PageStack } from "./viewer/page-stack.js";
@@ -9,6 +8,7 @@ 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 { GlobalStyles } from "./ui/global-styles.js";
export {
resolveCoValue,
@@ -16,3 +16,7 @@ export {
} from "./viewer/use-resolve-covalue.js";
export type { PageInfo } from "./viewer/types.js";
import { setup } from "goober";
setup(React.createElement);

View File

@@ -1,55 +0,0 @@
import { defineConfig } from "@twind/core";
import presetAutoprefix from "@twind/preset-autoprefix";
import presetTailwind from "@twind/preset-tailwind";
const stonePalette = {
50: "oklch(0.988281 0.002 75)",
100: "oklch(0.980563 0.002 75)",
200: "oklch(0.917969 0.002 75)",
300: "oklch(0.853516 0.002 75)",
400: "oklch(0.789063 0.002 75)",
500: "oklch(0.726563 0.002 75)",
600: "oklch(0.613281 0.002 75)",
700: "oklch(0.523438 0.002 75)",
800: "oklch(0.412109 0.002 75)",
900: "oklch(0.302734 0.002 75)",
925: "oklch(0.220000 0.002 75)",
950: "oklch(0.193359 0.002 75)",
};
const stonePaletteWithAlpha = { ...stonePalette };
Object.keys(stonePalette).forEach((key) => {
// @ts-ignore
stonePaletteWithAlpha[key] = stonePaletteWithAlpha[key].replace(
")",
"/ <alpha-value>)",
);
});
export default defineConfig({
hash: true,
presets: [presetAutoprefix(), presetTailwind()],
theme: {
extend: {
colors: {
stone: stonePaletteWithAlpha,
gray: stonePaletteWithAlpha,
blue: {
50: "#f5f7ff",
100: "#ebf0fe",
200: "#d6e0fd",
300: "#b3c7fc",
400: "#8aa6f9",
500: "#5870F1",
600: "#3651E7",
700: "#3313F7",
800: "#2A12BE",
900: "#12046A",
950: "#1e1b4b",
DEFAULT: "#3313F7",
},
},
},
},
});

View File

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

View File

@@ -0,0 +1,24 @@
import { styled } from "goober";
const StyledBadge = styled("span")<{ className?: string }>`
font-size: 0.875rem;
font-weight: 500;
padding: 0.125rem 0.25rem;
margin-left: -0.125rem;
border-radius: var(--j-radius-sm);
background-color: var(--j-neutral-200);
display: inline-block;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
color: var(--j-text-color-strong);
@media (prefers-color-scheme: dark) {
background-color: var(--j-neutral-900);
}
`;
export function Badge({
children,
className,
}: React.PropsWithChildren<{ className?: string }>) {
return <StyledBadge className={className}>{children}</StyledBadge>;
}

View File

@@ -1,20 +1,63 @@
import { styled } from "goober";
import { forwardRef } from "react";
import { classNames } from "../utils.js";
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary" | "tertiary" | "destructive" | "plain";
size?: "sm" | "md" | "lg";
variant?: "primary" | "secondary" | "link" | "plain";
children?: React.ReactNode;
className?: string;
disabled?: boolean;
}
const StyledButton = styled("button")<{ variant: string; disabled?: boolean }>`
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
text-align: center;
transition: colors 0.2s;
border-radius: var(--j-radius-lg);
pointer-events: ${(props) => (props.disabled ? "none" : "auto")};
opacity: ${(props) => (props.disabled ? 0.5 : 1)};
cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
${(props) => {
switch (props.variant) {
case "primary":
return `
padding: 0.375rem 0.75rem;
background-color: var(--j-primary-color);
border-color: var(--j-primary-color);
color: white;
font-weight: 500;
`;
case "secondary":
return `
padding: 0.375rem 0.75rem;
color: var(--j-text-color-strong);
border: 1px solid var(--j-border-color);
font-weight: 500;
&:hover {
border-color: var(--j-border-color-hover);
}
`;
case "link":
return `
color: var(--j-link-color);
&:hover {
text-decoration: underline;
}
`;
default:
return "";
}
}}
`;
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{
className,
children,
size = "md",
variant = "primary",
disabled,
type = "button",
@@ -22,44 +65,17 @@ export const Button = forwardRef<HTMLButtonElement, 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 classes =
variant === "plain"
? className
: classNames(
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
<StyledButton
ref={ref}
{...buttonProps}
disabled={disabled}
className={classes}
className={className}
type={type}
variant={variant}
>
{children}
</button>
</StyledButton>
);
},
);

View File

@@ -0,0 +1,71 @@
import { styled } from "goober";
export const GlobalStyles = styled("div")`
/* Colors */
--j-primary-color: #3313F7;
--j-link-color: var(--j-primary-color);
/* Neutral Colors */
--j-neutral-100: #faf8f8;
--j-neutral-200: #e5e3e4;
--j-neutral-300: #d0cecf;
--j-neutral-400: #bbbaba;
--j-neutral-500: #a8a6a6;
--j-neutral-600: #858484;
--j-neutral-700: #6b696a;
--j-neutral-900: #2f2e2e;
--j-neutral-925: #1b1a1a;
--j-neutral-950: #151414;
/* Text Colors */
--j-text-color: var(--j-neutral-700);
--j-text-color-strong: var(--j-neutral-900);
/* Border Colors */
--j-border-color: var(--j-neutral-200);
--j-border-color-hover: var(--j-neutral-300);
--j-border-dark: var(--j-neutral-900);
--j-border-focus: var(--j-primary-color);
/* Background Colors */
--j-background: #FFFFFF;
--j-foreground: var(--j-neutral-100);
/* Border Radius */
--j-radius-sm: 0.25rem;
--j-radius-md: 0.375rem;
--j-radius-lg: 0.5rem;
/* Shadows */
--j-shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
@media (prefers-color-scheme: dark) {
--j-text-color: var(--j-neutral-400);
--j-link-color: #5870f1;
--j-border-color: var(--j-neutral-900);
--j-background: var(--j-neutral-950);
--j-foreground: var(--j-neutral-925);
--j-border-color-hover: var(--j-neutral-700);
--j-text-color-strong: var(--j-neutral-100);
}
*:focus {
outline: none;
}
*:focus-visible {
box-shadow: 0 0 0 2px var(--j-link-color);
}
.j-sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
`;

View File

@@ -0,0 +1,15 @@
import { styled } from "goober";
const StyledHeading = styled("h1")<{ className?: string }>`
font-size: 1.125rem;
text-align: center;
font-weight: 500;
color: var(--j-text-color-strong);
`;
export function Heading({
children,
className,
}: React.PropsWithChildren<{ className?: string }>) {
return <StyledHeading className={className}>{children}</StyledHeading>;
}

View File

@@ -1,10 +1,11 @@
import { classNames } from "../utils.js";
import { ChevronDownIcon } from "./icons/chevron-down-icon.js";
import { DeleteIcon } from "./icons/delete-icon.js";
import { LinkIcon } from "./icons/link-icon.js";
const icons = {
chevronDown: ChevronDownIcon,
delete: DeleteIcon,
link: LinkIcon,
};
// copied from tailwind line height https://tailwindcss.com/docs/font-size
@@ -50,7 +51,6 @@ export function Icon({
}: {
name?: string;
size?: keyof typeof sizes;
className?: string;
} & React.SVGProps<SVGSVGElement>) {
if (!name || !icons.hasOwnProperty(name)) {
throw new Error(`Icon not found: ${name}`);
@@ -65,7 +65,6 @@ export function Icon({
size={sizes[size]}
strokeWidth={strokeWidths[size]}
strokeLinecap="round"
className={classNames(className)}
{...svgProps}
/>
);

View File

@@ -1,6 +1,4 @@
import { classNames } from "../../utils.js";
export function LinkIcon() {
export function LinkIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -8,7 +6,7 @@ export function LinkIcon() {
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className={classNames("w-3 h-3")}
{...props}
>
<path
strokeLinecap="round"

View File

@@ -1,40 +1,51 @@
import { styled } from "goober";
import { forwardRef, useId } from "react";
import { classNames } from "../utils.js";
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
interface LabelProps {
hideLabel?: boolean;
}
interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement>,
LabelProps {
// label can be hidden with a "label:sr-only" className
label: string;
className?: string;
id?: string;
hideLabel?: boolean;
}
const Container = styled("div")`
display: grid;
gap: 0.25rem;
`;
const StyledInput = styled("input")`
width: 100%;
border-radius: var(--j-radius-md);
border: 1px solid var(--j-border-color);
padding: 0.5rem 0.875rem;
box-shadow: var(--j-shadow-sm);
font-weight: 500;
background-color: white;
color: var(--text-color-strong);
@media (prefers-color-scheme: dark) {
background-color: var(--j-foreground);
}
`;
export const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, className, hideLabel, id: customId, ...inputProps }, ref) => {
const generatedId = useId();
const id = customId || generatedId;
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 = classNames("grid gap-1", className);
return (
<div className={containerClassName}>
<label
htmlFor={id}
className={classNames(
"text-stone-600 dark:text-stone-300",
hideLabel && "sr-only",
)}
>
<Container className={className}>
<label htmlFor={id} className={hideLabel ? "j-sr-only" : ""}>
{label}
</label>
<input ref={ref} {...inputProps} id={id} className={inputClassName} />
</div>
<StyledInput ref={ref} {...inputProps} id={id} />
</Container>
);
},
);

View File

@@ -1,7 +1,48 @@
import { styled } from "goober";
import { useId } from "react";
import { classNames } from "../utils.js";
import { Icon } from "./icon.js";
const SelectContainer = styled("div")<{ className?: string }>`
display: grid;
gap: 0.25rem;
`;
const SelectWrapper = styled("div")`
position: relative;
display: flex;
align-items: center;
`;
const StyledSelect = styled("select")`
width: 100%;
border-radius: var(--j-radius-md);
border: 1px solid var(--j-border-color);
padding: 0.5rem 0.875rem 0.5rem 0.875rem;
padding-right: 2rem;
box-shadow: var(--j-shadow-sm);
font-weight: 500;
color: var(--j-text-color-strong);
appearance: none;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@media (prefers-color-scheme: dark) {
background-color: var(--j-foreground);
}
`;
const SelectIcon = styled("span")`
position: absolute;
right: 0.5em;
color: var(--j-neutral-400);
pointer-events: none;
@media (prefers-color-scheme: dark) {
color: var(--j-neutral-900);
}
`;
export function Select(
props: React.SelectHTMLAttributes<HTMLSelectElement> & {
label: string;
@@ -12,40 +53,21 @@ export function Select(
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,
})}
>
<SelectContainer className={className}>
<label htmlFor={id} className={hideLabel ? "j-sr-only" : ""}>
{label}
</label>
<div className={classNames("relative flex items-center")}>
<select {...selectProps} id={id} className={selectClassName}>
<SelectWrapper>
<StyledSelect {...selectProps} id={id}>
{props.children}
</select>
</StyledSelect>
<Icon
name="chevronDown"
className={classNames(
"absolute right-[0.5em] text-stone-400 dark:text-stone-600",
)}
size="sm"
/>
</div>
</div>
<SelectIcon>
<Icon name="chevronDown" size="sm" />
</SelectIcon>
</SelectWrapper>
</SelectContainer>
);
}

View File

@@ -0,0 +1,59 @@
import { styled } from "goober";
const StyledTable = styled("table")`
width: 100%;
`;
const StyledThead = styled("thead")`
text-align: left;
border-bottom: 1px solid var(--j-border-color);
background-color: var(--j-neutral-100);
@media (prefers-color-scheme: dark) {
background-color: var(--j-neutral-925);
}
`;
const StyledTbody = styled("tbody")`
tr {
border-bottom: 1px solid var(--j-border-color);
&:last-child {
border-bottom: none;
}
}
`;
const StyledTh = styled("th")`
font-weight: 500;
padding: 0.5rem 0.75rem;
color: var(--j-text-color-strong);
`;
const StyledTd = styled("td")`
padding: 0.5rem 0.75rem;
`;
export function Table({ children }: React.PropsWithChildren<{}>) {
return <StyledTable>{children}</StyledTable>;
}
export function TableHead({ children }: React.PropsWithChildren<{}>) {
return <StyledThead>{children}</StyledThead>;
}
export function TableBody({ children }: React.PropsWithChildren<{}>) {
return <StyledTbody>{children}</StyledTbody>;
}
export function TableRow({ children }: React.PropsWithChildren<{}>) {
return <tr>{children}</tr>;
}
export function TableHeader({ children }: React.PropsWithChildren<{}>) {
return <StyledTh>{children}</StyledTh>;
}
export function TableCell({ children }: React.PropsWithChildren<{}>) {
return <StyledTd>{children}</StyledTd>;
}

View File

@@ -0,0 +1,67 @@
import { styled } from "goober";
import React from "react";
interface TextProps extends React.HTMLAttributes<HTMLParagraphElement> {
muted?: boolean;
strong?: boolean;
small?: boolean;
inline?: boolean;
}
const BaseText = React.forwardRef<HTMLParagraphElement, TextProps>(
({ muted, strong, small, inline, ...rest }, ref) => <p ref={ref} {...rest} />,
);
const StyledText = styled(BaseText)<TextProps>`
${(props) =>
props.muted &&
`
color: var(--j-neutral-500);
`}
${(props) =>
props.strong &&
`
font-weight: 500;
color: var(--j-text-color-strong);
`}
${(props) =>
props.small &&
`
font-size: 0.875rem;
`}
${(props) =>
props.inline &&
`
display: inline;
`}
`;
export function Text({
children,
className,
muted,
strong,
inline,
small,
}: React.PropsWithChildren<{
className?: string;
muted?: boolean;
strong?: boolean;
inline?: boolean;
small?: boolean;
}>) {
return (
<StyledText
className={className}
muted={muted}
strong={strong}
inline={inline}
small={small}
>
{children}
</StyledText>
);
}

View File

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

View File

@@ -1,8 +1,19 @@
import { styled } from "goober";
import React from "react";
import { Button } from "../ui/button.js";
import { PageInfo } from "./types.js";
import { classNames } from "../utils.js";
const BreadcrumbsContainer = styled("div")`
position: relative;
z-index: 20;
flex: 1;
display: flex;
align-items: center;
`;
const Separator = styled("span")`
padding: 0 0.125rem;
`;
interface BreadcrumbsProps {
path: PageInfo[];
@@ -14,10 +25,10 @@ export const Breadcrumbs: React.FC<BreadcrumbsProps> = ({
onBreadcrumbClick,
}) => {
return (
<div className={classNames("relative z-20 flex-1 flex items-center")}>
<BreadcrumbsContainer>
<Button
variant="plain"
className={classNames("text-blue px-1 dark:text-blue-400")}
variant="link"
style={{ padding: "0 0.25rem" }}
onClick={() => onBreadcrumbClick(-1)}
>
Home
@@ -25,17 +36,10 @@ export const Breadcrumbs: React.FC<BreadcrumbsProps> = ({
{path.map((page, index) => {
return (
<React.Fragment key={page.coId}>
<span
aria-hidden
className={classNames(
"text-stone-400 dark:text-stone-600 px-0.5",
)}
>
/
</span>
<Separator aria-hidden>/</Separator>
<Button
variant="plain"
className={classNames("text-blue px-1 dark:text-blue-400")}
variant="link"
style={{ padding: "0 0.25rem" }}
onClick={() => onBreadcrumbClick(index)}
>
{index === 0 ? page.name || "Root" : page.name}
@@ -43,6 +47,6 @@ export const Breadcrumbs: React.FC<BreadcrumbsProps> = ({
</React.Fragment>
);
})}
</div>
</BreadcrumbsContainer>
);
};

View File

@@ -8,13 +8,13 @@ import {
import { base64URLtoBytes } from "cojson";
import { BinaryStreamItem, BinaryStreamStart, CoStreamItem } from "cojson";
import type { JsonObject, JsonValue } from "cojson";
import { styled } from "goober";
import { useEffect, useState } from "react";
import { Badge } from "../ui/badge.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 (
@@ -156,6 +156,53 @@ const BinaryDownloadButton = ({
);
};
const LabelContentPairContainer = styled("div")`
display: flex;
flex-direction: column;
gap: 0.375rem;
`;
const BinaryStreamContainer = styled("div")`
margin-top: 2rem;
display: flex;
flex-direction: column;
gap: 2rem;
`;
const BinaryStreamGrid = styled("div")`
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
max-width: 48rem;
`;
const ImagePreviewContainer = styled("div")`
background-color: rgb(249 250 251);
padding: 0.75rem;
border-radius: var(--j-radius-md);
@media (prefers-color-scheme: dark) {
background-color: rgb(28 25 23);
}
`;
const CoStreamGrid = styled("div")`
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
`;
const CoStreamItemContainer = styled("div")`
padding: 0.75rem;
border-radius: var(--j-radius-lg);
overflow: hidden;
border: 1px solid rgb(229 231 235);
cursor: pointer;
box-shadow: var(--j-shadow-sm);
&:hover {
background-color: rgb(243 244 246 / 0.05);
}
`;
const LabelContentPair = ({
label,
content,
@@ -164,10 +211,10 @@ const LabelContentPair = ({
content: React.ReactNode;
}) => {
return (
<div className={classNames("flex flex-col gap-1.5")}>
<LabelContentPairContainer>
<span>{label}</span>
<span>{content}</span>
</div>
</LabelContentPairContainer>
);
};
@@ -222,19 +269,11 @@ function RenderCoBinaryStream({
const sizeInKB = (file.totalSize || 0) / 1024;
return (
<div className={classNames("mt-8 flex flex-col gap-8")}>
<div className={classNames("grid grid-cols-3 gap-2 max-w-3xl")}>
<BinaryStreamContainer>
<BinaryStreamGrid>
<LabelContentPair
label="Mime Type"
content={
<span
className={classNames(
"font-mono bg-gray-100 rounded px-2 py-1 text-sm dark:bg-stone-900",
)}
>
{mimeType || "No mime type"}
</span>
}
content={<Badge>{mimeType || "No mime type"}</Badge>}
/>
<LabelContentPair
label="Size"
@@ -255,20 +294,18 @@ function RenderCoBinaryStream({
/>
}
/>
</div>
</BinaryStreamGrid>
{mimeType === "image/png" || mimeType === "image/jpeg" ? (
<LabelContentPair
label="Preview"
content={
<div
className={classNames("bg-gray-50 dark:bg-gray-925 p-3 rounded")}
>
<ImagePreviewContainer>
<RenderBlobImage blob={blob} />
</div>
</ImagePreviewContainer>
}
/>
) : null}
</div>
</BinaryStreamContainer>
);
}
@@ -283,14 +320,9 @@ function RenderCoStream({
const userCoIds = streamPerUser.map((stream) => stream.split("_session")[0]);
return (
<div className={classNames("grid grid-cols-3 gap-2")}>
<CoStreamGrid>
{userCoIds.map((id, idx) => (
<div
className={classNames(
"p-3 rounded-lg overflow-hidden border border-gray-200 cursor-pointer shadow-sm hover:bg-gray-100/5",
)}
key={id}
>
<CoStreamItemContainer key={id}>
<AccountOrGroupPreview coId={id as CoID<RawCoValue>} node={node} />
{/* @ts-expect-error - TODO: fix types */}
{value.items[streamPerUser[idx]]?.map(
@@ -301,9 +333,9 @@ function RenderCoStream({
</div>
),
)}
</div>
</CoStreamItemContainer>
))}
</div>
</CoStreamGrid>
);
}

View File

@@ -1,11 +1,66 @@
import { CoID, LocalNode, RawCoValue } from "cojson";
import { JsonObject } from "cojson";
import { Button } from "../ui/button.js";
import { styled } from "goober";
import { ResolveIcon } from "./type-icon.js";
import { PageInfo, isCoId } from "./types.js";
import { CoMapPreview, ValueRenderer } from "./value-renderer.js";
import { classNames } from "../utils.js";
import { Badge } from "../ui/badge.js";
import { Text } from "../ui/text.js";
const GridContainer = styled("div")`
display: grid;
grid-template-columns: repeat(1, 1fr);
gap: 1rem;
@media (min-width: 768px) {
grid-template-columns: repeat(2, 1fr);
}
@media (min-width: 1280px) {
grid-template-columns: repeat(3, 1fr);
}
`;
const GridItem = styled(
({
isCoId,
...rest
}: { isCoId: boolean } & React.ButtonHTMLAttributes<HTMLButtonElement>) => (
<button {...rest} />
),
)`
padding: 0.75rem;
text-align: left;
border-radius: var(--j-radius-lg);
overflow: hidden;
transition: background-color 0.2s;
cursor: ${(props) => (props.isCoId ? "pointer" : "default")};
${(props) =>
props.isCoId
? `
border: 1px solid var(--j-border-color);
box-shadow: var(--j-shadow-sm);
`
: `
background-color: var(--j-foreground);
`}
`;
const TitleContainer = styled("h3")`
display: flex;
justify-content: space-between;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--j-text-color-strong);
`;
const ContentContainer = styled("div")`
margin-top: 0.5rem;
font-size: 0.875rem;
`;
export function GridView({
data,
@@ -19,49 +74,29 @@ export function GridView({
const entries = Object.entries(data);
return (
<div
className={classNames(
"grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4",
)}
>
<GridContainer>
{entries.map(([key, child], childIndex) => (
<Button
variant="plain"
<GridItem
key={childIndex}
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"
}`,
)}
isCoId={isCoId(child)}
onClick={() =>
isCoId(child) &&
onNavigate([{ coId: child as CoID<RawCoValue>, name: key }])
}
>
<h3
className={classNames(
"overflow-hidden text-ellipsis whitespace-nowrap",
)}
>
<TitleContainer>
{isCoId(child) ? (
<span className={classNames("font-medium flex justify-between")}>
{key}
<div
className={classNames(
"py-1 px-2 text-sm bg-gray-100 rounded dark:bg-gray-900",
)}
>
<>
<Text strong>{key}</Text>
<Badge>
<ResolveIcon coId={child as CoID<RawCoValue>} node={node} />
</div>
</span>
</Badge>
</>
) : (
<span>{key}</span>
<Text strong>{key}</Text>
)}
</h3>
<div className={classNames("mt-2 text-sm")}>
</TitleContainer>
<ContentContainer>
{isCoId(child) ? (
<CoMapPreview coId={child as CoID<RawCoValue>} node={node} />
) : (
@@ -72,9 +107,9 @@ export function GridView({
}}
/>
)}
</div>
</Button>
</ContentContainer>
</GridItem>
))}
</div>
</GridContainer>
);
}

View File

@@ -0,0 +1,88 @@
import { styled } from "goober";
import React from "react";
export type Position =
| "bottom right"
| "bottom left"
| "top right"
| "top left"
| "right"
| "left";
const StyledInspectorButton = styled("button")<{ position: Position }>`
position: fixed;
width: 2.5rem;
height: 2.5rem;
background-color: white;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
margin: 1rem;
padding: 0.5rem !important;
border: 1px solid #e5e3e4;
border-radius: 0.375rem;
${(props) => {
switch (props.position) {
case "bottom right":
return "bottom: 0; right: 0;";
case "bottom left":
return "bottom: 0; left: 0;";
case "top right":
return "top: 0; right: 0;";
case "top left":
return "top: 0; left: 0;";
case "right":
return "right: 0; top: 50%; transform: translateY(-50%);";
case "left":
return "left: 0; top: 50%; transform: translateY(-50%);";
default:
return "";
}
}}
`;
const JazzIcon = styled("svg")`
width: 100%;
height: auto;
position: relative;
left: -1px;
color: #3313F7;
`;
export function InspectorButton({
position = "right",
...buttonProps
}: React.ComponentPropsWithoutRef<"button"> & { position?: Position }) {
return (
<StyledInspectorButton position={position} {...buttonProps}>
<JazzIcon
xmlns="http://www.w3.org/2000/svg"
width="119"
height="115"
viewBox="0 0 119 115"
fill="none"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M118.179 23.8277V0.167999C99.931 7.5527 79.9854 11.6192 59.0897 11.6192C47.1466 11.6192 35.5138 10.2908 24.331 7.7737V30.4076V60.1508C23.2955 59.4385 22.1568 58.8458 20.9405 58.3915C18.1732 57.358 15.128 57.0876 12.1902 57.6145C9.2524 58.1414 6.5539 59.4419 4.4358 61.3516C2.3178 63.2613 0.875401 65.6944 0.291001 68.3433C-0.293399 70.9921 0.00659978 73.7377 1.1528 76.2329C2.2991 78.728 4.2403 80.861 6.7308 82.361C9.2214 83.862 12.1495 84.662 15.1448 84.662C15.6054 84.662 15.8365 84.662 16.0314 84.659C26.5583 84.449 35.042 75.9656 35.2513 65.4386C35.2534 65.3306 35.2544 65.2116 35.2548 65.0486L35.2552 64.7149V64.5521V61.0762V32.1993C43.0533 33.2324 51.0092 33.7656 59.0897 33.7656C59.6696 33.7656 60.2489 33.7629 60.8276 33.7574V89.696C59.792 88.983 58.6533 88.391 57.437 87.936C54.6697 86.903 51.6246 86.632 48.6867 87.159C45.7489 87.686 43.0504 88.987 40.9323 90.896C38.8143 92.806 37.3719 95.239 36.7875 97.888C36.2032 100.537 36.5031 103.283 37.6494 105.778C38.7956 108.273 40.7368 110.405 43.2273 111.906C45.7179 113.406 48.646 114.207 51.6414 114.207C52.1024 114.207 52.3329 114.207 52.5279 114.203C63.0548 113.994 71.5385 105.51 71.7478 94.983C71.7517 94.788 71.7517 94.558 71.7517 94.097V90.621V33.3266C83.962 32.4768 95.837 30.4075 107.255 27.2397V59.9017C106.219 59.1894 105.081 58.5966 103.864 58.1424C101.097 57.1089 98.052 56.8384 95.114 57.3653C92.176 57.8922 89.478 59.1927 87.36 61.1025C85.242 63.0122 83.799 65.4453 83.215 68.0941C82.631 70.743 82.931 73.4886 84.077 75.9837C85.223 78.4789 87.164 80.612 89.655 82.112C92.145 83.612 95.073 84.413 98.069 84.413C98.53 84.413 98.76 84.413 98.955 84.409C109.482 84.2 117.966 75.7164 118.175 65.1895C118.179 64.9945 118.179 64.764 118.179 64.3029V60.8271V23.8277Z"
fill="currentColor"
/>
</JazzIcon>
<span
style={{
position: "absolute",
width: "1px",
height: "1px",
padding: "0",
margin: "-1px",
overflow: "hidden",
clip: "rect(0, 0, 0, 0)",
whiteSpace: "nowrap",
border: "0",
}}
>
Open Jazz Inspector
</span>
</StyledInspectorButton>
);
}

View File

@@ -1,4 +1,5 @@
import { CoID, RawCoValue } from "cojson";
import { styled } from "goober";
import { useAccount } from "jazz-react-core";
import React, { useState } from "react";
import { Button } from "../ui/button.js";
@@ -7,15 +8,55 @@ import { Breadcrumbs } from "./breadcrumbs.js";
import { PageStack } from "./page-stack.js";
import { usePagePath } from "./use-page-path.js";
import { classNames } from "../utils.js";
import { GlobalStyles } from "../ui/global-styles.js";
import { Heading } from "../ui/heading.js";
import { InspectorButton, type Position } from "./inpsector-button.js";
type Position =
| "bottom right"
| "bottom left"
| "top right"
| "top left"
| "right"
| "left";
const InspectorContainer = styled("div")`
position: fixed;
height: calc(100% - 12rem);
display: flex;
flex-direction: column;
bottom: 0;
left: 0;
width: 100%;
background-color: white;
border-top: 1px solid var(--j-border-color);
color: var(--j-text-color);
@media (prefers-color-scheme: dark) {
background-color: var(--j-background);
}
`;
const HeaderContainer = styled("div")`
display: flex;
align-items: center;
gap: 1rem;
padding: 0 0.75rem;
margin: 0.75rem 0;
`;
const Form = styled("form")`
width: 24rem;
`;
const InitialForm = styled("form")`
display: flex;
flex-direction: column;
position: relative;
top: -1.5rem;
justify-content: center;
gap: 0.5rem;
height: 100%;
width: 100%;
max-width: 24rem;
margin: 0 auto;
`;
const OrText = styled("p")`
text-align: center;
`;
export function JazzInspector({ position = "right" }: { position?: Position }) {
const [open, setOpen] = useState(false);
@@ -35,73 +76,32 @@ export function JazzInspector({ position = "right" }: { position?: Position }) {
setCoValueId("");
};
const positionClasses = {
"bottom right": "bottom-0 right-0",
"bottom left": "bottom-0 left-0",
"top right": "top-0 right-0",
"top left": "top-0 left-0",
right: "right-0 top-1/2 -translate-y-1/2",
left: "left-0 top-1/2 -translate-y-1/2",
};
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={classNames(
`fixed w-10 h-10 bg-white shadow-sm bottom-0 right-0 m-4 p-1.5 ${positionClasses[position]}`,
)}
>
<svg
className={classNames("w-full h-auto relative -left-px text-blue")}
xmlns="http://www.w3.org/2000/svg"
width="119"
height="115"
viewBox="0 0 119 115"
fill="none"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M118.179 23.8277V0.167999C99.931 7.5527 79.9854 11.6192 59.0897 11.6192C47.1466 11.6192 35.5138 10.2908 24.331 7.7737V30.4076V60.1508C23.2955 59.4385 22.1568 58.8458 20.9405 58.3915C18.1732 57.358 15.128 57.0876 12.1902 57.6145C9.2524 58.1414 6.5539 59.4419 4.4358 61.3516C2.3178 63.2613 0.875401 65.6944 0.291001 68.3433C-0.293399 70.9921 0.00659978 73.7377 1.1528 76.2329C2.2991 78.728 4.2403 80.861 6.7308 82.361C9.2214 83.862 12.1495 84.662 15.1448 84.662C15.6054 84.662 15.8365 84.662 16.0314 84.659C26.5583 84.449 35.042 75.9656 35.2513 65.4386C35.2534 65.3306 35.2544 65.2116 35.2548 65.0486L35.2552 64.7149V64.5521V61.0762V32.1993C43.0533 33.2324 51.0092 33.7656 59.0897 33.7656C59.6696 33.7656 60.2489 33.7629 60.8276 33.7574V89.696C59.792 88.983 58.6533 88.391 57.437 87.936C54.6697 86.903 51.6246 86.632 48.6867 87.159C45.7489 87.686 43.0504 88.987 40.9323 90.896C38.8143 92.806 37.3719 95.239 36.7875 97.888C36.2032 100.537 36.5031 103.283 37.6494 105.778C38.7956 108.273 40.7368 110.405 43.2273 111.906C45.7179 113.406 48.646 114.207 51.6414 114.207C52.1024 114.207 52.3329 114.207 52.5279 114.203C63.0548 113.994 71.5385 105.51 71.7478 94.983C71.7517 94.788 71.7517 94.558 71.7517 94.097V90.621V33.3266C83.962 32.4768 95.837 30.4075 107.255 27.2397V59.9017C106.219 59.1894 105.081 58.5966 103.864 58.1424C101.097 57.1089 98.052 56.8384 95.114 57.3653C92.176 57.8922 89.478 59.1927 87.36 61.1025C85.242 63.0122 83.799 65.4453 83.215 68.0941C82.631 70.743 82.931 73.4886 84.077 75.9837C85.223 78.4789 87.164 80.612 89.655 82.112C92.145 83.612 95.073 84.413 98.069 84.413C98.53 84.413 98.76 84.413 98.955 84.409C109.482 84.2 117.966 75.7164 118.175 65.1895C118.179 64.9945 118.179 64.764 118.179 64.3029V60.8271V23.8277Z"
fill="currentColor"
/>
</svg>
<span className={classNames("sr-only")}>Open Jazz Inspector</span>
</Button>
<InspectorButton position={position} onClick={() => setOpen(true)} />
);
}
return (
<div
className={classNames(
"fixed h-[calc(100%-12rem)] flex flex-col bottom-0 left-0 w-full bg-white 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")}>
<InspectorContainer as={GlobalStyles} id="__jazz_inspector">
<HeaderContainer>
<Breadcrumbs path={path} onBreadcrumbClick={goToIndex} />
<form onSubmit={handleCoValueIdSubmit} className={classNames("w-96")}>
<Form onSubmit={handleCoValueIdSubmit}>
{path.length !== 0 && (
<Input
label="CoValue ID"
className={classNames("font-mono")}
style={{ fontFamily: "monospace" }}
hideLabel
placeholder="co_z1234567890abcdef123456789"
value={coValueId}
onChange={(e) => setCoValueId(e.target.value as CoID<RawCoValue>)}
/>
)}
</form>
</Form>
<Button variant="plain" type="button" onClick={() => setOpen(false)}>
Close
</Button>
</div>
</HeaderContainer>
<PageStack
path={path}
@@ -110,23 +110,14 @@ export function JazzInspector({ position = "right" }: { position?: Position }) {
addPages={addPages}
>
{path.length <= 0 && (
<form
<InitialForm
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>
<Heading>Jazz CoValue Inspector</Heading>
<Input
label="CoValue ID"
className={classNames("min-w-[21rem] font-mono")}
style={{ minWidth: "21rem", fontFamily: "monospace" }}
hideLabel
placeholder="co_z1234567890abcdef123456789"
value={coValueId}
@@ -136,7 +127,7 @@ export function JazzInspector({ position = "right" }: { position?: Position }) {
Inspect CoValue
</Button>
<p className={classNames("text-center")}>or</p>
<OrText>or</OrText>
<Button
variant="secondary"
@@ -147,9 +138,9 @@ export function JazzInspector({ position = "right" }: { position?: Position }) {
>
Inspect my account
</Button>
</form>
</InitialForm>
)}
</PageStack>
</div>
</InspectorContainer>
);
}

View File

@@ -1,7 +1,7 @@
import { CoID, LocalNode, RawCoValue } from "cojson";
import { Page } from "./page.js"; // Assuming you have a Page component
import { styled } from "goober";
import { Page } from "./page.js";
import { classNames } from "../utils.js";
// Define the structure of a page in the path
interface PageInfo {
coId: CoID<RawCoValue>;
@@ -17,6 +17,15 @@ interface PageStackProps {
children?: React.ReactNode;
}
const PageStackContainer = styled("div")`
position: relative;
padding: 0 0.75rem;
overflow-y: auto;
flex: 1;
color: var(--j-text-color);
font-size: 16px;
`;
export function PageStack({
path,
node,
@@ -28,22 +37,20 @@ export function PageStack({
const index = path.length - 1;
return (
<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}
node={node}
name={page.name || page.coId}
onHeaderClick={goBack}
onNavigate={addPages}
isTopLevel={index === path.length - 1}
/>
)}
</div>
<>
<PageStackContainer>
{children}
{node && page && (
<Page
coId={page.coId}
node={node}
name={page.name || page.coId}
onHeaderClick={goBack}
onNavigate={addPages}
isTopLevel={index === path.length - 1}
/>
)}
</PageStackContainer>
</>
);
}

View File

@@ -1,6 +1,10 @@
import { CoID, LocalNode, RawCoStream, RawCoValue } from "cojson";
import { styled } from "goober";
import { useMemo } from "react";
import { classNames } from "../utils.js";
import React from "react";
import { Badge } from "../ui/badge.js";
import { Heading } from "../ui/heading.js";
import { Text } from "../ui/text.js";
import { CoStreamView } from "./co-stream-view.js";
import { GridView } from "./grid-view.js";
import { TableView } from "./table-viewer.js";
@@ -9,6 +13,65 @@ import { PageInfo } from "./types.js";
import { useResolvedCoValue } from "./use-resolve-covalue.js";
import { AccountOrGroupPreview } from "./value-renderer.js";
interface PageContainerProps extends React.HTMLAttributes<HTMLDivElement> {
isTopLevel?: boolean;
}
const BasePageContainer = React.forwardRef<HTMLDivElement, PageContainerProps>(
({ isTopLevel, ...rest }, ref) => <div ref={ref} {...rest} />,
);
const PageContainer = styled(BasePageContainer)<PageContainerProps>`
position: absolute;
z-index: 10;
inset: 0;
width: 100%;
height: 100%;
padding: 0 0.75rem;
`;
const BackButton = styled("div")`
position: absolute;
left: 0;
right: 0;
top: 0;
height: 2.5rem;
`;
const HeaderContainer = styled("div")`
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
`;
const TitleContainer = styled("div")`
display: flex;
align-items: center;
gap: 0.75rem;
`;
const Title = styled(Heading)`
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.25rem;
`;
const BadgeContainer = styled("div")`
display: flex;
align-items: center;
gap: 0.75rem;
`;
const ContentContainer = styled("div")`
overflow: auto;
`;
const OwnerText = styled(Text)`
margin-top: 1rem;
`;
type PageProps = {
coId: CoID<RawCoValue>;
node: LocalNode;
@@ -52,54 +115,38 @@ export function Page({
}
return (
<div
style={style}
className={className + "absolute z-10 inset-0 w-full h-full px-3"}
>
<PageContainer style={style} className={className} isTopLevel={isTopLevel}>
{!isTopLevel && (
<div
className={classNames("absolute left-0 right-0 top-0 h-10")}
<BackButton
aria-label="Back"
onClick={() => {
onHeaderClick?.();
}}
aria-hidden="true"
></div>
></BackButton>
)}
<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",
)}
>
<HeaderContainer>
<TitleContainer>
<Title>
<span>
{name}
{typeof snapshot === "object" && "name" in snapshot ? (
<span className={classNames("text-gray-600 font-medium")}>
<span style={{ color: "#57534e", fontWeight: 500 }}>
{" "}
{(snapshot as { name: string }).name}
</span>
) : null}
</span>
</h2>
<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={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={classNames("overflow-auto")}>
</Title>
<BadgeContainer>
<Badge>
{type && <TypeIcon type={type} extendedType={extendedType} />}
</Badge>
<Badge>{coId}</Badge>
</BadgeContainer>
</TitleContainer>
</HeaderContainer>
<ContentContainer>
{type === "costream" ? (
<CoStreamView
data={snapshot}
@@ -113,7 +160,7 @@ export function Page({
<TableView data={snapshot} node={node} onNavigate={onNavigate} />
)}
{extendedType !== "account" && extendedType !== "group" && (
<div className={classNames("text-sm text-gray-500 mt-4")}>
<OwnerText muted>
Owned by{" "}
<AccountOrGroupPreview
coId={value.group.id}
@@ -123,9 +170,9 @@ export function Page({
onNavigate([{ coId: value.group.id, name: "owner" }]);
}}
/>
</div>
</OwnerText>
)}
</div>
</div>
</ContentContainer>
</PageContainer>
);
}

View File

@@ -1,13 +1,34 @@
import { CoID, LocalNode, RawCoValue } from "cojson";
import type { JsonObject } from "cojson";
import { styled } from "goober";
import { useMemo, useState } from "react";
import { Button } from "../ui/button.js";
import { Icon } from "../ui/icon.js";
import { classNames } from "../utils.js";
import { PageInfo } from "./types.js";
import { useResolvedCoValues } from "./use-resolve-covalue.js";
import { ValueRenderer } from "./value-renderer.js";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "../ui/table.js";
import { Text } from "../ui/text.js";
const TableContainer = styled("div")`
margin-top: 2rem;
`;
const PaginationContainer = styled("div")`
padding: 1rem 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
`;
export function TableView({
data,
node,
@@ -52,46 +73,20 @@ export function TableView({
};
return (
<div>
<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={classNames(
"p-3 bg-gray-50 dark:bg-gray-925 text-left font-medium rounded",
)}
>
{key}
</th>
<TableContainer>
<Table>
<TableHead>
<TableRow>
{[...keys, "Action"].map((key) => (
<TableHeader key={key}>{key}</TableHeader>
))}
</tr>
</thead>
<tbody className={classNames(" border-t border-gray-200")}>
</TableRow>
</TableHead>
<TableBody>
{resolvedRows.slice(0, visibleRowsCount).map((item, index) => (
<tr key={index}>
<td className={classNames("p-1")}>
<Button
variant="tertiary"
onClick={() =>
onNavigate([
{
coId: item.value!.id,
name: index.toString(),
},
])
}
>
<Icon name="link" />
</Button>
</td>
<TableRow key={index}>
{keys.map((key) => (
<td key={key} className={classNames("p-4 whitespace-nowrap")}>
<TableCell key={key}>
<ValueRenderer
json={(item.snapshot as JsonObject)[key]}
onCoIDClick={(coId) => {
@@ -111,35 +106,39 @@ export function TableView({
handleClick();
}}
/>
</td>
</TableCell>
))}
</tr>
<TableCell>
<Button
variant="secondary"
onClick={() =>
onNavigate([
{
coId: item.value!.id,
name: index.toString(),
},
])
}
>
View
</Button>
</TableCell>
</TableRow>
))}
</tbody>
</table>
<div
className={classNames(
"py-4 text-gray-500 flex items-center justify-between gap-2",
)}
>
<span>
</TableBody>
</Table>
<PaginationContainer>
<Text muted small>
Showing {Math.min(visibleRowsCount, coIdArray.length)} of{" "}
{coIdArray.length}
</span>
</Text>
{hasMore && (
<div className={classNames("text-center")}>
<Button
variant="plain"
onClick={loadMore}
className={classNames(
"px-4 py-2 bg-blue text-white rounded hover:bg-blue-800",
)}
>
Load more
</Button>
</div>
<Button variant="secondary" onClick={loadMore}>
Load more
</Button>
)}
</div>
</div>
</PaginationContainer>
</TableContainer>
);
}

View File

@@ -1,11 +1,24 @@
import { CoID, LocalNode, RawCoValue } from "cojson";
import { styled } from "goober";
import {
CoJsonType,
ExtendedCoJsonType,
useResolvedCoValue,
} from "./use-resolve-covalue.js";
import { classNames } from "../utils.js";
const IconText = styled("span")`
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
`;
const UnavailableText = styled("div")`
font-weight: 500;
`;
const EmptySpace = styled("div")`
white-space: pre;
width: 3.5rem;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
`;
export const TypeIcon = ({
type,
@@ -28,7 +41,7 @@ export const TypeIcon = ({
const iconKey = extendedType || type;
const icon = iconMap[iconKey as keyof typeof iconMap];
return icon ? <span className={classNames("font-mono")}>{icon}</span> : null;
return icon ? <IconText>{icon}</IconText> : null;
};
export const ResolveIcon = ({
@@ -41,13 +54,10 @@ export const ResolveIcon = ({
const { type, extendedType, snapshot } = useResolvedCoValue(coId, node);
if (snapshot === "unavailable" && !type) {
return (
<div className={classNames("text-gray-600 font-medium")}>Unavailable</div>
);
return <UnavailableText>Unavailable</UnavailableText>;
}
if (!type)
return <div className={classNames("whitespace-pre w-14 font-mono")}> </div>;
if (!type) return <EmptySpace> </EmptySpace>;
return <TypeIcon type={type} extendedType={extendedType} />;
};

View File

@@ -1,14 +1,137 @@
import { CoID, JsonValue, LocalNode, RawCoValue } from "cojson";
import { styled } from "goober";
import React, { useEffect, useState } from "react";
import { Button } from "../ui/button.js";
import { Icon } from "../ui/icon.js";
import { classNames } from "../utils.js";
import { Text } from "../ui/text.js";
import {
isBrowserImage,
resolveCoValue,
useResolvedCoValue,
} from "./use-resolve-covalue.js";
const UndefinedText = styled("span")`
color: #a8a29e;
`;
const NullText = styled("span")`
color: #a8a29e;
`;
const LinkContainer = styled("span")`
display: inline-flex;
gap: 0.25rem;
align-items: center;
`;
const LinkButton = styled(Button)`
display: inline-flex;
gap: 0.25rem;
align-items: center;
`;
const StringText = styled("span")`
color: #166534;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
@media (prefers-color-scheme: dark) {
color: #4ade80;
}
`;
const NumberText = styled("span")`
color: #6b21a8;
@media (prefers-color-scheme: dark) {
color: #c084fc;
}
`;
const BooleanText = styled("span")<{ value: boolean }>`
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
display: inline-block;
padding: 0.125rem 0.25rem;
border-radius: 0.125rem;
${(props) =>
props.value
? `
color: #166534;
background-color: rgba(22, 101, 52, 0.05);
`
: `
color: #92400e;
background-color: rgba(245, 158, 11, 0.05);
`}
`;
const ObjectContainer = styled("span")`
display: inline-block;
max-width: 16rem;
`;
const ObjectType = styled("span")`
color: #57534e;
`;
const ObjectContent = styled("pre")`
margin-top: 0.375rem;
font-size: 0.875rem;
white-space: pre-wrap;
`;
const ShowMoreButton = styled("button")`
margin-top: 0.375rem;
font-size: 0.875rem;
`;
const PreviewContainer = styled("div")`
font-size: 0.875rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
align-items: flex-start;
`;
const PreviewGrid = styled("div")`
display: grid;
grid-template-columns: auto 1fr;
gap: 0.5rem;
`;
const PreviewMoreText = styled(Text)`
text-align: left;
margin-top: 0.5rem;
`;
const ImagePreviewContainer = styled("div")`
display: flex;
flex-direction: column;
align-items: flex-start;
`;
const PreviewImage = styled("img")`
width: 2rem;
height: 2rem;
border: 2px solid white;
box-shadow: var(--j-shadow-sm);
margin: 0.5rem 0;
`;
const ImageSizeText = styled(Text)`
display: inline;
font-size: 0.875rem;
`;
const RecordText = styled("div")`
display: flex;
align-items: center;
gap: 0.25rem;
`;
const ListText = styled("div")`
display: flex;
align-items: center;
gap: 0.25rem;
`;
// Is there a chance we can pass the actual CoValue here?
export function ValueRenderer({
json,
@@ -20,18 +143,14 @@ export function ValueRenderer({
const [isExpanded, setIsExpanded] = useState(false);
if (typeof json === "undefined" || json === undefined) {
return <span className={classNames("text-gray-400")}>undefined</span>;
return <UndefinedText>undefined</UndefinedText>;
}
if (json === null) {
return <span className={classNames("text-gray-400")}>null</span>;
return <NullText>null</NullText>;
}
if (typeof json === "string" && json.startsWith("co_")) {
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}
@@ -41,80 +160,48 @@ export function ValueRenderer({
if (onCoIDClick) {
return (
<Button
className={classNames(linkClasses)}
<LinkButton
onClick={() => {
onCoIDClick?.(json as CoID<RawCoValue>);
}}
variant="plain"
variant="link"
>
{content}
</Button>
</LinkButton>
);
}
return <span className={classNames(linkClasses)}>{content}</span>;
return <LinkContainer>{content}</LinkContainer>;
}
if (typeof json === "string") {
return (
<span
className={classNames("text-green-700 font-mono dark:text-green-400")}
>
{json}
</span>
);
return <StringText>{json}</StringText>;
}
if (typeof json === "number") {
return (
<span className={classNames("text-purple-700 dark:text-purple-400")}>
{json}
</span>
);
return <NumberText>{json}</NumberText>;
}
if (typeof json === "boolean") {
return (
<span
className={classNames(
json
? "text-green-700 bg-green-700/5"
: "text-amber-700 bg-amber-500/5",
"font-mono",
"inline-block px-1 py-0.5 rounded",
)}
>
{json.toString()}
</span>
);
return <BooleanText value={json}>{json.toString()}</BooleanText>;
}
if (typeof json === "object") {
return (
<span
title={JSON.stringify(json, null, 2)}
className={classNames("inline-block max-w-64")}
>
<span className={classNames("text-gray-600")}>
<ObjectContainer title={JSON.stringify(json, null, 2)}>
<ObjectType>
{Array.isArray(json) ? <>Array ({json.length})</> : <>Object</>}
</span>
<pre className={classNames("mt-1.5 text-sm whitespace-pre-wrap")}>
</ObjectType>
<ObjectContent>
{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",
)}
>
</ObjectContent>
<ShowMoreButton onClick={() => setIsExpanded(!isExpanded)}>
{isExpanded ? "Show less" : "Show more"}
</Button>
</span>
</ShowMoreButton>
</ObjectContainer>
);
}
@@ -138,9 +225,13 @@ export const CoMapPreview = ({
if (!snapshot) {
return (
<div
className={classNames(
"rounded bg-gray-100 animate-pulse whitespace-pre w-24",
)}
style={{
borderRadius: "0.25rem",
backgroundColor: "var(--j-foreground)",
animation: "pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite",
whiteSpace: "pre",
width: "6rem",
}}
>
{" "}
</div>
@@ -148,72 +239,66 @@ export const CoMapPreview = ({
}
if (snapshot === "unavailable" && !value) {
return <div className={classNames("text-gray-500")}>Unavailable</div>;
return (
<Text inline muted>
Unavailable
</Text>
);
}
if (extendedType === "image" && isBrowserImage(snapshot)) {
return (
<div>
<img
src={snapshot.placeholderDataURL}
className={classNames(
"size-8 border-2 border-white drop-shadow-md my-2",
)}
/>
<span className={classNames("text-gray-500 text-sm")}>
<ImagePreviewContainer>
<PreviewImage src={snapshot.placeholderDataURL} />
<ImageSizeText inline small muted>
{snapshot.originalSize[0]} x {snapshot.originalSize[1]}
</span>
{/* <CoMapPreview coId={value[]} node={node} /> */}
{/* <ProgressiveImg image={value}>
{({ src }) => <img src={src} className={clsx("w-full")} />}
</ProgressiveImg> */}
</div>
</ImageSizeText>
</ImagePreviewContainer>
);
}
if (extendedType === "record") {
return (
<div>
<RecordText>
Record{" "}
<span className={classNames("text-gray-500")}>
<Text inline muted>
({Object.keys(snapshot).length})
</span>
</div>
</Text>
</RecordText>
);
}
if (type === "colist") {
return (
<div>
<ListText>
List{" "}
<span className={classNames("text-gray-500")}>
<Text inline muted>
({(snapshot as unknown as []).length})
</span>
</div>
</Text>
</ListText>
);
}
return (
<div className={classNames("text-sm flex flex-col gap-2 items-start")}>
<div className={classNames("grid grid-cols-[auto_1fr] gap-2")}>
<PreviewContainer>
<PreviewGrid>
{Object.entries(snapshot)
.slice(0, limit)
.map(([key, value]) => (
<React.Fragment key={key}>
<span className={classNames("font-medium")}>{key}: </span>
<span>
<Text strong>{key}: </Text>
<Text inline>
<ValueRenderer json={value} />
</span>
</Text>
</React.Fragment>
))}
</div>
</PreviewGrid>
{Object.entries(snapshot).length > limit && (
<div className={classNames("text-left text-sm text-gray-500 mt-2")}>
<PreviewMoreText muted small>
{Object.entries(snapshot).length - limit} more
</div>
</PreviewMoreText>
)}
</div>
</PreviewContainer>
);
};
@@ -256,14 +341,17 @@ export function AccountOrGroupPreview({
const displayName = extendedType === "account" ? name || "Account" : "Group";
const displayText = showId ? `${displayName} (${coId})` : displayName;
const props = onClick
? {
onClick: () => onClick(displayName),
className: classNames("text-blue-500 cursor-pointer hover:underline"),
}
: {
className: classNames("text-gray-500"),
};
if (onClick) {
return (
<Button variant="link" onClick={() => onClick(displayName)}>
{displayText}
</Button>
);
}
return <span {...props}>{displayText}</span>;
return (
<Text muted inline>
{displayText}
</Text>
);
}

74
pnpm-lock.yaml generated
View File

@@ -1737,21 +1737,15 @@ importers:
packages/jazz-inspector:
dependencies:
'@twind/core':
specifier: ^1.1.3
version: 1.1.3(typescript@5.6.3)
'@twind/preset-autoprefix':
specifier: ^1.0.7
version: 1.0.7(@twind/core@1.1.3(typescript@5.6.3))(typescript@5.6.3)
'@twind/preset-tailwind':
specifier: ^1.1.4
version: 1.1.4(@twind/core@1.1.3(typescript@5.6.3))(typescript@5.6.3)
clsx:
specifier: ^2.0.0
version: 2.1.1
cojson:
specifier: workspace:*
version: link:../cojson
goober:
specifier: ^2.1.16
version: 2.1.16(csstype@3.1.3)
jazz-react-core:
specifier: workspace:*
version: link:../jazz-react-core
@@ -5314,35 +5308,6 @@ packages:
'@tsconfig/node22@22.0.0':
resolution: {integrity: sha512-twLQ77zevtxobBOD4ToAtVmuYrpeYUh3qh+TEp+08IWhpsrIflVHqQ1F1CiPxQGL7doCdBIOOCF+1Tm833faNg==}
'@twind/core@1.1.3':
resolution: {integrity: sha512-/B/aNFerMb2IeyjSJy3SJxqVxhrT77gBDknLMiZqXIRr4vNJqiuhx7KqUSRzDCwUmyGuogkamz+aOLzN6MeSLw==}
engines: {node: '>=14.15.0'}
peerDependencies:
typescript: ^4.8.4
peerDependenciesMeta:
typescript:
optional: true
'@twind/preset-autoprefix@1.0.7':
resolution: {integrity: sha512-3wmHO0pG/CVxYBNZUV0tWcL7CP0wD5KpyWAQE/KOalWmOVBj+nH6j3v6Y3I3pRuMFaG5DC78qbYbhA1O11uG3w==}
engines: {node: '>=14.15.0'}
peerDependencies:
'@twind/core': ^1.1.0
typescript: ^4.8.4
peerDependenciesMeta:
typescript:
optional: true
'@twind/preset-tailwind@1.1.4':
resolution: {integrity: sha512-zv85wrP/DW4AxgWrLfH7kyGn/KJF3K04FMLVl2AjoxZGYdCaoZDkL8ma3hzaKQ+WGgBFRubuB/Ku2Rtv/wjzVw==}
engines: {node: '>=14.15.0'}
peerDependencies:
'@twind/core': ^1.1.0
typescript: ^4.8.4
peerDependenciesMeta:
typescript:
optional: true
'@types/argparse@1.0.38':
resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==}
@@ -7819,6 +7784,11 @@ packages:
glur@1.1.2:
resolution: {integrity: sha512-l+8esYHTKOx2G/Aao4lEQ0bnHWg4fWtJbVoZZT9Knxi01pB8C80BR85nONLFwkkQoFRCmXY+BUcGZN3yZ2QsRA==}
goober@2.1.16:
resolution: {integrity: sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==}
peerDependencies:
csstype: ^3.0.10
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
@@ -10938,9 +10908,6 @@ packages:
structured-headers@0.4.1:
resolution: {integrity: sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==}
style-vendorizer@2.2.3:
resolution: {integrity: sha512-/VDRsWvQAgspVy9eATN3z6itKTuyg+jW1q6UoTCQCFRqPDw8bi3E1hXIKnGw5LvXS2AQPuJ7Af4auTLYeBOLEg==}
styleq@0.1.3:
resolution: {integrity: sha512-3ZUifmCDCQanjeej1f6kyl/BeP/Vae5EYkQ9iJfUm/QwZvlgnZzyflqAsAWYURdtea8Vkvswu2GrC57h3qffcA==}
@@ -15580,25 +15547,6 @@ snapshots:
'@tsconfig/node22@22.0.0': {}
'@twind/core@1.1.3(typescript@5.6.3)':
dependencies:
csstype: 3.1.3
optionalDependencies:
typescript: 5.6.3
'@twind/preset-autoprefix@1.0.7(@twind/core@1.1.3(typescript@5.6.3))(typescript@5.6.3)':
dependencies:
'@twind/core': 1.1.3(typescript@5.6.3)
style-vendorizer: 2.2.3
optionalDependencies:
typescript: 5.6.3
'@twind/preset-tailwind@1.1.4(@twind/core@1.1.3(typescript@5.6.3))(typescript@5.6.3)':
dependencies:
'@twind/core': 1.1.3(typescript@5.6.3)
optionalDependencies:
typescript: 5.6.3
'@types/argparse@1.0.38': {}
'@types/aria-query@5.0.4': {}
@@ -18442,6 +18390,10 @@ snapshots:
glur@1.1.2: {}
goober@2.1.16(csstype@3.1.3):
dependencies:
csstype: 3.1.3
gopd@1.2.0: {}
got@12.6.1:
@@ -21892,8 +21844,6 @@ snapshots:
structured-headers@0.4.1: {}
style-vendorizer@2.2.3: {}
styleq@0.1.3: {}
stylis@4.2.0: {}