czon 0.9.7 → 0.9.8

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.
@@ -90,20 +90,36 @@ const ContentPage = props => {
90
90
  react_1.default.createElement("button", { className: "share-float-btn", id: "share-float-btn" }, "Share"),
91
91
  react_1.default.createElement("div", { className: "share-modal-overlay", id: "share-modal-overlay" },
92
92
  react_1.default.createElement("div", { className: "share-modal" },
93
- react_1.default.createElement("canvas", { id: "share-canvas" }),
93
+ react_1.default.createElement("img", { className: "share-preview", id: "share-preview", alt: "Share preview", src: "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" }),
94
94
  react_1.default.createElement("div", { className: "share-modal-actions" },
95
95
  react_1.default.createElement("button", { className: "share-download-btn", id: "share-download-btn" }, "Save Image"),
96
96
  react_1.default.createElement("button", { className: "share-close-btn", id: "share-close-btn" }, "Close")))),
97
+ react_1.default.createElement("div", { className: "share-card", id: "share-card" },
98
+ react_1.default.createElement("div", { className: "share-card-header" },
99
+ react_1.default.createElement("div", { className: "share-card-header-left" },
100
+ react_1.default.createElement("div", { className: "share-card-site", id: "share-card-site" }),
101
+ react_1.default.createElement("div", { className: "share-card-title", id: "share-card-title" })),
102
+ react_1.default.createElement("div", { className: "share-card-qr" },
103
+ react_1.default.createElement("canvas", { id: "share-qr-canvas", width: "64", height: "64" }),
104
+ react_1.default.createElement("span", { className: "share-card-qr-hint" }, "Scan to read"))),
105
+ react_1.default.createElement("div", { className: "share-card-divider" }),
106
+ react_1.default.createElement("div", { className: "share-card-body", id: "share-card-body" })),
97
107
  react_1.default.createElement("script", { id: "qrcode-lib", src: "https://cdn.jsdelivr.net/npm/qrcode-generator@1.4.4/qrcode.min.js", defer: true }),
108
+ react_1.default.createElement("script", { id: "html2canvas-lib", src: "https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js", defer: true }),
98
109
  react_1.default.createElement("script", { dangerouslySetInnerHTML: {
99
110
  __html: `
100
111
  (function() {
101
112
  var floatBtn = document.getElementById('share-float-btn');
102
113
  var overlay = document.getElementById('share-modal-overlay');
103
- var canvas = document.getElementById('share-canvas');
114
+ var preview = document.getElementById('share-preview');
104
115
  var downloadBtn = document.getElementById('share-download-btn');
105
116
  var closeBtn = document.getElementById('share-close-btn');
106
- var selectedText = '';
117
+ var shareCard = document.getElementById('share-card');
118
+ var cardSite = document.getElementById('share-card-site');
119
+ var cardTitle = document.getElementById('share-card-title');
120
+ var cardBody = document.getElementById('share-card-body');
121
+ var qrCanvas = document.getElementById('share-qr-canvas');
122
+ var savedRange = null;
107
123
  var articleTitle = ${JSON.stringify(title)};
108
124
  var siteName = ${JSON.stringify(props.ctx.site.options?.site?.title || 'CZON')};
109
125
 
@@ -125,25 +141,18 @@ const ContentPage = props => {
125
141
  floatBtn.style.display = 'none';
126
142
  return;
127
143
  }
128
- selectedText = text;
144
+ savedRange = range.cloneRange();
129
145
  var rect = range.getBoundingClientRect();
130
146
  floatBtn.style.display = 'block';
131
- floatBtn.style.top = (window.scrollY + rect.top - 36) + 'px';
147
+ floatBtn.style.top = (window.scrollY + rect.bottom + 6) + 'px';
132
148
  floatBtn.style.left = (window.scrollX + rect.left + rect.width / 2 - 30) + 'px';
133
149
  });
134
150
 
135
- // Hide float button on click elsewhere
136
- document.addEventListener('mousedown', function(e) {
137
- if (e.target === floatBtn) return;
138
- // Let selectionchange handle hiding
139
- });
140
-
141
151
  floatBtn.addEventListener('click', function(e) {
142
152
  e.preventDefault();
143
153
  e.stopPropagation();
144
- if (!selectedText) return;
145
- renderShareCard(selectedText);
146
- overlay.classList.add('active');
154
+ if (!savedRange) return;
155
+ renderShareCard(savedRange);
147
156
  floatBtn.style.display = 'none';
148
157
  });
149
158
 
@@ -155,159 +164,82 @@ const ContentPage = props => {
155
164
  });
156
165
 
157
166
  downloadBtn.addEventListener('click', function() {
158
- canvas.toBlob(function(blob) {
159
- var url = URL.createObjectURL(blob);
160
- var a = document.createElement('a');
161
- a.href = url;
162
- a.download = 'share.png';
163
- a.click();
164
- URL.revokeObjectURL(url);
165
- }, 'image/png');
167
+ var a = document.createElement('a');
168
+ a.href = preview.src;
169
+ a.download = 'share.png';
170
+ a.click();
166
171
  });
167
172
 
168
- function wrapText(ctx, text, maxWidth, lineHeight) {
169
- var lines = [];
170
- var paragraphs = text.split('\\n');
171
- for (var p = 0; p < paragraphs.length; p++) {
172
- var words = paragraphs[p];
173
- var line = '';
174
- for (var i = 0; i < words.length; i++) {
175
- var testLine = line + words[i];
176
- var metrics = ctx.measureText(testLine);
177
- if (metrics.width > maxWidth && line.length > 0) {
178
- lines.push(line);
179
- line = words[i];
180
- } else {
181
- line = testLine;
173
+ function renderQR() {
174
+ if (typeof qrcode === 'undefined') return;
175
+ var size = 64;
176
+ var qr = qrcode(0, 'M');
177
+ qr.addData(window.location.href);
178
+ qr.make();
179
+ var moduleCount = qr.getModuleCount();
180
+ var cellSize = size / moduleCount;
181
+ var ctx = qrCanvas.getContext('2d');
182
+ ctx.clearRect(0, 0, size, size);
183
+ ctx.fillStyle = '#1a1a1a';
184
+ for (var r = 0; r < moduleCount; r++) {
185
+ for (var c = 0; c < moduleCount; c++) {
186
+ if (qr.isDark(r, c)) {
187
+ ctx.fillRect(c * cellSize, r * cellSize, cellSize + 0.5, cellSize + 0.5);
182
188
  }
183
189
  }
184
- if (line) lines.push(line);
185
- if (p < paragraphs.length - 1) lines.push('');
186
190
  }
187
- return lines;
188
191
  }
189
192
 
190
- function renderShareCard(text) {
191
- var dpr = window.devicePixelRatio || 1;
192
- var W = 800;
193
- var pad = 48;
194
- var contentW = W - pad * 2;
195
- var ctx = canvas.getContext('2d');
196
-
197
- // Pre-calculate heights
198
- ctx.font = 'bold 28px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif';
199
- var titleLines = wrapText(ctx, articleTitle, contentW, 36);
200
- var titleH = titleLines.length * 36;
201
-
202
- ctx.font = '20px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif';
203
- var maxTextLen = 300;
204
- var displayText = text.length > maxTextLen ? text.slice(0, maxTextLen) + '...' : text;
205
- var textLines = wrapText(ctx, displayText, contentW - 32, 30);
206
- var textH = textLines.length * 30;
193
+ function renderShareCard(range) {
194
+ // Populate card content
195
+ cardSite.textContent = siteName;
196
+ cardTitle.textContent = articleTitle;
207
197
 
208
- var qrSize = 120;
209
- var siteNameH = 40;
210
- var separatorGap = 24;
211
- var quoteTopPad = 24;
212
- var quoteBottomPad = 24;
213
- var qrTopPad = 32;
214
- var qrBottomPad = 16;
215
- var bottomPad = 32;
198
+ // Clone selected DOM fragment with rich formatting
199
+ var fragment = range.cloneContents();
200
+ cardBody.innerHTML = '';
201
+ cardBody.appendChild(fragment);
216
202
 
217
- var H = pad + siteNameH + titleH + separatorGap * 2 + quoteTopPad + textH + quoteBottomPad + qrTopPad + qrSize + qrBottomPad + 20 + bottomPad;
203
+ // Copy computed styles for elements that need them (e.g. KaTeX)
204
+ var allStyles = document.querySelectorAll('style, link[rel="stylesheet"]');
205
+ var styleClones = [];
206
+ allStyles.forEach(function(s) {
207
+ styleClones.push(s.cloneNode(true));
208
+ });
218
209
 
219
- canvas.width = W * dpr;
220
- canvas.height = H * dpr;
221
- canvas.style.width = W + 'px';
222
- canvas.style.height = H + 'px';
223
- ctx.scale(dpr, dpr);
210
+ // Render QR code
211
+ renderQR();
224
212
 
225
- // Background
226
- ctx.fillStyle = '#ffffff';
227
- ctx.beginPath();
228
- ctx.roundRect(0, 0, W, H, 12);
229
- ctx.fill();
230
-
231
- var y = pad;
232
-
233
- // Site name
234
- ctx.fillStyle = '#999999';
235
- ctx.font = '16px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif';
236
- ctx.fillText(siteName, pad, y + 16);
237
- y += siteNameH;
238
-
239
- // Title
240
- ctx.fillStyle = '#1a1a1a';
241
- ctx.font = 'bold 28px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif';
242
- for (var i = 0; i < titleLines.length; i++) {
243
- ctx.fillText(titleLines[i], pad, y + 28);
244
- y += 36;
245
- }
246
- y += separatorGap;
213
+ // Show card for html2canvas to capture
214
+ shareCard.classList.add('rendering');
247
215
 
248
- // Separator
249
- ctx.strokeStyle = '#e5e5e5';
250
- ctx.lineWidth = 1;
251
- ctx.beginPath();
252
- ctx.moveTo(pad, y);
253
- ctx.lineTo(W - pad, y);
254
- ctx.stroke();
255
- y += separatorGap;
256
-
257
- // Quote block background
258
- var quoteBlockY = y;
259
- var quoteBlockH = quoteTopPad + textH + quoteBottomPad;
260
- ctx.fillStyle = '#f8f9fa';
261
- ctx.beginPath();
262
- ctx.roundRect(pad, quoteBlockY, contentW, quoteBlockH, 8);
263
- ctx.fill();
264
-
265
- // Quote accent bar
266
- ctx.fillStyle = '#007bff';
267
- ctx.beginPath();
268
- ctx.roundRect(pad, quoteBlockY, 4, quoteBlockH, 2);
269
- ctx.fill();
270
-
271
- // Quote text
272
- y += quoteTopPad;
273
- ctx.fillStyle = '#333333';
274
- ctx.font = '20px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif';
275
- for (var i = 0; i < textLines.length; i++) {
276
- if (textLines[i] !== '') {
277
- ctx.fillText(textLines[i], pad + 16, y + 20);
216
+ // Wait a frame for layout
217
+ requestAnimationFrame(function() {
218
+ // Enforce minimum 3:4 aspect ratio
219
+ var cardW = shareCard.offsetWidth;
220
+ var cardH = shareCard.offsetHeight;
221
+ var minH = Math.round(cardW * 4 / 3);
222
+ if (cardH < minH) {
223
+ shareCard.style.minHeight = minH + 'px';
278
224
  }
279
- y += 30;
280
- }
281
- y += quoteBottomPad + qrTopPad;
282
-
283
- // QR code
284
- if (typeof qrcode !== 'undefined') {
285
- var qr = qrcode(0, 'M');
286
- qr.addData(window.location.href);
287
- qr.make();
288
- var moduleCount = qr.getModuleCount();
289
- var cellSize = qrSize / moduleCount;
290
- var qrX = W - pad - qrSize;
291
- var qrY = y;
292
225
 
293
- // White background for QR
294
- ctx.fillStyle = '#ffffff';
295
- ctx.fillRect(qrX - 4, qrY - 4, qrSize + 8, qrSize + 8);
226
+ html2canvas(shareCard, {
227
+ scale: 2,
228
+ useCORS: true,
229
+ backgroundColor: '#ffffff',
230
+ logging: false,
231
+ }).then(function(canvas) {
232
+ shareCard.classList.remove('rendering');
233
+ shareCard.style.minHeight = '';
296
234
 
297
- ctx.fillStyle = '#1a1a1a';
298
- for (var r = 0; r < moduleCount; r++) {
299
- for (var c = 0; c < moduleCount; c++) {
300
- if (qr.isDark(r, c)) {
301
- ctx.fillRect(qrX + c * cellSize, qrY + r * cellSize, cellSize + 0.5, cellSize + 0.5);
302
- }
303
- }
304
- }
305
-
306
- // Hint text next to QR
307
- ctx.fillStyle = '#999999';
308
- ctx.font = '14px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif';
309
- ctx.fillText('Scan to read the original', pad, y + qrSize / 2 + 5);
310
- }
235
+ preview.src = canvas.toDataURL('image/png');
236
+ overlay.classList.add('active');
237
+ }).catch(function(err) {
238
+ console.error('Share card render error:', err);
239
+ shareCard.classList.remove('rendering');
240
+ shareCard.style.minHeight = '';
241
+ });
242
+ });
311
243
  }
312
244
  })();
313
245
  `,
@@ -410,7 +342,7 @@ const ContentPage = props => {
410
342
 
411
343
  function renderEmblaCarousels() {
412
344
  // Detect image groups, make them carousels automatically
413
- Map.groupBy(document.querySelectorAll('img'), x => x.parentNode).entries().forEach(([container, images]) => {
345
+ Map.groupBy(document.querySelectorAll('.content-body img'), x => x.parentNode).entries().forEach(([container, images]) => {
414
346
  const outer = document.createElement('div');
415
347
  outer.classList.add('embla');
416
348
  container.appendChild(outer);
package/dist/ssg/style.js CHANGED
@@ -424,7 +424,7 @@ html:not(.dark) body {
424
424
  align-items: center;
425
425
  gap: 16px;
426
426
  }
427
- .share-modal canvas {
427
+ .share-modal img.share-preview {
428
428
  max-width: 100%;
429
429
  height: auto;
430
430
  border-radius: 8px;
@@ -449,5 +449,90 @@ html:not(.dark) body {
449
449
  background: var(--tag-bg);
450
450
  color: var(--tag-text);
451
451
  }
452
+
453
+ /* Share card (off-screen DOM for html2canvas capture) */
454
+ .share-card {
455
+ display: none;
456
+ position: fixed;
457
+ left: -9999px;
458
+ top: 0;
459
+ width: 540px;
460
+ min-height: 720px;
461
+ background: #ffffff;
462
+ border-radius: 12px;
463
+ padding: 36px;
464
+ box-sizing: border-box;
465
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
466
+ color: #1a1a1a;
467
+ flex-direction: column;
468
+ pointer-events: none;
469
+ }
470
+ .share-card.rendering {
471
+ display: flex;
472
+ }
473
+ .share-card-header {
474
+ display: flex;
475
+ justify-content: space-between;
476
+ align-items: flex-start;
477
+ gap: 12px;
478
+ margin-bottom: 20px;
479
+ }
480
+ .share-card-header-left {
481
+ flex: 1;
482
+ min-width: 0;
483
+ }
484
+ .share-card-site {
485
+ font-size: 14px;
486
+ color: #999999;
487
+ margin-bottom: 8px;
488
+ }
489
+ .share-card-title {
490
+ font-size: 24px;
491
+ font-weight: 700;
492
+ color: #1a1a1a;
493
+ line-height: 1.3;
494
+ word-wrap: break-word;
495
+ }
496
+ .share-card-qr {
497
+ flex-shrink: 0;
498
+ display: flex;
499
+ flex-direction: column;
500
+ align-items: center;
501
+ }
502
+ .share-card-qr canvas {
503
+ display: block;
504
+ }
505
+ .share-card-qr-hint {
506
+ font-size: 10px;
507
+ color: #bbbbbb;
508
+ margin-top: 4px;
509
+ white-space: nowrap;
510
+ }
511
+ .share-card-divider {
512
+ height: 1px;
513
+ background: #e5e5e5;
514
+ margin-bottom: 20px;
515
+ }
516
+ .share-card-body {
517
+ flex: 1;
518
+ background: #f8f9fa;
519
+ border-radius: 8px;
520
+ padding: 20px;
521
+ font-size: 18px;
522
+ line-height: 1.8;
523
+ color: #333333;
524
+ overflow: hidden;
525
+ }
526
+ .share-card-body img {
527
+ max-width: 100%;
528
+ height: auto;
529
+ }
530
+ .share-card-body pre {
531
+ white-space: pre-wrap;
532
+ word-wrap: break-word;
533
+ }
534
+ .share-card-body strong {
535
+ color: #ff5722;
536
+ }
452
537
  `;
453
538
  //# sourceMappingURL=style.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "czon",
3
- "version": "0.9.7",
3
+ "version": "0.9.8",
4
4
  "description": "CZON - AI enhanced Markdown content engine",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",