@portel/photon-core 2.3.0 → 2.5.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 (130) hide show
  1. package/dist/asset-discovery.d.ts +25 -0
  2. package/dist/asset-discovery.d.ts.map +1 -0
  3. package/dist/asset-discovery.js +145 -0
  4. package/dist/asset-discovery.js.map +1 -0
  5. package/dist/base.d.ts +6 -0
  6. package/dist/base.d.ts.map +1 -1
  7. package/dist/base.js +11 -1
  8. package/dist/base.js.map +1 -1
  9. package/dist/class-detection.d.ts +32 -0
  10. package/dist/class-detection.d.ts.map +1 -0
  11. package/dist/class-detection.js +86 -0
  12. package/dist/class-detection.js.map +1 -0
  13. package/dist/collections/ReactiveArray.d.ts +97 -0
  14. package/dist/collections/ReactiveArray.d.ts.map +1 -0
  15. package/dist/collections/ReactiveArray.js +158 -0
  16. package/dist/collections/ReactiveArray.js.map +1 -0
  17. package/dist/collections/ReactiveMap.d.ts +50 -0
  18. package/dist/collections/ReactiveMap.d.ts.map +1 -0
  19. package/dist/collections/ReactiveMap.js +71 -0
  20. package/dist/collections/ReactiveMap.js.map +1 -0
  21. package/dist/collections/ReactiveSet.d.ts +50 -0
  22. package/dist/collections/ReactiveSet.d.ts.map +1 -0
  23. package/dist/collections/ReactiveSet.js +71 -0
  24. package/dist/collections/ReactiveSet.js.map +1 -0
  25. package/dist/collections/index.d.ts +44 -0
  26. package/dist/collections/index.d.ts.map +1 -0
  27. package/dist/collections/index.js +44 -0
  28. package/dist/collections/index.js.map +1 -0
  29. package/dist/compiler.d.ts +22 -0
  30. package/dist/compiler.d.ts.map +1 -0
  31. package/dist/compiler.js +48 -0
  32. package/dist/compiler.js.map +1 -0
  33. package/dist/env-utils.d.ts +61 -0
  34. package/dist/env-utils.d.ts.map +1 -0
  35. package/dist/env-utils.js +171 -0
  36. package/dist/env-utils.js.map +1 -0
  37. package/dist/index.d.ts +9 -0
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +37 -0
  40. package/dist/index.js.map +1 -1
  41. package/dist/mime-types.d.ts +13 -0
  42. package/dist/mime-types.d.ts.map +1 -0
  43. package/dist/mime-types.js +47 -0
  44. package/dist/mime-types.js.map +1 -0
  45. package/dist/rendering/index.d.ts +49 -0
  46. package/dist/rendering/index.d.ts.map +1 -1
  47. package/dist/rendering/index.js +153 -0
  48. package/dist/rendering/index.js.map +1 -1
  49. package/dist/schema-extractor.d.ts.map +1 -1
  50. package/dist/schema-extractor.js +3 -0
  51. package/dist/schema-extractor.js.map +1 -1
  52. package/dist/types.d.ts +4 -0
  53. package/dist/types.d.ts.map +1 -1
  54. package/dist/types.js.map +1 -1
  55. package/dist/ui-types/Cards.d.ts +139 -0
  56. package/dist/ui-types/Cards.d.ts.map +1 -0
  57. package/dist/ui-types/Cards.js +235 -0
  58. package/dist/ui-types/Cards.js.map +1 -0
  59. package/dist/ui-types/Chart.d.ts +136 -0
  60. package/dist/ui-types/Chart.d.ts.map +1 -0
  61. package/dist/ui-types/Chart.js +188 -0
  62. package/dist/ui-types/Chart.js.map +1 -0
  63. package/dist/ui-types/Field.d.ts +342 -0
  64. package/dist/ui-types/Field.d.ts.map +1 -0
  65. package/dist/ui-types/Field.js +200 -0
  66. package/dist/ui-types/Field.js.map +1 -0
  67. package/dist/ui-types/FieldRenderer.d.ts +32 -0
  68. package/dist/ui-types/FieldRenderer.d.ts.map +1 -0
  69. package/dist/ui-types/FieldRenderer.js +277 -0
  70. package/dist/ui-types/FieldRenderer.js.map +1 -0
  71. package/dist/ui-types/Form.d.ts +212 -0
  72. package/dist/ui-types/Form.d.ts.map +1 -0
  73. package/dist/ui-types/Form.js +278 -0
  74. package/dist/ui-types/Form.js.map +1 -0
  75. package/dist/ui-types/Progress.d.ts +130 -0
  76. package/dist/ui-types/Progress.d.ts.map +1 -0
  77. package/dist/ui-types/Progress.js +191 -0
  78. package/dist/ui-types/Progress.js.map +1 -0
  79. package/dist/ui-types/Stats.d.ts +108 -0
  80. package/dist/ui-types/Stats.d.ts.map +1 -0
  81. package/dist/ui-types/Stats.js +162 -0
  82. package/dist/ui-types/Stats.js.map +1 -0
  83. package/dist/ui-types/Table.d.ts +206 -0
  84. package/dist/ui-types/Table.d.ts.map +1 -0
  85. package/dist/ui-types/Table.js +367 -0
  86. package/dist/ui-types/Table.js.map +1 -0
  87. package/dist/ui-types/base.d.ts +17 -0
  88. package/dist/ui-types/base.d.ts.map +1 -0
  89. package/dist/ui-types/base.js +18 -0
  90. package/dist/ui-types/base.js.map +1 -0
  91. package/dist/ui-types/index.d.ts +42 -0
  92. package/dist/ui-types/index.d.ts.map +1 -0
  93. package/dist/ui-types/index.js +50 -0
  94. package/dist/ui-types/index.js.map +1 -0
  95. package/dist/validation.d.ts +51 -0
  96. package/dist/validation.d.ts.map +1 -0
  97. package/dist/validation.js +249 -0
  98. package/dist/validation.js.map +1 -0
  99. package/dist/version-check.d.ts +22 -0
  100. package/dist/version-check.d.ts.map +1 -0
  101. package/dist/version-check.js +91 -0
  102. package/dist/version-check.js.map +1 -0
  103. package/package.json +2 -2
  104. package/src/asset-discovery.ts +161 -0
  105. package/src/base.ts +13 -1
  106. package/src/class-detection.ts +94 -0
  107. package/src/collections/ReactiveArray.ts +179 -0
  108. package/src/collections/ReactiveMap.ts +81 -0
  109. package/src/collections/ReactiveSet.ts +81 -0
  110. package/src/collections/index.ts +44 -0
  111. package/src/compiler.ts +57 -0
  112. package/src/env-utils.ts +216 -0
  113. package/src/index.ts +155 -0
  114. package/src/mime-types.ts +49 -0
  115. package/src/rendering/index.ts +197 -0
  116. package/src/schema-extractor.ts +4 -0
  117. package/src/types.ts +4 -0
  118. package/src/ui-types/Cards.ts +286 -0
  119. package/src/ui-types/Chart.ts +239 -0
  120. package/src/ui-types/Field.ts +594 -0
  121. package/src/ui-types/FieldRenderer.ts +364 -0
  122. package/src/ui-types/Form.ts +363 -0
  123. package/src/ui-types/Progress.ts +237 -0
  124. package/src/ui-types/Stats.ts +204 -0
  125. package/src/ui-types/Table.ts +438 -0
  126. package/src/ui-types/base.ts +25 -0
  127. package/src/ui-types/index.ts +96 -0
  128. package/src/ui-types/ui-types.test.ts +444 -0
  129. package/src/validation.ts +363 -0
  130. package/src/version-check.ts +92 -0
@@ -0,0 +1,363 @@
1
+ /**
2
+ * Form - Purpose-driven type for interactive forms
3
+ *
4
+ * Automatically renders as a form that submits back to the photon.
5
+ * Unlike io.ask.form (which is for elicitation during method execution),
6
+ * Form is a return type that creates a persistent interactive form.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * async settings() {
11
+ * return new Form()
12
+ * .title('User Settings')
13
+ * .text('name', 'Display Name', { required: true })
14
+ * .email('email', 'Email Address')
15
+ * .select('theme', 'Theme', ['light', 'dark', 'auto'])
16
+ * .toggle('notifications', 'Enable Notifications')
17
+ * .submit('Save Settings', 'saveSettings');
18
+ * }
19
+ *
20
+ * async saveSettings(params: { name: string; email: string; theme: string; notifications: boolean }) {
21
+ * // Handle form submission
22
+ * return { success: true };
23
+ * }
24
+ * ```
25
+ */
26
+
27
+ import { PhotonUIType } from './base.js';
28
+
29
+ export type FieldType =
30
+ | 'text'
31
+ | 'email'
32
+ | 'password'
33
+ | 'number'
34
+ | 'textarea'
35
+ | 'select'
36
+ | 'multiselect'
37
+ | 'toggle'
38
+ | 'checkbox'
39
+ | 'radio'
40
+ | 'date'
41
+ | 'time'
42
+ | 'datetime'
43
+ | 'file'
44
+ | 'color'
45
+ | 'range'
46
+ | 'hidden';
47
+
48
+ export interface FormField {
49
+ name: string;
50
+ type: FieldType;
51
+ label?: string;
52
+ placeholder?: string;
53
+ defaultValue?: any;
54
+ required?: boolean;
55
+ disabled?: boolean;
56
+ options?: Array<string | { label: string; value: any }>;
57
+ min?: number;
58
+ max?: number;
59
+ step?: number;
60
+ rows?: number;
61
+ accept?: string; // For file inputs
62
+ pattern?: string;
63
+ helpText?: string;
64
+ group?: string; // For grouping fields
65
+ }
66
+
67
+ export interface FormOptions {
68
+ title?: string;
69
+ description?: string;
70
+ submitLabel?: string;
71
+ submitMethod?: string; // Photon method to call on submit
72
+ cancelLabel?: string;
73
+ layout?: 'vertical' | 'horizontal' | 'inline';
74
+ columns?: 1 | 2 | 3;
75
+ showReset?: boolean;
76
+ confirmSubmit?: string; // Confirmation message before submit
77
+ }
78
+
79
+ export class Form extends PhotonUIType {
80
+ readonly _photonType = 'form' as const;
81
+
82
+ private _fields: FormField[] = [];
83
+ private _options: FormOptions = {
84
+ layout: 'vertical',
85
+ columns: 1,
86
+ submitLabel: 'Submit',
87
+ };
88
+
89
+ /**
90
+ * Create a new Form
91
+ */
92
+ constructor() {
93
+ super();
94
+ }
95
+
96
+ /**
97
+ * Set form title
98
+ */
99
+ title(title: string): this {
100
+ this._options.title = title;
101
+ return this;
102
+ }
103
+
104
+ /**
105
+ * Set form description
106
+ */
107
+ description(text: string): this {
108
+ this._options.description = text;
109
+ return this;
110
+ }
111
+
112
+ /**
113
+ * Add a generic field
114
+ */
115
+ field(name: string, type: FieldType, label?: string, options?: Partial<FormField>): this {
116
+ this._fields.push({
117
+ name,
118
+ type,
119
+ label: label ?? this._formatLabel(name),
120
+ ...options,
121
+ });
122
+ return this;
123
+ }
124
+
125
+ /**
126
+ * Add a text input
127
+ */
128
+ text(name: string, label?: string, options?: Partial<FormField>): this {
129
+ return this.field(name, 'text', label, options);
130
+ }
131
+
132
+ /**
133
+ * Add an email input
134
+ */
135
+ email(name: string, label?: string, options?: Partial<FormField>): this {
136
+ return this.field(name, 'email', label, options);
137
+ }
138
+
139
+ /**
140
+ * Add a password input
141
+ */
142
+ password(name: string, label?: string, options?: Partial<FormField>): this {
143
+ return this.field(name, 'password', label, options);
144
+ }
145
+
146
+ /**
147
+ * Add a number input
148
+ */
149
+ number(name: string, label?: string, options?: Partial<FormField> & { min?: number; max?: number; step?: number }): this {
150
+ return this.field(name, 'number', label, options);
151
+ }
152
+
153
+ /**
154
+ * Add a textarea
155
+ */
156
+ textarea(name: string, label?: string, options?: Partial<FormField> & { rows?: number }): this {
157
+ return this.field(name, 'textarea', label, { rows: 4, ...options });
158
+ }
159
+
160
+ /**
161
+ * Add a select dropdown
162
+ */
163
+ select(name: string, label: string | undefined, choices: Array<string | { label: string; value: any }>, options?: Partial<FormField>): this {
164
+ return this.field(name, 'select', label, { options: choices, ...options });
165
+ }
166
+
167
+ /**
168
+ * Add a multi-select
169
+ */
170
+ multiselect(name: string, label: string | undefined, choices: Array<string | { label: string; value: any }>, options?: Partial<FormField>): this {
171
+ return this.field(name, 'multiselect', label, { options: choices, ...options });
172
+ }
173
+
174
+ /**
175
+ * Add a toggle switch
176
+ */
177
+ toggle(name: string, label?: string, options?: Partial<FormField>): this {
178
+ return this.field(name, 'toggle', label, options);
179
+ }
180
+
181
+ /**
182
+ * Add a checkbox
183
+ */
184
+ checkbox(name: string, label?: string, options?: Partial<FormField>): this {
185
+ return this.field(name, 'checkbox', label, options);
186
+ }
187
+
188
+ /**
189
+ * Add radio buttons
190
+ */
191
+ radio(name: string, label: string | undefined, choices: Array<string | { label: string; value: any }>, options?: Partial<FormField>): this {
192
+ return this.field(name, 'radio', label, { options: choices, ...options });
193
+ }
194
+
195
+ /**
196
+ * Add a date picker
197
+ */
198
+ date(name: string, label?: string, options?: Partial<FormField>): this {
199
+ return this.field(name, 'date', label, options);
200
+ }
201
+
202
+ /**
203
+ * Add a time picker
204
+ */
205
+ time(name: string, label?: string, options?: Partial<FormField>): this {
206
+ return this.field(name, 'time', label, options);
207
+ }
208
+
209
+ /**
210
+ * Add a datetime picker
211
+ */
212
+ datetime(name: string, label?: string, options?: Partial<FormField>): this {
213
+ return this.field(name, 'datetime', label, options);
214
+ }
215
+
216
+ /**
217
+ * Add a file upload
218
+ */
219
+ file(name: string, label?: string, options?: Partial<FormField> & { accept?: string }): this {
220
+ return this.field(name, 'file', label, options);
221
+ }
222
+
223
+ /**
224
+ * Add a color picker
225
+ */
226
+ color(name: string, label?: string, options?: Partial<FormField>): this {
227
+ return this.field(name, 'color', label, options);
228
+ }
229
+
230
+ /**
231
+ * Add a range slider
232
+ */
233
+ range(name: string, label?: string, options?: Partial<FormField> & { min?: number; max?: number; step?: number }): this {
234
+ return this.field(name, 'range', label, { min: 0, max: 100, step: 1, ...options });
235
+ }
236
+
237
+ /**
238
+ * Add a hidden field
239
+ */
240
+ hidden(name: string, value: any): this {
241
+ return this.field(name, 'hidden', undefined, { defaultValue: value });
242
+ }
243
+
244
+ /**
245
+ * Configure submit button
246
+ */
247
+ submit(label: string, method?: string): this {
248
+ this._options.submitLabel = label;
249
+ if (method) this._options.submitMethod = method;
250
+ return this;
251
+ }
252
+
253
+ /**
254
+ * Add cancel button
255
+ */
256
+ cancel(label: string = 'Cancel'): this {
257
+ this._options.cancelLabel = label;
258
+ return this;
259
+ }
260
+
261
+ /**
262
+ * Set form layout
263
+ */
264
+ layout(type: 'vertical' | 'horizontal' | 'inline'): this {
265
+ this._options.layout = type;
266
+ return this;
267
+ }
268
+
269
+ /**
270
+ * Set number of columns
271
+ */
272
+ columns(count: 1 | 2 | 3): this {
273
+ this._options.columns = count;
274
+ return this;
275
+ }
276
+
277
+ /**
278
+ * Show reset button
279
+ */
280
+ showReset(enabled: boolean = true): this {
281
+ this._options.showReset = enabled;
282
+ return this;
283
+ }
284
+
285
+ /**
286
+ * Require confirmation before submit
287
+ */
288
+ confirmSubmit(message: string): this {
289
+ this._options.confirmSubmit = message;
290
+ return this;
291
+ }
292
+
293
+ /**
294
+ * Group subsequent fields
295
+ */
296
+ group(name: string): this {
297
+ // Mark for next fields
298
+ // Implementation note: fields added after this call get this group
299
+ return this;
300
+ }
301
+
302
+ /**
303
+ * Format field name to label
304
+ */
305
+ private _formatLabel(name: string): string {
306
+ return name
307
+ .replace(/([A-Z])/g, ' $1')
308
+ .replace(/[_-]/g, ' ')
309
+ .replace(/^\w/, c => c.toUpperCase())
310
+ .trim();
311
+ }
312
+
313
+ toJSON() {
314
+ return {
315
+ _photonType: this._photonType,
316
+ fields: this._fields,
317
+ options: this._options,
318
+ };
319
+ }
320
+
321
+ /**
322
+ * Render as plain text for MCP clients
323
+ * Shows the form structure (fields are not interactive in plain text)
324
+ */
325
+ toString(): string {
326
+ const lines: string[] = [];
327
+
328
+ if (this._options.title) {
329
+ lines.push(`## ${this._options.title}`);
330
+ }
331
+ if (this._options.description) {
332
+ lines.push(this._options.description);
333
+ }
334
+ if (this._options.title || this._options.description) {
335
+ lines.push('');
336
+ }
337
+
338
+ lines.push('**Form Fields:**');
339
+ for (const field of this._fields) {
340
+ if (field.type === 'hidden') continue;
341
+
342
+ let line = `- ${field.label ?? field.name}`;
343
+ if (field.required) line += ' (required)';
344
+
345
+ // Show options for select/radio
346
+ if (field.options && field.options.length > 0) {
347
+ const opts = field.options.map(o => typeof o === 'string' ? o : o.label);
348
+ line += `: [${opts.join(', ')}]`;
349
+ } else {
350
+ line += `: (${field.type})`;
351
+ }
352
+
353
+ if (field.helpText) line += ` - ${field.helpText}`;
354
+ lines.push(line);
355
+ }
356
+
357
+ if (this._options.submitLabel) {
358
+ lines.push('', `[${this._options.submitLabel}]`);
359
+ }
360
+
361
+ return lines.join('\n');
362
+ }
363
+ }
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Progress - Purpose-driven type for progress indicators
3
+ *
4
+ * Automatically renders as progress bars or steps.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * // Simple progress bar
9
+ * async uploadStatus() {
10
+ * return new Progress(75)
11
+ * .label('Uploading files...')
12
+ * .color('blue');
13
+ * }
14
+ *
15
+ * // Multiple progress bars
16
+ * async projectStatus() {
17
+ * return new Progress()
18
+ * .bar('Design', 100, { color: 'green' })
19
+ * .bar('Development', 65, { color: 'blue' })
20
+ * .bar('Testing', 20, { color: 'yellow' });
21
+ * }
22
+ *
23
+ * // Step indicator
24
+ * async checkoutSteps() {
25
+ * return new Progress('steps')
26
+ * .step('Cart', 'completed')
27
+ * .step('Shipping', 'current')
28
+ * .step('Payment', 'pending')
29
+ * .step('Confirm', 'pending');
30
+ * }
31
+ * ```
32
+ */
33
+
34
+ import { PhotonUIType } from './base.js';
35
+
36
+ export type ProgressStyle = 'bar' | 'steps' | 'circle';
37
+ export type StepStatus = 'pending' | 'current' | 'completed' | 'error';
38
+
39
+ export interface ProgressBar {
40
+ label: string;
41
+ value: number;
42
+ max?: number;
43
+ color?: string;
44
+ showValue?: boolean;
45
+ }
46
+
47
+ export interface ProgressStep {
48
+ label: string;
49
+ status: StepStatus;
50
+ description?: string;
51
+ icon?: string;
52
+ }
53
+
54
+ export interface ProgressOptions {
55
+ title?: string;
56
+ style?: ProgressStyle;
57
+ color?: string;
58
+ size?: 'sm' | 'md' | 'lg';
59
+ striped?: boolean;
60
+ animated?: boolean;
61
+ showValue?: boolean;
62
+ }
63
+
64
+ export class Progress extends PhotonUIType {
65
+ readonly _photonType = 'progress' as const;
66
+
67
+ private _bars: ProgressBar[] = [];
68
+ private _steps: ProgressStep[] = [];
69
+ private _value: number = 0;
70
+ private _max: number = 100;
71
+ private _options: ProgressOptions = {
72
+ style: 'bar',
73
+ size: 'md',
74
+ showValue: true,
75
+ animated: true,
76
+ };
77
+
78
+ /**
79
+ * Create a new Progress indicator
80
+ * @param valueOrStyle Initial value (0-100) or style ('steps', 'circle')
81
+ */
82
+ constructor(valueOrStyle?: number | ProgressStyle) {
83
+ super();
84
+ if (typeof valueOrStyle === 'number') {
85
+ this._value = valueOrStyle;
86
+ } else if (valueOrStyle) {
87
+ this._options.style = valueOrStyle;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Set progress value (0-100)
93
+ */
94
+ value(val: number, max?: number): this {
95
+ this._value = val;
96
+ if (max !== undefined) this._max = max;
97
+ return this;
98
+ }
99
+
100
+ /**
101
+ * Set label
102
+ */
103
+ label(text: string): this {
104
+ this._options.title = text;
105
+ return this;
106
+ }
107
+
108
+ /**
109
+ * Add a progress bar (for multi-bar display)
110
+ */
111
+ bar(label: string, value: number, options?: { max?: number; color?: string; showValue?: boolean }): this {
112
+ this._bars.push({
113
+ label,
114
+ value,
115
+ max: options?.max ?? 100,
116
+ color: options?.color,
117
+ showValue: options?.showValue ?? true,
118
+ });
119
+ return this;
120
+ }
121
+
122
+ /**
123
+ * Add a step (for step indicator)
124
+ */
125
+ step(label: string, status: StepStatus = 'pending', options?: { description?: string; icon?: string }): this {
126
+ this._options.style = 'steps';
127
+ this._steps.push({
128
+ label,
129
+ status,
130
+ ...options,
131
+ });
132
+ return this;
133
+ }
134
+
135
+ /**
136
+ * Set color
137
+ */
138
+ color(color: string): this {
139
+ this._options.color = color;
140
+ return this;
141
+ }
142
+
143
+ /**
144
+ * Set size
145
+ */
146
+ size(size: 'sm' | 'md' | 'lg'): this {
147
+ this._options.size = size;
148
+ return this;
149
+ }
150
+
151
+ /**
152
+ * Use striped style
153
+ */
154
+ striped(enabled: boolean = true): this {
155
+ this._options.striped = enabled;
156
+ return this;
157
+ }
158
+
159
+ /**
160
+ * Enable animation
161
+ */
162
+ animated(enabled: boolean = true): this {
163
+ this._options.animated = enabled;
164
+ return this;
165
+ }
166
+
167
+ /**
168
+ * Show/hide value text
169
+ */
170
+ showValue(enabled: boolean = true): this {
171
+ this._options.showValue = enabled;
172
+ return this;
173
+ }
174
+
175
+ /**
176
+ * Use circle style
177
+ */
178
+ circle(): this {
179
+ this._options.style = 'circle';
180
+ return this;
181
+ }
182
+
183
+ toJSON() {
184
+ return {
185
+ _photonType: this._photonType,
186
+ value: this._value,
187
+ max: this._max,
188
+ bars: this._bars,
189
+ steps: this._steps,
190
+ options: this._options,
191
+ };
192
+ }
193
+
194
+ /**
195
+ * Render as plain text for MCP clients
196
+ */
197
+ toString(): string {
198
+ const lines: string[] = [];
199
+
200
+ if (this._options.title) {
201
+ lines.push(this._options.title);
202
+ }
203
+
204
+ // Steps display
205
+ if (this._steps.length > 0) {
206
+ const stepMarkers = this._steps.map(s => {
207
+ switch (s.status) {
208
+ case 'completed': return `[✓] ${s.label}`;
209
+ case 'current': return `[●] ${s.label}`;
210
+ case 'error': return `[✗] ${s.label}`;
211
+ default: return `[ ] ${s.label}`;
212
+ }
213
+ });
214
+ lines.push(stepMarkers.join(' → '));
215
+ return lines.join('\n');
216
+ }
217
+
218
+ // Multiple bars
219
+ if (this._bars.length > 0) {
220
+ for (const bar of this._bars) {
221
+ const pct = Math.round((bar.value / (bar.max ?? 100)) * 100);
222
+ const filled = Math.round(pct / 5);
223
+ const barStr = '█'.repeat(filled) + '░'.repeat(20 - filled);
224
+ lines.push(`${bar.label}: [${barStr}] ${pct}%`);
225
+ }
226
+ return lines.join('\n');
227
+ }
228
+
229
+ // Single progress bar
230
+ const pct = Math.round((this._value / this._max) * 100);
231
+ const filled = Math.round(pct / 5);
232
+ const barStr = '█'.repeat(filled) + '░'.repeat(20 - filled);
233
+ lines.push(`[${barStr}] ${pct}%`);
234
+
235
+ return lines.join('\n');
236
+ }
237
+ }