@0m0g1/griot 0.1.13 → 0.1.15

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0m0g1/griot",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "A self-contained block-based rich text editor and renderer built for historical document authoring.",
5
5
  "type": "module",
6
6
  "main": "./src/Griot.js",
@@ -4,11 +4,14 @@
4
4
  //
5
5
  // Usage:
6
6
  // import { renderGallery } from './GalleryRenderer.js';
7
- // const el = renderGallery(items, 'carousel');
7
+ // const el = renderGallery(items, 'carousel'); // loops by default
8
+ // const el = renderGallery(items, 'carousel', { loop: false }); // no loop
8
9
  // container.appendChild(el);
9
10
  //
10
11
  // items shape: { src?, url?, alt?, alt_text?, caption? }[]
11
12
  // layouts: 'grid' | 'masonry' | 'carousel' | 'strip'
13
+ // options:
14
+ // loop boolean (carousel only) whether prev/next wraps around. default: true
12
15
  // ─────────────────────────────────────────────────────────────────────────────
13
16
 
14
17
  import { lightbox } from './Lightbox.js';
@@ -21,12 +24,15 @@ const VALID_LAYOUTS = new Set(['grid', 'masonry', 'carousel', 'strip']);
21
24
  * Render a gallery element.
22
25
  * @param {object[]} items
23
26
  * @param {'grid'|'masonry'|'carousel'|'strip'} layout
27
+ * @param {{ loop?: boolean }} [options]
24
28
  * @returns {HTMLElement}
25
29
  */
26
- export function renderGallery(items = [], layout = 'grid') {
30
+ export function renderGallery(items = [], layout = 'grid', options = {}) {
27
31
  _injectStyles();
28
32
 
29
33
  const l = VALID_LAYOUTS.has(layout) ? layout : 'grid';
34
+ const opts = { loop: true, ...options };
35
+
30
36
  const wrap = document.createElement('div');
31
37
  wrap.className = `griot-gallery griot-gallery--${l}`;
32
38
  wrap.dataset.layout = l;
@@ -40,7 +46,7 @@ export function renderGallery(items = [], layout = 'grid') {
40
46
  }
41
47
 
42
48
  switch (l) {
43
- case 'carousel': return _carousel(items, wrap);
49
+ case 'carousel': return _carousel(items, wrap, opts);
44
50
  case 'masonry': return _masonry(items, wrap);
45
51
  case 'strip': return _strip(items, wrap);
46
52
  default: return _grid(items, wrap);
@@ -113,16 +119,10 @@ function _itemEl(item, index, allItems) {
113
119
 
114
120
  // ── Carousel ──────────────────────────────────────────────────────────────────
115
121
 
116
- function _carousel(items, wrap) {
122
+ function _carousel(items, wrap, opts) {
123
+ const loop = opts.loop !== false; // default true
117
124
  let idx = 0;
118
125
 
119
- // ── DOM structure ────────────────────────────────────────────────────────
120
- // .griot-carousel__viewport (clips)
121
- // .griot-carousel__track (slides)
122
- // .griot-carousel__slide × N
123
- // .griot-carousel__controls (prev · counter · next)
124
- // .griot-carousel__dots (dot buttons, hidden if > 12 items)
125
-
126
126
  const viewport = document.createElement('div');
127
127
  viewport.className = 'griot-carousel__viewport';
128
128
 
@@ -140,8 +140,6 @@ function _carousel(items, wrap) {
140
140
  img.decoding = 'async';
141
141
  img.draggable = false;
142
142
 
143
- // Click on carousel image → open lightbox at CURRENT idx (not i, since user
144
- // may have navigated away from the first image)
145
143
  img.addEventListener('click', () => lightbox.open(items, idx));
146
144
 
147
145
  slide.appendChild(img);
@@ -185,8 +183,17 @@ function _carousel(items, wrap) {
185
183
 
186
184
  // ── Navigation logic ──────────────────────────────────────────────────────
187
185
 
186
+ function canGoPrev() { return loop || idx > 0; }
187
+ function canGoNext() { return loop || idx < items.length - 1; }
188
+
188
189
  function goTo(n, animate = true) {
189
- idx = Math.max(0, Math.min(n, items.length - 1));
190
+ if (loop) {
191
+ // wrap around in both directions
192
+ idx = ((n % items.length) + items.length) % items.length;
193
+ } else {
194
+ // clamp to valid range
195
+ idx = Math.max(0, Math.min(n, items.length - 1));
196
+ }
190
197
 
191
198
  if (!animate) {
192
199
  track.style.transition = 'none';
@@ -196,10 +203,13 @@ function _carousel(items, wrap) {
196
203
  track.style.transform = `translateX(-${idx * 100}%)`;
197
204
  counter.textContent = `${idx + 1} / ${items.length}`;
198
205
 
199
- prevBtn.disabled = items.length <= 1;
200
- nextBtn.disabled = items.length <= 1;
201
- prevBtn.classList.toggle('is-edge', idx === 0);
202
- nextBtn.classList.toggle('is-edge', idx === items.length - 1);
206
+ // In loop mode buttons are always enabled (single-item galleries still hide them)
207
+ prevBtn.disabled = items.length <= 1 || (!loop && idx === 0);
208
+ nextBtn.disabled = items.length <= 1 || (!loop && idx === items.length - 1);
209
+
210
+ // edge class only meaningful when not looping
211
+ prevBtn.classList.toggle('is-edge', !loop && idx === 0);
212
+ nextBtn.classList.toggle('is-edge', !loop && idx === items.length - 1);
203
213
 
204
214
  dotEls.forEach((d, i) => {
205
215
  d.classList.toggle('is-active', i === idx);
@@ -207,10 +217,10 @@ function _carousel(items, wrap) {
207
217
  });
208
218
  }
209
219
 
210
- prevBtn.addEventListener('click', () => goTo(idx - 1));
211
- nextBtn.addEventListener('click', () => goTo(idx + 1));
220
+ prevBtn.addEventListener('click', () => { if (canGoPrev()) goTo(idx - 1); });
221
+ nextBtn.addEventListener('click', () => { if (canGoNext()) goTo(idx + 1); });
212
222
 
213
- // Touch / swipe on the viewport
223
+ // Touch / swipe
214
224
  let touchX = 0, touchY = 0, isScrolling = null;
215
225
 
216
226
  viewport.addEventListener('touchstart', e => {
@@ -230,14 +240,18 @@ function _carousel(items, wrap) {
230
240
  viewport.addEventListener('touchend', e => {
231
241
  if (isScrolling) return;
232
242
  const dx = e.changedTouches[0].clientX - touchX;
233
- if (Math.abs(dx) > 40) goTo(dx < 0 ? idx + 1 : idx - 1);
243
+ if (Math.abs(dx) > 40) {
244
+ const dir = dx < 0 ? 1 : -1;
245
+ if (dir === -1 && canGoPrev()) goTo(idx - 1);
246
+ if (dir === 1 && canGoNext()) goTo(idx + 1);
247
+ }
234
248
  }, { passive: true });
235
249
 
236
250
  // Keyboard when carousel has focus
237
251
  wrap.tabIndex = 0;
238
252
  wrap.addEventListener('keydown', e => {
239
- if (e.key === 'ArrowLeft') { e.preventDefault(); goTo(idx - 1); }
240
- if (e.key === 'ArrowRight') { e.preventDefault(); goTo(idx + 1); }
253
+ if (e.key === 'ArrowLeft' && canGoPrev()) { e.preventDefault(); goTo(idx - 1); }
254
+ if (e.key === 'ArrowRight' && canGoNext()) { e.preventDefault(); goTo(idx + 1); }
241
255
  });
242
256
 
243
257
  // Accessibility
@@ -250,7 +264,7 @@ function _carousel(items, wrap) {
250
264
 
251
265
  wrap.append(viewport, controls, dots);
252
266
 
253
- goTo(0, false); // initial position, no animation
267
+ goTo(0, false);
254
268
  return wrap;
255
269
  }
256
270
 
@@ -17,6 +17,8 @@ const FORMATS = [
17
17
  { key: 'italic', label: 'I', title: 'Italic (Ctrl+I)', syntax: '*' },
18
18
  { key: 'underline', label: 'U', title: 'Underline (Ctrl+U)', syntax: '__' },
19
19
  { key: 'strike', label: 'S̶', title: 'Strikethrough', syntax: '~~' },
20
+ { key: 'sup', label: 'x²', title: 'Superscript', syntax: '^' },
21
+ { key: 'sub', label: 'x₂', title: 'Subscript', syntax: '~' },
20
22
  { key: 'code', label: '`', title: 'Inline Code', syntax: '`' },
21
23
  { key: 'highlight', label: '▐', title: 'Highlight', syntax: '==' },
22
24
  { key: 'link', label: '🔗', title: 'Link', action: 'link' },
@@ -7,6 +7,8 @@
7
7
  // *italic* → TOKEN.ITALIC { text }
8
8
  // __underline__ → TOKEN.UNDERLINE { text }
9
9
  // ~~strikethrough~~ → TOKEN.STRIKE { text }
10
+ // ^superscript^ → TOKEN.SUPER { text }
11
+ // ~subscript~ → TOKEN.SUB { text }
10
12
  // `inline code` → TOKEN.CODE { code }
11
13
  // ==highlight== → TOKEN.HIGHLIGHT { text }
12
14
  // {#f00:red text} {blue:text} → TOKEN.COLOR_MARK { color, text }
@@ -25,6 +27,8 @@ export const TOKEN = Object.freeze({
25
27
  ITALIC: 'italic',
26
28
  UNDERLINE: 'underline',
27
29
  STRIKE: 'strike',
30
+ SUPER: 'super',
31
+ SUB: 'sub',
28
32
  CODE: 'code',
29
33
  LINK: 'link',
30
34
  IMAGE: 'image',
@@ -45,8 +49,12 @@ const RULES = [
45
49
  { type: TOKEN.ITALIC, re: /^\*((?:[^*])+)\*/, build: m => ({ text: m[1] }) },
46
50
  // Underline __text__
47
51
  { type: TOKEN.UNDERLINE, re: /^__((?:[^_])+)__/, build: m => ({ text: m[1] }) },
48
- // Strikethrough ~~text~~
52
+ // Strikethrough ~~text~~ (must come before single ~ subscript)
49
53
  { type: TOKEN.STRIKE, re: /^~~((?:[^~])+)~~/, build: m => ({ text: m[1] }) },
54
+ // Subscript ~text~
55
+ { type: TOKEN.SUB, re: /^~((?:[^~])+)~/, build: m => ({ text: m[1] }) },
56
+ // Superscript ^text^
57
+ { type: TOKEN.SUPER, re: /^\^((?:[^^])+)\^/, build: m => ({ text: m[1] }) },
50
58
  // Highlight ==text==
51
59
  { type: TOKEN.HIGHLIGHT, re: /^==((?:[^=])+)==/, build: m => ({ text: m[1] }) },
52
60
  // Colour mark {#hex:text} or {colorname:text}
@@ -45,6 +45,18 @@ function _toNode(t, opts) {
45
45
  el.textContent = t.text;
46
46
  return el;
47
47
  }
48
+ case TOKEN.SUPER: {
49
+ const el = document.createElement('sup');
50
+ el.className = 'griot-sup';
51
+ el.textContent = t.text;
52
+ return el;
53
+ }
54
+ case TOKEN.SUB: {
55
+ const el = document.createElement('sub');
56
+ el.className = 'griot-sub';
57
+ el.textContent = t.text;
58
+ return el;
59
+ }
48
60
  case TOKEN.HIGHLIGHT: {
49
61
  const el = document.createElement('mark');
50
62
  el.className = 'griot-highlight';
@@ -119,6 +131,8 @@ function _toHTML(t) {
119
131
  case TOKEN.ITALIC: return `<em>${escHtml(t.text)}</em>`;
120
132
  case TOKEN.UNDERLINE: return `<u class="griot-underline">${escHtml(t.text)}</u>`;
121
133
  case TOKEN.STRIKE: return `<s class="griot-strike">${escHtml(t.text)}</s>`;
134
+ case TOKEN.SUPER: return `<sup class="griot-sup">${escHtml(t.text)}</sup>`;
135
+ case TOKEN.SUB: return `<sub class="griot-sub">${escHtml(t.text)}</sub>`;
122
136
  case TOKEN.HIGHLIGHT: return `<mark class="griot-highlight">${escHtml(t.text)}</mark>`;
123
137
  case TOKEN.COLOR_MARK: return `<span class="griot-color-mark" style="color:${escAttr(t.color)}">${escHtml(t.text)}</span>`;
124
138
  case TOKEN.CODE: return `<code class="griot-inline-code">${escHtml(t.code)}</code>`;