motimeline 2.3.0 → 2.5.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.
package/README.md CHANGED
@@ -4,6 +4,7 @@ Responsive two-column timeline layout library — plain JavaScript, zero depende
4
4
 
5
5
  **[Live demo & docs → mattopen.github.io/moTimeline](https://mattopen.github.io/moTimeline/)**
6
6
 
7
+ [![npm](https://img.shields.io/npm/v/motimeline.svg)](https://www.npmjs.com/package/motimeline)
7
8
  [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
8
9
 
9
10
  [![Preview](images/preview1.PNG)](https://mattopen.github.io/moTimeline/)
@@ -97,12 +98,42 @@ import 'motimeline/dist/moTimeline.css';
97
98
 
98
99
  | Option | Type | Default | Description |
99
100
  |---|---|---|---|
100
- | `columnCount` | object | `{xs:1, sm:2, md:2, lg:2}` | Columns at each breakpoint (xs <600 px, sm <992 px, md <1200 px, lg ≥1200 px) |
101
- | `badgeShow` | boolean | `false` | Show numbered badges on the center line |
102
- | `arrowShow` | boolean | `false` | Show triangle arrows pointing from each card toward the center line |
103
- | `theme` | boolean | `false` | Enable the built-in card theme (banners, avatars, styled badges) |
104
- | `showCounter` | boolean | `true` | Render the badge number. `false` keeps the badge for layout but sets it transparent |
105
- | `showCounterStyle` | string | `'counter'` | `'counter'` shows the item number; `'image'` shows the icon from `data-mo-icon` on the `<li>`, or a built-in SVG fallback |
101
+ | `columnCount` | object | `{xs:1, sm:2, md:2, lg:2}` | Columns at each responsive breakpoint: `xs` < 600 px · `sm` < 992 px · `md` < 1 200 px · `lg`1 200 px. Set any key to `1` to force single-column at that width. The center line, badges, and arrows are only visible in two-column mode. |
102
+ | `badgeShow` | boolean | `false` | Render a circular badge on the center line for every item, numbered sequentially. Badges are automatically hidden when single-column mode is active. |
103
+ | `arrowShow` | boolean | `false` | Render a triangle arrow pointing from each card toward the center line. Automatically hidden in single-column mode. |
104
+ | `theme` | boolean | `false` | Enable the built-in card theme: white cards with drop shadow, full-width image banners (160 px), overlapping circular avatars, and styled badges. Adds `mo-theme` to the container — can also be set manually in HTML. |
105
+ | `showCounterStyle` | string | `'counter'` | `'counter'` sequential item number (1, 2, 3…). `'image'` image from `data-mo-icon` on the `<li>`; falls back to a built-in flat SVG dot if the attribute is absent. `'none'` — badge element is created (preserving center-line spacing) but rendered with `opacity: 0`. |
106
+
107
+ ---
108
+
109
+ ## Data attributes
110
+
111
+ | Attribute | Element | Description |
112
+ |---|---|---|
113
+ | `data-mo-icon` | `<li>` | URL of the image shown inside the badge when `showCounterStyle: 'image'`. Accepts any web-safe format including inline SVG data URIs. Falls back to a built-in SVG icon if absent. Also set automatically by `addItems()` when an `icon` field is provided. |
114
+
115
+ ---
116
+
117
+ ## CSS classes reference
118
+
119
+ | Class | Applied to | Description |
120
+ |---|---|---|
121
+ | `mo-timeline` | container `<ul>` | Core layout class. Added automatically on init; safe to add in HTML before init. |
122
+ | `mo-twocol` | container | Present when two-column mode is active. Triggers the center vertical line and badge/arrow positioning. |
123
+ | `mo-theme` | container | Activates the built-in card theme. Added by `theme: true` or set manually. |
124
+ | `mo-item` | `<li>` | Applied to every timeline item. Controls 50 % width and float direction. |
125
+ | `mo-inverted` | `<li>` | Added to right-column items. Flips float, badge, arrow, and avatar positions. |
126
+ | `mo-offset` | `<li>` | Added when a badge would overlap the previous badge — nudges badge and arrow down to avoid collision. |
127
+ | `mo-badge` | `<span>` | Badge circle on the center line. Style via CSS custom properties. |
128
+ | `mo-badge-icon` | `<img>` inside badge | Image inside the badge when `showCounterStyle: 'image'`. |
129
+ | `mo-arrow` | `<span>` | Triangle arrow pointing from the card toward the center line. |
130
+ | `mo-card` | `<div>` | Card wrapper. Shadow, border-radius, and margins when `mo-theme` is active. |
131
+ | `mo-card-image` | `<div>` | Optional image container inside a card. Required for the avatar-over-banner overlap. |
132
+ | `mo-banner` | `<img>` | Full-width banner image at the top of a themed card. |
133
+ | `mo-avatar` | `<img>` | Circular avatar overlapping the bottom of the banner. Mirrors position on right-column items. |
134
+ | `mo-card-body` | `<div>` | Text content area. Padding and typography when `mo-theme` is active. |
135
+ | `mo-meta` | `<p>` | Date / subtitle line inside a card body. Muted colour, smaller font. |
136
+ | `js-mo-item` · `js-mo-inverted` | `<li>` | JS-only selector mirrors of `mo-item` / `mo-inverted`. Use in your own JS queries to avoid coupling to styling class names. |
106
137
 
107
138
  ---
108
139
 
@@ -111,13 +142,178 @@ import 'motimeline/dist/moTimeline.css';
111
142
  ```js
112
143
  const tl = new MoTimeline(elementOrSelector, options);
113
144
 
114
- tl.refresh(); // re-layout all items (called automatically on resize)
115
- tl.initNewItems(); // pick up newly appended <li> elements
116
- tl.destroy(); // remove listeners and reset DOM classes
145
+ tl.refresh(); // re-layout all items (called automatically on resize)
146
+ tl.initNewItems(); // pick up manually appended <li> elements
147
+ tl.addItems(items); // create and append <li> from an array of item objects (or JSON string)
148
+ tl.destroy(); // remove listeners and reset DOM classes
149
+ ```
150
+
151
+ ### addItems — item schema
152
+
153
+ ```js
154
+ tl.addItems([
155
+ {
156
+ title: "Project kickoff", // <h3> heading
157
+ meta: "January 2024", // date / subtitle line
158
+ text: "Kicked off the roadmap.", // body paragraph
159
+ banner: "images/banner.jpg", // img.mo-banner (optional)
160
+ avatar: "images/avatar.jpg", // img.mo-avatar (optional)
161
+ icon: "images/icon.svg" // data-mo-icon on <li>, used by showCounterStyle:'image'
162
+ },
163
+ ]);
164
+
165
+ // A JSON string is also accepted:
166
+ tl.addItems('[{"title":"From JSON","meta":"Today","text":"Parsed automatically."}]');
117
167
  ```
118
168
 
119
169
  ---
120
170
 
171
+ ## React
172
+
173
+ moTimeline manipulates the DOM directly, so use a `useRef` + `useEffect` wrapper to bridge it with React's rendering. Save the snippet below as `Timeline.jsx`:
174
+
175
+ ```jsx
176
+ import { useEffect, useRef } from 'react';
177
+ import MoTimeline from 'motimeline';
178
+ import 'motimeline/dist/moTimeline.css';
179
+
180
+ /**
181
+ * items shape: [{ id, title, meta, text, banner, avatar, icon }]
182
+ * All item fields are optional except a stable `id` for React keys.
183
+ */
184
+ export default function Timeline({ items = [], options = {} }) {
185
+ const ulRef = useRef(null);
186
+ const tlRef = useRef(null);
187
+ const lenRef = useRef(0);
188
+
189
+ // Initialise once on mount
190
+ useEffect(() => {
191
+ tlRef.current = new MoTimeline(ulRef.current, options);
192
+ lenRef.current = items.length;
193
+ return () => tlRef.current?.destroy();
194
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
195
+
196
+ // When items array grows, pick up the new <li> elements React just rendered
197
+ useEffect(() => {
198
+ if (!tlRef.current) return;
199
+ if (items.length > lenRef.current) {
200
+ tlRef.current.initNewItems();
201
+ } else {
202
+ // Items removed or reordered — full reinit
203
+ tlRef.current.destroy();
204
+ tlRef.current = new MoTimeline(ulRef.current, options);
205
+ }
206
+ lenRef.current = items.length;
207
+ }, [items]); // eslint-disable-line react-hooks/exhaustive-deps
208
+
209
+ return (
210
+ <ul ref={ulRef}>
211
+ {items.map((item) => (
212
+ <li key={item.id} {...(item.icon && { 'data-mo-icon': item.icon })}>
213
+ <div className="mo-card">
214
+ {item.banner && (
215
+ <div className="mo-card-image">
216
+ <img className="mo-banner" src={item.banner} alt="" />
217
+ {item.avatar && <img className="mo-avatar" src={item.avatar} alt="" />}
218
+ </div>
219
+ )}
220
+ <div className="mo-card-body">
221
+ {item.title && <h3>{item.title}</h3>}
222
+ {item.meta && <p className="mo-meta">{item.meta}</p>}
223
+ {item.text && <p>{item.text}</p>}
224
+ </div>
225
+ </div>
226
+ </li>
227
+ ))}
228
+ </ul>
229
+ );
230
+ }
231
+ ```
232
+
233
+ ### Usage
234
+
235
+ ```jsx
236
+ import { useState } from 'react';
237
+ import Timeline from './Timeline';
238
+
239
+ export default function App() {
240
+ const [items, setItems] = useState([
241
+ { id: '1', title: 'Project kickoff', meta: 'Jan 2024', text: 'Team aligned on goals.' },
242
+ { id: '2', title: 'Design system', meta: 'Feb 2024', text: 'Component library shipped.',
243
+ banner: 'banner.jpg', avatar: 'avatar.jpg' },
244
+ ]);
245
+
246
+ const addItem = () =>
247
+ setItems(prev => [...prev, {
248
+ id: String(Date.now()),
249
+ title: 'New event',
250
+ meta: 'Just now',
251
+ text: 'Added dynamically from React state.',
252
+ }]);
253
+
254
+ return (
255
+ <>
256
+ <Timeline
257
+ items={items}
258
+ options={{ badgeShow: true, arrowShow: true, theme: true }}
259
+ />
260
+ <button onClick={addItem}>Add item</button>
261
+ </>
262
+ );
263
+ }
264
+ ```
265
+
266
+ > **How it works:** React renders the `<li>` elements. moTimeline initialises once on mount and reads the DOM. When the `items` array grows, `initNewItems()` picks up the new `<li>` nodes React just appended. When items are removed or reordered React re-renders the list and the instance is fully reinitialised.
267
+
268
+ ---
269
+
270
+ ## Infinite scroll recipe
271
+
272
+ moTimeline handles the layout — you own the data fetching. Wire an `IntersectionObserver` to a sentinel element below the list and call `addItems()` when it comes into view.
273
+
274
+ ```html
275
+ <!-- Place a sentinel element right after the <ul> -->
276
+ <ul id="my-timeline"></ul>
277
+ <div id="sentinel"></div>
278
+ ```
279
+
280
+ ```js
281
+ const tl = new MoTimeline('#my-timeline', { theme: true, badgeShow: true });
282
+ const sentinel = document.getElementById('sentinel');
283
+ let loading = false;
284
+ let page = 1;
285
+ let exhausted = false;
286
+
287
+ const observer = new IntersectionObserver(async (entries) => {
288
+ if (!entries[0].isIntersecting || loading || exhausted) return;
289
+
290
+ loading = true;
291
+ const items = await fetchPage(page); // your own async data fetch
292
+
293
+ if (items.length === 0) {
294
+ exhausted = true;
295
+ observer.disconnect();
296
+ } else {
297
+ tl.addItems(items); // moTimeline creates <li> and lays out
298
+ page++;
299
+ }
300
+ loading = false;
301
+ });
302
+
303
+ observer.observe(sentinel);
304
+
305
+ // Example fetch — replace with your real API call
306
+ async function fetchPage(page) {
307
+ const res = await fetch(`/api/events?page=${page}`);
308
+ const data = await res.json();
309
+ return data.items; // [{ title, meta, text, banner, avatar }, …]
310
+ }
311
+ ```
312
+
313
+ > `IntersectionObserver` is supported in all modern browsers with no polyfill needed. The `loading` flag prevents duplicate requests if the sentinel stays visible while a fetch is in flight. Set `exhausted = true` and disconnect when your API returns an empty page.
314
+
315
+ ---
316
+
121
317
  ## CSS custom properties
122
318
 
123
319
  ```css
@@ -157,8 +353,16 @@ No framework option needed. Wrap the `<ul>` inside a Bootstrap `.container`:
157
353
 
158
354
  ## Changelog
159
355
 
356
+ ### v2.5.0
357
+ - **Breaking:** `showCounter` (boolean) removed — replaced by `showCounterStyle: 'none'`, which preserves center-line spacing with an invisible badge
358
+ - `showCounterStyle` now accepts three values: `'counter'` · `'image'` · `'none'`
359
+
360
+ ### v2.4.0
361
+ - Added `addItems(items)` — creates and appends `<li>` elements from an array of item objects or a JSON string, then initializes them in one batch
362
+ - Badges and arrows now hidden in single-column mode (center-line elements have no meaning without a center line)
363
+
160
364
  ### v2.3.0
161
- - Added `showCounter` (opacity toggle) and `showCounterStyle` (`'counter'` | `'image'`) badge options
365
+ - Added `showCounterStyle` (`'counter'` | `'image'`) and `showCounter` opacity toggle (consolidated into `showCounterStyle: 'none'` in v2.5.0)
162
366
  - `data-mo-icon` attribute on `<li>` sets a custom icon in image mode; built-in flat SVG used as fallback
163
367
 
164
368
  ### v2.2.0
@@ -1,6 +1,6 @@
1
- "use strict";var b=Object.defineProperty;var E=(i,e,t)=>e in i?b(i,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):i[e]=t;var p=(i,e,t)=>E(i,typeof e!="symbol"?e+"":e,t);Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});/*!
2
- * moTimeline v2.3.0
1
+ "use strict";var _=Object.defineProperty;var b=(o,e,t)=>e in o?_(o,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):o[e]=t;var p=(o,e,t)=>b(o,typeof e!="symbol"?e+"":e,t);Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});/*!
2
+ * moTimeline v2.5.0
3
3
  * Responsive two-column timeline layout library
4
4
  * https://github.com/MattOpen/moTimeline
5
5
  * MIT License
6
- */const a=new WeakMap,w={columnCount:{xs:1,sm:2,md:2,lg:2},badgeShow:!1,arrowShow:!1,theme:!1,showCounter:!0,showCounterStyle:"counter"},I="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><circle cx='12' cy='12' r='11' fill='%234f46e5'/><circle cx='12' cy='12' r='4.5' fill='white'/></svg>";function v(){const i=window.innerWidth;return i<600?"xs":i<992?"sm":i<1200?"md":"lg"}function y(i,e=100){let t;return(...s)=>{clearTimeout(t),t=setTimeout(()=>i(...s),e)}}function m(i){return i?{o:i.offsetTop,h:i.offsetHeight,gppu:i.offsetTop+i.offsetHeight}:{o:0,h:0,gppu:0}}function _(i,e){const t=[];let s=i.previousElementSibling;for(;s;)(!e||s.matches(e))&&t.push(s),s=s.previousElementSibling;return t}const l=class l{constructor(e,t={}){if(typeof e=="string"&&(e=document.querySelector(e)),!e)throw new Error("moTimeline: element not found");this.element=e,this.settings=Object.assign({},w,t),this.settings.columnCount=Object.assign({},w.columnCount,t.columnCount),this._resizeHandler=y(()=>this.refresh(),100),this._initialized=!1,this.init()}init(){const e=this.element;if(a.has(e)){this.refresh();return}const t=Object.assign({},this.settings,{lastItemIdx:0});a.set(e,t),l.instances.add(this),e.classList.add("mo-timeline"),t.theme&&e.classList.add("mo-theme"),Array.from(e.children).length!==0&&(this._initItems(),this._initialized=!0,window.addEventListener("resize",this._resizeHandler))}refresh(){l.instances.forEach(e=>{const t=e.element,s=a.get(t);s&&(s.col=s.columnCount[v()],e._setDivider(),Array.from(t.children).forEach(o=>{e._setPostPosition(o)}))})}initNewItems(){this._initItems()}destroy(){window.removeEventListener("resize",this._resizeHandler),a.delete(this.element),l.instances.delete(this),this.element.classList.remove("mo-timeline","mo-theme","mo-twocol"),Array.from(this.element.children).forEach(e=>{e.classList.remove("mo-item","js-mo-item","mo-inverted","js-mo-inverted","mo-offset"),e.querySelectorAll(".js-mo-badge, .js-mo-arrow").forEach(t=>t.remove())})}_getData(){return a.get(this.element)}_setDivider(){const e=this._getData();e&&(e.col=e.columnCount[v()],this.element.classList.toggle("mo-twocol",e.col>1))}_initItems(){const e=this.element,t=this._getData();if(!t)return;const s=t.lastItemIdx,o=Array.from(e.children),n=o.slice(s);n.length!==0&&(n.forEach((r,c)=>{r.id||(r.id="moT"+crypto.randomUUID()+"_"+(c+s)),r.classList.add("mo-item","js-mo-item")}),this._setDivider(),n.forEach((r,c)=>{t.badgeShow&&this._createBadge(r,c+s+1),t.arrowShow&&this._createArrow(r)}),t.lastItemIdx=o.length,a.set(e,t),this.refresh())}_setPostPosition(e){const t=this._getLeftOrRight(e);t&&(e.classList.toggle("mo-inverted",t.lr>0),e.classList.toggle("js-mo-inverted",t.lr>0),e.classList.toggle("mo-offset",t.badge_offset>0))}_getLeftOrRight(e){if(!e)return null;const t=this._getData();if(!t)return null;const s=t.col,o=_(e,".js-mo-inverted")[0]||null,n=_(e,".js-mo-item:not(.js-mo-inverted)")[0]||null,r=m(n),c=m(o),u=m(e);let h=0,f=0;if(s>1){r.gppu>u.o&&(h=1),c.gppu>r.gppu&&(h=0);const g=e.previousElementSibling;g&&Math.abs(u.o-m(g).o)<40&&(f=1)}return{lr:h,badge_offset:f}}_createBadge(e,t){const s=this._getData(),o=document.createElement("span");if(o.className="mo-badge js-mo-badge",s.showCounter||(o.style.opacity="0"),s.showCounterStyle==="image"){const n=document.createElement("img");n.className="mo-badge-icon",n.alt="",n.src=e.dataset.moIcon||I,o.appendChild(n)}else o.textContent=t;e.prepend(o)}_createArrow(e){const t=document.createElement("span");t.className="mo-arrow js-mo-arrow",e.prepend(t)}};p(l,"instances",new Set);let d=l;exports.MoTimeline=d;exports.default=d;
6
+ */const c=new WeakMap,v={columnCount:{xs:1,sm:2,md:2,lg:2},badgeShow:!1,arrowShow:!1,theme:!1,showCounterStyle:"counter"},C="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><circle cx='12' cy='12' r='11' fill='%234f46e5'/><circle cx='12' cy='12' r='4.5' fill='white'/></svg>";function w(){const o=window.innerWidth;return o<600?"xs":o<992?"sm":o<1200?"md":"lg"}function I(o,e=100){let t;return(...s)=>{clearTimeout(t),t=setTimeout(()=>o(...s),e)}}function m(o){return o?{o:o.offsetTop,h:o.offsetHeight,gppu:o.offsetTop+o.offsetHeight}:{o:0,h:0,gppu:0}}function E(o,e){const t=[];let s=o.previousElementSibling;for(;s;)(!e||s.matches(e))&&t.push(s),s=s.previousElementSibling;return t}const l=class l{constructor(e,t={}){if(typeof e=="string"&&(e=document.querySelector(e)),!e)throw new Error("moTimeline: element not found");this.element=e,this.settings=Object.assign({},v,t),this.settings.columnCount=Object.assign({},v.columnCount,t.columnCount),this._resizeHandler=I(()=>this.refresh(),100),this._initialized=!1,this.init()}init(){const e=this.element;if(c.has(e)){this.refresh();return}const t=Object.assign({},this.settings,{lastItemIdx:0});c.set(e,t),l.instances.add(this),e.classList.add("mo-timeline"),t.theme&&e.classList.add("mo-theme"),Array.from(e.children).length!==0&&(this._initItems(),this._initialized=!0,window.addEventListener("resize",this._resizeHandler))}refresh(){l.instances.forEach(e=>{const t=e.element,s=c.get(t);s&&(s.col=s.columnCount[w()],e._setDivider(),Array.from(t.children).forEach(i=>{e._setPostPosition(i)}))})}initNewItems(){this._initItems()}addItems(e){typeof e=="string"&&(e=JSON.parse(e)),e.forEach(t=>this.element.appendChild(this._createItemElement(t))),this._initItems()}destroy(){window.removeEventListener("resize",this._resizeHandler),c.delete(this.element),l.instances.delete(this),this.element.classList.remove("mo-timeline","mo-theme","mo-twocol"),Array.from(this.element.children).forEach(e=>{e.classList.remove("mo-item","js-mo-item","mo-inverted","js-mo-inverted","mo-offset"),e.querySelectorAll(".js-mo-badge, .js-mo-arrow").forEach(t=>t.remove())})}_getData(){return c.get(this.element)}_setDivider(){const e=this._getData();e&&(e.col=e.columnCount[w()],this.element.classList.toggle("mo-twocol",e.col>1))}_initItems(){const e=this.element,t=this._getData();if(!t)return;const s=t.lastItemIdx,i=Array.from(e.children),n=i.slice(s);n.length!==0&&(n.forEach((r,a)=>{r.id||(r.id="moT"+crypto.randomUUID()+"_"+(a+s)),r.classList.add("mo-item","js-mo-item")}),this._setDivider(),n.forEach((r,a)=>{t.badgeShow&&this._createBadge(r,a+s+1),t.arrowShow&&this._createArrow(r)}),t.lastItemIdx=i.length,c.set(e,t),this.refresh())}_setPostPosition(e){const t=this._getLeftOrRight(e);t&&(e.classList.toggle("mo-inverted",t.lr>0),e.classList.toggle("js-mo-inverted",t.lr>0),e.classList.toggle("mo-offset",t.badge_offset>0))}_getLeftOrRight(e){if(!e)return null;const t=this._getData();if(!t)return null;const s=t.col,i=E(e,".js-mo-inverted")[0]||null,n=E(e,".js-mo-item:not(.js-mo-inverted)")[0]||null,r=m(n),a=m(i),f=m(e);let h=0,u=0;if(s>1){r.gppu>f.o&&(h=1),a.gppu>r.gppu&&(h=0);const g=e.previousElementSibling;g&&Math.abs(f.o-m(g).o)<40&&(u=1)}return{lr:h,badge_offset:u}}_createBadge(e,t){const s=this._getData(),i=document.createElement("span");if(i.className="mo-badge js-mo-badge",s.showCounterStyle==="none")i.style.opacity="0";else if(s.showCounterStyle==="image"){const n=document.createElement("img");n.className="mo-badge-icon",n.alt="",n.src=e.dataset.moIcon||C,i.appendChild(n)}else i.textContent=t;e.prepend(i)}_createItemElement(e){const t=document.createElement("li");e.icon&&(t.dataset.moIcon=e.icon);const s=document.createElement("div");if(s.className="mo-card",e.banner){const n=document.createElement("div");n.className="mo-card-image";const r=document.createElement("img");if(r.className="mo-banner",r.src=e.banner,r.alt="",n.appendChild(r),e.avatar){const a=document.createElement("img");a.className="mo-avatar",a.src=e.avatar,a.alt="",n.appendChild(a)}s.appendChild(n)}const i=document.createElement("div");if(i.className="mo-card-body",e.title){const n=document.createElement("h3");n.textContent=e.title,i.appendChild(n)}if(e.meta){const n=document.createElement("p");n.className="mo-meta",n.textContent=e.meta,i.appendChild(n)}if(e.text){const n=document.createElement("p");n.textContent=e.text,i.appendChild(n)}return s.appendChild(i),t.appendChild(s),t}_createArrow(e){const t=document.createElement("span");t.className="mo-arrow js-mo-arrow",e.prepend(t)}};p(l,"instances",new Set);let d=l;exports.MoTimeline=d;exports.default=d;
@@ -1,3 +1,3 @@
1
1
  /*!
2
2
  * moTimeline v2.3.0 — CSS
3
- */:root{--mo-line-color: #dde1e7;--mo-badge-bg: #4f46e5;--mo-badge-color: #fff;--mo-badge-size: 26px;--mo-badge-font-size: 12px;--mo-arrow-color: #dde1e7}.mo-timeline{display:block;list-style:none;margin:0;padding:0;position:relative;width:100%}.mo-timeline:after{content:"";display:table;clear:both}.mo-timeline.mo-twocol:before{background-color:var(--mo-line-color);bottom:0;content:"";left:50%;margin-left:-1.5px;position:absolute;top:0;width:3px;z-index:0}.mo-timeline>.mo-item{box-sizing:border-box;display:block;float:left;position:relative;width:50%}.mo-timeline:not(.mo-twocol)>.mo-item{float:none;width:100%}.mo-timeline>.mo-item.mo-inverted{float:right}.mo-badge{align-items:center;background:var(--mo-badge-bg);border-radius:50%;color:var(--mo-badge-color);display:flex;font-size:var(--mo-badge-font-size);font-weight:700;height:var(--mo-badge-size);justify-content:center;min-width:var(--mo-badge-size);overflow:hidden;position:absolute;top:18px;z-index:2}.mo-badge .mo-badge-icon{border-radius:50%;height:100%;object-fit:cover;width:100%}.mo-timeline.mo-twocol>.mo-item:not(.mo-inverted) .mo-badge{left:auto;right:calc(var(--mo-badge-size) / -2)}.mo-timeline.mo-twocol>.mo-item.mo-inverted .mo-badge{left:calc(var(--mo-badge-size) / -2);right:auto}.mo-timeline.mo-twocol>.mo-item.mo-offset .mo-badge{top:calc(18px + var(--mo-badge-size) + 10px)}.mo-timeline.mo-twocol>.mo-item.mo-offset .mo-arrow{top:calc(26px + var(--mo-badge-size) + 10px)}.mo-timeline:not(.mo-twocol)>.mo-item .mo-badge{right:12px;left:auto;top:12px}.mo-arrow{border:8px solid transparent;display:block;height:0;position:absolute;top:26px;width:0;z-index:1}.mo-timeline.mo-twocol>.mo-item:not(.mo-inverted) .mo-arrow{border-left:8px solid var(--mo-arrow-color);border-right:none;left:auto;right:0}.mo-timeline.mo-twocol>.mo-item.mo-inverted .mo-arrow{border-right:8px solid var(--mo-arrow-color);border-left:none;left:0;right:auto}.mo-timeline:not(.mo-twocol)>.mo-item .mo-arrow{display:none}.mo-theme>.mo-item .mo-card{background:#fff;border-radius:8px;box-shadow:0 2px 14px #0000001a;margin:.5rem 1.25rem .5rem .5rem;overflow:hidden;position:relative}.mo-theme>.mo-item.mo-inverted .mo-card{margin:.5rem .5rem .5rem 1.25rem}.mo-theme>.mo-item .mo-banner{display:block;height:160px;object-fit:cover;width:100%}.mo-theme>.mo-item .mo-card-image{overflow:visible;position:relative}.mo-theme>.mo-item .mo-avatar{border:3px solid #fff;border-radius:50%;bottom:-22px;box-shadow:0 2px 8px #0000002e;height:50px;object-fit:cover;position:absolute;right:14px;width:50px;z-index:1}.mo-theme>.mo-item.mo-inverted .mo-avatar{left:14px;right:auto}.mo-theme>.mo-item .mo-card-body{padding:1.75rem 1rem 1rem}.mo-theme>.mo-item .mo-card-body h3{font-size:1rem;font-weight:700;margin:0 0 .3rem}.mo-theme>.mo-item .mo-card-body .mo-meta{color:#9ca3af;font-size:.75rem;margin-bottom:.5rem}.mo-theme>.mo-item .mo-card-body p{color:#6b7280;font-size:.875rem;line-height:1.55;margin:0}.mo-theme.mo-twocol>.mo-item:not(.mo-inverted) .mo-arrow{border-left-color:#e5e7eb;right:10px}.mo-theme.mo-twocol>.mo-item.mo-inverted .mo-arrow{border-right-color:#e5e7eb;left:10px}.mo-theme .mo-badge{background:#fff;border:2px solid var(--mo-line-color);box-shadow:0 2px 6px #0000001a;color:#374151}
3
+ */:root{--mo-line-color: #dde1e7;--mo-badge-bg: #4f46e5;--mo-badge-color: #fff;--mo-badge-size: 26px;--mo-badge-font-size: 12px;--mo-arrow-color: #dde1e7}.mo-timeline{display:block;list-style:none;margin:0;padding:0;position:relative;width:100%}.mo-timeline:after{content:"";display:table;clear:both}.mo-timeline.mo-twocol:before{background-color:var(--mo-line-color);bottom:0;content:"";left:50%;margin-left:-1.5px;position:absolute;top:0;width:3px;z-index:0}.mo-timeline>.mo-item{box-sizing:border-box;display:block;float:left;position:relative;width:50%}.mo-timeline:not(.mo-twocol)>.mo-item{float:none;width:100%}.mo-timeline>.mo-item.mo-inverted{float:right}.mo-badge{align-items:center;background:var(--mo-badge-bg);border-radius:50%;color:var(--mo-badge-color);display:flex;font-size:var(--mo-badge-font-size);font-weight:700;height:var(--mo-badge-size);justify-content:center;min-width:var(--mo-badge-size);overflow:hidden;position:absolute;top:18px;z-index:2}.mo-badge .mo-badge-icon{border-radius:50%;height:100%;object-fit:cover;width:100%}.mo-timeline.mo-twocol>.mo-item:not(.mo-inverted) .mo-badge{left:auto;right:calc(var(--mo-badge-size) / -2)}.mo-timeline.mo-twocol>.mo-item.mo-inverted .mo-badge{left:calc(var(--mo-badge-size) / -2);right:auto}.mo-timeline.mo-twocol>.mo-item.mo-offset .mo-badge{top:calc(18px + var(--mo-badge-size) + 10px)}.mo-timeline.mo-twocol>.mo-item.mo-offset .mo-arrow{top:calc(26px + var(--mo-badge-size) + 10px)}.mo-timeline:not(.mo-twocol)>.mo-item .mo-badge{display:none}.mo-arrow{border:8px solid transparent;display:block;height:0;position:absolute;top:26px;width:0;z-index:1}.mo-timeline.mo-twocol>.mo-item:not(.mo-inverted) .mo-arrow{border-left:8px solid var(--mo-arrow-color);border-right:none;left:auto;right:0}.mo-timeline.mo-twocol>.mo-item.mo-inverted .mo-arrow{border-right:8px solid var(--mo-arrow-color);border-left:none;left:0;right:auto}.mo-timeline:not(.mo-twocol)>.mo-item .mo-arrow{display:none}.mo-theme>.mo-item .mo-card{background:#fff;border-radius:8px;box-shadow:0 2px 14px #0000001a;margin:.5rem 1.25rem .5rem .5rem;overflow:hidden;position:relative}.mo-theme>.mo-item.mo-inverted .mo-card{margin:.5rem .5rem .5rem 1.25rem}.mo-theme>.mo-item .mo-banner{display:block;height:160px;object-fit:cover;width:100%}.mo-theme>.mo-item .mo-card-image{overflow:visible;position:relative}.mo-theme>.mo-item .mo-avatar{border:3px solid #fff;border-radius:50%;bottom:-22px;box-shadow:0 2px 8px #0000002e;height:50px;object-fit:cover;position:absolute;right:14px;width:50px;z-index:1}.mo-theme>.mo-item.mo-inverted .mo-avatar{left:14px;right:auto}.mo-theme>.mo-item .mo-card-body{padding:1.75rem 1rem 1rem}.mo-theme>.mo-item .mo-card-body h3{font-size:1rem;font-weight:700;margin:0 0 .3rem}.mo-theme>.mo-item .mo-card-body .mo-meta{color:#9ca3af;font-size:.75rem;margin-bottom:.5rem}.mo-theme>.mo-item .mo-card-body p{color:#6b7280;font-size:.875rem;line-height:1.55;margin:0}.mo-theme.mo-twocol>.mo-item:not(.mo-inverted) .mo-arrow{border-left-color:#e5e7eb;right:10px}.mo-theme.mo-twocol>.mo-item.mo-inverted .mo-arrow{border-right-color:#e5e7eb;left:10px}.mo-theme .mo-badge{background:#fff;border:2px solid var(--mo-line-color);box-shadow:0 2px 6px #0000001a;color:#374151}
@@ -1,127 +1,169 @@
1
- var b = Object.defineProperty;
2
- var E = (i, t, e) => t in i ? b(i, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : i[t] = e;
3
- var g = (i, t, e) => E(i, typeof t != "symbol" ? t + "" : t, e);
1
+ var _ = Object.defineProperty;
2
+ var C = (o, e, t) => e in o ? _(o, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : o[e] = t;
3
+ var g = (o, e, t) => C(o, typeof e != "symbol" ? e + "" : e, t);
4
4
  /*!
5
- * moTimeline v2.3.0
5
+ * moTimeline v2.5.0
6
6
  * Responsive two-column timeline layout library
7
7
  * https://github.com/MattOpen/moTimeline
8
8
  * MIT License
9
9
  */
10
- const a = /* @__PURE__ */ new WeakMap(), p = {
10
+ const c = /* @__PURE__ */ new WeakMap(), p = {
11
11
  columnCount: { xs: 1, sm: 2, md: 2, lg: 2 },
12
12
  badgeShow: !1,
13
13
  arrowShow: !1,
14
14
  theme: !1,
15
- showCounter: !0,
16
15
  showCounterStyle: "counter"
17
- // 'counter' | 'image'
18
- }, I = "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><circle cx='12' cy='12' r='11' fill='%234f46e5'/><circle cx='12' cy='12' r='4.5' fill='white'/></svg>";
16
+ // 'counter' | 'image' | 'none'
17
+ }, b = "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><circle cx='12' cy='12' r='11' fill='%234f46e5'/><circle cx='12' cy='12' r='4.5' fill='white'/></svg>";
19
18
  function w() {
20
- const i = window.innerWidth;
21
- return i < 600 ? "xs" : i < 992 ? "sm" : i < 1200 ? "md" : "lg";
19
+ const o = window.innerWidth;
20
+ return o < 600 ? "xs" : o < 992 ? "sm" : o < 1200 ? "md" : "lg";
22
21
  }
23
- function L(i, t = 100) {
24
- let e;
22
+ function I(o, e = 100) {
23
+ let t;
25
24
  return (...s) => {
26
- clearTimeout(e), e = setTimeout(() => i(...s), t);
25
+ clearTimeout(t), t = setTimeout(() => o(...s), e);
27
26
  };
28
27
  }
29
- function m(i) {
30
- return i ? {
31
- o: i.offsetTop,
32
- h: i.offsetHeight,
33
- gppu: i.offsetTop + i.offsetHeight
28
+ function m(o) {
29
+ return o ? {
30
+ o: o.offsetTop,
31
+ h: o.offsetHeight,
32
+ gppu: o.offsetTop + o.offsetHeight
34
33
  } : { o: 0, h: 0, gppu: 0 };
35
34
  }
36
- function v(i, t) {
37
- const e = [];
38
- let s = i.previousElementSibling;
35
+ function v(o, e) {
36
+ const t = [];
37
+ let s = o.previousElementSibling;
39
38
  for (; s; )
40
- (!t || s.matches(t)) && e.push(s), s = s.previousElementSibling;
41
- return e;
39
+ (!e || s.matches(e)) && t.push(s), s = s.previousElementSibling;
40
+ return t;
42
41
  }
43
- const c = class c {
44
- constructor(t, e = {}) {
45
- if (typeof t == "string" && (t = document.querySelector(t)), !t) throw new Error("moTimeline: element not found");
46
- this.element = t, this.settings = Object.assign({}, p, e), this.settings.columnCount = Object.assign({}, p.columnCount, e.columnCount), this._resizeHandler = L(() => this.refresh(), 100), this._initialized = !1, this.init();
42
+ const l = class l {
43
+ constructor(e, t = {}) {
44
+ if (typeof e == "string" && (e = document.querySelector(e)), !e) throw new Error("moTimeline: element not found");
45
+ this.element = e, this.settings = Object.assign({}, p, t), this.settings.columnCount = Object.assign({}, p.columnCount, t.columnCount), this._resizeHandler = I(() => this.refresh(), 100), this._initialized = !1, this.init();
47
46
  }
48
47
  init() {
49
- const t = this.element;
50
- if (a.has(t)) {
48
+ const e = this.element;
49
+ if (c.has(e)) {
51
50
  this.refresh();
52
51
  return;
53
52
  }
54
- const e = Object.assign({}, this.settings, { lastItemIdx: 0 });
55
- a.set(t, e), c.instances.add(this), t.classList.add("mo-timeline"), e.theme && t.classList.add("mo-theme"), Array.from(t.children).length !== 0 && (this._initItems(), this._initialized = !0, window.addEventListener("resize", this._resizeHandler));
53
+ const t = Object.assign({}, this.settings, { lastItemIdx: 0 });
54
+ c.set(e, t), l.instances.add(this), e.classList.add("mo-timeline"), t.theme && e.classList.add("mo-theme"), Array.from(e.children).length !== 0 && (this._initItems(), this._initialized = !0, window.addEventListener("resize", this._resizeHandler));
56
55
  }
57
56
  refresh() {
58
- c.instances.forEach((t) => {
59
- const e = t.element, s = a.get(e);
60
- s && (s.col = s.columnCount[w()], t._setDivider(), Array.from(e.children).forEach((o) => {
61
- t._setPostPosition(o);
57
+ l.instances.forEach((e) => {
58
+ const t = e.element, s = c.get(t);
59
+ s && (s.col = s.columnCount[w()], e._setDivider(), Array.from(t.children).forEach((i) => {
60
+ e._setPostPosition(i);
62
61
  }));
63
62
  });
64
63
  }
65
64
  initNewItems() {
66
65
  this._initItems();
67
66
  }
67
+ /**
68
+ * Create <li> elements from an array of item objects (or a JSON string) and
69
+ * append them to the timeline, then initialize them in one batch.
70
+ *
71
+ * Item shape:
72
+ * { title, meta, text, banner, avatar, icon }
73
+ * — banner / avatar / icon are all optional
74
+ * — icon sets data-mo-icon on the <li> for showCounterStyle:'image'
75
+ */
76
+ addItems(e) {
77
+ typeof e == "string" && (e = JSON.parse(e)), e.forEach((t) => this.element.appendChild(this._createItemElement(t))), this._initItems();
78
+ }
68
79
  destroy() {
69
- window.removeEventListener("resize", this._resizeHandler), a.delete(this.element), c.instances.delete(this), this.element.classList.remove("mo-timeline", "mo-theme", "mo-twocol"), Array.from(this.element.children).forEach((t) => {
70
- t.classList.remove("mo-item", "js-mo-item", "mo-inverted", "js-mo-inverted", "mo-offset"), t.querySelectorAll(".js-mo-badge, .js-mo-arrow").forEach((e) => e.remove());
80
+ window.removeEventListener("resize", this._resizeHandler), c.delete(this.element), l.instances.delete(this), this.element.classList.remove("mo-timeline", "mo-theme", "mo-twocol"), Array.from(this.element.children).forEach((e) => {
81
+ e.classList.remove("mo-item", "js-mo-item", "mo-inverted", "js-mo-inverted", "mo-offset"), e.querySelectorAll(".js-mo-badge, .js-mo-arrow").forEach((t) => t.remove());
71
82
  });
72
83
  }
73
84
  // ─── Private ────────────────────────────────────────────────────────────────
74
85
  _getData() {
75
- return a.get(this.element);
86
+ return c.get(this.element);
76
87
  }
77
88
  _setDivider() {
78
- const t = this._getData();
79
- t && (t.col = t.columnCount[w()], this.element.classList.toggle("mo-twocol", t.col > 1));
89
+ const e = this._getData();
90
+ e && (e.col = e.columnCount[w()], this.element.classList.toggle("mo-twocol", e.col > 1));
80
91
  }
81
92
  _initItems() {
82
- const t = this.element, e = this._getData();
83
- if (!e) return;
84
- const s = e.lastItemIdx, o = Array.from(t.children), n = o.slice(s);
85
- n.length !== 0 && (n.forEach((r, l) => {
86
- r.id || (r.id = "moT" + crypto.randomUUID() + "_" + (l + s)), r.classList.add("mo-item", "js-mo-item");
87
- }), this._setDivider(), n.forEach((r, l) => {
88
- e.badgeShow && this._createBadge(r, l + s + 1), e.arrowShow && this._createArrow(r);
89
- }), e.lastItemIdx = o.length, a.set(t, e), this.refresh());
93
+ const e = this.element, t = this._getData();
94
+ if (!t) return;
95
+ const s = t.lastItemIdx, i = Array.from(e.children), n = i.slice(s);
96
+ n.length !== 0 && (n.forEach((r, a) => {
97
+ r.id || (r.id = "moT" + crypto.randomUUID() + "_" + (a + s)), r.classList.add("mo-item", "js-mo-item");
98
+ }), this._setDivider(), n.forEach((r, a) => {
99
+ t.badgeShow && this._createBadge(r, a + s + 1), t.arrowShow && this._createArrow(r);
100
+ }), t.lastItemIdx = i.length, c.set(e, t), this.refresh());
90
101
  }
91
- _setPostPosition(t) {
92
- const e = this._getLeftOrRight(t);
93
- e && (t.classList.toggle("mo-inverted", e.lr > 0), t.classList.toggle("js-mo-inverted", e.lr > 0), t.classList.toggle("mo-offset", e.badge_offset > 0));
102
+ _setPostPosition(e) {
103
+ const t = this._getLeftOrRight(e);
104
+ t && (e.classList.toggle("mo-inverted", t.lr > 0), e.classList.toggle("js-mo-inverted", t.lr > 0), e.classList.toggle("mo-offset", t.badge_offset > 0));
94
105
  }
95
- _getLeftOrRight(t) {
96
- if (!t) return null;
97
- const e = this._getData();
106
+ _getLeftOrRight(e) {
98
107
  if (!e) return null;
99
- const s = e.col, o = v(t, ".js-mo-inverted")[0] || null, n = v(t, ".js-mo-item:not(.js-mo-inverted)")[0] || null, r = m(n), l = m(o), d = m(t);
100
- let h = 0, f = 0;
108
+ const t = this._getData();
109
+ if (!t) return null;
110
+ const s = t.col, i = v(e, ".js-mo-inverted")[0] || null, n = v(e, ".js-mo-item:not(.js-mo-inverted)")[0] || null, r = m(n), a = m(i), h = m(e);
111
+ let d = 0, f = 0;
101
112
  if (s > 1) {
102
- r.gppu > d.o && (h = 1), l.gppu > r.gppu && (h = 0);
103
- const u = t.previousElementSibling;
104
- u && Math.abs(d.o - m(u).o) < 40 && (f = 1);
113
+ r.gppu > h.o && (d = 1), a.gppu > r.gppu && (d = 0);
114
+ const u = e.previousElementSibling;
115
+ u && Math.abs(h.o - m(u).o) < 40 && (f = 1);
105
116
  }
106
- return { lr: h, badge_offset: f };
117
+ return { lr: d, badge_offset: f };
107
118
  }
108
- _createBadge(t, e) {
109
- const s = this._getData(), o = document.createElement("span");
110
- if (o.className = "mo-badge js-mo-badge", s.showCounter || (o.style.opacity = "0"), s.showCounterStyle === "image") {
119
+ _createBadge(e, t) {
120
+ const s = this._getData(), i = document.createElement("span");
121
+ if (i.className = "mo-badge js-mo-badge", s.showCounterStyle === "none")
122
+ i.style.opacity = "0";
123
+ else if (s.showCounterStyle === "image") {
111
124
  const n = document.createElement("img");
112
- n.className = "mo-badge-icon", n.alt = "", n.src = t.dataset.moIcon || I, o.appendChild(n);
125
+ n.className = "mo-badge-icon", n.alt = "", n.src = e.dataset.moIcon || b, i.appendChild(n);
113
126
  } else
114
- o.textContent = e;
115
- t.prepend(o);
127
+ i.textContent = t;
128
+ e.prepend(i);
129
+ }
130
+ _createItemElement(e) {
131
+ const t = document.createElement("li");
132
+ e.icon && (t.dataset.moIcon = e.icon);
133
+ const s = document.createElement("div");
134
+ if (s.className = "mo-card", e.banner) {
135
+ const n = document.createElement("div");
136
+ n.className = "mo-card-image";
137
+ const r = document.createElement("img");
138
+ if (r.className = "mo-banner", r.src = e.banner, r.alt = "", n.appendChild(r), e.avatar) {
139
+ const a = document.createElement("img");
140
+ a.className = "mo-avatar", a.src = e.avatar, a.alt = "", n.appendChild(a);
141
+ }
142
+ s.appendChild(n);
143
+ }
144
+ const i = document.createElement("div");
145
+ if (i.className = "mo-card-body", e.title) {
146
+ const n = document.createElement("h3");
147
+ n.textContent = e.title, i.appendChild(n);
148
+ }
149
+ if (e.meta) {
150
+ const n = document.createElement("p");
151
+ n.className = "mo-meta", n.textContent = e.meta, i.appendChild(n);
152
+ }
153
+ if (e.text) {
154
+ const n = document.createElement("p");
155
+ n.textContent = e.text, i.appendChild(n);
156
+ }
157
+ return s.appendChild(i), t.appendChild(s), t;
116
158
  }
117
- _createArrow(t) {
118
- const e = document.createElement("span");
119
- e.className = "mo-arrow js-mo-arrow", t.prepend(e);
159
+ _createArrow(e) {
160
+ const t = document.createElement("span");
161
+ t.className = "mo-arrow js-mo-arrow", e.prepend(t);
120
162
  }
121
163
  };
122
- g(c, "instances", /* @__PURE__ */ new Set());
123
- let _ = c;
164
+ g(l, "instances", /* @__PURE__ */ new Set());
165
+ let E = l;
124
166
  export {
125
- _ as MoTimeline,
126
- _ as default
167
+ E as MoTimeline,
168
+ E as default
127
169
  };
@@ -1,6 +1,6 @@
1
- (function(n,i){typeof exports=="object"&&typeof module<"u"?i(exports):typeof define=="function"&&define.amd?define(["exports"],i):(n=typeof globalThis<"u"?globalThis:n||self,i(n.MoTimeline={}))})(this,function(n){"use strict";var I=Object.defineProperty;var j=(n,i,c)=>i in n?I(n,i,{enumerable:!0,configurable:!0,writable:!0,value:c}):n[i]=c;var b=(n,i,c)=>j(n,typeof i!="symbol"?i+"":i,c);/*!
2
- * moTimeline v2.3.0
1
+ (function(a,o){typeof exports=="object"&&typeof module<"u"?o(exports):typeof define=="function"&&define.amd?define(["exports"],o):(a=typeof globalThis<"u"?globalThis:a||self,o(a.MoTimeline={}))})(this,function(a){"use strict";var y=Object.defineProperty;var I=(a,o,m)=>o in a?y(a,o,{enumerable:!0,configurable:!0,writable:!0,value:m}):a[o]=m;var _=(a,o,m)=>I(a,typeof o!="symbol"?o+"":o,m);/*!
2
+ * moTimeline v2.5.0
3
3
  * Responsive two-column timeline layout library
4
4
  * https://github.com/MattOpen/moTimeline
5
5
  * MIT License
6
- */const i=new WeakMap,c={columnCount:{xs:1,sm:2,md:2,lg:2},badgeShow:!1,arrowShow:!1,theme:!1,showCounter:!0,showCounterStyle:"counter"},y="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><circle cx='12' cy='12' r='11' fill='%234f46e5'/><circle cx='12' cy='12' r='4.5' fill='white'/></svg>";function g(){const o=window.innerWidth;return o<600?"xs":o<992?"sm":o<1200?"md":"lg"}function E(o,e=100){let t;return(...s)=>{clearTimeout(t),t=setTimeout(()=>o(...s),e)}}function f(o){return o?{o:o.offsetTop,h:o.offsetHeight,gppu:o.offsetTop+o.offsetHeight}:{o:0,h:0,gppu:0}}function p(o,e){const t=[];let s=o.previousElementSibling;for(;s;)(!e||s.matches(e))&&t.push(s),s=s.previousElementSibling;return t}const m=class m{constructor(e,t={}){if(typeof e=="string"&&(e=document.querySelector(e)),!e)throw new Error("moTimeline: element not found");this.element=e,this.settings=Object.assign({},c,t),this.settings.columnCount=Object.assign({},c.columnCount,t.columnCount),this._resizeHandler=E(()=>this.refresh(),100),this._initialized=!1,this.init()}init(){const e=this.element;if(i.has(e)){this.refresh();return}const t=Object.assign({},this.settings,{lastItemIdx:0});i.set(e,t),m.instances.add(this),e.classList.add("mo-timeline"),t.theme&&e.classList.add("mo-theme"),Array.from(e.children).length!==0&&(this._initItems(),this._initialized=!0,window.addEventListener("resize",this._resizeHandler))}refresh(){m.instances.forEach(e=>{const t=e.element,s=i.get(t);s&&(s.col=s.columnCount[g()],e._setDivider(),Array.from(t.children).forEach(r=>{e._setPostPosition(r)}))})}initNewItems(){this._initItems()}destroy(){window.removeEventListener("resize",this._resizeHandler),i.delete(this.element),m.instances.delete(this),this.element.classList.remove("mo-timeline","mo-theme","mo-twocol"),Array.from(this.element.children).forEach(e=>{e.classList.remove("mo-item","js-mo-item","mo-inverted","js-mo-inverted","mo-offset"),e.querySelectorAll(".js-mo-badge, .js-mo-arrow").forEach(t=>t.remove())})}_getData(){return i.get(this.element)}_setDivider(){const e=this._getData();e&&(e.col=e.columnCount[g()],this.element.classList.toggle("mo-twocol",e.col>1))}_initItems(){const e=this.element,t=this._getData();if(!t)return;const s=t.lastItemIdx,r=Array.from(e.children),a=r.slice(s);a.length!==0&&(a.forEach((l,d)=>{l.id||(l.id="moT"+crypto.randomUUID()+"_"+(d+s)),l.classList.add("mo-item","js-mo-item")}),this._setDivider(),a.forEach((l,d)=>{t.badgeShow&&this._createBadge(l,d+s+1),t.arrowShow&&this._createArrow(l)}),t.lastItemIdx=r.length,i.set(e,t),this.refresh())}_setPostPosition(e){const t=this._getLeftOrRight(e);t&&(e.classList.toggle("mo-inverted",t.lr>0),e.classList.toggle("js-mo-inverted",t.lr>0),e.classList.toggle("mo-offset",t.badge_offset>0))}_getLeftOrRight(e){if(!e)return null;const t=this._getData();if(!t)return null;const s=t.col,r=p(e,".js-mo-inverted")[0]||null,a=p(e,".js-mo-item:not(.js-mo-inverted)")[0]||null,l=f(a),d=f(r),w=f(e);let u=0,v=0;if(s>1){l.gppu>w.o&&(u=1),d.gppu>l.gppu&&(u=0);const _=e.previousElementSibling;_&&Math.abs(w.o-f(_).o)<40&&(v=1)}return{lr:u,badge_offset:v}}_createBadge(e,t){const s=this._getData(),r=document.createElement("span");if(r.className="mo-badge js-mo-badge",s.showCounter||(r.style.opacity="0"),s.showCounterStyle==="image"){const a=document.createElement("img");a.className="mo-badge-icon",a.alt="",a.src=e.dataset.moIcon||y,r.appendChild(a)}else r.textContent=t;e.prepend(r)}_createArrow(e){const t=document.createElement("span");t.className="mo-arrow js-mo-arrow",e.prepend(t)}};b(m,"instances",new Set);let h=m;n.MoTimeline=h,n.default=h,Object.defineProperties(n,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
6
+ */const o=new WeakMap,m={columnCount:{xs:1,sm:2,md:2,lg:2},badgeShow:!1,arrowShow:!1,theme:!1,showCounterStyle:"counter"},b="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><circle cx='12' cy='12' r='11' fill='%234f46e5'/><circle cx='12' cy='12' r='4.5' fill='white'/></svg>";function g(){const c=window.innerWidth;return c<600?"xs":c<992?"sm":c<1200?"md":"lg"}function C(c,e=100){let t;return(...n)=>{clearTimeout(t),t=setTimeout(()=>c(...n),e)}}function h(c){return c?{o:c.offsetTop,h:c.offsetHeight,gppu:c.offsetTop+c.offsetHeight}:{o:0,h:0,gppu:0}}function p(c,e){const t=[];let n=c.previousElementSibling;for(;n;)(!e||n.matches(e))&&t.push(n),n=n.previousElementSibling;return t}const d=class d{constructor(e,t={}){if(typeof e=="string"&&(e=document.querySelector(e)),!e)throw new Error("moTimeline: element not found");this.element=e,this.settings=Object.assign({},m,t),this.settings.columnCount=Object.assign({},m.columnCount,t.columnCount),this._resizeHandler=C(()=>this.refresh(),100),this._initialized=!1,this.init()}init(){const e=this.element;if(o.has(e)){this.refresh();return}const t=Object.assign({},this.settings,{lastItemIdx:0});o.set(e,t),d.instances.add(this),e.classList.add("mo-timeline"),t.theme&&e.classList.add("mo-theme"),Array.from(e.children).length!==0&&(this._initItems(),this._initialized=!0,window.addEventListener("resize",this._resizeHandler))}refresh(){d.instances.forEach(e=>{const t=e.element,n=o.get(t);n&&(n.col=n.columnCount[g()],e._setDivider(),Array.from(t.children).forEach(i=>{e._setPostPosition(i)}))})}initNewItems(){this._initItems()}addItems(e){typeof e=="string"&&(e=JSON.parse(e)),e.forEach(t=>this.element.appendChild(this._createItemElement(t))),this._initItems()}destroy(){window.removeEventListener("resize",this._resizeHandler),o.delete(this.element),d.instances.delete(this),this.element.classList.remove("mo-timeline","mo-theme","mo-twocol"),Array.from(this.element.children).forEach(e=>{e.classList.remove("mo-item","js-mo-item","mo-inverted","js-mo-inverted","mo-offset"),e.querySelectorAll(".js-mo-badge, .js-mo-arrow").forEach(t=>t.remove())})}_getData(){return o.get(this.element)}_setDivider(){const e=this._getData();e&&(e.col=e.columnCount[g()],this.element.classList.toggle("mo-twocol",e.col>1))}_initItems(){const e=this.element,t=this._getData();if(!t)return;const n=t.lastItemIdx,i=Array.from(e.children),s=i.slice(n);s.length!==0&&(s.forEach((r,l)=>{r.id||(r.id="moT"+crypto.randomUUID()+"_"+(l+n)),r.classList.add("mo-item","js-mo-item")}),this._setDivider(),s.forEach((r,l)=>{t.badgeShow&&this._createBadge(r,l+n+1),t.arrowShow&&this._createArrow(r)}),t.lastItemIdx=i.length,o.set(e,t),this.refresh())}_setPostPosition(e){const t=this._getLeftOrRight(e);t&&(e.classList.toggle("mo-inverted",t.lr>0),e.classList.toggle("js-mo-inverted",t.lr>0),e.classList.toggle("mo-offset",t.badge_offset>0))}_getLeftOrRight(e){if(!e)return null;const t=this._getData();if(!t)return null;const n=t.col,i=p(e,".js-mo-inverted")[0]||null,s=p(e,".js-mo-item:not(.js-mo-inverted)")[0]||null,r=h(s),l=h(i),v=h(e);let u=0,w=0;if(n>1){r.gppu>v.o&&(u=1),l.gppu>r.gppu&&(u=0);const E=e.previousElementSibling;E&&Math.abs(v.o-h(E).o)<40&&(w=1)}return{lr:u,badge_offset:w}}_createBadge(e,t){const n=this._getData(),i=document.createElement("span");if(i.className="mo-badge js-mo-badge",n.showCounterStyle==="none")i.style.opacity="0";else if(n.showCounterStyle==="image"){const s=document.createElement("img");s.className="mo-badge-icon",s.alt="",s.src=e.dataset.moIcon||b,i.appendChild(s)}else i.textContent=t;e.prepend(i)}_createItemElement(e){const t=document.createElement("li");e.icon&&(t.dataset.moIcon=e.icon);const n=document.createElement("div");if(n.className="mo-card",e.banner){const s=document.createElement("div");s.className="mo-card-image";const r=document.createElement("img");if(r.className="mo-banner",r.src=e.banner,r.alt="",s.appendChild(r),e.avatar){const l=document.createElement("img");l.className="mo-avatar",l.src=e.avatar,l.alt="",s.appendChild(l)}n.appendChild(s)}const i=document.createElement("div");if(i.className="mo-card-body",e.title){const s=document.createElement("h3");s.textContent=e.title,i.appendChild(s)}if(e.meta){const s=document.createElement("p");s.className="mo-meta",s.textContent=e.meta,i.appendChild(s)}if(e.text){const s=document.createElement("p");s.textContent=e.text,i.appendChild(s)}return n.appendChild(i),t.appendChild(n),t}_createArrow(e){const t=document.createElement("span");t.className="mo-arrow js-mo-arrow",e.prepend(t)}};_(d,"instances",new Set);let f=d;a.MoTimeline=f,a.default=f,Object.defineProperties(a,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "motimeline",
3
- "version": "2.3.0",
3
+ "version": "2.5.0",
4
4
  "description": "Responsive two-column timeline layout library. Plain JavaScript, no dependencies.",
5
5
  "main": "./dist/moTimeline.cjs",
6
6
  "module": "./dist/moTimeline.js",
@@ -113,11 +113,9 @@
113
113
  top: calc(26px + var(--mo-badge-size) + 10px);
114
114
  }
115
115
 
116
- /* Single column: badge in top-right of item */
116
+ /* Single column: hide badge (center-line elements have no meaning without a center line) */
117
117
  .mo-timeline:not(.mo-twocol) > .mo-item .mo-badge {
118
- right: 12px;
119
- left: auto;
120
- top: 12px;
118
+ display: none;
121
119
  }
122
120
 
123
121
  /* ── Arrow — triangle pointing FROM card TOWARD center line ─── */
package/src/moTimeline.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * moTimeline v2.3.0
2
+ * moTimeline v2.5.0
3
3
  * Responsive two-column timeline layout library
4
4
  * https://github.com/MattOpen/moTimeline
5
5
  * MIT License
@@ -21,8 +21,7 @@ const DEFAULTS = {
21
21
  badgeShow: false,
22
22
  arrowShow: false,
23
23
  theme: false,
24
- showCounter: true,
25
- showCounterStyle: 'counter', // 'counter' | 'image'
24
+ showCounterStyle: 'counter', // 'counter' | 'image' | 'none'
26
25
  };
27
26
 
28
27
  const DEFAULT_BADGE_ICON = "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><circle cx='12' cy='12' r='11' fill='%234f46e5'/><circle cx='12' cy='12' r='4.5' fill='white'/></svg>";
@@ -124,6 +123,21 @@ export class MoTimeline {
124
123
  this._initItems();
125
124
  }
126
125
 
126
+ /**
127
+ * Create <li> elements from an array of item objects (or a JSON string) and
128
+ * append them to the timeline, then initialize them in one batch.
129
+ *
130
+ * Item shape:
131
+ * { title, meta, text, banner, avatar, icon }
132
+ * — banner / avatar / icon are all optional
133
+ * — icon sets data-mo-icon on the <li> for showCounterStyle:'image'
134
+ */
135
+ addItems(items) {
136
+ if (typeof items === 'string') items = JSON.parse(items);
137
+ items.forEach((item) => this.element.appendChild(this._createItemElement(item)));
138
+ this._initItems();
139
+ }
140
+
127
141
  destroy() {
128
142
  window.removeEventListener('resize', this._resizeHandler);
129
143
  instanceData.delete(this.element);
@@ -231,11 +245,9 @@ export class MoTimeline {
231
245
  const span = document.createElement('span');
232
246
  span.className = 'mo-badge js-mo-badge';
233
247
 
234
- if (!data.showCounter) {
248
+ if (data.showCounterStyle === 'none') {
235
249
  span.style.opacity = '0';
236
- }
237
-
238
- if (data.showCounterStyle === 'image') {
250
+ } else if (data.showCounterStyle === 'image') {
239
251
  const img = document.createElement('img');
240
252
  img.className = 'mo-badge-icon';
241
253
  img.alt = '';
@@ -248,6 +260,54 @@ export class MoTimeline {
248
260
  el.prepend(span);
249
261
  }
250
262
 
263
+ _createItemElement(item) {
264
+ const li = document.createElement('li');
265
+ if (item.icon) li.dataset.moIcon = item.icon;
266
+
267
+ const card = document.createElement('div');
268
+ card.className = 'mo-card';
269
+
270
+ if (item.banner) {
271
+ const wrap = document.createElement('div');
272
+ wrap.className = 'mo-card-image';
273
+ const banner = document.createElement('img');
274
+ banner.className = 'mo-banner';
275
+ banner.src = item.banner;
276
+ banner.alt = '';
277
+ wrap.appendChild(banner);
278
+ if (item.avatar) {
279
+ const avatar = document.createElement('img');
280
+ avatar.className = 'mo-avatar';
281
+ avatar.src = item.avatar;
282
+ avatar.alt = '';
283
+ wrap.appendChild(avatar);
284
+ }
285
+ card.appendChild(wrap);
286
+ }
287
+
288
+ const body = document.createElement('div');
289
+ body.className = 'mo-card-body';
290
+ if (item.title) {
291
+ const h = document.createElement('h3');
292
+ h.textContent = item.title;
293
+ body.appendChild(h);
294
+ }
295
+ if (item.meta) {
296
+ const m = document.createElement('p');
297
+ m.className = 'mo-meta';
298
+ m.textContent = item.meta;
299
+ body.appendChild(m);
300
+ }
301
+ if (item.text) {
302
+ const p = document.createElement('p');
303
+ p.textContent = item.text;
304
+ body.appendChild(p);
305
+ }
306
+ card.appendChild(body);
307
+ li.appendChild(card);
308
+ return li;
309
+ }
310
+
251
311
  _createArrow(el) {
252
312
  const span = document.createElement('span');
253
313
  span.className = 'mo-arrow js-mo-arrow';