focomy 0.1.115__py3-none-any.whl → 0.1.117__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 +32 -0
- core/engine/routes.py +53 -0
- core/services/theme.py +100 -12
- core/templates/admin/customize.html +189 -9
- core/templates/admin/entity_form.html +29 -0
- {focomy-0.1.115.dist-info → focomy-0.1.117.dist-info}/METADATA +1 -1
- {focomy-0.1.115.dist-info → focomy-0.1.117.dist-info}/RECORD +11 -10
- themes/default/customizations.json +22 -0
- {focomy-0.1.115.dist-info → focomy-0.1.117.dist-info}/WHEEL +0 -0
- {focomy-0.1.115.dist-info → focomy-0.1.117.dist-info}/entry_points.txt +0 -0
- {focomy-0.1.115.dist-info → focomy-0.1.117.dist-info}/licenses/LICENSE +0 -0
core/admin/routes.py
CHANGED
|
@@ -2020,6 +2020,38 @@ async def preview_render(
|
|
|
2020
2020
|
return {"html": html}
|
|
2021
2021
|
|
|
2022
2022
|
|
|
2023
|
+
@router.post("/api/preview/token")
|
|
2024
|
+
async def create_preview_token(
|
|
2025
|
+
request: Request,
|
|
2026
|
+
db: AsyncSession = Depends(get_db),
|
|
2027
|
+
current_user: Entity = Depends(require_admin_api),
|
|
2028
|
+
):
|
|
2029
|
+
"""Create a preview token for an entity."""
|
|
2030
|
+
from ..services.preview import get_preview_service
|
|
2031
|
+
|
|
2032
|
+
try:
|
|
2033
|
+
body = await request.json()
|
|
2034
|
+
except Exception:
|
|
2035
|
+
raise HTTPException(status_code=400, detail="Invalid JSON")
|
|
2036
|
+
|
|
2037
|
+
entity_id = body.get("entity_id")
|
|
2038
|
+
if not entity_id:
|
|
2039
|
+
raise HTTPException(status_code=400, detail="entity_id required")
|
|
2040
|
+
|
|
2041
|
+
# Verify entity exists
|
|
2042
|
+
entity_svc = EntityService(db)
|
|
2043
|
+
entity = await entity_svc.get(entity_id)
|
|
2044
|
+
if not entity:
|
|
2045
|
+
raise HTTPException(status_code=404, detail="Entity not found")
|
|
2046
|
+
|
|
2047
|
+
# Create preview token
|
|
2048
|
+
preview_svc = get_preview_service(db)
|
|
2049
|
+
token = await preview_svc.create_token(entity_id, current_user.id)
|
|
2050
|
+
preview_url = preview_svc.get_preview_url(token)
|
|
2051
|
+
|
|
2052
|
+
return {"token": token, "url": preview_url}
|
|
2053
|
+
|
|
2054
|
+
|
|
2023
2055
|
@router.get("/system", response_class=HTMLResponse)
|
|
2024
2056
|
async def system_info(
|
|
2025
2057
|
request: Request,
|
core/engine/routes.py
CHANGED
|
@@ -261,6 +261,59 @@ async def sitemap_xml(
|
|
|
261
261
|
return Response(content=xml_content, media_type="application/xml")
|
|
262
262
|
|
|
263
263
|
|
|
264
|
+
# === Preview Routes ===
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@router.get("/preview/{token}", response_class=HTMLResponse)
|
|
268
|
+
async def preview_entity(
|
|
269
|
+
token: str,
|
|
270
|
+
request: Request,
|
|
271
|
+
db: AsyncSession = Depends(get_db),
|
|
272
|
+
):
|
|
273
|
+
"""Preview an entity with a valid token (shows draft/unpublished content)."""
|
|
274
|
+
from ..services.preview import get_preview_service
|
|
275
|
+
|
|
276
|
+
preview_svc = get_preview_service(db)
|
|
277
|
+
entity = await preview_svc.get_preview_entity(token)
|
|
278
|
+
|
|
279
|
+
if not entity:
|
|
280
|
+
raise HTTPException(status_code=404, detail="Preview not found or expired")
|
|
281
|
+
|
|
282
|
+
entity_svc = EntityService(db)
|
|
283
|
+
entity_data = entity_svc.serialize(entity)
|
|
284
|
+
|
|
285
|
+
# Get content type info
|
|
286
|
+
content_type = entity.type
|
|
287
|
+
ct = field_service.get_content_type(content_type)
|
|
288
|
+
|
|
289
|
+
# Get site URL and contexts
|
|
290
|
+
site_url = str(request.base_url).rstrip("/")
|
|
291
|
+
menus_ctx = await get_menus_context(db)
|
|
292
|
+
widgets_ctx = await get_widgets_context(db)
|
|
293
|
+
seo_ctx = await get_seo_settings(db, site_url)
|
|
294
|
+
|
|
295
|
+
# Add preview flag to context
|
|
296
|
+
context = {
|
|
297
|
+
"post": entity_data,
|
|
298
|
+
"entity": entity_data,
|
|
299
|
+
"content": entity_data,
|
|
300
|
+
"is_preview": True,
|
|
301
|
+
"content_type": ct,
|
|
302
|
+
**menus_ctx,
|
|
303
|
+
**widgets_ctx,
|
|
304
|
+
**seo_ctx,
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
# Determine template
|
|
308
|
+
template = "post.html"
|
|
309
|
+
if ct and ct.template:
|
|
310
|
+
template = ct.template
|
|
311
|
+
|
|
312
|
+
html = await render_theme(db, template, context, request=request)
|
|
313
|
+
|
|
314
|
+
return HTMLResponse(content=html)
|
|
315
|
+
|
|
316
|
+
|
|
264
317
|
async def get_menus_context(db: AsyncSession) -> dict:
|
|
265
318
|
"""Get menus context for templates."""
|
|
266
319
|
menu_svc = MenuService(db)
|
core/services/theme.py
CHANGED
|
@@ -388,25 +388,39 @@ class ThemeService:
|
|
|
388
388
|
return css.strip()
|
|
389
389
|
|
|
390
390
|
def get_css_variables(self, theme_name: str = None, minify: bool = True) -> str:
|
|
391
|
-
"""Generate CSS variables from theme config."""
|
|
391
|
+
"""Generate CSS variables from theme config with customizations applied."""
|
|
392
392
|
theme = self.get_theme(theme_name)
|
|
393
393
|
if not theme:
|
|
394
394
|
return ""
|
|
395
395
|
|
|
396
|
+
# Get user customizations
|
|
397
|
+
customizations = self.get_customizations(theme_name)
|
|
398
|
+
|
|
396
399
|
lines = [":root {"]
|
|
397
400
|
|
|
398
|
-
# Colors
|
|
399
|
-
for name,
|
|
401
|
+
# Colors (with customization override)
|
|
402
|
+
for name, default_value in theme.colors.items():
|
|
403
|
+
value = customizations.get(f"color_{name}", default_value)
|
|
400
404
|
lines.append(f" --color-{name}: {value};")
|
|
401
405
|
|
|
402
|
-
# Fonts
|
|
403
|
-
for name,
|
|
406
|
+
# Fonts (with customization override)
|
|
407
|
+
for name, default_value in theme.fonts.items():
|
|
408
|
+
value = customizations.get(f"font_{name}", default_value)
|
|
404
409
|
lines.append(f" --font-{name}: {value};")
|
|
405
410
|
|
|
406
|
-
# Spacing
|
|
407
|
-
for name,
|
|
411
|
+
# Spacing (with customization override)
|
|
412
|
+
for name, default_value in theme.spacing.items():
|
|
413
|
+
value = customizations.get(f"space_{name}", default_value)
|
|
408
414
|
lines.append(f" --space-{name}: {value};")
|
|
409
415
|
|
|
416
|
+
# Header/Background images
|
|
417
|
+
header_image = customizations.get("header_image", "")
|
|
418
|
+
background_image = customizations.get("background_image", "")
|
|
419
|
+
if header_image:
|
|
420
|
+
lines.append(f" --header-image: url({header_image});")
|
|
421
|
+
if background_image:
|
|
422
|
+
lines.append(f" --background-image: url({background_image});")
|
|
423
|
+
|
|
410
424
|
lines.append("}")
|
|
411
425
|
|
|
412
426
|
# Add base styles
|
|
@@ -416,7 +430,10 @@ class ThemeService:
|
|
|
416
430
|
|
|
417
431
|
body {
|
|
418
432
|
font-family: var(--font-sans);
|
|
419
|
-
background: var(--color-background);
|
|
433
|
+
background: var(--background-image, var(--color-background));
|
|
434
|
+
background-size: cover;
|
|
435
|
+
background-position: center;
|
|
436
|
+
background-attachment: fixed;
|
|
420
437
|
color: var(--color-text);
|
|
421
438
|
line-height: 1.6;
|
|
422
439
|
}
|
|
@@ -428,7 +445,9 @@ body {
|
|
|
428
445
|
}
|
|
429
446
|
|
|
430
447
|
.site-header {
|
|
431
|
-
background: var(--color-surface);
|
|
448
|
+
background: var(--header-image, var(--color-surface));
|
|
449
|
+
background-size: cover;
|
|
450
|
+
background-position: center;
|
|
432
451
|
border-bottom: 1px solid var(--color-border);
|
|
433
452
|
padding: var(--space-md) 0;
|
|
434
453
|
}
|
|
@@ -645,9 +664,12 @@ body {
|
|
|
645
664
|
"""
|
|
646
665
|
)
|
|
647
666
|
|
|
648
|
-
# Add custom CSS
|
|
649
|
-
|
|
650
|
-
|
|
667
|
+
# Add custom CSS (from customizations or theme default)
|
|
668
|
+
custom_css = customizations.get("custom_css", theme.custom_css or "")
|
|
669
|
+
if custom_css:
|
|
670
|
+
lines.append("")
|
|
671
|
+
lines.append("/* Custom CSS */")
|
|
672
|
+
lines.append(custom_css)
|
|
651
673
|
|
|
652
674
|
css = "\n".join(lines)
|
|
653
675
|
return self._minify_css(css) if minify else css
|
|
@@ -1240,6 +1262,46 @@ body {
|
|
|
1240
1262
|
customizations = self.get_customizations(theme_name)
|
|
1241
1263
|
settings = []
|
|
1242
1264
|
|
|
1265
|
+
# Site Identity (logo, favicon)
|
|
1266
|
+
settings.append({
|
|
1267
|
+
"id": "site_logo",
|
|
1268
|
+
"type": "image",
|
|
1269
|
+
"label": "サイトロゴ",
|
|
1270
|
+
"category": "site_identity",
|
|
1271
|
+
"default": "",
|
|
1272
|
+
"value": customizations.get("site_logo", ""),
|
|
1273
|
+
"description": "ヘッダーに表示されるロゴ画像(推奨: 高さ60px以下)",
|
|
1274
|
+
})
|
|
1275
|
+
settings.append({
|
|
1276
|
+
"id": "site_icon",
|
|
1277
|
+
"type": "image",
|
|
1278
|
+
"label": "サイトアイコン",
|
|
1279
|
+
"category": "site_identity",
|
|
1280
|
+
"default": "",
|
|
1281
|
+
"value": customizations.get("site_icon", ""),
|
|
1282
|
+
"description": "ファビコン・アプリアイコン(推奨: 512x512px)",
|
|
1283
|
+
})
|
|
1284
|
+
|
|
1285
|
+
# Header Images
|
|
1286
|
+
settings.append({
|
|
1287
|
+
"id": "header_image",
|
|
1288
|
+
"type": "image",
|
|
1289
|
+
"label": "ヘッダー画像",
|
|
1290
|
+
"category": "header",
|
|
1291
|
+
"default": "",
|
|
1292
|
+
"value": customizations.get("header_image", ""),
|
|
1293
|
+
"description": "ヘッダー背景画像(推奨: 1920x400px)",
|
|
1294
|
+
})
|
|
1295
|
+
settings.append({
|
|
1296
|
+
"id": "background_image",
|
|
1297
|
+
"type": "image",
|
|
1298
|
+
"label": "背景画像",
|
|
1299
|
+
"category": "header",
|
|
1300
|
+
"default": "",
|
|
1301
|
+
"value": customizations.get("background_image", ""),
|
|
1302
|
+
"description": "サイト全体の背景画像",
|
|
1303
|
+
})
|
|
1304
|
+
|
|
1243
1305
|
# Colors
|
|
1244
1306
|
for name, default_value in theme.colors.items():
|
|
1245
1307
|
settings.append({
|
|
@@ -1273,6 +1335,17 @@ body {
|
|
|
1273
1335
|
"value": customizations.get(f"space_{name}", default_value),
|
|
1274
1336
|
})
|
|
1275
1337
|
|
|
1338
|
+
# Custom CSS
|
|
1339
|
+
settings.append({
|
|
1340
|
+
"id": "custom_css",
|
|
1341
|
+
"type": "code",
|
|
1342
|
+
"label": "カスタムCSS",
|
|
1343
|
+
"category": "custom_css",
|
|
1344
|
+
"default": theme.custom_css or "",
|
|
1345
|
+
"value": customizations.get("custom_css", theme.custom_css or ""),
|
|
1346
|
+
"description": "独自のCSSを追加できます",
|
|
1347
|
+
})
|
|
1348
|
+
|
|
1276
1349
|
return settings
|
|
1277
1350
|
|
|
1278
1351
|
def generate_preview_css(self, preview_values: dict, theme_name: str = None) -> str:
|
|
@@ -1311,8 +1384,23 @@ body {
|
|
|
1311
1384
|
value = values.get(f"space_{name}", default_value)
|
|
1312
1385
|
lines.append(f" --space-{name}: {value};")
|
|
1313
1386
|
|
|
1387
|
+
# Header/Background images
|
|
1388
|
+
header_image = values.get("header_image", "")
|
|
1389
|
+
background_image = values.get("background_image", "")
|
|
1390
|
+
if header_image:
|
|
1391
|
+
lines.append(f" --header-image: url({header_image});")
|
|
1392
|
+
if background_image:
|
|
1393
|
+
lines.append(f" --background-image: url({background_image});")
|
|
1394
|
+
|
|
1314
1395
|
lines.append("}")
|
|
1315
1396
|
|
|
1397
|
+
# Custom CSS
|
|
1398
|
+
custom_css = values.get("custom_css", "")
|
|
1399
|
+
if custom_css:
|
|
1400
|
+
lines.append("")
|
|
1401
|
+
lines.append("/* Custom CSS */")
|
|
1402
|
+
lines.append(custom_css)
|
|
1403
|
+
|
|
1316
1404
|
return "\n".join(lines)
|
|
1317
1405
|
|
|
1318
1406
|
|
|
@@ -22,6 +22,74 @@
|
|
|
22
22
|
|
|
23
23
|
<div class="customize-layout">
|
|
24
24
|
<div class="customize-sidebar">
|
|
25
|
+
<!-- Site Identity -->
|
|
26
|
+
{% if grouped_settings.site_identity %}
|
|
27
|
+
<div class="customize-section">
|
|
28
|
+
<h3>サイトID</h3>
|
|
29
|
+
{% for setting in grouped_settings.site_identity %}
|
|
30
|
+
<div class="customize-field">
|
|
31
|
+
<label for="{{ setting.id }}">{{ setting.label }}</label>
|
|
32
|
+
{% if setting.description %}
|
|
33
|
+
<p class="field-description">{{ setting.description }}</p>
|
|
34
|
+
{% endif %}
|
|
35
|
+
<div class="image-input-wrapper">
|
|
36
|
+
<div class="image-preview" id="{{ setting.id }}_preview">
|
|
37
|
+
{% if setting.value %}
|
|
38
|
+
<img src="{{ setting.value }}" alt="{{ setting.label }}">
|
|
39
|
+
{% else %}
|
|
40
|
+
<span class="no-image">画像未設定</span>
|
|
41
|
+
{% endif %}
|
|
42
|
+
</div>
|
|
43
|
+
<input type="url"
|
|
44
|
+
id="{{ setting.id }}"
|
|
45
|
+
name="{{ setting.id }}"
|
|
46
|
+
value="{{ setting.value }}"
|
|
47
|
+
data-default="{{ setting.default }}"
|
|
48
|
+
class="customize-input image-url-input"
|
|
49
|
+
placeholder="https://example.com/image.png">
|
|
50
|
+
<div class="image-actions">
|
|
51
|
+
<button type="button" class="btn btn-sm btn-outline" onclick="clearImageInput('{{ setting.id }}')">クリア</button>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
{% endfor %}
|
|
56
|
+
</div>
|
|
57
|
+
{% endif %}
|
|
58
|
+
|
|
59
|
+
<!-- Header Images -->
|
|
60
|
+
{% if grouped_settings.header %}
|
|
61
|
+
<div class="customize-section">
|
|
62
|
+
<h3>ヘッダー・背景</h3>
|
|
63
|
+
{% for setting in grouped_settings.header %}
|
|
64
|
+
<div class="customize-field">
|
|
65
|
+
<label for="{{ setting.id }}">{{ setting.label }}</label>
|
|
66
|
+
{% if setting.description %}
|
|
67
|
+
<p class="field-description">{{ setting.description }}</p>
|
|
68
|
+
{% endif %}
|
|
69
|
+
<div class="image-input-wrapper">
|
|
70
|
+
<div class="image-preview" id="{{ setting.id }}_preview">
|
|
71
|
+
{% if setting.value %}
|
|
72
|
+
<img src="{{ setting.value }}" alt="{{ setting.label }}">
|
|
73
|
+
{% else %}
|
|
74
|
+
<span class="no-image">画像未設定</span>
|
|
75
|
+
{% endif %}
|
|
76
|
+
</div>
|
|
77
|
+
<input type="url"
|
|
78
|
+
id="{{ setting.id }}"
|
|
79
|
+
name="{{ setting.id }}"
|
|
80
|
+
value="{{ setting.value }}"
|
|
81
|
+
data-default="{{ setting.default }}"
|
|
82
|
+
class="customize-input image-url-input"
|
|
83
|
+
placeholder="https://example.com/image.png">
|
|
84
|
+
<div class="image-actions">
|
|
85
|
+
<button type="button" class="btn btn-sm btn-outline" onclick="clearImageInput('{{ setting.id }}')">クリア</button>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
{% endfor %}
|
|
90
|
+
</div>
|
|
91
|
+
{% endif %}
|
|
92
|
+
|
|
25
93
|
<!-- Colors -->
|
|
26
94
|
{% if grouped_settings.colors %}
|
|
27
95
|
<div class="customize-section">
|
|
@@ -82,6 +150,27 @@
|
|
|
82
150
|
{% endfor %}
|
|
83
151
|
</div>
|
|
84
152
|
{% endif %}
|
|
153
|
+
|
|
154
|
+
<!-- Custom CSS -->
|
|
155
|
+
{% if grouped_settings.custom_css %}
|
|
156
|
+
<div class="customize-section">
|
|
157
|
+
<h3>カスタムCSS</h3>
|
|
158
|
+
{% for setting in grouped_settings.custom_css %}
|
|
159
|
+
<div class="customize-field">
|
|
160
|
+
<label for="{{ setting.id }}">{{ setting.label }}</label>
|
|
161
|
+
{% if setting.description %}
|
|
162
|
+
<p class="field-description">{{ setting.description }}</p>
|
|
163
|
+
{% endif %}
|
|
164
|
+
<textarea id="{{ setting.id }}"
|
|
165
|
+
name="{{ setting.id }}"
|
|
166
|
+
data-default="{{ setting.default }}"
|
|
167
|
+
class="customize-input code-input"
|
|
168
|
+
rows="10"
|
|
169
|
+
placeholder="/* CSSを入力 */ .my-class { color: red; }">{{ setting.value }}</textarea>
|
|
170
|
+
</div>
|
|
171
|
+
{% endfor %}
|
|
172
|
+
</div>
|
|
173
|
+
{% endif %}
|
|
85
174
|
</div>
|
|
86
175
|
|
|
87
176
|
<div class="customize-preview">
|
|
@@ -191,6 +280,66 @@
|
|
|
191
280
|
font-family: monospace;
|
|
192
281
|
}
|
|
193
282
|
|
|
283
|
+
.field-description {
|
|
284
|
+
font-size: 0.75rem;
|
|
285
|
+
color: var(--text-muted);
|
|
286
|
+
margin-bottom: 0.5rem;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.code-input {
|
|
290
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
291
|
+
font-size: 0.8125rem;
|
|
292
|
+
line-height: 1.5;
|
|
293
|
+
resize: vertical;
|
|
294
|
+
min-height: 150px;
|
|
295
|
+
background: #1e1e1e;
|
|
296
|
+
color: #d4d4d4;
|
|
297
|
+
padding: 0.75rem;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.image-input-wrapper {
|
|
301
|
+
display: flex;
|
|
302
|
+
flex-direction: column;
|
|
303
|
+
gap: 0.5rem;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.image-preview {
|
|
307
|
+
width: 100%;
|
|
308
|
+
height: 80px;
|
|
309
|
+
border: 2px dashed var(--border);
|
|
310
|
+
border-radius: 0.375rem;
|
|
311
|
+
display: flex;
|
|
312
|
+
align-items: center;
|
|
313
|
+
justify-content: center;
|
|
314
|
+
background: var(--bg);
|
|
315
|
+
overflow: hidden;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.image-preview img {
|
|
319
|
+
max-width: 100%;
|
|
320
|
+
max-height: 100%;
|
|
321
|
+
object-fit: contain;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.image-preview .no-image {
|
|
325
|
+
color: var(--text-muted);
|
|
326
|
+
font-size: 0.75rem;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.image-url-input {
|
|
330
|
+
font-size: 0.8125rem;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.image-actions {
|
|
334
|
+
display: flex;
|
|
335
|
+
gap: 0.5rem;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.btn-sm {
|
|
339
|
+
padding: 0.25rem 0.5rem;
|
|
340
|
+
font-size: 0.75rem;
|
|
341
|
+
}
|
|
342
|
+
|
|
194
343
|
.customize-preview {
|
|
195
344
|
background: var(--card);
|
|
196
345
|
border: 1px solid var(--border);
|
|
@@ -360,8 +509,36 @@
|
|
|
360
509
|
if (!input.classList.contains('color-input')) {
|
|
361
510
|
input.addEventListener('input', debouncedUpdate);
|
|
362
511
|
}
|
|
512
|
+
// Add image preview update for image-url-input
|
|
513
|
+
if (input.classList.contains('image-url-input')) {
|
|
514
|
+
input.addEventListener('input', () => {
|
|
515
|
+
updateImagePreview(input.id, input.value);
|
|
516
|
+
});
|
|
517
|
+
}
|
|
363
518
|
});
|
|
364
519
|
|
|
520
|
+
// Update image preview
|
|
521
|
+
function updateImagePreview(inputId, url) {
|
|
522
|
+
const previewEl = document.getElementById(inputId + '_preview');
|
|
523
|
+
if (!previewEl) return;
|
|
524
|
+
|
|
525
|
+
if (url && url.trim()) {
|
|
526
|
+
previewEl.innerHTML = `<img src="${url}" alt="Preview" onerror="this.parentElement.innerHTML='<span class=\\'no-image\\'>読み込みエラー</span>'">`;
|
|
527
|
+
} else {
|
|
528
|
+
previewEl.innerHTML = '<span class="no-image">画像未設定</span>';
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Clear image input (global function for onclick)
|
|
533
|
+
window.clearImageInput = function(inputId) {
|
|
534
|
+
const input = document.getElementById(inputId);
|
|
535
|
+
if (input) {
|
|
536
|
+
input.value = '';
|
|
537
|
+
updateImagePreview(inputId, '');
|
|
538
|
+
debouncedUpdate();
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
|
|
365
542
|
// Save button
|
|
366
543
|
saveBtn.addEventListener('click', async () => {
|
|
367
544
|
const values = getValues();
|
|
@@ -402,15 +579,18 @@
|
|
|
402
579
|
if (!confirm('カスタマイズをデフォルトに戻しますか?')) return;
|
|
403
580
|
|
|
404
581
|
inputs.forEach(input => {
|
|
405
|
-
const defaultValue = input.dataset.default;
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
582
|
+
const defaultValue = input.dataset.default || '';
|
|
583
|
+
input.value = defaultValue;
|
|
584
|
+
|
|
585
|
+
// Sync color text input
|
|
586
|
+
if (input.classList.contains('color-input')) {
|
|
587
|
+
const textInput = document.getElementById(input.id + '_text');
|
|
588
|
+
if (textInput) textInput.value = defaultValue;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Update image preview
|
|
592
|
+
if (input.classList.contains('image-url-input')) {
|
|
593
|
+
updateImagePreview(input.id, defaultValue);
|
|
414
594
|
}
|
|
415
595
|
});
|
|
416
596
|
|
|
@@ -388,6 +388,7 @@
|
|
|
388
388
|
<a href="{{ cancel_url }}" class="btn btn-secondary">Cancel</a>
|
|
389
389
|
{% if entity %}
|
|
390
390
|
<button type="button" class="btn btn-secondary" onclick="toggleRevisions()">History</button>
|
|
391
|
+
<button type="button" class="btn btn-secondary" onclick="openPreview()">Preview</button>
|
|
391
392
|
{% endif %}
|
|
392
393
|
<button type="submit" class="btn btn-primary">
|
|
393
394
|
{% if entity %}Update{% else %}Create{% endif %}
|
|
@@ -1239,6 +1240,34 @@ function toggleRevisions() {
|
|
|
1239
1240
|
}
|
|
1240
1241
|
}
|
|
1241
1242
|
|
|
1243
|
+
// Preview in new tab
|
|
1244
|
+
async function openPreview() {
|
|
1245
|
+
const entityId = '{{ entity.id if entity else "" }}';
|
|
1246
|
+
if (!entityId) {
|
|
1247
|
+
alert('Please save the content first before previewing.');
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
try {
|
|
1252
|
+
const response = await fetch('/admin/api/preview/token', {
|
|
1253
|
+
method: 'POST',
|
|
1254
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1255
|
+
body: JSON.stringify({ entity_id: entityId })
|
|
1256
|
+
});
|
|
1257
|
+
|
|
1258
|
+
if (!response.ok) {
|
|
1259
|
+
const error = await response.json();
|
|
1260
|
+
alert('Preview error: ' + (error.detail || 'Unknown error'));
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
const data = await response.json();
|
|
1265
|
+
window.open(data.url, '_blank');
|
|
1266
|
+
} catch (e) {
|
|
1267
|
+
alert('Preview error: ' + e.message);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1242
1271
|
async function loadRevisions() {
|
|
1243
1272
|
const entityId = '{{ entity.id }}';
|
|
1244
1273
|
const list = document.getElementById('revisions-list');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: focomy
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.117
|
|
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=
|
|
10
|
+
core/admin/routes.py,sha256=U6NCzZNjOwdRzU-pzktVbPWYUTNExjNnaMfgzW0X_Hw,150612
|
|
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=
|
|
47
|
+
core/engine/routes.py,sha256=W09MJIEddsQbsmeyGd2P9nBZUDwDlTpOY4O-DvUd0_4,43849
|
|
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
|
|
@@ -124,7 +124,7 @@ core/services/seo.py,sha256=qFKKnAzZ2CspWzlHHxvfKx4JBVgNhPWxCSn-adX-2z4,15658
|
|
|
124
124
|
core/services/settings.py,sha256=oZJx1HgFNeSicagtURXHbqq0_O3yPwvcwosuqDmZ13c,8822
|
|
125
125
|
core/services/spam_filter.py,sha256=2O8YWDlZoCr7MhGzsEsx6AKm2SgBP3-kxsXaETVd-0A,11314
|
|
126
126
|
core/services/storage.py,sha256=gaaVf594Ck-zkZMtdt--YcIMvWgPBs7ZGPY0FRIVIzQ,8807
|
|
127
|
-
core/services/theme.py,sha256
|
|
127
|
+
core/services/theme.py,sha256=uMfbkik7NjtsXjyKef9txYGMMUqBEFdlAEPjuP5dJ1o,52910
|
|
128
128
|
core/services/theme_inheritance.py,sha256=v4AT_VmVLQ0o3O0bO1S9qGZpnGX5d41JPrID1lmAxZI,9418
|
|
129
129
|
core/services/thumbnail.py,sha256=ey1Gxl5sXr-u9GoiLe6cr9Mu6ez2kjkDrWWcQWD-Rnw,6726
|
|
130
130
|
core/services/update.py,sha256=8me-Gy2Esi5HeTX1PdFI0U4vVMqKkeBHzc_SpaGuJ5g,9281
|
|
@@ -152,9 +152,9 @@ core/services/wordpress_import/wxr_parser.py,sha256=GrfiMoSj75hp3beHx9aLe-lxXtoW
|
|
|
152
152
|
core/templates/admin/backup.html,sha256=Di93BRvmyxdvF-eOAMKTYFB7qubH1Fms7ppYbJ3YHiA,3165
|
|
153
153
|
core/templates/admin/base.html,sha256=yWQNumbtcLIE0iffIoDB2xTF-9W5bt2mdsMYqA5QOa0,23926
|
|
154
154
|
core/templates/admin/comments.html,sha256=AEzCMYu1-EdFC1yUa9oIObtRS-U9RlMTe_5P3O5alb8,10012
|
|
155
|
-
core/templates/admin/customize.html,sha256=
|
|
155
|
+
core/templates/admin/customize.html,sha256=UohKyxBDmKhtbOiipyRkFJlrD_5u0JIc8b7--U5EuUk,18631
|
|
156
156
|
core/templates/admin/dashboard.html,sha256=WzURfANu1k9AIjdP82fwJE1o3sRhNVr77iUCRfUoBm8,3439
|
|
157
|
-
core/templates/admin/entity_form.html,sha256=
|
|
157
|
+
core/templates/admin/entity_form.html,sha256=lX2l7iS6psm29wh1tHcK-4tArhG65gJxmCDInUSqV0c,61161
|
|
158
158
|
core/templates/admin/entity_list.html,sha256=DxaM2c_1UGdPpQHTIe7LrFxjC11f8hXDy-9C-T8LO6o,7966
|
|
159
159
|
core/templates/admin/forgot_password.html,sha256=j2r9N5G8T5zKODQu7IVc0NdRvSVuyXBA9FCLn7egp1Q,2320
|
|
160
160
|
core/templates/admin/import.html,sha256=2b2Bu9Do1pQBUgsseowt19oaf0vEfGzTOCJ8Rr-Nphc,48385
|
|
@@ -183,6 +183,7 @@ themes/corporate/theme.yaml,sha256=jPVAkwmKr6KniZ0uBw6PJNkZLdbOF7mq6j_drf3IGPI,2
|
|
|
183
183
|
themes/corporate/templates/base.html,sha256=qN1bwJjc3Q7j7rCs_Y-Tg76KVPllENCWYC91o6t6hUQ,13976
|
|
184
184
|
themes/corporate/templates/home.html,sha256=PrbLkEup19Eg-lYS9QpcSJyOTycmv0mhaoKRJqy_eFY,3216
|
|
185
185
|
themes/corporate/templates/page.html,sha256=p7ESVUvpRU8XoUfa7EpWPj6vNEdN2VVFWW2xZdCvfIQ,1868
|
|
186
|
+
themes/default/customizations.json,sha256=m2AoocQ9D0_3DBWFXoJQj7st-KQrHAb8UbisndeEqFI,713
|
|
186
187
|
themes/default/theme.yaml,sha256=tgcUP1YFptyXVNL2a8DBiPrP7zTjWNH62Cy9D_w6Chk,1872
|
|
187
188
|
themes/default/templates/404.html,sha256=6pYUz7zg5hx3nikgxiZWSkwYnv2nENCSV3KjdIF0_lE,1105
|
|
188
189
|
themes/default/templates/500.html,sha256=CtU3gEsHsxAh-vbcnx5McH8V8ruKtdP8usj8hPuu8zY,1174
|
|
@@ -202,8 +203,8 @@ themes/minimal/templates/base.html,sha256=LFkx-XLDMGH7oFHHa0e6KPB0DJITOBvr6GtPkD
|
|
|
202
203
|
themes/minimal/templates/home.html,sha256=ygYQgYj1OGCiKwmfsxwkPselVKT8vDH3jLLbfphpqKI,1577
|
|
203
204
|
themes/minimal/templates/page.html,sha256=7Xcoq-ryaxlp913H2S1ishrAro2wsqqGmvsm1osXxd4,389
|
|
204
205
|
themes/minimal/templates/post.html,sha256=FkTRHci8HNIIi3DU6Mb3oL0aDisGyDcsT_IUDwHmrvo,1387
|
|
205
|
-
focomy-0.1.
|
|
206
|
-
focomy-0.1.
|
|
207
|
-
focomy-0.1.
|
|
208
|
-
focomy-0.1.
|
|
209
|
-
focomy-0.1.
|
|
206
|
+
focomy-0.1.117.dist-info/METADATA,sha256=3zOi-f0VwCbVG8wwNQKX0t3_YY2v797WC2unUXNuJQ0,7042
|
|
207
|
+
focomy-0.1.117.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
208
|
+
focomy-0.1.117.dist-info/entry_points.txt,sha256=_rF-wxGI1axY7gox3DBsTLHq-JrFKkMCjA65a6b_oqE,41
|
|
209
|
+
focomy-0.1.117.dist-info/licenses/LICENSE,sha256=z9Z7gN7NNV7zYCaY-Knh3bv8RBCu89VueYtAlN_-lro,1063
|
|
210
|
+
focomy-0.1.117.dist-info/RECORD,,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"color_primary": "#2563eb",
|
|
3
|
+
"color_primary-hover": "#1d4ed8",
|
|
4
|
+
"color_background": "#ffffff",
|
|
5
|
+
"color_surface": "#f8fafc",
|
|
6
|
+
"color_text": "#1e293b",
|
|
7
|
+
"color_text-muted": "#64748b",
|
|
8
|
+
"color_border": "#e2e8f0",
|
|
9
|
+
"color_success": "#22c55e",
|
|
10
|
+
"color_error": "#ef4444",
|
|
11
|
+
"color_warning": "#f59e0b",
|
|
12
|
+
"font_sans": "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
|
13
|
+
"font_serif": "Georgia, 'Times New Roman', serif",
|
|
14
|
+
"font_mono": "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
15
|
+
"space_xs": "0.25rem",
|
|
16
|
+
"space_sm": "0.5rem",
|
|
17
|
+
"space_md": "1rem",
|
|
18
|
+
"space_lg": "1.5rem",
|
|
19
|
+
"space_xl": "2rem",
|
|
20
|
+
"space_2xl": "3rem",
|
|
21
|
+
"custom_css": "/* Test CSS */\n.test-class { color: red; }"
|
|
22
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|