@sundaeswap/sprinkles 0.5.0 → 0.6.1
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/dist/cjs/Sprinkle/__tests__/encryption.test.js +3 -1
- package/dist/cjs/Sprinkle/__tests__/encryption.test.js.map +1 -1
- package/dist/cjs/Sprinkle/__tests__/enhancements.test.js +3 -37
- package/dist/cjs/Sprinkle/__tests__/enhancements.test.js.map +1 -1
- package/dist/cjs/Sprinkle/__tests__/field-utils.test.js +170 -0
- package/dist/cjs/Sprinkle/__tests__/field-utils.test.js.map +1 -0
- package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js +377 -84
- package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
- package/dist/cjs/Sprinkle/__tests__/formatting.test.js +97 -0
- package/dist/cjs/Sprinkle/__tests__/formatting.test.js.map +1 -0
- package/dist/cjs/Sprinkle/__tests__/show-menu.test.js +9 -5
- package/dist/cjs/Sprinkle/__tests__/show-menu.test.js.map +1 -1
- package/dist/cjs/Sprinkle/__tests__/tx-dialog.test.js +9 -0
- package/dist/cjs/Sprinkle/__tests__/tx-dialog.test.js.map +1 -1
- package/dist/cjs/Sprinkle/index.js +174 -94
- package/dist/cjs/Sprinkle/index.js.map +1 -1
- package/dist/cjs/Sprinkle/menus/array-menu.js +195 -0
- package/dist/cjs/Sprinkle/menus/array-menu.js.map +1 -0
- package/dist/cjs/Sprinkle/menus/field-menu.js +161 -0
- package/dist/cjs/Sprinkle/menus/field-menu.js.map +1 -0
- package/dist/cjs/Sprinkle/menus/index.js +33 -0
- package/dist/cjs/Sprinkle/menus/index.js.map +1 -0
- package/dist/cjs/Sprinkle/menus/object-menu.js +324 -0
- package/dist/cjs/Sprinkle/menus/object-menu.js.map +1 -0
- package/dist/cjs/Sprinkle/prompts.js +68 -2
- package/dist/cjs/Sprinkle/prompts.js.map +1 -1
- package/dist/cjs/Sprinkle/type-guards.js +48 -1
- package/dist/cjs/Sprinkle/type-guards.js.map +1 -1
- package/dist/cjs/Sprinkle/types.js +24 -0
- package/dist/cjs/Sprinkle/types.js.map +1 -1
- package/dist/cjs/Sprinkle/utils/field-utils.js +154 -0
- package/dist/cjs/Sprinkle/utils/field-utils.js.map +1 -0
- package/dist/cjs/Sprinkle/utils/formatting.js +126 -0
- package/dist/cjs/Sprinkle/utils/formatting.js.map +1 -0
- package/dist/cjs/Sprinkle/utils/index.js +56 -0
- package/dist/cjs/Sprinkle/utils/index.js.map +1 -0
- package/dist/esm/Sprinkle/__tests__/encryption.test.js +3 -1
- package/dist/esm/Sprinkle/__tests__/encryption.test.js.map +1 -1
- package/dist/esm/Sprinkle/__tests__/enhancements.test.js +3 -37
- package/dist/esm/Sprinkle/__tests__/enhancements.test.js.map +1 -1
- package/dist/esm/Sprinkle/__tests__/field-utils.test.js +168 -0
- package/dist/esm/Sprinkle/__tests__/field-utils.test.js.map +1 -0
- package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js +378 -85
- package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
- package/dist/esm/Sprinkle/__tests__/formatting.test.js +95 -0
- package/dist/esm/Sprinkle/__tests__/formatting.test.js.map +1 -0
- package/dist/esm/Sprinkle/__tests__/show-menu.test.js +9 -5
- package/dist/esm/Sprinkle/__tests__/show-menu.test.js.map +1 -1
- package/dist/esm/Sprinkle/__tests__/tx-dialog.test.js +9 -0
- package/dist/esm/Sprinkle/__tests__/tx-dialog.test.js.map +1 -1
- package/dist/esm/Sprinkle/index.js +141 -96
- package/dist/esm/Sprinkle/index.js.map +1 -1
- package/dist/esm/Sprinkle/menus/array-menu.js +190 -0
- package/dist/esm/Sprinkle/menus/array-menu.js.map +1 -0
- package/dist/esm/Sprinkle/menus/field-menu.js +155 -0
- package/dist/esm/Sprinkle/menus/field-menu.js.map +1 -0
- package/dist/esm/Sprinkle/menus/index.js +8 -0
- package/dist/esm/Sprinkle/menus/index.js.map +1 -0
- package/dist/esm/Sprinkle/menus/object-menu.js +318 -0
- package/dist/esm/Sprinkle/menus/object-menu.js.map +1 -0
- package/dist/esm/Sprinkle/prompts.js +59 -1
- package/dist/esm/Sprinkle/prompts.js.map +1 -1
- package/dist/esm/Sprinkle/type-guards.js +42 -0
- package/dist/esm/Sprinkle/type-guards.js.map +1 -1
- package/dist/esm/Sprinkle/types.js +24 -0
- package/dist/esm/Sprinkle/types.js.map +1 -1
- package/dist/esm/Sprinkle/utils/field-utils.js +145 -0
- package/dist/esm/Sprinkle/utils/field-utils.js.map +1 -0
- package/dist/esm/Sprinkle/utils/formatting.js +118 -0
- package/dist/esm/Sprinkle/utils/formatting.js.map +1 -0
- package/dist/esm/Sprinkle/utils/index.js +7 -0
- package/dist/esm/Sprinkle/utils/index.js.map +1 -0
- package/dist/types/Sprinkle/index.d.ts +9 -3
- package/dist/types/Sprinkle/index.d.ts.map +1 -1
- package/dist/types/Sprinkle/menus/array-menu.d.ts +31 -0
- package/dist/types/Sprinkle/menus/array-menu.d.ts.map +1 -0
- package/dist/types/Sprinkle/menus/field-menu.d.ts +34 -0
- package/dist/types/Sprinkle/menus/field-menu.d.ts.map +1 -0
- package/dist/types/Sprinkle/menus/index.d.ts +10 -0
- package/dist/types/Sprinkle/menus/index.d.ts.map +1 -0
- package/dist/types/Sprinkle/menus/object-menu.d.ts +34 -0
- package/dist/types/Sprinkle/menus/object-menu.d.ts.map +1 -0
- package/dist/types/Sprinkle/prompts.d.ts +25 -0
- package/dist/types/Sprinkle/prompts.d.ts.map +1 -1
- package/dist/types/Sprinkle/type-guards.d.ts +24 -1
- package/dist/types/Sprinkle/type-guards.d.ts.map +1 -1
- package/dist/types/Sprinkle/types.d.ts +53 -0
- package/dist/types/Sprinkle/types.d.ts.map +1 -1
- package/dist/types/Sprinkle/utils/field-utils.d.ts +47 -0
- package/dist/types/Sprinkle/utils/field-utils.d.ts.map +1 -0
- package/dist/types/Sprinkle/utils/formatting.d.ts +30 -0
- package/dist/types/Sprinkle/utils/formatting.d.ts.map +1 -0
- package/dist/types/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/Sprinkle/__tests__/encryption.test.ts +2 -0
- package/src/Sprinkle/__tests__/enhancements.test.ts +3 -42
- package/src/Sprinkle/__tests__/field-utils.test.ts +191 -0
- package/src/Sprinkle/__tests__/fill-in-struct.test.ts +393 -100
- package/src/Sprinkle/__tests__/formatting.test.ts +115 -0
- package/src/Sprinkle/__tests__/show-menu.test.ts +14 -8
- package/src/Sprinkle/__tests__/tx-dialog.test.ts +9 -0
- package/src/Sprinkle/index.ts +175 -122
- package/src/Sprinkle/menus/array-menu.ts +191 -0
- package/src/Sprinkle/menus/field-menu.ts +145 -0
- package/src/Sprinkle/menus/index.ts +12 -0
- package/src/Sprinkle/menus/object-menu.ts +336 -0
- package/src/Sprinkle/prompts.ts +71 -1
- package/src/Sprinkle/type-guards.ts +42 -0
- package/src/Sprinkle/types.ts +43 -0
- package/src/Sprinkle/utils/field-utils.ts +158 -0
- package/src/Sprinkle/utils/formatting.ts +127 -0
- package/src/Sprinkle/utils/index.ts +17 -0
package/src/Sprinkle/prompts.ts
CHANGED
|
@@ -97,7 +97,7 @@ export const selectCancellable = createPrompt<
|
|
|
97
97
|
unknown | null,
|
|
98
98
|
SelectConfig<unknown>
|
|
99
99
|
>((config, done) => {
|
|
100
|
-
const { loop = true, pageSize =
|
|
100
|
+
const { loop = true, pageSize = 15 } = config;
|
|
101
101
|
const theme = makeTheme(selectTheme, config.theme);
|
|
102
102
|
const [status, setStatus] = useState<"idle" | "done" | "cancelled">("idle");
|
|
103
103
|
const prefix = usePrefix({ status: status === "cancelled" ? "done" : status, theme });
|
|
@@ -203,6 +203,76 @@ export const selectCancellable = createPrompt<
|
|
|
203
203
|
return `${prefix} ${config.message} ${helpTip}\n${page}`;
|
|
204
204
|
});
|
|
205
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Clears N lines above the cursor.
|
|
208
|
+
* Used after prompts to clean up menu output.
|
|
209
|
+
*/
|
|
210
|
+
export function clearLines(count: number): void {
|
|
211
|
+
// Move up and clear each line
|
|
212
|
+
process.stdout.write("\x1b[1A\x1b[2K".repeat(count));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Select prompt that clears its output after completion.
|
|
217
|
+
* Returns the selected value or null if cancelled.
|
|
218
|
+
*/
|
|
219
|
+
export async function selectWithClear<T>(
|
|
220
|
+
config: SelectConfig<T>,
|
|
221
|
+
): Promise<T | null> {
|
|
222
|
+
const result = await selectCancellable(config as SelectConfig<unknown>);
|
|
223
|
+
|
|
224
|
+
// Clear the "done" line that inquirer left behind
|
|
225
|
+
// Move up one line, clear it, move cursor to start
|
|
226
|
+
process.stdout.write("\x1b[1A\x1b[2K\x1b[G");
|
|
227
|
+
|
|
228
|
+
return result as T | null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Input prompt that clears its output after completion.
|
|
233
|
+
* Returns the input value or null if cancelled.
|
|
234
|
+
*/
|
|
235
|
+
export async function inputWithClear(
|
|
236
|
+
config: InputConfig,
|
|
237
|
+
): Promise<string | null> {
|
|
238
|
+
const result = await inputCancellable(config);
|
|
239
|
+
|
|
240
|
+
// Clear the "done" line
|
|
241
|
+
process.stdout.write("\x1b[1A\x1b[2K\x1b[G");
|
|
242
|
+
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Password prompt that clears its output after completion.
|
|
248
|
+
* Returns the password or null if cancelled.
|
|
249
|
+
*/
|
|
250
|
+
export async function passwordWithClear(
|
|
251
|
+
config: PasswordConfig,
|
|
252
|
+
): Promise<string | null> {
|
|
253
|
+
const result = await passwordCancellable(config);
|
|
254
|
+
|
|
255
|
+
// Clear the "done" line
|
|
256
|
+
process.stdout.write("\x1b[1A\x1b[2K\x1b[G");
|
|
257
|
+
|
|
258
|
+
return result;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Confirm prompt that clears its output after completion.
|
|
263
|
+
* Returns true/false or null if cancelled.
|
|
264
|
+
*/
|
|
265
|
+
export async function confirmWithClear(
|
|
266
|
+
config: ConfirmConfig,
|
|
267
|
+
): Promise<boolean | null> {
|
|
268
|
+
const result = await confirmCancellable(config);
|
|
269
|
+
|
|
270
|
+
// Clear the "done" line
|
|
271
|
+
process.stdout.write("\x1b[1A\x1b[2K\x1b[G");
|
|
272
|
+
|
|
273
|
+
return result;
|
|
274
|
+
}
|
|
275
|
+
|
|
206
276
|
interface InputConfig {
|
|
207
277
|
message: string;
|
|
208
278
|
default?: string;
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
Kind,
|
|
8
8
|
type TBigInt,
|
|
9
9
|
type TLiteral,
|
|
10
|
+
type TNull,
|
|
10
11
|
type TObject,
|
|
11
12
|
type TSchema,
|
|
12
13
|
type TString,
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
type TRef,
|
|
18
19
|
type TImport,
|
|
19
20
|
type TOptional,
|
|
21
|
+
type Static,
|
|
20
22
|
OptionalKind,
|
|
21
23
|
} from "@sinclair/typebox";
|
|
22
24
|
|
|
@@ -49,3 +51,43 @@ export const isUnion = (t: TSchema): t is TUnion => t[Kind] === "Union";
|
|
|
49
51
|
*/
|
|
50
52
|
export const isSensitive = (t: TSchema): boolean =>
|
|
51
53
|
isString(t) && t.sensitive === true;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Check if a schema is a Null type.
|
|
57
|
+
*/
|
|
58
|
+
export const isNull = (t: TSchema): t is TNull => t[Kind] === "Null";
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Check if a schema allows null values.
|
|
62
|
+
* Returns true for TNull or unions containing TNull.
|
|
63
|
+
*/
|
|
64
|
+
export const isNullable = (t: TSchema): boolean => {
|
|
65
|
+
if (isNull(t)) return true;
|
|
66
|
+
if (isUnion(t)) return t.anyOf.some((member) => isNull(member));
|
|
67
|
+
return false;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Unwrap a nullable type to get the non-null inner type.
|
|
72
|
+
* For unions with Null, returns the union without the Null member.
|
|
73
|
+
* For single non-null member unions, returns just that type.
|
|
74
|
+
*/
|
|
75
|
+
export const unwrapNullable = (t: TSchema): TSchema => {
|
|
76
|
+
if (isUnion(t)) {
|
|
77
|
+
const nonNull = t.anyOf.filter((member) => !isNull(member));
|
|
78
|
+
if (nonNull.length === 1) return nonNull[0]!;
|
|
79
|
+
return { ...t, anyOf: nonNull } as TSchema;
|
|
80
|
+
}
|
|
81
|
+
return t;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Check if a schema has a default value defined.
|
|
86
|
+
*/
|
|
87
|
+
export const hasDefault = (t: TSchema): boolean => "default" in t;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get the default value from a schema, if defined.
|
|
91
|
+
*/
|
|
92
|
+
export const getDefault = <T extends TSchema>(t: T): Static<T> | undefined =>
|
|
93
|
+
t.default as Static<T> | undefined;
|
package/src/Sprinkle/types.ts
CHANGED
|
@@ -71,3 +71,46 @@ export class UserCancelledError extends Error {
|
|
|
71
71
|
this.name = "UserCancelledError";
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
|
+
|
|
75
|
+
// --- Menu-based FillInStruct types ---
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* State tracking for a single field during menu-based editing.
|
|
79
|
+
*/
|
|
80
|
+
export type FieldState<T = unknown> =
|
|
81
|
+
| { status: "unset" }
|
|
82
|
+
| { status: "set"; value: T }
|
|
83
|
+
| { status: "null" }; // explicitly set to null
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Result of counting required fields in a schema.
|
|
87
|
+
*/
|
|
88
|
+
export interface RequiredFieldCount {
|
|
89
|
+
total: number;
|
|
90
|
+
filled: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Result from field menu interaction.
|
|
95
|
+
*/
|
|
96
|
+
export type FieldMenuResult<T = unknown> =
|
|
97
|
+
| { action: "edit" }
|
|
98
|
+
| { action: "view" }
|
|
99
|
+
| { action: "clear" }
|
|
100
|
+
| { action: "setNull" }
|
|
101
|
+
| { action: "reset"; defaultValue: T }
|
|
102
|
+
| { action: "back" };
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Result from object menu interaction.
|
|
106
|
+
*/
|
|
107
|
+
export type ObjectMenuResult<T = unknown> =
|
|
108
|
+
| { action: "submit"; value: T }
|
|
109
|
+
| { action: "cancel" };
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Result from array menu interaction.
|
|
113
|
+
*/
|
|
114
|
+
export type ArrayMenuResult<T = unknown> =
|
|
115
|
+
| { action: "done"; value: T[] }
|
|
116
|
+
| { action: "back" };
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Field utilities for menu-based struct editing.
|
|
3
|
+
* Provides required field counting and label building.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { TObject, TSchema } from "@sinclair/typebox";
|
|
7
|
+
import type { FieldState, RequiredFieldCount } from "../types.js";
|
|
8
|
+
import {
|
|
9
|
+
isOptional,
|
|
10
|
+
isNullable,
|
|
11
|
+
hasDefault,
|
|
12
|
+
getDefault,
|
|
13
|
+
isLiteral,
|
|
14
|
+
} from "../type-guards.js";
|
|
15
|
+
import { formatValuePreview } from "./formatting.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Count required fields in an object schema and how many are filled.
|
|
19
|
+
*
|
|
20
|
+
* @param type - The object schema
|
|
21
|
+
* @param state - Map of field names to their current state
|
|
22
|
+
* @returns Count of total required fields and how many are filled
|
|
23
|
+
*/
|
|
24
|
+
export function countRequiredFields(
|
|
25
|
+
type: TObject,
|
|
26
|
+
state: Map<string, FieldState>,
|
|
27
|
+
): RequiredFieldCount {
|
|
28
|
+
const fields = type.properties as Record<string, TSchema>;
|
|
29
|
+
let total = 0;
|
|
30
|
+
let filled = 0;
|
|
31
|
+
|
|
32
|
+
for (const [fieldName, fieldType] of Object.entries(fields)) {
|
|
33
|
+
// Skip optional fields - they're not required
|
|
34
|
+
if (isOptional(fieldType)) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Non-nullable fields with defaults don't require user input
|
|
39
|
+
if (hasDefault(fieldType) && !isNullable(fieldType)) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Literal fields auto-fill, so they're not required from the user
|
|
44
|
+
if (isLiteral(fieldType)) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// This is a required field
|
|
49
|
+
total++;
|
|
50
|
+
|
|
51
|
+
// Check if it's filled
|
|
52
|
+
const fieldState = state.get(fieldName);
|
|
53
|
+
if (fieldState?.status === "set" || fieldState?.status === "null") {
|
|
54
|
+
filled++;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { total, filled };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Determine if a field is required (must have a value to submit).
|
|
63
|
+
*
|
|
64
|
+
* @param fieldType - The field's schema
|
|
65
|
+
* @returns true if the field is required
|
|
66
|
+
*/
|
|
67
|
+
export function isFieldRequired(fieldType: TSchema): boolean {
|
|
68
|
+
// Optional fields are not required
|
|
69
|
+
if (isOptional(fieldType)) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Literal fields auto-fill, so they're not required from the user
|
|
74
|
+
if (isLiteral(fieldType)) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Non-nullable fields with defaults are not required (default will be used)
|
|
79
|
+
if (hasDefault(fieldType) && !isNullable(fieldType)) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Build a display label for a field in the menu.
|
|
88
|
+
*
|
|
89
|
+
* @param fieldName - The field name
|
|
90
|
+
* @param fieldType - The field's schema
|
|
91
|
+
* @param state - The current state of the field
|
|
92
|
+
* @returns Formatted label string
|
|
93
|
+
*/
|
|
94
|
+
export function buildFieldLabel(
|
|
95
|
+
fieldName: string,
|
|
96
|
+
fieldType: TSchema,
|
|
97
|
+
state: FieldState,
|
|
98
|
+
): string {
|
|
99
|
+
const required = isFieldRequired(fieldType);
|
|
100
|
+
const requiredMarker = required ? " *" : "";
|
|
101
|
+
|
|
102
|
+
switch (state.status) {
|
|
103
|
+
case "unset": {
|
|
104
|
+
// Check for default value
|
|
105
|
+
if (hasDefault(fieldType)) {
|
|
106
|
+
const defaultVal = getDefault(fieldType);
|
|
107
|
+
const preview = formatValuePreview(defaultVal, 30);
|
|
108
|
+
return `${fieldName}: [default: ${preview}]${requiredMarker}`;
|
|
109
|
+
}
|
|
110
|
+
return `${fieldName}: [not set]${requiredMarker}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
case "null":
|
|
114
|
+
return `${fieldName}: null${requiredMarker}`;
|
|
115
|
+
|
|
116
|
+
case "set": {
|
|
117
|
+
const preview = formatValuePreview(state.value, 40);
|
|
118
|
+
return `${fieldName}: ${preview}${requiredMarker}`;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get the initial FieldState for a field based on its type and any existing value.
|
|
125
|
+
*
|
|
126
|
+
* @param _fieldType - The field's schema (reserved for future logic)
|
|
127
|
+
* @param existingValue - Existing value if editing
|
|
128
|
+
* @returns Initial FieldState
|
|
129
|
+
*/
|
|
130
|
+
export function getInitialFieldState<T>(
|
|
131
|
+
_fieldType: TSchema,
|
|
132
|
+
existingValue?: T,
|
|
133
|
+
): FieldState<T> {
|
|
134
|
+
if (existingValue === undefined) {
|
|
135
|
+
return { status: "unset" };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (existingValue === null) {
|
|
139
|
+
return { status: "null" };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { status: "set", value: existingValue };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Check if all required fields are filled in the state map.
|
|
147
|
+
*
|
|
148
|
+
* @param type - The object schema
|
|
149
|
+
* @param state - Map of field names to their current state
|
|
150
|
+
* @returns true if all required fields have values
|
|
151
|
+
*/
|
|
152
|
+
export function allRequiredFieldsFilled(
|
|
153
|
+
type: TObject,
|
|
154
|
+
state: Map<string, FieldState>,
|
|
155
|
+
): boolean {
|
|
156
|
+
const { total, filled } = countRequiredFields(type, state);
|
|
157
|
+
return filled >= total;
|
|
158
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formatting utilities for menu-based struct editing.
|
|
3
|
+
* Provides value preview, path formatting, and breadcrumb generation.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Format a value for display in a menu item.
|
|
8
|
+
* Truncates long values and provides type-appropriate previews.
|
|
9
|
+
*
|
|
10
|
+
* @param value - The value to format
|
|
11
|
+
* @param maxLength - Maximum length before truncation (default 40)
|
|
12
|
+
* @returns Formatted string representation
|
|
13
|
+
*/
|
|
14
|
+
export function formatValuePreview(value: unknown, maxLength = 40): string {
|
|
15
|
+
if (value === null) {
|
|
16
|
+
return "null";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (value === undefined) {
|
|
20
|
+
return "[not set]";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (typeof value === "string") {
|
|
24
|
+
const escaped = JSON.stringify(value);
|
|
25
|
+
if (escaped.length <= maxLength) {
|
|
26
|
+
return escaped;
|
|
27
|
+
}
|
|
28
|
+
// Truncate inside the quotes (guard against negative maxLength from recursion)
|
|
29
|
+
return `"${value.slice(0, Math.max(0, maxLength - 5))}..."`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (typeof value === "number" || typeof value === "bigint") {
|
|
33
|
+
return String(value);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (typeof value === "boolean") {
|
|
37
|
+
return String(value);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (Array.isArray(value)) {
|
|
41
|
+
return `[${value.length} item${value.length === 1 ? "" : "s"}]`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (typeof value === "object") {
|
|
45
|
+
// For objects, try to show a meaningful preview
|
|
46
|
+
const keys = Object.keys(value);
|
|
47
|
+
if (keys.length === 0) {
|
|
48
|
+
return "{}";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// If object has a single key (like union type), show it
|
|
52
|
+
if (keys.length === 1) {
|
|
53
|
+
const key = keys[0]!;
|
|
54
|
+
const innerValue = (value as Record<string, unknown>)[key];
|
|
55
|
+
const innerPreview = formatValuePreview(innerValue, maxLength - key.length - 6);
|
|
56
|
+
const preview = `{ ${key}: ${innerPreview} }`;
|
|
57
|
+
if (preview.length <= maxLength) {
|
|
58
|
+
return preview;
|
|
59
|
+
}
|
|
60
|
+
return `{ ${key}: ... }`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Multiple keys - show abbreviated
|
|
64
|
+
const firstKey = keys[0]!;
|
|
65
|
+
return `{ ${firstKey}: ..., +${keys.length - 1} }`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return String(value).slice(0, maxLength);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Format a path array for display, stripping the "root" prefix.
|
|
73
|
+
*
|
|
74
|
+
* @param path - Array of path segments (e.g., ["root", "settings", "name"])
|
|
75
|
+
* @returns Formatted path string (e.g., "settings.name")
|
|
76
|
+
*/
|
|
77
|
+
export function formatPath(path: string[]): string {
|
|
78
|
+
// Filter out "root" prefix
|
|
79
|
+
const filtered = path.filter((segment) => segment !== "root");
|
|
80
|
+
if (filtered.length === 0) {
|
|
81
|
+
return "";
|
|
82
|
+
}
|
|
83
|
+
return filtered.join(".");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Format a path as a breadcrumb trail for nested navigation.
|
|
88
|
+
* Truncates from the left if too long.
|
|
89
|
+
*
|
|
90
|
+
* @param path - Array of path segments
|
|
91
|
+
* @param maxLength - Maximum total length (default 50)
|
|
92
|
+
* @returns Breadcrumb string (e.g., "settings → permissions → mint")
|
|
93
|
+
*/
|
|
94
|
+
export function formatBreadcrumb(path: string[], maxLength = 50): string {
|
|
95
|
+
// Filter out "root" prefix
|
|
96
|
+
const filtered = path.filter((segment) => segment !== "root");
|
|
97
|
+
if (filtered.length === 0) {
|
|
98
|
+
return "";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const separator = " \u2192 "; // Unicode right arrow
|
|
102
|
+
const joined = filtered.join(separator);
|
|
103
|
+
|
|
104
|
+
if (joined.length <= maxLength) {
|
|
105
|
+
return joined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Truncate from left, keeping most recent segments
|
|
109
|
+
const ellipsis = "...";
|
|
110
|
+
let result = "";
|
|
111
|
+
for (let i = filtered.length - 1; i >= 0; i--) {
|
|
112
|
+
const segment = filtered[i]!;
|
|
113
|
+
const candidate = i === filtered.length - 1 ? segment : segment + separator + result;
|
|
114
|
+
if (candidate.length + ellipsis.length + separator.length > maxLength && i < filtered.length - 1) {
|
|
115
|
+
return ellipsis + separator + result;
|
|
116
|
+
}
|
|
117
|
+
result = candidate;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Safety clamp: if we still exceeded maxLength (e.g., last segment alone is too long),
|
|
121
|
+
// fall back to just the ellipsis to honor the maxLength contract.
|
|
122
|
+
if (result.length > maxLength) {
|
|
123
|
+
return ellipsis;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for menu-based struct editing.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
formatValuePreview,
|
|
7
|
+
formatPath,
|
|
8
|
+
formatBreadcrumb,
|
|
9
|
+
} from "./formatting.js";
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
countRequiredFields,
|
|
13
|
+
isFieldRequired,
|
|
14
|
+
buildFieldLabel,
|
|
15
|
+
getInitialFieldState,
|
|
16
|
+
allRequiredFieldsFilled,
|
|
17
|
+
} from "./field-utils.js";
|