runeforge 0.0.2 → 0.0.4

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.
@@ -12,11 +12,11 @@
12
12
  } = $props();
13
13
 
14
14
  const icons = $derived(getIconSet());
15
- const IconComponent = $derived(icons?.renderByName ?? null);
15
+ const IconComponent = $derived(icons?.getByName?.(name) ?? null);
16
16
  </script>
17
17
 
18
18
  {#if IconComponent}
19
- <IconComponent {name} {size} class={className} />
19
+ <IconComponent {size} class={className} />
20
20
  {:else}
21
21
  <span class={className} title={name}></span>
22
22
  {/if}
@@ -13,7 +13,7 @@
13
13
  } = $props();
14
14
  </script>
15
15
 
16
- <div class="modal modal-open" role="dialog" aria-modal="true">
16
+ <dialog class="modal" open>
17
17
  <div class="modal-box">
18
18
  <div class="flex items-center justify-between gap-4">
19
19
  <h3 class="text-lg font-bold">{title}</h3>
@@ -35,13 +35,8 @@
35
35
  </div>
36
36
 
37
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>
38
+ <div class="modal-backdrop">
39
+ <button onclick={onClose} aria-label="Cerrar"></button>
40
+ </div>
46
41
  {/if}
47
- </div>
42
+ </dialog>
@@ -17,13 +17,13 @@
17
17
  </script>
18
18
 
19
19
  <div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
20
- <div class="flex flex-col gap-1">
20
+ <div class="flex flex-col gap-1 flex-1">
21
21
  <h1>{title}</h1>
22
22
  <Breadcrumbs items={breadcrumbs} {admin} />
23
23
  </div>
24
24
 
25
25
  {#if buttons}
26
- <div class="flex items-center gap-2 pt-1">
26
+ <div class="flex shrink-0 items-center gap-2 sm:pt-1">
27
27
  {@render buttons()}
28
28
  </div>
29
29
  {/if}
@@ -43,7 +43,7 @@
43
43
  <div class="flex flex-col gap-1">
44
44
  {#if field.type === 'file'}
45
45
  <div class="flex justify-center">
46
- <Avatar src={preview} text={avatarInitials} alt={labelText} class="w-32 rounded-full" textClass="text-3xl" />
46
+ <Avatar src={preview} text={avatarInitials} alt={labelText} class="w-20 rounded-full" textClass="text-xl" />
47
47
  </div>
48
48
  {/if}
49
49
 
@@ -59,25 +59,20 @@
59
59
  type="checkbox"
60
60
  id={field.attribute}
61
61
  {name}
62
- class="toggle"
62
+ class="toggle toggle-primary"
63
63
  checked={!!saved}
64
64
  disabled={readonly}
65
65
  />
66
66
  {:else if field.type === 'file'}
67
67
  {#if !readonly}
68
- <fieldset class="fieldset w-full p-0">
69
- <input
70
- type="file"
71
- id={field.attribute}
72
- {name}
73
- class="file-input w-full"
74
- class:file-input-error={!!error}
75
- onchange={onFileChange}
76
- />
77
- {#if field.placeholder}
78
- <Label text={field.placeholder} for={field.attribute} class="label" />
79
- {/if}
80
- </fieldset>
68
+ <input
69
+ type="file"
70
+ id={field.attribute}
71
+ {name}
72
+ class="file-input file-input-bordered w-full"
73
+ class:file-input-error={!!error}
74
+ onchange={onFileChange}
75
+ />
81
76
  {/if}
82
77
  <!-- read-only file value is shown as the avatar above -->
83
78
  {:else if field.type === 'select'}
@@ -121,14 +116,14 @@
121
116
  {/if}
122
117
  {:else if field.type === 'textarea'}
123
118
  {#if readonly}
124
- <textarea id={field.attribute} class="textarea textarea-bordered w-full" value={displayValue} disabled></textarea>
119
+ <textarea id={field.attribute} class="textarea textarea-bordered bg-base-100 w-full" value={displayValue} disabled></textarea>
125
120
  {:else}
126
121
  <textarea
127
122
  id={field.attribute}
128
123
  {name}
129
124
  placeholder={field.placeholder ?? ''}
130
125
  bind:value={record[field.attribute]}
131
- class="textarea textarea-bordered w-full"
126
+ class="textarea textarea-bordered bg-base-100 w-full"
132
127
  class:textarea-error={!!error}
133
128
  ></textarea>
134
129
  {/if}
@@ -34,7 +34,7 @@
34
34
  {@const ItemIcon = item.icon}
35
35
  <ItemIcon class="size-4" />
36
36
  {/if}
37
- <span class={item.prominent ? '' : 'hidden sm:inline'}>{item.label}</span>
37
+ <span>{item.label}</span>
38
38
  </a>
39
39
  {:else}
40
40
  <span class="inline-flex items-center gap-2">
@@ -42,10 +42,46 @@
42
42
  {@const ItemIcon = item.icon}
43
43
  <ItemIcon class="size-4" />
44
44
  {/if}
45
- <span class={item.prominent ? '' : 'hidden sm:inline'}>{item.label}</span>
45
+ <span>{item.label}</span>
46
46
  </span>
47
47
  {/if}
48
48
  </li>
49
49
  {/each}
50
50
  </ul>
51
51
  </div>
52
+
53
+ <style>
54
+ /*
55
+ * DaisyUI breadcrumbs use @layer rules that Tailwind v4 doesn't propagate
56
+ * from node_modules. These unlayered rules override that gap.
57
+ */
58
+ :global(.breadcrumbs > ul),
59
+ :global(.breadcrumbs > ol),
60
+ :global(.breadcrumbs > menu) {
61
+ display: flex;
62
+ align-items: center;
63
+ white-space: nowrap;
64
+ overflow-x: auto;
65
+ }
66
+
67
+ :global(.breadcrumbs > ul > li),
68
+ :global(.breadcrumbs > ol > li),
69
+ :global(.breadcrumbs > menu > li) {
70
+ display: flex;
71
+ align-items: center;
72
+ }
73
+
74
+ :global(.breadcrumbs > ul > li + li::before),
75
+ :global(.breadcrumbs > ol > li + li::before),
76
+ :global(.breadcrumbs > menu > li + li::before) {
77
+ content: '';
78
+ display: block;
79
+ opacity: 0.4;
80
+ border-top: 1px solid;
81
+ border-right: 1px solid;
82
+ width: 0.375rem;
83
+ height: 0.375rem;
84
+ margin-inline: 0.5rem 0.75rem;
85
+ rotate: 45deg;
86
+ }
87
+ </style>
@@ -24,6 +24,41 @@
24
24
  function next() {
25
25
  if (page < totalPages) page++;
26
26
  }
27
+
28
+ function buildPages(current: number, total: number): number[] {
29
+ if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
30
+
31
+ const visible = new Set<number>();
32
+ visible.add(1);
33
+ visible.add(total);
34
+ for (let i = Math.max(1, current - 2); i <= Math.min(total, current + 2); i++) {
35
+ visible.add(i);
36
+ }
37
+
38
+ const sorted = Array.from(visible).sort((a, b) => a - b);
39
+ const result: number[] = [];
40
+ for (let i = 0; i < sorted.length; i++) {
41
+ if (i > 0 && sorted[i] - sorted[i - 1] > 1) result.push(0);
42
+ result.push(sorted[i]);
43
+ }
44
+ return result;
45
+ }
46
+
47
+ const pages = $derived(buildPages(page, totalPages));
48
+
49
+ function handlePageInput(e: KeyboardEvent) {
50
+ if (e.key !== 'Enter') return;
51
+ const val = parseInt((e.currentTarget as HTMLInputElement).value);
52
+ if (!isNaN(val) && val >= 1 && val <= totalPages) {
53
+ page = val;
54
+ } else {
55
+ (e.currentTarget as HTMLInputElement).value = String(page);
56
+ }
57
+ }
58
+
59
+ function resetInput(e: FocusEvent) {
60
+ (e.currentTarget as HTMLInputElement).value = String(page);
61
+ }
27
62
  </script>
28
63
 
29
64
  {#if totalPages > 1}
@@ -33,30 +68,27 @@
33
68
  </span>
34
69
 
35
70
  <div class="join">
36
- <Button
37
- class="join-item btn-sm"
38
- disabled={page === 1}
39
- onclick={prev}
40
- >
41
- «
42
- </Button>
43
-
44
- {#each Array.from({ length: totalPages }, (_, i) => i + 1) as n (n)}
45
- <Button
46
- class={['join-item btn-sm', n === page && 'btn-active']}
47
- onclick={() => (page = n)}
48
- >
49
- {n}
50
- </Button>
71
+ <Button class="join-item btn-sm" disabled={page === 1} onclick={prev}>«</Button>
72
+
73
+ {#each pages as p, i (i)}
74
+ {#if p === 0}
75
+ <Button class="join-item btn-sm" disabled>…</Button>
76
+ {:else if p === page}
77
+ <input
78
+ type="number"
79
+ class="join-item btn btn-sm btn-active w-16 text-center appearance-none"
80
+ min="1"
81
+ max={totalPages}
82
+ value={page}
83
+ onkeydown={handlePageInput}
84
+ onblur={resetInput}
85
+ />
86
+ {:else}
87
+ <Button class="join-item btn-sm" onclick={() => (page = p)}>{p}</Button>
88
+ {/if}
51
89
  {/each}
52
90
 
53
- <Button
54
- class="join-item btn-sm"
55
- disabled={page === totalPages}
56
- onclick={next}
57
- >
58
- »
59
- </Button>
91
+ <Button class="join-item btn-sm" disabled={page === totalPages} onclick={next}>»</Button>
60
92
  </div>
61
93
  </div>
62
94
  {/if}
@@ -9,7 +9,8 @@
9
9
  * import { bootstrapIconSet } from 'runeforge/icons/sets/bootstrap';
10
10
  * setIconSet(bootstrapIconSet);
11
11
  */
12
- import { ChevronExpand, CaretUpFill, CaretDownFill, Funnel, FunnelFill, Plus, Eye, PencilSquare, Trash3, HouseDoor, Folder, EyeSlash, } from 'svelte-bootstrap-icons';
12
+ import * as Icons from 'svelte-bootstrap-icons';
13
+ const { ChevronExpand, CaretUpFill, CaretDownFill, Funnel, FunnelFill, Plus, Eye, PencilSquare, Trash3, HouseDoor, Folder, EyeSlash, } = Icons;
13
14
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
15
  function asIcon(c) { return c; }
15
16
  export const bootstrapIconSet = {
@@ -26,4 +27,6 @@ export const bootstrapIconSet = {
26
27
  folder: asIcon(Folder),
27
28
  passwordShow: asIcon(Eye),
28
29
  passwordHide: asIcon(EyeSlash),
30
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
31
+ getByName: (name) => asIcon(Icons[name]) ?? null,
29
32
  };
@@ -3,11 +3,6 @@ export type IconComponent = Component<{
3
3
  size?: string | number;
4
4
  class?: string;
5
5
  }>;
6
- export type IconByNameComponent = Component<{
7
- name: string;
8
- size?: string | number;
9
- class?: string;
10
- }>;
11
6
  export interface CRUDIconSet {
12
7
  sortNone: IconComponent;
13
8
  sortAsc: IconComponent;
@@ -22,5 +17,5 @@ export interface CRUDIconSet {
22
17
  folder: IconComponent;
23
18
  passwordShow: IconComponent;
24
19
  passwordHide: IconComponent;
25
- renderByName?: IconByNameComponent;
20
+ getByName?: (name: string) => IconComponent | null;
26
21
  }
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ 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 { CRUDIconSet, IconComponent, IconByNameComponent } from './icons/types.js';
6
+ export type { CRUDIconSet, IconComponent } from './icons/types.js';
7
7
  export { setIconSet, getIconSet } from './icons/context.js';
8
8
  export { defaultIconSet } from './icons/sets/default.js';
9
9
  export { bootstrapIconSet } from './icons/sets/bootstrap.js';
package/package.json CHANGED
@@ -1,79 +1,81 @@
1
1
  {
2
- "name": "runeforge",
3
- "version": "0.0.2",
4
- "description": "SvelteKit toolkit for building metadata-driven CRUD interfaces with tables, forms, and actions",
5
- "license": "MIT",
6
- "author": "Ezequiel Puerta",
7
- "keywords": [
8
- "svelte",
9
- "sveltekit",
10
- "crud",
11
- "table",
12
- "form",
13
- "daisyui",
14
- "tailwindcss",
15
- "ui"
16
- ],
17
- "files": [
18
- "dist",
19
- "!dist/**/*.test.*",
20
- "!dist/**/*.spec.*"
21
- ],
22
- "sideEffects": [
23
- "**/*.css"
24
- ],
25
- "svelte": "./dist/index.js",
26
- "types": "./dist/index.d.ts",
27
- "type": "module",
28
- "exports": {
29
- ".": {
30
- "types": "./dist/index.d.ts",
31
- "svelte": "./dist/index.js"
32
- }
33
- },
34
- "peerDependencies": {
35
- "@sveltejs/kit": "^2.0.0",
36
- "daisyui": "^5.0.0",
37
- "svelte": "^5.0.0",
38
- "tailwindcss": "^4.0.0"
39
- },
40
- "devDependencies": {
41
- "@eslint/js": "^10.0.1",
42
- "@playwright/test": "^1.60.0",
43
- "@sveltejs/adapter-auto": "^7.0.1",
44
- "@sveltejs/kit": "^2.63.0",
45
- "@sveltejs/package": "^2.5.8",
46
- "@sveltejs/vite-plugin-svelte": "^7.1.2",
47
- "@tailwindcss/vite": "^4.3.1",
48
- "@types/node": "^22",
49
- "daisyui": "^5.5.23",
50
- "eslint": "^10.4.1",
51
- "eslint-config-prettier": "^10.1.8",
52
- "eslint-plugin-svelte": "^3.19.0",
53
- "globals": "^17.6.0",
54
- "prettier": "^3.8.3",
55
- "prettier-plugin-svelte": "^4.1.0",
56
- "publint": "^0.3.21",
57
- "svelte": "^5.56.1",
58
- "svelte-bootstrap-icons": "^3.3.0",
59
- "svelte-check": "^4.6.0",
60
- "tailwindcss": "^4.3.1",
61
- "typescript": "^6.0.3",
62
- "typescript-eslint": "^8.60.1",
63
- "vite": "^8.0.16",
64
- "vitest": "^4.1.8"
65
- },
66
- "scripts": {
67
- "dev": "vite dev",
68
- "build": "vite build && npm run prepack",
69
- "preview": "vite preview",
70
- "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
71
- "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
72
- "test": "pnpm run test:unit && pnpm run test:e2e",
73
- "test:unit": "vitest run",
74
- "test:unit:watch": "vitest",
75
- "test:e2e": "playwright test",
76
- "lint": "prettier --check . && eslint .",
77
- "format": "prettier --write ."
78
- }
79
- }
2
+ "name": "runeforge",
3
+ "version": "0.0.4",
4
+ "description": "SvelteKit toolkit for building metadata-driven CRUD interfaces with tables, forms, and actions",
5
+ "license": "MIT",
6
+ "author": "Ezequiel Puerta",
7
+ "keywords": [
8
+ "svelte",
9
+ "sveltekit",
10
+ "crud",
11
+ "table",
12
+ "form",
13
+ "daisyui",
14
+ "tailwindcss",
15
+ "ui"
16
+ ],
17
+ "scripts": {
18
+ "dev": "vite dev",
19
+ "build": "vite build && npm run prepack",
20
+ "preview": "vite preview",
21
+ "prepare": "svelte-kit sync || echo ''",
22
+ "prepack": "svelte-kit sync && svelte-package && publint",
23
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
24
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
25
+ "test": "pnpm run test:unit && pnpm run test:e2e",
26
+ "test:unit": "vitest run",
27
+ "test:unit:watch": "vitest",
28
+ "test:e2e": "playwright test",
29
+ "lint": "prettier --check . && eslint .",
30
+ "format": "prettier --write ."
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "!dist/**/*.test.*",
35
+ "!dist/**/*.spec.*"
36
+ ],
37
+ "sideEffects": [
38
+ "**/*.css"
39
+ ],
40
+ "svelte": "./dist/index.js",
41
+ "types": "./dist/index.d.ts",
42
+ "type": "module",
43
+ "exports": {
44
+ ".": {
45
+ "types": "./dist/index.d.ts",
46
+ "svelte": "./dist/index.js"
47
+ }
48
+ },
49
+ "peerDependencies": {
50
+ "@sveltejs/kit": "^2.0.0",
51
+ "daisyui": "^5.0.0",
52
+ "svelte": "^5.0.0",
53
+ "tailwindcss": "^4.0.0"
54
+ },
55
+ "devDependencies": {
56
+ "@eslint/js": "^10.0.1",
57
+ "@playwright/test": "^1.60.0",
58
+ "@sveltejs/adapter-auto": "^7.0.1",
59
+ "@sveltejs/kit": "^2.63.0",
60
+ "@sveltejs/package": "^2.5.8",
61
+ "@sveltejs/vite-plugin-svelte": "^7.1.2",
62
+ "@tailwindcss/vite": "^4.3.1",
63
+ "@types/node": "^22",
64
+ "daisyui": "^5.5.23",
65
+ "eslint": "^10.4.1",
66
+ "eslint-config-prettier": "^10.1.8",
67
+ "eslint-plugin-svelte": "^3.19.0",
68
+ "globals": "^17.6.0",
69
+ "prettier": "^3.8.3",
70
+ "prettier-plugin-svelte": "^4.1.0",
71
+ "publint": "^0.3.21",
72
+ "svelte": "^5.56.1",
73
+ "svelte-bootstrap-icons": "^3.3.0",
74
+ "svelte-check": "^4.6.0",
75
+ "tailwindcss": "^4.3.1",
76
+ "typescript": "^6.0.3",
77
+ "typescript-eslint": "^8.60.1",
78
+ "vite": "^8.0.16",
79
+ "vitest": "^4.1.8"
80
+ }
81
+ }