Hakushi.Kit behaviour (Hakushi v0.6.0)

Copy Markdown View Source

A kit: the things of one kind of canvas, which the canvas draws, makes and edits by itself. A canvas with kits needs no :node slots and no new_node for them:

<.canvas id="model" graph={@graph} kits={[Hakushi.Kits.Schema]} on_change={&save/2} />
<.kit_items for="model" kits={[Hakushi.Kits.Schema]} />

Kits are optional: a canvas draws anything with :node slots, and a kit is a way to package a kind of canvas (its nodes, how people make and edit them, its paper and its rules) so that it is used in one line, by one app or many. Hakushi has six: Hakushi.Kits.Schema (tables, their columns and foreign keys), Hakushi.Kits.Flow (triggers, steps and conditions), Hakushi.Kits.Notes (sticky notes), Hakushi.Kits.Ink (drawing with a pen), Hakushi.Kits.Shapes (rectangles, ellipses, text, lines) and Hakushi.Kits.Images (image files); an app makes its own with the same callbacks. See the Kits guide.

A kit of the app's own

A kit is a module that uses Hakushi.Kit and implements its callbacks:

defmodule MyAppWeb.Kits.Roadmap do
  use Hakushi.Kit

  @impl true
  def name, do: "roadmap"
  @impl true
  def title(_messages), do: "Roadmap"
  @impl true
  def icon, do: "workflow"
  @impl true
  def types, do: ["roadmap-milestone"]
  @impl true
  def elements(_messages), do: [%{item: "milestone", label: "Milestone", icon: "zap"}]

  @impl true
  def new_node(_item, {x, y}, _messages),
    do: %{type: "roadmap-milestone", x: x, y: y, data: %{"title" => "Milestone"}}

  @impl true
  def node(assigns) do
    ~H"""
    <div class="milestone">
      <.handle kind={:target} side={:left} />
      <input
        :if={!@readonly}
        value={get(@node.data, "title")}
        phx-blur="hakushi:edit"
        phx-target={@target}
        phx-value-node={@node.id}
        phx-value-action="title"
      />
      <span :if={@readonly}>{get(@node.data, "title")}</span>
      <.handle kind={:source} side={:right} />
    </div>
    """
  end

  @impl true
  def edit("title", %{"value" => title}, data), do: {:ok, Map.put(data, "title", title)}
  def edit(_action, _params, _data), do: :error
end

The node's controls send "hakushi:edit" to the canvas (@target) with the node's id and an action; the canvas calls edit/3 with the node's data and puts the data it returns in the node, as a change of the person's own: checked by the rules and on_change, undone with ⌘Z, and shared with everyone on a shared canvas.

A kit's data has string keys, as a graph loaded from JSON has; get/2 reads a key of either kind.

Summary

Types

An element of a kit: an item of its bar (Hakushi.Components.kit_items/1).

Callbacks

The canvas the kit likes: its paper (background, grid, snap), the way the tidy up goes (layout, as the canvas's attribute: :right, :down, :force, :grid, or {:layered, :down}) and the rules of its edges (rules, a keyword list of Hakushi.Rules). Defaults to [].

A new node for a line drawn with a tool of the kit (tools/1), or nil. params are the line's "points" on the canvas ([x, y, pressure]), and the "color" and "size" chosen in the bar. A node with no id gets one; drawn over a group, it is in it.

The node's data after an action of its controls (params are the event's), or :error for an action the kit refuses.

The things people add, in the canvas's words.

The canvas of a diagram that to_markdown/1 wrote, or that a person wrote in the kit's kind of diagram: {:ok, graph}, or :error for a text the kit does not read.

Whether the nodes of a type are groups, which hold others (a lane, a frame), as a :node slot's group makes them. Defaults to false. See the Groups guide.

The kit's icon, a name of Hakushi.Icons.

A node's name for screen readers, where the canvas's node_label is its default: "Drawing". Defaults to the default's.

The kit's name, unique among a canvas's kits.

A new node for an element's item dropped at {x, y}, or for a double click on the paper (item is nil). A node with no id gets one.

Draws a node of the kit's types. The assigns are node (the Hakushi.Node), canvas (the canvas's id, for the ids of forms), readonly, target (the canvas, for phx-target) and messages (the canvas's words).

A node of an address (a URL) pasted on the canvas, as uploaded/4. It runs away from the LiveView (it may download), and the canvas adds its node when it is done.

Whether people resize the nodes of a type by their corners. Defaults to false.

The style of the kit's nodes that people change, in a panel over the selection, for one node or many, as one change: a list of groups, each with a key of the nodes' data, a label, a kind ("color", "width" or "choice") and its options (maps of value and label); or a field for a text ("text", with a placeholder), which sends its value as a choice does; or a button of the canvas's own for one node ("action", by its key: "crop" crops an image in place). A node shows its style in an element of its own with data-hakushi-style: JSON of the node's values, by the groups' keys. The panel reads it to show the groups and press the choices the nodes share; a group shows only when a selected node has its key there (an "action" too), so that a node with no such element has an empty panel

The kit's title, in the canvas's words (Hakushi.Messages).

The kit's nodes of graph as Markdown: a fenced diagram (Mermaid), so that a document with the canvas in it (a Kotoba embed) exports as text people read and Markdown viewers draw, and that from_markdown/1 reads back. nil when the graph has none of the kit's nodes.

The tools of the kit, for a bar on the canvas: a way to make its nodes by drawing them, as a pen draws ink (Hakushi.Kits.Ink). Defaults to none.

The node types the kit draws.

The files people drop, paste or pick on the canvas, for a kit whose nodes are made of them (Hakushi.Kits.Images): the options of a LiveView upload (Phoenix.LiveView.allow_upload/3's accept, max_file_size and max_entries), which the canvas allows for the first such kit of its kits. Defaults to none.

A node of an uploaded file, which the canvas adds where the person put it: {:ok, node}, or {:error, reason} for a file the kit refuses. path is the uploaded file, a temporary file; meta has the client's :name and :content_type (a claim); box is the place the person put it, [x, y, width, height], or nil.

Functions

A canvas from Markdown (a diagram of to_markdown/2): read by the kit it names (%% hakushi kit=...), or else by the first of kits that reads it. Gives {:ok, graph, kit} or :error.

The value of key in a node's data, whose keys are strings (a graph loaded from JSON) or atoms (a graph made in Elixir).

A canvas as Markdown: the diagram of each of kits that has nodes in graph (to_markdown/1), one after the other. nil when none has.

A word of messages, with its {name} filled.

Types

element()

@type element() :: %{item: String.t(), label: String.t(), icon: String.t()}

An element of a kit: an item of its bar (Hakushi.Components.kit_items/1).

Callbacks

canvas()

(optional)
@callback canvas() :: keyword()

The canvas the kit likes: its paper (background, grid, snap), the way the tidy up goes (layout, as the canvas's attribute: :right, :down, :force, :grid, or {:layered, :down}) and the rules of its edges (rules, a keyword list of Hakushi.Rules). Defaults to [].

The canvas's own attributes win; among kits, the first kit's paper and layout, and the rules of all of them (the first kit's, for a rule two kits set).

def canvas, do: [background: :lines, grid: 24, rules: [loops: false]]

drawn(tool, params, messages)

(optional)
@callback drawn(tool :: String.t(), params :: map(), messages :: map()) :: map() | nil

A new node for a line drawn with a tool of the kit (tools/1), or nil. params are the line's "points" on the canvas ([x, y, pressure]), and the "color" and "size" chosen in the bar. A node with no id gets one; drawn over a group, it is in it.

edit(action, params, data)

@callback edit(action :: String.t(), params :: map(), data :: map()) ::
  {:ok, map()} | :error

The node's data after an action of its controls (params are the event's), or :error for an action the kit refuses.

elements(messages)

@callback elements(messages :: map()) :: [element()]

The things people add, in the canvas's words.

from_markdown(text)

(optional)
@callback from_markdown(text :: String.t()) :: {:ok, Hakushi.Graph.t()} | :error

The canvas of a diagram that to_markdown/1 wrote, or that a person wrote in the kit's kind of diagram: {:ok, graph}, or :error for a text the kit does not read.

group?(type)

(optional)
@callback group?(type :: String.t()) :: boolean()

Whether the nodes of a type are groups, which hold others (a lane, a frame), as a :node slot's group makes them. Defaults to false. See the Groups guide.

icon()

@callback icon() :: String.t()

The kit's icon, a name of Hakushi.Icons.

label(node, messages)

(optional)
@callback label(node :: Hakushi.Node.t(), messages :: map()) :: String.t()

A node's name for screen readers, where the canvas's node_label is its default: "Drawing". Defaults to the default's.

name()

@callback name() :: String.t()

The kit's name, unique among a canvas's kits.

new_node(item, at, messages)

@callback new_node(
  item :: String.t() | nil,
  at :: {number(), number()},
  messages :: map()
) :: map() | nil

A new node for an element's item dropped at {x, y}, or for a double click on the paper (item is nil). A node with no id gets one.

node(assigns)

@callback node(assigns :: map()) :: Phoenix.LiveView.Rendered.t()

Draws a node of the kit's types. The assigns are node (the Hakushi.Node), canvas (the canvas's id, for the ids of forms), readonly, target (the canvas, for phx-target) and messages (the canvas's words).

pasted(url, box, messages)

(optional)
@callback pasted(url :: String.t(), box :: [number()] | nil, messages :: map()) ::
  {:ok, map()} | {:error, term()}

A node of an address (a URL) pasted on the canvas, as uploaded/4. It runs away from the LiveView (it may download), and the canvas adds its node when it is done.

resizable?(type)

(optional)
@callback resizable?(type :: String.t()) :: boolean()

Whether people resize the nodes of a type by their corners. Defaults to false.

styles(messages)

(optional)
@callback styles(messages :: map()) :: [map()]

The style of the kit's nodes that people change, in a panel over the selection, for one node or many, as one change: a list of groups, each with a key of the nodes' data, a label, a kind ("color", "width" or "choice") and its options (maps of value and label); or a field for a text ("text", with a placeholder), which sends its value as a choice does; or a button of the canvas's own for one node ("action", by its key: "crop" crops an image in place). A node shows its style in an element of its own with data-hakushi-style: JSON of the node's values, by the groups' keys. The panel reads it to show the groups and press the choices the nodes share; a group shows only when a selected node has its key there (an "action" too), so that a node with no such element has an empty panel:

<div class="milestone" data-hakushi-style={JSON.encode!(%{status: get(@node.data, "status")})}>

A choice sends "hakushi:edit" with the nodes, the action "style" and %{"style" => %{key => value}}, which edit/3 gets for each node. Defaults to none. See Hakushi.Kits.Shapes and the Kits guide.

title(messages)

@callback title(messages :: map()) :: String.t()

The kit's title, in the canvas's words (Hakushi.Messages).

to_markdown(graph)

(optional)
@callback to_markdown(graph :: Hakushi.Graph.t()) :: String.t() | nil

The kit's nodes of graph as Markdown: a fenced diagram (Mermaid), so that a document with the canvas in it (a Kotoba embed) exports as text people read and Markdown viewers draw, and that from_markdown/1 reads back. nil when the graph has none of the kit's nodes.

Write the places of the nodes in it (Hakushi.Mermaid.places/3), so that the canvas comes back as it was. See the Kits guide.

tools(messages)

(optional)
@callback tools(messages :: map()) :: [map()]

The tools of the kit, for a bar on the canvas: a way to make its nodes by drawing them, as a pen draws ink (Hakushi.Kits.Ink). Defaults to none.

A tool is a map with tool (its name in the kit), label, icon (a name of Hakushi.Icons), key (a letter that takes it, or nil) and mode, what the pointer does with it on the canvas:

  • "draw": draws a line, which becomes a node (drawn/3) when the pen lifts. Its options are colors (the names of the colours the bar has, --hakushi-ink-<name> in the CSS), color, sizes, size and flat (one width, whatever the pressure).
  • "erase": deletes the nodes of the kit's types that it passes over.
  • "lasso": selects what a line drawn around encloses.
  • "pick": opens the browser's picker of files, for a kit that takes them (upload/0); the pointer stays as it was.

The bar has the pointer first, which selects and drags as usual.

types()

@callback types() :: [String.t()]

The node types the kit draws.

upload()

(optional)
@callback upload() :: keyword()

The files people drop, paste or pick on the canvas, for a kit whose nodes are made of them (Hakushi.Kits.Images): the options of a LiveView upload (Phoenix.LiveView.allow_upload/3's accept, max_file_size and max_entries), which the canvas allows for the first such kit of its kits. Defaults to none.

uploaded(path, meta, box, messages)

(optional)
@callback uploaded(
  path :: Path.t(),
  meta :: map(),
  box :: [number()] | nil,
  messages :: map()
) ::
  {:ok, map()} | {:error, term()}

A node of an uploaded file, which the canvas adds where the person put it: {:ok, node}, or {:error, reason} for a file the kit refuses. path is the uploaded file, a temporary file; meta has the client's :name and :content_type (a claim); box is the place the person put it, [x, y, width, height], or nil.

Functions

from_markdown(kits, text)

@spec from_markdown([module()], String.t()) ::
  {:ok, Hakushi.Graph.t(), module()} | :error

A canvas from Markdown (a diagram of to_markdown/2): read by the kit it names (%% hakushi kit=...), or else by the first of kits that reads it. Gives {:ok, graph, kit} or :error.

get(data, key)

@spec get(map(), String.t()) :: term()

The value of key in a node's data, whose keys are strings (a graph loaded from JSON) or atoms (a graph made in Elixir).

to_markdown(kits, graph)

@spec to_markdown([module()], Hakushi.Graph.t()) :: String.t() | nil

A canvas as Markdown: the diagram of each of kits that has nodes in graph (to_markdown/1), one after the other. nil when none has.

Hakushi.Kit.to_markdown([Hakushi.Kits.Schema], graph)
#=> "```mermaid

%% hakushi kit=schema erDiagram ..."

word(messages, key, name)

@spec word(map(), String.t(), String.t()) :: String.t()

A word of messages, with its {name} filled.