@stephen-netu/design-system 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.
Files changed (204) hide show
  1. package/README.md +66 -0
  2. package/package.json +130 -0
  3. package/src/b0-app-shell/clarification-overlay/ClarificationOverlay.svelte +221 -0
  4. package/src/b0-app-shell/clarification-overlay/index.ts +1 -0
  5. package/src/b0-app-shell/command-bar/CommandBar.svelte +565 -0
  6. package/src/b0-app-shell/index.ts +14 -0
  7. package/src/base.css +152 -0
  8. package/src/d0-data-viz/BarChart.svelte +236 -0
  9. package/src/d0-data-viz/CheckpointBar.svelte +574 -0
  10. package/src/d0-data-viz/CheckpointPill.svelte +297 -0
  11. package/src/d0-data-viz/MetricCard.svelte +159 -0
  12. package/src/d0-data-viz/SparkLine.svelte +66 -0
  13. package/src/d0-data-viz/StorageMap.svelte +182 -0
  14. package/src/d0-data-viz/Timeline.svelte +188 -0
  15. package/src/d0-data-viz/checkpoint-types.ts +162 -0
  16. package/src/d0-data-viz/d0-tokens.css +32 -0
  17. package/src/d0-data-viz/index.ts +58 -0
  18. package/src/d0-data-viz/storage-map-types.ts +142 -0
  19. package/src/d0-data-viz/theme-integration.ts +134 -0
  20. package/src/d0-data-viz/types.ts +53 -0
  21. package/src/demos/BspSizingInfographic.svelte +386 -0
  22. package/src/demos/BspZoneDemo.svelte +255 -0
  23. package/src/demos/ControlRoomDemo.svelte +570 -0
  24. package/src/demos/NodepadBlocksShowcase.svelte +189 -0
  25. package/src/demos/index.ts +7 -0
  26. package/src/editor/CodeEditor.svelte +188 -0
  27. package/src/editor/FindReplaceDialog.svelte +516 -0
  28. package/src/editor/MarkdownEditor.svelte +660 -0
  29. package/src/editor/index.ts +2 -0
  30. package/src/entity-graph-view/WorldView.svelte +171 -0
  31. package/src/entity-graph-view/index.ts +6 -0
  32. package/src/entity-graph-view/types.ts +364 -0
  33. package/src/epistemic/index.ts +2 -0
  34. package/src/epistemic/types.ts +134 -0
  35. package/src/epistemic/utils.ts +32 -0
  36. package/src/f0-forms/checkbox/Checkbox.svelte +158 -0
  37. package/src/f0-forms/field/FormField.svelte +91 -0
  38. package/src/f0-forms/index.ts +11 -0
  39. package/src/f0-forms/radio/RadioGroup.svelte +192 -0
  40. package/src/f0-forms/search/SearchBar.svelte +99 -0
  41. package/src/f0-forms/select/Select.svelte +198 -0
  42. package/src/f0-forms/textarea/TextArea.svelte +170 -0
  43. package/src/f0-forms/toggle/FormToggle.svelte +147 -0
  44. package/src/f0-forms/validation/validate.ts +47 -0
  45. package/src/fabric/ai/ChatPanel.svelte +648 -0
  46. package/src/fabric/ai/index.ts +11 -0
  47. package/src/fabric/data/index.ts +21 -0
  48. package/src/fabric/feedback/StatusBar.svelte +274 -0
  49. package/src/fabric/feedback/ToastContainer.svelte +122 -0
  50. package/src/fabric/feedback/index.ts +14 -0
  51. package/src/fabric/index.ts +37 -0
  52. package/src/fabric/input/CommandBar.svelte +79 -0
  53. package/src/fabric/input/KeyboardShortcuts.svelte +388 -0
  54. package/src/fabric/input/index.ts +12 -0
  55. package/src/fabric/layout/GlassBox.svelte +193 -0
  56. package/src/fabric/layout/KanbanBoard.svelte +158 -0
  57. package/src/fabric/layout/KanbanCard.svelte +226 -0
  58. package/src/fabric/layout/KanbanColumn.svelte +278 -0
  59. package/src/fabric/layout/index.ts +17 -0
  60. package/src/fabric/navigation/ActivityRail.svelte +271 -0
  61. package/src/fabric/navigation/index.ts +11 -0
  62. package/src/g0-geo/SovereignMap.svelte +149 -0
  63. package/src/g0-geo/index.ts +2 -0
  64. package/src/index.ts +68 -0
  65. package/src/l0-layout/block-writer/BlockConnectionLines.svelte +98 -0
  66. package/src/l0-layout/block-writer/BlockContent.svelte +213 -0
  67. package/src/l0-layout/block-writer/BlockHeader.svelte +230 -0
  68. package/src/l0-layout/block-writer/BlockItem.svelte +177 -0
  69. package/src/l0-layout/block-writer/BlockWriter.svelte +219 -0
  70. package/src/l0-layout/block-writer/block-writer-types.ts +63 -0
  71. package/src/l0-layout/block-writer/index.ts +24 -0
  72. package/src/l0-layout/block-writer/markdown.ts +110 -0
  73. package/src/l0-layout/chat-view/ChatViewLayout.svelte +61 -0
  74. package/src/l0-layout/epistemic/AllayDetailPanel.svelte +113 -0
  75. package/src/l0-layout/epistemic/AssayDetailPanel.svelte +164 -0
  76. package/src/l0-layout/epistemic/AvenueTrace.svelte +253 -0
  77. package/src/l0-layout/epistemic/KnowledgeDetailPanel.svelte +204 -0
  78. package/src/l0-layout/epistemic/ParadigmLens.svelte +170 -0
  79. package/src/l0-layout/epistemic/ProvenanceChain.svelte +164 -0
  80. package/src/l0-layout/epistemic/TrustView.svelte +195 -0
  81. package/src/l0-layout/epistemic/index.ts +7 -0
  82. package/src/l0-layout/grove/AccordCard.svelte +156 -0
  83. package/src/l0-layout/grove/GroveCard.svelte +197 -0
  84. package/src/l0-layout/grove/MemberBadge.svelte +96 -0
  85. package/src/l0-layout/grove/MemberList.svelte +59 -0
  86. package/src/l0-layout/grove/grove-types.ts +29 -0
  87. package/src/l0-layout/grove/index.ts +14 -0
  88. package/src/l0-layout/index.ts +45 -0
  89. package/src/l0-layout/kanban/KanbanBoard.svelte +71 -0
  90. package/src/l0-layout/kanban/KanbanCard.svelte +276 -0
  91. package/src/l0-layout/kanban/KanbanColumn.svelte +198 -0
  92. package/src/l0-layout/kanban/KanbanViewLayout.svelte +61 -0
  93. package/src/l0-layout/workspace/WorkspaceLayout.svelte +93 -0
  94. package/src/n0-node-graph/GraphEditor.svelte +342 -0
  95. package/src/n0-node-graph/GraphHistory.svelte +122 -0
  96. package/src/n0-node-graph/Node.svelte +269 -0
  97. package/src/n0-node-graph/NodeCanvas.svelte +187 -0
  98. package/src/n0-node-graph/NodeEdge.svelte +152 -0
  99. package/src/n0-node-graph/NodePalette.svelte +318 -0
  100. package/src/n0-node-graph/NodePort.svelte +128 -0
  101. package/src/n0-node-graph/index.ts +33 -0
  102. package/src/n0-node-graph/n0-tokens.css +29 -0
  103. package/src/n0-node-graph/node-port-types.ts +19 -0
  104. package/src/n0-node-graph/types.ts +96 -0
  105. package/src/n0-node-graph/validate.test.ts +722 -0
  106. package/src/n0-node-graph/validate.ts +371 -0
  107. package/src/n1-flow-canvas/FlowCanvas.svelte +202 -0
  108. package/src/n1-flow-canvas/graph-state.svelte.ts +95 -0
  109. package/src/n1-flow-canvas/index.ts +21 -0
  110. package/src/n1-flow-canvas/layout.ts +118 -0
  111. package/src/n1-flow-canvas/layout.worker.ts +128 -0
  112. package/src/n1-flow-canvas/types.ts +102 -0
  113. package/src/n1-flow-canvas/utils.ts +146 -0
  114. package/src/n2-manifold-disk/ManifoldDisk.svelte +314 -0
  115. package/src/n2-manifold-disk/index.ts +6 -0
  116. package/src/n2-manifold-disk/poincare.ts +184 -0
  117. package/src/n3-force-graph/ForceGraph.svelte +180 -0
  118. package/src/n3-force-graph/GraphTooltip.svelte +99 -0
  119. package/src/n3-force-graph/animated-edge.ts +107 -0
  120. package/src/n3-force-graph/health-node.ts +128 -0
  121. package/src/n3-force-graph/index.ts +31 -0
  122. package/src/n3-force-graph/types.ts +94 -0
  123. package/src/n4-adaptive-manifold/AdaptiveManifold.svelte +1165 -0
  124. package/src/n4-adaptive-manifold/DetailCard.svelte +212 -0
  125. package/src/n4-adaptive-manifold/SuggestionIndicator.svelte +91 -0
  126. package/src/n4-adaptive-manifold/classifier.ts +139 -0
  127. package/src/n4-adaptive-manifold/index.ts +45 -0
  128. package/src/n4-adaptive-manifold/n4-tokens.css +155 -0
  129. package/src/n4-adaptive-manifold/orchestrator.ts +301 -0
  130. package/src/n4-adaptive-manifold/phase-theme.ts +92 -0
  131. package/src/n4-adaptive-manifold/pinnacle-events.ts +135 -0
  132. package/src/n4-adaptive-manifold/providers/gravitational-provider.ts +191 -0
  133. package/src/n4-adaptive-manifold/providers/manifold-provider.ts +276 -0
  134. package/src/n4-adaptive-manifold/transition.ts +90 -0
  135. package/src/n4-adaptive-manifold/types.ts +313 -0
  136. package/src/p0-primitives/index.ts +6 -0
  137. package/src/p0-primitives/tokens/control-room.css +309 -0
  138. package/src/p0-primitives/tokens/index.ts +6 -0
  139. package/src/p0-primitives/tokens/tokens.css +140 -0
  140. package/src/p0-primitives/tokens/tokens.json +132 -0
  141. package/src/p0-primitives/types/index.ts +123 -0
  142. package/src/p0-primitives/types/plugin-types.ts +74 -0
  143. package/src/p0-primitives/utils/index.ts +39 -0
  144. package/src/platform/hud-protocol.ts +20 -0
  145. package/src/platform/index.ts +3 -0
  146. package/src/s0-state/create-store.ts +84 -0
  147. package/src/s0-state/derived.ts +41 -0
  148. package/src/s0-state/index.ts +6 -0
  149. package/src/showcase/Showcase.svelte +401 -0
  150. package/src/spatial/BspTilingCanvas.svelte +232 -0
  151. package/src/spatial/ForceCanvas.svelte +289 -0
  152. package/src/spatial/LodRenderer.svelte +334 -0
  153. package/src/spatial/SnapZoomCamera.svelte +185 -0
  154. package/src/spatial/ZoneTiler.svelte +311 -0
  155. package/src/spatial/camera-types.ts +45 -0
  156. package/src/spatial/content-proportional-bsp.ts +253 -0
  157. package/src/spatial/force-canvas-types.ts +102 -0
  158. package/src/spatial/index.ts +15 -0
  159. package/src/spatial/lod-types.ts +70 -0
  160. package/src/spatial/zone-tiler-types.ts +177 -0
  161. package/src/stores/vaultStore.svelte +14 -0
  162. package/src/t0-transport/connection.ts +34 -0
  163. package/src/t0-transport/error.ts +37 -0
  164. package/src/t0-transport/events.ts +50 -0
  165. package/src/t0-transport/index.ts +6 -0
  166. package/src/t0-transport/invoke.ts +30 -0
  167. package/src/tauri/index.ts +13 -0
  168. package/src/u0-primitives/allay-gauge/AllayGauge.svelte +90 -0
  169. package/src/u0-primitives/allay-gauge/index.ts +1 -0
  170. package/src/u0-primitives/animated-icon/AnimatedIcon.svelte +273 -0
  171. package/src/u0-primitives/animated-icon/README.md +53 -0
  172. package/src/u0-primitives/animated-icon/index.ts +31 -0
  173. package/src/u0-primitives/assay-badge/AssayBadge.svelte +121 -0
  174. package/src/u0-primitives/assay-badge/AssaySignalBar.svelte +83 -0
  175. package/src/u0-primitives/assay-badge/index.ts +2 -0
  176. package/src/u0-primitives/avatar/Avatar.svelte +177 -0
  177. package/src/u0-primitives/badge/Badge.svelte +149 -0
  178. package/src/u0-primitives/button/Button.svelte +304 -0
  179. package/src/u0-primitives/card/Card.svelte +182 -0
  180. package/src/u0-primitives/dropdown/Dropdown.svelte +305 -0
  181. package/src/u0-primitives/expert-badge/ExpertBadge.svelte +89 -0
  182. package/src/u0-primitives/expert-badge/index.ts +1 -0
  183. package/src/u0-primitives/index.ts +35 -0
  184. package/src/u0-primitives/input/Input.svelte +237 -0
  185. package/src/u0-primitives/modal/Modal.svelte +331 -0
  186. package/src/u0-primitives/modal/index.ts +1 -0
  187. package/src/u0-primitives/spinner/Spinner.svelte +129 -0
  188. package/src/u0-primitives/tabs/Tabs.svelte +225 -0
  189. package/src/u0-primitives/tabs/tabs-types.ts +11 -0
  190. package/src/u0-primitives/toggle/Toggle.svelte +161 -0
  191. package/src/u0-primitives/tooltip/Tooltip.svelte +234 -0
  192. package/src/v0-diagrams/DiagramPanel.svelte +68 -0
  193. package/src/v0-diagrams/MarkmapDiagram.svelte +56 -0
  194. package/src/v0-diagrams/MermaidDiagram.svelte +53 -0
  195. package/src/v0-diagrams/index.ts +4 -0
  196. package/src/v0-diagrams/types.ts +28 -0
  197. package/src/v0-diagrams/v0-tokens.css +6 -0
  198. package/src/vite-env.d.ts +7 -0
  199. package/src/x0-enchanted-blocks/ENCHANTED_BLOCKS_SPEC.md +457 -0
  200. package/src/x0-enchanted-blocks/components/EnchantedBlock.svelte +485 -0
  201. package/src/x0-enchanted-blocks/components/NodepadBlock.svelte +262 -0
  202. package/src/x0-enchanted-blocks/core/block-layout.ts +211 -0
  203. package/src/x0-enchanted-blocks/core/block-types.ts +227 -0
  204. package/src/x0-enchanted-blocks/index.ts +46 -0
@@ -0,0 +1,262 @@
1
+ <script lang="ts">
2
+ // NodepadBlock — Flat, inline-editing block inspired by Nodepad spatial notes
3
+ // Key differences from bad first attempt:
4
+ // - NO nested card containers (single flat element)
5
+ // - NO edit mode toggle (just click and type)
6
+ // - NO collapse button (content flows naturally)
7
+ // - Stable cursor (no $effect overwriting content while typing)
8
+
9
+ import { measureBlock, type BlockMetrics, DEFAULT_FONT } from '../core/block-layout.js';
10
+ import {
11
+ type BlockType,
12
+ type EnchantedBlockData,
13
+ BLOCK_TYPE_COLORS,
14
+ BLOCK_TYPE_ICONS,
15
+ classifyBlockHeuristic,
16
+ } from '../core/block-types.js';
17
+
18
+ interface Props {
19
+ block: EnchantedBlockData;
20
+ maxWidth?: number;
21
+ onChange?: (content: string) => void;
22
+ onTypeChange?: (type: BlockType) => void;
23
+ }
24
+
25
+ let {
26
+ block,
27
+ maxWidth = 320,
28
+ onChange,
29
+ onTypeChange,
30
+ }: Props = $props();
31
+
32
+ // ── Stable Local State ─────────────────────────────────────────────────────
33
+ // CRITICAL: Local content copy prevents parent re-renders from resetting cursor
34
+ let localContent = $state('');
35
+ let isFocused = $state(false);
36
+ let hasUnsavedChanges = $state(false);
37
+ let contentEl: HTMLDivElement | undefined = $state();
38
+
39
+ // Initialize content on mount (once only)
40
+ $effect(() => {
41
+ if (contentEl && localContent === '') {
42
+ localContent = block.content;
43
+ contentEl.innerText = block.content;
44
+ }
45
+ });
46
+
47
+ // ── Derived ────────────────────────────────────────────────────────────────
48
+ const metrics = $derived<BlockMetrics>(
49
+ measureBlock(localContent, DEFAULT_FONT, maxWidth, { whiteSpace: 'pre-wrap' })
50
+ );
51
+
52
+ const suggestedType = $derived(
53
+ block.type === 'general' && localContent.length > 10
54
+ ? classifyBlockHeuristic(localContent)
55
+ : null
56
+ );
57
+
58
+ const displayType = $derived(suggestedType?.type ?? block.type);
59
+ const typeColor = $derived(BLOCK_TYPE_COLORS[displayType]);
60
+ const typeIcon = $derived(BLOCK_TYPE_ICONS[displayType]);
61
+
62
+ // ── Handlers ─────────────────────────────────────────────────────────────────
63
+ function handleInput() {
64
+ if (!contentEl) return;
65
+ localContent = contentEl.innerText;
66
+ hasUnsavedChanges = true;
67
+ // Auto-save on input (debounced by parent or just stream it)
68
+ onChange?.(localContent);
69
+ }
70
+
71
+ function handleFocus() {
72
+ isFocused = true;
73
+ }
74
+
75
+ function handleBlur() {
76
+ isFocused = false;
77
+ hasUnsavedChanges = false;
78
+ }
79
+
80
+ function cycleType() {
81
+ const types: BlockType[] = ['claim', 'question', 'idea', 'task', 'reference', 'general'];
82
+ const currentIndex = types.indexOf(block.type);
83
+ const nextType = types[(currentIndex + 1) % types.length];
84
+ onTypeChange?.(nextType);
85
+ }
86
+
87
+ function handleKeydown(e: KeyboardEvent) {
88
+ // Tab inserts spaces
89
+ if (e.key === 'Tab') {
90
+ e.preventDefault();
91
+ document.execCommand('insertText', false, ' ');
92
+ }
93
+ }
94
+ </script>
95
+
96
+ <div
97
+ class="nodepad-block"
98
+ style:--type-color={typeColor}
99
+ style:max-width="{maxWidth}px"
100
+ class:suggested={!!suggestedType && block.type === 'general'}
101
+ >
102
+ <!-- Header: Type indicator + ID -->
103
+ <div class="block-header">
104
+ <button class="type-chip" onclick={cycleType} title="Click to change type">
105
+ <span class="type-icon">{typeIcon}</span>
106
+ <span class="type-name">{displayType}</span>
107
+ </button>
108
+ <span class="block-id">{block.id}</span>
109
+ </div>
110
+
111
+ <!-- Content: Always editable (like Nodepad) -->
112
+ <!-- CRITICAL: No {block.content} binding here - we set innerText manually to prevent cursor reset -->
113
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
114
+ <div
115
+ bind:this={contentEl}
116
+ class="block-content"
117
+ contenteditable="true"
118
+ role="textbox"
119
+ aria-multiline="true"
120
+ tabindex="0"
121
+ oninput={handleInput}
122
+ onfocus={handleFocus}
123
+ onblur={handleBlur}
124
+ onkeydown={handleKeydown}
125
+ ></div>
126
+
127
+ <!-- Footer: Annotation if present -->
128
+ {#if block.annotation}
129
+ <div class="block-annotation">
130
+ <span class="annotation-dot">•</span>
131
+ {block.annotation}
132
+ </div>
133
+ {/if}
134
+
135
+ <!-- Saving indicator -->
136
+ {#if hasUnsavedChanges}
137
+ <div class="save-indicator">•</div>
138
+ {/if}
139
+ </div>
140
+
141
+ <style>
142
+ .nodepad-block {
143
+ /* FLAT: No nested containers, no card styling - just the essentials */
144
+ position: relative;
145
+ padding: 12px 14px;
146
+ background: var(--color-bg-elevated, #252422);
147
+ border-left: 3px solid var(--type-color, #b87333);
148
+ border-radius: 0 6px 6px 0;
149
+ /* CRITICAL: Constant dimensions - no size change on focus/hover */
150
+ transition: box-shadow 0.15s ease, border-color 0.15s ease;
151
+ box-sizing: border-box;
152
+ }
153
+
154
+ .nodepad-block:hover {
155
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
156
+ }
157
+
158
+ .nodepad-block:focus-within {
159
+ /* Use outline instead of border change to avoid layout shift */
160
+ outline: 2px solid color-mix(in srgb, var(--type-color, #b87333) 50%, transparent);
161
+ outline-offset: 2px;
162
+ }
163
+
164
+ .nodepad-block.suggested {
165
+ border-left-style: dashed;
166
+ }
167
+
168
+ /* Header */
169
+ .block-header {
170
+ display: flex;
171
+ align-items: center;
172
+ justify-content: space-between;
173
+ margin-bottom: 8px;
174
+ gap: 8px;
175
+ }
176
+
177
+ .type-chip {
178
+ display: inline-flex;
179
+ align-items: center;
180
+ gap: 4px;
181
+ padding: 2px 8px;
182
+ background: color-mix(in srgb, var(--type-color, #b87333) 15%, transparent);
183
+ border: 1px solid color-mix(in srgb, var(--type-color, #b87333) 25%, transparent);
184
+ border-radius: 4px;
185
+ font-size: 11px;
186
+ font-weight: 500;
187
+ color: var(--type-color, #b87333);
188
+ cursor: pointer;
189
+ transition: all 0.15s ease;
190
+ font-family: inherit;
191
+ }
192
+
193
+ .type-chip:hover {
194
+ background: color-mix(in srgb, var(--type-color, #b87333) 25%, transparent);
195
+ border-color: color-mix(in srgb, var(--type-color, #b87333) 50%, transparent);
196
+ }
197
+
198
+ .type-icon {
199
+ font-size: 10px;
200
+ }
201
+
202
+ .type-name {
203
+ text-transform: capitalize;
204
+ }
205
+
206
+ .block-id {
207
+ font-size: 11px;
208
+ color: var(--color-text-muted, #706858);
209
+ font-family: var(--font-mono, monospace);
210
+ }
211
+
212
+ /* Content - The key: just a contenteditable div, no wrapper, no truncation */
213
+ .block-content {
214
+ font-size: 14px;
215
+ line-height: 1.6;
216
+ color: var(--color-text-primary, #e8e4dc);
217
+ white-space: pre-wrap;
218
+ word-break: break-word;
219
+ min-height: 1.6em;
220
+ outline: none;
221
+ caret-color: var(--type-color, #b87333);
222
+ }
223
+
224
+ .block-content:empty::before {
225
+ content: 'Click to add content...';
226
+ color: var(--color-text-muted, #706858);
227
+ font-style: italic;
228
+ pointer-events: none;
229
+ }
230
+
231
+ /* Annotation */
232
+ .block-annotation {
233
+ margin-top: 8px;
234
+ padding-top: 8px;
235
+ border-top: 1px solid var(--color-border, #333);
236
+ font-size: 12px;
237
+ color: var(--color-text-secondary, #a09880);
238
+ font-style: italic;
239
+ }
240
+
241
+ .annotation-dot {
242
+ color: var(--type-color, #b87333);
243
+ margin-right: 4px;
244
+ }
245
+
246
+ /* Save indicator (subtle dot when typing) */
247
+ .save-indicator {
248
+ position: absolute;
249
+ top: 6px;
250
+ right: 6px;
251
+ width: 6px;
252
+ height: 6px;
253
+ border-radius: 50%;
254
+ background: var(--type-color, #b87333);
255
+ animation: pulse 1.5s infinite;
256
+ }
257
+
258
+ @keyframes pulse {
259
+ 0%, 100% { opacity: 1; }
260
+ 50% { opacity: 0.4; }
261
+ }
262
+ </style>
@@ -0,0 +1,211 @@
1
+ // Block Layout Engine — DOM-free text measurement using Pretext
2
+ // Provides accurate sizing for adaptive block rendering
3
+
4
+ import { prepareWithSegments, layout, measureNaturalWidth as pretextMeasureNatural } from '@chenglou/pretext';
5
+
6
+ /** Layout options for text measurement */
7
+ export interface LayoutOptions {
8
+ /** White space handling: 'normal' (collapse) or 'pre-wrap' (preserve) */
9
+ whiteSpace?: 'normal' | 'pre-wrap';
10
+ /** Word breaking: 'normal' (break at word boundaries) or 'keep-all' (CJK/Hangul) */
11
+ wordBreak?: 'normal' | 'keep-all';
12
+ }
13
+
14
+ /** Metrics returned from block measurement */
15
+ export interface BlockMetrics {
16
+ /** Minimum width before text wraps to additional lines (shrink-wrap) */
17
+ minWidth: number;
18
+ /** Optimal reading width (~65 characters) */
19
+ idealWidth: number;
20
+ /** Total height at the measured width */
21
+ height: number;
22
+ /** Number of lines at the measured width */
23
+ lineCount: number;
24
+ /** True if content exceeds reasonable display limits */
25
+ hasOverflow: boolean;
26
+ /** The font string used for measurement */
27
+ font: string;
28
+ }
29
+
30
+ /** Default font configuration matching KOS design system */
31
+ export const DEFAULT_FONT = '14px "Sovereign Sans", system-ui, -apple-system, sans-serif';
32
+
33
+ /** Default monospace font for code/preformatted content */
34
+ export const DEFAULT_MONO_FONT = '13px "Sovereign Mono", "SF Mono", Monaco, "Cascadia Code", monospace';
35
+
36
+ /** Ideal reading width in pixels (~65ch at 14px) */
37
+ export const IDEAL_READING_WIDTH = 520;
38
+
39
+ /** Maximum reasonable block height before overflow indicator */
40
+ export const MAX_BLOCK_HEIGHT = 400;
41
+
42
+ /** Maximum characters before overflow detection */
43
+ export const MAX_CONTENT_LENGTH = 5000;
44
+
45
+ /**
46
+ * Measure block content using Pretext's DOM-free layout engine.
47
+ *
48
+ * This function uses canvas-based text measurement for accurate sizing
49
+ * without triggering DOM layout/reflow cycles.
50
+ *
51
+ * @param content - The text content to measure
52
+ * @param font - Font specification string (defaults to DEFAULT_FONT)
53
+ * @param maxWidth - Maximum container width constraint
54
+ * @param options - Layout options (whiteSpace, wordBreak)
55
+ * @param lineHeight - Line height multiplier (default: 1.6)
56
+ * @returns BlockMetrics with sizing information
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * const metrics = measureBlock(
61
+ * 'Task: Implement authentication system with OAuth2 support',
62
+ * '14px Inter',
63
+ * 400,
64
+ * { whiteSpace: 'normal' }
65
+ * );
66
+ * // metrics.height → precise pixel height
67
+ * // metrics.lineCount → number of wrapped lines
68
+ * ```
69
+ */
70
+ export function measureBlock(
71
+ content: string,
72
+ font: string = DEFAULT_FONT,
73
+ maxWidth: number = IDEAL_READING_WIDTH,
74
+ options: LayoutOptions = {},
75
+ lineHeight: number = 1.6
76
+ ): BlockMetrics {
77
+ // SSR Safety: Pretext requires DOM/Canvas. Return safe defaults if run on server.
78
+ if (typeof window === 'undefined') {
79
+ return {
80
+ minWidth: 0,
81
+ idealWidth: Math.min(IDEAL_READING_WIDTH, maxWidth),
82
+ height: lineHeight * 14,
83
+ lineCount: 1,
84
+ hasOverflow: false,
85
+ font,
86
+ };
87
+ }
88
+
89
+ // Handle empty content
90
+ if (!content || content.length === 0) {
91
+ return {
92
+ minWidth: 0,
93
+ idealWidth: Math.min(IDEAL_READING_WIDTH, maxWidth),
94
+ height: lineHeight * 14, // One line height
95
+ lineCount: 1,
96
+ hasOverflow: false,
97
+ font,
98
+ };
99
+ }
100
+
101
+ // Truncate extremely long content to prevent performance issues
102
+ const truncatedContent = content.length > MAX_CONTENT_LENGTH
103
+ ? content.slice(0, MAX_CONTENT_LENGTH) + '…'
104
+ : content;
105
+
106
+ // Prepare text for measurement (one-time work)
107
+ const prepared = prepareWithSegments(truncatedContent, font, {
108
+ whiteSpace: options.whiteSpace ?? 'normal',
109
+ wordBreak: options.wordBreak ?? 'normal',
110
+ });
111
+
112
+ // Measure at the target width
113
+ const { height, lineCount } = layout(prepared, maxWidth, lineHeight);
114
+
115
+ // Calculate shrink-wrap width (tightest fit)
116
+ const minWidth = pretextMeasureNatural(prepared);
117
+
118
+ // Determine ideal width (between min and max, capped at reading width)
119
+ const idealWidth = Math.min(
120
+ Math.max(minWidth, Math.min(IDEAL_READING_WIDTH, maxWidth)),
121
+ maxWidth
122
+ );
123
+
124
+ // Detect overflow conditions
125
+ const hasOverflow = height > MAX_BLOCK_HEIGHT || content.length > MAX_CONTENT_LENGTH;
126
+
127
+ return {
128
+ minWidth,
129
+ idealWidth,
130
+ height,
131
+ lineCount,
132
+ hasOverflow,
133
+ font,
134
+ };
135
+ }
136
+
137
+ /**
138
+ * Quick measurement for content previews (truncated at first overflow).
139
+ * Returns faster results for long content by sampling first N characters.
140
+ */
141
+ export function measureBlockPreview(
142
+ content: string,
143
+ font: string = DEFAULT_FONT,
144
+ maxWidth: number = IDEAL_READING_WIDTH,
145
+ maxLines: number = 10
146
+ ): BlockMetrics {
147
+ const previewLength = 1000; // Sample size for preview
148
+ const previewContent = content.length > previewLength
149
+ ? content.slice(0, previewLength) + '…'
150
+ : content;
151
+
152
+ const metrics = measureBlock(previewContent, font, maxWidth);
153
+
154
+ // Cap line count for preview
155
+ if (metrics.lineCount > maxLines) {
156
+ const lineHeight = 1.6 * 14; // approximate
157
+ return {
158
+ ...metrics,
159
+ lineCount: maxLines,
160
+ height: maxLines * lineHeight,
161
+ hasOverflow: true,
162
+ };
163
+ }
164
+
165
+ return { ...metrics, hasOverflow: content.length > previewLength };
166
+ }
167
+
168
+ /**
169
+ * Measure multiple blocks efficiently with shared font preparation.
170
+ * Useful for virtualized lists where font is constant.
171
+ */
172
+ export function measureBlocksBatch(
173
+ contents: string[],
174
+ font: string = DEFAULT_FONT,
175
+ maxWidth: number = IDEAL_READING_WIDTH
176
+ ): BlockMetrics[] {
177
+ return contents.map(content => measureBlock(content, font, maxWidth));
178
+ }
179
+
180
+ /**
181
+ * Calculate total canvas height for a collection of blocks.
182
+ * Includes spacing between blocks.
183
+ */
184
+ export function calculateCanvasHeight(
185
+ metrics: BlockMetrics[],
186
+ spacing: number = 16,
187
+ padding: number = 24
188
+ ): number {
189
+ const contentHeight = metrics.reduce((sum, m) => sum + m.height, 0);
190
+ const spacingHeight = Math.max(0, metrics.length - 1) * spacing;
191
+ return padding * 2 + contentHeight + spacingHeight;
192
+ }
193
+
194
+ /**
195
+ * Determine optimal column width for kanban-style layouts.
196
+ * Balances content width against container constraints.
197
+ */
198
+ export function calculateOptimalColumnWidth(
199
+ allMetrics: BlockMetrics[],
200
+ containerWidth: number,
201
+ columnCount: number,
202
+ minColumnWidth: number = 280
203
+ ): number {
204
+ const maxContentWidth = Math.max(...allMetrics.map(m => m.minWidth));
205
+ const availableWidth = (containerWidth - (columnCount - 1) * 16) / columnCount;
206
+
207
+ return Math.max(
208
+ minColumnWidth,
209
+ Math.min(maxContentWidth + 48, availableWidth) // +48 for padding
210
+ );
211
+ }
@@ -0,0 +1,227 @@
1
+ // Block Types — 14 spatial research classifications
2
+ // Based on Nodepad's auto-classification system
3
+
4
+ /** The 14 types of enchanted blocks for spatial research */
5
+ export type BlockType =
6
+ | 'claim' // Asserted truth or proposition
7
+ | 'question' // Open inquiry or research question
8
+ | 'idea' // Proposed concept or hypothesis
9
+ | 'task' // Action item or todo (Ryu integration)
10
+ | 'entity' // Named thing (person, organization, concept)
11
+ | 'quote' // External citation or verbatim excerpt
12
+ | 'reference' // Link, pointer, or citation
13
+ | 'definition' // Term explanation or glossary entry
14
+ | 'opinion' // Subjective stance or perspective
15
+ | 'reflection' // Self-aware musing or metacognition
16
+ | 'narrative' // Story, timeline, or sequential account
17
+ | 'comparison' // Side-by-side analysis or contrast
18
+ | 'thesis' // Synthesized position or argument
19
+ | 'general'; // Unclassified or mixed content
20
+
21
+ /** All block types as an array for iteration */
22
+ export const ALL_BLOCK_TYPES: BlockType[] = [
23
+ 'claim',
24
+ 'question',
25
+ 'idea',
26
+ 'task',
27
+ 'entity',
28
+ 'quote',
29
+ 'reference',
30
+ 'definition',
31
+ 'opinion',
32
+ 'reflection',
33
+ 'narrative',
34
+ 'comparison',
35
+ 'thesis',
36
+ 'general',
37
+ ];
38
+
39
+ /** Human-readable labels for each block type */
40
+ export const BLOCK_TYPE_LABELS: Record<BlockType, string> = {
41
+ claim: 'Claim',
42
+ question: 'Question',
43
+ idea: 'Idea',
44
+ task: 'Task',
45
+ entity: 'Entity',
46
+ quote: 'Quote',
47
+ reference: 'Reference',
48
+ definition: 'Definition',
49
+ opinion: 'Opinion',
50
+ reflection: 'Reflection',
51
+ narrative: 'Narrative',
52
+ comparison: 'Comparison',
53
+ thesis: 'Thesis',
54
+ general: 'Note',
55
+ };
56
+
57
+ /** Lucide icon names for each block type */
58
+ export const BLOCK_TYPE_ICONS: Record<BlockType, string> = {
59
+ claim: 'CheckCircle', // Asserted truth
60
+ question: 'HelpCircle', // Inquiry
61
+ idea: 'Lightbulb', // Concept
62
+ task: 'CheckSquare', // Action item
63
+ entity: 'Box', // Named thing
64
+ quote: 'Quote', // Citation
65
+ reference: 'Link', // Pointer
66
+ definition: 'BookOpen', // Explanation
67
+ opinion: 'MessageCircle', // Subjective view
68
+ reflection: 'Mirror', // Self-aware
69
+ narrative: 'ScrollText', // Story
70
+ comparison: 'Scale', // Analysis
71
+ thesis: 'GraduationCap', // Synthesized argument
72
+ general: 'StickyNote', // Default
73
+ };
74
+
75
+ /**
76
+ * CSS color tokens for each block type.
77
+ * Uses KOS brass/copper palette with semantic differentiation.
78
+ * Format: HSL values for --type-color CSS custom property.
79
+ */
80
+ export const BLOCK_TYPE_COLORS: Record<BlockType, string> = {
81
+ // Warm/assertive types (brass/gold variants)
82
+ claim: 'hsl(30, 60%, 55%)', // Brass
83
+ thesis: 'hsl(25, 70%, 50%)', // Copper
84
+ task: 'hsl(35, 55%, 50%)', // Bronze
85
+
86
+ // Cool/inquisitive types (muted complementary)
87
+ question: 'hsl(210, 50%, 60%)', // Steel blue
88
+ idea: 'hsl(160, 45%, 50%)', // Sage
89
+ opinion: 'hsl(280, 40%, 55%)', // Muted purple
90
+
91
+ // Reference/external types (neutral)
92
+ quote: 'hsl(0, 0%, 70%)', // Silver
93
+ reference: 'hsl(200, 30%, 60%)', // Slate
94
+ definition: 'hsl(180, 35%, 55%)', // Teal
95
+
96
+ // Process/meta types (warm earth)
97
+ reflection: 'hsl(45, 50%, 55%)', // Amber
98
+ narrative: 'hsl(20, 45%, 50%)', // Terracotta
99
+ comparison: 'hsl(40, 45%, 50%)', // Gold ochre
100
+
101
+ // Structural types
102
+ entity: 'hsl(220, 40%, 55%)', // Denim
103
+ general: 'hsl(30, 40%, 50%)', // Default brass
104
+ };
105
+
106
+ /** Get the next type in sequence (for cycling via click) */
107
+ export function nextBlockType(current: BlockType): BlockType {
108
+ const idx = ALL_BLOCK_TYPES.indexOf(current);
109
+ return ALL_BLOCK_TYPES[(idx + 1) % ALL_BLOCK_TYPES.length];
110
+ }
111
+
112
+ /** Get the previous type in sequence */
113
+ export function prevBlockType(current: BlockType): BlockType {
114
+ const idx = ALL_BLOCK_TYPES.indexOf(current);
115
+ return ALL_BLOCK_TYPES[(idx - 1 + ALL_BLOCK_TYPES.length) % ALL_BLOCK_TYPES.length];
116
+ }
117
+
118
+ /**
119
+ * Simple heuristic classifier based on content patterns.
120
+ * Returns confidence score 0-1 alongside type.
121
+ */
122
+ export function classifyBlockHeuristic(content: string): { type: BlockType; confidence: number } {
123
+ const lower = content.toLowerCase().trim();
124
+
125
+ // Question detection
126
+ if (lower.match(/^(what|why|how|when|where|who|is\s+it|are\s+they|can\s+we|should\s+we)\b/i) ||
127
+ lower.endsWith('?')) {
128
+ return { type: 'question', confidence: 0.9 };
129
+ }
130
+
131
+ // Task detection
132
+ if (lower.match(/^(todo|task|action|implement|fix|add|remove|update|create|build)\b/i) ||
133
+ lower.match(/\[x\]|\[ \]|\[-\]/)) {
134
+ return { type: 'task', confidence: 0.85 };
135
+ }
136
+
137
+ // Quote detection
138
+ if (lower.startsWith('"') && lower.includes('"') ||
139
+ lower.startsWith('"') && lower.endsWith('"') ||
140
+ lower.match(/^as\s+\w+\s+said/i)) {
141
+ return { type: 'quote', confidence: 0.8 };
142
+ }
143
+
144
+ // Definition detection
145
+ if (lower.match(/\bis\s+(defined\s+as|a\s+type\s+of|the\s+process\s+of)\b/i) ||
146
+ lower.match(/^\w+:\s+\w+/)) {
147
+ return { type: 'definition', confidence: 0.75 };
148
+ }
149
+
150
+ // Claim detection (assertive language)
151
+ if (lower.match(/\b(is|are|will|must|should|always|never|proves|shows|demonstrates)\b/i) &&
152
+ !lower.endsWith('?')) {
153
+ return { type: 'claim', confidence: 0.7 };
154
+ }
155
+
156
+ // Idea detection
157
+ if (lower.match(/\b(idea|concept|proposal|suggest|consider|perhaps|maybe|could|might)\b/i)) {
158
+ return { type: 'idea', confidence: 0.65 };
159
+ }
160
+
161
+ // Reference detection
162
+ if (lower.match(/^(see|cf\.|refer\s+to|check\s+out|read|http|www\.|github\.com)/i)) {
163
+ return { type: 'reference', confidence: 0.8 };
164
+ }
165
+
166
+ // Narrative detection (time words)
167
+ if (lower.match(/\b(then|next|after|before|finally|eventually|once|when\s+we)\b/i)) {
168
+ return { type: 'narrative', confidence: 0.6 };
169
+ }
170
+
171
+ // Comparison detection
172
+ if (lower.match(/\b(vs|versus|compared\s+to|unlike|similarly|both|either|or)\b/i)) {
173
+ return { type: 'comparison', confidence: 0.65 };
174
+ }
175
+
176
+ // Reflection detection (self-aware)
177
+ if (lower.match(/\b(i\s+think|i\s+wonder|i\s+realized|reflecting|thinking\s+about)\b/i)) {
178
+ return { type: 'reflection', confidence: 0.7 };
179
+ }
180
+
181
+ // Opinion detection
182
+ if (lower.match(/\b(i\s+believe|in\s+my\s+opinion|personally|i\s+feel|i\s+prefer)\b/i)) {
183
+ return { type: 'opinion', confidence: 0.75 };
184
+ }
185
+
186
+ // Entity detection (proper nouns, capitalized)
187
+ if (lower.match(/^[A-Z][a-z]+\s+(is|was|are|refers\s+to)/)) {
188
+ return { type: 'entity', confidence: 0.6 };
189
+ }
190
+
191
+ return { type: 'general', confidence: 0.5 };
192
+ }
193
+
194
+ /** Core data structure for an enchanted block */
195
+ export interface EnchantedBlockData {
196
+ id: string;
197
+ type: BlockType;
198
+ content: string;
199
+
200
+ /** AI-generated annotation: "what the note doesn't say" */
201
+ annotation?: string;
202
+
203
+ /** Epistemic provenance from KVE */
204
+ provenance?: {
205
+ source: string;
206
+ createdAt: number;
207
+ modifiedAt: number;
208
+ confidence?: number;
209
+ };
210
+
211
+ /** Inferred connections to other blocks */
212
+ connections?: {
213
+ targetId: string;
214
+ strength: number; // 0.0 - 1.0
215
+ type: 'supports' | 'contradicts' | 'extends' | 'similar' | 'related';
216
+ }[];
217
+
218
+ /** Spatial position (for canvas views) */
219
+ x?: number;
220
+ y?: number;
221
+ width?: number;
222
+ height?: number;
223
+
224
+ /** Timestamps */
225
+ createdAt: number;
226
+ modifiedAt: number;
227
+ }