@hirokisakabe/pom-cli 0.2.3 → 0.2.4

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
@@ -21,6 +21,8 @@ pom preview slides.pom.md
21
21
 
22
22
  Open http://localhost:3000 in your browser. The page updates automatically when the file is saved.
23
23
 
24
+ Use the zoom buttons in the toolbar or press `+` / `-` to zoom in and out. The current zoom level is saved across sessions.
25
+
24
26
  ### Build
25
27
 
26
28
  Converts a pom file to a PPTX file.
@@ -1 +1 @@
1
- {"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../src/preview.ts"],"names":[],"mappings":"AA2OA,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAyGlD"}
1
+ {"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../src/preview.ts"],"names":[],"mappings":"AA8WA,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAyGlD"}
package/dist/preview.js CHANGED
@@ -66,78 +66,188 @@ async function generateSvgs(inputFile) {
66
66
  const svgs = slides.map((s) => s.svg);
67
67
  return { type: "success", svgs, slideWidth };
68
68
  }
69
- function buildPreviewHtml() {
69
+ function escapeHtml(s) {
70
+ return s
71
+ .replace(/&/g, "&")
72
+ .replace(/</g, "&lt;")
73
+ .replace(/>/g, "&gt;")
74
+ .replace(/"/g, "&quot;");
75
+ }
76
+ function buildPreviewHtml(filename) {
77
+ const safeFilename = escapeHtml(filename);
70
78
  return `<!DOCTYPE html>
71
79
  <html>
72
80
  <head>
73
81
  <meta charset="UTF-8">
74
- <title>pom preview</title>
82
+ <title>pom — ${safeFilename}</title>
75
83
  <style>
76
- * { box-sizing: border-box; }
77
- body { margin: 0; padding: 0; background: #f5f5f5; font-family: sans-serif; }
84
+ * { box-sizing: border-box; margin: 0; padding: 0; }
85
+ body {
86
+ background: #0f0f13;
87
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
88
+ color: #e2e2e8;
89
+ min-height: 100vh;
90
+ }
78
91
  .toolbar {
79
92
  position: sticky; top: 0; z-index: 100;
80
- display: flex; align-items: center; gap: 4px;
81
- padding: 6px 16px; background: #fff;
82
- border-bottom: 1px solid #ddd;
93
+ display: flex; align-items: center; gap: 12px;
94
+ padding: 0 20px; height: 48px;
95
+ background: #1a1a2e;
96
+ border-bottom: 1px solid #2d2d4e;
97
+ }
98
+ .app-name {
99
+ font-size: 13px; font-weight: 700;
100
+ color: #a78bfa; letter-spacing: 0.06em;
101
+ flex-shrink: 0;
83
102
  }
103
+ .filename {
104
+ font-size: 12px; color: #94a3b8;
105
+ font-family: 'SF Mono', 'Fira Code', Consolas, monospace;
106
+ background: #0f172a; border: 1px solid #2d2d4e;
107
+ padding: 2px 8px; border-radius: 4px;
108
+ flex-shrink: 1; min-width: 0; overflow: hidden;
109
+ text-overflow: ellipsis; white-space: nowrap;
110
+ }
111
+ .zoom-controls { display: flex; gap: 4px; flex-shrink: 0; }
84
112
  .zoom-btn {
85
- padding: 3px 10px; font-size: 12px;
86
- border: 1px solid #ccc; border-radius: 3px;
87
- background: #fff; color: #333; cursor: pointer;
113
+ padding: 4px 10px; font-size: 11px;
114
+ border: 1px solid #3d3d5e; border-radius: 4px;
115
+ background: #2d2d4e; color: #c4c4d4; cursor: pointer;
116
+ transition: background 0.15s, color 0.15s;
117
+ }
118
+ .zoom-btn:hover { background: #3d3d6e; color: #e2e2f2; }
119
+ .zoom-btn.active { background: #7c3aed; color: #fff; border-color: #7c3aed; }
120
+ .zoom-hint { font-size: 11px; color: #3d3d5e; flex-shrink: 0; }
121
+ .status-group {
122
+ margin-left: auto; display: flex; align-items: center;
123
+ gap: 6px; flex-shrink: 0;
124
+ }
125
+ .status-dot {
126
+ width: 8px; height: 8px; border-radius: 50%;
127
+ background: #f59e0b;
128
+ transition: background 0.3s, box-shadow 0.3s;
88
129
  }
89
- .zoom-btn:hover { background: #e8e8e8; }
90
- .zoom-btn.active { background: #007acc; color: #fff; border-color: #007acc; }
91
- .status { margin-left: auto; font-size: 12px; color: #888; }
92
- .slides-container { padding: 16px; }
93
- .slide-wrapper { margin-bottom: 24px; }
94
- .slide-label { font-size: 12px; color: #888; margin-bottom: 4px; }
130
+ .status-dot.connected { background: #22c55e; box-shadow: 0 0 6px #22c55e88; }
131
+ .status-dot.warning { background: #f59e0b; box-shadow: 0 0 6px #f59e0b88; }
132
+ .status-dot.error { background: #ef4444; box-shadow: 0 0 6px #ef444488; }
133
+ .status-text { font-size: 12px; color: #94a3b8; }
134
+ .slides-container {
135
+ padding: 32px 20px;
136
+ display: flex; flex-direction: column;
137
+ align-items: center; gap: 32px;
138
+ }
139
+ .slide-wrapper { width: 100%; display: flex; justify-content: center; }
95
140
  .slide-frame {
96
- border: 1px solid #ddd; border-radius: 4px;
97
- overflow: hidden; background: #fff; display: inline-block;
141
+ position: relative;
142
+ border-radius: 8px; overflow: hidden; background: #fff;
143
+ box-shadow: 0 8px 32px rgba(0,0,0,0.6), 0 2px 8px rgba(0,0,0,0.4);
98
144
  }
99
145
  .slide-frame svg { display: block; }
100
- .error-banner {
101
- background: #fee; border: 1px solid #fcc;
102
- border-radius: 4px; padding: 12px; margin: 16px; color: #c00;
146
+ .slide-number {
147
+ position: absolute; bottom: 10px; right: 12px;
148
+ font-size: 11px; font-weight: 500;
149
+ color: rgba(255,255,255,0.8);
150
+ background: rgba(0,0,0,0.5);
151
+ padding: 2px 8px; border-radius: 3px;
152
+ font-family: 'SF Mono', 'Fira Code', Consolas, monospace;
153
+ pointer-events: none; user-select: none;
154
+ }
155
+ .loading-screen {
156
+ display: flex; flex-direction: column;
157
+ align-items: center; justify-content: center;
158
+ height: calc(100vh - 48px); gap: 16px;
159
+ }
160
+ .spinner {
161
+ width: 32px; height: 32px;
162
+ border: 3px solid #2d2d4e;
163
+ border-top-color: #7c3aed;
164
+ border-radius: 50%;
165
+ animation: spin 0.8s linear infinite;
103
166
  }
104
- .loading-message, .empty-message {
167
+ @keyframes spin { to { transform: rotate(360deg); } }
168
+ .loading-text { font-size: 13px; color: #555578; }
169
+ .empty-screen {
105
170
  display: flex; align-items: center; justify-content: center;
106
- height: calc(100vh - 40px); color: #888; flex-direction: column; gap: 8px;
171
+ height: calc(100vh - 48px);
172
+ font-size: 13px; color: #555578;
173
+ }
174
+ .error-screen { padding: 32px; display: flex; justify-content: center; }
175
+ .error-block {
176
+ max-width: 720px; width: 100%;
177
+ background: #1a0a0a; border: 1px solid #5c1a1a;
178
+ border-radius: 8px; overflow: hidden;
179
+ }
180
+ .error-header {
181
+ padding: 10px 16px; background: #2a0a0a;
182
+ border-bottom: 1px solid #5c1a1a;
183
+ font-size: 12px; font-weight: 600; color: #f87171;
184
+ }
185
+ .error-body {
186
+ padding: 14px 16px;
187
+ font-family: 'SF Mono', 'Fira Code', Consolas, monospace;
188
+ font-size: 12px; color: #fca5a5; line-height: 1.6;
189
+ white-space: pre-wrap; word-break: break-all;
107
190
  }
108
191
  </style>
109
192
  </head>
110
193
  <body>
111
194
  <div class="toolbar">
112
- <button class="zoom-btn" data-zoom="fit">Fit to Width</button>
113
- <button class="zoom-btn" data-zoom="50">50%</button>
114
- <button class="zoom-btn" data-zoom="75">75%</button>
115
- <button class="zoom-btn" data-zoom="100">100%</button>
116
- <button class="zoom-btn" data-zoom="150">150%</button>
117
- <span class="status" id="status">Connecting...</span>
195
+ <span class="app-name">pom</span>
196
+ <span class="filename">${safeFilename}</span>
197
+ <div class="zoom-controls">
198
+ <button class="zoom-btn" data-zoom="fit">Fit</button>
199
+ <button class="zoom-btn" data-zoom="50">50%</button>
200
+ <button class="zoom-btn" data-zoom="75">75%</button>
201
+ <button class="zoom-btn" data-zoom="100">100%</button>
202
+ <button class="zoom-btn" data-zoom="150">150%</button>
203
+ </div>
204
+ <span class="zoom-hint">+ / −</span>
205
+ <div class="status-group">
206
+ <span class="status-dot warning" id="statusDot"></span>
207
+ <span class="status-text" id="statusText">Connecting...</span>
208
+ </div>
209
+ </div>
210
+ <div id="content">
211
+ <div class="loading-screen">
212
+ <div class="spinner"></div>
213
+ <span class="loading-text">Building preview...</span>
214
+ </div>
118
215
  </div>
119
- <div id="content"><div class="loading-message"><span>Building preview...</span></div></div>
120
216
 
121
217
  <script>
122
218
  (function() {
123
- var VALID_ZOOMS = ['fit', '50', '75', '100', '150'];
219
+ var ZOOM_STEPS = ['fit', '50', '75', '100', '150'];
124
220
  var currentZoom = localStorage.getItem('pom-zoom') || 'fit';
125
221
  var currentSlideWidth = 1280;
126
222
 
223
+ if (ZOOM_STEPS.indexOf(currentZoom) === -1) currentZoom = 'fit';
127
224
  applyZoom(currentZoom);
128
225
 
129
226
  document.querySelectorAll('.zoom-btn').forEach(function(btn) {
130
227
  btn.addEventListener('click', function() {
131
- var zoom = this.getAttribute('data-zoom');
132
- applyZoom(zoom);
133
- localStorage.setItem('pom-zoom', zoom);
228
+ setZoom(this.getAttribute('data-zoom'));
134
229
  });
135
230
  });
136
231
 
232
+ document.addEventListener('keydown', function(e) {
233
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
234
+ if (e.key === '+' || e.key === '=') {
235
+ var idx = ZOOM_STEPS.indexOf(currentZoom);
236
+ if (idx < ZOOM_STEPS.length - 1) setZoom(ZOOM_STEPS[idx + 1]);
237
+ } else if (e.key === '-') {
238
+ var idx = ZOOM_STEPS.indexOf(currentZoom);
239
+ if (idx > 0) setZoom(ZOOM_STEPS[idx - 1]);
240
+ }
241
+ });
242
+
243
+ function setZoom(zoom) {
244
+ localStorage.setItem('pom-zoom', zoom);
245
+ applyZoom(zoom);
246
+ }
247
+
137
248
  function applyZoom(zoom) {
138
- if (VALID_ZOOMS.indexOf(zoom) === -1) zoom = 'fit';
249
+ if (ZOOM_STEPS.indexOf(zoom) === -1) zoom = 'fit';
139
250
  currentZoom = zoom;
140
- document.body.setAttribute('data-zoom', zoom);
141
251
  document.querySelectorAll('.zoom-btn').forEach(function(b) {
142
252
  b.classList.toggle('active', b.getAttribute('data-zoom') === zoom);
143
253
  });
@@ -149,35 +259,45 @@ function buildPreviewHtml() {
149
259
  function applySvgZoom(svg, zoom, slideWidth) {
150
260
  var frame = svg.closest('.slide-frame');
151
261
  if (zoom === 'fit') {
262
+ frame.style.width = '100%';
263
+ frame.style.maxWidth = slideWidth + 'px';
152
264
  svg.style.width = '100%';
153
265
  svg.style.height = 'auto';
154
- frame.style.display = 'block';
155
266
  } else {
156
267
  var scale = parseInt(zoom) / 100;
268
+ frame.style.width = (slideWidth * scale) + 'px';
269
+ frame.style.maxWidth = '';
157
270
  svg.style.width = (slideWidth * scale) + 'px';
158
271
  svg.style.height = 'auto';
159
- frame.style.display = 'inline-block';
160
272
  }
161
273
  }
162
274
 
163
- var status = document.getElementById('status');
275
+ var statusDot = document.getElementById('statusDot');
276
+ var statusText = document.getElementById('statusText');
164
277
  var content = document.getElementById('content');
165
278
 
279
+ function setStatus(state, text) {
280
+ statusDot.className = 'status-dot ' + state;
281
+ statusText.textContent = text;
282
+ }
283
+
166
284
  var es = new EventSource('/_sse');
167
285
 
168
286
  es.addEventListener('open', function() {
169
- status.textContent = 'Connected';
287
+ setStatus('connected', 'Connected');
170
288
  });
171
289
 
172
290
  es.addEventListener('update', function(e) {
173
291
  var data = JSON.parse(e.data);
174
292
  if (data.type === 'success') {
175
293
  currentSlideWidth = data.slideWidth;
176
- status.textContent = 'Updated ' + new Date().toLocaleTimeString();
294
+ setStatus('connected', 'Updated ' + new Date().toLocaleTimeString());
295
+ var total = data.svgs.length;
177
296
  var slideHtml = data.svgs.map(function(svg, i) {
178
297
  return '<div class="slide-wrapper">' +
179
- '<div class="slide-label">Slide ' + (i + 1) + '</div>' +
180
- '<div class="slide-frame">' + svg + '</div>' +
298
+ '<div class="slide-frame">' + svg +
299
+ '<span class="slide-number">' + (i + 1) + ' / ' + total + '</span>' +
300
+ '</div>' +
181
301
  '</div>';
182
302
  }).join('');
183
303
  content.innerHTML = '<div class="slides-container">' + slideHtml + '</div>';
@@ -185,21 +305,31 @@ function buildPreviewHtml() {
185
305
  applySvgZoom(svgEl, currentZoom, currentSlideWidth);
186
306
  });
187
307
  } else if (data.type === 'error') {
188
- status.textContent = 'Error';
308
+ setStatus('error', 'Error');
189
309
  var escaped = data.message
190
310
  .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
191
- content.innerHTML = '<div class="error-banner"><strong>Error:</strong> ' + escaped + '</div>';
311
+ content.innerHTML =
312
+ '<div class="error-screen">' +
313
+ '<div class="error-block">' +
314
+ '<div class="error-header">&#9888; Build Error</div>' +
315
+ '<pre class="error-body">' + escaped + '</pre>' +
316
+ '</div>' +
317
+ '</div>';
192
318
  } else if (data.type === 'empty') {
193
- status.textContent = 'No slides';
194
- content.innerHTML = '<div class="empty-message">No slides to preview</div>';
319
+ setStatus('connected', 'No slides');
320
+ content.innerHTML = '<div class="empty-screen">No slides to preview</div>';
195
321
  } else if (data.type === 'building') {
196
- status.textContent = 'Building...';
197
- content.innerHTML = '<div class="loading-message"><span>Building preview...</span></div>';
322
+ setStatus('warning', 'Building...');
323
+ content.innerHTML =
324
+ '<div class="loading-screen">' +
325
+ '<div class="spinner"></div>' +
326
+ '<span class="loading-text">Building preview...</span>' +
327
+ '</div>';
198
328
  }
199
329
  });
200
330
 
201
331
  es.addEventListener('error', function() {
202
- status.textContent = 'Disconnected — retrying...';
332
+ setStatus('error', 'Disconnected — retrying...');
203
333
  });
204
334
  })();
205
335
  </script>
@@ -257,7 +387,7 @@ export function runPreview(inputFile) {
257
387
  clearTimeout(debounceTimer);
258
388
  debounceTimer = setTimeout(refresh, 100);
259
389
  });
260
- const html = buildPreviewHtml();
390
+ const html = buildPreviewHtml(path.basename(absInput));
261
391
  const server = http.createServer((req, res) => {
262
392
  if (req.url === "/_sse") {
263
393
  res.writeHead(200, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hirokisakabe/pom-cli",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "CLI tool for pom — preview and build presentations",
5
5
  "type": "module",
6
6
  "bin": {