@shortlink-org/portolan 0.2.2 → 0.2.4

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.
Files changed (84) hide show
  1. package/README.md +7 -2
  2. package/cli/portolan.mjs +36 -6
  3. package/package.json +4 -4
  4. package/plugins/README.md +5 -0
  5. package/plugins/extract-python-kafka/README.md +6 -0
  6. package/plugins/extract-python-kafka/extract.py +2 -2
  7. package/plugins/extract-python-kafka/extract_test.py +17 -1
  8. package/plugins/portolan-go.wasm +0 -0
  9. package/plugins/pyplugin/catalog.py +12 -1
  10. package/plugins/pyplugin/kafka.py +74 -3
  11. package/scripts/builtin-plugins.mjs +3 -2
  12. package/scripts/catalog-sources.mjs +2 -1
  13. package/scripts/catalog-sources.test.mjs +11 -1
  14. package/scripts/delivery-presets.mjs +207 -24
  15. package/scripts/diff.mjs +5 -1
  16. package/scripts/gen-likec4.mjs +78 -20
  17. package/scripts/gen-likec4.test.mjs +25 -2
  18. package/scripts/local-api.mjs +26 -377
  19. package/scripts/local-api.test.mjs +50 -0
  20. package/scripts/local-discovery.mjs +378 -0
  21. package/scripts/manifest.mjs +42 -1
  22. package/scripts/manifest.test.mjs +24 -1
  23. package/scripts/run-builtin.mjs +4 -2
  24. package/scripts/schema.mjs +4 -0
  25. package/scripts/site-docs.mjs +2 -2
  26. package/src/app/Breadcrumbs.test.ts +4 -0
  27. package/src/app/Breadcrumbs.tsx +1 -0
  28. package/src/app/CatalogApp.tsx +4 -4
  29. package/src/app/Sidebar.tsx +13 -730
  30. package/src/app/SidebarFlowSections.tsx +251 -0
  31. package/src/app/SidebarFooter.tsx +160 -0
  32. package/src/app/SidebarTree.tsx +322 -0
  33. package/src/catalog-index.ts +485 -0
  34. package/src/catalog-model.ts +1360 -0
  35. package/src/catalog-stores.test.ts +17 -0
  36. package/src/catalog-validation.ts +1611 -0
  37. package/src/catalog.test.ts +29 -2
  38. package/src/catalog.ts +6 -3306
  39. package/src/components/ChannelRows.messagepack.test.tsx +28 -0
  40. package/src/components/ChannelRows.test.tsx +54 -0
  41. package/src/components/ChannelRows.tsx +57 -10
  42. package/src/components/LifecycleDiagram.tsx +28 -12
  43. package/src/components/{PageHeader.test.ts → PageHeader.test.tsx} +9 -10
  44. package/src/components/ProblemRow.tsx +7 -0
  45. package/src/components/SourcePreview.tsx +1 -1
  46. package/src/components/WhatLinksHere.tsx +6 -4
  47. package/src/enrich.test.ts +4 -5
  48. package/src/flow/StepDetail.tsx +98 -54
  49. package/src/flow/answers.test.ts +18 -1
  50. package/src/flow/answers.ts +37 -8
  51. package/src/index.css +0 -17
  52. package/src/landing/LandingPage.tsx +4 -4
  53. package/src/lib/all-problems.ts +1 -1
  54. package/src/lib/backlinks.test.ts +16 -1
  55. package/src/lib/backlinks.ts +20 -0
  56. package/src/lib/catalog-diff.test.ts +18 -0
  57. package/src/lib/catalog-diff.ts +19 -1
  58. package/src/lib/derive.ts +2 -0
  59. package/src/lib/kafka-ui.test.ts +87 -0
  60. package/src/lib/kafka-ui.ts +105 -0
  61. package/src/lib/local-api.ts +19 -7
  62. package/src/lib/motion.test.ts +4 -2
  63. package/src/lib/motion.tsx +5 -4
  64. package/src/lib/proto-problems.test.ts +170 -3
  65. package/src/lib/proto-problems.ts +176 -4
  66. package/src/lib/wire-problems.test.ts +21 -0
  67. package/src/lib/wire-problems.ts +62 -1
  68. package/src/likec4/FlowView.tsx +2 -6
  69. package/src/likec4/flow-edges.test.ts +64 -1
  70. package/src/likec4/flow-edges.ts +43 -7
  71. package/src/likec4/view-index.ts +8 -2
  72. package/src/merge.test.ts +69 -0
  73. package/src/merge.ts +52 -2
  74. package/src/pages/CatalogFailure.tsx +2 -2
  75. package/src/pages/Settings.tsx +12 -129
  76. package/src/pages/settings/AboutSettings.tsx +129 -0
  77. package/src/pages/settings/DeliverySettings.tsx +71 -15
  78. package/src/pages/settings/IntegrationsSettings.tsx +117 -0
  79. package/src/routes.test.ts +2 -0
  80. package/src/routes.ts +2 -1
  81. package/src/selection/DetailPanel.tsx +46 -1
  82. package/src/selection/pages.test.ts +14 -1
  83. package/src/selection/pages.ts +9 -3
  84. package/vite.config.ts +3 -1
@@ -0,0 +1,322 @@
1
+ import type React from "react";
2
+ import { ChevronRight } from "lucide-react";
3
+ import { NavLink, useLocation, useMatch } from "react-router";
4
+
5
+ import { KindIcon } from "../components/kind";
6
+ import type { LeafKind } from "../lib/kinds";
7
+ import { m, transitions, Unfold } from "../lib/motion";
8
+ import { selectionHash } from "../selection/hash";
9
+ import { selectionFor } from "../selection/model";
10
+ import { selectsInPlace } from "../selection/pages";
11
+ import { useSelectionStore } from "../selection/store";
12
+
13
+ export const indent = (depth: number) => 8 + depth * 12;
14
+
15
+ /**
16
+ * The selection's light: the raised background and the accent edge of the
17
+ * row that is selected. One element for the whole tree - a new selection is
18
+ * the same light arriving on another row, and it travels there on the settle
19
+ * spring rather than going out here and coming on over there. `-left-0.5`
20
+ * puts its edge over the row's own transparent 2px border, so the text does
21
+ * not move.
22
+ */
23
+ function SelectionLight() {
24
+ return (
25
+ <m.span
26
+ layoutId="selection"
27
+ aria-hidden
28
+ className="pointer-events-none absolute inset-y-0 right-0 -left-0.5 -z-10 border-l-2 border-accent"
29
+ style={{ background: "var(--surface-2)" }}
30
+ transition={transitions.settle}
31
+ />
32
+ );
33
+ }
34
+ export function Chevron({ open }: { open: boolean }) {
35
+ return (
36
+ <ChevronRight
37
+ size={14}
38
+ aria-hidden
39
+ className="block shrink-0 t-micro transition-transform"
40
+ style={{
41
+ transform: open ? "rotate(90deg)" : "none",
42
+ color: "var(--fg-muted)",
43
+ }}
44
+ />
45
+ );
46
+ }
47
+
48
+ /**
49
+ * Clicking an unselected row selects it. Whether it also navigates is the
50
+ * question the page on screen answers: on a flow page, picking something that
51
+ * flow already draws is a question about that sequence, so the reader stays
52
+ * put. Clicking the highlighted row again follows its link to its own page.
53
+ *
54
+ * Only the in-place case writes the store here. A row that navigates carries
55
+ * its selection in the link's hash instead (see `rowTarget`): writing the
56
+ * store first and then letting the link go would race SelectionSync, whose
57
+ * `replace` lands with the pathname it rendered with - the page being left -
58
+ * and undoes the navigation. A selection in the URL wins on arrival, so one
59
+ * write does both jobs. The palette works the same way.
60
+ */
61
+ function useRowClick(selId: string | undefined) {
62
+ const select = useSelectionStore((s) => s.select);
63
+ const { pathname } = useLocation();
64
+ return (e: React.MouseEvent) => {
65
+ if (selId === undefined) return;
66
+ const selection = selectionFor(selId);
67
+ const current = useSelectionStore.getState().selection;
68
+ if (!selectsInPlace(pathname, selection, current)) return;
69
+ e.preventDefault();
70
+ select(selId, "sidebar");
71
+ };
72
+ }
73
+
74
+ /** Where a row's link goes: its page, with the row's selection in the hash. */
75
+ function rowTarget(to: string, selId: string | undefined): string {
76
+ if (selId === undefined || to.includes("#")) return to;
77
+ return `${to}${selectionHash(selectionFor(selId))}`;
78
+ }
79
+
80
+ /**
81
+ * A leaf: the whole row navigates. Rows that stand for a selectable entity also
82
+ * carry `selId`, which makes them both a writer of the selection and a reader
83
+ * of it - the row lights up whether it was clicked here, on a diagram, or in
84
+ * the palette.
85
+ */
86
+ export function Leaf({
87
+ to,
88
+ depth,
89
+ children,
90
+ title,
91
+ selId,
92
+ }: {
93
+ to: string;
94
+ depth: number;
95
+ children: React.ReactNode;
96
+ title?: string;
97
+ selId?: string;
98
+ }) {
99
+ const selected = useSelectionStore(
100
+ (s) => selId !== undefined && s.selection?.id === selId,
101
+ );
102
+ const onClick = useRowClick(selId);
103
+ return (
104
+ <NavLink
105
+ to={rowTarget(to, selId)}
106
+ end
107
+ title={title}
108
+ data-sel={selId}
109
+ data-tree-row
110
+ data-tree-depth={depth}
111
+ onClick={onClick}
112
+ style={({ isActive }) => ({
113
+ paddingLeft: indent(depth),
114
+ background: isActive && !selected ? "var(--surface-2)" : undefined,
115
+ borderLeftWidth: 2,
116
+ borderLeftStyle: "solid",
117
+ borderLeftColor:
118
+ isActive && !selected ? "var(--accent)" : "transparent",
119
+ })}
120
+ /* The edge is always 2px, transparent when idle: lighting a row must not
121
+ shift the text beside it. The route's own row paints its light in
122
+ place; the SELECTION's light is one element that slides from the row
123
+ it was on to this one. */
124
+ data-nav-item
125
+ className="tree-row relative isolate flex items-center gap-1.5 py-[3px] pr-2 t-micro transition-colors hover:bg-surface"
126
+ >
127
+ {selected ? <SelectionLight /> : null}
128
+ {children}
129
+ </NavLink>
130
+ );
131
+ }
132
+
133
+ /**
134
+ * A branch: a disclosure triangle that only expands, next to a link that only
135
+ * navigates. Keeping them separate means neither is nested inside the other.
136
+ *
137
+ * `end` is for controls that must be their own target - the unresolved-edge
138
+ * count on a context, which goes somewhere else entirely. It sits OUTSIDE the
139
+ * link, because an anchor may not contain another one, and the link gives up
140
+ * its claim on the rest of the row to make room for it.
141
+ */
142
+ export function Branch({
143
+ to,
144
+ depth,
145
+ open,
146
+ onToggle,
147
+ label,
148
+ children,
149
+ right,
150
+ end,
151
+ under,
152
+ selId,
153
+ }: {
154
+ to: string;
155
+ depth: number;
156
+ open: boolean;
157
+ onToggle: () => void;
158
+ label: string;
159
+ children: React.ReactNode;
160
+ right?: React.ReactNode;
161
+ end?: React.ReactNode;
162
+ under?: React.ReactNode;
163
+ selId?: string;
164
+ }) {
165
+ const selected = useSelectionStore(
166
+ (s) => selId !== undefined && s.selection?.id === selId,
167
+ );
168
+ const onClick = useRowClick(selId);
169
+ const active = useMatch({ path: to, end: true }) !== null && !selected;
170
+ return (
171
+ <>
172
+ <div
173
+ className="tree-row group relative isolate flex items-stretch t-micro transition-colors hover:bg-surface"
174
+ style={{
175
+ paddingLeft: indent(depth),
176
+ background: active ? "var(--surface-2)" : undefined,
177
+ borderLeftWidth: 2,
178
+ borderLeftStyle: "solid",
179
+ borderLeftColor: active ? "var(--accent)" : "transparent",
180
+ }}
181
+ >
182
+ {selected ? <SelectionLight /> : null}
183
+ <button
184
+ type="button"
185
+ onClick={onToggle}
186
+ aria-expanded={open}
187
+ aria-label={`${open ? "Collapse" : "Expand"} ${label}`}
188
+ data-tree-toggle
189
+ className="flex shrink-0 items-center pr-1"
190
+ >
191
+ <Chevron open={open} />
192
+ </button>
193
+ <NavLink
194
+ to={rowTarget(to, selId)}
195
+ end
196
+ data-sel={selId}
197
+ data-nav-item
198
+ data-tree-row
199
+ data-tree-depth={depth}
200
+ data-tree-open={open}
201
+ onClick={onClick}
202
+ className={`flex min-w-0 items-center gap-1.5 py-[3px] pr-2 ${
203
+ end ? "shrink" : "flex-1"
204
+ }`}
205
+ >
206
+ {children}
207
+ {right && !end ? (
208
+ <span className="ml-auto shrink-0">{right}</span>
209
+ ) : null}
210
+ </NavLink>
211
+ {end}
212
+ {right && end ? (
213
+ <span className="flex shrink-0 items-center pr-2">{right}</span>
214
+ ) : null}
215
+ </div>
216
+ {under}
217
+ </>
218
+ );
219
+ }
220
+
221
+ /**
222
+ * A group header inside an aggregate - "value objects (3)". It names a kind
223
+ * rather than a thing, so it has no page and does nothing but open and close.
224
+ */
225
+ export function Group({
226
+ kind,
227
+ label,
228
+ count,
229
+ open,
230
+ onToggle,
231
+ depth,
232
+ children,
233
+ }: {
234
+ kind: LeafKind;
235
+ /** Overrides the kind's own plural — the stores group is "data", not "tables". */
236
+ label?: string;
237
+ count: number;
238
+ open: boolean;
239
+ onToggle: () => void;
240
+ depth: number;
241
+ children: React.ReactNode;
242
+ }) {
243
+ if (count === 0) return null;
244
+ return (
245
+ <>
246
+ <button
247
+ type="button"
248
+ onClick={onToggle}
249
+ aria-expanded={open}
250
+ data-tree-row
251
+ data-tree-depth={depth}
252
+ data-tree-open={open}
253
+ style={{ paddingLeft: indent(depth) }}
254
+ className="tree-row group-row"
255
+ >
256
+ <Chevron open={open} />
257
+ <KindIcon kind={kind} />
258
+ <span>
259
+ {label ?? KIND_GROUP_LABEL[kind]} ({count})
260
+ </span>
261
+ </button>
262
+ <Unfold open={open}>{children}</Unfold>
263
+ </>
264
+ );
265
+ }
266
+
267
+ const KIND_GROUP_LABEL: Record<LeafKind, string> = {
268
+ vo: "value objects",
269
+ entity: "entities",
270
+ enum: "enums",
271
+ event: "events",
272
+ command: "commands",
273
+ query: "queries",
274
+ endpoint: "api",
275
+ table: "tables",
276
+ view: "views",
277
+ };
278
+
279
+ // ---------------------------------------------------------------------------
280
+ // Sections
281
+ // ---------------------------------------------------------------------------
282
+
283
+ /**
284
+ * A top-level band of the tree. Its header folds it and always states its size:
285
+ * a folded section that also hid its count would be indistinguishable from an
286
+ * empty one, and the reader would have to open it to find out which.
287
+ *
288
+ * 12px above the header and nothing else between sections. The tree is one
289
+ * continuous list; boxing each band would make a reader count boxes.
290
+ */
291
+ export function Section({
292
+ title,
293
+ count,
294
+ open,
295
+ onToggle,
296
+ first = false,
297
+ children,
298
+ }: {
299
+ title: string;
300
+ count: number;
301
+ open: boolean;
302
+ onToggle: () => void;
303
+ /** The first section needs no air above it - the header is already there. */
304
+ first?: boolean;
305
+ children: React.ReactNode;
306
+ }) {
307
+ return (
308
+ <div className={first ? "" : "mt-3"}>
309
+ <button
310
+ type="button"
311
+ onClick={onToggle}
312
+ aria-expanded={open}
313
+ className="sb-section"
314
+ >
315
+ <Chevron open={open} />
316
+ {title}
317
+ <span className="sb-count">{count}</span>
318
+ </button>
319
+ <Unfold open={open}>{children}</Unfold>
320
+ </div>
321
+ );
322
+ }