Fixed command id collision. Added class support in style preset

This commit is contained in:
2026-02-08 19:50:10 +01:00
parent 3ec994d6df
commit d44e0a0c01
14 changed files with 623 additions and 3677 deletions

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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"),

View File

@@ -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

View File

@@ -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:

View File

@@ -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]

View File

@@ -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 = {

View File

@@ -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)