refactor: rename third-party JS libs folder from vendor to lib
Rename static/shared/js/vendor/ to static/shared/js/lib/ to avoid confusion with the app's "vendor" (seller) dashboard code in static/vendor/js/. - Rename directory: vendor/ → lib/ - Update all template references to use new path - Update CDN fallback documentation - Fix .gitignore to use /lib/ (root only) instead of lib/ (everywhere) Third-party libraries: - alpine.min.js - chart.umd.min.js - flatpickr.min.js - quill.js - tom-select.complete.min.js Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -14,8 +14,8 @@ dist/
|
|||||||
downloads/
|
downloads/
|
||||||
eggs/
|
eggs/
|
||||||
.eggs/
|
.eggs/
|
||||||
lib/
|
/lib/
|
||||||
lib64/
|
/lib64/
|
||||||
parts/
|
parts/
|
||||||
sdist/
|
sdist/
|
||||||
var/
|
var/
|
||||||
|
|||||||
@@ -72,6 +72,9 @@
|
|||||||
<!-- Flatpickr CSS with CDN fallback (loaded on demand via block) -->
|
<!-- Flatpickr CSS with CDN fallback (loaded on demand via block) -->
|
||||||
{% block flatpickr_css %}{% endblock %}
|
{% block flatpickr_css %}{% endblock %}
|
||||||
|
|
||||||
|
<!-- Quill CSS with CDN fallback (loaded on demand via block) -->
|
||||||
|
{% block quill_css %}{% endblock %}
|
||||||
|
|
||||||
{% block extra_head %}{% endblock %}
|
{% block extra_head %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body x-cloak>
|
<body x-cloak>
|
||||||
@@ -117,7 +120,7 @@
|
|||||||
script.onerror = function() {
|
script.onerror = function() {
|
||||||
console.warn('Tom Select CDN failed, loading local copy...');
|
console.warn('Tom Select CDN failed, loading local copy...');
|
||||||
var fallbackScript = document.createElement('script');
|
var fallbackScript = document.createElement('script');
|
||||||
fallbackScript.src = '{{ url_for("static", path="shared/js/vendor/tom-select.complete.min.js") }}';
|
fallbackScript.src = '{{ url_for("static", path="shared/js/lib/tom-select.complete.min.js") }}';
|
||||||
document.head.appendChild(fallbackScript);
|
document.head.appendChild(fallbackScript);
|
||||||
};
|
};
|
||||||
document.head.appendChild(script);
|
document.head.appendChild(script);
|
||||||
@@ -138,7 +141,7 @@
|
|||||||
console.warn('Alpine.js CDN failed, loading local copy...');
|
console.warn('Alpine.js CDN failed, loading local copy...');
|
||||||
var fallbackScript = document.createElement('script');
|
var fallbackScript = document.createElement('script');
|
||||||
fallbackScript.defer = true;
|
fallbackScript.defer = true;
|
||||||
fallbackScript.src = '{{ url_for("static", path="shared/js/vendor/alpine.min.js") }}';
|
fallbackScript.src = '{{ url_for("static", path="shared/js/lib/alpine.min.js") }}';
|
||||||
document.head.appendChild(fallbackScript);
|
document.head.appendChild(fallbackScript);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -152,7 +155,10 @@
|
|||||||
<!-- 10. OPTIONAL: Flatpickr with CDN fallback (loaded on demand via block) -->
|
<!-- 10. OPTIONAL: Flatpickr with CDN fallback (loaded on demand via block) -->
|
||||||
{% block flatpickr_script %}{% endblock %}
|
{% block flatpickr_script %}{% endblock %}
|
||||||
|
|
||||||
<!-- 11. LAST: Page-specific scripts -->
|
<!-- 11. OPTIONAL: Quill with CDN fallback (loaded on demand via block) -->
|
||||||
|
{% block quill_script %}{% endblock %}
|
||||||
|
|
||||||
|
<!-- 12. LAST: Page-specific scripts -->
|
||||||
{% block extra_scripts %}{% endblock %}
|
{% block extra_scripts %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -309,7 +309,7 @@
|
|||||||
script.onerror = function() {
|
script.onerror = function() {
|
||||||
console.warn('Tom Select CDN failed, loading local copy...');
|
console.warn('Tom Select CDN failed, loading local copy...');
|
||||||
var fallbackScript = document.createElement('script');
|
var fallbackScript = document.createElement('script');
|
||||||
fallbackScript.src = '{{ url_for("static", path="shared/js/vendor/tom-select.complete.min.js") }}';
|
fallbackScript.src = '{{ url_for("static", path="shared/js/lib/tom-select.complete.min.js") }}';
|
||||||
document.head.appendChild(fallbackScript);
|
document.head.appendChild(fallbackScript);
|
||||||
};
|
};
|
||||||
document.head.appendChild(script);
|
document.head.appendChild(script);
|
||||||
|
|||||||
@@ -117,8 +117,22 @@
|
|||||||
<!-- 4. API Client -->
|
<!-- 4. API Client -->
|
||||||
<script src="{{ url_for('static', path='shared/js/api-client.js') }}"></script>
|
<script src="{{ url_for('static', path='shared/js/api-client.js') }}"></script>
|
||||||
|
|
||||||
<!-- 5. Alpine.js v3 -->
|
<!-- 5. Alpine.js v3 with CDN fallback -->
|
||||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.0/dist/cdn.min.js"></script>
|
<script>
|
||||||
|
(function() {
|
||||||
|
var script = document.createElement('script');
|
||||||
|
script.defer = true;
|
||||||
|
script.src = 'https://cdn.jsdelivr.net/npm/alpinejs@3.14.0/dist/cdn.min.js';
|
||||||
|
script.onerror = function() {
|
||||||
|
console.warn('Alpine.js CDN failed, loading local copy...');
|
||||||
|
var fallbackScript = document.createElement('script');
|
||||||
|
fallbackScript.defer = true;
|
||||||
|
fallbackScript.src = '{{ url_for("static", path="shared/js/lib/alpine.min.js") }}';
|
||||||
|
document.head.appendChild(fallbackScript);
|
||||||
|
};
|
||||||
|
document.head.appendChild(script);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
<!-- 6. Login Logic -->
|
<!-- 6. Login Logic -->
|
||||||
<script src="{{ url_for('static', path='admin/js/login.js') }}"></script>
|
<script src="{{ url_for('static', path='admin/js/login.js') }}"></script>
|
||||||
|
|||||||
@@ -593,7 +593,7 @@
|
|||||||
script.onerror = function() {
|
script.onerror = function() {
|
||||||
console.warn('Tom Select CDN failed, loading local copy...');
|
console.warn('Tom Select CDN failed, loading local copy...');
|
||||||
var fallbackScript = document.createElement('script');
|
var fallbackScript = document.createElement('script');
|
||||||
fallbackScript.src = '{{ url_for("static", path="shared/js/vendor/tom-select.complete.min.js") }}';
|
fallbackScript.src = '{{ url_for("static", path="shared/js/lib/tom-select.complete.min.js") }}';
|
||||||
document.head.appendChild(fallbackScript);
|
document.head.appendChild(fallbackScript);
|
||||||
};
|
};
|
||||||
document.head.appendChild(script);
|
document.head.appendChild(script);
|
||||||
|
|||||||
@@ -466,7 +466,7 @@
|
|||||||
script.onerror = function() {
|
script.onerror = function() {
|
||||||
console.warn('Tom Select CDN failed, loading local copy...');
|
console.warn('Tom Select CDN failed, loading local copy...');
|
||||||
var fallbackScript = document.createElement('script');
|
var fallbackScript = document.createElement('script');
|
||||||
fallbackScript.src = '{{ url_for("static", path="shared/js/vendor/tom-select.complete.min.js") }}';
|
fallbackScript.src = '{{ url_for("static", path="shared/js/lib/tom-select.complete.min.js") }}';
|
||||||
document.head.appendChild(fallbackScript);
|
document.head.appendChild(fallbackScript);
|
||||||
};
|
};
|
||||||
document.head.appendChild(script);
|
document.head.appendChild(script);
|
||||||
|
|||||||
@@ -2,11 +2,20 @@
|
|||||||
{% extends "admin/base.html" %}
|
{% extends "admin/base.html" %}
|
||||||
{% from 'shared/macros/headers.html' import detail_page_header %}
|
{% from 'shared/macros/headers.html' import detail_page_header %}
|
||||||
{% from 'shared/macros/modals.html' import media_picker_modal %}
|
{% from 'shared/macros/modals.html' import media_picker_modal %}
|
||||||
|
{% from 'shared/macros/richtext.html' import quill_css, quill_js, quill_editor %}
|
||||||
|
|
||||||
{% block title %}Create Vendor Product{% endblock %}
|
{% block title %}Create Vendor Product{% endblock %}
|
||||||
|
|
||||||
{% block alpine_data %}adminVendorProductCreate(){% endblock %}
|
{% block alpine_data %}adminVendorProductCreate(){% endblock %}
|
||||||
|
|
||||||
|
{% block quill_css %}
|
||||||
|
{{ quill_css() }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block quill_script %}
|
||||||
|
{{ quill_js() }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_head %}
|
{% block extra_head %}
|
||||||
<!-- Tom Select CSS with local fallback -->
|
<!-- Tom Select CSS with local fallback -->
|
||||||
<link
|
<link
|
||||||
@@ -90,34 +99,98 @@
|
|||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Translation Fields -->
|
<!-- Translation Fields - English -->
|
||||||
<template x-for="lang in ['en', 'fr', 'de', 'lu']" :key="lang">
|
<div x-show="activeLanguage === 'en'" class="space-y-4">
|
||||||
<div x-show="activeLanguage === lang" class="space-y-4">
|
<div>
|
||||||
<div>
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1">
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1">
|
Title (EN) <span class="text-red-500">*</span>
|
||||||
Title (<span x-text="lang.toUpperCase()"></span>) <span x-show="lang === 'en'" class="text-red-500">*</span>
|
</label>
|
||||||
</label>
|
<input
|
||||||
<input
|
type="text"
|
||||||
type="text"
|
x-model="form.translations.en.title"
|
||||||
x-model="form.translations[lang].title"
|
required
|
||||||
:required="lang === 'en'"
|
class="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300"
|
||||||
class="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300"
|
placeholder="Product title"
|
||||||
placeholder="Product title"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1">
|
|
||||||
Description (<span x-text="lang.toUpperCase()"></span>)
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
x-model="form.translations[lang].description"
|
|
||||||
rows="5"
|
|
||||||
class="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300"
|
|
||||||
placeholder="Product description (HTML supported)"
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
{{ quill_editor(
|
||||||
|
id='create-desc-editor-en',
|
||||||
|
model='form.translations.en.description',
|
||||||
|
label='Description (EN)',
|
||||||
|
placeholder='Enter product description in English...',
|
||||||
|
min_height='150px',
|
||||||
|
toolbar='standard'
|
||||||
|
) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Translation Fields - French -->
|
||||||
|
<div x-show="activeLanguage === 'fr'" class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1">
|
||||||
|
Title (FR)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
x-model="form.translations.fr.title"
|
||||||
|
class="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300"
|
||||||
|
placeholder="Product title"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{{ quill_editor(
|
||||||
|
id='create-desc-editor-fr',
|
||||||
|
model='form.translations.fr.description',
|
||||||
|
label='Description (FR)',
|
||||||
|
placeholder='Enter product description in French...',
|
||||||
|
min_height='150px',
|
||||||
|
toolbar='standard'
|
||||||
|
) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Translation Fields - German -->
|
||||||
|
<div x-show="activeLanguage === 'de'" class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1">
|
||||||
|
Title (DE)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
x-model="form.translations.de.title"
|
||||||
|
class="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300"
|
||||||
|
placeholder="Product title"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{{ quill_editor(
|
||||||
|
id='create-desc-editor-de',
|
||||||
|
model='form.translations.de.description',
|
||||||
|
label='Description (DE)',
|
||||||
|
placeholder='Enter product description in German...',
|
||||||
|
min_height='150px',
|
||||||
|
toolbar='standard'
|
||||||
|
) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Translation Fields - Luxembourgish -->
|
||||||
|
<div x-show="activeLanguage === 'lu'" class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1">
|
||||||
|
Title (LU)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
x-model="form.translations.lu.title"
|
||||||
|
class="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300"
|
||||||
|
placeholder="Product title"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{{ quill_editor(
|
||||||
|
id='create-desc-editor-lu',
|
||||||
|
model='form.translations.lu.description',
|
||||||
|
label='Description (LU)',
|
||||||
|
placeholder='Enter product description in Luxembourgish...',
|
||||||
|
min_height='150px',
|
||||||
|
toolbar='standard'
|
||||||
|
) }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Product Identifiers -->
|
<!-- Product Identifiers -->
|
||||||
@@ -428,7 +501,7 @@
|
|||||||
script.onerror = function() {
|
script.onerror = function() {
|
||||||
console.warn('Tom Select CDN failed, loading local copy...');
|
console.warn('Tom Select CDN failed, loading local copy...');
|
||||||
var fallbackScript = document.createElement('script');
|
var fallbackScript = document.createElement('script');
|
||||||
fallbackScript.src = '{{ url_for("static", path="shared/js/vendor/tom-select.complete.min.js") }}';
|
fallbackScript.src = '{{ url_for("static", path="shared/js/lib/tom-select.complete.min.js") }}';
|
||||||
document.head.appendChild(fallbackScript);
|
document.head.appendChild(fallbackScript);
|
||||||
};
|
};
|
||||||
document.head.appendChild(script);
|
document.head.appendChild(script);
|
||||||
|
|||||||
@@ -390,7 +390,7 @@
|
|||||||
script.onerror = function() {
|
script.onerror = function() {
|
||||||
console.warn('Tom Select CDN failed, loading local copy...');
|
console.warn('Tom Select CDN failed, loading local copy...');
|
||||||
var fallbackScript = document.createElement('script');
|
var fallbackScript = document.createElement('script');
|
||||||
fallbackScript.src = '{{ url_for("static", path="shared/js/vendor/tom-select.complete.min.js") }}';
|
fallbackScript.src = '{{ url_for("static", path="shared/js/lib/tom-select.complete.min.js") }}';
|
||||||
document.head.appendChild(fallbackScript);
|
document.head.appendChild(fallbackScript);
|
||||||
};
|
};
|
||||||
document.head.appendChild(script);
|
document.head.appendChild(script);
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
script.onerror = function() {
|
script.onerror = function() {
|
||||||
console.warn('Chart.js CDN failed, loading local copy...');
|
console.warn('Chart.js CDN failed, loading local copy...');
|
||||||
var fallbackScript = document.createElement('script');
|
var fallbackScript = document.createElement('script');
|
||||||
fallbackScript.src = '{{ url_for("static", path="shared/js/vendor/chart.umd.min.js") }}';
|
fallbackScript.src = '{{ url_for("static", path="shared/js/lib/chart.umd.min.js") }}';
|
||||||
document.head.appendChild(fallbackScript);
|
document.head.appendChild(fallbackScript);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
script.onerror = function() {
|
script.onerror = function() {
|
||||||
console.warn('Flatpickr CDN failed, loading local copy...');
|
console.warn('Flatpickr CDN failed, loading local copy...');
|
||||||
var fallbackScript = document.createElement('script');
|
var fallbackScript = document.createElement('script');
|
||||||
fallbackScript.src = '{{ url_for("static", path="shared/js/vendor/flatpickr.min.js") }}';
|
fallbackScript.src = '{{ url_for("static", path="shared/js/lib/flatpickr.min.js") }}';
|
||||||
document.head.appendChild(fallbackScript);
|
document.head.appendChild(fallbackScript);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
326
app/templates/shared/macros/richtext.html
Normal file
326
app/templates/shared/macros/richtext.html
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
{#
|
||||||
|
Rich Text Editor Macros (Quill)
|
||||||
|
===============================
|
||||||
|
Reusable rich text editor components using Quill.js with Alpine.js integration.
|
||||||
|
|
||||||
|
Setup (in your template):
|
||||||
|
1. Import the macros:
|
||||||
|
{% from 'shared/macros/richtext.html' import quill_css, quill_js, quill_editor %}
|
||||||
|
|
||||||
|
2. Add CSS block (in {% block quill_css %}):
|
||||||
|
{{ quill_css() }}
|
||||||
|
|
||||||
|
3. Add JS block (in {% block quill_script %}):
|
||||||
|
{{ quill_js() }}
|
||||||
|
|
||||||
|
4. Use the editor in your form:
|
||||||
|
{{ quill_editor(
|
||||||
|
id='content-editor',
|
||||||
|
model='form.content',
|
||||||
|
placeholder='Write your content here...'
|
||||||
|
) }}
|
||||||
|
|
||||||
|
Note: Quill loads from CDN with local fallback for offline use.
|
||||||
|
#}
|
||||||
|
|
||||||
|
|
||||||
|
{#
|
||||||
|
Quill CSS Block
|
||||||
|
===============
|
||||||
|
Include this in your {% block quill_css %} to load Quill styles.
|
||||||
|
Includes dark mode support.
|
||||||
|
#}
|
||||||
|
{% macro quill_css() %}
|
||||||
|
<!-- Quill Snow Theme CSS with CDN fallback -->
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/quill@2.0.2/dist/quill.snow.css"
|
||||||
|
onerror="this.onerror=null; this.href='/static/shared/css/vendor/quill.snow.css';"
|
||||||
|
/>
|
||||||
|
<!-- Quill Dark Mode Overrides -->
|
||||||
|
<style>
|
||||||
|
/* Editor container styling */
|
||||||
|
.quill-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.quill-container .ql-toolbar {
|
||||||
|
border-top-left-radius: 0.5rem;
|
||||||
|
border-top-right-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
.quill-container .ql-container {
|
||||||
|
border-bottom-left-radius: 0.5rem;
|
||||||
|
border-bottom-right-radius: 0.5rem;
|
||||||
|
min-height: 200px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode styles */
|
||||||
|
.dark .ql-toolbar.ql-snow {
|
||||||
|
background-color: rgb(55 65 81);
|
||||||
|
border-color: rgb(75 85 99);
|
||||||
|
}
|
||||||
|
.dark .ql-container.ql-snow {
|
||||||
|
background-color: rgb(55 65 81);
|
||||||
|
border-color: rgb(75 85 99);
|
||||||
|
}
|
||||||
|
.dark .ql-editor {
|
||||||
|
color: rgb(209 213 219);
|
||||||
|
}
|
||||||
|
.dark .ql-editor.ql-blank::before {
|
||||||
|
color: rgb(156 163 175);
|
||||||
|
}
|
||||||
|
.dark .ql-snow .ql-stroke {
|
||||||
|
stroke: rgb(209 213 219);
|
||||||
|
}
|
||||||
|
.dark .ql-snow .ql-fill {
|
||||||
|
fill: rgb(209 213 219);
|
||||||
|
}
|
||||||
|
.dark .ql-snow .ql-picker {
|
||||||
|
color: rgb(209 213 219);
|
||||||
|
}
|
||||||
|
.dark .ql-snow .ql-picker-label {
|
||||||
|
color: rgb(209 213 219);
|
||||||
|
}
|
||||||
|
.dark .ql-snow .ql-picker-options {
|
||||||
|
background-color: rgb(55 65 81);
|
||||||
|
border-color: rgb(75 85 99);
|
||||||
|
}
|
||||||
|
.dark .ql-snow .ql-picker-item {
|
||||||
|
color: rgb(209 213 219);
|
||||||
|
}
|
||||||
|
.dark .ql-snow .ql-picker-item:hover {
|
||||||
|
color: rgb(147 51 234);
|
||||||
|
}
|
||||||
|
.dark .ql-snow .ql-picker-item.ql-selected {
|
||||||
|
color: rgb(147 51 234);
|
||||||
|
}
|
||||||
|
.dark .ql-toolbar.ql-snow .ql-picker-label:hover,
|
||||||
|
.dark .ql-toolbar.ql-snow .ql-picker-label.ql-active,
|
||||||
|
.dark .ql-toolbar.ql-snow button:hover,
|
||||||
|
.dark .ql-toolbar.ql-snow button.ql-active {
|
||||||
|
color: rgb(147 51 234);
|
||||||
|
}
|
||||||
|
.dark .ql-toolbar.ql-snow button:hover .ql-stroke,
|
||||||
|
.dark .ql-toolbar.ql-snow button.ql-active .ql-stroke {
|
||||||
|
stroke: rgb(147 51 234);
|
||||||
|
}
|
||||||
|
.dark .ql-toolbar.ql-snow button:hover .ql-fill,
|
||||||
|
.dark .ql-toolbar.ql-snow button.ql-active .ql-fill {
|
||||||
|
fill: rgb(147 51 234);
|
||||||
|
}
|
||||||
|
.dark .ql-snow a {
|
||||||
|
color: rgb(167 139 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Focus state */
|
||||||
|
.ql-container.ql-snow:focus-within {
|
||||||
|
border-color: rgb(147 51 234);
|
||||||
|
}
|
||||||
|
.ql-toolbar.ql-snow:has(+ .ql-container:focus-within) {
|
||||||
|
border-color: rgb(147 51 234);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code block styling */
|
||||||
|
.dark .ql-snow .ql-editor pre.ql-syntax {
|
||||||
|
background-color: rgb(31 41 55);
|
||||||
|
color: rgb(209 213 219);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tooltip styling */
|
||||||
|
.dark .ql-snow .ql-tooltip {
|
||||||
|
background-color: rgb(55 65 81);
|
||||||
|
border-color: rgb(75 85 99);
|
||||||
|
color: rgb(209 213 219);
|
||||||
|
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.3);
|
||||||
|
}
|
||||||
|
.dark .ql-snow .ql-tooltip input[type=text] {
|
||||||
|
background-color: rgb(31 41 55);
|
||||||
|
border-color: rgb(75 85 99);
|
||||||
|
color: rgb(209 213 219);
|
||||||
|
}
|
||||||
|
.dark .ql-snow .ql-tooltip a.ql-action::after,
|
||||||
|
.dark .ql-snow .ql-tooltip a.ql-remove::before {
|
||||||
|
color: rgb(167 139 250);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
|
{#
|
||||||
|
Quill JS Block
|
||||||
|
==============
|
||||||
|
Include this in your {% block quill_script %} to load Quill.
|
||||||
|
Uses CDN with local fallback.
|
||||||
|
#}
|
||||||
|
{% macro quill_js() %}
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
// Check if Quill already loaded
|
||||||
|
if (window.Quill) return;
|
||||||
|
|
||||||
|
var script = document.createElement('script');
|
||||||
|
script.src = 'https://cdn.jsdelivr.net/npm/quill@2.0.2/dist/quill.js';
|
||||||
|
script.onerror = function() {
|
||||||
|
console.warn('Quill CDN failed, loading local copy...');
|
||||||
|
var fallbackScript = document.createElement('script');
|
||||||
|
fallbackScript.src = '/static/shared/js/lib/quill.js';
|
||||||
|
document.head.appendChild(fallbackScript);
|
||||||
|
};
|
||||||
|
document.head.appendChild(script);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
|
{#
|
||||||
|
Quill Editor Component
|
||||||
|
======================
|
||||||
|
A rich text editor with Alpine.js two-way binding.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- id: Unique ID for the editor (required)
|
||||||
|
- model: Alpine.js model to bind HTML content to (required)
|
||||||
|
- placeholder: Placeholder text (default: 'Write something...')
|
||||||
|
- min_height: Minimum height of editor (default: '200px')
|
||||||
|
- toolbar: Toolbar configuration - 'full', 'standard', 'minimal' (default: 'standard')
|
||||||
|
- readonly: Alpine.js variable for readonly state (optional)
|
||||||
|
- disabled: Alpine.js variable for disabled state (optional)
|
||||||
|
- label: Label text (optional)
|
||||||
|
- required: Whether field is required (default: false)
|
||||||
|
- help_text: Help text below editor (optional)
|
||||||
|
|
||||||
|
Toolbar presets:
|
||||||
|
- 'full': All formatting options
|
||||||
|
- 'standard': Headers, bold, italic, lists, links, images (default)
|
||||||
|
- 'minimal': Bold, italic, lists only
|
||||||
|
#}
|
||||||
|
{% macro quill_editor(
|
||||||
|
id,
|
||||||
|
model,
|
||||||
|
placeholder='Write something...',
|
||||||
|
min_height='200px',
|
||||||
|
toolbar='standard',
|
||||||
|
readonly=none,
|
||||||
|
disabled=none,
|
||||||
|
label=none,
|
||||||
|
required=false,
|
||||||
|
help_text=none
|
||||||
|
) %}
|
||||||
|
<div class="quill-wrapper" {% if disabled %}x-bind:class="{ 'opacity-50 pointer-events-none': {{ disabled }} }"{% endif %}>
|
||||||
|
{% if label %}
|
||||||
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
{{ label }}{% if required %} <span class="text-red-500">*</span>{% endif %}
|
||||||
|
</label>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="quill-container">
|
||||||
|
<div id="{{ id }}" style="min-height: {{ min_height }};"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if help_text %}
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{ help_text }}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
// Wait for Alpine and Quill to be ready
|
||||||
|
function initEditor() {
|
||||||
|
if (typeof Alpine === 'undefined' || typeof Quill === 'undefined') {
|
||||||
|
setTimeout(initEditor, 50);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = document.getElementById('{{ id }}');
|
||||||
|
if (!container || container._quillInitialized) return;
|
||||||
|
container._quillInitialized = true;
|
||||||
|
|
||||||
|
// Toolbar configurations
|
||||||
|
const toolbars = {
|
||||||
|
full: [
|
||||||
|
[{'header': [1, 2, 3, 4, 5, 6, false]}],
|
||||||
|
['bold', 'italic', 'underline', 'strike'],
|
||||||
|
[{'color': []}, {'background': []}],
|
||||||
|
[{'list': 'ordered'}, {'list': 'bullet'}],
|
||||||
|
[{'indent': '-1'}, {'indent': '+1'}],
|
||||||
|
[{'align': []}],
|
||||||
|
['blockquote', 'code-block'],
|
||||||
|
['link', 'image', 'video'],
|
||||||
|
['clean']
|
||||||
|
],
|
||||||
|
standard: [
|
||||||
|
[{'header': [1, 2, 3, false]}],
|
||||||
|
['bold', 'italic', 'underline'],
|
||||||
|
[{'list': 'ordered'}, {'list': 'bullet'}],
|
||||||
|
['link', 'image'],
|
||||||
|
['clean']
|
||||||
|
],
|
||||||
|
minimal: [
|
||||||
|
['bold', 'italic'],
|
||||||
|
[{'list': 'ordered'}, {'list': 'bullet'}],
|
||||||
|
['link'],
|
||||||
|
['clean']
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const quill = new Quill(container, {
|
||||||
|
theme: 'snow',
|
||||||
|
placeholder: '{{ placeholder }}',
|
||||||
|
readOnly: {{ 'true' if readonly else 'false' }},
|
||||||
|
modules: {
|
||||||
|
toolbar: toolbars['{{ toolbar }}'] || toolbars.standard
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find the Alpine component scope
|
||||||
|
const wrapper = container.closest('[x-data]');
|
||||||
|
if (wrapper && wrapper._x_dataStack) {
|
||||||
|
const data = wrapper._x_dataStack[0];
|
||||||
|
|
||||||
|
// Set initial content from model
|
||||||
|
const getModelValue = () => {
|
||||||
|
try {
|
||||||
|
return eval('data.{{ model }}');
|
||||||
|
} catch(e) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setModelValue = (val) => {
|
||||||
|
try {
|
||||||
|
const parts = '{{ model }}'.split('.');
|
||||||
|
let obj = data;
|
||||||
|
for (let i = 0; i < parts.length - 1; i++) {
|
||||||
|
obj = obj[parts[i]];
|
||||||
|
}
|
||||||
|
obj[parts[parts.length - 1]] = val;
|
||||||
|
} catch(e) {
|
||||||
|
console.warn('Could not set model value:', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set initial content
|
||||||
|
const initialContent = getModelValue();
|
||||||
|
if (initialContent) {
|
||||||
|
quill.root.innerHTML = initialContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync changes back to Alpine model
|
||||||
|
quill.on('text-change', function() {
|
||||||
|
const html = quill.root.innerHTML;
|
||||||
|
const isEmpty = html === '<p><br></p>' || html === '<p></p>' || html === '';
|
||||||
|
setModelValue(isEmpty ? '' : html);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start initialization
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', initEditor);
|
||||||
|
} else {
|
||||||
|
setTimeout(initEditor, 100);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
@@ -340,7 +340,7 @@
|
|||||||
console.warn('Alpine.js CDN failed, loading local copy...');
|
console.warn('Alpine.js CDN failed, loading local copy...');
|
||||||
var fallbackScript = document.createElement('script');
|
var fallbackScript = document.createElement('script');
|
||||||
fallbackScript.defer = true;
|
fallbackScript.defer = true;
|
||||||
fallbackScript.src = '{{ url_for("static", path="shared/js/vendor/alpine.min.js") }}';
|
fallbackScript.src = '{{ url_for("static", path="shared/js/lib/alpine.min.js") }}';
|
||||||
document.head.appendChild(fallbackScript);
|
document.head.appendChild(fallbackScript);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
2
app/templates/vendor/base.html
vendored
2
app/templates/vendor/base.html
vendored
@@ -88,7 +88,7 @@
|
|||||||
console.warn('Alpine.js CDN failed, loading local copy...');
|
console.warn('Alpine.js CDN failed, loading local copy...');
|
||||||
var fallbackScript = document.createElement('script');
|
var fallbackScript = document.createElement('script');
|
||||||
fallbackScript.defer = true;
|
fallbackScript.defer = true;
|
||||||
fallbackScript.src = '{{ url_for("static", path="shared/js/vendor/alpine.min.js") }}';
|
fallbackScript.src = '{{ url_for("static", path="shared/js/lib/alpine.min.js") }}';
|
||||||
document.head.appendChild(fallbackScript);
|
document.head.appendChild(fallbackScript);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -505,13 +505,13 @@ All frontends use CDN with local fallback. Check that local copies exist:
|
|||||||
```bash
|
```bash
|
||||||
# From project root
|
# From project root
|
||||||
ls -lh static/shared/css/tailwind.min.css
|
ls -lh static/shared/css/tailwind.min.css
|
||||||
ls -lh static/shared/js/vendor/alpine.min.js
|
ls -lh static/shared/js/lib/alpine.min.js
|
||||||
```
|
```
|
||||||
|
|
||||||
**Expected output:**
|
**Expected output:**
|
||||||
```
|
```
|
||||||
-rw-r--r-- 1 user user 2.9M static/shared/css/tailwind.min.css
|
-rw-r--r-- 1 user user 2.9M static/shared/css/tailwind.min.css
|
||||||
-rw-r--r-- 1 user user 44K static/shared/js/vendor/alpine.min.js
|
-rw-r--r-- 1 user user 44K static/shared/js/lib/alpine.min.js
|
||||||
```
|
```
|
||||||
|
|
||||||
**If files are missing:**
|
**If files are missing:**
|
||||||
@@ -569,7 +569,7 @@ app.mount("/static", StaticFiles(directory="static"), name="static")
|
|||||||
```bash
|
```bash
|
||||||
# With app running, test directly:
|
# With app running, test directly:
|
||||||
curl http://localhost:8000/static/shared/css/tailwind.min.css | head -n 5
|
curl http://localhost:8000/static/shared/css/tailwind.min.css | head -n 5
|
||||||
curl http://localhost:8000/static/shared/js/vendor/alpine.min.js | head -n 5
|
curl http://localhost:8000/static/shared/js/lib/alpine.min.js | head -n 5
|
||||||
```
|
```
|
||||||
|
|
||||||
### Solution 5: File Permissions
|
### Solution 5: File Permissions
|
||||||
@@ -579,7 +579,7 @@ If files exist but return 403 Forbidden:
|
|||||||
```bash
|
```bash
|
||||||
# Fix permissions
|
# Fix permissions
|
||||||
chmod 644 static/shared/css/tailwind.min.css
|
chmod 644 static/shared/css/tailwind.min.css
|
||||||
chmod 644 static/shared/js/vendor/alpine.min.js
|
chmod 644 static/shared/js/lib/alpine.min.js
|
||||||
chmod 755 static/shared/css
|
chmod 755 static/shared/css
|
||||||
chmod 755 static/shared/js/vendor
|
chmod 755 static/shared/js/vendor
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -18,14 +18,14 @@ The following assets are loaded from CDN with automatic fallback to local copies
|
|||||||
| Asset | CDN Source | Local Fallback |
|
| Asset | CDN Source | Local Fallback |
|
||||||
|-------|-----------|----------------|
|
|-------|-----------|----------------|
|
||||||
| **Tailwind CSS** | `https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css` | `static/shared/css/tailwind.min.css` |
|
| **Tailwind CSS** | `https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css` | `static/shared/css/tailwind.min.css` |
|
||||||
| **Alpine.js** | `https://cdn.jsdelivr.net/npm/alpinejs@3.13.3/dist/cdn.min.js` | `static/shared/js/vendor/alpine.min.js` |
|
| **Alpine.js** | `https://cdn.jsdelivr.net/npm/alpinejs@3.13.3/dist/cdn.min.js` | `static/shared/js/lib/alpine.min.js` |
|
||||||
|
|
||||||
### Optional Assets (Loaded On Demand)
|
### Optional Assets (Loaded On Demand)
|
||||||
|
|
||||||
| Asset | CDN Source | Local Fallback | Used For |
|
| Asset | CDN Source | Local Fallback | Used For |
|
||||||
|-------|-----------|----------------|----------|
|
|-------|-----------|----------------|----------|
|
||||||
| **Chart.js** | `https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js` | `static/shared/js/vendor/chart.umd.min.js` | Charts macros |
|
| **Chart.js** | `https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js` | `static/shared/js/lib/chart.umd.min.js` | Charts macros |
|
||||||
| **Flatpickr JS** | `https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.js` | `static/shared/js/vendor/flatpickr.min.js` | Datepicker macros |
|
| **Flatpickr JS** | `https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.js` | `static/shared/js/lib/flatpickr.min.js` | Datepicker macros |
|
||||||
| **Flatpickr CSS** | `https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.css` | `static/shared/css/vendor/flatpickr.min.css` | Datepicker styling |
|
| **Flatpickr CSS** | `https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.css` | `static/shared/css/vendor/flatpickr.min.css` | Datepicker styling |
|
||||||
|
|
||||||
## Implementation
|
## Implementation
|
||||||
@@ -63,7 +63,7 @@ Alpine.js uses dynamic script loading with error handling:
|
|||||||
console.warn('Alpine.js CDN failed, loading local copy...');
|
console.warn('Alpine.js CDN failed, loading local copy...');
|
||||||
var fallbackScript = document.createElement('script');
|
var fallbackScript = document.createElement('script');
|
||||||
fallbackScript.defer = true;
|
fallbackScript.defer = true;
|
||||||
fallbackScript.src = '{{ url_for("static", path="shared/js/vendor/alpine.min.js") }}';
|
fallbackScript.src = '{{ url_for("static", path="shared/js/lib/alpine.min.js") }}';
|
||||||
document.head.appendChild(fallbackScript);
|
document.head.appendChild(fallbackScript);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ In your page template:
|
|||||||
script.onerror = function() {
|
script.onerror = function() {
|
||||||
console.warn('Chart.js CDN failed, loading local copy...');
|
console.warn('Chart.js CDN failed, loading local copy...');
|
||||||
var fallbackScript = document.createElement('script');
|
var fallbackScript = document.createElement('script');
|
||||||
fallbackScript.src = '/static/shared/js/vendor/chart.umd.min.js';
|
fallbackScript.src = '/static/shared/js/lib/chart.umd.min.js';
|
||||||
document.head.appendChild(fallbackScript);
|
document.head.appendChild(fallbackScript);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -207,7 +207,7 @@ app/templates/shared/
|
|||||||
console.warn('Alpine.js CDN failed, loading local copy...');
|
console.warn('Alpine.js CDN failed, loading local copy...');
|
||||||
var fallbackScript = document.createElement('script');
|
var fallbackScript = document.createElement('script');
|
||||||
fallbackScript.defer = true;
|
fallbackScript.defer = true;
|
||||||
fallbackScript.src = '{{ url_for("static", path="shared/js/vendor/alpine.min.js") }}';
|
fallbackScript.src = '{{ url_for("static", path="shared/js/lib/alpine.min.js") }}';
|
||||||
document.head.appendChild(fallbackScript);
|
document.head.appendChild(fallbackScript);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -261,13 +261,13 @@ Check that local fallback files exist:
|
|||||||
```bash
|
```bash
|
||||||
# From project root
|
# From project root
|
||||||
ls -lh static/shared/css/tailwind.min.css
|
ls -lh static/shared/css/tailwind.min.css
|
||||||
ls -lh static/shared/js/vendor/alpine.min.js
|
ls -lh static/shared/js/lib/alpine.min.js
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected output:
|
Expected output:
|
||||||
```
|
```
|
||||||
-rw-r--r-- 1 user user 2.9M static/shared/css/tailwind.min.css
|
-rw-r--r-- 1 user user 2.9M static/shared/css/tailwind.min.css
|
||||||
-rw-r--r-- 1 user user 44K static/shared/js/vendor/alpine.min.js
|
-rw-r--r-- 1 user user 44K static/shared/js/lib/alpine.min.js
|
||||||
```
|
```
|
||||||
|
|
||||||
### Testing Offline Mode
|
### Testing Offline Mode
|
||||||
@@ -291,7 +291,7 @@ To test the fallback behavior:
|
|||||||
|
|
||||||
4. **Verify fallback loaded:**
|
4. **Verify fallback loaded:**
|
||||||
- Open DevTools Network tab
|
- Open DevTools Network tab
|
||||||
- Check that `/static/shared/js/vendor/alpine.min.js` was loaded
|
- Check that `/static/shared/js/lib/alpine.min.js` was loaded
|
||||||
- Check that `/static/shared/css/tailwind.min.css` was loaded
|
- Check that `/static/shared/css/tailwind.min.css` was loaded
|
||||||
|
|
||||||
### Updating Local Assets
|
### Updating Local Assets
|
||||||
@@ -362,11 +362,11 @@ COPY static/ /app/static/
|
|||||||
|
|
||||||
# Verify core files exist
|
# Verify core files exist
|
||||||
RUN test -f /app/static/shared/css/tailwind.min.css && \
|
RUN test -f /app/static/shared/css/tailwind.min.css && \
|
||||||
test -f /app/static/shared/js/vendor/alpine.min.js
|
test -f /app/static/shared/js/lib/alpine.min.js
|
||||||
|
|
||||||
# Verify optional library files exist
|
# Verify optional library files exist
|
||||||
RUN test -f /app/static/shared/js/vendor/chart.umd.min.js && \
|
RUN test -f /app/static/shared/js/lib/chart.umd.min.js && \
|
||||||
test -f /app/static/shared/js/vendor/flatpickr.min.js && \
|
test -f /app/static/shared/js/lib/flatpickr.min.js && \
|
||||||
test -f /app/static/shared/css/vendor/flatpickr.min.css
|
test -f /app/static/shared/css/vendor/flatpickr.min.css
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -447,7 +447,7 @@ app.mount("/static", StaticFiles(directory="static"), name="static")
|
|||||||
```html
|
```html
|
||||||
<!-- Local-only mode (no CDN) -->
|
<!-- Local-only mode (no CDN) -->
|
||||||
<link rel="stylesheet" href="{{ url_for('static', path='shared/css/tailwind.min.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', path='shared/css/tailwind.min.css') }}">
|
||||||
<script defer src="{{ url_for('static', path='shared/js/vendor/alpine.min.js') }}"></script>
|
<script defer src="{{ url_for('static', path='shared/js/lib/alpine.min.js') }}"></script>
|
||||||
```
|
```
|
||||||
|
|
||||||
## Browser Compatibility
|
## Browser Compatibility
|
||||||
|
|||||||
3
static/shared/js/lib/quill.js
Normal file
3
static/shared/js/lib/quill.js
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user