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
|
||||
@@ -441,7 +477,7 @@ def apply_format(
|
||||
rules: list[FormatRule],
|
||||
cell_value: Any,
|
||||
row_data: dict = None
|
||||
) -> tuple[str | None, str | None]:
|
||||
) -> tuple[StyleContainer | None, str | None]:
|
||||
"""
|
||||
Apply format rules to a cell value.
|
||||
|
||||
@@ -451,8 +487,8 @@ def apply_format(
|
||||
row_data: Dict of {col_id: value} for column references
|
||||
|
||||
Returns:
|
||||
Tuple of (css_string, formatted_value):
|
||||
- css_string: CSS inline style string, or None
|
||||
Tuple of (style_container, formatted_value):
|
||||
- style_container: StyleContainer with cls and css attributes, or None
|
||||
- formatted_value: Formatted string, or None
|
||||
"""
|
||||
```
|
||||
@@ -478,9 +514,17 @@ rules = [
|
||||
]
|
||||
|
||||
# Apply to cell
|
||||
css, formatted = engine.apply_format(rules, -1234.56)
|
||||
style, formatted = engine.apply_format(rules, -1234.56)
|
||||
# style = StyleContainer(
|
||||
# cls=None,
|
||||
# css="background-color: var(--color-error); color: var(--color-error-content);"
|
||||
# )
|
||||
# formatted = "-1 234,56 €"
|
||||
|
||||
# Access CSS string
|
||||
if style:
|
||||
css_string = style.css
|
||||
css_classes = style.cls
|
||||
```
|
||||
|
||||
### Sub-components
|
||||
@@ -511,11 +555,47 @@ Converts `Style` objects to CSS strings:
|
||||
resolver = StyleResolver()
|
||||
|
||||
style = Style(preset="error", font_weight="bold")
|
||||
|
||||
# Get CSS properties dict
|
||||
css_dict = resolver.resolve(style)
|
||||
# {"background-color": "var(--color-error)", "color": "var(--color-error-content)", "font-weight": "bold"}
|
||||
|
||||
# Get CSS inline string
|
||||
css_string = resolver.to_css_string(style)
|
||||
# "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
|
||||
@@ -1217,9 +1297,12 @@ Used by `DataGridFormattingEditor` to configure the CodeMirror editor.
|
||||
7. DataGrid renders cells
|
||||
- mk_body_cell_content() applies formatting
|
||||
- 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)
|
||||
|
||||
# Style preset with CSS properties only
|
||||
manager.add_style_preset("corporate", {
|
||||
"background-color": "#003366",
|
||||
"color": "#FFFFFF",
|
||||
"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
|
||||
|
||||
@@ -941,4 +941,4 @@ The Panel component uses JavaScript for manual resizing:
|
||||
- Sends width updates to server via HTMX
|
||||
- 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;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* *********************************************** */
|
||||
/* ********** Preset Styles *********** */
|
||||
/* *********************************************** */
|
||||
|
||||
.mf-formatting-primary {
|
||||
background-color: var(--color-primary);
|
||||
color: var(--color-primary-content)
|
||||
}
|
||||
|
||||
.mf-formatting-secondary {
|
||||
background-color: var(--color-secondary);
|
||||
color: var(--color-secondary-content);
|
||||
}
|
||||
|
||||
.mf-formatting-accent {
|
||||
background-color: var(--color-accent);
|
||||
color: var(--color-accent-content);
|
||||
}
|
||||
|
||||
.mf-formatting-neutral {
|
||||
background-color: var(--color-neutral);
|
||||
color: var(--color-neutral-content);
|
||||
}
|
||||
|
||||
.mf-formatting-info {
|
||||
background-color: var(--color-info);
|
||||
color: var(--color-info-content);
|
||||
}
|
||||
|
||||
.mf-formatting-success {
|
||||
background-color: var(--color-success);
|
||||
color: var(--color-success-content);
|
||||
}
|
||||
|
||||
.mf-formatting-warning {
|
||||
background-color: var(--color-warning);
|
||||
color: var(--color-warning-content);
|
||||
}
|
||||
|
||||
.mf-formatting-error {
|
||||
background-color: var(--color-error);
|
||||
color: var(--color-error-content);
|
||||
}
|
||||
@@ -375,6 +375,7 @@ function triggerHtmxAction(elementId, config, combinationStr, isInside, event) {
|
||||
}
|
||||
|
||||
// Make AJAX call with htmx
|
||||
console.debug(`Triggering HTMX action for element ${elementId}: ${method} ${url}`, htmxOptions);
|
||||
htmx.ajax(method, url, htmxOptions);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,901 +0,0 @@
|
||||
/*:root {*/
|
||||
/* --color-border-primary: color-mix(in oklab, var(--color-primary) 40%, #0000);*/
|
||||
/* --color-border: color-mix(in oklab, var(--color-base-content) 20%, #0000);*/
|
||||
/* --color-resize: color-mix(in oklab, var(--color-base-content) 50%, #0000);*/
|
||||
/* --color-selection: color-mix(in oklab, var(--color-primary) 20%, #0000);*/
|
||||
|
||||
/* --datagrid-resize-zindex: 1;*/
|
||||
/* --font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';*/
|
||||
/* --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;*/
|
||||
/* --spacing: 0.25rem;*/
|
||||
/* --text-xs: 0.6875rem;*/
|
||||
/* --text-sm: 0.875rem;*/
|
||||
/* --text-sm--line-height: calc(1.25 / 0.875);*/
|
||||
/* --text-xl: 1.25rem;*/
|
||||
/* --text-xl--line-height: calc(1.75 / 1.25);*/
|
||||
/* --font-weight-medium: 500;*/
|
||||
/* --radius-md: 0.375rem;*/
|
||||
/* --default-font-family: var(--font-sans);*/
|
||||
/* --default-mono-font-family: var(--font-mono);*/
|
||||
/* --properties-font-size: var(--text-xs);*/
|
||||
/* --mf-tooltip-zindex: 10;*/
|
||||
/*}*/
|
||||
|
||||
|
||||
/*.mf-icon-16 {*/
|
||||
/* width: 16px;*/
|
||||
/* min-width: 16px;*/
|
||||
/* height: 16px;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-icon-20 {*/
|
||||
/* width: 20px;*/
|
||||
/* min-width: 20px;*/
|
||||
/* height: 20px;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-icon-24 {*/
|
||||
/* width: 24px;*/
|
||||
/* min-width: 24px;*/
|
||||
/* height: 24px;*/
|
||||
|
||||
/*}*/
|
||||
|
||||
/*.mf-icon-28 {*/
|
||||
/* width: 28px;*/
|
||||
/* min-width: 28px;*/
|
||||
/* height: 28px;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-icon-32 {*/
|
||||
/* width: 32px;*/
|
||||
/* min-width: 32px;*/
|
||||
/* height: 32px;*/
|
||||
/*}*/
|
||||
|
||||
/*!**/
|
||||
/* * MF Layout Component - CSS Grid Layout*/
|
||||
/* * Provides fixed header/footer, collapsible drawers, and scrollable main content*/
|
||||
/* * Compatible with DaisyUI 5*/
|
||||
/* *!*/
|
||||
|
||||
/*.mf-button {*/
|
||||
/* border-radius: 0.375rem;*/
|
||||
/* transition: background-color 0.15s ease;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-button:hover {*/
|
||||
/* background-color: var(--color-base-300);*/
|
||||
/*}*/
|
||||
|
||||
|
||||
/*.mf-tooltip-container {*/
|
||||
/* background: var(--color-base-200);*/
|
||||
/* padding: 5px 10px;*/
|
||||
/* border-radius: 4px;*/
|
||||
/* pointer-events: none; !* Prevent interfering with mouse events *!*/
|
||||
/* font-size: 12px;*/
|
||||
/* white-space: nowrap;*/
|
||||
/* opacity: 0; !* Default to invisible *!*/
|
||||
/* visibility: hidden; !* Prevent interaction when invisible *!*/
|
||||
/* transition: opacity 0.3s ease, visibility 0s linear 0.3s; !* Delay visibility removal *!*/
|
||||
/* position: fixed; !* Keep it above other content and adjust position *!*/
|
||||
/* z-index: var(--mf-tooltip-zindex); !* Ensure it's on top *!*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-tooltip-container[data-visible="true"] {*/
|
||||
/* opacity: 1;*/
|
||||
/* visibility: visible; !* Show tooltip *!*/
|
||||
/* transition: opacity 0.3s ease; !* No delay when becoming visible *!*/
|
||||
/*}*/
|
||||
|
||||
/*!* Main layout container using CSS Grid *!*/
|
||||
/*.mf-layout {*/
|
||||
/* display: grid;*/
|
||||
/* grid-template-areas:*/
|
||||
/* "header header header"*/
|
||||
/* "left-drawer main right-drawer"*/
|
||||
/* "footer footer footer";*/
|
||||
/* grid-template-rows: 32px 1fr 32px;*/
|
||||
/* grid-template-columns: auto 1fr auto;*/
|
||||
/* height: 100vh;*/
|
||||
/* width: 100vw;*/
|
||||
/* overflow: hidden;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Header - fixed at top *!*/
|
||||
/*.mf-layout-header {*/
|
||||
/* grid-area: header;*/
|
||||
/* display: flex;*/
|
||||
/* align-items: center;*/
|
||||
/* justify-content: space-between; !* put one item on each side *!*/
|
||||
/* gap: 1rem;*/
|
||||
/* padding: 0 1rem;*/
|
||||
/* background-color: var(--color-base-300);*/
|
||||
/* border-bottom: 1px solid color-mix(in oklab, var(--color-base-content) 10%, #0000);*/
|
||||
/* z-index: 30;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Footer - fixed at bottom *!*/
|
||||
/*.mf-layout-footer {*/
|
||||
/* grid-area: footer;*/
|
||||
/* display: flex;*/
|
||||
/* align-items: center;*/
|
||||
/* padding: 0 1rem;*/
|
||||
/* background-color: var(--color-neutral);*/
|
||||
/* color: var(--color-neutral-content);*/
|
||||
/* border-top: 1px solid color-mix(in oklab, var(--color-base-content) 10%, #0000);*/
|
||||
/* z-index: 30;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Main content area - scrollable *!*/
|
||||
/*.mf-layout-main {*/
|
||||
/* grid-area: main;*/
|
||||
/* overflow-y: auto;*/
|
||||
/* overflow-x: auto;*/
|
||||
/* padding: 0.5rem;*/
|
||||
/* background-color: var(--color-base-100);*/
|
||||
/*}*/
|
||||
|
||||
/*!* Drawer base styles *!*/
|
||||
/*.mf-layout-drawer {*/
|
||||
/* overflow-y: auto;*/
|
||||
/* overflow-x: hidden;*/
|
||||
/* background-color: var(--color-base-100);*/
|
||||
/* transition: width 0.3s ease-in-out, margin 0.3s ease-in-out;*/
|
||||
/* width: 250px;*/
|
||||
/* padding: 1rem;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Left drawer *!*/
|
||||
/*.mf-layout-left-drawer {*/
|
||||
/* grid-area: left-drawer;*/
|
||||
/* border-right: 1px solid var(--color-border-primary);*/
|
||||
/*}*/
|
||||
|
||||
/*!* Right drawer *!*/
|
||||
/*.mf-layout-right-drawer {*/
|
||||
/* grid-area: right-drawer;*/
|
||||
/* !*border-left: 1px solid color-mix(in oklab, var(--color-base-content) 10%, #0000);*!*/
|
||||
/* border-left: 1px solid var(--color-border-primary);*/
|
||||
/*}*/
|
||||
|
||||
/*!* Collapsed drawer states *!*/
|
||||
/*.mf-layout-drawer.collapsed {*/
|
||||
/* width: 0;*/
|
||||
/* padding: 0;*/
|
||||
/* border: none;*/
|
||||
/* overflow: hidden;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Toggle buttons positioning *!*/
|
||||
/*.mf-layout-toggle-left {*/
|
||||
/* margin-right: auto;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-layout-toggle-right {*/
|
||||
/* margin-left: auto;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Smooth scrollbar styling for webkit browsers *!*/
|
||||
/*.mf-layout-main::-webkit-scrollbar,*/
|
||||
/*.mf-layout-drawer::-webkit-scrollbar {*/
|
||||
/* width: 8px;*/
|
||||
/* height: 8px;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-layout-main::-webkit-scrollbar-track,*/
|
||||
/*.mf-layout-drawer::-webkit-scrollbar-track {*/
|
||||
/* background: var(--color-base-200);*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-layout-main::-webkit-scrollbar-thumb,*/
|
||||
/*.mf-layout-drawer::-webkit-scrollbar-thumb {*/
|
||||
/* background: color-mix(in oklab, var(--color-base-content) 20%, #0000);*/
|
||||
/* border-radius: 4px;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-layout-main::-webkit-scrollbar-thumb:hover,*/
|
||||
/*.mf-layout-drawer::-webkit-scrollbar-thumb:hover {*/
|
||||
/* background: color-mix(in oklab, var(--color-base-content) 30%, #0000);*/
|
||||
/*}*/
|
||||
|
||||
/*!* Responsive adjustments for smaller screens *!*/
|
||||
/*@media (max-width: 768px) {*/
|
||||
/* .mf-layout-drawer {*/
|
||||
/* width: 200px;*/
|
||||
/* }*/
|
||||
|
||||
/* .mf-layout-header,*/
|
||||
/* .mf-layout-footer {*/
|
||||
/* padding: 0 0.5rem;*/
|
||||
/* }*/
|
||||
|
||||
/* .mf-layout-main {*/
|
||||
/* padding: 0.5rem;*/
|
||||
/* }*/
|
||||
/*}*/
|
||||
|
||||
/*!* Handle layouts with no drawers *!*/
|
||||
/*.mf-layout[data-left-drawer="false"] {*/
|
||||
/* grid-template-areas:*/
|
||||
/* "header header"*/
|
||||
/* "main right-drawer"*/
|
||||
/* "footer footer";*/
|
||||
/* grid-template-columns: 1fr auto;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-layout[data-right-drawer="false"] {*/
|
||||
/* grid-template-areas:*/
|
||||
/* "header header"*/
|
||||
/* "left-drawer main"*/
|
||||
/* "footer footer";*/
|
||||
/* grid-template-columns: auto 1fr;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-layout[data-left-drawer="false"][data-right-drawer="false"] {*/
|
||||
/* grid-template-areas:*/
|
||||
/* "header"*/
|
||||
/* "main"*/
|
||||
/* "footer";*/
|
||||
/* grid-template-columns: 1fr;*/
|
||||
/*}*/
|
||||
|
||||
|
||||
/*!***/
|
||||
/* * Layout Drawer Resizer Styles*/
|
||||
/* **/
|
||||
/* * Styles for the resizable drawer borders with visual feedback*/
|
||||
/* *!*/
|
||||
|
||||
/*!* Ensure drawer has relative positioning and no overflow *!*/
|
||||
/*.mf-layout-drawer {*/
|
||||
/* position: relative;*/
|
||||
/* overflow: hidden;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Content wrapper handles scrolling *!*/
|
||||
/*.mf-layout-drawer-content {*/
|
||||
/* position: absolute;*/
|
||||
/* top: 0;*/
|
||||
/* left: 0;*/
|
||||
/* right: 0;*/
|
||||
/* bottom: 0;*/
|
||||
/* overflow-y: auto;*/
|
||||
/* overflow-x: hidden;*/
|
||||
/* padding: 1rem;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Base resizer styles *!*/
|
||||
/*.mf-layout-resizer {*/
|
||||
/* position: absolute;*/
|
||||
/* top: 0;*/
|
||||
/* bottom: 0;*/
|
||||
/* width: 4px;*/
|
||||
/* background-color: transparent;*/
|
||||
/* transition: background-color 0.2s ease;*/
|
||||
/* z-index: 100;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Resizer on the right side (for left drawer) *!*/
|
||||
/*.mf-layout-resizer-right {*/
|
||||
/* right: 0;*/
|
||||
/* cursor: col-resize;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Resizer on the left side (for right drawer) *!*/
|
||||
/*.mf-layout-resizer-left {*/
|
||||
/* left: 0;*/
|
||||
/* cursor: col-resize;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Hover state *!*/
|
||||
/*.mf-layout-resizer:hover {*/
|
||||
/* background-color: rgba(59, 130, 246, 0.3); !* Blue-500 with opacity *!*/
|
||||
/*}*/
|
||||
|
||||
/*!* Active state during resize *!*/
|
||||
/*.mf-layout-drawer-resizing .mf-layout-resizer {*/
|
||||
/* background-color: rgba(59, 130, 246, 0.5);*/
|
||||
/*}*/
|
||||
|
||||
/*!* Disable transitions during resize for smooth dragging *!*/
|
||||
/*.mf-layout-drawer-resizing {*/
|
||||
/* transition: none !important;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Prevent text selection during resize *!*/
|
||||
/*.mf-layout-resizing {*/
|
||||
/* user-select: none;*/
|
||||
/* -webkit-user-select: none;*/
|
||||
/* -moz-user-select: none;*/
|
||||
/* -ms-user-select: none;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Cursor override for entire body during resize *!*/
|
||||
/*.mf-layout-resizing * {*/
|
||||
/* cursor: col-resize !important;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Visual indicator for resizer on hover - subtle border *!*/
|
||||
/*.mf-layout-resizer::before {*/
|
||||
/* content: '';*/
|
||||
/* position: absolute;*/
|
||||
/* top: 50%;*/
|
||||
/* transform: translateY(-50%);*/
|
||||
/* width: 2px;*/
|
||||
/* height: 40px;*/
|
||||
/* background-color: rgba(156, 163, 175, 0.4); !* Gray-400 with opacity *!*/
|
||||
/* border-radius: 2px;*/
|
||||
/* opacity: 0;*/
|
||||
/* transition: opacity 0.2s ease;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-layout-resizer-right::before {*/
|
||||
/* right: 1px;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-layout-resizer-left::before {*/
|
||||
/* left: 1px;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-layout-resizer:hover::before,*/
|
||||
/*.mf-layout-drawer-resizing .mf-layout-resizer::before {*/
|
||||
/* opacity: 1;*/
|
||||
/*}*/
|
||||
|
||||
|
||||
/*.mf-layout-group {*/
|
||||
/* font-weight: bold;*/
|
||||
/* !*font-size: var(--text-sm);*!*/
|
||||
/* overflow: hidden;*/
|
||||
/* text-overflow: ellipsis;*/
|
||||
/* white-space: nowrap;*/
|
||||
/* margin-bottom: 0.2rem;*/
|
||||
/*}*/
|
||||
|
||||
/*!* *********************************************** *!*/
|
||||
/*!* *********** Tabs Manager Component ************ *!*/
|
||||
/*!* *********************************************** *!*/
|
||||
|
||||
/*!* Tabs Manager Container *!*/
|
||||
/*.mf-tabs-manager {*/
|
||||
/* display: flex;*/
|
||||
/* flex-direction: column;*/
|
||||
/* height: 100%;*/
|
||||
/* width: 100%;*/
|
||||
/* background-color: var(--color-base-200);*/
|
||||
/* color: color-mix(in oklab, var(--color-base-content) 50%, transparent);*/
|
||||
/* border-radius: .5rem;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Tabs Header using DaisyUI tabs component *!*/
|
||||
/*.mf-tabs-header {*/
|
||||
/* display: flex;*/
|
||||
/* gap: 0;*/
|
||||
/* flex-shrink: 1;*/
|
||||
/* min-height: 25px;*/
|
||||
|
||||
/* overflow-x: hidden;*/
|
||||
/* overflow-y: hidden;*/
|
||||
/* white-space: nowrap;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-tabs-header-wrapper {*/
|
||||
/* display: flex;*/
|
||||
/* justify-content: space-between;*/
|
||||
/* align-items: center;*/
|
||||
|
||||
/* width: 100%;*/
|
||||
/* !*overflow: hidden; important *!*/
|
||||
/*}*/
|
||||
|
||||
/*!* Individual Tab Button using DaisyUI tab classes *!*/
|
||||
/*.mf-tab-button {*/
|
||||
/* display: flex;*/
|
||||
/* align-items: center;*/
|
||||
/* gap: 0.5rem;*/
|
||||
/* padding: 0 0.5rem 0 1rem;*/
|
||||
/* cursor: pointer;*/
|
||||
/* transition: all 0.2s ease;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-tab-button:hover {*/
|
||||
/* color: var(--color-base-content); !* Change text color on hover *!*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-tab-button.mf-tab-active {*/
|
||||
/* --depth: 1;*/
|
||||
/* background-color: var(--color-base-100);*/
|
||||
/* color: var(--color-base-content);*/
|
||||
/* border-radius: .25rem;*/
|
||||
/* border-bottom: 4px solid var(--color-primary);*/
|
||||
/* box-shadow: 0 1px oklch(100% 0 0/calc(var(--depth) * .1)) inset, 0 1px 1px -1px color-mix(in oklab, var(--color-neutral) calc(var(--depth) * 50%), #0000), 0 1px 6px -4px color-mix(in oklab, var(--color-neutral) calc(var(--depth) * 100%), #0000);*/
|
||||
/*}*/
|
||||
|
||||
/*!* Tab Label *!*/
|
||||
/*.mf-tab-label {*/
|
||||
/* user-select: none;*/
|
||||
/* white-space: nowrap;*/
|
||||
/* max-width: 150px;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Tab Close Button *!*/
|
||||
/*.mf-tab-close-btn {*/
|
||||
/* display: flex;*/
|
||||
/* align-items: center;*/
|
||||
/* justify-content: center;*/
|
||||
/* width: 1rem;*/
|
||||
/* height: 1rem;*/
|
||||
/* border-radius: 0.25rem;*/
|
||||
/* font-size: 1.25rem;*/
|
||||
/* line-height: 1;*/
|
||||
/* @apply text-base-content/50;*/
|
||||
/* transition: all 0.2s ease;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-tab-close-btn:hover {*/
|
||||
/* @apply bg-base-300 text-error;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Tab Content Area *!*/
|
||||
/*.mf-tab-content {*/
|
||||
/* flex: 1;*/
|
||||
/* overflow: auto;*/
|
||||
/* height: 100%;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-tab-content-wrapper {*/
|
||||
/* flex: 1;*/
|
||||
/* overflow: auto;*/
|
||||
/* background-color: var(--color-base-100);*/
|
||||
/* padding: 0.5rem;*/
|
||||
/* border-top: 1px solid var(--color-border-primary);*/
|
||||
/*}*/
|
||||
|
||||
/*!* Empty Content State *!*/
|
||||
/*.mf-empty-content {*/
|
||||
/* align-items: center;*/
|
||||
/* justify-content: center;*/
|
||||
/* height: 100%;*/
|
||||
/* @apply text-base-content/50;*/
|
||||
/* font-style: italic;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-vis {*/
|
||||
/* width: 100%;*/
|
||||
/* height: 100%;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-search-results {*/
|
||||
/* margin-top: 0.5rem;*/
|
||||
/* !*max-height: 400px;*!*/
|
||||
/* overflow: auto;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-dropdown-wrapper {*/
|
||||
/* position: relative; !* CRUCIAL for the anchor *!*/
|
||||
/*}*/
|
||||
|
||||
|
||||
/*.mf-dropdown {*/
|
||||
/* display: none;*/
|
||||
/* position: absolute;*/
|
||||
/* top: 100%;*/
|
||||
/* left: 0;*/
|
||||
/* z-index: 50;*/
|
||||
/* min-width: 200px;*/
|
||||
/* padding: 0.5rem;*/
|
||||
/* box-sizing: border-box;*/
|
||||
/* overflow-x: auto;*/
|
||||
|
||||
/* !* DaisyUI styling *!*/
|
||||
/* background-color: var(--color-base-100);*/
|
||||
/* border: 1px solid var(--color-border);*/
|
||||
/* border-radius: var(--radius-md);*/
|
||||
/* box-shadow: 0 4px 6px -1px color-mix(in oklab, var(--color-neutral) 20%, #0000),*/
|
||||
/* 0 2px 4px -2px color-mix(in oklab, var(--color-neutral) 20%, #0000);*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-dropdown.is-visible {*/
|
||||
/* display: block;*/
|
||||
/* opacity: 1;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Dropdown vertical positioning *!*/
|
||||
/*.mf-dropdown-below {*/
|
||||
/* top: 100%;*/
|
||||
/* bottom: auto;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-dropdown-above {*/
|
||||
/* bottom: 100%;*/
|
||||
/* top: auto;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Dropdown horizontal alignment *!*/
|
||||
/*.mf-dropdown-left {*/
|
||||
/* left: 0;*/
|
||||
/* right: auto;*/
|
||||
/* transform: none;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-dropdown-right {*/
|
||||
/* right: 0;*/
|
||||
/* left: auto;*/
|
||||
/* transform: none;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-dropdown-center {*/
|
||||
/* left: 50%;*/
|
||||
/* right: auto;*/
|
||||
/* transform: translateX(-50%);*/
|
||||
/*}*/
|
||||
|
||||
/*!* *********************************************** *!*/
|
||||
/*!* ************** TreeView Component ************* *!*/
|
||||
/*!* *********************************************** *!*/
|
||||
|
||||
/*!* TreeView Container *!*/
|
||||
/*.mf-treeview {*/
|
||||
/* width: 100%;*/
|
||||
/* user-select: none;*/
|
||||
/*}*/
|
||||
|
||||
/*!* TreeNode Container *!*/
|
||||
/*.mf-treenode-container {*/
|
||||
/* width: 100%;*/
|
||||
/*}*/
|
||||
|
||||
/*!* TreeNode Element *!*/
|
||||
/*.mf-treenode {*/
|
||||
/* display: flex;*/
|
||||
/* align-items: center;*/
|
||||
/* gap: 0.25rem;*/
|
||||
/* padding: 2px 0.5rem;*/
|
||||
/* cursor: pointer;*/
|
||||
/* transition: background-color 0.15s ease;*/
|
||||
/* border-radius: 0.25rem;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Input for Editing *!*/
|
||||
/*.mf-treenode-input {*/
|
||||
/* flex: 1;*/
|
||||
/* padding: 2px 0.25rem;*/
|
||||
/* border: 1px solid var(--color-primary);*/
|
||||
/* border-radius: 0.25rem;*/
|
||||
/* background-color: var(--color-base-100);*/
|
||||
/* outline: none;*/
|
||||
/*}*/
|
||||
|
||||
|
||||
/*.mf-treenode:hover {*/
|
||||
/* background-color: var(--color-base-200);*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-treenode.selected {*/
|
||||
/* background-color: var(--color-primary);*/
|
||||
/* color: var(--color-primary-content);*/
|
||||
/*}*/
|
||||
|
||||
/*!* Toggle Icon *!*/
|
||||
/*.mf-treenode-toggle {*/
|
||||
/* flex-shrink: 0;*/
|
||||
/* width: 20px;*/
|
||||
/* text-align: center;*/
|
||||
/* font-size: 0.75rem;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Node Label *!*/
|
||||
/*.mf-treenode-label {*/
|
||||
/* flex: 1;*/
|
||||
/* overflow: hidden;*/
|
||||
/* text-overflow: ellipsis;*/
|
||||
/* white-space: nowrap;*/
|
||||
/*}*/
|
||||
|
||||
|
||||
/*.mf-treenode-input:focus {*/
|
||||
/* box-shadow: 0 0 0 2px color-mix(in oklab, var(--color-primary) 25%, transparent);*/
|
||||
/*}*/
|
||||
|
||||
/*!* Action Buttons - Hidden by default, shown on hover *!*/
|
||||
/*.mf-treenode-actions {*/
|
||||
/* display: none;*/
|
||||
/* gap: 0.1rem;*/
|
||||
/* white-space: nowrap;*/
|
||||
/* margin-left: 0.5rem;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-treenode:hover .mf-treenode-actions {*/
|
||||
/* display: flex;*/
|
||||
/*}*/
|
||||
|
||||
/*!* *********************************************** *!*/
|
||||
/*!* ********** Generic Resizer Classes ************ *!*/
|
||||
/*!* *********************************************** *!*/
|
||||
|
||||
/*!* Generic resizer - used by both Layout and Panel *!*/
|
||||
/*.mf-resizer {*/
|
||||
/* position: absolute;*/
|
||||
/* width: 4px;*/
|
||||
/* cursor: col-resize;*/
|
||||
/* background-color: transparent;*/
|
||||
/* transition: background-color 0.2s ease;*/
|
||||
/* z-index: 100;*/
|
||||
/* top: 0;*/
|
||||
/* bottom: 0;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-resizer:hover {*/
|
||||
/* background-color: rgba(59, 130, 246, 0.3);*/
|
||||
/*}*/
|
||||
|
||||
/*!* Active state during resize *!*/
|
||||
/*.mf-resizing .mf-resizer {*/
|
||||
/* background-color: rgba(59, 130, 246, 0.5);*/
|
||||
/*}*/
|
||||
|
||||
/*!* Prevent text selection during resize *!*/
|
||||
/*.mf-resizing {*/
|
||||
/* user-select: none;*/
|
||||
/* -webkit-user-select: none;*/
|
||||
/* -moz-user-select: none;*/
|
||||
/* -ms-user-select: none;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Cursor override for entire body during resize *!*/
|
||||
/*.mf-resizing * {*/
|
||||
/* cursor: col-resize !important;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Visual indicator for resizer on hover - subtle border *!*/
|
||||
/*.mf-resizer::before {*/
|
||||
/* content: '';*/
|
||||
/* position: absolute;*/
|
||||
/* top: 50%;*/
|
||||
/* transform: translateY(-50%);*/
|
||||
/* width: 2px;*/
|
||||
/* height: 40px;*/
|
||||
/* background-color: rgba(156, 163, 175, 0.4);*/
|
||||
/* border-radius: 2px;*/
|
||||
/* opacity: 0;*/
|
||||
/* transition: opacity 0.2s ease;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-resizer:hover::before,*/
|
||||
/*.mf-resizing .mf-resizer::before {*/
|
||||
/* opacity: 1;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Resizer positioning *!*/
|
||||
/*!* Left resizer is on the right side of the left panel *!*/
|
||||
/*.mf-resizer-left {*/
|
||||
/* right: 0;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Right resizer is on the left side of the right panel *!*/
|
||||
/*.mf-resizer-right {*/
|
||||
/* left: 0;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Position indicator for resizer *!*/
|
||||
/*.mf-resizer-left::before {*/
|
||||
/* right: 1px;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-resizer-right::before {*/
|
||||
/* left: 1px;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Disable transitions during resize for smooth dragging *!*/
|
||||
/*.mf-item-resizing {*/
|
||||
/* transition: none !important;*/
|
||||
/*}*/
|
||||
|
||||
/*!* *********************************************** *!*/
|
||||
/*!* *************** Panel Component *************** *!*/
|
||||
/*!* *********************************************** *!*/
|
||||
|
||||
/*.mf-panel {*/
|
||||
/* display: flex;*/
|
||||
/* width: 100%;*/
|
||||
/* height: 100%;*/
|
||||
/* overflow: hidden;*/
|
||||
/* position: relative;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Common properties for side panels *!*/
|
||||
/*.mf-panel-left,*/
|
||||
/*.mf-panel-right {*/
|
||||
/* position: relative;*/
|
||||
/* flex-shrink: 0;*/
|
||||
/* width: 250px;*/
|
||||
/* min-width: 150px;*/
|
||||
/* max-width: 500px;*/
|
||||
/* height: 100%;*/
|
||||
/* overflow: auto;*/
|
||||
/* transition: width 0.3s ease, min-width 0.3s ease, max-width 0.3s ease;*/
|
||||
/* padding-top: 25px;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Left panel specific *!*/
|
||||
/*.mf-panel-left {*/
|
||||
/* border-right: 1px solid var(--color-border-primary);*/
|
||||
/*}*/
|
||||
|
||||
/*!* Right panel specific *!*/
|
||||
/*.mf-panel-right {*/
|
||||
/* border-left: 1px solid var(--color-border-primary);*/
|
||||
/* padding-left: 0.5rem;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-panel-main {*/
|
||||
/* flex: 1;*/
|
||||
/* height: 100%;*/
|
||||
/* overflow: hidden;*/
|
||||
/* min-width: 0; !* Important to allow the shrinking of flexbox *!*/
|
||||
/*}*/
|
||||
|
||||
/*!* Hidden state - common for both panels *!*/
|
||||
/*.mf-panel-left.mf-hidden,*/
|
||||
/*.mf-panel-right.mf-hidden {*/
|
||||
/* width: 0;*/
|
||||
/* min-width: 0;*/
|
||||
/* max-width: 0;*/
|
||||
/* overflow: hidden;*/
|
||||
/* border: none;*/
|
||||
/* padding: 0;*/
|
||||
/*}*/
|
||||
|
||||
/*!* No transition during manual resize - common for both panels *!*/
|
||||
/*.mf-panel-left.no-transition,*/
|
||||
/*.mf-panel-right.no-transition {*/
|
||||
/* transition: none;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Common properties for panel toggle icons *!*/
|
||||
/*.mf-panel-hide-icon,*/
|
||||
/*.mf-panel-show-icon {*/
|
||||
/* position: absolute;*/
|
||||
/* top: 0;*/
|
||||
/* right: 0;*/
|
||||
/* cursor: pointer;*/
|
||||
/* z-index: 10;*/
|
||||
/* border-radius: 0.25rem;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-panel-hide-icon:hover,*/
|
||||
/*.mf-panel-show-icon:hover {*/
|
||||
/* background-color: var(--color-bg-hover, rgba(0, 0, 0, 0.05));*/
|
||||
/*}*/
|
||||
|
||||
/*!* Show icon positioning *!*/
|
||||
/*.mf-panel-show-icon-left {*/
|
||||
/* left: 0.5rem;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-panel-show-icon-right {*/
|
||||
/* right: 0.5rem;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Panel with title - grid layout for header + scrollable content *!*/
|
||||
/*.mf-panel-body {*/
|
||||
/* display: grid;*/
|
||||
/* grid-template-rows: auto 1fr;*/
|
||||
/* height: 100%;*/
|
||||
/* overflow: hidden;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-panel-header {*/
|
||||
/* display: flex;*/
|
||||
/* justify-content: space-between;*/
|
||||
/* align-items: center;*/
|
||||
/* padding: 0.25rem 0.5rem;*/
|
||||
/* background-color: var(--color-base-200);*/
|
||||
/* border-bottom: 1px solid var(--color-border);*/
|
||||
/*}*/
|
||||
|
||||
/*!* Override absolute positioning for hide icon when inside header *!*/
|
||||
/*.mf-panel-header .mf-panel-hide-icon {*/
|
||||
/* position: static;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-panel-content {*/
|
||||
/* overflow-y: auto;*/
|
||||
/*}*/
|
||||
|
||||
/*!* Remove padding-top when using title layout *!*/
|
||||
/*.mf-panel-left.mf-panel-with-title,*/
|
||||
/*.mf-panel-right.mf-panel-with-title {*/
|
||||
/* padding-top: 0;*/
|
||||
/*}*/
|
||||
|
||||
/*!* *********************************************** *!*/
|
||||
/*!* ************* Properties Component ************ *!*/
|
||||
/*!* *********************************************** *!*/
|
||||
|
||||
/*!*!* Properties container *!*!*/
|
||||
/*.mf-properties {*/
|
||||
/* display: flex;*/
|
||||
/* flex-direction: column;*/
|
||||
/* gap: 1rem;*/
|
||||
/*}*/
|
||||
|
||||
/*!*!* Group card - using DaisyUI card styling *!*!*/
|
||||
/*.mf-properties-group-card {*/
|
||||
/* background-color: var(--color-base-100);*/
|
||||
/* border: 1px solid color-mix(in oklab, var(--color-base-content) 10%, transparent);*/
|
||||
/* border-radius: var(--radius-md);*/
|
||||
/* box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);*/
|
||||
/* overflow: auto;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-properties-group-container {*/
|
||||
/* display: inline-block; !* important *!*/
|
||||
/* min-width: max-content; !* important *!*/
|
||||
/* width: 100%;*/
|
||||
/*}*/
|
||||
|
||||
|
||||
/*!*!* Group header - gradient using DaisyUI primary color *!*!*/
|
||||
/*.mf-properties-group-header {*/
|
||||
/* background: linear-gradient(135deg, var(--color-primary) 0%, color-mix(in oklab, var(--color-primary) 80%, black) 100%);*/
|
||||
/* color: var(--color-primary-content);*/
|
||||
/* padding: calc(var(--properties-font-size) * 0.5) calc(var(--properties-font-size) * 0.75);*/
|
||||
/* font-weight: 700;*/
|
||||
/* font-size: var(--properties-font-size);*/
|
||||
/*}*/
|
||||
|
||||
/*!*!* Group content area *!*!*/
|
||||
/*.mf-properties-group-content {*/
|
||||
/* display: flex;*/
|
||||
/* flex-direction: column;*/
|
||||
/* min-width: max-content;*/
|
||||
/*}*/
|
||||
|
||||
/*!*!* Property row *!*!*/
|
||||
/*.mf-properties-row {*/
|
||||
/* display: grid;*/
|
||||
/* grid-template-columns: 6rem 1fr;*/
|
||||
|
||||
/* align-items: center;*/
|
||||
/* padding: calc(var(--properties-font-size) * 0.4) calc(var(--properties-font-size) * 0.75);*/
|
||||
|
||||
/* border-bottom: 1px solid color-mix(in oklab, var(--color-base-content) 5%, transparent);*/
|
||||
/* transition: background-color 0.15s ease;*/
|
||||
|
||||
/* gap: calc(var(--properties-font-size) * 0.75);*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-properties-row:last-child {*/
|
||||
/* border-bottom: none;*/
|
||||
/*}*/
|
||||
|
||||
/*.mf-properties-row:hover {*/
|
||||
/* background-color: color-mix(in oklab, var(--color-base-content) 3%, transparent);*/
|
||||
/*}*/
|
||||
|
||||
/*!*!* Property key - normal font *!*!*/
|
||||
/*.mf-properties-key {*/
|
||||
/* align-items: start;*/
|
||||
/* font-weight: 600;*/
|
||||
/* color: color-mix(in oklab, var(--color-base-content) 70%, transparent);*/
|
||||
/* flex: 0 0 40%;*/
|
||||
/* font-size: var(--properties-font-size);*/
|
||||
/* white-space: nowrap;*/
|
||||
/* overflow: hidden;*/
|
||||
/* text-overflow: ellipsis;*/
|
||||
/*}*/
|
||||
|
||||
/*!*!* Property value - monospace font *!*!*/
|
||||
/*.mf-properties-value {*/
|
||||
/* font-family: var(--default-mono-font-family);*/
|
||||
/* color: var(--color-base-content);*/
|
||||
/* flex: 1;*/
|
||||
/* text-align: left;*/
|
||||
/* font-size: var(--properties-font-size);*/
|
||||
/* overflow: hidden;*/
|
||||
/* text-overflow: ellipsis;*/
|
||||
/* white-space: nowrap;*/
|
||||
/*}*/
|
||||
File diff suppressed because it is too large
Load Diff
@@ -31,11 +31,11 @@ from myfasthtml.core.formatting.dsl.parser import DSLParser
|
||||
from myfasthtml.core.formatting.engine import FormattingEngine
|
||||
from myfasthtml.core.instances import MultipleInstance
|
||||
from myfasthtml.core.optimized_ft import OptimizedDiv
|
||||
from myfasthtml.core.utils import make_safe_id
|
||||
from myfasthtml.core.utils import make_safe_id, merge_classes
|
||||
from myfasthtml.icons.carbon import row, column, grid
|
||||
from myfasthtml.icons.fluent import checkbox_unchecked16_regular
|
||||
from myfasthtml.icons.fluent_p1 import settings16_regular
|
||||
from myfasthtml.icons.fluent_p2 import checkbox_checked16_regular
|
||||
from myfasthtml.icons.fluent_p2 import checkbox_checked16_regular, column_edit20_regular
|
||||
from myfasthtml.icons.fluent_p3 import text_edit_style20_regular
|
||||
|
||||
# OPTIMIZATION: Pre-compiled regex to detect HTML special characters
|
||||
_HTML_SPECIAL_CHARS_REGEX = re.compile(r'[<>&"\']')
|
||||
@@ -592,12 +592,12 @@ class DataGrid(MultipleInstance):
|
||||
return NotStr(f'<span class="dt2-cell-content-number truncate">{row_index}</span>')
|
||||
|
||||
# Get format rules and apply formatting
|
||||
css_string = None
|
||||
style = None
|
||||
formatted_value = None
|
||||
rules = self._get_format_rules(col_pos, row_index, col_def)
|
||||
if rules:
|
||||
row_data = self._state.ns_row_data[row_index] if row_index < len(self._state.ns_row_data) else None
|
||||
css_string, formatted_value = self._formatting_engine.apply_format(rules, value, row_data)
|
||||
style, formatted_value = self._formatting_engine.apply_format(rules, value, row_data)
|
||||
|
||||
# Use formatted value or convert to string
|
||||
value_str = formatted_value if formatted_value is not None else str(value)
|
||||
@@ -606,11 +606,18 @@ class DataGrid(MultipleInstance):
|
||||
if _HTML_SPECIAL_CHARS_REGEX.search(value_str):
|
||||
value_str = html.escape(value_str)
|
||||
|
||||
if style:
|
||||
cls = style.cls
|
||||
css_string = style.css
|
||||
else:
|
||||
cls = None
|
||||
css_string = ""
|
||||
|
||||
# Number or Text type
|
||||
if column_type == ColumnType.Number:
|
||||
return mk_highlighted_text(value_str, "dt2-cell-content-number", css_string)
|
||||
return mk_highlighted_text(value_str, merge_classes("dt2-cell-content-number", cls), css_string)
|
||||
else:
|
||||
return mk_highlighted_text(value_str, "dt2-cell-content-text", css_string)
|
||||
return mk_highlighted_text(value_str, merge_classes("dt2-cell-content-text", cls), css_string)
|
||||
|
||||
def mk_body_cell(self, col_pos, row_index, col_def: DataGridColumnState, filter_keyword_lower=None):
|
||||
"""
|
||||
@@ -816,10 +823,10 @@ class DataGrid(MultipleInstance):
|
||||
Div(self._datagrid_filter,
|
||||
Div(
|
||||
self._selection_mode_selector,
|
||||
mk.icon(settings16_regular,
|
||||
mk.icon(column_edit20_regular,
|
||||
command=self.commands.toggle_columns_manager(),
|
||||
tooltip="Show column manager"),
|
||||
mk.icon(settings16_regular,
|
||||
mk.icon(text_edit_style20_regular,
|
||||
command=self.commands.toggle_formatting_editor(),
|
||||
tooltip="Show formatting editor"),
|
||||
cls="flex"),
|
||||
|
||||
@@ -47,13 +47,14 @@ 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
|
||||
):
|
||||
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()}")
|
||||
|
||||
@@ -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 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:
|
||||
logger.warning(f"Command {self.name} has no key, it will not be registered.")
|
||||
|
||||
def get_key(self):
|
||||
return self._key
|
||||
|
||||
@@ -337,7 +337,7 @@ class FormattingCompletionEngine(BaseCompletionEngine):
|
||||
|
||||
try:
|
||||
# Use table_name from scope, or empty string as fallback
|
||||
table_name = scope.table_name or ""
|
||||
table_name = scope.table_name or self.table_name or ""
|
||||
values = self.provider.list_column_values(table_name, scope.column_name)
|
||||
suggestions = []
|
||||
for value in values:
|
||||
|
||||
@@ -3,7 +3,7 @@ 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:
|
||||
@@ -48,7 +48,7 @@ class FormattingEngine:
|
||||
rules: list[FormatRule],
|
||||
cell_value: Any,
|
||||
row_data: dict = None
|
||||
) -> tuple[str | None, str | None]:
|
||||
) -> tuple[StyleContainer | None, str | None]:
|
||||
"""
|
||||
Apply format rules to a cell value.
|
||||
|
||||
@@ -77,18 +77,16 @@ class FormattingEngine:
|
||||
winning_formatter = self._resolve_formatter(matching_rules)
|
||||
|
||||
# Apply style
|
||||
css_string = None
|
||||
style = None
|
||||
if winning_style:
|
||||
css_string = self._style_resolver.to_css_string(winning_style)
|
||||
if css_string == "":
|
||||
css_string = None
|
||||
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 css_string, formatted_value
|
||||
return style, formatted_value
|
||||
|
||||
def _get_matching_rules(
|
||||
self,
|
||||
|
||||
@@ -3,40 +3,31 @@
|
||||
|
||||
DEFAULT_STYLE_PRESETS = {
|
||||
"primary": {
|
||||
"background-color": "var(--color-primary)",
|
||||
"color": "var(--color-primary-content)",
|
||||
"__class__": "mf-formatting-primary",
|
||||
},
|
||||
"secondary": {
|
||||
"background-color": "var(--color-secondary)",
|
||||
"color": "var(--color-secondary-content)",
|
||||
"__class__": "mf-formatting-secondary",
|
||||
},
|
||||
"accent": {
|
||||
"background-color": "var(--color-accent)",
|
||||
"color": "var(--color-accent-content)",
|
||||
"__class__": "mf-formatting-accent",
|
||||
},
|
||||
"neutral": {
|
||||
"background-color": "var(--color-neutral)",
|
||||
"color": "var(--color-neutral-content)",
|
||||
"__class__": "mf-formatting-neutral",
|
||||
},
|
||||
"info": {
|
||||
"background-color": "var(--color-info)",
|
||||
"color": "var(--color-info-content)",
|
||||
"__class__": "mf-formatting-info",
|
||||
},
|
||||
"success": {
|
||||
"background-color": "var(--color-success)",
|
||||
"color": "var(--color-success-content)",
|
||||
"__class__": "mf-formatting-success",
|
||||
},
|
||||
"warning": {
|
||||
"background-color": "var(--color-warning)",
|
||||
"color": "var(--color-warning-content)",
|
||||
"__class__": "mf-formatting-warning",
|
||||
},
|
||||
"error": {
|
||||
"background-color": "var(--color-error)",
|
||||
"color": "var(--color-error-content)",
|
||||
"__class__": "mf-formatting-error",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# === Formatter Presets ===
|
||||
|
||||
DEFAULT_FORMATTER_PRESETS = {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from myfasthtml.core.formatting.dataclasses import Style
|
||||
from myfasthtml.core.formatting.presets import DEFAULT_STYLE_PRESETS
|
||||
|
||||
|
||||
# Mapping from Python attribute names to CSS property names
|
||||
PROPERTY_NAME_MAP = {
|
||||
"background_color": "background-color",
|
||||
@@ -13,6 +14,12 @@ PROPERTY_NAME_MAP = {
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class StyleContainer:
|
||||
cls: str | None = None
|
||||
css: str = None
|
||||
|
||||
|
||||
class StyleResolver:
|
||||
"""Resolves styles by applying presets and explicit properties."""
|
||||
|
||||
@@ -72,4 +79,25 @@ class StyleResolver:
|
||||
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.
|
||||
|
||||
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,
|
||||
)
|
||||
from myfasthtml.core.formatting.engine import FormattingEngine
|
||||
from myfasthtml.core.formatting.style_resolver import StyleContainer
|
||||
|
||||
|
||||
class TestApplyFormat:
|
||||
@@ -18,9 +19,9 @@ class TestApplyFormat:
|
||||
|
||||
css, formatted = engine.apply_format(rules, cell_value=42)
|
||||
|
||||
assert css is not None
|
||||
assert "background-color: red" in css
|
||||
assert "color: white" in css
|
||||
assert isinstance(css, StyleContainer)
|
||||
assert "background-color: red" in css.css
|
||||
assert "color: white" in css.css
|
||||
assert formatted is None
|
||||
|
||||
def test_apply_format_with_formatter_only(self):
|
||||
@@ -45,8 +46,8 @@ class TestApplyFormat:
|
||||
|
||||
css, formatted = engine.apply_format(rules, cell_value=42.567)
|
||||
|
||||
assert css is not None
|
||||
assert "color: green" in css
|
||||
assert isinstance(css, StyleContainer)
|
||||
assert "color: green" in css.css
|
||||
assert formatted == "42.57"
|
||||
|
||||
def test_apply_format_condition_met(self):
|
||||
@@ -61,8 +62,8 @@ class TestApplyFormat:
|
||||
|
||||
css, formatted = engine.apply_format(rules, cell_value=-5)
|
||||
|
||||
assert css is not None
|
||||
assert "color: red" in css
|
||||
assert isinstance(css, StyleContainer)
|
||||
assert "color: red" in css.css
|
||||
|
||||
def test_apply_format_condition_not_met(self):
|
||||
"""Conditional rule does not apply when condition is not met."""
|
||||
@@ -97,8 +98,8 @@ class TestConflictResolution:
|
||||
|
||||
css, _ = engine.apply_format(rules, cell_value="anything")
|
||||
|
||||
assert css is not None
|
||||
assert "color: gray" in css
|
||||
assert isinstance(css, StyleContainer)
|
||||
assert "color: gray" in css.css
|
||||
|
||||
def test_multiple_unconditional_rules_last_wins(self):
|
||||
"""Among unconditional rules, last one wins."""
|
||||
@@ -111,9 +112,9 @@ class TestConflictResolution:
|
||||
|
||||
css, _ = engine.apply_format(rules, cell_value=42)
|
||||
|
||||
assert "color: green" in css
|
||||
assert "color: red" not in css
|
||||
assert "color: blue" not in css
|
||||
assert "color: green" in css.css
|
||||
assert "color: red" not in css.css
|
||||
assert "color: blue" not in css.css
|
||||
|
||||
def test_conditional_beats_unconditional(self):
|
||||
"""Conditional rule (higher specificity) beats unconditional."""
|
||||
@@ -128,8 +129,8 @@ class TestConflictResolution:
|
||||
|
||||
css, _ = engine.apply_format(rules, cell_value=-5)
|
||||
|
||||
assert "color: red" in css
|
||||
assert "color: gray" not in css
|
||||
assert "color: red" in css.css
|
||||
assert "color: gray" not in css.css
|
||||
|
||||
def test_conditional_not_met_falls_back_to_unconditional(self):
|
||||
"""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
|
||||
|
||||
assert "color: gray" in css
|
||||
assert "color: gray" in css.css
|
||||
|
||||
def test_multiple_conditional_last_wins(self):
|
||||
"""Among conditional rules with same specificity, last wins."""
|
||||
@@ -162,8 +163,8 @@ class TestConflictResolution:
|
||||
|
||||
css, _ = engine.apply_format(rules, cell_value=5)
|
||||
|
||||
assert "color: blue" in css
|
||||
assert "color: red" not in css
|
||||
assert "color: blue" in css.css
|
||||
assert "color: red" not in css.css
|
||||
|
||||
def test_spec_example_value_minus_5(self):
|
||||
"""
|
||||
@@ -190,7 +191,7 @@ class TestConflictResolution:
|
||||
|
||||
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):
|
||||
"""
|
||||
@@ -213,7 +214,7 @@ class TestConflictResolution:
|
||||
|
||||
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):
|
||||
"""
|
||||
@@ -241,8 +242,8 @@ class TestConflictResolution:
|
||||
# Case 1: Condition met (value > budget)
|
||||
css, formatted = engine.apply_format(rules, cell_value=150, row_data=row_data)
|
||||
|
||||
assert css is not None
|
||||
assert "var(--color-secondary)" in css # Style from Rule 2
|
||||
assert isinstance(css, StyleContainer)
|
||||
assert "var(--color-secondary)" in css.css # Style from Rule 2
|
||||
assert formatted == "150.00 €" # Formatter from Rule 1
|
||||
|
||||
# Case 2: Condition not met (value <= budget)
|
||||
@@ -281,7 +282,7 @@ class TestConflictResolution:
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@@ -299,7 +300,7 @@ class TestWithRowData:
|
||||
|
||||
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):
|
||||
"""Row-level condition using col parameter."""
|
||||
@@ -314,8 +315,8 @@ class TestWithRowData:
|
||||
|
||||
css, _ = engine.apply_format(rules, cell_value=42, row_data=row_data)
|
||||
|
||||
assert css is not None
|
||||
assert "background-color" in css
|
||||
assert isinstance(css, StyleContainer)
|
||||
assert "background-color" in css.css
|
||||
|
||||
|
||||
class TestPresets:
|
||||
@@ -326,7 +327,7 @@ class TestPresets:
|
||||
|
||||
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):
|
||||
"""Formatter preset is resolved correctly."""
|
||||
@@ -347,5 +348,5 @@ class TestPresets:
|
||||
|
||||
css, _ = engine.apply_format(rules, cell_value=42)
|
||||
|
||||
assert "background-color: purple" in css
|
||||
assert "color: yellow" in css
|
||||
assert "background-color: purple" in css.css
|
||||
assert "color: yellow" in css.css
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import pytest
|
||||
|
||||
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:
|
||||
@@ -141,3 +141,108 @@ class TestToCssString:
|
||||
result = resolver.to_css_string(style)
|
||||
|
||||
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