elysia-rack 0.1.2 → 0.2.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/dist/panel-app.js CHANGED
@@ -1 +1,498 @@
1
- function e(e,t={}){let n=new URLSearchParams;for(let[r,i]of Object.entries({...e,...t}))i!=null&&i!==``&&n.set(r,Array.isArray(i)?i.join(`,`):String(i));let r=n.toString();return r?`?${r}`:`?`}function t(e,t){let n=[...new Set([1,t,e-1,e,e+1])].filter(e=>e>=1&&e<=t).sort((e,t)=>e-t),r=[],i=0;for(let e of n)e-i>1&&r.push(`…`),r.push(e),i=e;return r}function n(e){let t={},n=e.elements||[];for(let e=0;e<n.length;e++){let r=n[e];if(r&&r.name&&!r.disabled){if(r.type===`checkbox`){r.checked&&(t[r.name]=!0);continue}if(r.value!==``&&r.value!==void 0&&r.value!==null){if(r.hasAttribute&&r.hasAttribute(`data-json`)){try{t[r.name]=JSON.parse(r.value)}catch{return{error:`Invalid JSON in ${r.name}`}}continue}t[r.name]=r.value}}}return{body:t}}function r(e,t){let n=e.elements||[];for(let e=0;e<n.length;e++){let r=n[e];if(!r||!r.name)continue;let i=t[r.name];if(i!=null){if(r.type===`checkbox`){r.checked=i===!0||i===`true`||i===1;continue}if(r.type===`datetime-local`&&typeof i==`string`){r.value=i.slice(0,16);continue}r.value=typeof i==`object`?JSON.stringify(i):String(i)}}}function i(e){return String(e??``).split(`&`).join(`&amp;`).split(`<`).join(`&lt;`).split(`>`).join(`&gt;`).split(`"`).join(`&quot;`)}function a(e){return e==null?``:typeof e==`object`?JSON.stringify(e):String(e)}function o(e,t){if(e!==`status`||typeof t!=`string`)return null;let n=t.toLowerCase();return n===`active`?`bg-success-muted text-success-muted-foreground`:n===`archived`?`bg-warning-muted text-warning-muted-foreground`:`bg-badge-background text-badge-foreground`}function s(e){let t={};return e.forEach((e,n)=>{t[n]=e}),t}function c(e){let t=e.getAttribute(`data-query-url`)||``;return t.slice(-5)===`/data`?t.slice(0,-5):t||`?`}function l(){try{if(window.crypto?.randomUUID)return window.crypto.randomUUID()}catch{}return`k-${Date.now().toString(36)}-${Math.floor(Math.random()*1e9).toString(36)}`}function u(e,t){let n=e.querySelector(`[data-form-error]`);n&&(n.textContent=t)}function d(e){g(e,new URLSearchParams(window.location.search))}function f(e){return`<td class="border-table-border border-t px-4 py-2 whitespace-nowrap"><div class="border-table-border inline-flex overflow-hidden rounded-md border"><button type="button" data-edit-id="${i(e)}" class="text-link hover:bg-table-row-hover px-2 py-1 text-sm font-semibold">Edit</button><button type="button" data-delete-id="${i(e)}" class="border-table-border text-destructive hover:bg-table-row-hover border-l px-2 py-1 text-sm font-semibold">Delete</button></div></td>`}function p(e,t,n,r,s,c){if(!n.length){e.innerHTML=`<tr><td colspan="${t.length}" class="text-text-muted px-4 py-10 text-center">No records found.</td></tr>`;return}e.innerHTML=n.map(e=>{let n=e[r]==null?``:String(e[r]),l=`<tr class="hover:bg-table-row-hover">`;return s&&(l+=`<td class="border-table-border border-t px-4 py-2"><input type="checkbox" data-select-row value="${i(n)}" aria-label="Select row ${i(n)}"></td>`),l+=t.map(t=>{let n=o(t,e[t]);return`<td class="border-table-border border-t px-4 py-2">${n?`<span class="rounded-full px-2 py-0.5 text-xs font-semibold ${n}">${i(a(e[t]))}</span>`:i(a(e[t]))}</td>`}).join(``),c&&(l+=f(n)),l+`</tr>`}).join(``)}function m(n,r,a,o){let s=`border-border rounded-md border px-3 py-1 text-sm hover:bg-navigation-hover`,c=`border-border text-text-disabled rounded-md border px-3 py-1 text-sm`,l=a>1?`<a class="${s}" data-qlink href="${i(e(r,{page:a-1}))}">← Previous</a>`:`<span class="${c}">← Previous</span>`;for(let n of t(a,o))l+=n===`…`?`<span class="text-text-muted px-1">…</span>`:n===a?`<span class="bg-primary text-primary-foreground rounded-md px-3 py-1 text-sm font-bold">${n}</span>`:`<a class="${s}" data-qlink href="${i(e(r,{page:n}))}">${n}</a>`;l+=a<o?`<a class="${s}" data-qlink href="${i(e(r,{page:a+1}))}">Next →</a>`:`<span class="${c}">Next →</span>`,n.innerHTML=l}function h(t,n,r){let i=s(r),a=[];Array.prototype.forEach.call(t.querySelectorAll(`thead th[data-col]`),e=>{a.push(e.getAttribute(`data-col`))});let o=n.query??{},c=o.page??1,l=o.limit??20,u=n.total??0,d=Math.max(1,Math.ceil(u/l)),f=t.querySelector(`table`),h=!!f?.hasAttribute(`data-selectable`),g=!!f?.hasAttribute(`data-editable`),_=f?.getAttribute(`data-pk`)||`id`,y=t.querySelector(`[data-panel-rows]`);y&&p(y,a,n.data??[],_,h,g);let b=t.querySelector(`[data-panel-count]`);b&&(b.textContent=`Page ${c} of ${d} — ${u===0?0:(c-1)*l+1}–${Math.min(c*l,u)} of ${u} records`);let x=t.querySelector(`[data-panel-pages]`);x&&m(x,i,c,d);let S=o.sort??{};Array.prototype.forEach.call(t.querySelectorAll(`a[data-sort-link]`),t=>{let n=t.getAttribute(`data-sort-link`),r=S.field===n,a=r&&S.direction===`asc`?`desc`:`asc`;t.setAttribute(`href`,e(i,{sort:n,order:a}));let o=t.querySelector(`[data-sort-ind]`);o&&(o.textContent=r?S.direction===`asc`?` ▲`:` ▼`:``)});try{window.history.replaceState(null,``,e(i,{}))}catch{}v(t)}async function g(e,t){let n=e.getAttribute(`data-query-url`)||``,r=t.toString(),i=await fetch(n+(r?`?${r}`:``),{method:`QUERY`,headers:{Accept:`application/json`}});i.ok&&h(e,await i.json(),t)}function _(e){let t=[];return Array.prototype.forEach.call(e.querySelectorAll(`[data-select-row]:checked`),e=>{t.push(e.value)}),t}function v(e){let t=_(e),n=e.querySelector(`[data-bulk-bar]`);n&&(n.style.display=t.length?``:`none`);let r=e.querySelector(`[data-bulk-count]`);r&&(r.textContent=`${t.length} selected`);let i=e.querySelectorAll(`[data-select-row]`),a=e.querySelector(`[data-select-all]`);a&&(a.checked=i.length>0&&t.length===i.length)}async function y(e,t){u(t,``);let r=t.querySelector(`[data-create-json]`);if(r){let n;try{n=JSON.parse(r.value)}catch{u(t,`Invalid JSON body`);return}let i=await fetch(c(e),{method:`POST`,headers:{"Content-Type":`application/json`,"Idempotency-Key":l()},body:JSON.stringify(n)});if(!i.ok){u(t,`Error ${i.status}: ${(await i.text()).slice(0,200)}`);return}}else{let r=n(t);if(r.error){u(t,r.error);return}let i=await fetch(c(e),{method:`POST`,headers:{"Content-Type":`application/json`,"Idempotency-Key":l()},body:JSON.stringify(r.body)});if(!i.ok){u(t,`Error ${i.status}: ${(await i.text()).slice(0,200)}`);return}}let i=t.closest(`dialog`);t.reset(),i&&i.close(),d(e)}async function b(e,t){u(t,``);let r=t.dataset?.id||``;if(!r){u(t,`Missing record id`);return}let i=n(t);if(i.error){u(t,i.error);return}let a=await fetch(`${c(e)}/${encodeURIComponent(r)}`,{method:`PATCH`,headers:{"Content-Type":`application/json`},body:JSON.stringify(i.body)});if(!a.ok){u(t,`Error ${a.status}: ${(await a.text()).slice(0,200)}`);return}let o=t.closest(`dialog`);o&&o.close(),d(e)}async function x(e,t){let n=await fetch(`${c(e)}/data/${encodeURIComponent(t)}`,{method:`QUERY`,headers:{Accept:`application/json`}});if(!n.ok)return;let i=(await n.json()).data??{},a=document.getElementById(`rack-edit`);if(!a)return;let o=a.querySelector(`[data-edit-form]`);if(!o)return;o.dataset.id=t;let s=a.querySelector(`[data-edit-id-label]`);s&&(s.textContent=`#${t}`),r(o,i),u(o,``),a.showModal?.()}async function S(e,t){window.confirm(`Delete record ${t}?`)&&(await fetch(`${c(e)}/${encodeURIComponent(t)}`,{method:`DELETE`})).ok&&d(e)}async function C(e){let t=_(e);if(!t.length||!window.confirm(`Delete ${t.length} records?`))return;let n=c(e);for(let e of t)await fetch(`${n}/${encodeURIComponent(e)}`,{method:`DELETE`});d(e)}async function w(e){let t=e.querySelector(`[data-bulk-field]`),n=e.querySelector(`[data-bulk-value]`),r=_(e);if(!t?.value||!n||!r.length)return;let i=c(e),a=JSON.stringify({[t.value]:n.value});for(let e of r)await fetch(`${i}/${encodeURIComponent(e)}`,{method:`PATCH`,headers:{"Content-Type":`application/json`},body:a});n.value=``,d(e)}document.addEventListener(`click`,e=>{let t=e.target;if(!t?.closest)return;let n=t.closest(`[data-query-url]`);if(!n)return;let r=t.closest(`[data-close]`);if(r){r.closest(`dialog`)?.close();return}if(t.closest(`[data-open-create]`)){let e=document.getElementById(`rack-create`);if(e){let t=e.querySelector(`[data-create-form]`);t&&(t.reset(),u(t,``)),e.showModal?.()}return}let i=t.closest(`[data-edit-id]`);if(i){x(n,i.getAttribute(`data-edit-id`));return}let a=t.closest(`[data-delete-id]`);if(a){S(n,a.getAttribute(`data-delete-id`));return}if(t.closest(`[data-bulk-delete]`)){C(n);return}if(t.closest(`[data-bulk-apply]`)){w(n);return}if(t.closest(`[data-bulk-clear]`)){Array.prototype.forEach.call(n.querySelectorAll(`[data-select-row],[data-select-all]`),e=>{e.checked=!1}),v(n);return}let o=t.closest(`a[data-qlink]`);o&&(e.preventDefault(),g(n,new URLSearchParams(o.getAttribute(`href`).replace(/^\?/,``))))}),document.addEventListener(`submit`,e=>{let t=e.target;if(!t||t.tagName!==`FORM`)return;let n=t.closest?.(`[data-query-url]`);if(!n)return;if(t.hasAttribute(`data-create-form`)){e.preventDefault(),y(n,t);return}if(t.hasAttribute(`data-edit-form`)){e.preventDefault(),b(n,t);return}e.preventDefault();let r=new URLSearchParams,i=t.elements??[];for(let e=0;e<i.length;e++){let t=i[e];t.name&&!t.disabled&&(t.type!==`checkbox`&&t.type!==`radio`||t.checked)&&t.value!==``&&r.append(t.name,t.value)}g(n,r)}),document.addEventListener(`change`,e=>{let t=e.target;if(!t?.closest)return;let n=t.closest(`[data-query-url]`);if(!n)return;let r=t.hasAttribute(`data-select-all`);(r||t.hasAttribute(`data-select-row`))&&(r&&Array.prototype.forEach.call(n.querySelectorAll(`[data-select-row]`),e=>{e.checked=t.checked}),v(n))});
1
+ // src/react/page/components/href.ts
2
+ function href(params, over = {}) {
3
+ const query = new URLSearchParams;
4
+ for (const [key, value] of Object.entries({ ...params, ...over })) {
5
+ if (value === undefined || value === null || value === "")
6
+ continue;
7
+ query.set(key, Array.isArray(value) ? value.join(",") : String(value));
8
+ }
9
+ const str = query.toString();
10
+ return str ? `?${str}` : "?";
11
+ }
12
+ function pageWindow(page, total) {
13
+ const keep = new Set([1, total, page - 1, page, page + 1]);
14
+ const sorted = [...keep].filter((p) => p >= 1 && p <= total).sort((a, b) => a - b);
15
+ const out = [];
16
+ let prev = 0;
17
+ for (const p of sorted) {
18
+ if (p - prev > 1)
19
+ out.push("…");
20
+ out.push(p);
21
+ prev = p;
22
+ }
23
+ return out;
24
+ }
25
+
26
+ // src/react/page/components/panelForm.ts
27
+ function collectFormBody(form) {
28
+ const body = {};
29
+ const els = form.elements || [];
30
+ for (let i = 0;i < els.length; i++) {
31
+ const el = els[i];
32
+ if (!el || !el.name || el.disabled)
33
+ continue;
34
+ if (el.type === "checkbox") {
35
+ if (el.checked)
36
+ body[el.name] = true;
37
+ continue;
38
+ }
39
+ if (el.value === "" || el.value === undefined || el.value === null)
40
+ continue;
41
+ if (el.hasAttribute && el.hasAttribute("data-json")) {
42
+ try {
43
+ body[el.name] = JSON.parse(el.value);
44
+ } catch {
45
+ return { error: `Invalid JSON in ${el.name}` };
46
+ }
47
+ continue;
48
+ }
49
+ body[el.name] = el.value;
50
+ }
51
+ return { body };
52
+ }
53
+ function fillForm(form, row) {
54
+ const els = form.elements || [];
55
+ for (let i = 0;i < els.length; i++) {
56
+ const el = els[i];
57
+ if (!el || !el.name)
58
+ continue;
59
+ const v = row[el.name];
60
+ if (v === undefined || v === null)
61
+ continue;
62
+ if (el.type === "checkbox") {
63
+ el.checked = v === true || v === "true" || v === 1;
64
+ continue;
65
+ }
66
+ if (el.type === "datetime-local" && typeof v === "string") {
67
+ el.value = v.slice(0, 16);
68
+ continue;
69
+ }
70
+ el.value = typeof v === "object" ? JSON.stringify(v) : String(v);
71
+ }
72
+ }
73
+
74
+ // src/react/page/client/panel.ts
75
+ function debounce(fn, wait) {
76
+ let id;
77
+ return (...args) => {
78
+ clearTimeout(id);
79
+ id = setTimeout(() => fn(...args), wait);
80
+ };
81
+ }
82
+ function esc(value) {
83
+ return String(value ?? "").split("&").join("&amp;").split("<").join("&lt;").split(">").join("&gt;").split('"').join("&quot;");
84
+ }
85
+ function cellText(value) {
86
+ if (value === null || value === undefined)
87
+ return "";
88
+ if (typeof value === "object")
89
+ return JSON.stringify(value);
90
+ return String(value);
91
+ }
92
+ function badgeClass(col, value) {
93
+ if (col !== "status" || typeof value !== "string")
94
+ return null;
95
+ const text = value.toLowerCase();
96
+ if (text === "active")
97
+ return "bg-success-muted text-success-muted-foreground";
98
+ if (text === "archived")
99
+ return "bg-warning-muted text-warning-muted-foreground";
100
+ return "bg-badge-background text-badge-foreground";
101
+ }
102
+ function toParams(sp) {
103
+ const out = {};
104
+ sp.forEach((v, k) => {
105
+ out[k] = v;
106
+ });
107
+ return out;
108
+ }
109
+ function baseUrl(root) {
110
+ const q = root.getAttribute("data-query-url") || "";
111
+ return q.slice(-5) === "/data" ? q.slice(0, -5) : q || "?";
112
+ }
113
+ function uid() {
114
+ try {
115
+ if (window.crypto?.randomUUID)
116
+ return window.crypto.randomUUID();
117
+ } catch {}
118
+ return `k-${Date.now().toString(36)}-${Math.floor(Math.random() * 1e9).toString(36)}`;
119
+ }
120
+ function setErr(form, msg) {
121
+ const el = form.querySelector("[data-form-error]");
122
+ if (el)
123
+ el.textContent = msg;
124
+ }
125
+ function refresh(root) {
126
+ run(root, new URLSearchParams(window.location.search));
127
+ }
128
+ function actionCell(id) {
129
+ return `<td class="border-table-border border-t px-4 py-2 whitespace-nowrap">` + `<div class="border-table-border inline-flex overflow-hidden rounded-md border">` + `<button type="button" data-edit-id="${esc(id)}" class="text-link hover:bg-table-row-hover px-2 py-1 text-sm font-semibold">Edit</button>` + `<button type="button" data-delete-id="${esc(id)}" class="border-table-border text-destructive hover:bg-table-row-hover border-l px-2 py-1 text-sm font-semibold">Delete</button>` + `</div></td>`;
130
+ }
131
+ function paintRows(tbody, cols, rows, pk, selectable, editable) {
132
+ if (!rows.length) {
133
+ tbody.innerHTML = `<tr><td colspan="${cols.length}" class="text-text-muted px-4 py-10 text-center">No records found.</td></tr>`;
134
+ return;
135
+ }
136
+ tbody.innerHTML = rows.map((row) => {
137
+ const id = row[pk] == null ? "" : String(row[pk]);
138
+ let html = `<tr class="hover:bg-table-row-hover">`;
139
+ if (selectable)
140
+ html += `<td class="border-table-border border-t px-4 py-2"><input type="checkbox" data-select-row value="${esc(id)}" aria-label="Select row ${esc(id)}"></td>`;
141
+ html += cols.map((col) => {
142
+ const cls = badgeClass(col, row[col]);
143
+ const inner = cls ? `<span class="rounded-full px-2 py-0.5 text-xs font-semibold ${cls}">${esc(cellText(row[col]))}</span>` : esc(cellText(row[col]));
144
+ return `<td class="border-table-border border-t px-4 py-2">${inner}</td>`;
145
+ }).join("");
146
+ if (editable)
147
+ html += actionCell(id);
148
+ return html + "</tr>";
149
+ }).join("");
150
+ }
151
+ function paintPages(box, params, page, totalPages) {
152
+ const linkCls = "border-border rounded-md border px-3 py-1 text-sm hover:bg-navigation-hover";
153
+ const offCls = "border-border text-text-disabled rounded-md border px-3 py-1 text-sm";
154
+ let html = page > 1 ? `<a class="${linkCls}" data-qlink href="${esc(href(params, { page: page - 1 }))}">← Previous</a>` : `<span class="${offCls}">← Previous</span>`;
155
+ for (const p of pageWindow(page, totalPages)) {
156
+ if (p === "…")
157
+ html += `<span class="text-text-muted px-1">…</span>`;
158
+ else if (p === page)
159
+ html += `<span class="bg-primary text-primary-foreground rounded-md px-3 py-1 text-sm font-bold">${p}</span>`;
160
+ else
161
+ html += `<a class="${linkCls}" data-qlink href="${esc(href(params, { page: p }))}">${p}</a>`;
162
+ }
163
+ html += page < totalPages ? `<a class="${linkCls}" data-qlink href="${esc(href(params, { page: page + 1 }))}">Next →</a>` : `<span class="${offCls}">Next →</span>`;
164
+ box.innerHTML = html;
165
+ }
166
+ function paint(root, json, sp) {
167
+ const params = toParams(sp);
168
+ const cols = [];
169
+ Array.prototype.forEach.call(root.querySelectorAll("thead th[data-col]"), (th) => {
170
+ cols.push(th.getAttribute("data-col"));
171
+ });
172
+ const q = json.query ?? {};
173
+ const page = q.page ?? 1;
174
+ const limit = q.limit ?? 20;
175
+ const total = json.total ?? 0;
176
+ const totalPages = Math.max(1, Math.ceil(total / limit));
177
+ const tbl = root.querySelector("table");
178
+ const selectable = !!tbl?.hasAttribute("data-selectable");
179
+ const editable = !!tbl?.hasAttribute("data-editable");
180
+ const pk = tbl?.getAttribute("data-pk") || "id";
181
+ const tbody = root.querySelector("[data-panel-rows]");
182
+ if (tbody)
183
+ paintRows(tbody, cols, json.data ?? [], pk, selectable, editable);
184
+ const count = root.querySelector("[data-panel-count]");
185
+ if (count) {
186
+ const from = total === 0 ? 0 : (page - 1) * limit + 1;
187
+ count.textContent = `Page ${page} of ${totalPages} — ${from}–${Math.min(page * limit, total)} of ${total} records`;
188
+ }
189
+ const box = root.querySelector("[data-panel-pages]");
190
+ if (box)
191
+ paintPages(box, params, page, totalPages);
192
+ const sort = q.sort ?? {};
193
+ Array.prototype.forEach.call(root.querySelectorAll("a[data-sort-link]"), (a) => {
194
+ const col = a.getAttribute("data-sort-link");
195
+ const active = sort.field === col;
196
+ const order = active && sort.direction === "asc" ? "desc" : "asc";
197
+ a.setAttribute("href", href(params, { sort: col, order }));
198
+ const ind = a.querySelector("[data-sort-ind]");
199
+ if (ind)
200
+ ind.textContent = active ? sort.direction === "asc" ? " ▲" : " ▼" : "";
201
+ });
202
+ try {
203
+ window.history.replaceState(null, "", href(params, {}));
204
+ } catch {}
205
+ syncBulk(root);
206
+ }
207
+ async function run(root, sp) {
208
+ const base = root.getAttribute("data-query-url") || "";
209
+ const search = sp.toString();
210
+ const res = await fetch(base + (search ? `?${search}` : ""), {
211
+ method: "QUERY",
212
+ headers: { Accept: "application/json" }
213
+ });
214
+ if (!res.ok)
215
+ return;
216
+ paint(root, await res.json(), sp);
217
+ }
218
+ function selectedIds(root) {
219
+ const ids = [];
220
+ Array.prototype.forEach.call(root.querySelectorAll("[data-select-row]:checked"), (c) => {
221
+ ids.push(c.value);
222
+ });
223
+ return ids;
224
+ }
225
+ function syncBulk(root) {
226
+ const ids = selectedIds(root);
227
+ const bar = root.querySelector("[data-bulk-bar]");
228
+ if (bar)
229
+ bar.style.display = ids.length ? "" : "none";
230
+ const count = root.querySelector("[data-bulk-count]");
231
+ if (count)
232
+ count.textContent = `${ids.length} selected`;
233
+ const boxes = root.querySelectorAll("[data-select-row]");
234
+ const all = root.querySelector("[data-select-all]");
235
+ if (all)
236
+ all.checked = boxes.length > 0 && ids.length === boxes.length;
237
+ }
238
+ async function createRecord(root, form) {
239
+ setErr(form, "");
240
+ const raw = form.querySelector("[data-create-json]");
241
+ if (raw) {
242
+ let body;
243
+ try {
244
+ body = JSON.parse(raw.value);
245
+ } catch {
246
+ setErr(form, "Invalid JSON body");
247
+ return;
248
+ }
249
+ const res = await fetch(baseUrl(root), {
250
+ method: "POST",
251
+ headers: { "Content-Type": "application/json", "Idempotency-Key": uid() },
252
+ body: JSON.stringify(body)
253
+ });
254
+ if (!res.ok) {
255
+ setErr(form, `Error ${res.status}: ${(await res.text()).slice(0, 200)}`);
256
+ return;
257
+ }
258
+ } else {
259
+ const collected = collectFormBody(form);
260
+ if (collected.error) {
261
+ setErr(form, collected.error);
262
+ return;
263
+ }
264
+ const res = await fetch(baseUrl(root), {
265
+ method: "POST",
266
+ headers: { "Content-Type": "application/json", "Idempotency-Key": uid() },
267
+ body: JSON.stringify(collected.body)
268
+ });
269
+ if (!res.ok) {
270
+ setErr(form, `Error ${res.status}: ${(await res.text()).slice(0, 200)}`);
271
+ return;
272
+ }
273
+ }
274
+ const dlg = form.closest("dialog");
275
+ form.reset();
276
+ if (dlg)
277
+ dlg.close();
278
+ refresh(root);
279
+ }
280
+ async function updateRecord(root, form) {
281
+ setErr(form, "");
282
+ const id = form.dataset?.id || "";
283
+ if (!id) {
284
+ setErr(form, "Missing record id");
285
+ return;
286
+ }
287
+ const collected = collectFormBody(form);
288
+ if (collected.error) {
289
+ setErr(form, collected.error);
290
+ return;
291
+ }
292
+ const res = await fetch(`${baseUrl(root)}/${encodeURIComponent(id)}`, {
293
+ method: "PATCH",
294
+ headers: { "Content-Type": "application/json" },
295
+ body: JSON.stringify(collected.body)
296
+ });
297
+ if (!res.ok) {
298
+ setErr(form, `Error ${res.status}: ${(await res.text()).slice(0, 200)}`);
299
+ return;
300
+ }
301
+ const dlg = form.closest("dialog");
302
+ if (dlg)
303
+ dlg.close();
304
+ refresh(root);
305
+ }
306
+ async function openEdit(root, id) {
307
+ const res = await fetch(`${baseUrl(root)}/data/${encodeURIComponent(id)}`, {
308
+ method: "QUERY",
309
+ headers: { Accept: "application/json" }
310
+ });
311
+ if (!res.ok)
312
+ return;
313
+ const row = (await res.json()).data ?? {};
314
+ const dlg = document.getElementById("rack-edit");
315
+ if (!dlg)
316
+ return;
317
+ const form = dlg.querySelector("[data-edit-form]");
318
+ if (!form)
319
+ return;
320
+ form.dataset.id = id;
321
+ const label = dlg.querySelector("[data-edit-id-label]");
322
+ if (label)
323
+ label.textContent = `#${id}`;
324
+ fillForm(form, row);
325
+ setErr(form, "");
326
+ dlg.showModal?.();
327
+ }
328
+ async function deleteOne(root, id) {
329
+ if (!window.confirm(`Delete record ${id}?`))
330
+ return;
331
+ const res = await fetch(`${baseUrl(root)}/${encodeURIComponent(id)}`, {
332
+ method: "DELETE"
333
+ });
334
+ if (res.ok)
335
+ refresh(root);
336
+ }
337
+ async function bulkDelete(root) {
338
+ const ids = selectedIds(root);
339
+ if (!ids.length)
340
+ return;
341
+ if (!window.confirm(`Delete ${ids.length} records?`))
342
+ return;
343
+ const base = baseUrl(root);
344
+ for (const id of ids) {
345
+ await fetch(`${base}/${encodeURIComponent(id)}`, { method: "DELETE" });
346
+ }
347
+ refresh(root);
348
+ }
349
+ async function bulkApply(root) {
350
+ const field = root.querySelector("[data-bulk-field]");
351
+ const input = root.querySelector("[data-bulk-value]");
352
+ const ids = selectedIds(root);
353
+ if (!field?.value || !input || !ids.length)
354
+ return;
355
+ const base = baseUrl(root);
356
+ const payload = JSON.stringify({ [field.value]: input.value });
357
+ for (const id of ids) {
358
+ await fetch(`${base}/${encodeURIComponent(id)}`, {
359
+ method: "PATCH",
360
+ headers: { "Content-Type": "application/json" },
361
+ body: payload
362
+ });
363
+ }
364
+ input.value = "";
365
+ refresh(root);
366
+ }
367
+ document.addEventListener("click", (e) => {
368
+ const t = e.target;
369
+ if (!t?.closest)
370
+ return;
371
+ const root = t.closest("[data-query-url]");
372
+ if (!root)
373
+ return;
374
+ const closer = t.closest("[data-close]");
375
+ if (closer) {
376
+ closer.closest("dialog")?.close();
377
+ return;
378
+ }
379
+ if (t.closest("[data-open-create]")) {
380
+ const dlg = document.getElementById("rack-create");
381
+ if (dlg) {
382
+ const cf = dlg.querySelector("[data-create-form]");
383
+ if (cf) {
384
+ cf.reset();
385
+ setErr(cf, "");
386
+ }
387
+ dlg.showModal?.();
388
+ }
389
+ return;
390
+ }
391
+ const eb = t.closest("[data-edit-id]");
392
+ if (eb) {
393
+ openEdit(root, eb.getAttribute("data-edit-id"));
394
+ return;
395
+ }
396
+ const dbtn = t.closest("[data-delete-id]");
397
+ if (dbtn) {
398
+ deleteOne(root, dbtn.getAttribute("data-delete-id"));
399
+ return;
400
+ }
401
+ if (t.closest("[data-bulk-delete]")) {
402
+ bulkDelete(root);
403
+ return;
404
+ }
405
+ if (t.closest("[data-bulk-apply]")) {
406
+ bulkApply(root);
407
+ return;
408
+ }
409
+ if (t.closest("[data-bulk-clear]")) {
410
+ Array.prototype.forEach.call(root.querySelectorAll("[data-select-row],[data-select-all]"), (c) => {
411
+ c.checked = false;
412
+ });
413
+ syncBulk(root);
414
+ return;
415
+ }
416
+ const a = t.closest("a[data-qlink]");
417
+ if (!a)
418
+ return;
419
+ e.preventDefault();
420
+ run(root, new URLSearchParams(a.getAttribute("href").replace(/^\?/, "")));
421
+ });
422
+ document.addEventListener("submit", (e) => {
423
+ const f = e.target;
424
+ if (!f || f.tagName !== "FORM")
425
+ return;
426
+ const root = f.closest?.("[data-query-url]");
427
+ if (!root)
428
+ return;
429
+ if (f.hasAttribute("data-create-form")) {
430
+ e.preventDefault();
431
+ createRecord(root, f);
432
+ return;
433
+ }
434
+ if (f.hasAttribute("data-edit-form")) {
435
+ e.preventDefault();
436
+ updateRecord(root, f);
437
+ return;
438
+ }
439
+ e.preventDefault();
440
+ const clean = new URLSearchParams;
441
+ const els = f.elements ?? [];
442
+ for (let i = 0;i < els.length; i++) {
443
+ const el = els[i];
444
+ if (!el.name || el.disabled)
445
+ continue;
446
+ if ((el.type === "checkbox" || el.type === "radio") && !el.checked)
447
+ continue;
448
+ if (el.value === "")
449
+ continue;
450
+ clean.append(el.name, el.value);
451
+ }
452
+ run(root, clean);
453
+ });
454
+ var debouncedRunForSearch = debounce((form, root) => {
455
+ const clean = new URLSearchParams;
456
+ const els = form.elements ?? [];
457
+ for (let i = 0;i < els.length; i++) {
458
+ const el = els[i];
459
+ if (!el.name || el.disabled)
460
+ continue;
461
+ if ((el.type === "checkbox" || el.type === "radio") && !el.checked)
462
+ continue;
463
+ if (el.value === "")
464
+ continue;
465
+ clean.append(el.name, el.value);
466
+ }
467
+ run(root, clean);
468
+ }, 350);
469
+ document.addEventListener("input", (e) => {
470
+ const t = e.target;
471
+ if (!t || !t.closest)
472
+ return;
473
+ const isSearch = t.getAttribute?.("data-search-input") !== null || t.getAttribute?.("name") === "search";
474
+ if (!isSearch)
475
+ return;
476
+ const form = t.closest("form");
477
+ const root = t.closest("[data-query-url]") || form?.closest?.("[data-query-url]");
478
+ if (!form || !root)
479
+ return;
480
+ debouncedRunForSearch(form, root);
481
+ });
482
+ document.addEventListener("change", (e) => {
483
+ const t = e.target;
484
+ if (!t?.closest)
485
+ return;
486
+ const root = t.closest("[data-query-url]");
487
+ if (!root)
488
+ return;
489
+ const all = t.hasAttribute("data-select-all");
490
+ if (!all && !t.hasAttribute("data-select-row"))
491
+ return;
492
+ if (all) {
493
+ Array.prototype.forEach.call(root.querySelectorAll("[data-select-row]"), (c) => {
494
+ c.checked = t.checked;
495
+ });
496
+ }
497
+ syncBulk(root);
498
+ });
@@ -1 +1,38 @@
1
- var e=`koran-theme`;function t(){try{let t=localStorage.getItem(e);return t===`dark`||t===`light`?t:null}catch{return null}}function n(){try{return typeof window.matchMedia==`function`&&window.matchMedia(`(prefers-color-scheme: dark)`).matches}catch{return!1}}function r(){let t=document.documentElement,n=t.classList.toggle(`dark`);t.classList.toggle(`light`,!n);try{localStorage.setItem(e,n?`dark`:`light`)}catch{}}function i(){let e=t();e?document.documentElement.classList.add(e):n()&&document.documentElement.classList.add(`dark`),document.getElementById(`koran-theme-toggle`)?.addEventListener(`click`,r),window.__koranToggle=r}i();
1
+ // src/react/page/client/theme.ts
2
+ var STORAGE_KEY = "koran-theme";
3
+ function storedTheme() {
4
+ try {
5
+ const value = localStorage.getItem(STORAGE_KEY);
6
+ return value === "dark" || value === "light" ? value : null;
7
+ } catch {
8
+ return null;
9
+ }
10
+ }
11
+ function systemDark() {
12
+ try {
13
+ return typeof window.matchMedia === "function" && window.matchMedia("(prefers-color-scheme: dark)").matches;
14
+ } catch {
15
+ return false;
16
+ }
17
+ }
18
+ function toggleTheme() {
19
+ const root = document.documentElement;
20
+ const dark = root.classList.toggle("dark");
21
+ root.classList.toggle("light", !dark);
22
+ try {
23
+ localStorage.setItem(STORAGE_KEY, dark ? "dark" : "light");
24
+ } catch {}
25
+ }
26
+ function init() {
27
+ const stored = storedTheme();
28
+ if (stored)
29
+ document.documentElement.classList.add(stored);
30
+ else if (systemDark())
31
+ document.documentElement.classList.add("dark");
32
+ document.getElementById("koran-theme-toggle")?.addEventListener("click", toggleTheme);
33
+ window.__koranToggle = toggleTheme;
34
+ }
35
+ init();
36
+ export {
37
+ toggleTheme
38
+ };
@@ -6,6 +6,8 @@ export interface DashboardOptions {
6
6
  title?: string;
7
7
  /** React page registry key (default "/dashboard") */
8
8
  pagePath?: string;
9
+ /** Donate badge — false to hide, true/undefined to show */
10
+ donate?: boolean;
9
11
  }
10
12
  /**
11
13
  * Dashboard plugin — mounts `GET {path}` that renders
@@ -50,6 +52,7 @@ export declare function dashboard(options?: DashboardOptions): Elysia<"", {
50
52
  path: string;
51
53
  props: {
52
54
  resource: string | undefined;
55
+ donate?: boolean | undefined;
53
56
  name?: string | undefined;
54
57
  };
55
58
  };
@@ -686,6 +686,9 @@ function isNull(value) {
686
686
  function like(column, value) {
687
687
  return sql`${column} like ${value}`;
688
688
  }
689
+ function ilike(column, value) {
690
+ return sql`${column} ilike ${value}`;
691
+ }
689
692
 
690
693
  // node_modules/drizzle-orm/sql/expressions/select.js
691
694
  function asc(column) {
@@ -763,7 +766,16 @@ function whereFor(table, query, opts, withSoftDelete) {
763
766
  }
764
767
  if (query.search && query.search.fields.length > 0) {
765
768
  const pattern = `%${escapeLike(query.search.value)}%`;
766
- const ors = query.search.fields.map((f) => table[f] ? like(table[f], pattern) : undefined).filter((c) => c !== undefined);
769
+ const ors = query.search.fields.map((f) => {
770
+ const col = table[f];
771
+ if (!col)
772
+ return;
773
+ try {
774
+ return typeof ilike === "function" ? ilike(col, pattern) : like(col, pattern);
775
+ } catch {
776
+ return like(col, pattern);
777
+ }
778
+ }).filter((c) => c !== undefined);
767
779
  if (ors.length > 0)
768
780
  conds.push(or(...ors));
769
781
  }
@@ -1137,6 +1149,7 @@ function dashboard(options) {
1137
1149
  path: pagePath,
1138
1150
  props: {
1139
1151
  ...title !== undefined ? { name: title } : {},
1152
+ ...options?.donate !== undefined ? { donate: options.donate } : {},
1140
1153
  resource: typeof query.resource === "string" ? query.resource : undefined
1141
1154
  }
1142
1155
  }));
@@ -0,0 +1,10 @@
1
+ export declare const DEFAULT_DONATE_URL = "https://ko-fi.com/notoofly_manu";
2
+ export declare const DEFAULT_DONATE_LABEL = "\u2615 Donate";
3
+ export type ResolvedDonate = {
4
+ enabled: boolean;
5
+ url: string;
6
+ label: string;
7
+ };
8
+ export declare function setGlobalDonate(enabled?: boolean): void;
9
+ export declare function getGlobalDonate(): ResolvedDonate;
10
+ export declare function resolveDonateProp(prop?: boolean): ResolvedDonate;
@@ -2,7 +2,7 @@ import Elysia from "elysia";
2
2
  import { ReactRack } from "./types";
3
3
  export declare function page<Props = Record<string, never>>(path: string, props?: Props): ReactRack.PageDescriptor<Props>;
4
4
  export declare const pages: ReactRack.PageRegistry;
5
- export declare function reactPlugin(options: ReactRack.ReactPluginOptions): Elysia<"", {
5
+ export declare function reactPlugin(options?: ReactRack.ReactPluginOptions): Elysia<"", {
6
6
  decorator: {
7
7
  react: {
8
8
  render: (path: string, props: unknown) => Promise<Response>;
@@ -1,3 +1,6 @@
1
+ import {
2
+ setGlobalDonate
3
+ } from "../index-6v0ns1xn.js";
1
4
  import {
2
5
  __require
3
6
  } from "../index-37x76zdn.js";
@@ -152,11 +155,20 @@ function page(path, props) {
152
155
  };
153
156
  }
154
157
  var pages = {
155
- "/dashboard": () => import("../Dashboard-2j561p7c.js"),
156
- "/panel": () => import("../Panel-fbp3wrsw.js")
158
+ "/dashboard": () => import("../Dashboard-7gmx8c9y.js"),
159
+ "/panel": () => import("../Panel-wjxjtbmr.js")
157
160
  };
158
- function reactPlugin(options) {
159
- const render = createRenderer(options.pages);
161
+ var defaultPages = pages;
162
+ function mergeRegistries(base, extra) {
163
+ if (!extra)
164
+ return { ...base };
165
+ const list = Array.isArray(extra) ? extra : [extra];
166
+ return list.reduce((acc, r) => ({ ...acc, ...r }), { ...base });
167
+ }
168
+ function reactPlugin(options = {}) {
169
+ setGlobalDonate(options.donate);
170
+ const registry = mergeRegistries(defaultPages, options.pages);
171
+ const render = createRenderer(registry);
160
172
  const panelCssOverride = pickPanelCssOption(options);
161
173
  let cachedCustomCss = null;
162
174
  let cachedCustomCssLoaded = false;
@@ -1,7 +1,6 @@
1
- import { type RackRegistration } from "../../rack/registry";
2
1
  export interface DashboardProps {
3
2
  name?: string;
4
3
  resource?: string;
5
- racks?: RackRegistration[];
4
+ donate?: boolean;
6
5
  }
7
6
  export default function Dashboard(props: DashboardProps): Promise<import("react").JSX.Element>;
@@ -1,5 +1,6 @@
1
1
  import type { FieldDescriptor } from "../../rack/adapters/index";
2
2
  export interface PanelProps {
3
+ donate?: boolean;
3
4
  resource?: string;
4
5
  metadata?: {
5
6
  label?: string;