@jvelo/tapemark 0.7.0

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.
@@ -0,0 +1,656 @@
1
+ /*
2
+ * SPDX-License-Identifier: MPL-2.0
3
+ *
4
+ * This Source Code Form is subject to the terms of the Mozilla Public
5
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
7
+ */
8
+
9
+ // @ts-check
10
+ // tapemark — client-side interactivity
11
+
12
+ // <tm-confirm-button> web component
13
+ // Wraps a button and intercepts clicks to show a confirm dialog.
14
+ class TmConfirmButton extends HTMLElement {
15
+ connectedCallback() {
16
+ const button = this.querySelector("button, input[type=submit]");
17
+ if (!button) return;
18
+
19
+ button.addEventListener("click", (e) => {
20
+ const message = this.getAttribute("data-message") || "Are you sure?";
21
+ if (!confirm(message)) {
22
+ e.preventDefault();
23
+ e.stopPropagation();
24
+ }
25
+ });
26
+ }
27
+ }
28
+
29
+ if (!customElements.get("tm-confirm-button")) {
30
+ customElements.define("tm-confirm-button", TmConfirmButton);
31
+ }
32
+
33
+ // <tm-display-options> web component
34
+ // Renders option inputs based on the selected display type's JSON Schema.
35
+ // Reads schemas from a global data attribute and swaps inputs when the
36
+ // associated select changes.
37
+ class TmDisplayOptions extends HTMLElement {
38
+ connectedCallback() {
39
+ const colName = this.getAttribute("data-column");
40
+ const selectName = colName + "__display";
41
+ const select = this.closest("tr")?.querySelector(`select[name="${selectName}"]`);
42
+ if (!select) return;
43
+
44
+ // Parse schemas from the hidden script tag
45
+ const schemasEl = document.getElementById("tm-display-schemas");
46
+ if (!schemasEl) return;
47
+ let schemas;
48
+ try {
49
+ schemas = JSON.parse(schemasEl.textContent || "{}");
50
+ } catch { return; }
51
+
52
+ // Parse current options
53
+ let currentOptions = {};
54
+ try {
55
+ currentOptions = JSON.parse(this.getAttribute("data-options") || "{}");
56
+ } catch {}
57
+
58
+ const render = () => {
59
+ const type = select.value;
60
+ const schema = schemas[type];
61
+ this.innerHTML = "";
62
+ // Skip options for text (default type, rarely configured)
63
+ if (type === "text") return;
64
+ if (!schema || !schema.properties) return;
65
+
66
+ const props = schema.properties;
67
+ for (const [key, prop] of Object.entries(props)) {
68
+ const fieldName = colName + "__opt__" + key;
69
+ const currentVal = currentOptions[key];
70
+ const wrapper = document.createElement("div");
71
+ wrapper.className = "tm-opt-field";
72
+
73
+ const desc = prop.description || key;
74
+
75
+ let input;
76
+ if (prop.type === "boolean") {
77
+ input = document.createElement("input");
78
+ input.type = "checkbox";
79
+ input.name = fieldName;
80
+ input.value = "1";
81
+ input.title = desc;
82
+ if (currentVal !== undefined ? currentVal : prop.default) {
83
+ input.checked = true;
84
+ }
85
+ // Booleans need a visible label next to the checkbox
86
+ const label = document.createElement("label");
87
+ label.textContent = desc;
88
+ label.setAttribute("for", fieldName);
89
+ wrapper.appendChild(input);
90
+ wrapper.appendChild(label);
91
+ input.id = fieldName;
92
+ this.appendChild(wrapper);
93
+ continue;
94
+ } else if (prop.type === "number") {
95
+ input = document.createElement("input");
96
+ input.type = "number";
97
+ input.name = fieldName;
98
+ input.step = "any";
99
+ input.value = currentVal !== undefined ? String(currentVal) : "";
100
+ input.placeholder = desc;
101
+ input.title = desc;
102
+ } else {
103
+ input = document.createElement("input");
104
+ input.type = "text";
105
+ input.name = fieldName;
106
+ input.value = currentVal !== undefined ? String(currentVal) : "";
107
+ input.placeholder = desc;
108
+ input.title = desc;
109
+ }
110
+
111
+ input.id = fieldName;
112
+ wrapper.appendChild(input);
113
+ this.appendChild(wrapper);
114
+ }
115
+ };
116
+
117
+ render();
118
+ select.addEventListener("change", () => {
119
+ currentOptions = {};
120
+ render();
121
+ });
122
+ }
123
+ }
124
+
125
+ if (!customElements.get("tm-display-options")) {
126
+ customElements.define("tm-display-options", TmDisplayOptions);
127
+ }
128
+
129
+ // <tm-image-cell> web component
130
+ // Renders a thumbnail with hover preview. The preview <img> is only
131
+ // created on first hover, avoiding ghost images in the DOM.
132
+ class TmImageCell extends HTMLElement {
133
+ connectedCallback() {
134
+ const src = this.getAttribute("data-src");
135
+ const height = this.getAttribute("data-height") || "48";
136
+ const previewHeight = this.getAttribute("data-preview") || "240";
137
+ if (!src) return;
138
+
139
+ this.classList.add("tm-cell-image");
140
+
141
+ const thumb = document.createElement("img");
142
+ thumb.src = src;
143
+ thumb.loading = "lazy";
144
+ thumb.alt = "";
145
+ thumb.style.height = height + "px";
146
+ thumb.style.width = "auto";
147
+ thumb.style.display = "block";
148
+ this.appendChild(thumb);
149
+
150
+ let preview = null;
151
+
152
+ this.addEventListener("mouseenter", () => {
153
+ if (!preview) {
154
+ preview = document.createElement("img");
155
+ preview.src = src;
156
+ preview.alt = "";
157
+ preview.className = "tm-cell-image-preview";
158
+ preview.style.maxHeight = previewHeight + "px";
159
+ this.appendChild(preview);
160
+ }
161
+ preview.style.display = "block";
162
+ });
163
+
164
+ this.addEventListener("mouseleave", () => {
165
+ if (preview) preview.style.display = "none";
166
+ });
167
+ }
168
+ }
169
+
170
+ if (!customElements.get("tm-image-cell")) {
171
+ customElements.define("tm-image-cell", TmImageCell);
172
+ }
173
+
174
+ // <tm-modal> web component
175
+ // Generic modal shell. Create imperatively or in HTML:
176
+ // const modal = document.createElement("tm-modal");
177
+ // modal.setAttribute("data-title", "pick something");
178
+ // document.body.appendChild(modal);
179
+ // modal.open();
180
+ //
181
+ // Provides .modalBody, .modalFooter, .modalHeaderSlot for populating content.
182
+ // Dispatches "tm-modal-close" event when closed.
183
+ class TmModal extends HTMLElement {
184
+ /** @type {boolean} */
185
+ _initialized = false;
186
+ /** @type {HTMLDivElement | null} */
187
+ _overlay = null;
188
+ /** @type {HTMLDivElement | null} */
189
+ _body = null;
190
+ /** @type {HTMLDivElement | null} */
191
+ _footer = null;
192
+ /** @type {HTMLDivElement | null} */
193
+ _headerSlot = null;
194
+ /** @type {((e: KeyboardEvent) => void) | null} */
195
+ _escHandler = null;
196
+
197
+ connectedCallback() {
198
+ if (this._initialized) return;
199
+ this._initialized = true;
200
+
201
+ this._overlay = document.createElement("div");
202
+ this._overlay.className = "tm-modal-overlay";
203
+
204
+ const panel = document.createElement("div");
205
+ panel.className = "tm-modal";
206
+
207
+ const header = document.createElement("div");
208
+ header.className = "tm-modal-header";
209
+
210
+ const headerTop = document.createElement("div");
211
+ headerTop.className = "tm-modal-header-top";
212
+
213
+ const title = document.createElement("span");
214
+ title.className = "tm-modal-title";
215
+ title.textContent = this.getAttribute("data-title") || "";
216
+
217
+ const closeBtn = document.createElement("button");
218
+ closeBtn.type = "button";
219
+ closeBtn.className = "tm-modal-close";
220
+ closeBtn.textContent = "\u00d7";
221
+ closeBtn.addEventListener("click", () => this.close());
222
+
223
+ headerTop.appendChild(title);
224
+ headerTop.appendChild(closeBtn);
225
+
226
+ this._headerSlot = document.createElement("div");
227
+ this._headerSlot.className = "tm-modal-header-slot";
228
+
229
+ header.appendChild(headerTop);
230
+ header.appendChild(this._headerSlot);
231
+
232
+ this._body = document.createElement("div");
233
+ this._body.className = "tm-modal-body";
234
+
235
+ this._footer = document.createElement("div");
236
+ this._footer.className = "tm-modal-footer";
237
+
238
+ panel.appendChild(header);
239
+ panel.appendChild(this._body);
240
+ panel.appendChild(this._footer);
241
+ this._overlay.appendChild(panel);
242
+
243
+ this._overlay.addEventListener("click", (e) => {
244
+ if (e.target === this._overlay) this.close();
245
+ });
246
+
247
+ this._escHandler = (e) => {
248
+ if (e.key === "Escape") {
249
+ e.preventDefault();
250
+ this.close();
251
+ }
252
+ };
253
+ }
254
+
255
+ /** @returns {HTMLDivElement | null} */
256
+ get modalBody() { return this._body; }
257
+ /** @returns {HTMLDivElement | null} */
258
+ get modalFooter() { return this._footer; }
259
+ /** @returns {HTMLDivElement | null} */
260
+ get modalHeaderSlot() { return this._headerSlot; }
261
+
262
+ open() {
263
+ document.body.appendChild(this._overlay);
264
+ // Force layout reflow before adding the class to trigger CSS transition
265
+ void this._overlay.offsetHeight;
266
+ this._overlay.classList.add("is-open");
267
+ document.addEventListener("keydown", this._escHandler);
268
+ }
269
+
270
+ close() {
271
+ this._overlay.classList.remove("is-open");
272
+ this._overlay.addEventListener("transitionend", () => {
273
+ this._overlay.remove();
274
+ }, { once: true });
275
+ document.removeEventListener("keydown", this._escHandler);
276
+ this.dispatchEvent(new Event("tm-modal-close"));
277
+ }
278
+ }
279
+
280
+ if (!customElements.get("tm-modal")) {
281
+ customElements.define("tm-modal", TmModal);
282
+ }
283
+
284
+ // <tm-reference-input> web component
285
+ // Searchable select for foreign key fields. Fetches options from the _lookup endpoint.
286
+ // Includes an inline dropdown for quick selection and a browse modal for full table browsing.
287
+ class TmReferenceInput extends HTMLElement {
288
+ connectedCallback() {
289
+ const table = this.getAttribute("data-table");
290
+ const column = this.getAttribute("data-column");
291
+ const currentValue = this.getAttribute("data-value") || "";
292
+ const labelColumn = this.getAttribute("data-label-column");
293
+ if (!table || !column) return;
294
+
295
+ const hidden = this.querySelector("input[type=hidden]");
296
+ if (!hidden) return;
297
+
298
+ // --- DOM setup ---
299
+ const wrapper = document.createElement("div");
300
+ wrapper.className = "tm-ref-wrapper";
301
+
302
+ const inputRow = document.createElement("div");
303
+ inputRow.className = "tm-ref-input-row";
304
+
305
+ const input = document.createElement("input");
306
+ input.type = "text";
307
+ input.className = "tm-ref-search";
308
+ input.placeholder = "type to search\u2026";
309
+ input.autocomplete = "off";
310
+
311
+ const browseBtn = document.createElement("button");
312
+ browseBtn.type = "button";
313
+ browseBtn.className = "tm-ref-browse-btn";
314
+ browseBtn.title = `browse ${table}`;
315
+ browseBtn.textContent = "\u2026";
316
+
317
+ const dropdown = document.createElement("div");
318
+ dropdown.className = "tm-ref-dropdown";
319
+
320
+ const display = document.createElement("span");
321
+ display.className = "tm-ref-display";
322
+
323
+ inputRow.appendChild(display);
324
+ inputRow.appendChild(input);
325
+ inputRow.appendChild(browseBtn);
326
+ wrapper.appendChild(inputRow);
327
+ wrapper.appendChild(dropdown);
328
+ this.appendChild(wrapper);
329
+
330
+ // --- State ---
331
+ let debounceTimer = null;
332
+ let activeIndex = -1;
333
+ let open = false;
334
+ let totalRows = null;
335
+
336
+ // --- Dropdown helpers ---
337
+ const show = () => { dropdown.classList.add("is-open"); open = true; };
338
+ const hideDropdown = () => {
339
+ dropdown.classList.remove("is-open");
340
+ open = false;
341
+ activeIndex = -1;
342
+ clearHighlight();
343
+ };
344
+
345
+ const setSelected = () => { this.classList.add("has-value"); input.hidden = true; };
346
+ const setEditing = () => { this.classList.remove("has-value"); input.hidden = false; };
347
+
348
+ const clearHighlight = () => {
349
+ dropdown.querySelectorAll(".tm-ref-option").forEach((el) =>
350
+ el.classList.remove("is-active")
351
+ );
352
+ };
353
+ const highlightIndex = (i) => {
354
+ const items = dropdown.querySelectorAll(".tm-ref-option");
355
+ if (items.length === 0) return;
356
+ activeIndex = Math.max(0, Math.min(i, items.length - 1));
357
+ clearHighlight();
358
+ items[activeIndex].classList.add("is-active");
359
+ items[activeIndex].scrollIntoView({ block: "nearest" });
360
+ };
361
+
362
+ // --- Fetch helpers ---
363
+ const buildUrl = (params) => {
364
+ const prefix = window.__tapemarkPrefix || "";
365
+ return `${prefix}/${table}/_lookup?${params}`;
366
+ };
367
+
368
+ const fetchSearch = async (query, limit = 20, offset = 0) => {
369
+ const params = new URLSearchParams({ limit: String(limit), offset: String(offset) });
370
+ if (query) params.set("q", query);
371
+ if (labelColumn) params.set("label", labelColumn);
372
+ const resp = await fetch(buildUrl(params));
373
+ if (!resp.ok) return { results: [], total: 0 };
374
+ return resp.json();
375
+ };
376
+
377
+ const fetchByValue = async (value) => {
378
+ const params = new URLSearchParams({ value: String(value) });
379
+ if (labelColumn) params.set("label", labelColumn);
380
+ const resp = await fetch(buildUrl(params));
381
+ if (!resp.ok) return { results: [], total: 0 };
382
+ return resp.json();
383
+ };
384
+
385
+ const isLargeTable = () => totalRows !== null && totalRows > 50;
386
+
387
+ // --- Dropdown rendering ---
388
+ const renderOptions = (results, total) => {
389
+ dropdown.innerHTML = "";
390
+ activeIndex = -1;
391
+ if (results.length === 0) {
392
+ const hint = document.createElement("div");
393
+ hint.className = "tm-ref-hint";
394
+ hint.textContent = "no results";
395
+ dropdown.appendChild(hint);
396
+ show();
397
+ return;
398
+ }
399
+ for (const opt of results) {
400
+ const item = document.createElement("div");
401
+ item.className = "tm-ref-option";
402
+ item.textContent = opt.label || String(opt.value);
403
+ item.dataset.value = String(opt.value);
404
+ item.addEventListener("mousedown", (e) => {
405
+ e.preventDefault();
406
+ selectOpt(opt);
407
+ });
408
+ dropdown.appendChild(item);
409
+ }
410
+ if (results.length < total) {
411
+ const hint = document.createElement("div");
412
+ hint.className = "tm-ref-hint";
413
+ hint.textContent = `showing ${results.length} of ${total} \u2014 type to narrow`;
414
+ dropdown.appendChild(hint);
415
+ }
416
+ show();
417
+ };
418
+
419
+ const selectOpt = (opt) => {
420
+ hidden.value = String(opt.value);
421
+ display.textContent = opt.label || String(opt.value);
422
+ setSelected();
423
+ input.value = "";
424
+ hideDropdown();
425
+ };
426
+
427
+ const clearSelection = () => {
428
+ setEditing();
429
+ input.focus();
430
+ };
431
+
432
+ // --- Initial state ---
433
+ if (currentValue) {
434
+ display.textContent = currentValue;
435
+ setSelected();
436
+ fetchByValue(currentValue).then((data) => {
437
+ if (data.results.length > 0 && data.results[0].label) {
438
+ display.textContent = data.results[0].label;
439
+ }
440
+ });
441
+ } else {
442
+ setEditing();
443
+ }
444
+
445
+ const ensureTotal = async () => {
446
+ if (totalRows !== null) return;
447
+ const data = await fetchSearch("");
448
+ totalRows = data.total;
449
+ return data;
450
+ };
451
+
452
+ // --- Inline dropdown events ---
453
+ display.addEventListener("click", clearSelection);
454
+
455
+ input.addEventListener("input", () => {
456
+ clearTimeout(debounceTimer);
457
+ debounceTimer = setTimeout(async () => {
458
+ const data = await fetchSearch(input.value);
459
+ if (totalRows === null) totalRows = data.total;
460
+ renderOptions(data.results, data.total);
461
+ }, 150);
462
+ });
463
+
464
+ input.addEventListener("focus", async () => {
465
+ const data = await ensureTotal();
466
+ if (!isLargeTable() && data) {
467
+ renderOptions(data.results, data.total);
468
+ }
469
+ });
470
+
471
+ input.addEventListener("keydown", (e) => {
472
+ if (e.key === "ArrowDown") {
473
+ e.preventDefault();
474
+ if (!open) {
475
+ fetchSearch(input.value).then((data) => {
476
+ renderOptions(data.results, data.total);
477
+ });
478
+ return;
479
+ }
480
+ highlightIndex(activeIndex + 1);
481
+ } else if (e.key === "ArrowUp") {
482
+ e.preventDefault();
483
+ if (open) highlightIndex(activeIndex - 1);
484
+ } else if (e.key === "Enter" && open && activeIndex >= 0) {
485
+ e.preventDefault();
486
+ const items = dropdown.querySelectorAll(".tm-ref-option");
487
+ if (items[activeIndex]) {
488
+ selectOpt({
489
+ value: items[activeIndex].dataset.value,
490
+ label: items[activeIndex].textContent,
491
+ });
492
+ }
493
+ } else if (e.key === "Escape") {
494
+ e.preventDefault();
495
+ hideDropdown();
496
+ }
497
+ });
498
+
499
+ document.addEventListener("mousedown", (e) => {
500
+ if (open && !wrapper.contains(e.target)) {
501
+ hideDropdown();
502
+ }
503
+ });
504
+
505
+ // --- Browse modal ---
506
+ const PAGE_SIZE = 20;
507
+
508
+ browseBtn.addEventListener("click", () => {
509
+ hideDropdown();
510
+ openBrowseModal();
511
+ });
512
+
513
+ const openBrowseModal = () => {
514
+ const modal = document.createElement("tm-modal");
515
+ modal.setAttribute("data-title", `select from ${table}`);
516
+ // Ensure connectedCallback runs
517
+ document.body.appendChild(modal);
518
+
519
+ const body = modal.modalBody;
520
+ const footer = modal.modalFooter;
521
+ const headerSlot = modal.modalHeaderSlot;
522
+ const close = () => modal.close();
523
+
524
+ modal.open();
525
+
526
+ const searchInput = document.createElement("input");
527
+ searchInput.type = "text";
528
+ searchInput.className = "tm-modal-search";
529
+ searchInput.placeholder = "filter\u2026";
530
+ searchInput.autocomplete = "off";
531
+ headerSlot.appendChild(searchInput);
532
+
533
+ let _currentPage = 0;
534
+ let currentQuery = "";
535
+ let modalDebounce = null;
536
+
537
+ const loadPage = async (page, query) => {
538
+ _currentPage = page;
539
+ currentQuery = query;
540
+ body.innerHTML = '<div class="tm-ref-hint">loading\u2026</div>';
541
+
542
+ const data = await fetchSearch(query, PAGE_SIZE, page * PAGE_SIZE);
543
+ const totalPages = Math.ceil(data.total / PAGE_SIZE);
544
+
545
+ body.innerHTML = "";
546
+
547
+ if (data.results.length === 0) {
548
+ body.innerHTML = '<div class="tm-ref-hint">no results</div>';
549
+ } else {
550
+ const tbl = document.createElement("table");
551
+ tbl.className = "tm-modal-table";
552
+ for (const opt of data.results) {
553
+ const tr = document.createElement("tr");
554
+ tr.className = "tm-modal-row";
555
+
556
+ const tdValue = document.createElement("td");
557
+ tdValue.className = "tm-modal-cell-value";
558
+ tdValue.textContent = String(opt.value);
559
+
560
+ const tdLabel = document.createElement("td");
561
+ tdLabel.className = "tm-modal-cell-label";
562
+ tdLabel.textContent = opt.label || String(opt.value);
563
+
564
+ tr.appendChild(tdValue);
565
+ tr.appendChild(tdLabel);
566
+
567
+ tr.addEventListener("click", () => {
568
+ selectOpt(opt);
569
+ close();
570
+ });
571
+
572
+ tbl.appendChild(tr);
573
+ }
574
+ body.appendChild(tbl);
575
+ }
576
+
577
+ // Footer: pagination
578
+ footer.innerHTML = "";
579
+ const info = document.createElement("span");
580
+ info.className = "tm-modal-info";
581
+ const start = page * PAGE_SIZE + 1;
582
+ const end = Math.min((page + 1) * PAGE_SIZE, data.total);
583
+ info.textContent = data.total > 0
584
+ ? `${start}\u2013${end} of ${data.total}`
585
+ : "0 results";
586
+ footer.appendChild(info);
587
+
588
+ if (totalPages > 1) {
589
+ const nav = document.createElement("span");
590
+ nav.className = "tm-modal-nav";
591
+
592
+ const prevBtn = document.createElement("button");
593
+ prevBtn.type = "button";
594
+ prevBtn.className = "tm-btn";
595
+ prevBtn.textContent = "\u2190";
596
+ prevBtn.disabled = page === 0;
597
+ prevBtn.addEventListener("click", () => loadPage(page - 1, currentQuery));
598
+
599
+ const nextBtn = document.createElement("button");
600
+ nextBtn.type = "button";
601
+ nextBtn.className = "tm-btn";
602
+ nextBtn.textContent = "\u2192";
603
+ nextBtn.disabled = page >= totalPages - 1;
604
+ nextBtn.addEventListener("click", () => loadPage(page + 1, currentQuery));
605
+
606
+ nav.appendChild(prevBtn);
607
+ nav.appendChild(nextBtn);
608
+ footer.appendChild(nav);
609
+ }
610
+ };
611
+
612
+ searchInput.addEventListener("input", () => {
613
+ clearTimeout(modalDebounce);
614
+ modalDebounce = setTimeout(() => {
615
+ loadPage(0, searchInput.value);
616
+ }, 150);
617
+ });
618
+
619
+ loadPage(0, "");
620
+ searchInput.focus();
621
+ };
622
+ }
623
+ }
624
+
625
+ if (!customElements.get("tm-reference-input")) {
626
+ customElements.define("tm-reference-input", TmReferenceInput);
627
+ }
628
+
629
+ // Select-all checkbox and bulk delete button wiring
630
+ document.addEventListener("DOMContentLoaded", () => {
631
+ const selectAll = document.getElementById("tm-select-all");
632
+ const bulkDeleteBtn = document.getElementById("tm-bulk-delete-btn");
633
+
634
+ if (!selectAll || !bulkDeleteBtn) return;
635
+
636
+ const checkboxes = () =>
637
+ document.querySelectorAll('.tm-row-select:not(#tm-select-all)');
638
+
639
+ function updateBulkButton() {
640
+ const any = Array.from(checkboxes()).some((cb) => cb.checked);
641
+ bulkDeleteBtn.disabled = !any;
642
+ }
643
+
644
+ selectAll.addEventListener("change", () => {
645
+ checkboxes().forEach((cb) => {
646
+ cb.checked = selectAll.checked;
647
+ });
648
+ updateBulkButton();
649
+ });
650
+
651
+ document.addEventListener("change", (e) => {
652
+ if (e.target && e.target.classList && e.target.classList.contains("tm-row-select")) {
653
+ updateBulkButton();
654
+ }
655
+ });
656
+ });
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@jvelo/tapemark",
3
+ "version": "0.7.0",
4
+ "type": "module",
5
+ "description": "Self-contained admin UI for SQLite — framework-agnostic core (router, schema introspection, CRUD, server-rendered Preact UI).",
6
+ "keywords": [
7
+ "sqlite",
8
+ "admin",
9
+ "admin-panel",
10
+ "database",
11
+ "crud",
12
+ "datasette",
13
+ "preact",
14
+ "server-rendered"
15
+ ],
16
+ "license": "MPL-2.0",
17
+ "homepage": "https://github.com/jvelo/tapemark#readme",
18
+ "bugs": "https://github.com/jvelo/tapemark/issues",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/jvelo/tapemark.git",
22
+ "directory": "packages/core"
23
+ },
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.ts",
27
+ "import": "./dist/index.js"
28
+ }
29
+ },
30
+ "publishConfig": {
31
+ "access": "public",
32
+ "registry": "https://registry.npmjs.org"
33
+ },
34
+ "files": [
35
+ "dist"
36
+ ],
37
+ "peerDependencies": {
38
+ "hono": ">=4.0.0"
39
+ },
40
+ "devDependencies": {
41
+ "better-sqlite3": "^11.0.0",
42
+ "@types/better-sqlite3": "^7.6.0",
43
+ "node-gyp": "^12.2.0",
44
+ "vite": "^6.0.0",
45
+ "vite-plugin-dts": "^4.0.0",
46
+ "vitest": "^3.0.0",
47
+ "typescript": "^5.7.0",
48
+ "@jvelo/tapemark-better-sqlite3": "0.7.0"
49
+ },
50
+ "scripts": {
51
+ "build": "vite build",
52
+ "check": "tsc --noEmit -p tsconfig.check.json",
53
+ "test": "vitest run",
54
+ "test:watch": "vitest"
55
+ }
56
+ }