focomy 0.1.118__py3-none-any.whl → 0.1.120__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/admin/routes.py CHANGED
@@ -198,6 +198,12 @@ async def get_context(
198
198
  except Exception:
199
199
  pass # Post content type may not exist
200
200
 
201
+ # Get active theme for customize link
202
+ from ..services.settings import SettingsService
203
+ settings_svc = SettingsService(db)
204
+ theme_settings = await settings_svc.get_by_category("theme")
205
+ active_theme = theme_settings.get("active", "default")
206
+
201
207
  return {
202
208
  "request": request,
203
209
  "content_types": content_types,
@@ -208,6 +214,7 @@ async def get_context(
208
214
  "user_role": user_role,
209
215
  "channels": channels,
210
216
  "orphan_post_count": orphan_post_count,
217
+ "active_theme": active_theme,
211
218
  }
212
219
 
213
220
 
core/engine/routes.py CHANGED
@@ -160,6 +160,8 @@ async def render_theme(
160
160
  if admin_info:
161
161
  context["is_admin"] = True
162
162
  context["admin_user"] = admin_info["user_data"]
163
+ context["active_theme"] = active_theme # For customize link
164
+ context["csrf_token"] = getattr(request.state, "csrf_token", "") # For API calls
163
165
  # Build edit URL if entity provided
164
166
  if entity and content_type:
165
167
  context["edit_url"] = f"/admin/{content_type}/{entity.id}/edit"
@@ -551,6 +551,9 @@
551
551
  </a>
552
552
 
553
553
  <div class="nav-section">外観</div>
554
+ <a href="/admin/themes/{{ active_theme }}/customize" class="nav-item {% if current_page == 'customize' %}active{% endif %}">
555
+ カスタマイズ
556
+ </a>
554
557
  <a href="/admin/themes" class="nav-item {% if current_page == 'themes' %}active{% endif %}">
555
558
  テーマ
556
559
  </a>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: focomy
3
- Version: 0.1.118
3
+ Version: 0.1.120
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
@@ -7,7 +7,7 @@ core/rate_limit.py,sha256=CX5UjmsU03aFWKXSKjweoHvH2xn0v4NBHNN5ynJC8LE,180
7
7
  core/relations.yaml,sha256=7GUCrphKaouEXNkyd8Ht99e6TcUPERhc4m36RGcc41U,2128
8
8
  core/utils.py,sha256=Rqs1WStB0JjTb8-750jL-xJ_kaPH3ddupvqt46BXIBc,2754
9
9
  core/admin/__init__.py,sha256=IXrr-z-IDXmYodaZ-cVDou6wr_vsVhyWmXHdSNKkQsk,94
10
- core/admin/routes.py,sha256=U6NCzZNjOwdRzU-pzktVbPWYUTNExjNnaMfgzW0X_Hw,150612
10
+ core/admin/routes.py,sha256=6l24uI8gNiwBTa6YzOstMYik8DzBZaBcqsuCPzST4oU,150908
11
11
  core/admin/url.py,sha256=FlusKnSz3bZgPSBmRu-dI3W-bQo7lKBDZ3zN8cFHwQc,2243
12
12
  core/api/__init__.py,sha256=H1StbYGDVRS6g-Jk3UUf17ibAz1K8IUa27NfPMkaNrA,19
13
13
  core/api/auth.py,sha256=Zb37IHcUSjf8_hXiVzhoZPQw6WAiOOS_AoMqE96yat8,11565
@@ -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=W09MJIEddsQbsmeyGd2P9nBZUDwDlTpOY4O-DvUd0_4,43849
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
@@ -150,7 +150,7 @@ core/services/wordpress_import/rollback.py,sha256=NomM71QsBI3nt1Y_Hpod2nw-E9kpXQ
150
150
  core/services/wordpress_import/verification.py,sha256=efaTdah8oc63MsqJv-OWuse81ukxuLxl1xSwMYybJNY,19701
151
151
  core/services/wordpress_import/wxr_parser.py,sha256=GrfiMoSj75hp3beHx9aLe-lxXtoWmq5eP_Jzx8UajKE,15982
152
152
  core/templates/admin/backup.html,sha256=Di93BRvmyxdvF-eOAMKTYFB7qubH1Fms7ppYbJ3YHiA,3165
153
- core/templates/admin/base.html,sha256=yWQNumbtcLIE0iffIoDB2xTF-9W5bt2mdsMYqA5QOa0,23926
153
+ core/templates/admin/base.html,sha256=jsOz2mG2HzDhTriZ3Pi6Z3MjsxGnoGvMqDzlSJz737Q,24127
154
154
  core/templates/admin/comments.html,sha256=AEzCMYu1-EdFC1yUa9oIObtRS-U9RlMTe_5P3O5alb8,10012
155
155
  core/templates/admin/customize.html,sha256=UohKyxBDmKhtbOiipyRkFJlrD_5u0JIc8b7--U5EuUk,18631
156
156
  core/templates/admin/dashboard.html,sha256=WzURfANu1k9AIjdP82fwJE1o3sRhNVr77iUCRfUoBm8,3439
@@ -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=XFrqJ4g9OF2xz7pKqV6nYtT4q417I86ies-xEcMMwfI,12551
192
+ themes/default/templates/base.html,sha256=Vl0HbbOcCd6SRGoi5j7_xJopsaqZ7Kr9dI7ygS0vx14,26867
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.118.dist-info/METADATA,sha256=DojllWfZ47MT4qsnqsqArF74zM7XIzFkAfa2rVetQDM,7042
208
- focomy-0.1.118.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
209
- focomy-0.1.118.dist-info/entry_points.txt,sha256=_rF-wxGI1axY7gox3DBsTLHq-JrFKkMCjA65a6b_oqE,41
210
- focomy-0.1.118.dist-info/licenses/LICENSE,sha256=z9Z7gN7NNV7zYCaY-Knh3bv8RBCu89VueYtAlN_-lro,1063
211
- focomy-0.1.118.dist-info/RECORD,,
207
+ focomy-0.1.120.dist-info/METADATA,sha256=7UzC55msNplib9gEMyoHDcrY9cR_WFPPfOqbCKpd6G8,7042
208
+ focomy-0.1.120.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
209
+ focomy-0.1.120.dist-info/entry_points.txt,sha256=_rF-wxGI1axY7gox3DBsTLHq-JrFKkMCjA65a6b_oqE,41
210
+ focomy-0.1.120.dist-info/licenses/LICENSE,sha256=z9Z7gN7NNV7zYCaY-Knh3bv8RBCu89VueYtAlN_-lro,1063
211
+ focomy-0.1.120.dist-info/RECORD,,
@@ -146,6 +146,7 @@
146
146
  {% endif %}
147
147
  </div>
148
148
  </div>
149
+ <button type="button" id="customize-toggle" class="admin-bar-btn">カスタマイズ</button>
149
150
  {% if edit_url %}
150
151
  <a href="{{ edit_url }}">編集</a>
151
152
  {% endif %}
@@ -223,8 +224,403 @@
223
224
  .admin-bar-dropdown:hover .admin-bar-dropdown-menu {
224
225
  display: block;
225
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
+ }
226
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
+ #customize-panel-footer {
338
+ padding: 1rem;
339
+ display: flex;
340
+ gap: 0.5rem;
341
+ justify-content: flex-end;
342
+ }
343
+ #customize-panel-footer button {
344
+ padding: 0.5rem 1rem;
345
+ border-radius: 4px;
346
+ font-size: 0.8125rem;
347
+ cursor: pointer;
348
+ }
349
+ #customize-reset {
350
+ background: transparent;
351
+ border: 1px solid #374151;
352
+ color: #94a3b8;
353
+ }
354
+ #customize-reset:hover {
355
+ background: #374151;
356
+ color: white;
357
+ }
358
+ #customize-save {
359
+ background: #2563eb;
360
+ border: none;
361
+ color: white;
362
+ }
363
+ #customize-save:hover {
364
+ background: #1d4ed8;
365
+ }
366
+ #customize-save:disabled {
367
+ background: #374151;
368
+ cursor: not-allowed;
369
+ }
370
+ .customize-message {
371
+ padding: 0.5rem 1rem;
372
+ margin: 0 1rem;
373
+ border-radius: 4px;
374
+ font-size: 0.8125rem;
375
+ }
376
+ .customize-message.success {
377
+ background: #22c55e20;
378
+ color: #22c55e;
379
+ }
380
+ .customize-message.error {
381
+ background: #ef444420;
382
+ color: #ef4444;
383
+ }
227
384
  </style>
385
+
386
+ <!-- Customize Panel -->
387
+ <div id="customize-panel">
388
+ <div id="customize-panel-header">
389
+ <h2>カスタマイズ</h2>
390
+ <button type="button" id="customize-panel-close">&times;</button>
391
+ </div>
392
+ <div id="customize-panel-content">
393
+ <div class="customize-section">
394
+ <h3>カラー</h3>
395
+ <div class="customize-field">
396
+ <label>メインカラー</label>
397
+ <div class="color-row">
398
+ <input type="color" id="color_primary" value="#2563eb">
399
+ <input type="text" id="color_primary_text" value="#2563eb" pattern="^#[0-9A-Fa-f]{6}$">
400
+ </div>
401
+ </div>
402
+ <div class="customize-field">
403
+ <label>背景色</label>
404
+ <div class="color-row">
405
+ <input type="color" id="color_background" value="#ffffff">
406
+ <input type="text" id="color_background_text" value="#ffffff" pattern="^#[0-9A-Fa-f]{6}$">
407
+ </div>
408
+ </div>
409
+ <div class="customize-field">
410
+ <label>文字色</label>
411
+ <div class="color-row">
412
+ <input type="color" id="color_text" value="#1e293b">
413
+ <input type="text" id="color_text_text" value="#1e293b" pattern="^#[0-9A-Fa-f]{6}$">
414
+ </div>
415
+ </div>
416
+ </div>
417
+ <div class="customize-section">
418
+ <h3>カスタムCSS</h3>
419
+ <div class="customize-field">
420
+ <textarea id="custom_css" placeholder="/* CSSを入力 */"></textarea>
421
+ </div>
422
+ </div>
423
+ </div>
424
+ <div id="customize-panel-footer">
425
+ <button type="button" id="customize-reset">リセット</button>
426
+ <button type="button" id="customize-save">保存</button>
427
+ </div>
428
+ </div>
429
+
430
+ <script>
431
+ (function() {
432
+ const panel = document.getElementById('customize-panel');
433
+ const toggleBtn = document.getElementById('customize-toggle');
434
+ const closeBtn = document.getElementById('customize-panel-close');
435
+ const saveBtn = document.getElementById('customize-save');
436
+ const resetBtn = document.getElementById('customize-reset');
437
+ const csrfToken = '{{ csrf_token }}';
438
+ const themeName = '{{ active_theme }}';
439
+
440
+ let originalValues = {};
441
+ let currentValues = {};
442
+
443
+ // Toggle panel
444
+ toggleBtn.addEventListener('click', function() {
445
+ panel.classList.toggle('open');
446
+ if (panel.classList.contains('open')) {
447
+ loadSettings();
448
+ }
449
+ });
450
+
451
+ closeBtn.addEventListener('click', function() {
452
+ panel.classList.remove('open');
453
+ });
454
+
455
+ // Color input sync
456
+ document.querySelectorAll('#customize-panel input[type="color"]').forEach(colorInput => {
457
+ const textInput = document.getElementById(colorInput.id + '_text');
458
+ if (textInput) {
459
+ colorInput.addEventListener('input', () => {
460
+ textInput.value = colorInput.value;
461
+ updatePreview();
462
+ });
463
+ textInput.addEventListener('input', () => {
464
+ if (/^#[0-9A-Fa-f]{6}$/.test(textInput.value)) {
465
+ colorInput.value = textInput.value;
466
+ updatePreview();
467
+ }
468
+ });
469
+ }
470
+ });
471
+
472
+ // Custom CSS change
473
+ document.getElementById('custom_css').addEventListener('input', debounce(updatePreview, 300));
474
+
475
+ // Load settings from API
476
+ async function loadSettings() {
477
+ try {
478
+ const response = await fetch('/admin/api/theme/settings?theme_name=' + themeName, {
479
+ credentials: 'include'
480
+ });
481
+ if (!response.ok) throw new Error('Failed to load settings');
482
+ const data = await response.json();
483
+
484
+ // Populate form
485
+ data.settings.forEach(setting => {
486
+ if (setting.id.startsWith('color_')) {
487
+ const colorInput = document.getElementById(setting.id);
488
+ const textInput = document.getElementById(setting.id + '_text');
489
+ if (colorInput && textInput) {
490
+ colorInput.value = setting.value || setting.default;
491
+ textInput.value = setting.value || setting.default;
492
+ }
493
+ } else if (setting.id === 'custom_css') {
494
+ document.getElementById('custom_css').value = setting.value || '';
495
+ }
496
+ });
497
+
498
+ originalValues = getFormValues();
499
+ currentValues = {...originalValues};
500
+ } catch (error) {
501
+ console.error('Load settings error:', error);
502
+ showMessage('設定の読み込みに失敗しました', 'error');
503
+ }
504
+ }
505
+
506
+ // Get current form values
507
+ function getFormValues() {
508
+ return {
509
+ color_primary: document.getElementById('color_primary').value,
510
+ color_background: document.getElementById('color_background').value,
511
+ color_text: document.getElementById('color_text').value,
512
+ custom_css: document.getElementById('custom_css').value,
513
+ };
514
+ }
515
+
516
+ // Update preview
517
+ async function updatePreview() {
518
+ currentValues = getFormValues();
519
+ try {
520
+ const response = await fetch('/admin/api/theme/preview-css', {
521
+ method: 'POST',
522
+ headers: {
523
+ 'Content-Type': 'application/json',
524
+ 'X-CSRF-Token': csrfToken
525
+ },
526
+ credentials: 'include',
527
+ body: JSON.stringify({
528
+ theme_name: themeName,
529
+ values: currentValues
530
+ })
531
+ });
532
+ if (!response.ok) throw new Error('Preview failed');
533
+ const css = await response.text();
534
+ applyPreviewCSS(css);
535
+ } catch (error) {
536
+ console.error('Preview error:', error);
537
+ }
538
+ }
539
+
540
+ // Apply preview CSS
541
+ function applyPreviewCSS(css) {
542
+ let styleEl = document.getElementById('customize-preview-style');
543
+ if (!styleEl) {
544
+ styleEl = document.createElement('style');
545
+ styleEl.id = 'customize-preview-style';
546
+ document.head.appendChild(styleEl);
547
+ }
548
+ styleEl.textContent = css;
549
+ }
550
+
551
+ // Save settings
552
+ saveBtn.addEventListener('click', async function() {
553
+ saveBtn.disabled = true;
554
+ saveBtn.textContent = '保存中...';
555
+
556
+ try {
557
+ const response = await fetch('/admin/api/theme/settings', {
558
+ method: 'POST',
559
+ headers: {
560
+ 'Content-Type': 'application/json',
561
+ 'X-CSRF-Token': csrfToken
562
+ },
563
+ credentials: 'include',
564
+ body: JSON.stringify({
565
+ theme_name: themeName,
566
+ values: currentValues
567
+ })
568
+ });
569
+
570
+ const result = await response.json();
571
+ if (result.success) {
572
+ originalValues = {...currentValues};
573
+ showMessage('保存しました', 'success');
574
+ } else {
575
+ throw new Error(result.detail || 'Save failed');
576
+ }
577
+ } catch (error) {
578
+ console.error('Save error:', error);
579
+ showMessage('保存に失敗しました', 'error');
580
+ } finally {
581
+ saveBtn.disabled = false;
582
+ saveBtn.textContent = '保存';
583
+ }
584
+ });
585
+
586
+ // Reset
587
+ resetBtn.addEventListener('click', function() {
588
+ if (!confirm('変更をリセットしますか?')) return;
589
+
590
+ document.getElementById('color_primary').value = originalValues.color_primary || '#2563eb';
591
+ document.getElementById('color_primary_text').value = originalValues.color_primary || '#2563eb';
592
+ document.getElementById('color_background').value = originalValues.color_background || '#ffffff';
593
+ document.getElementById('color_background_text').value = originalValues.color_background || '#ffffff';
594
+ document.getElementById('color_text').value = originalValues.color_text || '#1e293b';
595
+ document.getElementById('color_text_text').value = originalValues.color_text || '#1e293b';
596
+ document.getElementById('custom_css').value = originalValues.custom_css || '';
597
+
598
+ updatePreview();
599
+ });
600
+
601
+ // Show message
602
+ function showMessage(text, type) {
603
+ const existing = document.querySelector('.customize-message');
604
+ if (existing) existing.remove();
605
+
606
+ const msg = document.createElement('div');
607
+ msg.className = 'customize-message ' + type;
608
+ msg.textContent = text;
609
+ document.getElementById('customize-panel-content').prepend(msg);
610
+
611
+ setTimeout(() => msg.remove(), 3000);
612
+ }
613
+
614
+ // Debounce helper
615
+ function debounce(fn, delay) {
616
+ let timer;
617
+ return function(...args) {
618
+ clearTimeout(timer);
619
+ timer = setTimeout(() => fn.apply(this, args), delay);
620
+ };
621
+ }
622
+ })();
623
+ </script>
228
624
  {% endif %}
229
625
  <header class="site-header">
230
626
  {% block header %}