runeforge 0.0.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.
Files changed (119) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +551 -0
  3. package/dist/components/Avatar.svelte +31 -0
  4. package/dist/components/Avatar.svelte.d.ts +10 -0
  5. package/dist/components/IconRenderer.svelte +22 -0
  6. package/dist/components/IconRenderer.svelte.d.ts +8 -0
  7. package/dist/components/Modal.svelte +47 -0
  8. package/dist/components/Modal.svelte.d.ts +9 -0
  9. package/dist/components/common/Header.svelte +30 -0
  10. package/dist/components/common/Header.svelte.d.ts +11 -0
  11. package/dist/components/crud/Field.svelte +159 -0
  12. package/dist/components/crud/Field.svelte.d.ts +30 -0
  13. package/dist/components/crud/GenericCRUD.svelte +236 -0
  14. package/dist/components/crud/GenericCRUD.svelte.d.ts +44 -0
  15. package/dist/components/crud/columns/Avatar.svelte +15 -0
  16. package/dist/components/crud/columns/Avatar.svelte.d.ts +8 -0
  17. package/dist/components/crud/columns/Icon.svelte +8 -0
  18. package/dist/components/crud/columns/Icon.svelte.d.ts +4 -0
  19. package/dist/components/crud/utils/constants.d.ts +1 -0
  20. package/dist/components/crud/utils/constants.js +6 -0
  21. package/dist/components/crud/utils/formatters.d.ts +6 -0
  22. package/dist/components/crud/utils/formatters.js +42 -0
  23. package/dist/components/crud/utils/misc.d.ts +5 -0
  24. package/dist/components/crud/utils/misc.js +8 -0
  25. package/dist/components/crud/utils/resolution.d.ts +4 -0
  26. package/dist/components/crud/utils/resolution.js +22 -0
  27. package/dist/components/crud/views/Create.svelte +147 -0
  28. package/dist/components/crud/views/Create.svelte.d.ts +34 -0
  29. package/dist/components/crud/views/List.svelte +170 -0
  30. package/dist/components/crud/views/List.svelte.d.ts +41 -0
  31. package/dist/components/crud/views/Read.svelte +85 -0
  32. package/dist/components/crud/views/Read.svelte.d.ts +33 -0
  33. package/dist/components/crud/views/Update.svelte +148 -0
  34. package/dist/components/crud/views/Update.svelte.d.ts +35 -0
  35. package/dist/components/form/Button.svelte +27 -0
  36. package/dist/components/form/Button.svelte.d.ts +10 -0
  37. package/dist/components/form/Label.svelte +37 -0
  38. package/dist/components/form/Label.svelte.d.ts +14 -0
  39. package/dist/components/form/PasswordInput.svelte +61 -0
  40. package/dist/components/form/PasswordInput.svelte.d.ts +13 -0
  41. package/dist/components/form/Required.svelte +1 -0
  42. package/dist/components/form/Required.svelte.d.ts +26 -0
  43. package/dist/components/form/Select.svelte +114 -0
  44. package/dist/components/form/Select.svelte.d.ts +14 -0
  45. package/dist/components/navigation/Breadcrumbs.svelte +51 -0
  46. package/dist/components/navigation/Breadcrumbs.svelte.d.ts +9 -0
  47. package/dist/components/table/ColumnFilter.svelte +140 -0
  48. package/dist/components/table/ColumnFilter.svelte.d.ts +33 -0
  49. package/dist/components/table/PaginatedTable.svelte +106 -0
  50. package/dist/components/table/PaginatedTable.svelte.d.ts +35 -0
  51. package/dist/components/table/Paginator.svelte +62 -0
  52. package/dist/components/table/Paginator.svelte.d.ts +10 -0
  53. package/dist/components/table/SortHeader.svelte +43 -0
  54. package/dist/components/table/SortHeader.svelte.d.ts +10 -0
  55. package/dist/components/table/TableBody.svelte +70 -0
  56. package/dist/components/table/TableBody.svelte.d.ts +36 -0
  57. package/dist/components/table/TableHeader.svelte +83 -0
  58. package/dist/components/table/TableHeader.svelte.d.ts +39 -0
  59. package/dist/components/table/state.svelte.d.ts +30 -0
  60. package/dist/components/table/state.svelte.js +109 -0
  61. package/dist/components/table/utils.d.ts +7 -0
  62. package/dist/components/table/utils.js +44 -0
  63. package/dist/i18n/context.d.ts +3 -0
  64. package/dist/i18n/context.js +10 -0
  65. package/dist/i18n/en.d.ts +2 -0
  66. package/dist/i18n/en.js +24 -0
  67. package/dist/i18n/es.d.ts +2 -0
  68. package/dist/i18n/es.js +24 -0
  69. package/dist/i18n/types.d.ts +24 -0
  70. package/dist/i18n/types.js +1 -0
  71. package/dist/icons/bootstrapIconSet.d.ts +1 -0
  72. package/dist/icons/bootstrapIconSet.js +1 -0
  73. package/dist/icons/context.d.ts +3 -0
  74. package/dist/icons/context.js +9 -0
  75. package/dist/icons/defaultIconSet.d.ts +1 -0
  76. package/dist/icons/defaultIconSet.js +1 -0
  77. package/dist/icons/defaults/Create.svelte +6 -0
  78. package/dist/icons/defaults/Create.svelte.d.ts +7 -0
  79. package/dist/icons/defaults/Delete.svelte +6 -0
  80. package/dist/icons/defaults/Delete.svelte.d.ts +7 -0
  81. package/dist/icons/defaults/Edit.svelte +7 -0
  82. package/dist/icons/defaults/Edit.svelte.d.ts +7 -0
  83. package/dist/icons/defaults/Filter.svelte +6 -0
  84. package/dist/icons/defaults/Filter.svelte.d.ts +7 -0
  85. package/dist/icons/defaults/FilterActive.svelte +6 -0
  86. package/dist/icons/defaults/FilterActive.svelte.d.ts +7 -0
  87. package/dist/icons/defaults/Folder.svelte +6 -0
  88. package/dist/icons/defaults/Folder.svelte.d.ts +7 -0
  89. package/dist/icons/defaults/Home.svelte +6 -0
  90. package/dist/icons/defaults/Home.svelte.d.ts +7 -0
  91. package/dist/icons/defaults/PasswordHide.svelte +9 -0
  92. package/dist/icons/defaults/PasswordHide.svelte.d.ts +7 -0
  93. package/dist/icons/defaults/PasswordShow.svelte +7 -0
  94. package/dist/icons/defaults/PasswordShow.svelte.d.ts +7 -0
  95. package/dist/icons/defaults/SortAsc.svelte +6 -0
  96. package/dist/icons/defaults/SortAsc.svelte.d.ts +7 -0
  97. package/dist/icons/defaults/SortDesc.svelte +6 -0
  98. package/dist/icons/defaults/SortDesc.svelte.d.ts +7 -0
  99. package/dist/icons/defaults/SortNone.svelte +6 -0
  100. package/dist/icons/defaults/SortNone.svelte.d.ts +7 -0
  101. package/dist/icons/defaults/View.svelte +7 -0
  102. package/dist/icons/defaults/View.svelte.d.ts +7 -0
  103. package/dist/icons/sets/bootstrap.d.ts +2 -0
  104. package/dist/icons/sets/bootstrap.js +29 -0
  105. package/dist/icons/sets/default.d.ts +2 -0
  106. package/dist/icons/sets/default.js +28 -0
  107. package/dist/icons/types.d.ts +26 -0
  108. package/dist/icons/types.js +1 -0
  109. package/dist/index.d.ts +42 -0
  110. package/dist/index.js +41 -0
  111. package/dist/types/attribute.d.ts +38 -0
  112. package/dist/types/attribute.js +11 -0
  113. package/dist/types/breadcrumb.d.ts +7 -0
  114. package/dist/types/breadcrumb.js +1 -0
  115. package/dist/types/crud.d.ts +48 -0
  116. package/dist/types/crud.js +1 -0
  117. package/dist/types/table.d.ts +16 -0
  118. package/dist/types/table.js +1 -0
  119. package/package.json +82 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ezequiel Puerta
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,551 @@
1
+ <div align="center">
2
+
3
+ <img src="docs/logo.png" alt="Runeforge logo" width="300" />
4
+
5
+ # Runeforge
6
+
7
+ A SvelteKit toolkit that forges forms, tables, actions, and CRUD workflows from reusable definitions.
8
+
9
+ <img src="docs/crud-list.png" alt="CRUD list view" />
10
+
11
+ </div>
12
+
13
+ ---
14
+
15
+ ## Introduction
16
+
17
+ Runeforge provides a set of composable, metadata-driven components for building data-heavy interfaces in SvelteKit. It handles the repetitive parts of CRUD UIs — listing records, creating and editing forms, sorting and filtering tables — through a declarative API built on top of [DaisyUI](https://daisyui.com/) and [Tailwind CSS](https://tailwindcss.com/).
18
+
19
+ ---
20
+
21
+ ## Requirements
22
+
23
+ - SvelteKit 2+
24
+ - Svelte 5 (runes mode)
25
+ - Tailwind CSS 4
26
+ - DaisyUI 5
27
+
28
+ ---
29
+
30
+ ## Key Features
31
+
32
+ - **GenericCRUD** — a single orchestrator component that wires together list, create, read, and update views from field and column definitions.
33
+ - **PaginatedTable** — a full-featured table with sorting, filtering, pagination, and row selection.
34
+ - **Field system** — declarative field definitions that drive both form rendering and display, supporting text, email, password, number, boolean, textarea, file, select, and datetime types.
35
+ - **Pluggable icon system** — swap the default icon set or use the included Bootstrap Icons alternative via `setIconSet`.
36
+ - **Standalone components** — table, form, and navigation components can be used independently without the full CRUD orchestrator.
37
+
38
+ ---
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ pnpm add runeforge
44
+ ```
45
+
46
+ ---
47
+
48
+ ## Basic Usage
49
+
50
+ ### 1. Define your interface and metadata
51
+
52
+ ```ts
53
+ // interface.ts
54
+ import { AttributeType, type InterfaceMetadata } from 'runeforge';
55
+ import { formatBoolean, formatDatetime } from 'runeforge';
56
+
57
+ export interface IArticle {
58
+ _id: string;
59
+ title: string;
60
+ published: boolean;
61
+ createdAt: Date;
62
+ }
63
+
64
+ export const articleMeta = {
65
+ title: {
66
+ label: 'Title',
67
+ type: AttributeType.text,
68
+ placeholder: 'My article',
69
+ required: true,
70
+ },
71
+ published: {
72
+ label: 'Published',
73
+ type: AttributeType.boolean,
74
+ formatter: formatBoolean,
75
+ default: false,
76
+ required: true,
77
+ },
78
+ createdAt: {
79
+ label: 'Created',
80
+ type: AttributeType.datetime,
81
+ formatter: formatDatetime(),
82
+ excludedFromCreate: true,
83
+ excludedFromUpdate: true
84
+ },
85
+ updatedAt: {
86
+ label: 'Updated',
87
+ type: AttributeType.datetime,
88
+ formatter: formatDatetime(),
89
+ excludedFromCreate: true,
90
+ excludedFromUpdate: true
91
+ },
92
+ } satisfies InterfaceMetadata<IArticle>;
93
+ ```
94
+
95
+ Each metadata entry drives both the table column and the form field for that attribute. You can use `excludedFromList`, `excludedFromCreate`, `excludedFromRead`, or `excludedFromUpdate` to hide a field from specific views.
96
+
97
+ ### 2. Create the model
98
+
99
+ ```ts
100
+ // model.ts
101
+ import crypto from 'node:crypto';
102
+ import mongoose from 'mongoose';
103
+ import type { IArticle } from './interface';
104
+
105
+ const schema = new mongoose.Schema<IArticle>(
106
+ {
107
+ _id: { type: String, default: () => crypto.randomUUID() },
108
+ title: { type: String, required: true, trim: true },
109
+ published: { type: Boolean, required: true, default: false },
110
+ },
111
+ { timestamps: true }
112
+ );
113
+
114
+ export const Article = mongoose.models.Article ?? mongoose.model<IArticle>('Article', schema);
115
+ ```
116
+
117
+ ### 3. Set up the server
118
+
119
+ ```ts
120
+ // +page.server.ts
121
+ import { fail, error } from '@sveltejs/kit';
122
+ import { Article } from '$lib/server/articles/model';
123
+ import type { Actions, PageServerLoad } from './$types';
124
+ import type { IArticle } from './interface';
125
+
126
+ export const load: PageServerLoad = async ({ url }) => {
127
+ const id = url.searchParams.get('id');
128
+ if (id) {
129
+ const article = await Article.findById(id).lean<IArticle>();
130
+ if (!article) error(404, 'Not found');
131
+ return { article };
132
+ }
133
+ const articles = await Article.find({}).sort({ createdAt: -1 }).lean<IArticle[]>();
134
+ return { articles };
135
+ };
136
+
137
+ export const actions: Actions = {
138
+ create: async ({ request }) => {
139
+ const data = await request.formData();
140
+ const title = String(data.get('title') ?? '').trim();
141
+ if (!title) return fail(400, { error: 'Title is required' });
142
+ await Article.create({ title, published: data.has('published') });
143
+ return { success: true };
144
+ },
145
+
146
+ update: async ({ request }) => {
147
+ const data = await request.formData();
148
+ const id = String(data.get('id') ?? '').trim();
149
+ if (!id) return fail(400, { error: 'ID is required' });
150
+ await Article.findByIdAndUpdate(id, {
151
+ title: String(data.get('title') ?? '').trim(),
152
+ published: data.has('published'),
153
+ });
154
+ return { success: true };
155
+ },
156
+
157
+ delete: async ({ request }) => {
158
+ const data = await request.formData();
159
+ const id = String(data.get('id') ?? '').trim();
160
+ if (!id) return fail(400, { error: 'ID is required' });
161
+ await Article.findByIdAndDelete(id);
162
+ return { success: true };
163
+ },
164
+ };
165
+ ```
166
+
167
+ The `load` function returns a single record when `?id=` is present (used by the read/edit views), or the full list otherwise.
168
+
169
+ ### 4. Add the page component
170
+
171
+ ```html
172
+ <!-- +page.svelte -->
173
+ <script lang="ts">
174
+ import { GenericCRUD } from 'runeforge';
175
+ import { articleMeta as meta } from './interface';
176
+
177
+ let { data, form } = $props();
178
+ </script>
179
+
180
+ <GenericCRUD
181
+ labelOne="Article"
182
+ labelMany="Articles"
183
+ {data}
184
+ {form}
185
+ {meta}
186
+ dataKey="articles"
187
+ creation={{ endpoint: '?/create' }}
188
+ read={{ endpoint: '?/read' }}
189
+ update={{ endpoint: '?/update' }}
190
+ deletion={{ endpoint: '?/delete' }}
191
+ />
192
+ ```
193
+
194
+ `dataKey` must match the key returned by the load function for the list. Each `endpoint` maps to a SvelteKit form action on the same page.
195
+
196
+ ---
197
+
198
+ ## Components
199
+
200
+ ### GenericCRUD
201
+
202
+ The main CRUD orchestrator. It manages navigation between List, Create, Read, and Update views using URL search params (`?view=create`, `?id=xxx`, `?view=edit`).
203
+
204
+ Key props:
205
+
206
+ - `data` / `dataKey` — the record array and its primary key field
207
+ - `labelOne` / `labelMany` — singular and plural names for the entity
208
+ - `columns` — `ColumnDefinition[]` for the table view
209
+ - `fields` — `FieldDefinition[]` for form views
210
+ - `creation`, `update`, `read`, `deletion` — `ActionConfiguration` objects that define handlers and permissions for each operation
211
+
212
+ ### PaginatedTable
213
+
214
+ A standalone table component with built-in sort, filter, and pagination.
215
+
216
+ ```svelte
217
+ <script>
218
+ import { PaginatedTable } from 'runeforge';
219
+ </script>
220
+
221
+ <PaginatedTable {data} {columns} />
222
+ ```
223
+
224
+ Sort and filter state can be managed externally via the exported `SortState` and `FilterState` classes.
225
+
226
+ ### Form Components
227
+
228
+ Individual form primitives styled with DaisyUI:
229
+
230
+ - `Button` — styled action button
231
+ - `Label` — form label with optional required marker
232
+ - `Select` — dropdown with option group support
233
+ - `PasswordInput` — password field with show/hide toggle
234
+
235
+ ### Shared Components
236
+
237
+ - `Avatar` — user avatar display
238
+ - `Modal` — DaisyUI modal wrapper
239
+ - `Breadcrumbs` — navigation breadcrumb trail
240
+ - `IconRenderer` — renders icons from the active icon set
241
+
242
+ ---
243
+
244
+ ## Formatters
245
+
246
+ Formatters are functions you attach to a metadata field to control how its value is displayed in the table and read view. They follow a curried signature: `(data) => (value) => string`, where `data` is the full page data object (useful for resolving related records).
247
+
248
+ ### `formatBoolean`
249
+
250
+ Converts a boolean to a readable label.
251
+
252
+ > [!INFO]
253
+ > Defaults to `Sí` / `No` because this was created at Argentina papá! 🇦🇷.
254
+
255
+ ```ts
256
+ import { formatBoolean } from 'runeforge';
257
+
258
+ isActive: {
259
+ label: 'Active',
260
+ type: AttributeType.boolean,
261
+ formatter: formatBoolean(),
262
+ // or with custom labels:
263
+ formatter: formatBoolean('Enabled', 'Disabled'),
264
+ },
265
+ ```
266
+
267
+ ### `formatDatetime`
268
+
269
+ Formats a `Date` value using the tokens `dd`, `mm`, `YYYY`, `HH`, `MM`, `ss`.
270
+
271
+ > [!INFO]
272
+ > Defaults to `'dd/mm/YYYY HH:MM'`.
273
+
274
+ ```ts
275
+ import { formatDatetime } from 'runeforge';
276
+
277
+ createdAt: {
278
+ label: 'Created',
279
+ type: AttributeType.datetime,
280
+ formatter: formatDatetime(), // → "13/06/2026 09:45"
281
+ },
282
+
283
+ publishedAt: {
284
+ label: 'Published',
285
+ type: AttributeType.datetime,
286
+ formatter: formatDatetime('dd/mm/YYYY'), // → "13/06/2026"
287
+ },
288
+ ```
289
+
290
+ ### `formatTruncateTextUpTo`
291
+
292
+ Truncates long text to a maximum character count, appending `…`.
293
+
294
+ ```ts
295
+ import { formatTruncateTextUpTo } from 'runeforge';
296
+
297
+ description: {
298
+ label: 'Description',
299
+ type: AttributeType.textarea,
300
+ formatter: formatTruncateTextUpTo(80),
301
+ },
302
+ ```
303
+
304
+ ### `formatInstance`
305
+
306
+ Resolves a foreign-key ID to a linked label. Receives the related records and the URL path for the detail view, and renders an anchor tag pointing to that record.
307
+
308
+ ```ts
309
+ import { formatInstance } from 'runeforge';
310
+ import type { ICategory } from '$lib/server/categories/interface';
311
+
312
+ categoryId: {
313
+ label: 'Category',
314
+ type: AttributeType.select,
315
+ options: (data: { categories?: ICategory[] }) =>
316
+ (data.categories ?? []).map((c) => ({ value: c._id, label: c.name })),
317
+ formatter: (data: { categories?: ICategory[] }) =>
318
+ formatInstance<ICategory>('name', data.categories ?? [], '/admin/categories'),
319
+ },
320
+ ```
321
+
322
+ ---
323
+
324
+ ## Custom Cell Components
325
+
326
+ Instead of a `formatter`, a metadata field can declare a `component` — a Svelte component that renders the cell in both the table list and the read view. This is useful when you need to render something visual, like an avatar image or an icon, rather than plain text.
327
+
328
+ A cell component receives two props defined by `CellProps<T, V>`:
329
+
330
+ - `value` — the raw field value for that cell
331
+ - `row` — the full record object, useful when the rendering depends on other fields
332
+
333
+ ```ts
334
+ // CellProps interface (from runeforge)
335
+ interface CellProps<T extends object, V> {
336
+ value: V;
337
+ row: T;
338
+ }
339
+ ```
340
+
341
+ ### Example: avatar column
342
+
343
+ The following renders a user photo with a fallback to initials, using data from sibling fields on the row:
344
+
345
+ ```svelte
346
+ <!-- components/UserAvatar.svelte -->
347
+ <script lang="ts">
348
+ import { Avatar } from 'runeforge';
349
+ import type { CellProps } from 'runeforge';
350
+
351
+ type UserRow = { firstName?: string; lastName?: string; email?: string };
352
+
353
+ let { value, row }: CellProps<UserRow, string | null> = $props();
354
+
355
+ const initials = [row.firstName?.[0], row.lastName?.[0]].filter(Boolean).join('').toUpperCase();
356
+ </script>
357
+
358
+ <Avatar src={value} text={initials} alt={row.email ?? ''} />
359
+ ```
360
+
361
+ Register it in the metadata with `component`:
362
+
363
+ ```ts
364
+ // interface.ts
365
+ import UserAvatar from './components/UserAvatar.svelte';
366
+
367
+ export const userMeta = {
368
+ photo: {
369
+ label: 'Photo',
370
+ type: AttributeType.file,
371
+ component: UserAvatar,
372
+ sortable: false,
373
+ filterable: false,
374
+ },
375
+ // ...
376
+ } satisfies InterfaceMetadata<IUser>;
377
+ ```
378
+
379
+ ### Example: icon column
380
+
381
+ A simpler case — render a Bootstrap icon by name stored as a plain string:
382
+
383
+ ```svelte
384
+ <!-- components/IconCell.svelte -->
385
+ <script lang="ts">
386
+ import { IconRenderer } from 'runeforge';
387
+ import type { CellProps } from 'runeforge';
388
+
389
+ let { value }: CellProps<Record<string, unknown>, string> = $props();
390
+ </script>
391
+
392
+ <IconRenderer name={value} />
393
+ ```
394
+
395
+ ```ts
396
+ icon: {
397
+ label: 'Icon',
398
+ type: AttributeType.text,
399
+ component: IconCell,
400
+ },
401
+ ```
402
+
403
+ > [!TIP]
404
+ > Both `AvatarCell` and `IconCell` are included in the package and ready to use — you don't need to build them from scratch:
405
+ >
406
+ > ```ts
407
+ > import { AvatarCell, IconCell } from 'runeforge';
408
+ >
409
+ > photo: { label: 'Photo', type: AttributeType.file, component: AvatarCell },
410
+ > icon: { label: 'Icon', type: AttributeType.text, component: IconCell },
411
+ > ```
412
+
413
+ ---
414
+
415
+ ## Internationalization
416
+
417
+ All UI strings default to **Spanish** (Argentina). To switch to another language, call `setStrings` in your root layout with a full or partial `RuneforgeStrings` object. Values you omit fall back to the Spanish defaults.
418
+
419
+ ### Switch to English
420
+
421
+ ```svelte
422
+ <!-- +layout.svelte -->
423
+ <script>
424
+ import { setStrings, en } from 'runeforge';
425
+
426
+ setStrings(en);
427
+ </script>
428
+ ```
429
+
430
+ ### Override individual strings
431
+
432
+ ```svelte
433
+ <script>
434
+ import { setStrings } from 'runeforge';
435
+
436
+ setStrings({
437
+ create: 'New',
438
+ save: 'Confirm',
439
+ required: (field) => `${field} cannot be blank`,
440
+ });
441
+ </script>
442
+ ```
443
+
444
+ ### Full `RuneforgeStrings` reference
445
+
446
+ | Key | Type | Spanish default |
447
+ | --- | --- | --- |
448
+ | `showing` | `(start, end, total) => string` | `Mostrando 1–10 de 25` |
449
+ | `actions` | `string` | `Acciones` |
450
+ | `filter` | `string` | `Filtrar` |
451
+ | `filterColumn` | `(column) => string` | `Filtrar Nombre` |
452
+ | `filterPlaceholder` | `string` | `Filtrar…` |
453
+ | `clearFilter` | `string` | `Limpiar filtro` |
454
+ | `emptyValue` | `string` | `(vacío)` |
455
+ | `previous` | `string` | `Anterior` |
456
+ | `next` | `string` | `Siguiente` |
457
+ | `selectPlaceholder` | `string` | `Seleccioná una opción` |
458
+ | `selectSearch` | `string` | `Buscar...` |
459
+ | `selectNoResults` | `string` | `Sin resultados` |
460
+ | `view` | `string` | `Ver` |
461
+ | `edit` | `string` | `Editar` |
462
+ | `delete` | `string` | `Eliminar` |
463
+ | `create` | `string` | `Crear` |
464
+ | `save` | `string` | `Guardar` |
465
+ | `saveAndContinue` | `string` | `Guardar y continuar` |
466
+ | `cancel` | `string` | `Cancelar` |
467
+ | `back` | `string` | `Volver` |
468
+ | `required` | `(field) => string` | `Título es requerido` |
469
+ | `serverError` | `string` | `Error inesperado del servidor.` |
470
+
471
+ > [!INFO]
472
+ > Defaults to Spanish because this was built in Argentina! 🇦🇷
473
+
474
+ ### Bundled locales
475
+
476
+ | Import | Language |
477
+ | --- | --- |
478
+ | `es` | Spanish 🇦🇷 (default) |
479
+ | `en` | English 🇺🇸 |
480
+
481
+ ---
482
+
483
+ ## Icon System
484
+
485
+ Runeforge ships with a default icon set. To use Bootstrap Icons instead:
486
+
487
+ ```svelte
488
+ <script>
489
+ import { setIconSet } from 'runeforge';
490
+ import bootstrapIcons from 'runeforge/icons/bootstrap';
491
+
492
+ setIconSet(bootstrapIcons);
493
+ </script>
494
+ ```
495
+
496
+ You can also provide a fully custom icon set by passing an object that satisfies the icon set interface.
497
+
498
+ ---
499
+
500
+ ## Running Tests
501
+
502
+ ### Unit Tests
503
+
504
+ Unit tests cover utility functions (formatters, resolution helpers, misc utilities) and run with [Vitest](https://vitest.dev/).
505
+
506
+ ```bash
507
+ # Single run
508
+ pnpm test:unit
509
+
510
+ # Watch mode
511
+ pnpm test:unit:watch
512
+ ```
513
+
514
+ ### End-to-End Tests
515
+
516
+ E2E tests cover table interactions (pagination, sorting, filtering) and run with [Playwright](https://playwright.dev/). The dev server starts automatically when running locally.
517
+
518
+ ```bash
519
+ pnpm test:e2e
520
+ ```
521
+
522
+ ### Run All Tests
523
+
524
+ ```bash
525
+ pnpm test
526
+ ```
527
+
528
+ ---
529
+
530
+ ## Development
531
+
532
+ ```bash
533
+ # Start the dev server
534
+ pnpm dev
535
+
536
+ # Type-check
537
+ pnpm check
538
+
539
+ # Lint and format
540
+ pnpm lint
541
+ pnpm format
542
+
543
+ # Build the library
544
+ pnpm build
545
+ ```
546
+
547
+ ---
548
+
549
+ ## License
550
+
551
+ MIT
@@ -0,0 +1,31 @@
1
+ <script lang="ts">
2
+ let {
3
+ src = null,
4
+ text = '',
5
+ alt = '',
6
+ class: className = 'w-10 rounded-full',
7
+ textClass = '',
8
+ }: {
9
+ src?: string | null;
10
+ text?: string;
11
+ alt?: string;
12
+ class?: string;
13
+ textClass?: string;
14
+ } = $props();
15
+
16
+ const placeholder = $derived(text.trim().slice(0, 2).toUpperCase() || '?');
17
+ </script>
18
+
19
+ {#if src}
20
+ <div class="avatar">
21
+ <div class={className}>
22
+ <img {src} {alt} />
23
+ </div>
24
+ </div>
25
+ {:else}
26
+ <div class="avatar avatar-placeholder">
27
+ <div class="bg-neutral text-neutral-content {className}">
28
+ <span class={textClass}>{placeholder}</span>
29
+ </div>
30
+ </div>
31
+ {/if}
@@ -0,0 +1,10 @@
1
+ type $$ComponentProps = {
2
+ src?: string | null;
3
+ text?: string;
4
+ alt?: string;
5
+ class?: string;
6
+ textClass?: string;
7
+ };
8
+ declare const Avatar: import("svelte").Component<$$ComponentProps, {}, "">;
9
+ type Avatar = ReturnType<typeof Avatar>;
10
+ export default Avatar;
@@ -0,0 +1,22 @@
1
+ <script lang="ts">
2
+ import { getIconSet } from '../icons/context.js';
3
+
4
+ let {
5
+ name,
6
+ size = '1em',
7
+ class: className = '',
8
+ }: {
9
+ name: string;
10
+ size?: string | number;
11
+ class?: string;
12
+ } = $props();
13
+
14
+ const icons = $derived(getIconSet());
15
+ const IconComponent = $derived(icons?.renderByName ?? null);
16
+ </script>
17
+
18
+ {#if IconComponent}
19
+ <IconComponent {name} {size} class={className} />
20
+ {:else}
21
+ <span class={className} title={name}></span>
22
+ {/if}
@@ -0,0 +1,8 @@
1
+ type $$ComponentProps = {
2
+ name: string;
3
+ size?: string | number;
4
+ class?: string;
5
+ };
6
+ declare const IconRenderer: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type IconRenderer = ReturnType<typeof IconRenderer>;
8
+ export default IconRenderer;
@@ -0,0 +1,47 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import Button from './form/Button.svelte';
4
+
5
+ let {
6
+ title = '',
7
+ onClose,
8
+ children,
9
+ }: {
10
+ title?: string;
11
+ onClose?: () => void;
12
+ children: Snippet;
13
+ } = $props();
14
+ </script>
15
+
16
+ <div class="modal modal-open" role="dialog" aria-modal="true">
17
+ <div class="modal-box">
18
+ <div class="flex items-center justify-between gap-4">
19
+ <h3 class="text-lg font-bold">{title}</h3>
20
+ {#if onClose}
21
+ <Button
22
+ variant="ghost"
23
+ class="btn-circle"
24
+ aria-label="Cerrar"
25
+ onclick={onClose}
26
+ >
27
+
28
+ </Button>
29
+ {/if}
30
+ </div>
31
+
32
+ <div class="divider my-2"></div>
33
+
34
+ {@render children()}
35
+ </div>
36
+
37
+ {#if onClose}
38
+ <div
39
+ class="modal-backdrop"
40
+ aria-label="Cerrar"
41
+ role="button"
42
+ tabindex="0"
43
+ onclick={onClose}
44
+ onkeydown={(e) => e.key === 'Enter' && onClose?.()}
45
+ ></div>
46
+ {/if}
47
+ </div>
@@ -0,0 +1,9 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ title?: string;
4
+ onClose?: () => void;
5
+ children: Snippet;
6
+ };
7
+ declare const Modal: import("svelte").Component<$$ComponentProps, {}, "">;
8
+ type Modal = ReturnType<typeof Modal>;
9
+ export default Modal;