@fraxic/ui 0.3.0 → 0.3.2
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/README.md +344 -0
- package/index.d.ts +0 -1
- package/index.js +20 -3
- package/package.json +3 -2
package/README.md
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
# @fraxic/ui
|
|
2
|
+
|
|
3
|
+
Build dashboard pages and handle dashboard interactions from a Fraxic application.
|
|
4
|
+
|
|
5
|
+
Import from `@fraxic/ui` by package name:
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import {
|
|
9
|
+
Chart,
|
|
10
|
+
ChartDataPoint,
|
|
11
|
+
ChartType,
|
|
12
|
+
ModalBuilder,
|
|
13
|
+
NoticeType,
|
|
14
|
+
SelectOption,
|
|
15
|
+
TextInputBuilder,
|
|
16
|
+
dashboard,
|
|
17
|
+
onButtonInteraction,
|
|
18
|
+
onModalInteraction,
|
|
19
|
+
} from "@fraxic/ui";
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Dashboard Pages
|
|
23
|
+
|
|
24
|
+
Register one `dashboard` handler. Fraxic calls it whenever the dashboard page renders.
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
dashboard((page) => {
|
|
28
|
+
page.section("Status", (section) => {
|
|
29
|
+
section.notice("Running", NoticeType.SUCCESS);
|
|
30
|
+
section.text("The application is healthy.");
|
|
31
|
+
section.progress("Queue", 3, 10);
|
|
32
|
+
section.button("Configure", "open-settings");
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`page.session` is a per-user session store. Use it for temporary UI state such as selected tabs, filters, and draft settings.
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
dashboard((page) => {
|
|
41
|
+
const tab = page.session.get<string>("tab") ?? "overview";
|
|
42
|
+
|
|
43
|
+
page.section("Controls", (section) => {
|
|
44
|
+
section.text(`Current tab: ${tab}`);
|
|
45
|
+
section.button("Overview", "tab:overview");
|
|
46
|
+
section.button("Settings", "tab:settings");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
onButtonInteraction((interaction) => {
|
|
51
|
+
if (interaction.customId.startsWith("tab:")) {
|
|
52
|
+
interaction.session.set("tab", interaction.customId.slice("tab:".length));
|
|
53
|
+
interaction.reply("Updated.");
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Layout
|
|
59
|
+
|
|
60
|
+
Every page contains sections. Sections and containers can contain text, notices, progress bars, buttons, charts, tables, separators, and nested containers.
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
dashboard((page) => {
|
|
64
|
+
page.section("Overview", (section) => {
|
|
65
|
+
section.horizontal((row) => {
|
|
66
|
+
row.card((card) => {
|
|
67
|
+
card.text("Processed");
|
|
68
|
+
card.text("128");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
row.card((card) => {
|
|
72
|
+
card.text("Errors");
|
|
73
|
+
card.notice("0", NoticeType.SUCCESS);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
section.separator();
|
|
78
|
+
|
|
79
|
+
section.vertical((column) => {
|
|
80
|
+
column.text("Recent activity");
|
|
81
|
+
column.button("Refresh", "refresh");
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Container methods:
|
|
88
|
+
|
|
89
|
+
- `horizontal(builder)` creates a horizontal child container.
|
|
90
|
+
- `vertical(builder)` creates a vertical child container.
|
|
91
|
+
- `card(builder)` creates a card child container.
|
|
92
|
+
- `table(builder)` adds a table.
|
|
93
|
+
- `text(content)` adds text.
|
|
94
|
+
- `notice(message, type)` adds an info, warning, error, or success notice.
|
|
95
|
+
- `progress(label, value, max)` adds a progress bar.
|
|
96
|
+
- `separator()` adds a visual separator.
|
|
97
|
+
- `button(label, customId)` adds a button.
|
|
98
|
+
- `chart(chart)` adds a chart.
|
|
99
|
+
|
|
100
|
+
## Tables
|
|
101
|
+
|
|
102
|
+
Tables have optional header rows and body rows. Each row must have the same number of cells.
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
dashboard((page) => {
|
|
106
|
+
page.section("Jobs", (section) => {
|
|
107
|
+
section.table((table) => {
|
|
108
|
+
table.header((row) => {
|
|
109
|
+
row.cell((cell) => cell.text("Name"));
|
|
110
|
+
row.cell((cell) => cell.text("Status"));
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
table.row((row) => {
|
|
114
|
+
row.cell((cell) => cell.text("Sync"));
|
|
115
|
+
row.cell((cell) => cell.notice("Done", NoticeType.SUCCESS));
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Charts
|
|
123
|
+
|
|
124
|
+
Use `Chart`, `ChartType`, and `ChartDataPoint`.
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
dashboard((page) => {
|
|
128
|
+
page.section("Usage", (section) => {
|
|
129
|
+
section.chart(
|
|
130
|
+
new Chart(ChartType.LINE)
|
|
131
|
+
.label("Requests")
|
|
132
|
+
.data([
|
|
133
|
+
new ChartDataPoint("09:00", 12),
|
|
134
|
+
new ChartDataPoint("10:00", 19),
|
|
135
|
+
new ChartDataPoint("11:00", 17),
|
|
136
|
+
])
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Chart types:
|
|
143
|
+
|
|
144
|
+
- `ChartType.LINE`
|
|
145
|
+
- `ChartType.BAR`
|
|
146
|
+
- `ChartType.PIE`
|
|
147
|
+
- `ChartType.AREA`
|
|
148
|
+
|
|
149
|
+
## Buttons
|
|
150
|
+
|
|
151
|
+
Register `onButtonInteraction` handlers for buttons.
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
onButtonInteraction((interaction) => {
|
|
155
|
+
if (interaction.customId === "refresh") {
|
|
156
|
+
interaction.reply("Refreshed.");
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Button interaction fields and methods:
|
|
162
|
+
|
|
163
|
+
- `customId` is the button id.
|
|
164
|
+
- `session` is the current user's `Session`.
|
|
165
|
+
- `reply(message)` responds with a message.
|
|
166
|
+
- `showModal(modal)` opens a modal.
|
|
167
|
+
|
|
168
|
+
Each interaction can be replied to once.
|
|
169
|
+
|
|
170
|
+
## Modals
|
|
171
|
+
|
|
172
|
+
Use `ModalBuilder` to show forms from button interactions.
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
onButtonInteraction((interaction) => {
|
|
176
|
+
if (interaction.customId !== "open-settings") return;
|
|
177
|
+
|
|
178
|
+
interaction.showModal(
|
|
179
|
+
new ModalBuilder("settings", "Settings")
|
|
180
|
+
.addTextInput(
|
|
181
|
+
"Webhook URL",
|
|
182
|
+
new TextInputBuilder("webhookUrl")
|
|
183
|
+
.setPlaceholder("https://example.com/webhook")
|
|
184
|
+
.setRequired(true),
|
|
185
|
+
"Stored by the application."
|
|
186
|
+
)
|
|
187
|
+
.addColorInput("Accent color", new ColorInputBuilder("accent").setDefault("#3366FF"))
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
onModalInteraction((interaction) => {
|
|
192
|
+
if (interaction.customId !== "settings") return;
|
|
193
|
+
|
|
194
|
+
const webhookUrl = interaction.getTextInput("webhookUrl");
|
|
195
|
+
const accent = interaction.getColorInput("accent");
|
|
196
|
+
|
|
197
|
+
if (webhookUrl === null || !webhookUrl.startsWith("https://")) {
|
|
198
|
+
interaction.rejectInputs({ webhookUrl: "Enter an HTTPS URL." });
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
interaction.session.set("settings", { webhookUrl, accent });
|
|
203
|
+
interaction.reply("Settings saved.");
|
|
204
|
+
});
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
`ModalBuilder` methods:
|
|
208
|
+
|
|
209
|
+
- `new ModalBuilder(customId, title)` creates a modal.
|
|
210
|
+
- `addTextInput(label, input, description?)`
|
|
211
|
+
- `addSelectInput(label, input, description?)`
|
|
212
|
+
- `addFileInput(label, input, description?)`
|
|
213
|
+
- `addColorInput(label, input, description?)`
|
|
214
|
+
- `addDateInput(label, input, description?)`
|
|
215
|
+
- `addParagraph(content, label?)`
|
|
216
|
+
|
|
217
|
+
Modal interactions:
|
|
218
|
+
|
|
219
|
+
- `getTextInput(customId)` returns `string | null`.
|
|
220
|
+
- `getSelectInput(customId)` returns `string[]`.
|
|
221
|
+
- `getFileInput(customId)` returns `Blob[]`.
|
|
222
|
+
- `getDateInput(customId)` returns `Date | null`.
|
|
223
|
+
- `getColorInput(customId)` returns `string | null`.
|
|
224
|
+
- `reply(message)` closes the interaction with a success message.
|
|
225
|
+
- `reject(message)` rejects the whole modal with a message.
|
|
226
|
+
- `rejectInputs({ [customId]: message })` rejects specific inputs and keeps the modal open.
|
|
227
|
+
|
|
228
|
+
## Inputs
|
|
229
|
+
|
|
230
|
+
Text input:
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
new TextInputBuilder("name")
|
|
234
|
+
.setPlaceholder("Display name")
|
|
235
|
+
.setDefault("My app")
|
|
236
|
+
.setRequired(true)
|
|
237
|
+
.setMinLength(2)
|
|
238
|
+
.setMaxLength(80);
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Use `.multiline()` for a multi-line text input.
|
|
242
|
+
|
|
243
|
+
Select input:
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
new SelectInputBuilder("mode")
|
|
247
|
+
.setPlaceholder("Choose mode")
|
|
248
|
+
.addOption(new SelectOption("Fast", "fast"))
|
|
249
|
+
.addOption(new SelectOption("Careful", "careful").setSelected(true))
|
|
250
|
+
.setRequired(true)
|
|
251
|
+
.setMinValues(1)
|
|
252
|
+
.setMaxValues(1);
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Searchable select input:
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
import { SelectInputBuilder, SelectOption, onSelectMenuSearch } from "@fraxic/ui";
|
|
259
|
+
|
|
260
|
+
new SelectInputBuilder("user")
|
|
261
|
+
.setPlaceholder("Search users")
|
|
262
|
+
.setSearchable(true);
|
|
263
|
+
|
|
264
|
+
onSelectMenuSearch((search) => {
|
|
265
|
+
if (search.customId !== "user") return;
|
|
266
|
+
|
|
267
|
+
const options = users
|
|
268
|
+
.filter((user) => user.name.toLowerCase().includes(search.query.toLowerCase()))
|
|
269
|
+
.slice(0, 25)
|
|
270
|
+
.map((user) => new SelectOption(user.name, user.id));
|
|
271
|
+
|
|
272
|
+
search.respond(options);
|
|
273
|
+
});
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
File input:
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
new FileInputBuilder("attachments")
|
|
280
|
+
.setRequired(false)
|
|
281
|
+
.setMinFiles(0)
|
|
282
|
+
.setMaxFiles(3);
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Date input:
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
new DateInputBuilder("runDate")
|
|
289
|
+
.setRequired(true)
|
|
290
|
+
.setDefault("2026-05-26");
|
|
291
|
+
|
|
292
|
+
new DateInputBuilder("runAt")
|
|
293
|
+
.includeTime()
|
|
294
|
+
.setDefault(new Date());
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Color input:
|
|
298
|
+
|
|
299
|
+
```ts
|
|
300
|
+
new ColorInputBuilder("accent")
|
|
301
|
+
.setRequired(false)
|
|
302
|
+
.setDefault("#3366FF");
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## Session
|
|
306
|
+
|
|
307
|
+
`Session` stores temporary per-user dashboard state.
|
|
308
|
+
|
|
309
|
+
```ts
|
|
310
|
+
const count = interaction.session.get<number>("count") ?? 0;
|
|
311
|
+
interaction.session.set("count", count + 1);
|
|
312
|
+
interaction.session.update<number>("count", (current) => (current ?? 0) + 1);
|
|
313
|
+
interaction.session.delete("count");
|
|
314
|
+
interaction.session.clear();
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Session methods:
|
|
318
|
+
|
|
319
|
+
- `get<T>(key)` returns `T | null`.
|
|
320
|
+
- `set<T>(key, value)` stores a value; `null` removes the key.
|
|
321
|
+
- `update<T>(key, updater)` updates a value.
|
|
322
|
+
- `delete(key)` removes a value and returns whether it existed.
|
|
323
|
+
- `list()` returns stored keys.
|
|
324
|
+
- `clear()` removes all session values.
|
|
325
|
+
|
|
326
|
+
Use application files or a database such as SQLite for durable state. Session values are for UI state only.
|
|
327
|
+
|
|
328
|
+
## Validation And Limits
|
|
329
|
+
|
|
330
|
+
Inputs validate before `onModalInteraction` runs. If validation fails, the modal is shown again with input errors.
|
|
331
|
+
|
|
332
|
+
Important limits:
|
|
333
|
+
|
|
334
|
+
- Text values are limited to 5000 characters.
|
|
335
|
+
- `customId` is limited to 100 characters.
|
|
336
|
+
- A page can contain up to 200 components.
|
|
337
|
+
- UI nesting depth is limited to 8.
|
|
338
|
+
- A table can contain up to 100 rows and 20 cells per row.
|
|
339
|
+
- A chart can contain up to 200 points.
|
|
340
|
+
- A modal can contain up to 10 components.
|
|
341
|
+
- A select input can contain up to 100 options.
|
|
342
|
+
- A file input accepts files up to 5 MiB each.
|
|
343
|
+
|
|
344
|
+
Use stable `customId` values. Prefix ids when there are multiple features, for example `settings:save` or `job:retry:123`.
|
package/index.d.ts
CHANGED
|
@@ -164,4 +164,3 @@ export declare function dashboard(handler: (page: Page) => void): void;
|
|
|
164
164
|
export declare function onButtonInteraction(handler: (interaction: ButtonInteraction) => void): void;
|
|
165
165
|
export declare function onModalInteraction(handler: (interaction: ModalInteraction) => void): void;
|
|
166
166
|
export declare function onSelectMenuSearch(handler: (search: SelectMenuSearch) => void): void;
|
|
167
|
-
export declare function __fraxicHandleUiRequest(payload: unknown): unknown;
|
package/index.js
CHANGED
|
@@ -18,6 +18,8 @@ const modalHandlers = [];
|
|
|
18
18
|
const selectMenuSearchHandlers = [];
|
|
19
19
|
const sessions = new Map();
|
|
20
20
|
const modalSchemas = new Map();
|
|
21
|
+
let ipcRegistered = false;
|
|
22
|
+
let ipcRegistrationQueued = false;
|
|
21
23
|
|
|
22
24
|
function requireText(name, value) {
|
|
23
25
|
if (typeof value !== "string") throw new Error(`${name} must be a string`);
|
|
@@ -894,21 +896,25 @@ export class SelectMenuSearch {
|
|
|
894
896
|
|
|
895
897
|
export function dashboard(handler) {
|
|
896
898
|
dashboardHandler = handler;
|
|
899
|
+
ensureIpcRegistered();
|
|
897
900
|
}
|
|
898
901
|
|
|
899
902
|
export function onButtonInteraction(handler) {
|
|
900
903
|
buttonHandlers.push(handler);
|
|
904
|
+
ensureIpcRegistered();
|
|
901
905
|
}
|
|
902
906
|
|
|
903
907
|
export function onModalInteraction(handler) {
|
|
904
908
|
modalHandlers.push(handler);
|
|
909
|
+
ensureIpcRegistered();
|
|
905
910
|
}
|
|
906
911
|
|
|
907
912
|
export function onSelectMenuSearch(handler) {
|
|
908
913
|
selectMenuSearchHandlers.push(handler);
|
|
914
|
+
ensureIpcRegistered();
|
|
909
915
|
}
|
|
910
916
|
|
|
911
|
-
|
|
917
|
+
function handleUiRequest(payload) {
|
|
912
918
|
const request = payload;
|
|
913
919
|
if (request.type === "render") return render(request);
|
|
914
920
|
if (request.type === "button") return handleButton(request);
|
|
@@ -984,8 +990,19 @@ function noResponse() {
|
|
|
984
990
|
return { ok: false, reason: "noResponse" };
|
|
985
991
|
}
|
|
986
992
|
|
|
993
|
+
function ensureIpcRegistered() {
|
|
994
|
+
if (ipcRegistered || ipcRegistrationQueued) return;
|
|
995
|
+
ipcRegistrationQueued = true;
|
|
996
|
+
queueMicrotask(() => {
|
|
997
|
+
ipcRegistrationQueued = false;
|
|
998
|
+
if (ipcRegistered) return;
|
|
999
|
+
const listen = globalThis.__fraxic?.ipc?.listen;
|
|
1000
|
+
if (typeof listen !== "function") return;
|
|
1001
|
+
listen("ui", handleUiRequest);
|
|
1002
|
+
ipcRegistered = true;
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
|
|
987
1006
|
function ensureModalHasComponents(modal) {
|
|
988
1007
|
if (modal.toMap().components.length === 0) throw new Error("modal must have at least 1 component");
|
|
989
1008
|
}
|
|
990
|
-
|
|
991
|
-
globalThis.__fraxic?.ipc?.listen?.("ui", __fraxicHandleUiRequest);
|
package/package.json
CHANGED