@fraxic/ui 0.3.0 → 0.3.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/README.md +344 -0
- 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/package.json
CHANGED