bridges-cli 0.0.1 → 0.2.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.
@@ -0,0 +1,137 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import type { HashiNodeData } from '../../types.ts'
3
+ import { constructNode, getDisplayMode } from '../bridges.ts'
4
+
5
+ // biome-ignore lint/security/noSecrets: false positive
6
+ describe('getDisplayMode()', () => {
7
+ it('returns normal when highlightedNode is undefined', () => {
8
+ const node: HashiNodeData = { value: 1 }
9
+ expect(getDisplayMode(node, undefined)).toBe('normal')
10
+ })
11
+
12
+ it('returns highlight when node value matches highlightedNode', () => {
13
+ const node: HashiNodeData = { value: 2 }
14
+ expect(getDisplayMode(node, 2)).toBe('highlight')
15
+ })
16
+
17
+ it('returns dim when node value does not match highlightedNode', () => {
18
+ const node: HashiNodeData = { value: 1 }
19
+ expect(getDisplayMode(node, 2)).toBe('dim')
20
+ })
21
+
22
+ it('returns dim for bridge nodes when highlightedNode is set', () => {
23
+ expect(getDisplayMode({ value: '-' }, 2)).toBe('dim')
24
+ expect(getDisplayMode({ value: '|' }, 2)).toBe('dim')
25
+ expect(getDisplayMode({ value: '#' }, 2)).toBe('dim')
26
+ })
27
+
28
+ it('returns highlight for bridge nodes that are the highlighted value', () => {
29
+ expect(getDisplayMode({ value: '-' }, 2)).toBe('dim')
30
+ })
31
+ })
32
+
33
+ describe('constructNode()', () => {
34
+ describe('empty node', () => {
35
+ it('renders space value with no lines', () => {
36
+ const node: HashiNodeData = { value: ' ' }
37
+ expect(constructNode(node, 0)).toEqual(' ')
38
+ expect(constructNode(node, 1)).toEqual(' ')
39
+ expect(constructNode(node, 2)).toEqual(' ')
40
+ })
41
+ })
42
+
43
+ describe('horizontal line node', () => {
44
+ it('renders a horizontal line in the middle', () => {
45
+ const node: HashiNodeData = { value: '-' }
46
+ expect(constructNode(node, 0)).toEqual(' ')
47
+ expect(constructNode(node, 1)).toEqual('─────')
48
+ expect(constructNode(node, 2)).toEqual(' ')
49
+ })
50
+
51
+ it('renders a double horizontal line in the middle', () => {
52
+ const node: HashiNodeData = { value: '=' }
53
+ expect(constructNode(node, 0)).toEqual(' ')
54
+ expect(constructNode(node, 1)).toEqual('═════')
55
+ expect(constructNode(node, 2)).toEqual(' ')
56
+ })
57
+ })
58
+
59
+ describe('vertical line node', () => {
60
+ it('renders a vertical line in the center', () => {
61
+ const node: HashiNodeData = { value: '|' }
62
+ expect(constructNode(node, 0)).toEqual(' │ ')
63
+ expect(constructNode(node, 1)).toEqual(' │ ')
64
+ expect(constructNode(node, 2)).toEqual(' │ ')
65
+ })
66
+
67
+ it('renders a double vertical line in the center', () => {
68
+ const node: HashiNodeData = { value: '#' }
69
+ expect(constructNode(node, 0)).toEqual(' ║ ')
70
+ expect(constructNode(node, 1)).toEqual(' ║ ')
71
+ expect(constructNode(node, 2)).toEqual(' ║ ')
72
+ })
73
+ })
74
+
75
+ describe('node with value', () => {
76
+ describe('TOP_ROW', () => {
77
+ it('renders top border', () => {
78
+ const node: HashiNodeData = { value: 5 }
79
+ expect(constructNode(node, 0)).toEqual('╭───╮')
80
+ })
81
+
82
+ it('renders border with vertical line up', () => {
83
+ const node: HashiNodeData = { value: 5, lineUp: 1 }
84
+ expect(constructNode(node, 0)).toEqual('╭─┴─╮')
85
+ })
86
+
87
+ it('renders border with double vertical line up', () => {
88
+ const node: HashiNodeData = { value: 5, lineUp: 2 }
89
+ expect(constructNode(node, 0)).toEqual('╭─╨─╮')
90
+ })
91
+ })
92
+
93
+ describe('MIDDLE_ROW', () => {
94
+ it('renders middle row - value with vertical borders', () => {
95
+ const node: HashiNodeData = { value: 5 }
96
+ expect(constructNode(node, 1)).toEqual('│ 5 │')
97
+ })
98
+
99
+ it('renders value with horizontal line on left', () => {
100
+ const node: HashiNodeData = { value: 5, lineLeft: 1 }
101
+ expect(constructNode(node, 1)).toEqual('┤ 5 │')
102
+ })
103
+
104
+ it('renders value with horizontal line on right', () => {
105
+ const node: HashiNodeData = { value: 5, lineRight: 1 }
106
+ expect(constructNode(node, 1)).toEqual('│ 5 ├')
107
+ })
108
+
109
+ it('renders value with horizontal lines on both sides', () => {
110
+ const node: HashiNodeData = { value: 5, lineLeft: 1, lineRight: 1 }
111
+ expect(constructNode(node, 1)).toEqual('┤ 5 ├')
112
+ })
113
+
114
+ it('renders value with double horizontal lines on both sides', () => {
115
+ const node: HashiNodeData = { value: 5, lineLeft: 2, lineRight: 2 }
116
+ expect(constructNode(node, 1)).toEqual('╡ 5 ╞')
117
+ })
118
+ })
119
+
120
+ describe('BOTTOM_ROW', () => {
121
+ it('renders bottom border without lines', () => {
122
+ const node: HashiNodeData = { value: 5 }
123
+ expect(constructNode(node, 2)).toEqual('╰───╯')
124
+ })
125
+
126
+ it('renders border with vertical line down', () => {
127
+ const node: HashiNodeData = { value: 5, lineDown: 1 }
128
+ expect(constructNode(node, 2)).toEqual('╰─┬─╯')
129
+ })
130
+
131
+ it('renders border with double vertical line down', () => {
132
+ const node: HashiNodeData = { value: 5, lineDown: 2 }
133
+ expect(constructNode(node, 2)).toEqual('╰─╥─╯')
134
+ })
135
+ })
136
+ })
137
+ })
@@ -0,0 +1,75 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { findNodeInDirection } from '../usePuzzleInput.ts'
4
+
5
+ describe('findNodeInDirection', () => {
6
+ describe('1x2 grid (horizontal)', () => {
7
+ const grid = [[{ value: 1 }, { value: 2 }]]
8
+
9
+ it('finds node to the right', () => {
10
+ expect(findNodeInDirection(grid, 0, 0, 'l')).toEqual({ row: 0, col: 1 })
11
+ })
12
+
13
+ it('finds node to the left', () => {
14
+ expect(findNodeInDirection(grid, 0, 1, 'h')).toEqual({ row: 0, col: 0 })
15
+ })
16
+
17
+ it('returns null when no node to the right', () => {
18
+ expect(findNodeInDirection(grid, 0, 1, 'l')).toBe(null)
19
+ })
20
+
21
+ it('returns null when no node to the left', () => {
22
+ expect(findNodeInDirection(grid, 0, 0, 'h')).toBe(null)
23
+ })
24
+
25
+ it('returns null for vertical directions (no rows above/below)', () => {
26
+ expect(findNodeInDirection(grid, 0, 0, 'j')).toBe(null)
27
+ expect(findNodeInDirection(grid, 0, 0, 'k')).toBe(null)
28
+ })
29
+ })
30
+
31
+ describe('2x1 grid (vertical)', () => {
32
+ const grid = [[{ value: 1 }], [{ value: 2 }]]
33
+
34
+ it('finds node below', () => {
35
+ expect(findNodeInDirection(grid, 0, 0, 'j')).toEqual({ row: 1, col: 0 })
36
+ })
37
+
38
+ it('finds node above', () => {
39
+ expect(findNodeInDirection(grid, 1, 0, 'k')).toEqual({ row: 0, col: 0 })
40
+ })
41
+
42
+ it('returns null when no node below', () => {
43
+ expect(findNodeInDirection(grid, 1, 0, 'j')).toBe(null)
44
+ })
45
+
46
+ it('returns null when no node above', () => {
47
+ expect(findNodeInDirection(grid, 0, 0, 'k')).toBe(null)
48
+ })
49
+
50
+ it('returns null for horizontal directions (no cols left/right)', () => {
51
+ expect(findNodeInDirection(grid, 0, 0, 'h')).toBe(null)
52
+ expect(findNodeInDirection(grid, 0, 0, 'l')).toBe(null)
53
+ })
54
+ })
55
+
56
+ describe('3x3 grid with empty cells', () => {
57
+ const grid: { value: number | '-' | '=' | '#' | ' ' | '|' }[][] = [
58
+ [{ value: 1 }, { value: ' ' }, { value: 2 }],
59
+ [{ value: ' ' }, { value: '#' }, { value: ' ' }],
60
+ [{ value: 3 }, { value: ' ' }, { value: 4 }],
61
+ ]
62
+
63
+ it('finds node across empty cells', () => {
64
+ expect(findNodeInDirection(grid, 0, 0, 'l')).toEqual({ row: 0, col: 2 })
65
+ })
66
+
67
+ it('finds node across empty cells (down)', () => {
68
+ expect(findNodeInDirection(grid, 0, 0, 'j')).toEqual({ row: 2, col: 0 })
69
+ })
70
+
71
+ it('returns node when blocked by wall (#) but path around exists', () => {
72
+ expect(findNodeInDirection(grid, 0, 0, 'j')).toEqual({ row: 2, col: 0 })
73
+ })
74
+ })
75
+ })
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from 'vitest'
2
2
 
3
- import { parsePuzzle } from '../parsePuzzle.ts'
3
+ import { parsePuzzle } from '../puzzle-encoding.ts'
4
4
 
5
5
  describe('parsePuzzle', () => {
6
6
  describe('encodings without the solution (bridges)', () => {
@@ -0,0 +1,395 @@
1
+ import type { HashiNodeData, HashiNodeDisplayMode } from '../types.ts'
2
+
3
+ export type NodeFilledState = 'valid' | 'invalid' | 'incomplete'
4
+
5
+ export const ROW_HEIGHT = 3
6
+ export const NODE_WIDTH = 5
7
+ export const SPACE_BETWEEN = 0
8
+ export const OUTER_PADDING = 1
9
+
10
+ export const TOP_ROW = 0
11
+ export const MIDDLE_ROW = 1
12
+ export const BOTTOM_ROW = 2
13
+
14
+ export type HashiGridValidationProps = {
15
+ rows: HashiNodeData[][]
16
+ numNodes: number
17
+ }
18
+
19
+ /**
20
+ * Ensure the grid data is consistent with a valid Bridges puzzle.
21
+ */
22
+ export function validateGrid({ rows, numNodes }: HashiGridValidationProps): void {
23
+ if (!rows || rows.length === 0) {
24
+ throw new Error('HashiGrid: empty data supplied')
25
+ }
26
+
27
+ let rowCount = 0
28
+ for (const nodes of rows) {
29
+ const prefix = `HashiGrid row ${rowCount}: `
30
+
31
+ if (nodes.length !== numNodes) {
32
+ throw new Error(`${prefix}expected ${numNodes} nodes, got ${nodes.length}`)
33
+ }
34
+
35
+ for (let i = 0; i < nodes.length; i++) {
36
+ const node = nodes[i]
37
+ if (!node) {
38
+ throw new Error(`${prefix}node at position ${i} is undefined`)
39
+ }
40
+ if (
41
+ typeof node.value !== 'number' &&
42
+ node.value !== '-' &&
43
+ node.value !== '=' &&
44
+ node.value !== ' ' &&
45
+ node.value !== '#' &&
46
+ node.value !== '|'
47
+ ) {
48
+ throw new Error(`${prefix}node at position ${i} has invalid value: ${node.value}`)
49
+ }
50
+ }
51
+ rowCount++
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Check if the puzzle is solved - all numbered nodes have the correct number of bridges
57
+ * and all nodes form a connected graph.
58
+ */
59
+ export function checkSolution(rows: HashiNodeData[][]): boolean {
60
+ for (const row of rows) {
61
+ for (const node of row) {
62
+ const state = getNodeFilledState(node)
63
+ if (state !== null && state !== 'valid') {
64
+ return false
65
+ }
66
+ }
67
+ }
68
+
69
+ if (!isGraphConnected(rows)) {
70
+ return false
71
+ }
72
+
73
+ return true
74
+ }
75
+
76
+ /**
77
+ * Check if all numbered nodes have the correct number of bridges (regardless of connectivity).
78
+ */
79
+ export function areAllNodesFilled(rows: HashiNodeData[][]): boolean {
80
+ for (const row of rows) {
81
+ for (const node of row) {
82
+ const state = getNodeFilledState(node)
83
+ if (state !== null && state !== 'valid') {
84
+ return false
85
+ }
86
+ }
87
+ }
88
+ return true
89
+ }
90
+
91
+ /**
92
+ * Check if the graph is connected (regardless of whether nodes have correct bridge counts).
93
+ */
94
+ export function isConnected(rows: HashiNodeData[][]): boolean {
95
+ return isGraphConnected(rows)
96
+ }
97
+
98
+ /**
99
+ * Check if all numbered nodes form a connected graph using BFS.
100
+ */
101
+ function isGraphConnected(rows: HashiNodeData[][]): boolean {
102
+ const numRows = rows.length
103
+ if (numRows === 0) return true
104
+ const numCols = rows[0].length
105
+
106
+ const numberedNodes: [number, number][] = []
107
+ for (let r = 0; r < numRows; r++) {
108
+ for (let c = 0; c < numCols; c++) {
109
+ if (typeof rows[r][c].value === 'number') {
110
+ numberedNodes.push([r, c])
111
+ }
112
+ }
113
+ }
114
+
115
+ if (numberedNodes.length === 0) return true
116
+
117
+ const visited = new Set<string>()
118
+ const queue: [number, number][] = [[numberedNodes[0][0], numberedNodes[0][1]]]
119
+ visited.add(`${numberedNodes[0][0]},${numberedNodes[0][1]}`)
120
+
121
+ while (queue.length > 0) {
122
+ const next = queue.shift()
123
+ if (!next) continue
124
+ const [r, c] = next
125
+ const node = rows[r]?.[c]
126
+ if (!node) continue
127
+
128
+ if (node.lineRight === 1 || node.lineRight === 2) {
129
+ const dest = findNodeInDirection(rows, r, c, 0, 1)
130
+ if (dest) {
131
+ const key = `${dest[0]},${dest[1]}`
132
+ if (!visited.has(key)) {
133
+ visited.add(key)
134
+ queue.push(dest)
135
+ }
136
+ }
137
+ }
138
+
139
+ if (node.lineLeft === 1 || node.lineLeft === 2) {
140
+ const dest = findNodeInDirection(rows, r, c, 0, -1)
141
+ if (dest) {
142
+ const key = `${dest[0]},${dest[1]}`
143
+ if (!visited.has(key)) {
144
+ visited.add(key)
145
+ queue.push(dest)
146
+ }
147
+ }
148
+ }
149
+
150
+ if (node.lineDown === 1 || node.lineDown === 2) {
151
+ const dest = findNodeInDirection(rows, r, c, 1, 0)
152
+ if (dest) {
153
+ const key = `${dest[0]},${dest[1]}`
154
+ if (!visited.has(key)) {
155
+ visited.add(key)
156
+ queue.push(dest)
157
+ }
158
+ }
159
+ }
160
+
161
+ if (node.lineUp === 1 || node.lineUp === 2) {
162
+ const dest = findNodeInDirection(rows, r, c, -1, 0)
163
+ if (dest) {
164
+ const key = `${dest[0]},${dest[1]}`
165
+ if (!visited.has(key)) {
166
+ visited.add(key)
167
+ queue.push(dest)
168
+ }
169
+ }
170
+ }
171
+ }
172
+
173
+ return visited.size === numberedNodes.length
174
+ }
175
+
176
+ function findNodeInDirection(
177
+ rows: HashiNodeData[][],
178
+ startR: number,
179
+ startC: number,
180
+ dRow: number,
181
+ dCol: number
182
+ ): [number, number] | null {
183
+ const numRows = rows.length
184
+ const numCols = rows[0].length
185
+ let r = startR + dRow
186
+ let c = startC + dCol
187
+
188
+ while (r >= 0 && r < numRows && c >= 0 && c < numCols) {
189
+ if (typeof rows[r][c].value === 'number') {
190
+ return [r, c]
191
+ }
192
+ r += dRow
193
+ c += dCol
194
+ }
195
+
196
+ return null
197
+ }
198
+
199
+ /**
200
+ * Determines if a node has the correct number of bridges, too many, or too few.
201
+ */
202
+ export function getNodeFilledState(node: HashiNodeData): NodeFilledState | null {
203
+ if (typeof node.value !== 'number') {
204
+ return null
205
+ }
206
+
207
+ const bridges =
208
+ (node.lineUp ?? 0) + (node.lineDown ?? 0) + (node.lineLeft ?? 0) + (node.lineRight ?? 0)
209
+
210
+ if (bridges === node.value) {
211
+ return 'valid'
212
+ }
213
+ if (bridges > node.value) {
214
+ return 'invalid'
215
+ }
216
+ return 'incomplete'
217
+ }
218
+
219
+ /**
220
+ * Determines the display mode for a node based on the highlighted node value.
221
+ */
222
+ export function getDisplayMode(
223
+ node: HashiNodeData,
224
+ highlightedNode?: number,
225
+ row?: number,
226
+ col?: number,
227
+ selectedNode?: { row: number; col: number } | null,
228
+ mode?: string
229
+ ): HashiNodeDisplayMode {
230
+ // In selecting-node, selected, or invalid mode, highlight only the specific selected node
231
+ if (
232
+ (mode === 'selecting-node' || mode === 'selected' || mode === 'invalid') &&
233
+ selectedNode &&
234
+ row !== undefined &&
235
+ col !== undefined
236
+ ) {
237
+ if (row === selectedNode.row && col === selectedNode.col) {
238
+ return 'highlight'
239
+ }
240
+ return 'dim'
241
+ }
242
+
243
+ if (highlightedNode === undefined) {
244
+ return 'normal'
245
+ }
246
+ if (typeof node.value === 'number' && node.value === highlightedNode) {
247
+ return 'highlight'
248
+ }
249
+
250
+ // Bridge values are strings, so they can never match a number highlightedNode
251
+ return 'dim'
252
+ }
253
+
254
+ /**
255
+ * Build the HashiGrid node with its value and borders. Options:
256
+ * - node with a value (always 1 digit)
257
+ * - empty node - render just spaces
258
+ * - horizontal line - single and double
259
+ * - vertical line - single and double
260
+ */
261
+ export function constructNode(
262
+ node: HashiNodeData,
263
+ line: 0 | 1 | 2,
264
+ displayMode: HashiNodeDisplayMode = 'normal',
265
+ disambiguationLabel?: string,
266
+ validationState?: NodeFilledState | null,
267
+ showSolution?: boolean
268
+ ): string {
269
+ // Determine color prefix based on validation state
270
+ const getColorPrefix = (): string => {
271
+ if (validationState === 'valid') {
272
+ return '\x1b[32m' // green
273
+ }
274
+ if (validationState === 'invalid') {
275
+ return '\x1b[31m' // red
276
+ }
277
+ return ''
278
+ }
279
+
280
+ const colorReset = '\x1b[39m'
281
+ const colorPrefix = displayMode === 'dim' ? '' : getColorPrefix()
282
+ const useColor = colorPrefix !== ''
283
+
284
+ // Horizontal line
285
+ if (node.value === '-') {
286
+ if (displayMode === 'dim') {
287
+ return line === MIDDLE_ROW ? `\x1b[2m─────\x1b[22m` : ' '.repeat(NODE_WIDTH)
288
+ }
289
+ const content = line === MIDDLE_ROW ? '─────' : ' '.repeat(NODE_WIDTH)
290
+ if (showSolution) {
291
+ return `\x1b[32m${content}\x1b[39m`
292
+ }
293
+ return useColor ? `${getColorPrefix()}${content}${colorReset}` : content
294
+ }
295
+
296
+ // Double horizontal line
297
+ if (node.value === '=') {
298
+ if (displayMode === 'dim') {
299
+ return line === MIDDLE_ROW ? `\x1b[2m═════\x1b[22m` : ' '.repeat(NODE_WIDTH)
300
+ }
301
+ const content = line === MIDDLE_ROW ? '═════' : ' '.repeat(NODE_WIDTH)
302
+ if (showSolution) {
303
+ return `\x1b[32m${content}\x1b[39m`
304
+ }
305
+ return useColor ? `${getColorPrefix()}${content}${colorReset}` : content
306
+ }
307
+
308
+ // Vertical line
309
+ if (node.value === '|') {
310
+ if (displayMode === 'dim') {
311
+ return `\x1b[2m │ \x1b[22m`
312
+ }
313
+ const content = ' │ '
314
+ if (showSolution) {
315
+ return `\x1b[32m${content}\x1b[39m`
316
+ }
317
+ return useColor ? `${getColorPrefix()}${content}${colorReset}` : content
318
+ }
319
+
320
+ // Double vertical line
321
+ if (node.value === '#') {
322
+ if (displayMode === 'dim') {
323
+ return `\x1b[2m ║ \x1b[22m`
324
+ }
325
+ const content = ' ║ '
326
+ if (showSolution) {
327
+ return `\x1b[32m${content}\x1b[39m`
328
+ }
329
+ return useColor ? `${getColorPrefix()}${content}${colorReset}` : content
330
+ }
331
+
332
+ // Node with value to render
333
+ if (node.value !== ' ') {
334
+ if (line === TOP_ROW) {
335
+ const up = node.lineUp === 2 ? '╨' : node.lineUp === 1 ? '┴' : '─'
336
+ const label = disambiguationLabel ? disambiguationLabel : '─'
337
+ const border = `╭${label}${up}─╮`
338
+ if (displayMode === 'highlight') {
339
+ const highlighted = `\x1b[1m${border}\x1b[22m`
340
+ if (validationState === 'valid' || validationState === 'invalid') {
341
+ return `${getColorPrefix()}${highlighted}${colorReset}`
342
+ }
343
+ return highlighted
344
+ }
345
+ if (displayMode === 'dim') {
346
+ const dimmedBorder = `\x1b[2m${border}\x1b[22m`
347
+ if (validationState === 'valid' || validationState === 'invalid') {
348
+ return `${getColorPrefix()}${dimmedBorder}${colorReset}`
349
+ }
350
+ return dimmedBorder
351
+ }
352
+ return useColor ? `${getColorPrefix()}${border}${colorReset}` : border
353
+ } else if (line === MIDDLE_ROW) {
354
+ const left = node.lineLeft === 2 ? '╡' : node.lineLeft === 1 ? '┤' : '│'
355
+ const right = node.lineRight === 2 ? '╞' : node.lineRight === 1 ? '├' : '│'
356
+ const content = `${left} ${node.value} ${right}`
357
+ if (displayMode === 'highlight') {
358
+ const highlighted = `\x1b[1m${content}\x1b[22m`
359
+ if (validationState === 'valid' || validationState === 'invalid') {
360
+ return `${getColorPrefix()}${highlighted}${colorReset}`
361
+ }
362
+ return highlighted
363
+ }
364
+ if (displayMode === 'dim') {
365
+ const dimmedContent = `\x1b[2m${content}\x1b[22m`
366
+ if (validationState === 'valid' || validationState === 'invalid') {
367
+ return `${getColorPrefix()}${dimmedContent}${colorReset}`
368
+ }
369
+ return dimmedContent
370
+ }
371
+ return useColor ? `${getColorPrefix()}${content}${colorReset}` : content
372
+ } else if (line === BOTTOM_ROW) {
373
+ const down = node.lineDown === 2 ? '╥' : node.lineDown === 1 ? '┬' : '─'
374
+ const border = `╰─${down}─╯`
375
+ if (displayMode === 'highlight') {
376
+ const highlighted = `\x1b[1m${border}\x1b[22m`
377
+ if (validationState === 'valid' || validationState === 'invalid') {
378
+ return `${getColorPrefix()}${highlighted}${colorReset}`
379
+ }
380
+ return highlighted
381
+ }
382
+ if (displayMode === 'dim') {
383
+ const dimmedBorder = `\x1b[2m${border}\x1b[22m`
384
+ if (validationState === 'valid' || validationState === 'invalid') {
385
+ return `${getColorPrefix()}${dimmedBorder}${colorReset}`
386
+ }
387
+ return dimmedBorder
388
+ }
389
+ return useColor ? `${getColorPrefix()}${border}${colorReset}` : border
390
+ }
391
+ }
392
+
393
+ // Empty node
394
+ return ' '.repeat(NODE_WIDTH)
395
+ }