@bendyline/gilde 0.2.5 → 0.2.6

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 (67) hide show
  1. package/authoring/LICENSE-HankenGrotesk-OFL-1.1.txt +94 -0
  2. package/authoring/LICENSE-PTSerif-OFL-1.1.txt +94 -0
  3. package/authoring/page-demo-bootstrap.js +209 -0
  4. package/authoring/page-demo-injector.mjs +7 -0
  5. package/authoring/page-demo-typography.css +88 -0
  6. package/data/craftbook-templates/ch/character-sheet/versions/1.0.4/craftbook.json +213 -0
  7. package/data/craftbook-templates/ch/character-sheet/versions/1.0.4/test.json +143 -0
  8. package/data/craftbook-templates/ch/character-turnaround/versions/1.0.4/craftbook.json +254 -0
  9. package/data/craftbook-templates/ch/character-turnaround/versions/1.0.4/test.json +189 -0
  10. package/data/craftbook-templates/fa/favicon-set/versions/1.0.5/craftbook.json +282 -0
  11. package/data/craftbook-templates/fa/favicon-set/versions/1.0.5/test.json +94 -0
  12. package/data/craftbook-templates/he/hero-image/versions/1.0.5/craftbook.json +287 -0
  13. package/data/craftbook-templates/he/hero-image/versions/1.0.5/test.json +94 -0
  14. package/data/craftbook-templates/ic/icon-pack/versions/1.0.5/craftbook.json +279 -0
  15. package/data/craftbook-templates/ic/icon-pack/versions/1.0.5/test.json +94 -0
  16. package/data/craftbook-templates/index.json +1 -1
  17. package/data/craftbook-templates/lo/logo-set/versions/1.0.5/craftbook.json +279 -0
  18. package/data/craftbook-templates/lo/logo-set/versions/1.0.5/test.json +94 -0
  19. package/data/craftbook-templates/mo/moodboard/versions/1.0.5/craftbook.json +219 -0
  20. package/data/craftbook-templates/mo/moodboard/versions/1.0.5/test.json +89 -0
  21. package/data/craftbook-templates/pa/page-spread/versions/1.0.4/craftbook.json +214 -0
  22. package/data/craftbook-templates/pa/page-spread/versions/1.0.4/test.json +132 -0
  23. package/data/craftbook-templates/raw-index.json +1 -1
  24. package/data/craftbook-templates/sp/sprite-sheet/versions/1.0.5/craftbook.json +269 -0
  25. package/data/craftbook-templates/sp/sprite-sheet/versions/1.0.5/test.json +94 -0
  26. package/data/craftbook-templates/ti/tileset-batch/versions/1.0.4/craftbook.json +319 -0
  27. package/data/craftbook-templates/ti/tileset-batch/versions/1.0.4/test.json +162 -0
  28. package/data/gezel-templates/il/illustrator/manifest.json +21 -0
  29. package/data/gezel-templates/il/illustrator/versions/1.0.0/about.md +28 -0
  30. package/data/gezel-templates/il/illustrator/versions/1.0.0/manifest.json +9 -0
  31. package/data/gezel-templates/index.json +1 -1
  32. package/data/gezel-templates/raw-index.json +1 -1
  33. package/data/image-models/an/animagine-xl-4/manifest.json +2 -0
  34. package/data/image-models/fl/flux-2-klein-4b-q4/manifest.json +3 -1
  35. package/data/image-models/index.json +1 -1
  36. package/data/image-models/ir/iris-3b-f32/manifest.json +4 -2
  37. package/data/image-models/ir/iris-3b-f32/versions/1.0.2/manifest.json +25 -0
  38. package/data/image-models/kr/krea-2-turbo-q4/manifest.json +2 -0
  39. package/data/image-models/kr/krea-2-turbo-q6/manifest.json +2 -0
  40. package/data/image-models/kr/krea-2-turbo-q8/manifest.json +2 -0
  41. package/data/image-models/mi/ming-image-0.1-design-bf16/manifest.json +2 -0
  42. package/data/image-models/mi/ming-image-0.1-design-bf16/versions/1.0.2/manifest.json +50 -0
  43. package/data/image-models/mo/moss-image-sft-q4/manifest.json +3 -1
  44. package/data/image-models/mo/moss-image-sft-q4/versions/1.0.0/manifest.json +1 -1
  45. package/data/image-models/mo/moss-image-sft-q4/versions/1.0.1/manifest.json +37 -0
  46. package/data/image-models/raw-index.json +1 -1
  47. package/data/image-models/sd/sd-1.5/manifest.json +2 -0
  48. package/data/image-models/sd/sd-turbo/manifest.json +2 -0
  49. package/data/image-models/sd/sdxl-base-1.0/manifest.json +2 -0
  50. package/data/image-models/sd/sdxl-lightning-4step/manifest.json +2 -0
  51. package/data/image-models/sd/sdxl-turbo/manifest.json +2 -0
  52. package/data/project-types/da/data-analysis/versions/1.0.0/about.md +2 -0
  53. package/data/project-types/da/data-analysis/versions/1.0.0/manifest.json +44 -0
  54. package/data/project-types/da/data-analysis/versions/1.0.0/pages/dashboard.css +5 -8
  55. package/data/project-types/da/data-analysis/versions/1.0.0/pages/dashboard.js +11 -31
  56. package/data/project-types/da/data-analysis/versions/1.0.0/pages/demo.js +9 -0
  57. package/data/project-types/da/data-analysis/versions/1.0.0/pages/index.html +3 -91
  58. package/data/project-types/index.json +1 -1
  59. package/data/project-types/me/media-library/versions/1.0.0/about.md +1 -1
  60. package/data/project-types/me/media-library/versions/1.0.0/manifest.json +53 -0
  61. package/data/project-types/me/media-library/versions/1.0.0/pages/gallery.css +3 -14
  62. package/data/project-types/me/media-library/versions/1.0.0/pages/gallery.js +16 -28
  63. package/data/project-types/me/media-library/versions/1.0.0/pages/index.html +3 -90
  64. package/data/project-types/raw-index.json +1 -1
  65. package/package.json +3 -2
  66. package/schemas/image-model-identity.schema.json +10 -0
  67. package/schemas/project-type-version.schema.json +40 -0
@@ -11,7 +11,7 @@ Do not assume a filename or modification date proves when or where a photo was
11
11
  taken. Playback depends on the device's supported formats; unpreviewable files
12
12
  remain listed and can be opened through Gezel's existing viewers.
13
13
 
14
- The page only reads files. Hidden folders, dependency folders, and document
14
+ The shared Add files control can copy selected files into this workspace when managed writes are enabled. Existing names are preserved by choosing a new name. Browsing does not change files. Hidden folders, dependency folders, and document
15
15
  companion folders are skipped. Scans stop after 100 folders or 10,000 files and
16
16
  clearly label a partial library. The bundled samples are only a standalone demo;
17
17
  they are never inserted into a real project's folder.
@@ -15,6 +15,59 @@
15
15
  "path": ".",
16
16
  "subtree": true
17
17
  }
18
+ ],
19
+ "imports": [
20
+ {
21
+ "id": "library",
22
+ "source": "workspace",
23
+ "path": ".",
24
+ "accept": [
25
+ ".jpg",
26
+ ".jpeg",
27
+ ".png",
28
+ ".gif",
29
+ ".webp",
30
+ ".avif",
31
+ ".bmp",
32
+ ".svg",
33
+ ".ico",
34
+ ".heic",
35
+ ".heif",
36
+ ".tif",
37
+ ".tiff",
38
+ ".dng",
39
+ ".raw",
40
+ ".cr2",
41
+ ".cr3",
42
+ ".nef",
43
+ ".arw",
44
+ ".orf",
45
+ ".raf",
46
+ ".mp4",
47
+ ".webm",
48
+ ".mov",
49
+ ".m4v",
50
+ ".ogv",
51
+ ".avi",
52
+ ".mkv",
53
+ ".mpg",
54
+ ".mpeg",
55
+ ".mts",
56
+ ".m2ts",
57
+ ".mp3",
58
+ ".wav",
59
+ ".ogg",
60
+ ".oga",
61
+ ".m4a",
62
+ ".aac",
63
+ ".flac",
64
+ ".opus",
65
+ ".aif",
66
+ ".aiff",
67
+ ".wma"
68
+ ],
69
+ "maxFileBytes": 268435456
70
+ }
18
71
  ]
19
72
  }
20
73
  }
@@ -39,20 +39,8 @@ dt { color: var(--muted); font-size: .85rem; } dd { margin: 0; font-size: 1.3rem
39
39
  .view-choices [aria-pressed="true"] { border-color: var(--accent); background: var(--soft); color: var(--accent); box-shadow: inset 0 1px 3px #0002; }
40
40
  #gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); list-style: none; gap: 20px; margin: 0; padding: 0; }
41
41
  #gallery li { min-width: 0; }
42
- .media-card { display: flex; flex-direction: column; padding: 0; overflow: hidden; width: 100%; height: 100%; text-align: left; background: var(--surface); border-radius: 6px; }
43
- .thumbnail { display: flex; position: relative; flex-direction: column; align-items: center; justify-content: center; width: 100%; aspect-ratio: 4 / 3; background: var(--soft); color: var(--accent); overflow: hidden; }
44
- .thumbnail img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; background: var(--surface); }
45
42
  svg { width: 36px; height: 36px; stroke: currentColor; stroke-width: 1.4; fill: none; stroke-linecap: round; stroke-linejoin: round; }
46
- .fallback-label { font-size: .8rem; margin-top: 8px; }
47
- .card-info { padding: 12px; min-width: 0; }
48
- .file-name { display: block; font-size: .95rem; font-weight: 600; overflow-wrap: anywhere; }
49
- .file-folder { display: block; font-size: .8rem; color: var(--muted); overflow-wrap: anywhere; margin-block: 2px 8px; }
50
- .file-meta { display: block; font-size: .75rem; color: var(--muted); }
51
43
  #gallery[data-view="list"] { grid-template-columns: 1fr; gap: 8px; }
52
- [data-view="list"] .media-card { flex-direction: row; align-items: center; }
53
- [data-view="list"] .thumbnail { flex: 0 0 100px; width: 100px; }
54
- [data-view="list"] .fallback-label { display: none; }
55
- [data-view="list"] .file-folder { margin-bottom: 2px; }
56
44
  #show-more { display: block; margin: 24px auto; }
57
45
  .empty { padding: 40px 20px; text-align: center; border: 1px dashed var(--border); border-radius: 10px; background: var(--surface); }
58
46
  .empty p { margin: 12px 0 0; color: var(--muted); }
@@ -73,8 +61,6 @@ dialog::backdrop { background: #000b; }
73
61
  #position { font-size: .8rem; color: var(--muted); }
74
62
  @media (max-width: 480px) {
75
63
  #gallery { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
76
- .card-info { padding: 8px; }
77
- .file-name { font-size: .85rem; }
78
64
  .counts { gap: 20px; }
79
65
  header { align-items: flex-start; }
80
66
  .library-heading { align-items: flex-start; flex-direction: column; gap: 4px; }
@@ -83,3 +69,6 @@ dialog::backdrop { background: #000b; }
83
69
  .navigation { flex-wrap: wrap; gap: 8px; }
84
70
  [data-gezel-demo-banner] { align-items: flex-start; flex-direction: column; }
85
71
  }
72
+
73
+ /* Shared components own file presentation; the page owns its layout. */
74
+ #add-files { margin: 20px 0; }
@@ -1,7 +1,5 @@
1
1
  (() => {
2
2
  'use strict';
3
- const api = window.makeDemoGezel({ page: { typeName: 'Media Library' } });
4
- const demoMode = api.page.mode === 'demo';
5
3
  const model = window.mediaLibraryFiles;
6
4
  const $ = (id) => document.getElementById(id);
7
5
  const labels = { image: 'Image', video: 'Video', audio: 'Audio' };
@@ -12,9 +10,14 @@
12
10
  ['Studio/Evening light.svg', 'evening.svg', 1980], ['Motion/Light study.webm', 'light.webm', 24000],
13
11
  ['Recordings/Three notes.wav', 'notes.wav', 96044],
14
12
  ].map(([path, asset, size], i) => ({ path, asset, kind: model.kind(path), size, mtime: 1792368000000 - i * 86400000 }));
13
+ const api = window.gezel || window.makeDemoGezel({ page: { typeName: 'Media Library' }, existingPaths: samples.map(file => file.path) });
14
+ const demoMode = api.page.mode === 'demo';
15
+ const components = api.ui.components;
16
+ const componentCards = [];
17
+ const mountAdder = () => components.fileAdder('#add-files', { target: 'library', onAdded: () => refresh() });
18
+ let adder = mountAdder();
15
19
  let files = [], shown = [], visible = 24, view = 'grid', busy = false, again = false, stopped = false, debounce, currentPath = null, previewRevision = 0, focusPath = null;
16
20
  const watches = new Map();
17
- const cards = new Map();
18
21
  function element(tag, text, className) { const el = document.createElement(tag); if (text !== undefined) el.textContent = text; if (className) el.className = className; return el; }
19
22
  function icon(type) {
20
23
  const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
@@ -29,7 +32,7 @@
29
32
  return `${new Intl.NumberFormat(undefined, { maximumFractionDigits: 1 }).format(n)} ${units[i]}`;
30
33
  }
31
34
  const stamp = (value) => value ? new Date(value).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }) : 'Date unavailable';
32
- const mediaUrl = (file) => demoMode ? `assets/${file.asset}` : api.data.url(file.path);
35
+ const mediaUrl = (file) => demoMode && file.asset ? `assets/${file.asset}` : api.data.url(file.path);
33
36
  function stopPlayback() {
34
37
  previewRevision++;
35
38
  for (const media of $('preview-media').querySelectorAll('video,audio')) { media.pause(); media.removeAttribute('src'); media.load(); }
@@ -37,30 +40,15 @@
37
40
  }
38
41
  function card(file) {
39
42
  const li = document.createElement('li');
40
- const button = element('button', undefined, 'media-card'); button.type = 'button';
41
- button.dataset.path = file.path; button.setAttribute('aria-label', `Preview ${file.path}`);
42
- const thumb = element('span', undefined, 'thumbnail');
43
- thumb.append(icon(file.kind), element('span', labels[file.kind], 'fallback-label'));
44
- if (file.kind === 'image' && model.browserImages.has(model.extension(file.path))) {
45
- const image = document.createElement('img'); image.alt = ''; image.loading = 'lazy'; image.decoding = 'async';
46
- image.addEventListener('error', () => { image.remove(); thumb.querySelector('.fallback-label').textContent = 'Preview unavailable'; });
47
- try { image.src = mediaUrl(file); thumb.append(image); } catch { /* Keep the usable file card. */ }
48
- }
49
- const info = element('span', undefined, 'card-info');
50
- info.append(element('span', file.path.split('/').pop(), 'file-name'), element('span', model.folderOf(file.path) === '.' ? 'Project folder' : model.folderOf(file.path), 'file-folder'), element('span', `${model.extension(file.path).toUpperCase()} · ${size(file.size)}`, 'file-meta'));
51
- button.append(thumb, info); button.addEventListener('click', () => openPreview(file.path)); li.append(button); return li;
43
+ const card = components.fileCard(li, { file: { ...file, source: 'workspace' }, variant: view === 'list' ? 'row' : 'tile', thumbnail: file.kind === 'image' && model.browserImages.has(model.extension(file.path)) ? mediaUrl(file) : false, onActivate: () => openPreview(file.path) });
44
+ card.element.dataset.path = file.path; componentCards.push(card); return li;
52
45
  }
53
46
  function render() {
54
47
  const previousFocus = document.activeElement?.dataset.path;
55
48
  shown = model.select(files, { query: $('search').value, kind: $('kind').value, folder: $('folder').value, sort: $('sort').value });
56
49
  $('gallery').dataset.view = view;
57
- const present = new Set(files.map((file) => file.path));
58
- for (const path of cards.keys()) if (!present.has(path)) cards.delete(path);
59
- $('gallery').replaceChildren(...shown.slice(0, visible).map((file) => {
60
- const stamp = `${file.size}:${file.mtime}`;
61
- if (cards.get(file.path)?.stamp !== stamp) cards.set(file.path, { stamp, node: card(file) });
62
- return cards.get(file.path).node;
63
- }));
50
+ componentCards.splice(0).forEach(card => card.destroy());
51
+ $('gallery').replaceChildren(...shown.slice(0, visible).map(card));
64
52
  for (const type of ['image', 'video', 'audio']) $(`${type}-count`).textContent = files.filter((f) => f.kind === type).length.toLocaleString();
65
53
  $('file-count').textContent = `${files.length.toLocaleString()} media file${files.length === 1 ? '' : 's'}`;
66
54
  $('status').textContent = shown.length > visible ? `Showing ${visible} of ${shown.length.toLocaleString()} matches` : `${shown.length.toLocaleString()} of ${files.length.toLocaleString()} media files`;
@@ -68,7 +56,7 @@
68
56
  $('no-matches').hidden = files.length === 0 || shown.length > 0;
69
57
  $('show-more').hidden = shown.length <= visible;
70
58
  $('view-grid').setAttribute('aria-pressed', String(view === 'grid')); $('view-list').setAttribute('aria-pressed', String(view === 'list'));
71
- if (previousFocus) [...$('gallery').querySelectorAll('button')].find((el) => el.dataset.path === previousFocus)?.focus();
59
+ if (previousFocus) [...$('gallery').querySelectorAll('gezel-file-card')].find((el) => el.dataset.path === previousFocus)?.shadowRoot.querySelector('button').focus();
72
60
  }
73
61
  function openPreview(path) {
74
62
  const file = files.find((f) => f.path === path); if (!file) return;
@@ -115,7 +103,7 @@
115
103
  if (busy) { again = true; return; } busy = true; $('refresh').disabled = true;
116
104
  $('gallery').setAttribute('aria-busy', 'true'); message('error', '');
117
105
  try {
118
- const result = demoMode ? { files: samples, folders: ['.', 'Journeys', 'Studio', 'Motion', 'Recordings'] } : await model.scan((path) => api.data.list(path), { cancelled: () => stopped });
106
+ const result = demoMode ? { files: samples.concat(components.demo.files().map(file => ({ ...file, kind: model.kind(file.path) }))), folders: ['.', 'Journeys', 'Studio', 'Motion', 'Recordings'] } : await model.scan((path) => api.data.list(path), { cancelled: () => stopped });
119
107
  if (stopped) return;
120
108
  files = result.files;
121
109
  const chosen = $('folder').value;
@@ -142,9 +130,9 @@
142
130
  for (const style of ['grid', 'list']) $(`view-${style}`).addEventListener('click', () => { view = style; render(); });
143
131
  $('show-more').addEventListener('click', () => { visible += 24; render(); });
144
132
  $('refresh').addEventListener('click', () => { void refresh(); });
145
- $('reset-demo').addEventListener('click', () => { $('search').value = ''; $('kind').value = ''; $('folder').value = ''; $('sort').value = 'newest'; visible = 24; view = 'grid'; $('preview').close(); void refresh(); });
133
+ $('reset-demo').addEventListener('click', () => { components.demo.reset(); adder.destroy(); adder = mountAdder(); $('search').value = ''; $('kind').value = ''; $('folder').value = ''; $('sort').value = 'newest'; visible = 24; view = 'grid'; $('preview').close(); void refresh(); });
146
134
  $('close-preview').addEventListener('click', () => $('preview').close());
147
- $('preview').addEventListener('close', () => { stopPlayback(); currentPath = null; [...$('gallery').querySelectorAll('button')].find((el) => el.dataset.path === focusPath)?.focus(); });
135
+ $('preview').addEventListener('close', () => { stopPlayback(); currentPath = null; [...$('gallery').querySelectorAll('gezel-file-card')].find((el) => el.dataset.path === focusPath)?.shadowRoot.querySelector('button').focus(); });
148
136
  $('previous').addEventListener('click', () => move(-1)); $('next').addEventListener('click', () => move(1));
149
137
  $('preview').addEventListener('keydown', (e) => {
150
138
  if (['VIDEO', 'AUDIO', 'INPUT', 'SELECT'].includes(e.target.tagName)) return;
@@ -160,6 +148,6 @@
160
148
  theme(api.ui.theme); const stopTheme = api.ui.onTheme(theme);
161
149
  const timer = demoMode ? null : setInterval(() => { if (document.visibilityState !== 'hidden') void refresh(); }, 30000);
162
150
  document.addEventListener('visibilitychange', () => { if (!demoMode && document.visibilityState !== 'hidden') schedule(); });
163
- window.addEventListener('pagehide', () => { stopped = true; clearTimeout(debounce); clearInterval(timer); stopPlayback(); stopTheme(); for (const stop of watches.values()) stop(); }, { once: true });
151
+ window.addEventListener('pagehide', () => { stopped = true; adder.destroy(); componentCards.forEach(card => card.destroy()); clearTimeout(debounce); clearInterval(timer); stopPlayback(); stopTheme(); for (const stop of watches.values()) stop(); }, { once: true });
164
152
  void refresh();
165
153
  })();
@@ -6,101 +6,14 @@
6
6
  <meta name="gezel-demo" content="standalone" />
7
7
  <title>Media Library</title>
8
8
  <link rel="stylesheet" href="gallery.css" />
9
- <script>
10
- /* gezel-page-demo-stub v1 */
11
- /*
12
- * The canonical demo stub for Gezel type pages. MIT License.
13
- *
14
- * Paste this file's bytes verbatim into a <script> block ahead of the
15
- * page's own code, then call makeDemoGezel({ page, tools, data }) with the
16
- * page's demo handlers. Served by Gezel, the real window.gezel is already
17
- * injected and the stub steps aside; opened as a raw file (the live demos
18
- * on gezelgilde.com), it installs a window.gezel whose tools and data are
19
- * the page-supplied handler maps — one code path in every mode. `tools`
20
- * and `data` map a tool name / declared read path to a function; data
21
- * handlers back read() and list(), and url() returns the handler's value
22
- * or passes a data: URI through. See docs/page-authoring.md.
23
- */
24
- (function () {
25
- 'use strict';
26
- window.makeDemoGezel = function (opts) {
27
- if (window.gezel) return window.gezel;
28
- var page = (opts && opts.page) || {};
29
- var tools = (opts && opts.tools) || {};
30
- var data = (opts && opts.data) || {};
31
- var themeListeners = [];
32
- var media = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null;
33
- var ui = {
34
- theme: { mode: media && media.matches ? 'dark' : 'light' },
35
- onTheme: function (cb) {
36
- themeListeners.push(cb);
37
- return function () {
38
- var at = themeListeners.indexOf(cb);
39
- if (at >= 0) themeListeners.splice(at, 1);
40
- };
41
- },
42
- };
43
- if (media && media.addEventListener) {
44
- media.addEventListener('change', function (ev) {
45
- ui.theme = { mode: ev.matches ? 'dark' : 'light' };
46
- themeListeners.slice().forEach(function (cb) { cb(ui.theme); });
47
- });
48
- }
49
- function fail(code, message) {
50
- var err = new Error(message);
51
- err.code = code;
52
- return Promise.reject(err);
53
- }
54
- function run(map, key, arg) {
55
- if (!Object.prototype.hasOwnProperty.call(map, key)) return null;
56
- return Promise.resolve().then(function () { return map[key](arg); });
57
- }
58
- window.gezel = {
59
- page: {
60
- api: 1,
61
- projectId: 'demo',
62
- source: 'type',
63
- entry: page.entry || 'index.html',
64
- typeName: page.typeName || 'demo',
65
- params: page.params || {},
66
- mode: 'demo',
67
- },
68
- tools: {
69
- list: function () { return Promise.resolve(Object.keys(tools)); },
70
- invoke: function (name, input) {
71
- var call = run(tools, name, input || {});
72
- if (!call) return fail('not-allowed', "The demo does not handle the tool '" + name + "'.");
73
- return call.then(function (output) { return { output: output }; });
74
- },
75
- },
76
- data: {
77
- read: function (path, o) {
78
- return run(data, path, o || {}) || fail('not-allowed', "The demo has no data for '" + path + "'.");
79
- },
80
- list: function (path, o) {
81
- return run(data, path, o || {}) || Promise.resolve([]);
82
- },
83
- watch: function () { return function () {}; },
84
- url: function (path) {
85
- if (Object.prototype.hasOwnProperty.call(data, path)) return String(data[path]({}));
86
- return typeof path === 'string' && path.indexOf('data:') === 0 ? path : '';
87
- },
88
- },
89
- ui: ui,
90
- refresh: function () {},
91
- };
92
- return window.gezel;
93
- };
94
- })();
95
-
96
- </script>
97
- <script src="media-files.js" defer></script>
9
+ <script src="media-files.js" defer></script>
98
10
  <script src="gallery.js" defer></script>
99
11
  </head>
100
12
  <body>
101
13
  <main>
102
14
  <aside data-gezel-demo-banner hidden><span>Sample library. Illustrations, a short clip, and three notes — all local to this page.</span><button id="reset-demo" type="button">Reset demo</button></aside>
103
15
  <header><div><p class="eyebrow">Your collection, in place</p><h1>Media Library</h1><p class="intro">A window into your photos, films, and recordings.</p></div><button id="refresh" type="button">Refresh</button></header>
16
+ <div id="add-files"></div>
104
17
  <dl class="counts" aria-label="Library summary"><div><dt>Images</dt><dd id="image-count">—</dd></div><div><dt>Videos</dt><dd id="video-count">—</dd></div><div><dt>Audio</dt><dd id="audio-count">—</dd></div></dl>
105
18
  <section aria-labelledby="library-heading">
106
19
  <div class="library-heading"><h2 id="library-heading">Browse your library</h2><span id="updated"></span></div>
@@ -117,7 +30,7 @@
117
30
  <div id="empty" class="empty" hidden><h2>Your library starts here</h2><p>Choose a media folder when creating this project, or set its folder in project settings. Images, videos, and audio in that folder and its subfolders appear automatically.</p></div>
118
31
  <p id="no-matches" class="empty" hidden>No media matches these filters. Try another name, folder, or media type.</p>
119
32
  </section>
120
- <footer><span id="file-count"></span> · Originals stay in your folder. This gallery only reads them. Playback depends on the formats supported by your device.</footer>
33
+ <footer><span id="file-count"></span> · Originals stay in your folder. Add files copies selected files into this folder. Playback depends on the formats supported by your device.</footer>
121
34
  </main>
122
35
  <dialog id="preview" aria-labelledby="preview-title">
123
36
  <div class="preview-heading"><div><h2 id="preview-title"></h2><p id="preview-path" class="muted"></p></div><button id="close-preview" type="button" autofocus>Close</button></div>