Fixed command id collision. Added class support in style preset
This commit is contained in:
@@ -207,3 +207,47 @@
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* *********************************************** */
|
||||
/* ********** Preset Styles *********** */
|
||||
/* *********************************************** */
|
||||
|
||||
.mf-formatting-primary {
|
||||
background-color: var(--color-primary);
|
||||
color: var(--color-primary-content)
|
||||
}
|
||||
|
||||
.mf-formatting-secondary {
|
||||
background-color: var(--color-secondary);
|
||||
color: var(--color-secondary-content);
|
||||
}
|
||||
|
||||
.mf-formatting-accent {
|
||||
background-color: var(--color-accent);
|
||||
color: var(--color-accent-content);
|
||||
}
|
||||
|
||||
.mf-formatting-neutral {
|
||||
background-color: var(--color-neutral);
|
||||
color: var(--color-neutral-content);
|
||||
}
|
||||
|
||||
.mf-formatting-info {
|
||||
background-color: var(--color-info);
|
||||
color: var(--color-info-content);
|
||||
}
|
||||
|
||||
.mf-formatting-success {
|
||||
background-color: var(--color-success);
|
||||
color: var(--color-success-content);
|
||||
}
|
||||
|
||||
.mf-formatting-warning {
|
||||
background-color: var(--color-warning);
|
||||
color: var(--color-warning-content);
|
||||
}
|
||||
|
||||
.mf-formatting-error {
|
||||
background-color: var(--color-error);
|
||||
color: var(--color-error-content);
|
||||
}
|
||||
@@ -375,6 +375,7 @@ function triggerHtmxAction(elementId, config, combinationStr, isInside, event) {
|
||||
}
|
||||
|
||||
// Make AJAX call with htmx
|
||||
console.debug(`Triggering HTMX action for element ${elementId}: ${method} ${url}`, htmxOptions);
|
||||
htmx.ajax(method, url, htmxOptions);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,901 +0,0 @@
|
||||
/*:root {*/
|
||||
/* --color-border-primary: color-mix(in oklab, var(--color-primary) 40%, #0000);*/
|
||||
/* --color-border: color-mix(in oklab, var(--color-base-content) 20%, #0000);*/
|
||||
/* --color-resize: color-mix(in oklab, var(--color-base-content) 50%, #0000);*/
|
||||
/* --color-selection: color-mix(in oklab, var(--color-primary) 20%, #0000);*/
|
||||
|
||||
/* --datagrid-resize-zindex: 1;*/
|
||||
/* --font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';*/
|
||||
/* --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;*/
|
||||
/* --spacing: 0.25rem;*/
|
||||
/* --text-xs: 0.6875rem;*/
|
||||
/* --text-sm: 0.875rem;*/
|
||||
/* --text-sm--line-height: calc(1.25 / 0.875);*/
|
||||
/* --text-xl: 1.25rem;*/
|
||||
/* --text-xl--line-height: calc(1.75 / 1.25);*/
|
||||
/* --font-weight-medium: 500;*/
|
||||
/* --radius-md: 0.375rem;*/
|
||||
/* --default-font-family: var(--font-sans);*/
|
||||
/* --default-mono-font-family: var(--font-mono);*/
|
||||
/* --properties-font-size: var(--text-xs);*/
|
||||
/* --mf-tooltip-zindex: 10;*/
|
||||
/*}*/
|
||||
|
||||
|
||||
/*.mf-icon-16 {*/
|
||||
/* width: 16px;*/
|
||||
/* min-width: 16px;*/
|
||||
/* height: 16px;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-icon-20 {*/
|
||||
/* width: 20px;*/
|
||||
/* min-width: 20px;*/
|
||||
/* height: 20px;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-icon-24 {*/
|
||||
/* width: 24px;*/
|
||||
/* min-width: 24px;*/
|
||||
/* height: 24px;*/
|
||||
|
||||
/*}*/
|
||||
|
||||
/*.mf-icon-28 {*/
|
||||
/* width: 28px;*/
|
||||
/* min-width: 28px;*/
|
||||
/* height: 28px;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-icon-32 {*/
|
||||
/* width: 32px;*/
|
||||
/* min-width: 32px;*/
|
||||
/* height: 32px;*/
|
||||
/*}*/
|
||||
|
||||
/*!**/
|
||||
/* * MF Layout Component - CSS Grid Layout*/
|
||||
/* * Provides fixed header/footer, collapsible drawers, and scrollable main content*/
|
||||
/* * Compatible with DaisyUI 5*/
|
||||
/* *!*/
|
||||
|
||||
/*.mf-button {*/
|
||||
/* border-radius: 0.375rem;*/
|
||||
/* transition: background-color 0.15s ease;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-button:hover {*/
|
||||
/* background-color: var(--color-base-300);*/
|
||||
/*}*/
|
||||
|
||||
|
||||
/*.mf-tooltip-container {*/
|
||||
/* background: var(--color-base-200);*/
|
||||
/* padding: 5px 10px;*/
|
||||
/* border-radius: 4px;*/
|
||||
/* pointer-events: none; !* Prevent interfering with mouse events *!*/
|
||||
/* font-size: 12px;*/
|
||||
/* white-space: nowrap;*/
|
||||
/* opacity: 0; !* Default to invisible *!*/
|
||||
/* visibility: hidden; !* Prevent interaction when invisible *!*/
|
||||
/* transition: opacity 0.3s ease, visibility 0s linear 0.3s; !* Delay visibility removal *!*/
|
||||
/* position: fixed; !* Keep it above other content and adjust position *!*/
|
||||
/* z-index: var(--mf-tooltip-zindex); !* Ensure it's on top *!*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-tooltip-container[data-visible="true"] {*/
|
||||
/* opacity: 1;*/
|
||||
/* visibility: visible; !* Show tooltip *!*/
|
||||
/* transition: opacity 0.3s ease; !* No delay when becoming visible *!*/
|
||||
/*}*/
|
||||
|
||||
/*!* Main layout container using CSS Grid *!*/
|
||||
/*.mf-layout {*/
|
||||
/* display: grid;*/
|
||||
/* grid-template-areas:*/
|
||||
/* "header header header"*/
|
||||
/* "left-drawer main right-drawer"*/
|
||||
/* "footer footer footer";*/
|
||||
/* grid-template-rows: 32px 1fr 32px;*/
|
||||
/* grid-template-columns: auto 1fr auto;*/
|
||||
/* height: 100vh;*/
|
||||
/* width: 100vw;*/
|
||||
/* overflow: hidden;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Header - fixed at top *!*/
|
||||
/*.mf-layout-header {*/
|
||||
/* grid-area: header;*/
|
||||
/* display: flex;*/
|
||||
/* align-items: center;*/
|
||||
/* justify-content: space-between; !* put one item on each side *!*/
|
||||
/* gap: 1rem;*/
|
||||
/* padding: 0 1rem;*/
|
||||
/* background-color: var(--color-base-300);*/
|
||||
/* border-bottom: 1px solid color-mix(in oklab, var(--color-base-content) 10%, #0000);*/
|
||||
/* z-index: 30;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Footer - fixed at bottom *!*/
|
||||
/*.mf-layout-footer {*/
|
||||
/* grid-area: footer;*/
|
||||
/* display: flex;*/
|
||||
/* align-items: center;*/
|
||||
/* padding: 0 1rem;*/
|
||||
/* background-color: var(--color-neutral);*/
|
||||
/* color: var(--color-neutral-content);*/
|
||||
/* border-top: 1px solid color-mix(in oklab, var(--color-base-content) 10%, #0000);*/
|
||||
/* z-index: 30;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Main content area - scrollable *!*/
|
||||
/*.mf-layout-main {*/
|
||||
/* grid-area: main;*/
|
||||
/* overflow-y: auto;*/
|
||||
/* overflow-x: auto;*/
|
||||
/* padding: 0.5rem;*/
|
||||
/* background-color: var(--color-base-100);*/
|
||||
/*}*/
|
||||
|
||||
/*!* Drawer base styles *!*/
|
||||
/*.mf-layout-drawer {*/
|
||||
/* overflow-y: auto;*/
|
||||
/* overflow-x: hidden;*/
|
||||
/* background-color: var(--color-base-100);*/
|
||||
/* transition: width 0.3s ease-in-out, margin 0.3s ease-in-out;*/
|
||||
/* width: 250px;*/
|
||||
/* padding: 1rem;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Left drawer *!*/
|
||||
/*.mf-layout-left-drawer {*/
|
||||
/* grid-area: left-drawer;*/
|
||||
/* border-right: 1px solid var(--color-border-primary);*/
|
||||
/*}*/
|
||||
|
||||
/*!* Right drawer *!*/
|
||||
/*.mf-layout-right-drawer {*/
|
||||
/* grid-area: right-drawer;*/
|
||||
/* !*border-left: 1px solid color-mix(in oklab, var(--color-base-content) 10%, #0000);*!*/
|
||||
/* border-left: 1px solid var(--color-border-primary);*/
|
||||
/*}*/
|
||||
|
||||
/*!* Collapsed drawer states *!*/
|
||||
/*.mf-layout-drawer.collapsed {*/
|
||||
/* width: 0;*/
|
||||
/* padding: 0;*/
|
||||
/* border: none;*/
|
||||
/* overflow: hidden;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Toggle buttons positioning *!*/
|
||||
/*.mf-layout-toggle-left {*/
|
||||
/* margin-right: auto;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-layout-toggle-right {*/
|
||||
/* margin-left: auto;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Smooth scrollbar styling for webkit browsers *!*/
|
||||
/*.mf-layout-main::-webkit-scrollbar,*/
|
||||
/*.mf-layout-drawer::-webkit-scrollbar {*/
|
||||
/* width: 8px;*/
|
||||
/* height: 8px;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-layout-main::-webkit-scrollbar-track,*/
|
||||
/*.mf-layout-drawer::-webkit-scrollbar-track {*/
|
||||
/* background: var(--color-base-200);*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-layout-main::-webkit-scrollbar-thumb,*/
|
||||
/*.mf-layout-drawer::-webkit-scrollbar-thumb {*/
|
||||
/* background: color-mix(in oklab, var(--color-base-content) 20%, #0000);*/
|
||||
/* border-radius: 4px;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-layout-main::-webkit-scrollbar-thumb:hover,*/
|
||||
/*.mf-layout-drawer::-webkit-scrollbar-thumb:hover {*/
|
||||
/* background: color-mix(in oklab, var(--color-base-content) 30%, #0000);*/
|
||||
/*}*/
|
||||
|
||||
/*!* Responsive adjustments for smaller screens *!*/
|
||||
/*@media (max-width: 768px) {*/
|
||||
/* .mf-layout-drawer {*/
|
||||
/* width: 200px;*/
|
||||
/* }*/
|
||||
|
||||
/* .mf-layout-header,*/
|
||||
/* .mf-layout-footer {*/
|
||||
/* padding: 0 0.5rem;*/
|
||||
/* }*/
|
||||
|
||||
/* .mf-layout-main {*/
|
||||
/* padding: 0.5rem;*/
|
||||
/* }*/
|
||||
/*}*/
|
||||
|
||||
/*!* Handle layouts with no drawers *!*/
|
||||
/*.mf-layout[data-left-drawer="false"] {*/
|
||||
/* grid-template-areas:*/
|
||||
/* "header header"*/
|
||||
/* "main right-drawer"*/
|
||||
/* "footer footer";*/
|
||||
/* grid-template-columns: 1fr auto;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-layout[data-right-drawer="false"] {*/
|
||||
/* grid-template-areas:*/
|
||||
/* "header header"*/
|
||||
/* "left-drawer main"*/
|
||||
/* "footer footer";*/
|
||||
/* grid-template-columns: auto 1fr;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-layout[data-left-drawer="false"][data-right-drawer="false"] {*/
|
||||
/* grid-template-areas:*/
|
||||
/* "header"*/
|
||||
/* "main"*/
|
||||
/* "footer";*/
|
||||
/* grid-template-columns: 1fr;*/
|
||||
/*}*/
|
||||
|
||||
|
||||
/*!***/
|
||||
/* * Layout Drawer Resizer Styles*/
|
||||
/* **/
|
||||
/* * Styles for the resizable drawer borders with visual feedback*/
|
||||
/* *!*/
|
||||
|
||||
/*!* Ensure drawer has relative positioning and no overflow *!*/
|
||||
/*.mf-layout-drawer {*/
|
||||
/* position: relative;*/
|
||||
/* overflow: hidden;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Content wrapper handles scrolling *!*/
|
||||
/*.mf-layout-drawer-content {*/
|
||||
/* position: absolute;*/
|
||||
/* top: 0;*/
|
||||
/* left: 0;*/
|
||||
/* right: 0;*/
|
||||
/* bottom: 0;*/
|
||||
/* overflow-y: auto;*/
|
||||
/* overflow-x: hidden;*/
|
||||
/* padding: 1rem;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Base resizer styles *!*/
|
||||
/*.mf-layout-resizer {*/
|
||||
/* position: absolute;*/
|
||||
/* top: 0;*/
|
||||
/* bottom: 0;*/
|
||||
/* width: 4px;*/
|
||||
/* background-color: transparent;*/
|
||||
/* transition: background-color 0.2s ease;*/
|
||||
/* z-index: 100;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Resizer on the right side (for left drawer) *!*/
|
||||
/*.mf-layout-resizer-right {*/
|
||||
/* right: 0;*/
|
||||
/* cursor: col-resize;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Resizer on the left side (for right drawer) *!*/
|
||||
/*.mf-layout-resizer-left {*/
|
||||
/* left: 0;*/
|
||||
/* cursor: col-resize;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Hover state *!*/
|
||||
/*.mf-layout-resizer:hover {*/
|
||||
/* background-color: rgba(59, 130, 246, 0.3); !* Blue-500 with opacity *!*/
|
||||
/*}*/
|
||||
|
||||
/*!* Active state during resize *!*/
|
||||
/*.mf-layout-drawer-resizing .mf-layout-resizer {*/
|
||||
/* background-color: rgba(59, 130, 246, 0.5);*/
|
||||
/*}*/
|
||||
|
||||
/*!* Disable transitions during resize for smooth dragging *!*/
|
||||
/*.mf-layout-drawer-resizing {*/
|
||||
/* transition: none !important;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Prevent text selection during resize *!*/
|
||||
/*.mf-layout-resizing {*/
|
||||
/* user-select: none;*/
|
||||
/* -webkit-user-select: none;*/
|
||||
/* -moz-user-select: none;*/
|
||||
/* -ms-user-select: none;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Cursor override for entire body during resize *!*/
|
||||
/*.mf-layout-resizing * {*/
|
||||
/* cursor: col-resize !important;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Visual indicator for resizer on hover - subtle border *!*/
|
||||
/*.mf-layout-resizer::before {*/
|
||||
/* content: '';*/
|
||||
/* position: absolute;*/
|
||||
/* top: 50%;*/
|
||||
/* transform: translateY(-50%);*/
|
||||
/* width: 2px;*/
|
||||
/* height: 40px;*/
|
||||
/* background-color: rgba(156, 163, 175, 0.4); !* Gray-400 with opacity *!*/
|
||||
/* border-radius: 2px;*/
|
||||
/* opacity: 0;*/
|
||||
/* transition: opacity 0.2s ease;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-layout-resizer-right::before {*/
|
||||
/* right: 1px;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-layout-resizer-left::before {*/
|
||||
/* left: 1px;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-layout-resizer:hover::before,*/
|
||||
/*.mf-layout-drawer-resizing .mf-layout-resizer::before {*/
|
||||
/* opacity: 1;*/
|
||||
/*}*/
|
||||
|
||||
|
||||
/*.mf-layout-group {*/
|
||||
/* font-weight: bold;*/
|
||||
/* !*font-size: var(--text-sm);*!*/
|
||||
/* overflow: hidden;*/
|
||||
/* text-overflow: ellipsis;*/
|
||||
/* white-space: nowrap;*/
|
||||
/* margin-bottom: 0.2rem;*/
|
||||
/*}*/
|
||||
|
||||
/*!* *********************************************** *!*/
|
||||
/*!* *********** Tabs Manager Component ************ *!*/
|
||||
/*!* *********************************************** *!*/
|
||||
|
||||
/*!* Tabs Manager Container *!*/
|
||||
/*.mf-tabs-manager {*/
|
||||
/* display: flex;*/
|
||||
/* flex-direction: column;*/
|
||||
/* height: 100%;*/
|
||||
/* width: 100%;*/
|
||||
/* background-color: var(--color-base-200);*/
|
||||
/* color: color-mix(in oklab, var(--color-base-content) 50%, transparent);*/
|
||||
/* border-radius: .5rem;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Tabs Header using DaisyUI tabs component *!*/
|
||||
/*.mf-tabs-header {*/
|
||||
/* display: flex;*/
|
||||
/* gap: 0;*/
|
||||
/* flex-shrink: 1;*/
|
||||
/* min-height: 25px;*/
|
||||
|
||||
/* overflow-x: hidden;*/
|
||||
/* overflow-y: hidden;*/
|
||||
/* white-space: nowrap;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-tabs-header-wrapper {*/
|
||||
/* display: flex;*/
|
||||
/* justify-content: space-between;*/
|
||||
/* align-items: center;*/
|
||||
|
||||
/* width: 100%;*/
|
||||
/* !*overflow: hidden; important *!*/
|
||||
/*}*/
|
||||
|
||||
/*!* Individual Tab Button using DaisyUI tab classes *!*/
|
||||
/*.mf-tab-button {*/
|
||||
/* display: flex;*/
|
||||
/* align-items: center;*/
|
||||
/* gap: 0.5rem;*/
|
||||
/* padding: 0 0.5rem 0 1rem;*/
|
||||
/* cursor: pointer;*/
|
||||
/* transition: all 0.2s ease;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-tab-button:hover {*/
|
||||
/* color: var(--color-base-content); !* Change text color on hover *!*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-tab-button.mf-tab-active {*/
|
||||
/* --depth: 1;*/
|
||||
/* background-color: var(--color-base-100);*/
|
||||
/* color: var(--color-base-content);*/
|
||||
/* border-radius: .25rem;*/
|
||||
/* border-bottom: 4px solid var(--color-primary);*/
|
||||
/* box-shadow: 0 1px oklch(100% 0 0/calc(var(--depth) * .1)) inset, 0 1px 1px -1px color-mix(in oklab, var(--color-neutral) calc(var(--depth) * 50%), #0000), 0 1px 6px -4px color-mix(in oklab, var(--color-neutral) calc(var(--depth) * 100%), #0000);*/
|
||||
/*}*/
|
||||
|
||||
/*!* Tab Label *!*/
|
||||
/*.mf-tab-label {*/
|
||||
/* user-select: none;*/
|
||||
/* white-space: nowrap;*/
|
||||
/* max-width: 150px;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Tab Close Button *!*/
|
||||
/*.mf-tab-close-btn {*/
|
||||
/* display: flex;*/
|
||||
/* align-items: center;*/
|
||||
/* justify-content: center;*/
|
||||
/* width: 1rem;*/
|
||||
/* height: 1rem;*/
|
||||
/* border-radius: 0.25rem;*/
|
||||
/* font-size: 1.25rem;*/
|
||||
/* line-height: 1;*/
|
||||
/* @apply text-base-content/50;*/
|
||||
/* transition: all 0.2s ease;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-tab-close-btn:hover {*/
|
||||
/* @apply bg-base-300 text-error;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Tab Content Area *!*/
|
||||
/*.mf-tab-content {*/
|
||||
/* flex: 1;*/
|
||||
/* overflow: auto;*/
|
||||
/* height: 100%;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-tab-content-wrapper {*/
|
||||
/* flex: 1;*/
|
||||
/* overflow: auto;*/
|
||||
/* background-color: var(--color-base-100);*/
|
||||
/* padding: 0.5rem;*/
|
||||
/* border-top: 1px solid var(--color-border-primary);*/
|
||||
/*}*/
|
||||
|
||||
/*!* Empty Content State *!*/
|
||||
/*.mf-empty-content {*/
|
||||
/* align-items: center;*/
|
||||
/* justify-content: center;*/
|
||||
/* height: 100%;*/
|
||||
/* @apply text-base-content/50;*/
|
||||
/* font-style: italic;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-vis {*/
|
||||
/* width: 100%;*/
|
||||
/* height: 100%;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-search-results {*/
|
||||
/* margin-top: 0.5rem;*/
|
||||
/* !*max-height: 400px;*!*/
|
||||
/* overflow: auto;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-dropdown-wrapper {*/
|
||||
/* position: relative; !* CRUCIAL for the anchor *!*/
|
||||
/*}*/
|
||||
|
||||
|
||||
/*.mf-dropdown {*/
|
||||
/* display: none;*/
|
||||
/* position: absolute;*/
|
||||
/* top: 100%;*/
|
||||
/* left: 0;*/
|
||||
/* z-index: 50;*/
|
||||
/* min-width: 200px;*/
|
||||
/* padding: 0.5rem;*/
|
||||
/* box-sizing: border-box;*/
|
||||
/* overflow-x: auto;*/
|
||||
|
||||
/* !* DaisyUI styling *!*/
|
||||
/* background-color: var(--color-base-100);*/
|
||||
/* border: 1px solid var(--color-border);*/
|
||||
/* border-radius: var(--radius-md);*/
|
||||
/* box-shadow: 0 4px 6px -1px color-mix(in oklab, var(--color-neutral) 20%, #0000),*/
|
||||
/* 0 2px 4px -2px color-mix(in oklab, var(--color-neutral) 20%, #0000);*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-dropdown.is-visible {*/
|
||||
/* display: block;*/
|
||||
/* opacity: 1;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Dropdown vertical positioning *!*/
|
||||
/*.mf-dropdown-below {*/
|
||||
/* top: 100%;*/
|
||||
/* bottom: auto;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-dropdown-above {*/
|
||||
/* bottom: 100%;*/
|
||||
/* top: auto;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Dropdown horizontal alignment *!*/
|
||||
/*.mf-dropdown-left {*/
|
||||
/* left: 0;*/
|
||||
/* right: auto;*/
|
||||
/* transform: none;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-dropdown-right {*/
|
||||
/* right: 0;*/
|
||||
/* left: auto;*/
|
||||
/* transform: none;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-dropdown-center {*/
|
||||
/* left: 50%;*/
|
||||
/* right: auto;*/
|
||||
/* transform: translateX(-50%);*/
|
||||
/*}*/
|
||||
|
||||
/*!* *********************************************** *!*/
|
||||
/*!* ************** TreeView Component ************* *!*/
|
||||
/*!* *********************************************** *!*/
|
||||
|
||||
/*!* TreeView Container *!*/
|
||||
/*.mf-treeview {*/
|
||||
/* width: 100%;*/
|
||||
/* user-select: none;*/
|
||||
/*}*/
|
||||
|
||||
/*!* TreeNode Container *!*/
|
||||
/*.mf-treenode-container {*/
|
||||
/* width: 100%;*/
|
||||
/*}*/
|
||||
|
||||
/*!* TreeNode Element *!*/
|
||||
/*.mf-treenode {*/
|
||||
/* display: flex;*/
|
||||
/* align-items: center;*/
|
||||
/* gap: 0.25rem;*/
|
||||
/* padding: 2px 0.5rem;*/
|
||||
/* cursor: pointer;*/
|
||||
/* transition: background-color 0.15s ease;*/
|
||||
/* border-radius: 0.25rem;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Input for Editing *!*/
|
||||
/*.mf-treenode-input {*/
|
||||
/* flex: 1;*/
|
||||
/* padding: 2px 0.25rem;*/
|
||||
/* border: 1px solid var(--color-primary);*/
|
||||
/* border-radius: 0.25rem;*/
|
||||
/* background-color: var(--color-base-100);*/
|
||||
/* outline: none;*/
|
||||
/*}*/
|
||||
|
||||
|
||||
/*.mf-treenode:hover {*/
|
||||
/* background-color: var(--color-base-200);*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-treenode.selected {*/
|
||||
/* background-color: var(--color-primary);*/
|
||||
/* color: var(--color-primary-content);*/
|
||||
/*}*/
|
||||
|
||||
/*!* Toggle Icon *!*/
|
||||
/*.mf-treenode-toggle {*/
|
||||
/* flex-shrink: 0;*/
|
||||
/* width: 20px;*/
|
||||
/* text-align: center;*/
|
||||
/* font-size: 0.75rem;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Node Label *!*/
|
||||
/*.mf-treenode-label {*/
|
||||
/* flex: 1;*/
|
||||
/* overflow: hidden;*/
|
||||
/* text-overflow: ellipsis;*/
|
||||
/* white-space: nowrap;*/
|
||||
/*}*/
|
||||
|
||||
|
||||
/*.mf-treenode-input:focus {*/
|
||||
/* box-shadow: 0 0 0 2px color-mix(in oklab, var(--color-primary) 25%, transparent);*/
|
||||
/*}*/
|
||||
|
||||
/*!* Action Buttons - Hidden by default, shown on hover *!*/
|
||||
/*.mf-treenode-actions {*/
|
||||
/* display: none;*/
|
||||
/* gap: 0.1rem;*/
|
||||
/* white-space: nowrap;*/
|
||||
/* margin-left: 0.5rem;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-treenode:hover .mf-treenode-actions {*/
|
||||
/* display: flex;*/
|
||||
/*}*/
|
||||
|
||||
/*!* *********************************************** *!*/
|
||||
/*!* ********** Generic Resizer Classes ************ *!*/
|
||||
/*!* *********************************************** *!*/
|
||||
|
||||
/*!* Generic resizer - used by both Layout and Panel *!*/
|
||||
/*.mf-resizer {*/
|
||||
/* position: absolute;*/
|
||||
/* width: 4px;*/
|
||||
/* cursor: col-resize;*/
|
||||
/* background-color: transparent;*/
|
||||
/* transition: background-color 0.2s ease;*/
|
||||
/* z-index: 100;*/
|
||||
/* top: 0;*/
|
||||
/* bottom: 0;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-resizer:hover {*/
|
||||
/* background-color: rgba(59, 130, 246, 0.3);*/
|
||||
/*}*/
|
||||
|
||||
/*!* Active state during resize *!*/
|
||||
/*.mf-resizing .mf-resizer {*/
|
||||
/* background-color: rgba(59, 130, 246, 0.5);*/
|
||||
/*}*/
|
||||
|
||||
/*!* Prevent text selection during resize *!*/
|
||||
/*.mf-resizing {*/
|
||||
/* user-select: none;*/
|
||||
/* -webkit-user-select: none;*/
|
||||
/* -moz-user-select: none;*/
|
||||
/* -ms-user-select: none;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Cursor override for entire body during resize *!*/
|
||||
/*.mf-resizing * {*/
|
||||
/* cursor: col-resize !important;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Visual indicator for resizer on hover - subtle border *!*/
|
||||
/*.mf-resizer::before {*/
|
||||
/* content: '';*/
|
||||
/* position: absolute;*/
|
||||
/* top: 50%;*/
|
||||
/* transform: translateY(-50%);*/
|
||||
/* width: 2px;*/
|
||||
/* height: 40px;*/
|
||||
/* background-color: rgba(156, 163, 175, 0.4);*/
|
||||
/* border-radius: 2px;*/
|
||||
/* opacity: 0;*/
|
||||
/* transition: opacity 0.2s ease;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-resizer:hover::before,*/
|
||||
/*.mf-resizing .mf-resizer::before {*/
|
||||
/* opacity: 1;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Resizer positioning *!*/
|
||||
/*!* Left resizer is on the right side of the left panel *!*/
|
||||
/*.mf-resizer-left {*/
|
||||
/* right: 0;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Right resizer is on the left side of the right panel *!*/
|
||||
/*.mf-resizer-right {*/
|
||||
/* left: 0;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Position indicator for resizer *!*/
|
||||
/*.mf-resizer-left::before {*/
|
||||
/* right: 1px;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-resizer-right::before {*/
|
||||
/* left: 1px;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Disable transitions during resize for smooth dragging *!*/
|
||||
/*.mf-item-resizing {*/
|
||||
/* transition: none !important;*/
|
||||
/*}*/
|
||||
|
||||
/*!* *********************************************** *!*/
|
||||
/*!* *************** Panel Component *************** *!*/
|
||||
/*!* *********************************************** *!*/
|
||||
|
||||
/*.mf-panel {*/
|
||||
/* display: flex;*/
|
||||
/* width: 100%;*/
|
||||
/* height: 100%;*/
|
||||
/* overflow: hidden;*/
|
||||
/* position: relative;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Common properties for side panels *!*/
|
||||
/*.mf-panel-left,*/
|
||||
/*.mf-panel-right {*/
|
||||
/* position: relative;*/
|
||||
/* flex-shrink: 0;*/
|
||||
/* width: 250px;*/
|
||||
/* min-width: 150px;*/
|
||||
/* max-width: 500px;*/
|
||||
/* height: 100%;*/
|
||||
/* overflow: auto;*/
|
||||
/* transition: width 0.3s ease, min-width 0.3s ease, max-width 0.3s ease;*/
|
||||
/* padding-top: 25px;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Left panel specific *!*/
|
||||
/*.mf-panel-left {*/
|
||||
/* border-right: 1px solid var(--color-border-primary);*/
|
||||
/*}*/
|
||||
|
||||
/*!* Right panel specific *!*/
|
||||
/*.mf-panel-right {*/
|
||||
/* border-left: 1px solid var(--color-border-primary);*/
|
||||
/* padding-left: 0.5rem;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-panel-main {*/
|
||||
/* flex: 1;*/
|
||||
/* height: 100%;*/
|
||||
/* overflow: hidden;*/
|
||||
/* min-width: 0; !* Important to allow the shrinking of flexbox *!*/
|
||||
/*}*/
|
||||
|
||||
/*!* Hidden state - common for both panels *!*/
|
||||
/*.mf-panel-left.mf-hidden,*/
|
||||
/*.mf-panel-right.mf-hidden {*/
|
||||
/* width: 0;*/
|
||||
/* min-width: 0;*/
|
||||
/* max-width: 0;*/
|
||||
/* overflow: hidden;*/
|
||||
/* border: none;*/
|
||||
/* padding: 0;*/
|
||||
/*}*/
|
||||
|
||||
/*!* No transition during manual resize - common for both panels *!*/
|
||||
/*.mf-panel-left.no-transition,*/
|
||||
/*.mf-panel-right.no-transition {*/
|
||||
/* transition: none;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Common properties for panel toggle icons *!*/
|
||||
/*.mf-panel-hide-icon,*/
|
||||
/*.mf-panel-show-icon {*/
|
||||
/* position: absolute;*/
|
||||
/* top: 0;*/
|
||||
/* right: 0;*/
|
||||
/* cursor: pointer;*/
|
||||
/* z-index: 10;*/
|
||||
/* border-radius: 0.25rem;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-panel-hide-icon:hover,*/
|
||||
/*.mf-panel-show-icon:hover {*/
|
||||
/* background-color: var(--color-bg-hover, rgba(0, 0, 0, 0.05));*/
|
||||
/*}*/
|
||||
|
||||
/*!* Show icon positioning *!*/
|
||||
/*.mf-panel-show-icon-left {*/
|
||||
/* left: 0.5rem;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-panel-show-icon-right {*/
|
||||
/* right: 0.5rem;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Panel with title - grid layout for header + scrollable content *!*/
|
||||
/*.mf-panel-body {*/
|
||||
/* display: grid;*/
|
||||
/* grid-template-rows: auto 1fr;*/
|
||||
/* height: 100%;*/
|
||||
/* overflow: hidden;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-panel-header {*/
|
||||
/* display: flex;*/
|
||||
/* justify-content: space-between;*/
|
||||
/* align-items: center;*/
|
||||
/* padding: 0.25rem 0.5rem;*/
|
||||
/* background-color: var(--color-base-200);*/
|
||||
/* border-bottom: 1px solid var(--color-border);*/
|
||||
/*}*/
|
||||
|
||||
/*!* Override absolute positioning for hide icon when inside header *!*/
|
||||
/*.mf-panel-header .mf-panel-hide-icon {*/
|
||||
/* position: static;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-panel-content {*/
|
||||
/* overflow-y: auto;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Remove padding-top when using title layout *!*/
|
||||
/*.mf-panel-left.mf-panel-with-title,*/
|
||||
/*.mf-panel-right.mf-panel-with-title {*/
|
||||
/* padding-top: 0;*/
|
||||
/*}*/
|
||||
|
||||
/*!* *********************************************** *!*/
|
||||
/*!* ************* Properties Component ************ *!*/
|
||||
/*!* *********************************************** *!*/
|
||||
|
||||
/*!*!* Properties container *!*!*/
|
||||
/*.mf-properties {*/
|
||||
/* display: flex;*/
|
||||
/* flex-direction: column;*/
|
||||
/* gap: 1rem;*/
|
||||
/*}*/
|
||||
|
||||
/*!*!* Group card - using DaisyUI card styling *!*!*/
|
||||
/*.mf-properties-group-card {*/
|
||||
/* background-color: var(--color-base-100);*/
|
||||
/* border: 1px solid color-mix(in oklab, var(--color-base-content) 10%, transparent);*/
|
||||
/* border-radius: var(--radius-md);*/
|
||||
/* box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);*/
|
||||
/* overflow: auto;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-properties-group-container {*/
|
||||
/* display: inline-block; !* important *!*/
|
||||
/* min-width: max-content; !* important *!*/
|
||||
/* width: 100%;*/
|
||||
/*}*/
|
||||
|
||||
|
||||
/*!*!* Group header - gradient using DaisyUI primary color *!*!*/
|
||||
/*.mf-properties-group-header {*/
|
||||
/* background: linear-gradient(135deg, var(--color-primary) 0%, color-mix(in oklab, var(--color-primary) 80%, black) 100%);*/
|
||||
/* color: var(--color-primary-content);*/
|
||||
/* padding: calc(var(--properties-font-size) * 0.5) calc(var(--properties-font-size) * 0.75);*/
|
||||
/* font-weight: 700;*/
|
||||
/* font-size: var(--properties-font-size);*/
|
||||
/*}*/
|
||||
|
||||
/*!*!* Group content area *!*!*/
|
||||
/*.mf-properties-group-content {*/
|
||||
/* display: flex;*/
|
||||
/* flex-direction: column;*/
|
||||
/* min-width: max-content;*/
|
||||
/*}*/
|
||||
|
||||
/*!*!* Property row *!*!*/
|
||||
/*.mf-properties-row {*/
|
||||
/* display: grid;*/
|
||||
/* grid-template-columns: 6rem 1fr;*/
|
||||
|
||||
/* align-items: center;*/
|
||||
/* padding: calc(var(--properties-font-size) * 0.4) calc(var(--properties-font-size) * 0.75);*/
|
||||
|
||||
/* border-bottom: 1px solid color-mix(in oklab, var(--color-base-content) 5%, transparent);*/
|
||||
/* transition: background-color 0.15s ease;*/
|
||||
|
||||
/* gap: calc(var(--properties-font-size) * 0.75);*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-properties-row:last-child {*/
|
||||
/* border-bottom: none;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-properties-row:hover {*/
|
||||
/* background-color: color-mix(in oklab, var(--color-base-content) 3%, transparent);*/
|
||||
/*}*/
|
||||
|
||||
/*!*!* Property key - normal font *!*!*/
|
||||
/*.mf-properties-key {*/
|
||||
/* align-items: start;*/
|
||||
/* font-weight: 600;*/
|
||||
/* color: color-mix(in oklab, var(--color-base-content) 70%, transparent);*/
|
||||
/* flex: 0 0 40%;*/
|
||||
/* font-size: var(--properties-font-size);*/
|
||||
/* white-space: nowrap;*/
|
||||
/* overflow: hidden;*/
|
||||
/* text-overflow: ellipsis;*/
|
||||
/*}*/
|
||||
|
||||
/*!*!* Property value - monospace font *!*!*/
|
||||
/*.mf-properties-value {*/
|
||||
/* font-family: var(--default-mono-font-family);*/
|
||||
/* color: var(--color-base-content);*/
|
||||
/* flex: 1;*/
|
||||
/* text-align: left;*/
|
||||
/* font-size: var(--properties-font-size);*/
|
||||
/* overflow: hidden;*/
|
||||
/* text-overflow: ellipsis;*/
|
||||
/* white-space: nowrap;*/
|
||||
/*}*/
|
||||
File diff suppressed because it is too large
Load Diff
@@ -31,11 +31,11 @@ from myfasthtml.core.formatting.dsl.parser import DSLParser
|
||||
from myfasthtml.core.formatting.engine import FormattingEngine
|
||||
from myfasthtml.core.instances import MultipleInstance
|
||||
from myfasthtml.core.optimized_ft import OptimizedDiv
|
||||
from myfasthtml.core.utils import make_safe_id
|
||||
from myfasthtml.core.utils import make_safe_id, merge_classes
|
||||
from myfasthtml.icons.carbon import row, column, grid
|
||||
from myfasthtml.icons.fluent import checkbox_unchecked16_regular
|
||||
from myfasthtml.icons.fluent_p1 import settings16_regular
|
||||
from myfasthtml.icons.fluent_p2 import checkbox_checked16_regular
|
||||
from myfasthtml.icons.fluent_p2 import checkbox_checked16_regular, column_edit20_regular
|
||||
from myfasthtml.icons.fluent_p3 import text_edit_style20_regular
|
||||
|
||||
# OPTIMIZATION: Pre-compiled regex to detect HTML special characters
|
||||
_HTML_SPECIAL_CHARS_REGEX = re.compile(r'[<>&"\']')
|
||||
@@ -592,12 +592,12 @@ class DataGrid(MultipleInstance):
|
||||
return NotStr(f'<span class="dt2-cell-content-number truncate">{row_index}</span>')
|
||||
|
||||
# Get format rules and apply formatting
|
||||
css_string = None
|
||||
style = None
|
||||
formatted_value = None
|
||||
rules = self._get_format_rules(col_pos, row_index, col_def)
|
||||
if rules:
|
||||
row_data = self._state.ns_row_data[row_index] if row_index < len(self._state.ns_row_data) else None
|
||||
css_string, formatted_value = self._formatting_engine.apply_format(rules, value, row_data)
|
||||
style, formatted_value = self._formatting_engine.apply_format(rules, value, row_data)
|
||||
|
||||
# Use formatted value or convert to string
|
||||
value_str = formatted_value if formatted_value is not None else str(value)
|
||||
@@ -606,11 +606,18 @@ class DataGrid(MultipleInstance):
|
||||
if _HTML_SPECIAL_CHARS_REGEX.search(value_str):
|
||||
value_str = html.escape(value_str)
|
||||
|
||||
if style:
|
||||
cls = style.cls
|
||||
css_string = style.css
|
||||
else:
|
||||
cls = None
|
||||
css_string = ""
|
||||
|
||||
# Number or Text type
|
||||
if column_type == ColumnType.Number:
|
||||
return mk_highlighted_text(value_str, "dt2-cell-content-number", css_string)
|
||||
return mk_highlighted_text(value_str, merge_classes("dt2-cell-content-number", cls), css_string)
|
||||
else:
|
||||
return mk_highlighted_text(value_str, "dt2-cell-content-text", css_string)
|
||||
return mk_highlighted_text(value_str, merge_classes("dt2-cell-content-text", cls), css_string)
|
||||
|
||||
def mk_body_cell(self, col_pos, row_index, col_def: DataGridColumnState, filter_keyword_lower=None):
|
||||
"""
|
||||
@@ -816,10 +823,10 @@ class DataGrid(MultipleInstance):
|
||||
Div(self._datagrid_filter,
|
||||
Div(
|
||||
self._selection_mode_selector,
|
||||
mk.icon(settings16_regular,
|
||||
mk.icon(column_edit20_regular,
|
||||
command=self.commands.toggle_columns_manager(),
|
||||
tooltip="Show column manager"),
|
||||
mk.icon(settings16_regular,
|
||||
mk.icon(text_edit_style20_regular,
|
||||
command=self.commands.toggle_formatting_editor(),
|
||||
tooltip="Show formatting editor"),
|
||||
cls="flex"),
|
||||
|
||||
@@ -47,15 +47,16 @@ class Command:
|
||||
# In this situation,
|
||||
# either there is no parameter (so one single instance of the command is enough)
|
||||
# or the parameter is a kwargs (so the parameters are provided when the command is called)
|
||||
if (key is None
|
||||
and owner is not None
|
||||
and args is None # args is not provided
|
||||
):
|
||||
key = f"{owner.get_full_id()}-{name}"
|
||||
|
||||
key = key.replace("#{args}", _compute_from_args())
|
||||
key = key.replace("#{id}", owner.get_full_id())
|
||||
key = key.replace("#{id-name-args}", f"{owner.get_full_id()}-{name}-{_compute_from_args()}")
|
||||
if key is None:
|
||||
if owner is not None and args is None: # args is not provided
|
||||
key = f"{owner.get_full_id()}-{name}"
|
||||
else:
|
||||
key = f"{name}-{_compute_from_args()}"
|
||||
else:
|
||||
key = key.replace("#{args}", _compute_from_args())
|
||||
if owner is not None:
|
||||
key = key.replace("#{id}", owner.get_full_id())
|
||||
key = key.replace("#{id-name-args}", f"{owner.get_full_id()}-{name}-{_compute_from_args()}")
|
||||
|
||||
return key
|
||||
|
||||
@@ -78,24 +79,17 @@ class Command:
|
||||
self._bindings = []
|
||||
self._ft = None
|
||||
self._callback_parameters = dict(inspect.signature(callback).parameters) if callback else {}
|
||||
self._key = key
|
||||
|
||||
# special management when kwargs are provided
|
||||
# In this situation,
|
||||
# either there is no parameter (so one single instance of the command is enough)
|
||||
# or the parameter is a kwargs (so the parameters are provided when the command is called)
|
||||
if (self._key is None
|
||||
and self.owner is not None
|
||||
and args is None # args is not provided
|
||||
):
|
||||
self._key = f"{owner.get_full_id()}-{name}"
|
||||
self._key = self.process_key(key, self.name, self.owner, self.default_args, self.default_kwargs)
|
||||
|
||||
# register the command
|
||||
if auto_register:
|
||||
if self._key in CommandsManager.commands_by_key:
|
||||
self.id = CommandsManager.commands_by_key[self._key].id
|
||||
if self._key is not None:
|
||||
if self._key in CommandsManager.commands_by_key:
|
||||
self.id = CommandsManager.commands_by_key[self._key].id
|
||||
else:
|
||||
CommandsManager.register(self)
|
||||
else:
|
||||
CommandsManager.register(self)
|
||||
logger.warning(f"Command {self.name} has no key, it will not be registered.")
|
||||
|
||||
def get_key(self):
|
||||
return self._key
|
||||
|
||||
@@ -105,13 +105,13 @@ class FormattingCompletionEngine(BaseCompletionEngine):
|
||||
|
||||
case Context.CELL_ROW:
|
||||
return self._get_row_index_suggestions()
|
||||
|
||||
|
||||
case Context.TABLE_NAME:
|
||||
return self._get_table_name_suggestion()
|
||||
|
||||
|
||||
case Context.TABLES_SCOPE:
|
||||
return [Suggestion(":", "Define global rules for all tables", "syntax")]
|
||||
|
||||
|
||||
# =================================================================
|
||||
# Rule-level contexts
|
||||
# =================================================================
|
||||
@@ -236,13 +236,13 @@ class FormattingCompletionEngine(BaseCompletionEngine):
|
||||
except Exception:
|
||||
pass
|
||||
return []
|
||||
|
||||
|
||||
def _get_table_name_suggestion(self) -> list[Suggestion]:
|
||||
"""Get table name suggestion (current table only)."""
|
||||
if self.table_name:
|
||||
return [Suggestion(f'"{self.table_name}"', f"Current table: {self.table_name}", "table")]
|
||||
return []
|
||||
|
||||
|
||||
def _get_style_preset_suggestions(self) -> list[Suggestion]:
|
||||
"""Get style preset suggestions (without quotes)."""
|
||||
suggestions = []
|
||||
@@ -337,7 +337,7 @@ class FormattingCompletionEngine(BaseCompletionEngine):
|
||||
|
||||
try:
|
||||
# Use table_name from scope, or empty string as fallback
|
||||
table_name = scope.table_name or ""
|
||||
table_name = scope.table_name or self.table_name or ""
|
||||
values = self.provider.list_column_values(table_name, scope.column_name)
|
||||
suggestions = []
|
||||
for value in values:
|
||||
|
||||
@@ -3,228 +3,226 @@ from typing import Any, Callable
|
||||
from myfasthtml.core.formatting.condition_evaluator import ConditionEvaluator
|
||||
from myfasthtml.core.formatting.dataclasses import FormatRule
|
||||
from myfasthtml.core.formatting.formatter_resolver import FormatterResolver
|
||||
from myfasthtml.core.formatting.style_resolver import StyleResolver
|
||||
from myfasthtml.core.formatting.style_resolver import StyleResolver, StyleContainer
|
||||
|
||||
|
||||
class FormattingEngine:
|
||||
"""
|
||||
Main facade for the formatting system.
|
||||
|
||||
Combines:
|
||||
- ConditionEvaluator: evaluates conditions
|
||||
- StyleResolver: resolves styles to CSS
|
||||
- FormatterResolver: formats values for display
|
||||
- Conflict resolution: handles multiple matching rules
|
||||
|
||||
Usage:
|
||||
engine = FormattingEngine()
|
||||
rules = [
|
||||
FormatRule(style=Style(preset="error"), condition=Condition(operator="<", value=0)),
|
||||
FormatRule(formatter=NumberFormatter(preset="EUR")),
|
||||
]
|
||||
css, formatted = engine.apply_format(rules, cell_value=-5.0)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
style_presets: dict = None,
|
||||
formatter_presets: dict = None,
|
||||
lookup_resolver: Callable[[str, str, str], dict] = None
|
||||
):
|
||||
"""
|
||||
Main facade for the formatting system.
|
||||
Initialize the FormattingEngine.
|
||||
|
||||
Combines:
|
||||
- ConditionEvaluator: evaluates conditions
|
||||
- StyleResolver: resolves styles to CSS
|
||||
- FormatterResolver: formats values for display
|
||||
- Conflict resolution: handles multiple matching rules
|
||||
|
||||
Usage:
|
||||
engine = FormattingEngine()
|
||||
rules = [
|
||||
FormatRule(style=Style(preset="error"), condition=Condition(operator="<", value=0)),
|
||||
FormatRule(formatter=NumberFormatter(preset="EUR")),
|
||||
]
|
||||
css, formatted = engine.apply_format(rules, cell_value=-5.0)
|
||||
Args:
|
||||
style_presets: Custom style presets. If None, uses defaults.
|
||||
formatter_presets: Custom formatter presets. If None, uses defaults.
|
||||
lookup_resolver: Function for resolving enum datagrid sources.
|
||||
"""
|
||||
self._condition_evaluator = ConditionEvaluator()
|
||||
self._style_resolver = StyleResolver(style_presets)
|
||||
self._formatter_resolver = FormatterResolver(formatter_presets, lookup_resolver)
|
||||
|
||||
def apply_format(
|
||||
self,
|
||||
rules: list[FormatRule],
|
||||
cell_value: Any,
|
||||
row_data: dict = None
|
||||
) -> tuple[StyleContainer | None, str | None]:
|
||||
"""
|
||||
Apply format rules to a cell value.
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
style_presets: dict = None,
|
||||
formatter_presets: dict = None,
|
||||
lookup_resolver: Callable[[str, str, str], dict] = None
|
||||
):
|
||||
"""
|
||||
Initialize the FormattingEngine.
|
||||
Args:
|
||||
rules: List of FormatRule to evaluate
|
||||
cell_value: The cell value to format
|
||||
row_data: Dict of {col_id: value} for column references
|
||||
|
||||
Args:
|
||||
style_presets: Custom style presets. If None, uses defaults.
|
||||
formatter_presets: Custom formatter presets. If None, uses defaults.
|
||||
lookup_resolver: Function for resolving enum datagrid sources.
|
||||
"""
|
||||
self._condition_evaluator = ConditionEvaluator()
|
||||
self._style_resolver = StyleResolver(style_presets)
|
||||
self._formatter_resolver = FormatterResolver(formatter_presets, lookup_resolver)
|
||||
Returns:
|
||||
Tuple of (css_string, formatted_value):
|
||||
- css_string: CSS inline style string, or None if no style
|
||||
- formatted_value: Formatted string, or None if no formatter
|
||||
"""
|
||||
if not rules:
|
||||
return None, None
|
||||
|
||||
# Find all matching rules
|
||||
matching_rules = self._get_matching_rules(rules, cell_value, row_data)
|
||||
|
||||
if not matching_rules:
|
||||
return None, None
|
||||
|
||||
# Resolve style and formatter independently
|
||||
# This allows combining style from one rule and formatter from another
|
||||
winning_style = self._resolve_style(matching_rules)
|
||||
winning_formatter = self._resolve_formatter(matching_rules)
|
||||
|
||||
# Apply style
|
||||
style = None
|
||||
if winning_style:
|
||||
style = self._style_resolver.to_style_container(winning_style)
|
||||
|
||||
# Apply formatter
|
||||
formatted_value = None
|
||||
if winning_formatter:
|
||||
formatted_value = self._formatter_resolver.resolve(winning_formatter, cell_value)
|
||||
|
||||
return style, formatted_value
|
||||
|
||||
def _get_matching_rules(
|
||||
self,
|
||||
rules: list[FormatRule],
|
||||
cell_value: Any,
|
||||
row_data: dict = None
|
||||
) -> list[FormatRule]:
|
||||
"""
|
||||
Get all rules that match the current cell.
|
||||
|
||||
def apply_format(
|
||||
self,
|
||||
rules: list[FormatRule],
|
||||
cell_value: Any,
|
||||
row_data: dict = None
|
||||
) -> tuple[str | None, str | None]:
|
||||
"""
|
||||
Apply format rules to a cell value.
|
||||
A rule matches if:
|
||||
- It has no condition (unconditional)
|
||||
- Its condition evaluates to True
|
||||
"""
|
||||
matching = []
|
||||
|
||||
for rule in rules:
|
||||
if rule.condition is None:
|
||||
# Unconditional rule always matches
|
||||
matching.append(rule)
|
||||
elif self._condition_evaluator.evaluate(rule.condition, cell_value, row_data):
|
||||
# Conditional rule matches
|
||||
matching.append(rule)
|
||||
|
||||
return matching
|
||||
|
||||
def _resolve_style(self, matching_rules: list[FormatRule]):
|
||||
"""
|
||||
Resolve style conflicts when multiple rules match.
|
||||
|
||||
Args:
|
||||
rules: List of FormatRule to evaluate
|
||||
cell_value: The cell value to format
|
||||
row_data: Dict of {col_id: value} for column references
|
||||
Resolution logic:
|
||||
1. Filter to rules that have a style
|
||||
2. Specificity = 1 if rule has condition, 0 otherwise
|
||||
3. Higher specificity wins
|
||||
4. At equal specificity, last rule wins
|
||||
|
||||
Returns:
|
||||
Tuple of (css_string, formatted_value):
|
||||
- css_string: CSS inline style string, or None if no style
|
||||
- formatted_value: Formatted string, or None if no formatter
|
||||
"""
|
||||
if not rules:
|
||||
return None, None
|
||||
Args:
|
||||
matching_rules: List of rules that matched
|
||||
|
||||
# Find all matching rules
|
||||
matching_rules = self._get_matching_rules(rules, cell_value, row_data)
|
||||
Returns:
|
||||
The winning Style, or None if no rules have style
|
||||
"""
|
||||
# Filter to rules with style
|
||||
style_rules = [rule for rule in matching_rules if rule.style is not None]
|
||||
|
||||
if not style_rules:
|
||||
return None
|
||||
|
||||
if len(style_rules) == 1:
|
||||
return style_rules[0].style
|
||||
|
||||
# Calculate specificity for each rule
|
||||
def get_specificity(rule: FormatRule) -> int:
|
||||
return 1 if rule.condition is not None else 0
|
||||
|
||||
# Find the maximum specificity
|
||||
max_specificity = max(get_specificity(rule) for rule in style_rules)
|
||||
|
||||
# Filter to rules with max specificity
|
||||
top_rules = [rule for rule in style_rules if get_specificity(rule) == max_specificity]
|
||||
|
||||
# Last rule wins among equal specificity
|
||||
return top_rules[-1].style
|
||||
|
||||
def _resolve_formatter(self, matching_rules: list[FormatRule]):
|
||||
"""
|
||||
Resolve formatter conflicts when multiple rules match.
|
||||
|
||||
if not matching_rules:
|
||||
return None, None
|
||||
Resolution logic:
|
||||
1. Filter to rules that have a formatter
|
||||
2. Specificity = 1 if rule has condition, 0 otherwise
|
||||
3. Higher specificity wins
|
||||
4. At equal specificity, last rule wins
|
||||
|
||||
# Resolve style and formatter independently
|
||||
# This allows combining style from one rule and formatter from another
|
||||
winning_style = self._resolve_style(matching_rules)
|
||||
winning_formatter = self._resolve_formatter(matching_rules)
|
||||
Args:
|
||||
matching_rules: List of rules that matched
|
||||
|
||||
# Apply style
|
||||
css_string = None
|
||||
if winning_style:
|
||||
css_string = self._style_resolver.to_css_string(winning_style)
|
||||
if css_string == "":
|
||||
css_string = None
|
||||
Returns:
|
||||
The winning Formatter, or None if no rules have formatter
|
||||
"""
|
||||
# Filter to rules with formatter
|
||||
formatter_rules = [rule for rule in matching_rules if rule.formatter is not None]
|
||||
|
||||
if not formatter_rules:
|
||||
return None
|
||||
|
||||
if len(formatter_rules) == 1:
|
||||
return formatter_rules[0].formatter
|
||||
|
||||
# Calculate specificity for each rule
|
||||
def get_specificity(rule: FormatRule) -> int:
|
||||
return 1 if rule.condition is not None else 0
|
||||
|
||||
# Find the maximum specificity
|
||||
max_specificity = max(get_specificity(rule) for rule in formatter_rules)
|
||||
|
||||
# Filter to rules with max specificity
|
||||
top_rules = [rule for rule in formatter_rules if get_specificity(rule) == max_specificity]
|
||||
|
||||
# Last rule wins among equal specificity
|
||||
return top_rules[-1].formatter
|
||||
|
||||
def _resolve_conflicts(self, matching_rules: list[FormatRule]) -> FormatRule | None:
|
||||
"""
|
||||
Resolve conflicts when multiple rules match.
|
||||
|
||||
# Apply formatter
|
||||
formatted_value = None
|
||||
if winning_formatter:
|
||||
formatted_value = self._formatter_resolver.resolve(winning_formatter, cell_value)
|
||||
DEPRECATED: This method is kept for backward compatibility but is no longer used.
|
||||
Use _resolve_style() and _resolve_formatter() instead.
|
||||
|
||||
return css_string, formatted_value
|
||||
Resolution logic:
|
||||
1. Specificity = 1 if rule has condition, 0 otherwise
|
||||
2. Higher specificity wins
|
||||
3. At equal specificity, last rule wins entirely (no fusion)
|
||||
|
||||
def _get_matching_rules(
|
||||
self,
|
||||
rules: list[FormatRule],
|
||||
cell_value: Any,
|
||||
row_data: dict = None
|
||||
) -> list[FormatRule]:
|
||||
"""
|
||||
Get all rules that match the current cell.
|
||||
Args:
|
||||
matching_rules: List of rules that matched
|
||||
|
||||
A rule matches if:
|
||||
- It has no condition (unconditional)
|
||||
- Its condition evaluates to True
|
||||
"""
|
||||
matching = []
|
||||
|
||||
for rule in rules:
|
||||
if rule.condition is None:
|
||||
# Unconditional rule always matches
|
||||
matching.append(rule)
|
||||
elif self._condition_evaluator.evaluate(rule.condition, cell_value, row_data):
|
||||
# Conditional rule matches
|
||||
matching.append(rule)
|
||||
|
||||
return matching
|
||||
|
||||
def _resolve_style(self, matching_rules: list[FormatRule]):
|
||||
"""
|
||||
Resolve style conflicts when multiple rules match.
|
||||
|
||||
Resolution logic:
|
||||
1. Filter to rules that have a style
|
||||
2. Specificity = 1 if rule has condition, 0 otherwise
|
||||
3. Higher specificity wins
|
||||
4. At equal specificity, last rule wins
|
||||
|
||||
Args:
|
||||
matching_rules: List of rules that matched
|
||||
|
||||
Returns:
|
||||
The winning Style, or None if no rules have style
|
||||
"""
|
||||
# Filter to rules with style
|
||||
style_rules = [rule for rule in matching_rules if rule.style is not None]
|
||||
|
||||
if not style_rules:
|
||||
return None
|
||||
|
||||
if len(style_rules) == 1:
|
||||
return style_rules[0].style
|
||||
|
||||
# Calculate specificity for each rule
|
||||
def get_specificity(rule: FormatRule) -> int:
|
||||
return 1 if rule.condition is not None else 0
|
||||
|
||||
# Find the maximum specificity
|
||||
max_specificity = max(get_specificity(rule) for rule in style_rules)
|
||||
|
||||
# Filter to rules with max specificity
|
||||
top_rules = [rule for rule in style_rules if get_specificity(rule) == max_specificity]
|
||||
|
||||
# Last rule wins among equal specificity
|
||||
return top_rules[-1].style
|
||||
|
||||
def _resolve_formatter(self, matching_rules: list[FormatRule]):
|
||||
"""
|
||||
Resolve formatter conflicts when multiple rules match.
|
||||
|
||||
Resolution logic:
|
||||
1. Filter to rules that have a formatter
|
||||
2. Specificity = 1 if rule has condition, 0 otherwise
|
||||
3. Higher specificity wins
|
||||
4. At equal specificity, last rule wins
|
||||
|
||||
Args:
|
||||
matching_rules: List of rules that matched
|
||||
|
||||
Returns:
|
||||
The winning Formatter, or None if no rules have formatter
|
||||
"""
|
||||
# Filter to rules with formatter
|
||||
formatter_rules = [rule for rule in matching_rules if rule.formatter is not None]
|
||||
|
||||
if not formatter_rules:
|
||||
return None
|
||||
|
||||
if len(formatter_rules) == 1:
|
||||
return formatter_rules[0].formatter
|
||||
|
||||
# Calculate specificity for each rule
|
||||
def get_specificity(rule: FormatRule) -> int:
|
||||
return 1 if rule.condition is not None else 0
|
||||
|
||||
# Find the maximum specificity
|
||||
max_specificity = max(get_specificity(rule) for rule in formatter_rules)
|
||||
|
||||
# Filter to rules with max specificity
|
||||
top_rules = [rule for rule in formatter_rules if get_specificity(rule) == max_specificity]
|
||||
|
||||
# Last rule wins among equal specificity
|
||||
return top_rules[-1].formatter
|
||||
|
||||
def _resolve_conflicts(self, matching_rules: list[FormatRule]) -> FormatRule | None:
|
||||
"""
|
||||
Resolve conflicts when multiple rules match.
|
||||
|
||||
DEPRECATED: This method is kept for backward compatibility but is no longer used.
|
||||
Use _resolve_style() and _resolve_formatter() instead.
|
||||
|
||||
Resolution logic:
|
||||
1. Specificity = 1 if rule has condition, 0 otherwise
|
||||
2. Higher specificity wins
|
||||
3. At equal specificity, last rule wins entirely (no fusion)
|
||||
|
||||
Args:
|
||||
matching_rules: List of rules that matched
|
||||
|
||||
Returns:
|
||||
The winning FormatRule, or None if no rules
|
||||
"""
|
||||
if not matching_rules:
|
||||
return None
|
||||
|
||||
if len(matching_rules) == 1:
|
||||
return matching_rules[0]
|
||||
|
||||
# Calculate specificity for each rule
|
||||
# Specificity = 1 if has condition, 0 otherwise
|
||||
def get_specificity(rule: FormatRule) -> int:
|
||||
return 1 if rule.condition is not None else 0
|
||||
|
||||
# Find the maximum specificity
|
||||
max_specificity = max(get_specificity(rule) for rule in matching_rules)
|
||||
|
||||
# Filter to rules with max specificity
|
||||
top_rules = [rule for rule in matching_rules if get_specificity(rule) == max_specificity]
|
||||
|
||||
# Last rule wins among equal specificity
|
||||
return top_rules[-1]
|
||||
Returns:
|
||||
The winning FormatRule, or None if no rules
|
||||
"""
|
||||
if not matching_rules:
|
||||
return None
|
||||
|
||||
if len(matching_rules) == 1:
|
||||
return matching_rules[0]
|
||||
|
||||
# Calculate specificity for each rule
|
||||
# Specificity = 1 if has condition, 0 otherwise
|
||||
def get_specificity(rule: FormatRule) -> int:
|
||||
return 1 if rule.condition is not None else 0
|
||||
|
||||
# Find the maximum specificity
|
||||
max_specificity = max(get_specificity(rule) for rule in matching_rules)
|
||||
|
||||
# Filter to rules with max specificity
|
||||
top_rules = [rule for rule in matching_rules if get_specificity(rule) == max_specificity]
|
||||
|
||||
# Last rule wins among equal specificity
|
||||
return top_rules[-1]
|
||||
|
||||
@@ -3,40 +3,31 @@
|
||||
|
||||
DEFAULT_STYLE_PRESETS = {
|
||||
"primary": {
|
||||
"background-color": "var(--color-primary)",
|
||||
"color": "var(--color-primary-content)",
|
||||
"__class__": "mf-formatting-primary",
|
||||
},
|
||||
"secondary": {
|
||||
"background-color": "var(--color-secondary)",
|
||||
"color": "var(--color-secondary-content)",
|
||||
"__class__": "mf-formatting-secondary",
|
||||
},
|
||||
"accent": {
|
||||
"background-color": "var(--color-accent)",
|
||||
"color": "var(--color-accent-content)",
|
||||
"__class__": "mf-formatting-accent",
|
||||
},
|
||||
"neutral": {
|
||||
"background-color": "var(--color-neutral)",
|
||||
"color": "var(--color-neutral-content)",
|
||||
"__class__": "mf-formatting-neutral",
|
||||
},
|
||||
"info": {
|
||||
"background-color": "var(--color-info)",
|
||||
"color": "var(--color-info-content)",
|
||||
"__class__": "mf-formatting-info",
|
||||
},
|
||||
"success": {
|
||||
"background-color": "var(--color-success)",
|
||||
"color": "var(--color-success-content)",
|
||||
"__class__": "mf-formatting-success",
|
||||
},
|
||||
"warning": {
|
||||
"background-color": "var(--color-warning)",
|
||||
"color": "var(--color-warning-content)",
|
||||
"__class__": "mf-formatting-warning",
|
||||
},
|
||||
"error": {
|
||||
"background-color": "var(--color-error)",
|
||||
"color": "var(--color-error-content)",
|
||||
"__class__": "mf-formatting-error",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# === Formatter Presets ===
|
||||
|
||||
DEFAULT_FORMATTER_PRESETS = {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from myfasthtml.core.formatting.dataclasses import Style
|
||||
from myfasthtml.core.formatting.presets import DEFAULT_STYLE_PRESETS
|
||||
|
||||
|
||||
# Mapping from Python attribute names to CSS property names
|
||||
PROPERTY_NAME_MAP = {
|
||||
"background_color": "background-color",
|
||||
@@ -13,63 +14,90 @@ PROPERTY_NAME_MAP = {
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class StyleContainer:
|
||||
cls: str | None = None
|
||||
css: str = None
|
||||
|
||||
|
||||
class StyleResolver:
|
||||
"""Resolves styles by applying presets and explicit properties."""
|
||||
"""Resolves styles by applying presets and explicit properties."""
|
||||
|
||||
def __init__(self, style_presets: dict = None):
|
||||
"""
|
||||
Initialize the StyleResolver.
|
||||
|
||||
def __init__(self, style_presets: dict = None):
|
||||
"""
|
||||
Initialize the StyleResolver.
|
||||
Args:
|
||||
style_presets: Custom style presets dict. If None, uses DEFAULT_STYLE_PRESETS.
|
||||
"""
|
||||
self.style_presets = style_presets or DEFAULT_STYLE_PRESETS
|
||||
|
||||
def resolve(self, style: Style) -> dict:
|
||||
"""
|
||||
Resolve a Style to CSS properties dict.
|
||||
|
||||
Args:
|
||||
style_presets: Custom style presets dict. If None, uses DEFAULT_STYLE_PRESETS.
|
||||
"""
|
||||
self.style_presets = style_presets or DEFAULT_STYLE_PRESETS
|
||||
Logic:
|
||||
1. If preset is defined, load preset properties
|
||||
2. Override with explicit properties (non-None values)
|
||||
3. Convert Python names to CSS names
|
||||
|
||||
def resolve(self, style: Style) -> dict:
|
||||
"""
|
||||
Resolve a Style to CSS properties dict.
|
||||
Args:
|
||||
style: The Style object to resolve
|
||||
|
||||
Logic:
|
||||
1. If preset is defined, load preset properties
|
||||
2. Override with explicit properties (non-None values)
|
||||
3. Convert Python names to CSS names
|
||||
Returns:
|
||||
Dict of CSS properties, e.g. {"background-color": "red", "color": "white"}
|
||||
"""
|
||||
if style is None:
|
||||
return {}
|
||||
|
||||
result = {}
|
||||
|
||||
# Apply preset first
|
||||
if style.preset and style.preset in self.style_presets:
|
||||
preset_props = self.style_presets[style.preset]
|
||||
for css_name, value in preset_props.items():
|
||||
result[css_name] = value
|
||||
|
||||
# Override with explicit properties
|
||||
for py_name, css_name in PROPERTY_NAME_MAP.items():
|
||||
value = getattr(style, py_name, None)
|
||||
if value is not None:
|
||||
result[css_name] = value
|
||||
|
||||
return result
|
||||
|
||||
def to_css_string(self, style: Style) -> str:
|
||||
"""
|
||||
Resolve a Style to a CSS inline string.
|
||||
|
||||
Args:
|
||||
style: The Style object to resolve
|
||||
Args:
|
||||
style: The Style object to resolve
|
||||
|
||||
Returns:
|
||||
Dict of CSS properties, e.g. {"background-color": "red", "color": "white"}
|
||||
"""
|
||||
if style is None:
|
||||
return {}
|
||||
Returns:
|
||||
CSS string, e.g. "background-color: red; color: white;"
|
||||
"""
|
||||
props = self.resolve(style)
|
||||
if not props:
|
||||
return ""
|
||||
|
||||
props.pop("__class__", "")
|
||||
return "; ".join(f"{key}: {value}" for key, value in props.items()) + ";"
|
||||
|
||||
def to_style_container(self, style: Style) -> StyleContainer:
|
||||
"""
|
||||
Resolve a Style to a class that contains the class name and the CSS inline string.
|
||||
|
||||
result = {}
|
||||
Args:
|
||||
style: The Style object to resolve
|
||||
|
||||
# Apply preset first
|
||||
if style.preset and style.preset in self.style_presets:
|
||||
preset_props = self.style_presets[style.preset]
|
||||
for css_name, value in preset_props.items():
|
||||
result[css_name] = value
|
||||
|
||||
# Override with explicit properties
|
||||
for py_name, css_name in PROPERTY_NAME_MAP.items():
|
||||
value = getattr(style, py_name, None)
|
||||
if value is not None:
|
||||
result[css_name] = value
|
||||
|
||||
return result
|
||||
|
||||
def to_css_string(self, style: Style) -> str:
|
||||
"""
|
||||
Resolve a Style to a CSS inline string.
|
||||
|
||||
Args:
|
||||
style: The Style object to resolve
|
||||
|
||||
Returns:
|
||||
CSS string, e.g. "background-color: red; color: white;"
|
||||
"""
|
||||
props = self.resolve(style)
|
||||
if not props:
|
||||
return ""
|
||||
return "; ".join(f"{key}: {value}" for key, value in props.items()) + ";"
|
||||
Returns:
|
||||
CSS string, e.g. "background-color: red; color: white;" and the class name
|
||||
"""
|
||||
props = self.resolve(style)
|
||||
if not props:
|
||||
return StyleContainer(None, "")
|
||||
|
||||
cls = props.pop("__class__", None)
|
||||
css = "; ".join(f"{key}: {value}" for key, value in props.items()) + ";"
|
||||
|
||||
return StyleContainer(cls, css)
|
||||
|
||||
Reference in New Issue
Block a user