runeforge 0.0.7 → 0.0.9

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 CHANGED
@@ -23,6 +23,7 @@ A SvelteKit toolkit that forges forms, tables, actions, and CRUD workflows from
23
23
  - [Theming](#theming)
24
24
  - [Tailwind source scanning](#tailwind-source-scanning)
25
25
  - [CSS variables](#css-variables)
26
+ - [Configuration](#configuration)
26
27
  - [Basic Usage](#basic-usage)
27
28
  - [1. Define your interface and metadata](#1-define-your-interface-and-metadata)
28
29
  - [2. Create the model](#2-create-the-model)
@@ -136,6 +137,25 @@ Responsive overrides work too:
136
137
 
137
138
  ---
138
139
 
140
+ ## Configuration
141
+
142
+ Global settings are applied once in your root layout via `setConfig`. This avoids passing the same prop to every CRUD component.
143
+
144
+ ```svelte
145
+ <!-- +layout.svelte -->
146
+ <script>
147
+ import { setConfig } from 'runeforge';
148
+
149
+ setConfig({ homeHref: '/admin' });
150
+ </script>
151
+ ```
152
+
153
+ | Option | Default | Description |
154
+ | --- | --- | --- |
155
+ | `homeHref` | `'/'` | URL for the home crumb in every breadcrumb trail |
156
+
157
+ ---
158
+
139
159
  ## Basic Usage
140
160
 
141
161
  ### 1. Define your interface and metadata
@@ -284,6 +304,14 @@ The `load` function returns a single record when `?id=` is present (used by the
284
304
 
285
305
  `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.
286
306
 
307
+ If your records use a different identifier field than `_id` (e.g. a plain `id`), pass the `idKey` prop:
308
+
309
+ ```svelte
310
+ <GenericCRUD idKey="id" ... />
311
+ ```
312
+
313
+ This propagates to navigation URLs, form submissions, deletion calls, and the auto-excluded column list, so no other changes are needed on your end.
314
+
287
315
  ---
288
316
 
289
317
  ## Components
@@ -7,19 +7,17 @@
7
7
  title,
8
8
  breadcrumbs = [],
9
9
  buttons,
10
- admin = false,
11
10
  }: {
12
11
  title: string;
13
12
  breadcrumbs?: BreadcrumbItem[];
14
13
  buttons?: Snippet;
15
- admin?: boolean;
16
14
  } = $props();
17
15
  </script>
18
16
 
19
17
  <div class="header-root flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
20
18
  <div class="flex flex-col gap-1 flex-1">
21
19
  <h1>{title}</h1>
22
- <Breadcrumbs items={breadcrumbs} {admin} />
20
+ <Breadcrumbs items={breadcrumbs} />
23
21
  </div>
24
22
 
25
23
  {#if buttons}
@@ -4,7 +4,6 @@ type $$ComponentProps = {
4
4
  title: string;
5
5
  breadcrumbs?: BreadcrumbItem[];
6
6
  buttons?: Snippet;
7
- admin?: boolean;
8
7
  };
9
8
  declare const Header: import("svelte").Component<$$ComponentProps, {}, "">;
10
9
  type Header = ReturnType<typeof Header>;
@@ -18,6 +18,7 @@
18
18
  let {
19
19
  data = undefined as Record<string, unknown> | undefined,
20
20
  dataKey = undefined as string | undefined,
21
+ idKey = '_id',
21
22
  labelOne = '',
22
23
  labelMany = '',
23
24
  icon,
@@ -34,6 +35,7 @@
34
35
  }: {
35
36
  data?: Record<string, unknown>;
36
37
  dataKey?: string;
38
+ idKey?: string;
37
39
  labelOne?: string;
38
40
  labelMany?: string;
39
41
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -61,9 +63,11 @@
61
63
  const reading = $derived(idParam !== null && viewParam === null);
62
64
  const editing = $derived(idParam !== null && viewParam === 'edit');
63
65
 
66
+ const excluded = $derived(new Set([...AUTO_EXCLUDED, idKey]));
67
+
64
68
  const singleInstance = $derived<T | undefined>(
65
69
  (Object.values(page.data as Record<string, unknown>).find(
66
- (v) => v !== null && typeof v === 'object' && !Array.isArray(v) && '_id' in (v as object)
70
+ (v) => v !== null && typeof v === 'object' && !Array.isArray(v) && idKey in (v as object)
67
71
  ) as T | undefined)
68
72
  );
69
73
 
@@ -72,10 +76,10 @@
72
76
  async function navList() { await goto('?'); }
73
77
  async function navCreate() { await goto('?view=create'); }
74
78
  async function navRead(item: T) {
75
- await goto(`?id=${encodeURIComponent(String((item as Record<string, unknown>)._id ?? ''))}`);
79
+ await goto(`?id=${encodeURIComponent(String((item as Record<string, unknown>)[idKey] ?? ''))}`);
76
80
  }
77
81
  async function navEdit(item: T) {
78
- await goto(`?id=${encodeURIComponent(String((item as Record<string, unknown>)._id ?? ''))}&view=edit`);
82
+ await goto(`?id=${encodeURIComponent(String((item as Record<string, unknown>)[idKey] ?? ''))}&view=edit`);
79
83
  }
80
84
 
81
85
  const resolvedColumns: ColumnDefinition<T>[] = $derived(
@@ -93,7 +97,7 @@
93
97
  }))
94
98
  : entityData.length > 0
95
99
  ? (Object.keys(entityData[0]) as (keyof T & string)[])
96
- .filter((k) => k !== '_id')
100
+ .filter((k) => !excluded.has(k))
97
101
  .map((k) => ({ attribute: k, title: k }))
98
102
  : [])
99
103
  );
@@ -101,7 +105,7 @@
101
105
  const resolvedFields: FieldDefinition<T>[] = $derived(
102
106
  fields ?? (meta
103
107
  ? (Object.entries(meta) as [string, AttributeMetadata][])
104
- .filter(([k, m]) => !AUTO_EXCLUDED.has(k) && !m.excludedFromCreate)
108
+ .filter(([k, m]) => !excluded.has(k) && !m.excludedFromCreate)
105
109
  .map(([k, m]) => ({
106
110
  attribute: k as keyof T & string,
107
111
  title: m.label,
@@ -114,7 +118,7 @@
114
118
  }))
115
119
  : entityData.length > 0
116
120
  ? (Object.entries(entityData[0]) as [string, unknown][])
117
- .filter(([k]) => !AUTO_EXCLUDED.has(k))
121
+ .filter(([k]) => !excluded.has(k))
118
122
  .map(([k, v]) => ({ attribute: k as keyof T & string, type: inferType(k, v) }))
119
123
  : [])
120
124
  );
@@ -122,7 +126,7 @@
122
126
  const resolvedReadFields: FieldDefinition<T>[] = $derived(
123
127
  fields ?? (meta
124
128
  ? (Object.entries(meta) as [string, AttributeMetadata][])
125
- .filter(([k, m]) => !AUTO_EXCLUDED.has(k) && !m.excludedFromRead)
129
+ .filter(([k, m]) => !excluded.has(k) && !m.excludedFromRead)
126
130
  .map(([k, m]) => ({
127
131
  attribute: k as keyof T & string,
128
132
  title: m.label,
@@ -135,7 +139,7 @@
135
139
  }))
136
140
  : entityData.length > 0
137
141
  ? (Object.entries(entityData[0]) as [string, unknown][])
138
- .filter(([k]) => !AUTO_EXCLUDED.has(k))
142
+ .filter(([k]) => !excluded.has(k))
139
143
  .map(([k, v]) => ({ attribute: k as keyof T & string, type: inferType(k, v) }))
140
144
  : [])
141
145
  );
@@ -143,7 +147,7 @@
143
147
  const resolvedUpdateFields: FieldDefinition<T>[] = $derived(
144
148
  fields ?? (meta
145
149
  ? (Object.entries(meta) as [string, AttributeMetadata][])
146
- .filter(([k, m]) => !AUTO_EXCLUDED.has(k) && !m.excludedFromUpdate)
150
+ .filter(([k, m]) => !excluded.has(k) && !m.excludedFromUpdate)
147
151
  .map(([k, m]) => ({
148
152
  attribute: k as keyof T & string,
149
153
  title: m.label,
@@ -156,7 +160,7 @@
156
160
  }))
157
161
  : entityData.length > 0
158
162
  ? (Object.entries(entityData[0]) as [string, unknown][])
159
- .filter(([k]) => !AUTO_EXCLUDED.has(k))
163
+ .filter(([k]) => !excluded.has(k))
160
164
  .map(([k, v]) => ({ attribute: k as keyof T & string, type: inferType(k, v) }))
161
165
  : [])
162
166
  );
@@ -184,6 +188,7 @@
184
188
  {labelOne}
185
189
  {labelMany}
186
190
  {icon}
191
+ {idKey}
187
192
  fields={resolvedReadFields}
188
193
  instance={singleInstance ?? {} as T}
189
194
  {read}
@@ -194,6 +199,7 @@
194
199
  {labelOne}
195
200
  {labelMany}
196
201
  {icon}
202
+ {idKey}
197
203
  fields={resolvedUpdateFields}
198
204
  instance={singleInstance ?? {} as T}
199
205
  {update}
@@ -208,6 +214,7 @@
208
214
  {labelMany}
209
215
  {icon}
210
216
  {pageSize}
217
+ {idKey}
211
218
  {creation}
212
219
  {update}
213
220
  {read}
@@ -4,6 +4,7 @@ declare function $$render<T extends object = Record<string, unknown>>(): {
4
4
  props: {
5
5
  data?: Record<string, unknown>;
6
6
  dataKey?: string;
7
+ idKey?: string;
7
8
  labelOne?: string;
8
9
  labelMany?: string;
9
10
  icon?: any;
@@ -1,6 +1,4 @@
1
1
  export declare const formatBoolean: (trueLabel?: string, falseLabel?: string) => () => (value: boolean) => string;
2
2
  export declare const formatDatetime: (format?: string) => () => (value: Date) => string;
3
3
  export declare function formatTruncateTextUpTo(maxLength: number): () => (value: string) => string;
4
- export declare function formatInstance<T extends {
5
- _id: string;
6
- }>(attribute: keyof T & string, instances: T[], urlPath: string): (value: unknown) => string;
4
+ export declare function formatInstance<T extends Record<string, unknown>>(attribute: keyof T & string, instances: T[], urlPath: string, idKey?: keyof T & string): (value: unknown) => string;
@@ -30,8 +30,8 @@ function escapeHtml(str) {
30
30
  .replace(/>/g, '&gt;')
31
31
  .replace(/"/g, '&quot;');
32
32
  }
33
- export function formatInstance(attribute, instances, urlPath) {
34
- const map = new Map(instances.map((i) => [i._id, i]));
33
+ export function formatInstance(attribute, instances, urlPath, idKey = '_id') {
34
+ const map = new Map(instances.map((i) => [String(i[idKey] ?? ''), i]));
35
35
  return (value) => {
36
36
  const id = String(value ?? '');
37
37
  const instance = map.get(id);
@@ -75,7 +75,6 @@
75
75
  <div class="flex flex-col gap-6">
76
76
 
77
77
  <Header
78
- admin
79
78
  title={labelMany}
80
79
  breadcrumbs={[
81
80
  { label: labelMany, icon: entityIcon, link: { href: '#', onclick: (e) => { e.preventDefault(); onCancel?.(); } }, prominent: true },
@@ -22,6 +22,7 @@
22
22
  labelMany = '',
23
23
  icon,
24
24
  pageSize = 10,
25
+ idKey = '_id',
25
26
  creation = {} as ActionConfiguration<T>,
26
27
  update = {} as ActionConfiguration<T>,
27
28
  read = {} as ActionConfiguration<T>,
@@ -39,6 +40,7 @@
39
40
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
40
41
  icon?: any;
41
42
  pageSize?: number;
43
+ idKey?: string;
42
44
  creation?: ActionConfiguration<T>;
43
45
  update?: ActionConfiguration<T>;
44
46
  read?: ActionConfiguration<T>;
@@ -67,7 +69,7 @@
67
69
  async function runEndpointAction(endpoint: string, items: T[]) {
68
70
  await Promise.all(items.map((item) => {
69
71
  const fd = new FormData();
70
- fd.set('id', String((item as Record<string, unknown>)._id ?? ''));
72
+ fd.set('id', String((item as Record<string, unknown>)[idKey] ?? ''));
71
73
  return fetch(endpoint, { method: 'POST', body: fd });
72
74
  }));
73
75
  await invalidateAll();
@@ -124,7 +126,6 @@
124
126
  <div class="flex flex-col gap-6">
125
127
 
126
128
  <Header
127
- admin
128
129
  title={labelMany}
129
130
  breadcrumbs={[
130
131
  { label: labelMany, icon: entityIcon, link: { href: '#' }, prominent: true },
@@ -6,6 +6,7 @@ declare function $$render<T extends object = Record<string, unknown>>(): {
6
6
  labelMany?: string;
7
7
  icon?: any;
8
8
  pageSize?: number;
9
+ idKey?: string;
9
10
  creation?: ActionConfiguration<T>;
10
11
  update?: ActionConfiguration<T>;
11
12
  read?: ActionConfiguration<T>;
@@ -15,6 +15,7 @@
15
15
  labelOne = '',
16
16
  labelMany = '',
17
17
  icon,
18
+ idKey = '_id',
18
19
  fields = [] as FieldDefinition<T>[],
19
20
  instance = {} as T,
20
21
  read = {} as ActionConfiguration<T>,
@@ -24,6 +25,7 @@
24
25
  labelMany?: string;
25
26
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
26
27
  icon?: any;
28
+ idKey?: string;
27
29
  fields?: FieldDefinition<T>[];
28
30
  instance?: T;
29
31
  read?: ActionConfiguration<T>;
@@ -37,7 +39,7 @@
37
39
 
38
40
  $effect(() => {
39
41
  const endpoint = read.endpoint;
40
- const id = (instance as Record<string, unknown>)._id;
42
+ const id = (instance as Record<string, unknown>)[idKey];
41
43
  if (!endpoint || id == null) return;
42
44
 
43
45
  let cancelled = false;
@@ -50,7 +52,7 @@
50
52
  const data = result.data as Record<string, unknown> | undefined;
51
53
  const fetched = data
52
54
  ? Object.values(data).find(
53
- (v) => v !== null && typeof v === 'object' && !Array.isArray(v) && '_id' in (v as object)
55
+ (v) => v !== null && typeof v === 'object' && !Array.isArray(v) && idKey in (v as object)
54
56
  )
55
57
  : undefined;
56
58
  if (fetched && typeof fetched === 'object') record = fetched as Record<string, unknown>;
@@ -63,7 +65,6 @@
63
65
  <div class="flex flex-col gap-6">
64
66
 
65
67
  <Header
66
- admin
67
68
  title={labelMany}
68
69
  breadcrumbs={[
69
70
  { label: labelMany, icon: entityIcon, link: { href: '#', onclick: (e) => { e.preventDefault(); onCancel?.(); } }, prominent: true },
@@ -4,6 +4,7 @@ declare function $$render<T extends object = Record<string, unknown>>(): {
4
4
  labelOne?: string;
5
5
  labelMany?: string;
6
6
  icon?: any;
7
+ idKey?: string;
7
8
  fields?: FieldDefinition<T>[];
8
9
  instance?: T;
9
10
  read?: ActionConfiguration<T>;
@@ -16,6 +16,7 @@
16
16
  labelOne = '',
17
17
  labelMany = '',
18
18
  icon,
19
+ idKey = '_id',
19
20
  fields = [] as FieldDefinition<T>[],
20
21
  instance = {} as T,
21
22
  update = {} as ActionConfiguration<T>,
@@ -27,6 +28,7 @@
27
28
  labelMany?: string;
28
29
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
30
  icon?: any;
31
+ idKey?: string;
30
32
  fields?: FieldDefinition<T>[];
31
33
  instance?: T;
32
34
  update?: ActionConfiguration<T>;
@@ -56,7 +58,7 @@
56
58
  );
57
59
 
58
60
  $effect(() => {
59
- const id = (instance as Record<string, unknown>)._id;
61
+ const id = (instance as Record<string, unknown>)[idKey];
60
62
  if (!id) return;
61
63
  untrack(() => { record = seedFromInstance(instance as Record<string, unknown>); });
62
64
  });
@@ -83,7 +85,6 @@
83
85
  <div class="flex flex-col gap-6">
84
86
 
85
87
  <Header
86
- admin
87
88
  title={labelMany}
88
89
  breadcrumbs={[
89
90
  { label: labelMany, icon: entityIcon, link: { href: '#', onclick: (e) => { e.preventDefault(); onCancel?.(); } }, prominent: true },
@@ -130,7 +131,7 @@
130
131
  };
131
132
  }}
132
133
  >
133
- <input type="hidden" name="id" value={String(record._id ?? '')} />
134
+ <input type="hidden" name="id" value={String(record[idKey] ?? '')} />
134
135
 
135
136
  {#each fields as field (field.attribute)}
136
137
  <Field {field} bind:record error={fieldErrors[field.attribute] ?? ''} />
@@ -4,6 +4,7 @@ declare function $$render<T extends object = Record<string, unknown>>(): {
4
4
  labelOne?: string;
5
5
  labelMany?: string;
6
6
  icon?: any;
7
+ idKey?: string;
7
8
  fields?: FieldDefinition<T>[];
8
9
  instance?: T;
9
10
  update?: ActionConfiguration<T>;
@@ -1,15 +1,14 @@
1
1
  <script lang="ts">
2
2
  import { getIconSet } from '../../icons/context.js';
3
3
  import { defaultIconSet } from '../../icons/sets/default.js';
4
+ import { getConfig } from '../../config/context.js';
4
5
  import type { BreadcrumbItem } from '../../types/breadcrumb.js';
5
6
 
6
7
  let {
7
8
  items = [],
8
- admin = false,
9
9
  homeHref,
10
10
  }: {
11
11
  items?: BreadcrumbItem[];
12
- admin?: boolean;
13
12
  homeHref?: string;
14
13
  } = $props();
15
14
 
@@ -18,7 +17,7 @@
18
17
  const home: BreadcrumbItem = $derived({
19
18
  label: 'Inicio',
20
19
  icon: icons.home,
21
- link: { href: homeHref ?? (admin ? '/admin' : '/') },
20
+ link: { href: homeHref ?? getConfig().homeHref ?? '/' },
22
21
  });
23
22
 
24
23
  const allItems = $derived([home, ...items]);
@@ -1,7 +1,6 @@
1
1
  import type { BreadcrumbItem } from '../../types/breadcrumb.js';
2
2
  type $$ComponentProps = {
3
3
  items?: BreadcrumbItem[];
4
- admin?: boolean;
5
4
  homeHref?: string;
6
5
  };
7
6
  declare const Breadcrumbs: import("svelte").Component<$$ComponentProps, {}, "">;
@@ -0,0 +1,5 @@
1
+ export interface RuneforgeConfig {
2
+ homeHref?: string;
3
+ }
4
+ export declare function setConfig(config: RuneforgeConfig): void;
5
+ export declare function getConfig(): RuneforgeConfig;
@@ -0,0 +1,9 @@
1
+ import { getContext, setContext } from 'svelte';
2
+ const KEY = Symbol('runeforge-config');
3
+ export function setConfig(config) {
4
+ const existing = getContext(KEY) ?? {};
5
+ setContext(KEY, { ...existing, ...config });
6
+ }
7
+ export function getConfig() {
8
+ return getContext(KEY) ?? {};
9
+ }
package/dist/index.d.ts CHANGED
@@ -3,6 +3,8 @@ export { AttributeType } from './types/attribute.js';
3
3
  export type { AttributeMetadata, InterfaceMetadata, SelectOption, OptionsResolver, FormatterResolver } from './types/attribute.js';
4
4
  export type { CellProps, CellComponent, CellFormatter, SortDirection, IndexedRow, DistinctEntry } from './types/table.js';
5
5
  export type { ColumnDefinition, FieldDefinition, ActionConfiguration, CustomAction, RowAction } from './types/crud.js';
6
+ export type { RuneforgeConfig } from './config/context.js';
7
+ export { setConfig, getConfig } from './config/context.js';
6
8
  export type { CRUDIconSet, IconComponent } from './icons/types.js';
7
9
  export { setIconSet, getIconSet } from './icons/context.js';
8
10
  export { defaultIconSet } from './icons/sets/default.js';
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { AttributeType } from './types/attribute.js';
2
+ export { setConfig, getConfig } from './config/context.js';
2
3
  export { setIconSet, getIconSet } from './icons/context.js';
3
4
  export { defaultIconSet } from './icons/sets/default.js';
4
5
  export { bootstrapIconSet } from './icons/sets/bootstrap.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runeforge",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "SvelteKit toolkit for building metadata-driven CRUD interfaces with tables, forms, and actions",
5
5
  "license": "MIT",
6
6
  "author": "Ezequiel Puerta",