Fixed command id collision. Added class support in style preset
This commit is contained in:
@@ -250,6 +250,42 @@ manager.add_formatter_preset("CHF", {
|
|||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**CSS Classes in Style Presets:**
|
||||||
|
|
||||||
|
Style presets can include a special `__class__` key to apply CSS classes (DaisyUI, Tailwind, or custom):
|
||||||
|
|
||||||
|
```python
|
||||||
|
manager.add_style_preset("badge", {
|
||||||
|
"__class__": "badge badge-primary",
|
||||||
|
"background-color": "blue",
|
||||||
|
"color": "white"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
When a preset with `__class__` is applied:
|
||||||
|
- The CSS classes are added to the element's `class` attribute
|
||||||
|
- The CSS properties are applied as inline styles
|
||||||
|
- This allows combining DaisyUI component classes with custom styling
|
||||||
|
|
||||||
|
**Example with DaisyUI badges:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Define badge presets
|
||||||
|
manager.add_style_preset("status_draft", {
|
||||||
|
"__class__": "badge badge-neutral"
|
||||||
|
})
|
||||||
|
|
||||||
|
manager.add_style_preset("status_approved", {
|
||||||
|
"__class__": "badge badge-success",
|
||||||
|
"font-weight": "bold"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Use in DSL
|
||||||
|
column status:
|
||||||
|
style("status_draft") if value == "draft"
|
||||||
|
style("status_approved") if value == "approved"
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Layer 1: Formatting Engine
|
## Layer 1: Formatting Engine
|
||||||
@@ -441,7 +477,7 @@ def apply_format(
|
|||||||
rules: list[FormatRule],
|
rules: list[FormatRule],
|
||||||
cell_value: Any,
|
cell_value: Any,
|
||||||
row_data: dict = None
|
row_data: dict = None
|
||||||
) -> tuple[str | None, str | None]:
|
) -> tuple[StyleContainer | None, str | None]:
|
||||||
"""
|
"""
|
||||||
Apply format rules to a cell value.
|
Apply format rules to a cell value.
|
||||||
|
|
||||||
@@ -451,8 +487,8 @@ def apply_format(
|
|||||||
row_data: Dict of {col_id: value} for column references
|
row_data: Dict of {col_id: value} for column references
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple of (css_string, formatted_value):
|
Tuple of (style_container, formatted_value):
|
||||||
- css_string: CSS inline style string, or None
|
- style_container: StyleContainer with cls and css attributes, or None
|
||||||
- formatted_value: Formatted string, or None
|
- formatted_value: Formatted string, or None
|
||||||
"""
|
"""
|
||||||
```
|
```
|
||||||
@@ -478,9 +514,17 @@ rules = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
# Apply to cell
|
# Apply to cell
|
||||||
css, formatted = engine.apply_format(rules, -1234.56)
|
style, formatted = engine.apply_format(rules, -1234.56)
|
||||||
# css = "background-color: var(--color-error); color: var(--color-error-content);"
|
# style = StyleContainer(
|
||||||
|
# cls=None,
|
||||||
|
# css="background-color: var(--color-error); color: var(--color-error-content);"
|
||||||
|
# )
|
||||||
# formatted = "-1 234,56 €"
|
# formatted = "-1 234,56 €"
|
||||||
|
|
||||||
|
# Access CSS string
|
||||||
|
if style:
|
||||||
|
css_string = style.css
|
||||||
|
css_classes = style.cls
|
||||||
```
|
```
|
||||||
|
|
||||||
### Sub-components
|
### Sub-components
|
||||||
@@ -511,11 +555,47 @@ Converts `Style` objects to CSS strings:
|
|||||||
resolver = StyleResolver()
|
resolver = StyleResolver()
|
||||||
|
|
||||||
style = Style(preset="error", font_weight="bold")
|
style = Style(preset="error", font_weight="bold")
|
||||||
|
|
||||||
|
# Get CSS properties dict
|
||||||
css_dict = resolver.resolve(style)
|
css_dict = resolver.resolve(style)
|
||||||
# {"background-color": "var(--color-error)", "color": "var(--color-error-content)", "font-weight": "bold"}
|
# {"background-color": "var(--color-error)", "color": "var(--color-error-content)", "font-weight": "bold"}
|
||||||
|
|
||||||
|
# Get CSS inline string
|
||||||
css_string = resolver.to_css_string(style)
|
css_string = resolver.to_css_string(style)
|
||||||
# "background-color: var(--color-error); color: var(--color-error-content); font-weight: bold;"
|
# "background-color: var(--color-error); color: var(--color-error-content); font-weight: bold;"
|
||||||
|
|
||||||
|
# Get StyleContainer with classes and CSS
|
||||||
|
container = resolver.to_style_container(style)
|
||||||
|
# StyleContainer(cls=None, css="background-color: var(--color-error); ...")
|
||||||
|
```
|
||||||
|
|
||||||
|
**StyleContainer:**
|
||||||
|
|
||||||
|
The `to_style_container()` method returns a `StyleContainer` object that separates CSS classes from inline styles:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclass
|
||||||
|
class StyleContainer:
|
||||||
|
cls: str | None = None # CSS class names
|
||||||
|
css: str = None # Inline CSS string
|
||||||
|
```
|
||||||
|
|
||||||
|
This is useful when presets include the `__class__` key:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Preset with CSS classes
|
||||||
|
custom_presets = {
|
||||||
|
"badge": {
|
||||||
|
"__class__": "badge badge-primary",
|
||||||
|
"background-color": "blue"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolver = StyleResolver(style_presets=custom_presets)
|
||||||
|
style = Style(preset="badge")
|
||||||
|
|
||||||
|
container = resolver.to_style_container(style)
|
||||||
|
# container.cls = "badge badge-primary"
|
||||||
|
# container.css = "background-color: blue;"
|
||||||
```
|
```
|
||||||
|
|
||||||
#### FormatterResolver
|
#### FormatterResolver
|
||||||
@@ -1217,9 +1297,12 @@ Used by `DataGridFormattingEditor` to configure the CodeMirror editor.
|
|||||||
7. DataGrid renders cells
|
7. DataGrid renders cells
|
||||||
- mk_body_cell_content() applies formatting
|
- mk_body_cell_content() applies formatting
|
||||||
- FormattingEngine.apply_format(rules, cell_value, row_data)
|
- FormattingEngine.apply_format(rules, cell_value, row_data)
|
||||||
|
- Returns (StyleContainer, formatted_value)
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
8. CSS + formatted value rendered in cell
|
8. CSS classes + inline styles + formatted value rendered in cell
|
||||||
|
- StyleContainer.cls applied to class attribute
|
||||||
|
- StyleContainer.css applied as inline style
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -1235,11 +1318,33 @@ from myfasthtml.controls.DataGridsManager import DataGridsManager
|
|||||||
|
|
||||||
manager = DataGridsManager.get_instance(session)
|
manager = DataGridsManager.get_instance(session)
|
||||||
|
|
||||||
|
# Style preset with CSS properties only
|
||||||
manager.add_style_preset("corporate", {
|
manager.add_style_preset("corporate", {
|
||||||
"background-color": "#003366",
|
"background-color": "#003366",
|
||||||
"color": "#FFFFFF",
|
"color": "#FFFFFF",
|
||||||
"font-weight": "bold"
|
"font-weight": "bold"
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Style preset with CSS classes (DaisyUI/Tailwind)
|
||||||
|
manager.add_style_preset("badge_primary", {
|
||||||
|
"__class__": "badge badge-primary",
|
||||||
|
"font-weight": "bold"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Style preset mixing classes and inline styles
|
||||||
|
manager.add_style_preset("highlighted", {
|
||||||
|
"__class__": "badge badge-accent",
|
||||||
|
"background-color": "#fef08a",
|
||||||
|
"color": "#854d0e"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage in DSL:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
column status:
|
||||||
|
style("badge_primary") if value == "active"
|
||||||
|
style("highlighted") if value == "important"
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Add Custom Formatter Presets
|
#### Add Custom Formatter Presets
|
||||||
|
|||||||
@@ -941,4 +941,4 @@ The Panel component uses JavaScript for manual resizing:
|
|||||||
- Sends width updates to server via HTMX
|
- Sends width updates to server via HTMX
|
||||||
- Constrains width between 150px and 500px
|
- Constrains width between 150px and 500px
|
||||||
|
|
||||||
**File:** `src/myfasthtml/assets/myfasthtml.js`
|
**File:** `src/myfasthtml/assets/core/myfasthtml.js`
|
||||||
|
|||||||
@@ -207,3 +207,47 @@
|
|||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
overflow: hidden;
|
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
|
// Make AJAX call with htmx
|
||||||
|
console.debug(`Triggering HTMX action for element ${elementId}: ${method} ${url}`, htmxOptions);
|
||||||
htmx.ajax(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.formatting.engine import FormattingEngine
|
||||||
from myfasthtml.core.instances import MultipleInstance
|
from myfasthtml.core.instances import MultipleInstance
|
||||||
from myfasthtml.core.optimized_ft import OptimizedDiv
|
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.carbon import row, column, grid
|
||||||
from myfasthtml.icons.fluent import checkbox_unchecked16_regular
|
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, column_edit20_regular
|
||||||
from myfasthtml.icons.fluent_p2 import checkbox_checked16_regular
|
from myfasthtml.icons.fluent_p3 import text_edit_style20_regular
|
||||||
|
|
||||||
# OPTIMIZATION: Pre-compiled regex to detect HTML special characters
|
# OPTIMIZATION: Pre-compiled regex to detect HTML special characters
|
||||||
_HTML_SPECIAL_CHARS_REGEX = re.compile(r'[<>&"\']')
|
_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>')
|
return NotStr(f'<span class="dt2-cell-content-number truncate">{row_index}</span>')
|
||||||
|
|
||||||
# Get format rules and apply formatting
|
# Get format rules and apply formatting
|
||||||
css_string = None
|
style = None
|
||||||
formatted_value = None
|
formatted_value = None
|
||||||
rules = self._get_format_rules(col_pos, row_index, col_def)
|
rules = self._get_format_rules(col_pos, row_index, col_def)
|
||||||
if rules:
|
if rules:
|
||||||
row_data = self._state.ns_row_data[row_index] if row_index < len(self._state.ns_row_data) else None
|
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
|
# Use formatted value or convert to string
|
||||||
value_str = formatted_value if formatted_value is not None else str(value)
|
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):
|
if _HTML_SPECIAL_CHARS_REGEX.search(value_str):
|
||||||
value_str = html.escape(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
|
# Number or Text type
|
||||||
if column_type == ColumnType.Number:
|
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:
|
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):
|
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._datagrid_filter,
|
||||||
Div(
|
Div(
|
||||||
self._selection_mode_selector,
|
self._selection_mode_selector,
|
||||||
mk.icon(settings16_regular,
|
mk.icon(column_edit20_regular,
|
||||||
command=self.commands.toggle_columns_manager(),
|
command=self.commands.toggle_columns_manager(),
|
||||||
tooltip="Show column manager"),
|
tooltip="Show column manager"),
|
||||||
mk.icon(settings16_regular,
|
mk.icon(text_edit_style20_regular,
|
||||||
command=self.commands.toggle_formatting_editor(),
|
command=self.commands.toggle_formatting_editor(),
|
||||||
tooltip="Show formatting editor"),
|
tooltip="Show formatting editor"),
|
||||||
cls="flex"),
|
cls="flex"),
|
||||||
|
|||||||
@@ -47,15 +47,16 @@ class Command:
|
|||||||
# In this situation,
|
# In this situation,
|
||||||
# either there is no parameter (so one single instance of the command is enough)
|
# 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)
|
# or the parameter is a kwargs (so the parameters are provided when the command is called)
|
||||||
if (key is None
|
if key is None:
|
||||||
and owner is not None
|
if owner is not None and args is None: # args is not provided
|
||||||
and args is None # args is not provided
|
key = f"{owner.get_full_id()}-{name}"
|
||||||
):
|
else:
|
||||||
key = f"{owner.get_full_id()}-{name}"
|
key = f"{name}-{_compute_from_args()}"
|
||||||
|
else:
|
||||||
key = key.replace("#{args}", _compute_from_args())
|
key = key.replace("#{args}", _compute_from_args())
|
||||||
key = key.replace("#{id}", owner.get_full_id())
|
if owner is not None:
|
||||||
key = key.replace("#{id-name-args}", f"{owner.get_full_id()}-{name}-{_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()}")
|
||||||
|
|
||||||
return key
|
return key
|
||||||
|
|
||||||
@@ -78,24 +79,17 @@ class Command:
|
|||||||
self._bindings = []
|
self._bindings = []
|
||||||
self._ft = None
|
self._ft = None
|
||||||
self._callback_parameters = dict(inspect.signature(callback).parameters) if callback else {}
|
self._callback_parameters = dict(inspect.signature(callback).parameters) if callback else {}
|
||||||
self._key = key
|
self._key = self.process_key(key, self.name, self.owner, self.default_args, self.default_kwargs)
|
||||||
|
|
||||||
# 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}"
|
|
||||||
|
|
||||||
# register the command
|
# register the command
|
||||||
if auto_register:
|
if auto_register:
|
||||||
if self._key in CommandsManager.commands_by_key:
|
if self._key is not None:
|
||||||
self.id = CommandsManager.commands_by_key[self._key].id
|
if self._key in CommandsManager.commands_by_key:
|
||||||
|
self.id = CommandsManager.commands_by_key[self._key].id
|
||||||
|
else:
|
||||||
|
CommandsManager.register(self)
|
||||||
else:
|
else:
|
||||||
CommandsManager.register(self)
|
logger.warning(f"Command {self.name} has no key, it will not be registered.")
|
||||||
|
|
||||||
def get_key(self):
|
def get_key(self):
|
||||||
return self._key
|
return self._key
|
||||||
|
|||||||
@@ -337,7 +337,7 @@ class FormattingCompletionEngine(BaseCompletionEngine):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Use table_name from scope, or empty string as fallback
|
# 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)
|
values = self.provider.list_column_values(table_name, scope.column_name)
|
||||||
suggestions = []
|
suggestions = []
|
||||||
for value in values:
|
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.condition_evaluator import ConditionEvaluator
|
||||||
from myfasthtml.core.formatting.dataclasses import FormatRule
|
from myfasthtml.core.formatting.dataclasses import FormatRule
|
||||||
from myfasthtml.core.formatting.formatter_resolver import FormatterResolver
|
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:
|
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:
|
Args:
|
||||||
- ConditionEvaluator: evaluates conditions
|
style_presets: Custom style presets. If None, uses defaults.
|
||||||
- StyleResolver: resolves styles to CSS
|
formatter_presets: Custom formatter presets. If None, uses defaults.
|
||||||
- FormatterResolver: formats values for display
|
lookup_resolver: Function for resolving enum datagrid sources.
|
||||||
- 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)
|
|
||||||
"""
|
"""
|
||||||
|
self._condition_evaluator = ConditionEvaluator()
|
||||||
|
self._style_resolver = StyleResolver(style_presets)
|
||||||
|
self._formatter_resolver = FormatterResolver(formatter_presets, lookup_resolver)
|
||||||
|
|
||||||
def __init__(
|
def apply_format(
|
||||||
self,
|
self,
|
||||||
style_presets: dict = None,
|
rules: list[FormatRule],
|
||||||
formatter_presets: dict = None,
|
cell_value: Any,
|
||||||
lookup_resolver: Callable[[str, str, str], dict] = None
|
row_data: dict = None
|
||||||
):
|
) -> tuple[StyleContainer | None, str | None]:
|
||||||
"""
|
"""
|
||||||
Initialize the FormattingEngine.
|
Apply format rules to a cell value.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
style_presets: Custom style presets. If None, uses defaults.
|
rules: List of FormatRule to evaluate
|
||||||
formatter_presets: Custom formatter presets. If None, uses defaults.
|
cell_value: The cell value to format
|
||||||
lookup_resolver: Function for resolving enum datagrid sources.
|
row_data: Dict of {col_id: value} for column references
|
||||||
"""
|
|
||||||
self._condition_evaluator = ConditionEvaluator()
|
|
||||||
self._style_resolver = StyleResolver(style_presets)
|
|
||||||
self._formatter_resolver = FormatterResolver(formatter_presets, lookup_resolver)
|
|
||||||
|
|
||||||
def apply_format(
|
Returns:
|
||||||
self,
|
Tuple of (css_string, formatted_value):
|
||||||
rules: list[FormatRule],
|
- css_string: CSS inline style string, or None if no style
|
||||||
cell_value: Any,
|
- formatted_value: Formatted string, or None if no formatter
|
||||||
row_data: dict = None
|
"""
|
||||||
) -> tuple[str | None, str | None]:
|
if not rules:
|
||||||
"""
|
return None, None
|
||||||
Apply format rules to a cell value.
|
|
||||||
|
|
||||||
Args:
|
# Find all matching rules
|
||||||
rules: List of FormatRule to evaluate
|
matching_rules = self._get_matching_rules(rules, cell_value, row_data)
|
||||||
cell_value: The cell value to format
|
|
||||||
row_data: Dict of {col_id: value} for column references
|
|
||||||
|
|
||||||
Returns:
|
if not matching_rules:
|
||||||
Tuple of (css_string, formatted_value):
|
return None, None
|
||||||
- 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
|
# Resolve style and formatter independently
|
||||||
matching_rules = self._get_matching_rules(rules, cell_value, row_data)
|
# 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)
|
||||||
|
|
||||||
if not matching_rules:
|
# Apply style
|
||||||
return None, None
|
style = None
|
||||||
|
if winning_style:
|
||||||
|
style = self._style_resolver.to_style_container(winning_style)
|
||||||
|
|
||||||
# Resolve style and formatter independently
|
# Apply formatter
|
||||||
# This allows combining style from one rule and formatter from another
|
formatted_value = None
|
||||||
winning_style = self._resolve_style(matching_rules)
|
if winning_formatter:
|
||||||
winning_formatter = self._resolve_formatter(matching_rules)
|
formatted_value = self._formatter_resolver.resolve(winning_formatter, cell_value)
|
||||||
|
|
||||||
# Apply style
|
return style, formatted_value
|
||||||
css_string = None
|
|
||||||
if winning_style:
|
|
||||||
css_string = self._style_resolver.to_css_string(winning_style)
|
|
||||||
if css_string == "":
|
|
||||||
css_string = None
|
|
||||||
|
|
||||||
# Apply formatter
|
def _get_matching_rules(
|
||||||
formatted_value = None
|
self,
|
||||||
if winning_formatter:
|
rules: list[FormatRule],
|
||||||
formatted_value = self._formatter_resolver.resolve(winning_formatter, cell_value)
|
cell_value: Any,
|
||||||
|
row_data: dict = None
|
||||||
|
) -> list[FormatRule]:
|
||||||
|
"""
|
||||||
|
Get all rules that match the current cell.
|
||||||
|
|
||||||
return css_string, formatted_value
|
A rule matches if:
|
||||||
|
- It has no condition (unconditional)
|
||||||
|
- Its condition evaluates to True
|
||||||
|
"""
|
||||||
|
matching = []
|
||||||
|
|
||||||
def _get_matching_rules(
|
for rule in rules:
|
||||||
self,
|
if rule.condition is None:
|
||||||
rules: list[FormatRule],
|
# Unconditional rule always matches
|
||||||
cell_value: Any,
|
matching.append(rule)
|
||||||
row_data: dict = None
|
elif self._condition_evaluator.evaluate(rule.condition, cell_value, row_data):
|
||||||
) -> list[FormatRule]:
|
# Conditional rule matches
|
||||||
"""
|
matching.append(rule)
|
||||||
Get all rules that match the current cell.
|
|
||||||
|
|
||||||
A rule matches if:
|
return matching
|
||||||
- It has no condition (unconditional)
|
|
||||||
- Its condition evaluates to True
|
|
||||||
"""
|
|
||||||
matching = []
|
|
||||||
|
|
||||||
for rule in rules:
|
def _resolve_style(self, matching_rules: list[FormatRule]):
|
||||||
if rule.condition is None:
|
"""
|
||||||
# Unconditional rule always matches
|
Resolve style conflicts when multiple rules match.
|
||||||
matching.append(rule)
|
|
||||||
elif self._condition_evaluator.evaluate(rule.condition, cell_value, row_data):
|
|
||||||
# Conditional rule matches
|
|
||||||
matching.append(rule)
|
|
||||||
|
|
||||||
return matching
|
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
|
||||||
|
|
||||||
def _resolve_style(self, matching_rules: list[FormatRule]):
|
Args:
|
||||||
"""
|
matching_rules: List of rules that matched
|
||||||
Resolve style conflicts when multiple rules match.
|
|
||||||
|
|
||||||
Resolution logic:
|
Returns:
|
||||||
1. Filter to rules that have a style
|
The winning Style, or None if no rules have style
|
||||||
2. Specificity = 1 if rule has condition, 0 otherwise
|
"""
|
||||||
3. Higher specificity wins
|
# Filter to rules with style
|
||||||
4. At equal specificity, last rule wins
|
style_rules = [rule for rule in matching_rules if rule.style is not None]
|
||||||
|
|
||||||
Args:
|
if not style_rules:
|
||||||
matching_rules: List of rules that matched
|
return None
|
||||||
|
|
||||||
Returns:
|
if len(style_rules) == 1:
|
||||||
The winning Style, or None if no rules have style
|
return style_rules[0].style
|
||||||
"""
|
|
||||||
# Filter to rules with style
|
|
||||||
style_rules = [rule for rule in matching_rules if rule.style is not None]
|
|
||||||
|
|
||||||
if not style_rules:
|
# Calculate specificity for each rule
|
||||||
return None
|
def get_specificity(rule: FormatRule) -> int:
|
||||||
|
return 1 if rule.condition is not None else 0
|
||||||
|
|
||||||
if len(style_rules) == 1:
|
# Find the maximum specificity
|
||||||
return style_rules[0].style
|
max_specificity = max(get_specificity(rule) for rule in style_rules)
|
||||||
|
|
||||||
# Calculate specificity for each rule
|
# Filter to rules with max specificity
|
||||||
def get_specificity(rule: FormatRule) -> int:
|
top_rules = [rule for rule in style_rules if get_specificity(rule) == max_specificity]
|
||||||
return 1 if rule.condition is not None else 0
|
|
||||||
|
|
||||||
# Find the maximum specificity
|
# Last rule wins among equal specificity
|
||||||
max_specificity = max(get_specificity(rule) for rule in style_rules)
|
return top_rules[-1].style
|
||||||
|
|
||||||
# Filter to rules with max specificity
|
def _resolve_formatter(self, matching_rules: list[FormatRule]):
|
||||||
top_rules = [rule for rule in style_rules if get_specificity(rule) == max_specificity]
|
"""
|
||||||
|
Resolve formatter conflicts when multiple rules match.
|
||||||
|
|
||||||
# Last rule wins among equal specificity
|
Resolution logic:
|
||||||
return top_rules[-1].style
|
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
|
||||||
|
|
||||||
def _resolve_formatter(self, matching_rules: list[FormatRule]):
|
Args:
|
||||||
"""
|
matching_rules: List of rules that matched
|
||||||
Resolve formatter conflicts when multiple rules match.
|
|
||||||
|
|
||||||
Resolution logic:
|
Returns:
|
||||||
1. Filter to rules that have a formatter
|
The winning Formatter, or None if no rules have formatter
|
||||||
2. Specificity = 1 if rule has condition, 0 otherwise
|
"""
|
||||||
3. Higher specificity wins
|
# Filter to rules with formatter
|
||||||
4. At equal specificity, last rule wins
|
formatter_rules = [rule for rule in matching_rules if rule.formatter is not None]
|
||||||
|
|
||||||
Args:
|
if not formatter_rules:
|
||||||
matching_rules: List of rules that matched
|
return None
|
||||||
|
|
||||||
Returns:
|
if len(formatter_rules) == 1:
|
||||||
The winning Formatter, or None if no rules have formatter
|
return formatter_rules[0].formatter
|
||||||
"""
|
|
||||||
# Filter to rules with formatter
|
|
||||||
formatter_rules = [rule for rule in matching_rules if rule.formatter is not None]
|
|
||||||
|
|
||||||
if not formatter_rules:
|
# Calculate specificity for each rule
|
||||||
return None
|
def get_specificity(rule: FormatRule) -> int:
|
||||||
|
return 1 if rule.condition is not None else 0
|
||||||
|
|
||||||
if len(formatter_rules) == 1:
|
# Find the maximum specificity
|
||||||
return formatter_rules[0].formatter
|
max_specificity = max(get_specificity(rule) for rule in formatter_rules)
|
||||||
|
|
||||||
# Calculate specificity for each rule
|
# Filter to rules with max specificity
|
||||||
def get_specificity(rule: FormatRule) -> int:
|
top_rules = [rule for rule in formatter_rules if get_specificity(rule) == max_specificity]
|
||||||
return 1 if rule.condition is not None else 0
|
|
||||||
|
|
||||||
# Find the maximum specificity
|
# Last rule wins among equal specificity
|
||||||
max_specificity = max(get_specificity(rule) for rule in formatter_rules)
|
return top_rules[-1].formatter
|
||||||
|
|
||||||
# Filter to rules with max specificity
|
def _resolve_conflicts(self, matching_rules: list[FormatRule]) -> FormatRule | None:
|
||||||
top_rules = [rule for rule in formatter_rules if get_specificity(rule) == max_specificity]
|
"""
|
||||||
|
Resolve conflicts when multiple rules match.
|
||||||
|
|
||||||
# Last rule wins among equal specificity
|
DEPRECATED: This method is kept for backward compatibility but is no longer used.
|
||||||
return top_rules[-1].formatter
|
Use _resolve_style() and _resolve_formatter() instead.
|
||||||
|
|
||||||
def _resolve_conflicts(self, matching_rules: list[FormatRule]) -> FormatRule | None:
|
Resolution logic:
|
||||||
"""
|
1. Specificity = 1 if rule has condition, 0 otherwise
|
||||||
Resolve conflicts when multiple rules match.
|
2. Higher specificity wins
|
||||||
|
3. At equal specificity, last rule wins entirely (no fusion)
|
||||||
|
|
||||||
DEPRECATED: This method is kept for backward compatibility but is no longer used.
|
Args:
|
||||||
Use _resolve_style() and _resolve_formatter() instead.
|
matching_rules: List of rules that matched
|
||||||
|
|
||||||
Resolution logic:
|
Returns:
|
||||||
1. Specificity = 1 if rule has condition, 0 otherwise
|
The winning FormatRule, or None if no rules
|
||||||
2. Higher specificity wins
|
"""
|
||||||
3. At equal specificity, last rule wins entirely (no fusion)
|
if not matching_rules:
|
||||||
|
return None
|
||||||
|
|
||||||
Args:
|
if len(matching_rules) == 1:
|
||||||
matching_rules: List of rules that matched
|
return matching_rules[0]
|
||||||
|
|
||||||
Returns:
|
# Calculate specificity for each rule
|
||||||
The winning FormatRule, or None if no rules
|
# Specificity = 1 if has condition, 0 otherwise
|
||||||
"""
|
def get_specificity(rule: FormatRule) -> int:
|
||||||
if not matching_rules:
|
return 1 if rule.condition is not None else 0
|
||||||
return None
|
|
||||||
|
|
||||||
if len(matching_rules) == 1:
|
# Find the maximum specificity
|
||||||
return matching_rules[0]
|
max_specificity = max(get_specificity(rule) for rule in matching_rules)
|
||||||
|
|
||||||
# Calculate specificity for each rule
|
# Filter to rules with max specificity
|
||||||
# Specificity = 1 if has condition, 0 otherwise
|
top_rules = [rule for rule in matching_rules if get_specificity(rule) == max_specificity]
|
||||||
def get_specificity(rule: FormatRule) -> int:
|
|
||||||
return 1 if rule.condition is not None else 0
|
|
||||||
|
|
||||||
# Find the maximum specificity
|
# Last rule wins among equal specificity
|
||||||
max_specificity = max(get_specificity(rule) for rule in matching_rules)
|
return top_rules[-1]
|
||||||
|
|
||||||
# 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 = {
|
DEFAULT_STYLE_PRESETS = {
|
||||||
"primary": {
|
"primary": {
|
||||||
"background-color": "var(--color-primary)",
|
"__class__": "mf-formatting-primary",
|
||||||
"color": "var(--color-primary-content)",
|
|
||||||
},
|
},
|
||||||
"secondary": {
|
"secondary": {
|
||||||
"background-color": "var(--color-secondary)",
|
"__class__": "mf-formatting-secondary",
|
||||||
"color": "var(--color-secondary-content)",
|
|
||||||
},
|
},
|
||||||
"accent": {
|
"accent": {
|
||||||
"background-color": "var(--color-accent)",
|
"__class__": "mf-formatting-accent",
|
||||||
"color": "var(--color-accent-content)",
|
|
||||||
},
|
},
|
||||||
"neutral": {
|
"neutral": {
|
||||||
"background-color": "var(--color-neutral)",
|
"__class__": "mf-formatting-neutral",
|
||||||
"color": "var(--color-neutral-content)",
|
|
||||||
},
|
},
|
||||||
"info": {
|
"info": {
|
||||||
"background-color": "var(--color-info)",
|
"__class__": "mf-formatting-info",
|
||||||
"color": "var(--color-info-content)",
|
|
||||||
},
|
},
|
||||||
"success": {
|
"success": {
|
||||||
"background-color": "var(--color-success)",
|
"__class__": "mf-formatting-success",
|
||||||
"color": "var(--color-success-content)",
|
|
||||||
},
|
},
|
||||||
"warning": {
|
"warning": {
|
||||||
"background-color": "var(--color-warning)",
|
"__class__": "mf-formatting-warning",
|
||||||
"color": "var(--color-warning-content)",
|
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"background-color": "var(--color-error)",
|
"__class__": "mf-formatting-error",
|
||||||
"color": "var(--color-error-content)",
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# === Formatter Presets ===
|
# === Formatter Presets ===
|
||||||
|
|
||||||
DEFAULT_FORMATTER_PRESETS = {
|
DEFAULT_FORMATTER_PRESETS = {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from myfasthtml.core.formatting.dataclasses import Style
|
from myfasthtml.core.formatting.dataclasses import Style
|
||||||
from myfasthtml.core.formatting.presets import DEFAULT_STYLE_PRESETS
|
from myfasthtml.core.formatting.presets import DEFAULT_STYLE_PRESETS
|
||||||
|
|
||||||
|
|
||||||
# Mapping from Python attribute names to CSS property names
|
# Mapping from Python attribute names to CSS property names
|
||||||
PROPERTY_NAME_MAP = {
|
PROPERTY_NAME_MAP = {
|
||||||
"background_color": "background-color",
|
"background_color": "background-color",
|
||||||
@@ -13,63 +14,90 @@ PROPERTY_NAME_MAP = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class StyleContainer:
|
||||||
|
cls: str | None = None
|
||||||
|
css: str = None
|
||||||
|
|
||||||
|
|
||||||
class StyleResolver:
|
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):
|
def __init__(self, style_presets: dict = None):
|
||||||
"""
|
"""
|
||||||
Initialize the StyleResolver.
|
Initialize the StyleResolver.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
style_presets: Custom style presets dict. If None, uses DEFAULT_STYLE_PRESETS.
|
style_presets: Custom style presets dict. If None, uses DEFAULT_STYLE_PRESETS.
|
||||||
"""
|
"""
|
||||||
self.style_presets = style_presets or DEFAULT_STYLE_PRESETS
|
self.style_presets = style_presets or DEFAULT_STYLE_PRESETS
|
||||||
|
|
||||||
def resolve(self, style: Style) -> dict:
|
def resolve(self, style: Style) -> dict:
|
||||||
"""
|
"""
|
||||||
Resolve a Style to CSS properties dict.
|
Resolve a Style to CSS properties dict.
|
||||||
|
|
||||||
Logic:
|
Logic:
|
||||||
1. If preset is defined, load preset properties
|
1. If preset is defined, load preset properties
|
||||||
2. Override with explicit properties (non-None values)
|
2. Override with explicit properties (non-None values)
|
||||||
3. Convert Python names to CSS names
|
3. Convert Python names to CSS names
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
style: The Style object to resolve
|
style: The Style object to resolve
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict of CSS properties, e.g. {"background-color": "red", "color": "white"}
|
Dict of CSS properties, e.g. {"background-color": "red", "color": "white"}
|
||||||
"""
|
"""
|
||||||
if style is None:
|
if style is None:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
result = {}
|
result = {}
|
||||||
|
|
||||||
# Apply preset first
|
# Apply preset first
|
||||||
if style.preset and style.preset in self.style_presets:
|
if style.preset and style.preset in self.style_presets:
|
||||||
preset_props = self.style_presets[style.preset]
|
preset_props = self.style_presets[style.preset]
|
||||||
for css_name, value in preset_props.items():
|
for css_name, value in preset_props.items():
|
||||||
result[css_name] = value
|
result[css_name] = value
|
||||||
|
|
||||||
# Override with explicit properties
|
# Override with explicit properties
|
||||||
for py_name, css_name in PROPERTY_NAME_MAP.items():
|
for py_name, css_name in PROPERTY_NAME_MAP.items():
|
||||||
value = getattr(style, py_name, None)
|
value = getattr(style, py_name, None)
|
||||||
if value is not None:
|
if value is not None:
|
||||||
result[css_name] = value
|
result[css_name] = value
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def to_css_string(self, style: Style) -> str:
|
def to_css_string(self, style: Style) -> str:
|
||||||
"""
|
"""
|
||||||
Resolve a Style to a CSS inline string.
|
Resolve a Style to a CSS inline string.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
style: The Style object to resolve
|
style: The Style object to resolve
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
CSS string, e.g. "background-color: red; color: white;"
|
CSS string, e.g. "background-color: red; color: white;"
|
||||||
"""
|
"""
|
||||||
props = self.resolve(style)
|
props = self.resolve(style)
|
||||||
if not props:
|
if not props:
|
||||||
return ""
|
return ""
|
||||||
return "; ".join(f"{key}: {value}" for key, value in props.items()) + ";"
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
style: The Style object to resolve
|
||||||
|
|
||||||
|
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)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from myfasthtml.core.formatting.dataclasses import (
|
|||||||
FormatRule,
|
FormatRule,
|
||||||
)
|
)
|
||||||
from myfasthtml.core.formatting.engine import FormattingEngine
|
from myfasthtml.core.formatting.engine import FormattingEngine
|
||||||
|
from myfasthtml.core.formatting.style_resolver import StyleContainer
|
||||||
|
|
||||||
|
|
||||||
class TestApplyFormat:
|
class TestApplyFormat:
|
||||||
@@ -18,9 +19,9 @@ class TestApplyFormat:
|
|||||||
|
|
||||||
css, formatted = engine.apply_format(rules, cell_value=42)
|
css, formatted = engine.apply_format(rules, cell_value=42)
|
||||||
|
|
||||||
assert css is not None
|
assert isinstance(css, StyleContainer)
|
||||||
assert "background-color: red" in css
|
assert "background-color: red" in css.css
|
||||||
assert "color: white" in css
|
assert "color: white" in css.css
|
||||||
assert formatted is None
|
assert formatted is None
|
||||||
|
|
||||||
def test_apply_format_with_formatter_only(self):
|
def test_apply_format_with_formatter_only(self):
|
||||||
@@ -45,8 +46,8 @@ class TestApplyFormat:
|
|||||||
|
|
||||||
css, formatted = engine.apply_format(rules, cell_value=42.567)
|
css, formatted = engine.apply_format(rules, cell_value=42.567)
|
||||||
|
|
||||||
assert css is not None
|
assert isinstance(css, StyleContainer)
|
||||||
assert "color: green" in css
|
assert "color: green" in css.css
|
||||||
assert formatted == "42.57"
|
assert formatted == "42.57"
|
||||||
|
|
||||||
def test_apply_format_condition_met(self):
|
def test_apply_format_condition_met(self):
|
||||||
@@ -61,8 +62,8 @@ class TestApplyFormat:
|
|||||||
|
|
||||||
css, formatted = engine.apply_format(rules, cell_value=-5)
|
css, formatted = engine.apply_format(rules, cell_value=-5)
|
||||||
|
|
||||||
assert css is not None
|
assert isinstance(css, StyleContainer)
|
||||||
assert "color: red" in css
|
assert "color: red" in css.css
|
||||||
|
|
||||||
def test_apply_format_condition_not_met(self):
|
def test_apply_format_condition_not_met(self):
|
||||||
"""Conditional rule does not apply when condition is not met."""
|
"""Conditional rule does not apply when condition is not met."""
|
||||||
@@ -97,8 +98,8 @@ class TestConflictResolution:
|
|||||||
|
|
||||||
css, _ = engine.apply_format(rules, cell_value="anything")
|
css, _ = engine.apply_format(rules, cell_value="anything")
|
||||||
|
|
||||||
assert css is not None
|
assert isinstance(css, StyleContainer)
|
||||||
assert "color: gray" in css
|
assert "color: gray" in css.css
|
||||||
|
|
||||||
def test_multiple_unconditional_rules_last_wins(self):
|
def test_multiple_unconditional_rules_last_wins(self):
|
||||||
"""Among unconditional rules, last one wins."""
|
"""Among unconditional rules, last one wins."""
|
||||||
@@ -111,9 +112,9 @@ class TestConflictResolution:
|
|||||||
|
|
||||||
css, _ = engine.apply_format(rules, cell_value=42)
|
css, _ = engine.apply_format(rules, cell_value=42)
|
||||||
|
|
||||||
assert "color: green" in css
|
assert "color: green" in css.css
|
||||||
assert "color: red" not in css
|
assert "color: red" not in css.css
|
||||||
assert "color: blue" not in css
|
assert "color: blue" not in css.css
|
||||||
|
|
||||||
def test_conditional_beats_unconditional(self):
|
def test_conditional_beats_unconditional(self):
|
||||||
"""Conditional rule (higher specificity) beats unconditional."""
|
"""Conditional rule (higher specificity) beats unconditional."""
|
||||||
@@ -128,8 +129,8 @@ class TestConflictResolution:
|
|||||||
|
|
||||||
css, _ = engine.apply_format(rules, cell_value=-5)
|
css, _ = engine.apply_format(rules, cell_value=-5)
|
||||||
|
|
||||||
assert "color: red" in css
|
assert "color: red" in css.css
|
||||||
assert "color: gray" not in css
|
assert "color: gray" not in css.css
|
||||||
|
|
||||||
def test_conditional_not_met_falls_back_to_unconditional(self):
|
def test_conditional_not_met_falls_back_to_unconditional(self):
|
||||||
"""When conditional doesn't match, unconditional applies."""
|
"""When conditional doesn't match, unconditional applies."""
|
||||||
@@ -144,7 +145,7 @@ class TestConflictResolution:
|
|||||||
|
|
||||||
css, _ = engine.apply_format(rules, cell_value=5) # positive, condition not met
|
css, _ = engine.apply_format(rules, cell_value=5) # positive, condition not met
|
||||||
|
|
||||||
assert "color: gray" in css
|
assert "color: gray" in css.css
|
||||||
|
|
||||||
def test_multiple_conditional_last_wins(self):
|
def test_multiple_conditional_last_wins(self):
|
||||||
"""Among conditional rules with same specificity, last wins."""
|
"""Among conditional rules with same specificity, last wins."""
|
||||||
@@ -162,8 +163,8 @@ class TestConflictResolution:
|
|||||||
|
|
||||||
css, _ = engine.apply_format(rules, cell_value=5)
|
css, _ = engine.apply_format(rules, cell_value=5)
|
||||||
|
|
||||||
assert "color: blue" in css
|
assert "color: blue" in css.css
|
||||||
assert "color: red" not in css
|
assert "color: red" not in css.css
|
||||||
|
|
||||||
def test_spec_example_value_minus_5(self):
|
def test_spec_example_value_minus_5(self):
|
||||||
"""
|
"""
|
||||||
@@ -190,7 +191,7 @@ class TestConflictResolution:
|
|||||||
|
|
||||||
css, _ = engine.apply_format(rules, cell_value=-5)
|
css, _ = engine.apply_format(rules, cell_value=-5)
|
||||||
|
|
||||||
assert "color: black" in css
|
assert "color: black" in css.css
|
||||||
|
|
||||||
def test_spec_example_value_minus_3(self):
|
def test_spec_example_value_minus_3(self):
|
||||||
"""
|
"""
|
||||||
@@ -213,7 +214,7 @@ class TestConflictResolution:
|
|||||||
|
|
||||||
css, _ = engine.apply_format(rules, cell_value=-3)
|
css, _ = engine.apply_format(rules, cell_value=-3)
|
||||||
|
|
||||||
assert "color: red" in css
|
assert "color: red" in css.css
|
||||||
|
|
||||||
def test_style_and_formatter_fusion(self):
|
def test_style_and_formatter_fusion(self):
|
||||||
"""
|
"""
|
||||||
@@ -241,8 +242,8 @@ class TestConflictResolution:
|
|||||||
# Case 1: Condition met (value > budget)
|
# Case 1: Condition met (value > budget)
|
||||||
css, formatted = engine.apply_format(rules, cell_value=150, row_data=row_data)
|
css, formatted = engine.apply_format(rules, cell_value=150, row_data=row_data)
|
||||||
|
|
||||||
assert css is not None
|
assert isinstance(css, StyleContainer)
|
||||||
assert "var(--color-secondary)" in css # Style from Rule 2
|
assert "var(--color-secondary)" in css.css # Style from Rule 2
|
||||||
assert formatted == "150.00 €" # Formatter from Rule 1
|
assert formatted == "150.00 €" # Formatter from Rule 1
|
||||||
|
|
||||||
# Case 2: Condition not met (value <= budget)
|
# Case 2: Condition not met (value <= budget)
|
||||||
@@ -281,7 +282,7 @@ class TestConflictResolution:
|
|||||||
|
|
||||||
css, formatted = engine.apply_format(rules, cell_value=-5.67)
|
css, formatted = engine.apply_format(rules, cell_value=-5.67)
|
||||||
|
|
||||||
assert "var(--color-error)" in css # Rule 3 wins for style
|
assert "var(--color-error)" in css.css # Rule 3 wins for style
|
||||||
assert formatted == "-6 €" # Rule 4 wins for formatter (precision=0)
|
assert formatted == "-6 €" # Rule 4 wins for formatter (precision=0)
|
||||||
|
|
||||||
|
|
||||||
@@ -299,7 +300,7 @@ class TestWithRowData:
|
|||||||
|
|
||||||
css, _ = engine.apply_format(rules, cell_value=150, row_data=row_data)
|
css, _ = engine.apply_format(rules, cell_value=150, row_data=row_data)
|
||||||
|
|
||||||
assert "color: red" in css
|
assert "color: red" in css.css
|
||||||
|
|
||||||
def test_condition_with_col_parameter(self):
|
def test_condition_with_col_parameter(self):
|
||||||
"""Row-level condition using col parameter."""
|
"""Row-level condition using col parameter."""
|
||||||
@@ -314,8 +315,8 @@ class TestWithRowData:
|
|||||||
|
|
||||||
css, _ = engine.apply_format(rules, cell_value=42, row_data=row_data)
|
css, _ = engine.apply_format(rules, cell_value=42, row_data=row_data)
|
||||||
|
|
||||||
assert css is not None
|
assert isinstance(css, StyleContainer)
|
||||||
assert "background-color" in css
|
assert "background-color" in css.css
|
||||||
|
|
||||||
|
|
||||||
class TestPresets:
|
class TestPresets:
|
||||||
@@ -326,7 +327,7 @@ class TestPresets:
|
|||||||
|
|
||||||
css, _ = engine.apply_format(rules, cell_value=42)
|
css, _ = engine.apply_format(rules, cell_value=42)
|
||||||
|
|
||||||
assert "var(--color-success)" in css
|
assert "var(--color-success)" in css.css
|
||||||
|
|
||||||
def test_formatter_preset(self):
|
def test_formatter_preset(self):
|
||||||
"""Formatter preset is resolved correctly."""
|
"""Formatter preset is resolved correctly."""
|
||||||
@@ -347,5 +348,5 @@ class TestPresets:
|
|||||||
|
|
||||||
css, _ = engine.apply_format(rules, cell_value=42)
|
css, _ = engine.apply_format(rules, cell_value=42)
|
||||||
|
|
||||||
assert "background-color: purple" in css
|
assert "background-color: purple" in css.css
|
||||||
assert "color: yellow" in css
|
assert "color: yellow" in css.css
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from myfasthtml.core.formatting.dataclasses import Style
|
from myfasthtml.core.formatting.dataclasses import Style
|
||||||
from myfasthtml.core.formatting.style_resolver import StyleResolver
|
from myfasthtml.core.formatting.style_resolver import StyleResolver, StyleContainer
|
||||||
|
|
||||||
|
|
||||||
class TestResolve:
|
class TestResolve:
|
||||||
@@ -141,3 +141,108 @@ class TestToCssString:
|
|||||||
result = resolver.to_css_string(style)
|
result = resolver.to_css_string(style)
|
||||||
|
|
||||||
assert result == "color: blue;"
|
assert result == "color: blue;"
|
||||||
|
|
||||||
|
|
||||||
|
class TestToStyleContainer:
|
||||||
|
@pytest.mark.parametrize("style_input,expected_cls,expected_css_contains", [
|
||||||
|
# CSS properties only
|
||||||
|
(
|
||||||
|
Style(background_color="red", color="white"),
|
||||||
|
None,
|
||||||
|
["background-color: red", "color: white"]
|
||||||
|
),
|
||||||
|
# Class only via preset
|
||||||
|
(
|
||||||
|
Style(preset="success"),
|
||||||
|
None, # Default presets don't have __class__
|
||||||
|
["background-color: var(--color-success)", "color: var(--color-success-content)"]
|
||||||
|
),
|
||||||
|
# Empty style
|
||||||
|
(
|
||||||
|
Style(),
|
||||||
|
None,
|
||||||
|
[]
|
||||||
|
),
|
||||||
|
# None style
|
||||||
|
(
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
[]
|
||||||
|
),
|
||||||
|
])
|
||||||
|
def test_i_can_resolve_to_style_container(self, style_input, expected_cls, expected_css_contains):
|
||||||
|
"""Test to_style_container() with various style inputs."""
|
||||||
|
resolver = StyleResolver()
|
||||||
|
result = resolver.to_style_container(style_input)
|
||||||
|
|
||||||
|
assert isinstance(result, StyleContainer)
|
||||||
|
assert result.cls == expected_cls
|
||||||
|
|
||||||
|
if expected_css_contains:
|
||||||
|
for css_part in expected_css_contains:
|
||||||
|
assert css_part in result.css
|
||||||
|
else:
|
||||||
|
assert result.css == ""
|
||||||
|
|
||||||
|
def test_i_can_resolve_preset_with_class_to_container(self):
|
||||||
|
"""Preset with __class__ key is extracted to cls attribute."""
|
||||||
|
custom_presets = {
|
||||||
|
"badge": {
|
||||||
|
"__class__": "badge badge-primary",
|
||||||
|
"background-color": "blue",
|
||||||
|
"color": "white"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolver = StyleResolver(style_presets=custom_presets)
|
||||||
|
style = Style(preset="badge")
|
||||||
|
result = resolver.to_style_container(style)
|
||||||
|
|
||||||
|
assert result.cls == "badge badge-primary"
|
||||||
|
assert "background-color: blue" in result.css
|
||||||
|
assert "color: white" in result.css
|
||||||
|
assert "__class__" not in result.css
|
||||||
|
|
||||||
|
def test_i_can_override_preset_class_with_explicit_properties(self):
|
||||||
|
"""Explicit properties override preset but __class__ is preserved."""
|
||||||
|
custom_presets = {
|
||||||
|
"badge": {
|
||||||
|
"__class__": "badge badge-primary",
|
||||||
|
"background-color": "blue",
|
||||||
|
"color": "white"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolver = StyleResolver(style_presets=custom_presets)
|
||||||
|
style = Style(preset="badge", color="black")
|
||||||
|
result = resolver.to_style_container(style)
|
||||||
|
|
||||||
|
assert result.cls == "badge badge-primary"
|
||||||
|
assert "background-color: blue" in result.css
|
||||||
|
assert "color: black" in result.css # Overridden
|
||||||
|
assert "color: white" not in result.css
|
||||||
|
|
||||||
|
def test_i_can_resolve_multiple_css_properties_to_container(self):
|
||||||
|
"""Multiple CSS properties are correctly formatted in container."""
|
||||||
|
resolver = StyleResolver()
|
||||||
|
style = Style(
|
||||||
|
background_color="#ff0000",
|
||||||
|
color="#ffffff",
|
||||||
|
font_weight="bold",
|
||||||
|
font_style="italic"
|
||||||
|
)
|
||||||
|
result = resolver.to_style_container(style)
|
||||||
|
|
||||||
|
assert result.cls is None
|
||||||
|
assert "background-color: #ff0000" in result.css
|
||||||
|
assert "color: #ffffff" in result.css
|
||||||
|
assert "font-weight: bold" in result.css
|
||||||
|
assert "font-style: italic" in result.css
|
||||||
|
assert result.css.endswith(";")
|
||||||
|
|
||||||
|
def test_i_can_resolve_empty_container_when_no_properties(self):
|
||||||
|
"""Empty style returns container with None cls and empty css."""
|
||||||
|
resolver = StyleResolver()
|
||||||
|
result = resolver.to_style_container(Style())
|
||||||
|
|
||||||
|
assert isinstance(result, StyleContainer)
|
||||||
|
assert result.cls is None
|
||||||
|
assert result.css == ""
|
||||||
|
|||||||
Reference in New Issue
Block a user