@onioneko/boardkit-blocks 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/status.ts ADDED
@@ -0,0 +1,103 @@
1
+ import type { BlockType } from "@onioneko/boardkit-core";
2
+
3
+ /**
4
+ * starter block: status — an ordered state machine with a current value. The
5
+ * cross-field rule "value must be a member of states" is exactly what
6
+ * BlockType.validate exists for (JSON Schema cannot express it).
7
+ *
8
+ * Usage: `states` is an ordered list of allowed values; the `transition`
9
+ * affordance moves `value` to a new state, emitting `status.changed` with
10
+ * `from`/`to`. The html hook renders `<div class="status" data-state="…">`
11
+ * (style a state with `.status[data-state="…"]`) holding a badge and a button
12
+ * for every state other than the current one.
13
+ */
14
+
15
+ /** Attrs for the `status` block, mirroring its JSON Schema. */
16
+ export type StatusAttrs = {
17
+ readonly id: string;
18
+ readonly title?: string;
19
+ readonly states: readonly string[];
20
+ readonly value: string;
21
+ readonly note?: string;
22
+ };
23
+
24
+ export const statusBlock: BlockType<StatusAttrs> = {
25
+ type: "status",
26
+ schema: {
27
+ type: "object",
28
+ required: ["id", "states", "value"],
29
+ properties: {
30
+ id: { type: "string" },
31
+ title: { type: "string" },
32
+ states: { type: "array", items: { type: "string" }, minItems: 1 },
33
+ value: { type: "string" },
34
+ note: { type: "string" },
35
+ },
36
+ additionalProperties: false,
37
+ },
38
+ // Declared `attrs: unknown` on purpose, wider than `Readonly<StatusAttrs>`
39
+ // (a function accepting `unknown` is assignable wherever a narrower-param
40
+ // function is expected, so this still satisfies `BlockType<StatusAttrs>`).
41
+ // `validate` is called directly by hosts and tests with arbitrary/possibly
42
+ // malformed data (its own doc contract says "never throws" — see
43
+ // BlockType.validate in core), so the null/non-object case is guarded
44
+ // before any property access, not just trusted from the generic.
45
+ validate: (attrs: unknown) => {
46
+ if (typeof attrs !== "object" || attrs === null) return [];
47
+ const { value, states } = attrs as { value?: unknown; states?: unknown };
48
+ if (Array.isArray(states) && typeof value === "string" && !states.includes(value)) {
49
+ return [{ code: "E_STATUS_VALUE", message: `value "${value}" is not in states` }];
50
+ }
51
+ return [];
52
+ },
53
+ transitions: [{ attr: "value", event: "status.changed" }],
54
+ affordances: [
55
+ {
56
+ name: "transition",
57
+ params: {
58
+ type: "object",
59
+ required: ["to"],
60
+ properties: { to: { type: "string" } },
61
+ additionalProperties: false,
62
+ },
63
+ patch: (_attrs, params) => ({ value: (params as { to: string }).to }),
64
+ },
65
+ ],
66
+ project: {
67
+ text: (attrs) => {
68
+ const value = String(attrs.value);
69
+ const title = String(attrs.title ?? "").trim();
70
+ return title === "" ? `**STATUS**: ${value}` : `**STATUS**: ${value} — ${title}`;
71
+ },
72
+ html: (attrs): import("hast").Nodes => {
73
+ const states = attrs.states;
74
+ const value = String(attrs.value);
75
+ const buttons: import("hast").ElementContent[] = states
76
+ .filter((state) => state !== value)
77
+ .map((state): import("hast").ElementContent => ({
78
+ type: "element",
79
+ tagName: "button",
80
+ properties: {
81
+ "data-intent": JSON.stringify({ affordance: "transition", params: { to: state } }),
82
+ },
83
+ children: [{ type: "text", value: state }],
84
+ }));
85
+ return {
86
+ type: "element",
87
+ tagName: "div",
88
+ // The state is data, not styling: one attribute holds it whatever it
89
+ // contains, and the class list stays fixed.
90
+ properties: { className: ["status"], "data-state": value },
91
+ children: [
92
+ {
93
+ type: "element",
94
+ tagName: "span",
95
+ properties: { className: ["badge"] },
96
+ children: [{ type: "text", value }],
97
+ },
98
+ ...buttons,
99
+ ],
100
+ };
101
+ },
102
+ },
103
+ };