From 33da380c20d64cd4568a2aa17a3c8ee65094c34f Mon Sep 17 00:00:00 2001 From: Syarif Ubaidillah Date: Wed, 19 Aug 2026 16:13:38 +0700 Subject: [PATCH] feat: enhance `SystemSettingKey` and refactor Alpine plugins - Add `CURRENT_TERM` to `SystemSettingKey` with dynamic selection using `SelectTermQuery`. - Refactor Alpine plugins (`chart`, `quill`, `select2`) to improve configurability, compatibility with Livewire, and dynamic updates. - Clean up Blade views for streamlined script injection. --- app/Domains/System/Enums/SystemSettingKey.php | 11 +- resources/js/alpine/chart.js | 223 ++++++++++++-- resources/js/alpine/quill.js | 136 ++++++--- resources/js/alpine/select2.js | 274 ++++++++++-------- resources/js/alpinejs.js | 40 +-- .../invoice/⚡form-modal/form-modal.blade.php | 4 - .../⚡form-modal/form-modal.blade.php | 3 - .../pages/identity/users/index.blade.php | 2 +- .../role-selection-modal.blade.php | 4 - .../charts/⚡role-distribution.blade.php | 87 +++--- .../widgets/charts/⚡user-growth.blade.php | 56 ++-- 11 files changed, 542 insertions(+), 298 deletions(-) diff --git a/app/Domains/System/Enums/SystemSettingKey.php b/app/Domains/System/Enums/SystemSettingKey.php index bc1606e..755f9a9 100644 --- a/app/Domains/System/Enums/SystemSettingKey.php +++ b/app/Domains/System/Enums/SystemSettingKey.php @@ -2,6 +2,7 @@ namespace App\Domains\System\Enums; +use App\Domains\Academic\Queries\Selection\SelectTermQuery; use App\Domains\System\Traits\Enum\HasPredicateMethod; use App\UI\Enums\Concerns\InteractsWithLabels; use App\UI\Enums\Contracts\HasLabel; @@ -25,6 +26,7 @@ enum SystemSettingKey: string implements HasLabel, HasSchema case WEB_ADDRESS = 'web-address'; case DEFAULT_LANGUAGE = 'default_language'; case TIMEZONE = 'timezone'; + case CURRENT_TERM = 'current_term'; case GOOGLE_TAG_MANAGER_ID = 'google-tag_manager_id'; case GOOGLE_WEBMASTER_ID = 'google-webmaster_id'; @@ -84,6 +86,13 @@ enum SystemSettingKey: string implements HasLabel, HasSchema 'id' => __('domains/system/enum.system_setting_key_options.default_language.id'), ]), + self::CURRENT_TERM => InputSchema::make() + ->type(InputType::SELECT) + ->default('en') + ->label($this->label()) + ->select2() + ->options(fn() => SelectTermQuery::fetch()), + self::TIMEZONE => InputSchema::make() ->type(InputType::SELECT) ->default('UTC') @@ -112,7 +121,7 @@ enum SystemSettingKey: string implements HasLabel, HasSchema ], ], [ - __('domains/system/pages.settings.sections.general') => [self::DEFAULT_LANGUAGE, self::TIMEZONE], + __('domains/system/pages.settings.sections.general') => [self::DEFAULT_LANGUAGE, self::TIMEZONE, self::CURRENT_TERM], __('domains/system/pages.settings.sections.webmaster') => [self::GOOGLE_TAG_MANAGER_ID, self::GOOGLE_WEBMASTER_ID], ], ]; diff --git a/resources/js/alpine/chart.js b/resources/js/alpine/chart.js index ac1f110..919a2e6 100644 --- a/resources/js/alpine/chart.js +++ b/resources/js/alpine/chart.js @@ -1,25 +1,212 @@ -import '../plugin/apexchart.js' - export default function AlpineChart(Alpine) { - Alpine.directive('chart', function (el, { expression }, { evaluate, cleanup }) { - let config = evaluate(expression); + Alpine.directive( + "chart", + function (el, { expression }, { evaluateLater, cleanup }) { + const getContent = evaluateLater(expression); + + import("apexcharts").then(({ default: ApexCharts }) => { + getContent((userConfig) => { + if (!userConfig || typeof userConfig !== "object") return; + + // 1. Fetch live computed CSS variables directly from :root (document.documentElement) + const getCoreUIColors = () => { + const style = getComputedStyle(document.body); + + // CoreUI v4/v5 & Bootstrap 5 theme variable names + const bodyColor = + style.getPropertyValue("--cui-body-color").trim() || + style.getPropertyValue("--bs-body-color").trim() || + "#a6b0cf"; + + const secondaryColor = + style + .getPropertyValue("--cui-secondary-color") + .trim() || + style + .getPropertyValue("--bs-secondary-color") + .trim() || + "#8a93a2"; + + const borderColor = + style + .getPropertyValue("--cui-border-color") + .trim() || + style + .getPropertyValue( + "--bs-border-color-translucent", + ) + .trim() || + style + .getPropertyValue("--bs-border-color") + .trim() || + "rgba(255, 255, 255, 0.08)"; + + const primaryColor = + style.getPropertyValue("--cui-primary").trim() || + style.getPropertyValue("--bs-primary").trim() || + "#6366f1"; + + const theme = + document.documentElement.getAttribute( + "data-coreui-theme", + ) || + document.documentElement.getAttribute( + "data-bs-theme", + ) || + "dark"; + + return { + bodyColor, + secondaryColor, + borderColor, + primaryColor, + theme, + }; + }; + + const colors = getCoreUIColors(); + + // 2. Base Configuration explicitly overriding ApexCharts SVG text fills + const baseConfig = { + chart: { + type: "line", + height: 360, + foreColor: colors.secondaryColor, // Sets default text/legend colors + background: "transparent", + }, + colors: [colors.primaryColor], + theme: { + mode: colors.theme, + }, + tooltip: { + theme: colors.theme, + }, + grid: { + borderColor: colors.borderColor, + }, + xaxis: { + labels: { + style: { + colors: colors.secondaryColor, + fontSize: "12px", + }, + }, + axisBorder: { color: colors.borderColor }, + axisTicks: { color: colors.borderColor }, + }, + yaxis: { + labels: { + style: { + colors: colors.secondaryColor, + fontSize: "12px", + }, + }, + }, + }; + + // Deep merge user options with base configuration + const options = Object.assign({}, baseConfig, userConfig); + options.chart = Object.assign( + {}, + baseConfig.chart, + userConfig.chart, + ); + options.theme = Object.assign( + {}, + baseConfig.theme, + userConfig.theme, + ); + options.tooltip = Object.assign( + {}, + baseConfig.tooltip, + userConfig.tooltip, + ); + + // Preserve user primary colors if explicitly provided in PHP, otherwise use CoreUI's + if (!userConfig.colors) { + options.colors = baseConfig.colors; + } + + requestAnimationFrame(() => { + const chart = new ApexCharts(el, options); + chart.render(); + + // 3. Theme switch handler + const observer = new MutationObserver(() => { + const updated = getCoreUIColors(); - const chart = new window.ApexCharts(el, config); - chart.render(); + chart.updateOptions( + { + chart: { + foreColor: updated.secondaryColor, + }, + theme: { mode: updated.theme }, + tooltip: { theme: updated.theme }, + grid: { borderColor: updated.borderColor }, + xaxis: { + labels: { + style: { + colors: updated.secondaryColor, + }, + }, + axisBorder: { + color: updated.borderColor, + }, + axisTicks: { + color: updated.borderColor, + }, + }, + yaxis: { + labels: { + style: { + colors: updated.secondaryColor, + }, + }, + }, + }, + false, + true, + ); + }); - const event = function(event) { - const newData = event.detail - chart.updateSeries(newData.series, true, true); - chart.updateOptions(newData.options, true, true); - } + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: [ + "data-coreui-theme", + "data-bs-theme", + "class", + ], + }); - name = el.getAttribute('id'); + // 4. Dynamic update event listener + const name = el.getAttribute("id"); + const eventHandler = (event) => { + const detail = event.detail || event; + if (detail.series) + chart.updateSeries(detail.series, true); + if (detail.options) + chart.updateOptions(detail.options, true); + }; - window.addEventListener(`chart-${name}-update`, event) + if (name) { + window.addEventListener( + `chart-${name}-update`, + eventHandler, + ); + } - cleanup(() => { - chart.destroy(); - window.removeEventListener(`chart-${name}-update`, event) - }); - }) + cleanup(() => { + observer.disconnect(); + if (name) + window.removeEventListener( + `chart-${name}-update`, + eventHandler, + ); + chart.destroy(); + }); + }); + }); + }); + }, + ); } diff --git a/resources/js/alpine/quill.js b/resources/js/alpine/quill.js index a95c0a3..bbb0987 100644 --- a/resources/js/alpine/quill.js +++ b/resources/js/alpine/quill.js @@ -1,46 +1,94 @@ +import Quill from "quill"; +import "quill/dist/quill.snow.css"; + export default function alpineQuill(Alpine) { - Alpine.directive('quill', function (el, { expression }, { evaluate, cleanup }) { - const options = expression ? evaluate(expression) : {}; - const defaultOptions = { theme: 'snow', ...options }; - - if (typeof window.Quill === 'undefined') { - throw new Error('Quill must be loaded'); - } - - const container = document.createElement('div'); - el.appendChild(container); - const quill = new window.Quill(container, defaultOptions); - - let isUpdating = false; - - // 1. Text changes handler - quill.on('text-change', () => { - if (isUpdating) return; - isUpdating = true; - const htmlContent = quill.root.innerHTML; - el.value = htmlContent === '


' ? '' : htmlContent; - el.dispatchEvent(new Event('input', { bubbles: true })); - isUpdating = false; - }); - - // 2. Element Value Getter/Setter - Object.defineProperty(el, 'value', { - get() { return quill.root.innerHTML; }, - set(newValue) { - if (isUpdating || newValue === quill.root.innerHTML) return; - isUpdating = true; - quill.root.innerHTML = newValue || ''; - isUpdating = false; - }, - configurable: true - }); - - quill.on('selection-change', (range) => { - if (range == null) { - el.dispatchEvent(new Event('blur', { bubbles: true })); - } - }); - - cleanup(() => el.innerHTML = '') - }) + Alpine.directive( + "quill", + (el, { expression }, { evaluateLater, cleanup }) => { + // 1. Fallback gracefully if x-quill has no parameters + const getContent = expression + ? evaluateLater(expression) + : (cb) => cb({}); + + getContent((userOptions) => { + const options = Object.assign( + { + theme: "snow", + placeholder: "Write something...", + }, + typeof userOptions === "object" ? userOptions : {}, + ); + + // 2. Identify or create container DIV (Quill crashes on