focomy 0.1.119__py3-none-any.whl → 0.1.121__py3-none-any.whl

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.
core/engine/routes.py CHANGED
@@ -161,6 +161,7 @@ async def render_theme(
161
161
  context["is_admin"] = True
162
162
  context["admin_user"] = admin_info["user_data"]
163
163
  context["active_theme"] = active_theme # For customize link
164
+ context["csrf_token"] = getattr(request.state, "csrf_token", "") # For API calls
164
165
  # Build edit URL if entity provided
165
166
  if entity and content_type:
166
167
  context["edit_url"] = f"/admin/{content_type}/{entity.id}/edit"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: focomy
3
- Version: 0.1.119
3
+ Version: 0.1.121
4
4
  Summary: The Most Beautiful CMS - A metadata-driven, zero-duplicate-code content management system
5
5
  Project-URL: Homepage, https://github.com/focomy/focomy
6
6
  Project-URL: Documentation, https://focomy.dev/docs
@@ -44,7 +44,7 @@ core/content_types/user.yaml,sha256=y3SwqzIc9_6C7R1GULk7AwYJPxcTT38ZmZe4_wekfyU,
44
44
  core/content_types/widget.yaml,sha256=Jotbts5QQtHaF2bJWQL3rkEoCkp_aq_A3gN-58eJwv8,1454
45
45
  core/content_types/workflow_history.yaml,sha256=3wi58LNLYbk7t6Z2QDRi9whQSedJCXKVKuyBhixNUK0,518
46
46
  core/engine/__init__.py,sha256=ycR0Kdn6buwdCH6QFG8bV69wFciFSKEg9Ro26cHpa2U,83
47
- core/engine/routes.py,sha256=r81cRjXcSpCMqr4CZDrfmGQnupPE7_yVAMLJL9psRiA,43922
47
+ core/engine/routes.py,sha256=O4Olf0yzLzhsMydB9eFbZhGirGlsV2s4lkRSQDcWsA0,44016
48
48
  core/migrations/env.py,sha256=1dLI8qcGojLDR_--MdgwP5q-V0p2Z-32klSPjokXx4M,1389
49
49
  core/migrations/script.py.mako,sha256=LyYLSC7HzBBGwHZ8s2SguBPMXsWCph0FJp49kPsGhU8,590
50
50
  core/migrations/versions/2038bdf6693b_add_import_jobs_table.py,sha256=v8lPC5WmwpUfHUG_YgQn6jepPtfKWFn0JIj9XvD9224,2325
@@ -189,7 +189,7 @@ themes/default/theme.yaml,sha256=tgcUP1YFptyXVNL2a8DBiPrP7zTjWNH62Cy9D_w6Chk,187
189
189
  themes/default/templates/404.html,sha256=6pYUz7zg5hx3nikgxiZWSkwYnv2nENCSV3KjdIF0_lE,1105
190
190
  themes/default/templates/500.html,sha256=CtU3gEsHsxAh-vbcnx5McH8V8ruKtdP8usj8hPuu8zY,1174
191
191
  themes/default/templates/archive.html,sha256=ZHBxPYewvc2TbrsB745LYO2uM5SJbTFQQR6savWUzYg,2385
192
- themes/default/templates/base.html,sha256=7M0D8J5SX-Y4V8_GYVO22Rk7BOx7Ob_OFfcjpNYBuBo,12635
192
+ themes/default/templates/base.html,sha256=mbT1wxaD9gygkod7sFPAda-uQsvfV-AT_Jzvn23eR1s,34012
193
193
  themes/default/templates/category.html,sha256=k-yN0vFoOpgxgg6DlGin5X4IzVDBG9xRZ0FOD7OJtU8,3061
194
194
  themes/default/templates/channel.html,sha256=1i1zkAWmvpcqyoEfaeQNDc2zrMao2xSXCkjRuwzxOUU,3213
195
195
  themes/default/templates/form.html,sha256=KFrFS6qxHELPrpRB0B_BNU-uqM3k11oMYwd6oY3qoPQ,8685
@@ -204,8 +204,8 @@ themes/minimal/templates/base.html,sha256=LFkx-XLDMGH7oFHHa0e6KPB0DJITOBvr6GtPkD
204
204
  themes/minimal/templates/home.html,sha256=ygYQgYj1OGCiKwmfsxwkPselVKT8vDH3jLLbfphpqKI,1577
205
205
  themes/minimal/templates/page.html,sha256=7Xcoq-ryaxlp913H2S1ishrAro2wsqqGmvsm1osXxd4,389
206
206
  themes/minimal/templates/post.html,sha256=FkTRHci8HNIIi3DU6Mb3oL0aDisGyDcsT_IUDwHmrvo,1387
207
- focomy-0.1.119.dist-info/METADATA,sha256=oY4MenoazpK5kOyrGpp65GOSipjHfzdKbaPYMfeYzHo,7042
208
- focomy-0.1.119.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
209
- focomy-0.1.119.dist-info/entry_points.txt,sha256=_rF-wxGI1axY7gox3DBsTLHq-JrFKkMCjA65a6b_oqE,41
210
- focomy-0.1.119.dist-info/licenses/LICENSE,sha256=z9Z7gN7NNV7zYCaY-Knh3bv8RBCu89VueYtAlN_-lro,1063
211
- focomy-0.1.119.dist-info/RECORD,,
207
+ focomy-0.1.121.dist-info/METADATA,sha256=ZH2pjblW1icUgF-ajYHNgHqDvhd6cJXCI43S-I-ZFDk,7042
208
+ focomy-0.1.121.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
209
+ focomy-0.1.121.dist-info/entry_points.txt,sha256=_rF-wxGI1axY7gox3DBsTLHq-JrFKkMCjA65a6b_oqE,41
210
+ focomy-0.1.121.dist-info/licenses/LICENSE,sha256=z9Z7gN7NNV7zYCaY-Knh3bv8RBCu89VueYtAlN_-lro,1063
211
+ focomy-0.1.121.dist-info/RECORD,,
@@ -146,7 +146,7 @@
146
146
  {% endif %}
147
147
  </div>
148
148
  </div>
149
- <a href="/admin/themes/{{ active_theme }}/customize">カスタマイズ</a>
149
+ <button type="button" id="customize-toggle" class="admin-bar-btn">カスタマイズ</button>
150
150
  {% if edit_url %}
151
151
  <a href="{{ edit_url }}">編集</a>
152
152
  {% endif %}
@@ -224,8 +224,562 @@
224
224
  .admin-bar-dropdown:hover .admin-bar-dropdown-menu {
225
225
  display: block;
226
226
  }
227
+ .admin-bar-btn {
228
+ background: transparent;
229
+ border: none;
230
+ color: white;
231
+ cursor: pointer;
232
+ font-size: inherit;
233
+ padding: 0;
234
+ }
235
+ .admin-bar-btn:hover {
236
+ text-decoration: underline;
237
+ }
227
238
  body { padding-top: 32px; }
239
+
240
+ /* Customize Panel */
241
+ #customize-panel {
242
+ display: none;
243
+ position: fixed;
244
+ top: 32px;
245
+ left: 0;
246
+ width: 320px;
247
+ height: calc(100vh - 32px);
248
+ background: #1e293b;
249
+ color: white;
250
+ z-index: 9998;
251
+ box-shadow: 2px 0 10px rgba(0,0,0,0.3);
252
+ overflow-y: auto;
253
+ }
254
+ #customize-panel.open {
255
+ display: block;
256
+ }
257
+ #customize-panel-header {
258
+ display: flex;
259
+ justify-content: space-between;
260
+ align-items: center;
261
+ padding: 1rem;
262
+ border-bottom: 1px solid #374151;
263
+ }
264
+ #customize-panel-header h2 {
265
+ margin: 0;
266
+ font-size: 1rem;
267
+ font-weight: 600;
268
+ }
269
+ #customize-panel-close {
270
+ background: transparent;
271
+ border: none;
272
+ color: #94a3b8;
273
+ cursor: pointer;
274
+ font-size: 1.25rem;
275
+ padding: 0;
276
+ }
277
+ #customize-panel-close:hover {
278
+ color: white;
279
+ }
280
+ .customize-section {
281
+ padding: 1rem;
282
+ border-bottom: 1px solid #374151;
283
+ }
284
+ .customize-section h3 {
285
+ font-size: 0.75rem;
286
+ font-weight: 600;
287
+ text-transform: uppercase;
288
+ color: #94a3b8;
289
+ margin: 0 0 0.75rem 0;
290
+ }
291
+ .customize-field {
292
+ margin-bottom: 0.75rem;
293
+ }
294
+ .customize-field label {
295
+ display: block;
296
+ font-size: 0.8125rem;
297
+ margin-bottom: 0.25rem;
298
+ }
299
+ .customize-field input[type="color"] {
300
+ width: 40px;
301
+ height: 28px;
302
+ padding: 0;
303
+ border: 1px solid #374151;
304
+ border-radius: 4px;
305
+ cursor: pointer;
306
+ }
307
+ .customize-field input[type="text"] {
308
+ width: 100%;
309
+ padding: 0.5rem;
310
+ background: #0f172a;
311
+ border: 1px solid #374151;
312
+ border-radius: 4px;
313
+ color: white;
314
+ font-size: 0.8125rem;
315
+ }
316
+ .customize-field textarea {
317
+ width: 100%;
318
+ min-height: 120px;
319
+ padding: 0.5rem;
320
+ background: #0f172a;
321
+ border: 1px solid #374151;
322
+ border-radius: 4px;
323
+ color: #d4d4d4;
324
+ font-family: monospace;
325
+ font-size: 0.75rem;
326
+ resize: vertical;
327
+ }
328
+ .color-row {
329
+ display: flex;
330
+ align-items: center;
331
+ gap: 0.5rem;
332
+ }
333
+ .color-row input[type="text"] {
334
+ flex: 1;
335
+ font-family: monospace;
336
+ }
337
+ .image-field {
338
+ display: flex;
339
+ flex-direction: column;
340
+ gap: 0.5rem;
341
+ }
342
+ .image-field input[type="url"] {
343
+ width: 100%;
344
+ padding: 0.5rem;
345
+ background: #0f172a;
346
+ border: 1px solid #374151;
347
+ border-radius: 4px;
348
+ color: white;
349
+ font-size: 0.75rem;
350
+ }
351
+ .image-preview {
352
+ width: 100%;
353
+ height: 60px;
354
+ border: 1px dashed #374151;
355
+ border-radius: 4px;
356
+ display: flex;
357
+ align-items: center;
358
+ justify-content: center;
359
+ background: #0f172a;
360
+ overflow: hidden;
361
+ }
362
+ .image-preview img {
363
+ max-width: 100%;
364
+ max-height: 100%;
365
+ object-fit: contain;
366
+ }
367
+ .image-preview .no-image {
368
+ color: #64748b;
369
+ font-size: 0.75rem;
370
+ }
371
+ .image-field-actions {
372
+ display: flex;
373
+ gap: 0.5rem;
374
+ }
375
+ .image-field-actions button {
376
+ padding: 0.25rem 0.5rem;
377
+ font-size: 0.6875rem;
378
+ background: transparent;
379
+ border: 1px solid #374151;
380
+ color: #94a3b8;
381
+ border-radius: 4px;
382
+ cursor: pointer;
383
+ }
384
+ .image-field-actions button:hover {
385
+ background: #374151;
386
+ color: white;
387
+ }
388
+ #customize-panel-footer {
389
+ padding: 1rem;
390
+ display: flex;
391
+ gap: 0.5rem;
392
+ justify-content: flex-end;
393
+ }
394
+ #customize-panel-footer button {
395
+ padding: 0.5rem 1rem;
396
+ border-radius: 4px;
397
+ font-size: 0.8125rem;
398
+ cursor: pointer;
399
+ }
400
+ #customize-reset {
401
+ background: transparent;
402
+ border: 1px solid #374151;
403
+ color: #94a3b8;
404
+ }
405
+ #customize-reset:hover {
406
+ background: #374151;
407
+ color: white;
408
+ }
409
+ #customize-save {
410
+ background: #2563eb;
411
+ border: none;
412
+ color: white;
413
+ }
414
+ #customize-save:hover {
415
+ background: #1d4ed8;
416
+ }
417
+ #customize-save:disabled {
418
+ background: #374151;
419
+ cursor: not-allowed;
420
+ }
421
+ .customize-message {
422
+ padding: 0.5rem 1rem;
423
+ margin: 0 1rem;
424
+ border-radius: 4px;
425
+ font-size: 0.8125rem;
426
+ }
427
+ .customize-message.success {
428
+ background: #22c55e20;
429
+ color: #22c55e;
430
+ }
431
+ .customize-message.error {
432
+ background: #ef444420;
433
+ color: #ef4444;
434
+ }
228
435
  </style>
436
+
437
+ <!-- Customize Panel -->
438
+ <div id="customize-panel">
439
+ <div id="customize-panel-header">
440
+ <h2>カスタマイズ</h2>
441
+ <button type="button" id="customize-panel-close">&times;</button>
442
+ </div>
443
+ <div id="customize-panel-content">
444
+ <div class="customize-section">
445
+ <h3>サイトID</h3>
446
+ <div class="customize-field">
447
+ <label>サイトロゴ</label>
448
+ <div class="image-field">
449
+ <input type="url" id="site_logo" placeholder="https://example.com/logo.png">
450
+ <div class="image-preview" id="site_logo_preview">
451
+ <span class="no-image">画像未設定</span>
452
+ </div>
453
+ <div class="image-field-actions">
454
+ <button type="button" onclick="clearImageField('site_logo')">クリア</button>
455
+ </div>
456
+ </div>
457
+ </div>
458
+ <div class="customize-field">
459
+ <label>サイトアイコン</label>
460
+ <div class="image-field">
461
+ <input type="url" id="site_icon" placeholder="https://example.com/icon.png">
462
+ <div class="image-preview" id="site_icon_preview">
463
+ <span class="no-image">画像未設定</span>
464
+ </div>
465
+ <div class="image-field-actions">
466
+ <button type="button" onclick="clearImageField('site_icon')">クリア</button>
467
+ </div>
468
+ </div>
469
+ </div>
470
+ </div>
471
+ <div class="customize-section">
472
+ <h3>ヘッダー・背景</h3>
473
+ <div class="customize-field">
474
+ <label>ヘッダー画像</label>
475
+ <div class="image-field">
476
+ <input type="url" id="header_image" placeholder="https://example.com/header.jpg">
477
+ <div class="image-preview" id="header_image_preview">
478
+ <span class="no-image">画像未設定</span>
479
+ </div>
480
+ <div class="image-field-actions">
481
+ <button type="button" onclick="clearImageField('header_image')">クリア</button>
482
+ </div>
483
+ </div>
484
+ </div>
485
+ <div class="customize-field">
486
+ <label>背景画像</label>
487
+ <div class="image-field">
488
+ <input type="url" id="background_image" placeholder="https://example.com/bg.jpg">
489
+ <div class="image-preview" id="background_image_preview">
490
+ <span class="no-image">画像未設定</span>
491
+ </div>
492
+ <div class="image-field-actions">
493
+ <button type="button" onclick="clearImageField('background_image')">クリア</button>
494
+ </div>
495
+ </div>
496
+ </div>
497
+ </div>
498
+ <div class="customize-section">
499
+ <h3>カラー</h3>
500
+ <div class="customize-field">
501
+ <label>メインカラー</label>
502
+ <div class="color-row">
503
+ <input type="color" id="color_primary" value="#2563eb">
504
+ <input type="text" id="color_primary_text" value="#2563eb" pattern="^#[0-9A-Fa-f]{6}$">
505
+ </div>
506
+ </div>
507
+ <div class="customize-field">
508
+ <label>背景色</label>
509
+ <div class="color-row">
510
+ <input type="color" id="color_background" value="#ffffff">
511
+ <input type="text" id="color_background_text" value="#ffffff" pattern="^#[0-9A-Fa-f]{6}$">
512
+ </div>
513
+ </div>
514
+ <div class="customize-field">
515
+ <label>文字色</label>
516
+ <div class="color-row">
517
+ <input type="color" id="color_text" value="#1e293b">
518
+ <input type="text" id="color_text_text" value="#1e293b" pattern="^#[0-9A-Fa-f]{6}$">
519
+ </div>
520
+ </div>
521
+ </div>
522
+ <div class="customize-section">
523
+ <h3>カスタムCSS</h3>
524
+ <div class="customize-field">
525
+ <textarea id="custom_css" placeholder="/* CSSを入力 */"></textarea>
526
+ </div>
527
+ </div>
528
+ </div>
529
+ <div id="customize-panel-footer">
530
+ <button type="button" id="customize-reset">リセット</button>
531
+ <button type="button" id="customize-save">保存</button>
532
+ </div>
533
+ </div>
534
+
535
+ <script>
536
+ (function() {
537
+ const panel = document.getElementById('customize-panel');
538
+ const toggleBtn = document.getElementById('customize-toggle');
539
+ const closeBtn = document.getElementById('customize-panel-close');
540
+ const saveBtn = document.getElementById('customize-save');
541
+ const resetBtn = document.getElementById('customize-reset');
542
+ const csrfToken = '{{ csrf_token }}';
543
+ const themeName = '{{ active_theme }}';
544
+
545
+ let originalValues = {};
546
+ let currentValues = {};
547
+
548
+ // Toggle panel
549
+ toggleBtn.addEventListener('click', function() {
550
+ panel.classList.toggle('open');
551
+ if (panel.classList.contains('open')) {
552
+ loadSettings();
553
+ }
554
+ });
555
+
556
+ closeBtn.addEventListener('click', function() {
557
+ panel.classList.remove('open');
558
+ });
559
+
560
+ // Color input sync
561
+ document.querySelectorAll('#customize-panel input[type="color"]').forEach(colorInput => {
562
+ const textInput = document.getElementById(colorInput.id + '_text');
563
+ if (textInput) {
564
+ colorInput.addEventListener('input', () => {
565
+ textInput.value = colorInput.value;
566
+ updatePreview();
567
+ });
568
+ textInput.addEventListener('input', () => {
569
+ if (/^#[0-9A-Fa-f]{6}$/.test(textInput.value)) {
570
+ colorInput.value = textInput.value;
571
+ updatePreview();
572
+ }
573
+ });
574
+ }
575
+ });
576
+
577
+ // Custom CSS change
578
+ document.getElementById('custom_css').addEventListener('input', debounce(updatePreview, 300));
579
+
580
+ // Load settings from API
581
+ async function loadSettings() {
582
+ try {
583
+ const response = await fetch('/admin/api/theme/settings?theme_name=' + themeName, {
584
+ credentials: 'include'
585
+ });
586
+ if (!response.ok) throw new Error('Failed to load settings');
587
+ const data = await response.json();
588
+
589
+ // Populate form
590
+ data.settings.forEach(setting => {
591
+ if (setting.id.startsWith('color_')) {
592
+ const colorInput = document.getElementById(setting.id);
593
+ const textInput = document.getElementById(setting.id + '_text');
594
+ if (colorInput && textInput) {
595
+ colorInput.value = setting.value || setting.default;
596
+ textInput.value = setting.value || setting.default;
597
+ }
598
+ } else if (setting.id === 'custom_css') {
599
+ document.getElementById('custom_css').value = setting.value || '';
600
+ } else if (['site_logo', 'site_icon', 'header_image', 'background_image'].includes(setting.id)) {
601
+ const input = document.getElementById(setting.id);
602
+ if (input) {
603
+ input.value = setting.value || '';
604
+ updateImagePreview(setting.id, setting.value || '');
605
+ }
606
+ }
607
+ });
608
+
609
+ originalValues = getFormValues();
610
+ currentValues = {...originalValues};
611
+ } catch (error) {
612
+ console.error('Load settings error:', error);
613
+ showMessage('設定の読み込みに失敗しました', 'error');
614
+ }
615
+ }
616
+
617
+ // Get current form values
618
+ function getFormValues() {
619
+ return {
620
+ site_logo: document.getElementById('site_logo').value,
621
+ site_icon: document.getElementById('site_icon').value,
622
+ header_image: document.getElementById('header_image').value,
623
+ background_image: document.getElementById('background_image').value,
624
+ color_primary: document.getElementById('color_primary').value,
625
+ color_background: document.getElementById('color_background').value,
626
+ color_text: document.getElementById('color_text').value,
627
+ custom_css: document.getElementById('custom_css').value,
628
+ };
629
+ }
630
+
631
+ // Update preview
632
+ async function updatePreview() {
633
+ currentValues = getFormValues();
634
+ try {
635
+ const response = await fetch('/admin/api/theme/preview-css', {
636
+ method: 'POST',
637
+ headers: {
638
+ 'Content-Type': 'application/json',
639
+ 'X-CSRF-Token': csrfToken
640
+ },
641
+ credentials: 'include',
642
+ body: JSON.stringify({
643
+ theme_name: themeName,
644
+ values: currentValues
645
+ })
646
+ });
647
+ if (!response.ok) throw new Error('Preview failed');
648
+ const css = await response.text();
649
+ applyPreviewCSS(css);
650
+ } catch (error) {
651
+ console.error('Preview error:', error);
652
+ }
653
+ }
654
+
655
+ // Apply preview CSS
656
+ function applyPreviewCSS(css) {
657
+ let styleEl = document.getElementById('customize-preview-style');
658
+ if (!styleEl) {
659
+ styleEl = document.createElement('style');
660
+ styleEl.id = 'customize-preview-style';
661
+ document.head.appendChild(styleEl);
662
+ }
663
+ styleEl.textContent = css;
664
+ }
665
+
666
+ // Save settings
667
+ saveBtn.addEventListener('click', async function() {
668
+ saveBtn.disabled = true;
669
+ saveBtn.textContent = '保存中...';
670
+
671
+ try {
672
+ const response = await fetch('/admin/api/theme/settings', {
673
+ method: 'POST',
674
+ headers: {
675
+ 'Content-Type': 'application/json',
676
+ 'X-CSRF-Token': csrfToken
677
+ },
678
+ credentials: 'include',
679
+ body: JSON.stringify({
680
+ theme_name: themeName,
681
+ values: currentValues
682
+ })
683
+ });
684
+
685
+ const result = await response.json();
686
+ if (result.success) {
687
+ originalValues = {...currentValues};
688
+ showMessage('保存しました', 'success');
689
+ } else {
690
+ throw new Error(result.detail || 'Save failed');
691
+ }
692
+ } catch (error) {
693
+ console.error('Save error:', error);
694
+ showMessage('保存に失敗しました', 'error');
695
+ } finally {
696
+ saveBtn.disabled = false;
697
+ saveBtn.textContent = '保存';
698
+ }
699
+ });
700
+
701
+ // Reset
702
+ resetBtn.addEventListener('click', function() {
703
+ if (!confirm('変更をリセットしますか?')) return;
704
+
705
+ // Image fields
706
+ document.getElementById('site_logo').value = originalValues.site_logo || '';
707
+ updateImagePreview('site_logo', originalValues.site_logo || '');
708
+ document.getElementById('site_icon').value = originalValues.site_icon || '';
709
+ updateImagePreview('site_icon', originalValues.site_icon || '');
710
+ document.getElementById('header_image').value = originalValues.header_image || '';
711
+ updateImagePreview('header_image', originalValues.header_image || '');
712
+ document.getElementById('background_image').value = originalValues.background_image || '';
713
+ updateImagePreview('background_image', originalValues.background_image || '');
714
+
715
+ // Color fields
716
+ document.getElementById('color_primary').value = originalValues.color_primary || '#2563eb';
717
+ document.getElementById('color_primary_text').value = originalValues.color_primary || '#2563eb';
718
+ document.getElementById('color_background').value = originalValues.color_background || '#ffffff';
719
+ document.getElementById('color_background_text').value = originalValues.color_background || '#ffffff';
720
+ document.getElementById('color_text').value = originalValues.color_text || '#1e293b';
721
+ document.getElementById('color_text_text').value = originalValues.color_text || '#1e293b';
722
+ document.getElementById('custom_css').value = originalValues.custom_css || '';
723
+
724
+ updatePreview();
725
+ });
726
+
727
+ // Show message
728
+ function showMessage(text, type) {
729
+ const existing = document.querySelector('.customize-message');
730
+ if (existing) existing.remove();
731
+
732
+ const msg = document.createElement('div');
733
+ msg.className = 'customize-message ' + type;
734
+ msg.textContent = text;
735
+ document.getElementById('customize-panel-content').prepend(msg);
736
+
737
+ setTimeout(() => msg.remove(), 3000);
738
+ }
739
+
740
+ // Debounce helper
741
+ function debounce(fn, delay) {
742
+ let timer;
743
+ return function(...args) {
744
+ clearTimeout(timer);
745
+ timer = setTimeout(() => fn.apply(this, args), delay);
746
+ };
747
+ }
748
+
749
+ // Update image preview
750
+ function updateImagePreview(fieldId, url) {
751
+ const previewEl = document.getElementById(fieldId + '_preview');
752
+ if (!previewEl) return;
753
+
754
+ if (url && url.trim()) {
755
+ previewEl.innerHTML = '<img src="' + url + '" alt="プレビュー" onerror="this.parentElement.innerHTML=\'<span class=no-image>読み込み失敗</span>\'">';
756
+ } else {
757
+ previewEl.innerHTML = '<span class="no-image">画像未設定</span>';
758
+ }
759
+ }
760
+
761
+ // Clear image field (global for onclick)
762
+ window.clearImageField = function(fieldId) {
763
+ const input = document.getElementById(fieldId);
764
+ if (input) {
765
+ input.value = '';
766
+ updateImagePreview(fieldId, '');
767
+ updatePreview();
768
+ }
769
+ };
770
+
771
+ // Image URL input listeners
772
+ ['site_logo', 'site_icon', 'header_image', 'background_image'].forEach(function(fieldId) {
773
+ const input = document.getElementById(fieldId);
774
+ if (input) {
775
+ input.addEventListener('input', debounce(function() {
776
+ updateImagePreview(fieldId, input.value);
777
+ updatePreview();
778
+ }, 300));
779
+ }
780
+ });
781
+ })();
782
+ </script>
229
783
  {% endif %}
230
784
  <header class="site-header">
231
785
  {% block header %}