Compare commits
6 Commits
jazz-react
...
refactor/i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0dbc2b555 | ||
|
|
050e007d1e | ||
|
|
b06650e52b | ||
|
|
c7f6f69c6b | ||
|
|
9554402bbe | ||
|
|
91dea6e813 |
6
.changeset/itchy-turkeys-enjoy.md
Normal file
6
.changeset/itchy-turkeys-enjoy.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"jazz-inspector": patch
|
||||
"jazz-inspector-app": patch
|
||||
---
|
||||
|
||||
use goober for css
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -24,4 +24,6 @@ test-results
|
||||
|
||||
.vscode/settings.json
|
||||
|
||||
.svelte-kit
|
||||
.svelte-kit
|
||||
|
||||
.idea
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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:*"
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,8 +0,0 @@
|
||||
import { setup } from "@twind/core";
|
||||
import config from "./twind.config";
|
||||
|
||||
export const tw = setup(
|
||||
config,
|
||||
undefined,
|
||||
document.getElementById("__jazz_inspector")!,
|
||||
);
|
||||
24
packages/jazz-inspector/src/ui/badge.tsx
Normal file
24
packages/jazz-inspector/src/ui/badge.tsx
Normal 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>;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
71
packages/jazz-inspector/src/ui/global-styles.tsx
Normal file
71
packages/jazz-inspector/src/ui/global-styles.tsx
Normal 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;
|
||||
}
|
||||
`;
|
||||
15
packages/jazz-inspector/src/ui/heading.tsx
Normal file
15
packages/jazz-inspector/src/ui/heading.tsx
Normal 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>;
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
59
packages/jazz-inspector/src/ui/table.tsx
Normal file
59
packages/jazz-inspector/src/ui/table.tsx
Normal 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>;
|
||||
}
|
||||
67
packages/jazz-inspector/src/ui/text.tsx
Normal file
67
packages/jazz-inspector/src/ui/text.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { ClassValue, clsx } from "clsx";
|
||||
import { tw } from "./twind.js";
|
||||
|
||||
export const classNames = (...inputs: ClassValue[]) => {
|
||||
return tw(clsx(inputs));
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
88
packages/jazz-inspector/src/viewer/inpsector-button.tsx
Normal file
88
packages/jazz-inspector/src/viewer/inpsector-button.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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} />;
|
||||
};
|
||||
|
||||
@@ -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
74
pnpm-lock.yaml
generated
@@ -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: {}
|
||||
|
||||
Reference in New Issue
Block a user