Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## 0.5.0

- **Click elements in `<WireframeViewer>`:** `onElementClick(hit, hits, event)` gets the element or flow arrow clicked (a press that hardly moves; dragging still pans) and everything under the point, most likely first, each with its lines in the text, type, label, `#id` and box. Hovered elements get a dashed outline. `onElementHover` follows the pointer. `selected` outlines the element on a line, or on each line in a list (a line inside a block, such as an editor's caret, outlines that block). When the text and `selected` change together, the outline waits for the new board instead of matching the new line against the old one; clicks aren't reported until then, and clearing `selected` takes effect right away. The outline color is `--tsquare-highlight`. Built on tsquare 0.14's viewer events.
- **The board is measured only when it's needed:** the viewer measures every element (about one more render) only with `onElementClick`, `onElementHover` or `selected`. Other boards cost the same as before.
- **While the text has problems, nothing is clickable or outlined:** the last valid board's lines no longer match the text.
- **`<WireframePlayground>` syncs the selection between the board and the text:** click an element to select its line in the editor; move the caret and its element is outlined on the board. On by default; `syncSelection={false}` turns it off (and skips measuring the board).
- **`<WireframeEditor onCursorLine>`:** the caret's line when it moves to another one, to connect your own editor and viewer (`selected`, `goToLine`).
- The `Measured` types (`Measured`, `MeasuredElement`, `MeasuredFlow`, `MeasuredBox`) are exported from `@tsquare-js/react`.
- **Needs tsquare 0.14.0 or later.**
- **Docs:** the WireframeViewer page has a Clicking elements section, the Playground page Clicking the board, the Editor page Next to a viewer; Styling lists `--tsquare-highlight` and the outline classes.

## 0.4.0

- **Needs tsquare 0.13.2 or later** (it was 0.12.1). Boards get tsquare's renderer fixes from 0.12.3 to 0.13.2.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function Reply({ text, streaming }: { text: string; streaming: boolean })
[tsquare.dev/docs/react](https://tsquare.dev/docs/react), also in [`docs/`](docs/README.md):

- [Wireframe](docs/wireframe.md): a board as an image, with an optional full-size view
- [WireframeViewer](docs/viewer.md): zoom, pan and a button per screen
- [WireframeViewer](docs/viewer.md): zoom, pan, a button per screen, and clicking elements
- [WireframeEditor](docs/editor.md): write and edit wireframe text, with autocomplete and problems underlined (from `@tsquare-js/react/editor`)
- [WireframePlayground](docs/playground.md): the editor and the board side by side (from `@tsquare-js/react/editor`)
- [Markdown and chat](docs/chat.md): react-markdown, streaming replies
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ The stylesheet is optional but recommended: it styles the buttons, problem lists
|---|---|
| Show a wireframe as an image in a page or card | [`<Wireframe>`](wireframe.md) |
| Let people zoom, pan and jump between screens | [`<WireframeViewer>`](viewer.md), or `<Wireframe expandable>` to open it full size from the image |
| Let people click an element, or outline one | [`<WireframeViewer>`](viewer.md#clicking-elements) with `onElementClick` and `selected` |
| Let people write or edit wireframe text | [`<WireframeEditor>`](editor.md), from `@tsquare-js/react/editor` |
| An editor and the board side by side | [`<WireframePlayground>`](playground.md), from `@tsquare-js/react/editor` |
| Show the ```` ```tsquare ```` blocks in Markdown you render with react-markdown, such as a model's replies in a chat | [`tsquareComponents`](chat.md) |
Expand Down
28 changes: 28 additions & 0 deletions docs/editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ The editor fills its box, so give it a height: with the stylesheet it's 20rem un
| `defaultValue` | | The starting text, when the editor keeps it. |
| `onChange` | | Called with the whole text after every change. |
| `onProblems` | | Called with the problems when the text has some, after a pause in typing, and with `[]` once they're fixed. See [Problems](problems.md). |
| `onCursorLine` | | Called with the caret's line (1 is the first) when it moves to another line. See [Next to a viewer](#next-to-a-viewer). |
| `autocomplete` | `true` | Suggestions for components, props, option words, icons and accents as you type (Ctrl+Space to ask). |
| `lineNumbers` | `true` | Line numbers in a gutter. |
| `lineWrapping` | `true` | Wrap long lines instead of scrolling sideways. A wrapped line continues 4 spaces past its own indentation, so it doesn't look like a nested element. |
Expand Down Expand Up @@ -64,6 +65,33 @@ export function EditorWithExample({ example }: { example: string }) {
| `focus()` | Put the cursor in the editor. |
| `goToLine(n)` | Select line `n`, scroll to it and focus the editor. |

## Next to a viewer

To build your own layout around the editor and a [viewer](viewer.md), connect them by line: `onCursorLine` gives the caret's line, which the viewer's `selected` outlines, and a click on the board moves the caret with `goToLine`. This is what [WireframePlayground](playground.md#clicking-the-board) does.

```tsx
import { useRef, useState } from "react";
import { WireframeViewer } from "@tsquare-js/react";
import { WireframeEditor, type WireframeEditorHandle } from "@tsquare-js/react/editor";

export function Split({ initial }: { initial: string }) {
const [text, setText] = useState(initial);
const [caret, setCaret] = useState<number | null>(null);
const editor = useRef<WireframeEditorHandle>(null);
return (
<div style={{ display: "flex", gap: 16 }}>
<WireframeEditor ref={editor} value={text} onChange={setText} onCursorLine={setCaret} style={{ flex: 1 }} />
<WireframeViewer
source={text}
selected={caret}
onElementClick={(hit) => { if (hit && hit.type !== "Board") editor.current?.goToLine(hit.line); }}
style={{ flex: 1 }}
/>
</div>
);
}
```

## Why it's imported from `@tsquare-js/react/editor`

The editor is built on CodeMirror, bundled inside tsquare (about 125 KB compressed after minifying, on top of the renderer). Importing it from its own entry point keeps it out of apps that only show boards: `@tsquare-js/react` never loads it. It doesn't share CodeMirror with the rest of your app and takes no CodeMirror extensions. If only some pages edit wireframes, load it when it's first needed (`lazy()` or a dynamic `import()`).
Expand Down
17 changes: 17 additions & 0 deletions docs/playground.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ With the stylesheet it's 36rem tall (or set a height, or `--tsquare-playground-h
| `showProblems` | `true` | The note on the board counting the problems. The editor underlines them either way. |
| `chips` | `true` | A chip per screen above the board. |
| `zoomControls` | `true` | Zoom out, zoom level and zoom in above the board. |
| `syncSelection` | `true` | Click an element on the board to put the editor's caret on its line, and outline the element at the caret. See [Clicking the board](#clicking-the-board). |
| `autocomplete` | `true` | Autocomplete in the editor. |
| `lineNumbers` | `true` | Line numbers in the editor. |
| `lineWrapping` | `true` | Wrap long lines in the editor instead of scrolling sideways. |
Expand All @@ -43,6 +44,22 @@ A `ref` gets the editor's methods (`getText`, `setText`, `focus`, `goToLine`), a

The board redraws every 200 ms or so with the latest text that's valid, so it keeps up without redrawing on every key. Problems wait for a pause: about 400 ms after the last key, the text is checked, problems are underlined in the editor, and a note on the board counts them: "2 problems · showing the last valid board", with that board dimmed. `onProblems` fires then, not while typing.

## Clicking the board

Click an element on the board, and the editor selects its line. Move the caret in the editor, and the element on that line is outlined on the board. A line inside a block (a blank line between a stack's children, say) outlines the block. Hovering the board outlines what a click would pick, dashed. While you type, the outline stays put until the board is redrawn with your change, so it never jumps to a neighbor whose line just moved, and a click on the board waits for that redraw too.

While the text has problems, the board shows the last valid version, whose lines no longer match the text: clicks pan and nothing is outlined until the problems are fixed.

Turn it off with `syncSelection={false}`, for example where the board is only a preview. Syncing the selection measures where every element is drawn, which costs about one more render each time the board is redrawn; off, it costs nothing.

```tsx
import { WireframePlayground } from "@tsquare-js/react/editor";

export function Preview({ text }: { text: string }) {
return <WireframePlayground defaultValue={text} syncSelection={false} />;
}
```

## Saving the text

Keep the text in your state to save it, share it or load another:
Expand Down
2 changes: 2 additions & 0 deletions docs/styling.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ The stylesheet reads these CSS variables. Set any of them, on `:root` or on a co
| `--tsquare-accent` | the selected screen chip, focus rings | `#1f2328` / `#e6e8eb` |
| `--tsquare-accent-fg` | text on the accent | `#ffffff` / `#1b1d21` |
| `--tsquare-error` | problem lists | `#cf222e` / `#ff7b72` |
| `--tsquare-highlight` | outlines on elements in the viewer (`selected`, hover); works without the stylesheet too | `#3B99FB` |
| `--tsquare-font` | text | your page's font |
| `--tsquare-font-mono` | problem lines | `ui-monospace, Menlo, …` |
| `--tsquare-radius` | corners | `6px` |
Expand Down Expand Up @@ -86,6 +87,7 @@ Every part has a class, so you can restyle it from your CSS. The stylesheet's ru
| `tsquare-viewer-bar` | the bar above the board: `tsquare-chips` (its buttons have `aria-pressed`) and `tsquare-zoom` (`tsquare-zoom-out`, `tsquare-zoom-level`, `tsquare-zoom-in`) |
| `tsquare-stage` | the area the board moves in, with `tsquare-canvas` inside; it gets `panning` while dragged and `stale` while problems show |
| `tsquare-note` | problems over the viewer's board |
| `tsquare-highlight` | an outline on an element in the viewer, with `tsquare-highlight-selected` or `tsquare-highlight-hover` |
| `tsquare-dialog` | the full-size view: `tsquare-dialog-header`, `tsquare-dialog-title`, `tsquare-dialog-close` |
| `tsquare-editor` | `<WireframeEditor>`'s root; the editor inside is CodeMirror's `cm-editor` |
| `tsquare-playground` | `<WireframePlayground>`'s root (`data-layout` is `auto`, `row` or `column`), with `tsquare-playground-panes` holding the editor and the viewer |
Expand Down
51 changes: 50 additions & 1 deletion docs/viewer.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# WireframeViewer

`<WireframeViewer>` shows a board people can explore: zoom with the buttons, a pinch or Ctrl/Cmd + scroll, drag to pan, double-click to fit, and select a screen's name to zoom to it. Use it where the wireframe is the main thing on screen, such as a design review page or a panel next to a spec.
`<WireframeViewer>` shows a board people can explore: zoom with the buttons, a pinch or Ctrl/Cmd + scroll, drag to pan, double-click to fit, and select a screen's name to zoom to it. It can also tell you which element was clicked and outline elements: see [Clicking elements](#clicking-elements). Use it where the wireframe is the main thing on screen, such as a design review page or a panel next to a spec.

```tsx
import { WireframeViewer } from "@tsquare-js/react";
Expand All @@ -22,6 +22,9 @@ It fills its box, so give it a height. With the stylesheet it's 32rem tall unles
| `streaming` | `false` | The text is still arriving. See [streaming](chat.md#while-a-reply-streams-in). |
| `showProblems` | `true` | Show the text's problems over the bottom of the board. See [Problems](problems.md). |
| `onProblems` | | Called with the problems when the text has some, and with `[]` once they're fixed. |
| `onElementClick` | | Called with the element clicked, and everything under the point. See [Clicking elements](#clicking-elements). |
| `onElementHover` | | Called when the element under the pointer changes, and with `null` when it leaves the board. |
| `selected` | | Outline the element on this line, or on each line in a list. |
| `className` | | Added to the root, next to `tsquare-viewer`. |
| `style` | | On the root. |

Expand All @@ -35,6 +38,52 @@ export function Minimal({ text }: { text: string }) {
}
```

## Clicking elements

With `onElementClick`, a click on the board tells you what was clicked: a button, an input, a screen, a flow arrow. Dragging still pans; only a press that hardly moves counts as a click. Hovered elements get a dashed outline, so people can see what they're about to pick. `selected` outlines elements by their line in the text.

While the board is behind its text (redrawing after an edit, or while a reply streams in), its lines are the older text's, so clicks aren't reported until the new board is shown, and `selected` keeps the outline where it was. Clearing `selected` (`null` or `[]`) takes effect right away.

This example outlines the clicked element and shows its lines, the part of the text you'd hand a model to change:

```tsx
import { useState } from "react";
import { WireframeViewer, type Measured } from "@tsquare-js/react";

export function Pick({ text }: { text: string }) {
const [picked, setPicked] = useState<Measured | null>(null);
const block = picked ? text.split("\n").slice(picked.line - 1, picked.endLine).join("\n") : "";
return (
<>
<WireframeViewer
source={text}
selected={picked?.line}
onElementClick={(hit) => setPicked(hit && hit.type !== "Board" ? hit : null)}
/>
{picked ? <pre>{block}</pre> : null}
</>
);
}
```

`onElementClick(hit, hits, event)` gets:

- **`hit`**: what the person most likely meant. A button over a flow arrow gets the button; empty space inside a screen gets the screen; empty board outside the screens gets the board itself (`type: "Board"`). An open menu or tooltip gets the element it belongs to.
- **`hits`**: everything under the point, most likely first, ending with the screen and the board. Use it to skip what you don't want, such as `hits.find((e) => e.type !== "Flow")`.
- **`event`**: the browser's pointer event, for modifier keys (`event.shiftKey` to add to a selection).

Each element has its `line` and `endLine` in the text (its whole block, children included), `type`, `label`, its `#id` if the text gives one, and its `box` on the board. A flow arrow has `type: "Flow"`, `from` and `to`. The full list is in tsquare's [element positions](https://tsquare.dev/docs/element-positions) docs.

`selected` takes a line or a list of lines. A line inside a block, such as a blank line between a stack's children, outlines that block, so you can pass an editor's caret line (see [Next to a viewer](editor.md#next-to-a-viewer)). The board's own line outlines nothing.

Some details:

- **Lines shift when the text changes.** An element picked on line 11 is on line 12 after someone adds a line above it. For anything that has to survive edits, such as a comment pinned to an element, key it by the element's `id` and give the elements you care about one in the text (`button "Save" #save`).
- **When `source` and `selected` change together, the outline waits for the new board.** Until it's drawn, the board on show has the old text's lines, so the new line could name a neighbor (after Enter in an editor, the line below). The outline stays where it was, then moves. If a board for text that has changed since is drawn meanwhile, the outline is cleared instead, since it was drawn on the board before, and comes back with the next board.
- **While the text has problems, nothing is clickable or outlined.** The viewer still shows the last board that worked, but its lines no longer match the text. Clicks pan, and outlines come back once the text is fixed.
- **Measuring costs about one more render**, so the viewer only measures the board when you set `onElementClick`, `onElementHover` or `selected`.
- **The outline color** is the CSS variable `--tsquare-highlight` (see [Styling](styling.md)).

## When the text changes

The viewer keeps its zoom and position when the board is redrawn at the same size, so editing the text doesn't jump the view. When the board's size changes, or the screen you zoomed to is gone, it fits the board again.
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tsquare-js/react",
"version": "0.4.0",
"version": "0.5.0",
"description": "React component for tsquare wireframes: render wireframe text, or the ```tsquare blocks in react-markdown, including while a model's reply is still streaming in.",
"keywords": [
"tsquare",
Expand Down Expand Up @@ -51,7 +51,7 @@
"prepare": "npm run build"
},
"dependencies": {
"tsquare": "^0.13.2"
"tsquare": "^0.14.0"
},
"peerDependencies": {
"react": ">=18"
Expand Down
7 changes: 6 additions & 1 deletion src/editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export interface WireframeEditorProps {
onChange?: (text: string) => void;
/** Called with the problems when the text has some (after a pause in typing), and with [] once they're fixed. */
onProblems?: (problems: Problem[]) => void;
/** Called with the caret's line (1 = the first) when it moves to another line: pass it to a viewer's `selected` to outline that element. */
onCursorLine?: (line: number) => void;
/** Autocomplete for components, props, option words, icons and accents. Default true. */
autocomplete?: boolean;
/** Line numbers. Default true. */
Expand Down Expand Up @@ -97,13 +99,15 @@ export interface EditorCoreProps extends Omit<WireframeEditorProps, "onProblems"

/** The editor itself: creates tsquare/editor in a div, keeps it in sync with `value`, underlines `issues`. */
export const EditorCore = forwardRef<WireframeEditorHandle, EditorCoreProps>(function EditorCore(
{ value, defaultValue, onChange, issues, autocomplete = true, lineNumbers = true, lineWrapping = true, ariaLabel, className, style },
{ value, defaultValue, onChange, onCursorLine, issues, autocomplete = true, lineNumbers = true, lineWrapping = true, ariaLabel, className, style },
ref,
) {
const host = useRef<HTMLDivElement>(null);
const editor = useRef<EditorInstance | null>(null);
const change = useRef(onChange);
change.current = onChange;
const cursor = useRef(onCursorLine);
cursor.current = onCursorLine;

// Made in a layout effect, which runs before useImperativeHandle below hands out the ref: so a
// callback ref or a parent's useLayoutEffect can already focus it or read its text.
Expand All @@ -115,6 +119,7 @@ export const EditorCore = forwardRef<WireframeEditorHandle, EditorCoreProps>(fun
lineWrapping,
ariaLabel,
onChange: (text) => change.current?.(text),
onCursorLine: (line) => cursor.current?.(line),
});
editor.current = instance;
return () => {
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export { WireframeViewer, type WireframeViewerProps } from "./viewer.js";
export { WireframeStreaming } from "./streaming.js";
export { tsquareComponents, createTsquareComponents, TsquarePre, type TsquareComponentsOptions } from "./markdown.js";
export type { Problem } from "./use-board.js";
export type { Measured, MeasuredBox, MeasuredElement, MeasuredFlow } from "tsquare";
Loading
Loading