diff --git a/src/app.py b/src/app.py index 8f1c6e4..870fd64 100644 --- a/src/app.py +++ b/src/app.py @@ -80,8 +80,7 @@ def index(session): formatting_manager = DataGridFormattingManager(layout) btn_show_formatting_manager = mk.label("Formatting", icon=text_edit_style20_regular, - command=add_tab("Formatting", formatting_manager), - id=formatting_manager.get_id()) + command=add_tab("Formatting", formatting_manager)) layout.left_drawer.add(btn_show_formatting_manager, "Parameters") layout.left_drawer.add(btn_file_upload, "Test") diff --git a/src/myfasthtml/assets/core/formatting_manager.css b/src/myfasthtml/assets/core/formatting_manager.css new file mode 100644 index 0000000..291bfe3 --- /dev/null +++ b/src/myfasthtml/assets/core/formatting_manager.css @@ -0,0 +1,85 @@ +/* ============================================= */ +/* ======== DataGridFormattingManager ========== */ +/* ============================================= */ + +.mf-formatting-manager { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; +} + +/* ---- Preset list items ---- */ + +.mf-fmgr-preset-item { + display: flex; + flex-direction: column; + gap: 2px; + padding: 6px 8px; + border-radius: var(--radius-md); + cursor: pointer; + border: 1px solid transparent; + margin-bottom: 0.5rem; + transition: background-color 0.1s ease; +} + +.mf-fmgr-preset-item:hover { + background-color: var(--color-button-hover); +} + +.mf-fmgr-preset-item-active { + background-color: color-mix(in oklab, var(--color-primary) 15%, #0000); + border-color: color-mix(in oklab, var(--color-primary) 40%, #0000); +} + + +.mf-fmgr-preset-name { + font-size: var(--text-sm); + font-weight: 500; +} + +.mf-fmgr-preset-desc { + font-size: var(--text-xs); + opacity: 0.55; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.mf-fmgr-preset-badges { + display: flex; + gap: 4px; + margin-top: 2px; +} + +/* ---- Editor panel ---- */ + +.mf-fmgr-editor-view { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; +} + +.mf-fmgr-editor-meta { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 12px; + border-bottom: 1px solid var(--color-border); + gap: 8px; + flex-shrink: 0; +} + +.mf-fmgr-placeholder { + display: flex; + align-items: center; + justify-content: center; + height: 100%; +} + +/* ---- New / Rename form ---- */ + +.mf-fmgr-form { + max-width: 480px; +} diff --git a/src/myfasthtml/assets/core/myfasthtml.css b/src/myfasthtml/assets/core/myfasthtml.css index 843463a..955d201 100644 --- a/src/myfasthtml/assets/core/myfasthtml.css +++ b/src/myfasthtml/assets/core/myfasthtml.css @@ -3,6 +3,7 @@ --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); + --color-button-hover: var(--color-base-300); --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'; @@ -58,7 +59,7 @@ } .mf-button:hover { - background-color: var(--color-base-300); + background-color: var(--color-button-hover); } .mf-tooltip-container { diff --git a/src/myfasthtml/assets/core/panel.css b/src/myfasthtml/assets/core/panel.css index f216723..d37bc9f 100644 --- a/src/myfasthtml/assets/core/panel.css +++ b/src/myfasthtml/assets/core/panel.css @@ -108,6 +108,7 @@ .mf-panel-content { overflow-y: auto; + margin-top: 0.5rem; } /* Remove padding-top when using title layout */ @@ -115,3 +116,12 @@ .mf-panel-right.mf-panel-with-title { padding-top: 0; } + +.mf-panel-body.mf-panel-body-right { + padding-left: 0.5rem; +} + +.mf-panel-body.mf-panel-body-left { + padding-right: 0.5rem; +} + diff --git a/src/myfasthtml/assets/core/treeview.css b/src/myfasthtml/assets/core/treeview.css index e2922d0..ae6c36d 100644 --- a/src/myfasthtml/assets/core/treeview.css +++ b/src/myfasthtml/assets/core/treeview.css @@ -36,7 +36,7 @@ .mf-treenode:hover { - background-color: var(--color-base-200); + background-color: var(--color-button-hover); } .mf-treenode.selected { diff --git a/src/myfasthtml/controls/DataGridFormattingManager.py b/src/myfasthtml/controls/DataGridFormattingManager.py index b22d170..0851e67 100644 --- a/src/myfasthtml/controls/DataGridFormattingManager.py +++ b/src/myfasthtml/controls/DataGridFormattingManager.py @@ -1,5 +1,5 @@ import logging -from typing import Optional +from typing import Optional, Literal from fasthtml.common import Form, Fieldset, Label, Input, Span from fasthtml.components import Div @@ -14,378 +14,406 @@ from myfasthtml.controls.helpers import mk from myfasthtml.core.commands import Command from myfasthtml.core.dbmanager import DbObject from myfasthtml.core.formatting.dataclasses import RulePreset +from myfasthtml.core.dsls import DslsManager from myfasthtml.core.formatting.dsl import parse_dsl +from myfasthtml.core.formatting.dsl.completion.FormattingCompletionEngine import FormattingCompletionEngine +from myfasthtml.core.formatting.dsl.completion.provider import DatagridMetadataProvider from myfasthtml.core.formatting.dsl.definition import FormattingDSL +from myfasthtml.core.formatting.dsl.parser import DSLParser from myfasthtml.core.formatting.presets import DEFAULT_RULE_PRESETS from myfasthtml.core.instances import SingleInstance logger = logging.getLogger("DataGridFormattingManager") -_DSL_PLACEHOLDER = 'tables:\n style("error") if value < 0\n style("success") if value > 0' - class DataGridFormattingManagerState(DbObject): - def __init__(self, owner): - with self.initializing(): - super().__init__(owner) - self.presets: list = [] - self.selected_name: Optional[str] = None - self.ns_mode: str = "view" + def __init__(self, owner): + with self.initializing(): + super().__init__(owner) + self.presets: list = [] + self.selected_name: Optional[str] = None + self.ns_mode: str = "view" class Commands(BaseCommands): - def new_preset(self): - return Command( - "NewPreset", - "New preset", - self._owner, - self._owner.on_new_preset, - icon=IconsHelper.get("Add20Regular"), - ).htmx(target=f"#{self._id}", swap="outerHTML") + def new_preset(self): + return Command( + "NewPreset", + "New preset", + self._owner, + self._owner.handle_new_preset, + icon=IconsHelper.get("add_circle20_regular"), + ).htmx(target=f"#{self._id}", swap="outerHTML") + + def save_preset(self): + return Command( + "SavePreset", + "Save preset", + self._owner, + self._owner.handle_save_preset, + icon=IconsHelper.get("Save20Regular"), + ).htmx(target=f"#{self._id}", swap="outerHTML") + + def rename_preset(self): + return Command( + "RenamePreset", + "Rename preset", + self._owner, + self._owner.handle_rename_preset, + icon=IconsHelper.get("edit20_regular"), + ).htmx(target=f"#{self._id}", swap="outerHTML") + + def delete_preset(self): + return Command( + "DeletePreset", + "Delete preset", + self._owner, + self._owner.handle_delete_preset, + icon=IconsHelper.get("Delete20Regular"), + ).htmx(target=f"#{self._id}", swap="outerHTML") + + def confirm_new(self): + return Command( + "ConfirmNew", + "Confirm new preset", + self._owner, + self._owner.handle_confirm_new, + ).htmx(target=f"#{self._id}", swap="outerHTML") + + def confirm_rename(self): + return Command( + "ConfirmRename", + "Confirm rename", + self._owner, + self._owner.handle_confirm_rename, + ).htmx(target=f"#{self._id}", swap="outerHTML") - def save_preset(self): - return Command( - "SavePreset", - "Save preset", - self._owner, - self._owner.on_save_preset, - icon=IconsHelper.get("Save20Regular"), - ).htmx(target=f"#{self._id}", swap="outerHTML") - - def rename_preset(self): - return Command( - "RenamePreset", - "Rename preset", - self._owner, - self._owner.on_rename_preset, - icon=IconsHelper.get("Rename20Regular"), - ).htmx(target=f"#{self._id}", swap="outerHTML") - - def delete_preset(self): - return Command( - "DeletePreset", - "Delete preset", - self._owner, - self._owner.on_delete_preset, - icon=IconsHelper.get("Delete20Regular"), - ).htmx(target=f"#{self._id}", swap="outerHTML") - - def confirm_new(self): - return Command( - "ConfirmNew", - "Confirm new preset", - self._owner, - self._owner.on_confirm_new, - ).htmx(target=f"#{self._id}", swap="outerHTML") - - def confirm_rename(self): - return Command( - "ConfirmRename", - "Confirm rename", - self._owner, - self._owner.on_confirm_rename, - ).htmx(target=f"#{self._id}", swap="outerHTML") - - def cancel(self): - return Command( - "Cancel", - "Cancel", - self._owner, - self._owner.on_cancel, - ).htmx(target=f"#{self._id}", swap="outerHTML") + def cancel(self): + return Command( + "Cancel", + "Cancel", + self._owner, + self._owner.handle_cancel, + ).htmx(target=f"#{self._id}", swap="outerHTML") + + def select_preset(self, name: str): + return Command( + "SelectPreset", + f"Select {name}", + self._owner, + self._owner.handle_select_preset, + args=[name], + ).htmx(target=f"#{self._id}", swap="outerHTML") class DataGridFormattingManager(SingleInstance): - def __init__(self, parent, _id=None): - super().__init__(parent, _id=_id) - self._state = DataGridFormattingManagerState(self) - self.commands = Commands(self) + def __init__(self, parent, _id=None): + super().__init__(parent, _id=_id) + self._state = DataGridFormattingManagerState(self) + self.commands = Commands(self) + + self._panel = Panel( + self, + conf=PanelConf(left=True, right=False, left_title="Presets"), + _id="-panel", + ) + self._menu = Menu( + self, + conf=MenuConf(fixed_items=["NewPreset", "SavePreset", "RenamePreset", "DeletePreset"]), + save_state=False, + _id="-menu", + ) + self._search = Search( + self, + items_names="Presets", + template=self._mk_preset_item, + _id="-search", + ) + provider = DatagridMetadataProvider(self) + completion_engine = FormattingCompletionEngine(provider, "") + DslsManager.register(completion_engine, DSLParser()) - self._panel = Panel( - self, - conf=PanelConf(left=True, right=False, left_title="Presets"), - _id="-panel", - ) - self._menu = Menu( - self, - conf=MenuConf(fixed_items=["NewPreset", "SavePreset", "RenamePreset", "DeletePreset"]), - save_state=False, - _id="-menu", - ) - self._search = Search( - self, - items_names="Presets", - template=self._mk_preset_item, - _id="-search", - ) - self._editor = DslEditor( - self, - dsl=FormattingDSL(), - conf=DslEditorConf( - save_button=False, - autocompletion=False, - linting=False, - placeholder=_DSL_PLACEHOLDER, - ), - save_state=False, - _id="-editor", - ) + self._editor = DslEditor( + self, + dsl=FormattingDSL(), + conf=DslEditorConf( + save_button=False, + autocompletion=True, + linting=True, + engine_id=completion_engine.get_id(), + ), + save_state=False, + _id="-editor", + ) + + self.handle_select_preset(self._state.selected_name) + self._sync_provider() + logger.debug(f"DataGridFormattingManager created with id={self._id}") + + def get_main_content_id(self): + return self._panel.get_ids().main + + # === Helpers === + + def _get_all_presets(self) -> list: + """Returns builtin presets followed by user presets.""" + return list(DEFAULT_RULE_PRESETS.values()) + self._state.presets + + def _is_builtin(self, name: str) -> bool: + return name in DEFAULT_RULE_PRESETS + + def _get_selected_preset(self) -> Optional[RulePreset]: + if not self._state.selected_name: + return None + for p in self._get_all_presets(): + if p.name == self._state.selected_name: + return p + return None + + def _get_user_preset(self, name: str) -> Optional[RulePreset]: + for p in self._state.presets: + if p.name == name: + return p + return None + + def _parse_dsl_to_rules(self, dsl_text: str) -> list: + """Parse DSL text and extract FormatRule objects, ignoring scopes.""" + try: + scoped_rules = parse_dsl(dsl_text) + return [sr.rule for sr in scoped_rules] + except Exception: + return [] - logger.debug(f"DataGridFormattingManager created with id={self._id}") - - # === Helpers === - - def _get_all_presets(self) -> list: - """Returns builtin presets followed by user presets.""" - return list(DEFAULT_RULE_PRESETS.values()) + self._state.presets - - def _is_builtin(self, name: str) -> bool: - return name in DEFAULT_RULE_PRESETS - - def _get_selected_preset(self) -> Optional[RulePreset]: - if not self._state.selected_name: - return None - for p in self._get_all_presets(): - if p.name == self._state.selected_name: - return p - return None - - def _get_user_preset(self, name: str) -> Optional[RulePreset]: - for p in self._state.presets: - if p.name == name: - return p - return None - - def _parse_dsl_to_rules(self, dsl_text: str) -> list: - """Parse DSL text and extract FormatRule objects, ignoring scopes.""" - try: - scoped_rules = parse_dsl(dsl_text) - return [sr.rule for sr in scoped_rules] - except Exception: - return [] - - # === Command handlers === - - def on_new_preset(self): - self._state.ns_mode = "new" - return self.render() - - def on_save_preset(self): - if not self._state.selected_name or self._is_builtin(self._state.selected_name): - return self.render() - preset = self._get_user_preset(self._state.selected_name) - if preset is None: - return self.render() - dsl = self._editor.get_content() - preset.dsl = dsl - preset.rules = self._parse_dsl_to_rules(dsl) - logger.debug(f"Saved preset '{preset.name}' with {len(preset.rules)} rules") - return self.render() - - def on_rename_preset(self): - if not self._state.selected_name or self._is_builtin(self._state.selected_name): - return self.render() - self._state.ns_mode = "rename" - return self.render() - - def on_delete_preset(self): - if not self._state.selected_name or self._is_builtin(self._state.selected_name): - return self.render() - self._state.presets = [p for p in self._state.presets if p.name != self._state.selected_name] - self._state.selected_name = None - self._editor.set_content("") - self._editor.conf.readonly = False - logger.debug(f"Deleted preset '{self._state.selected_name}'") - return self.render() - - def on_select_preset(self, name: str): - preset = None - for p in self._get_all_presets(): - if p.name == name: - preset = p - break - if preset is None: - return self.render() - self._state.selected_name = name - self._state.ns_mode = "view" - self._editor.set_content(preset.dsl) - self._editor.conf.readonly = self._is_builtin(name) - logger.debug(f"Selected preset '{name}', readonly={self._editor.conf.readonly}") - return self.render() - - def on_confirm_new(self, client_response): - name = (client_response.get("name") or "").strip() - description = (client_response.get("description") or "").strip() - - if not name: - self._state.ns_mode = "view" - return self.render() - - all_names = {p.name for p in self._get_all_presets()} - if name in all_names: - logger.debug(f"Cannot create preset '{name}': name already exists") - self._state.ns_mode = "view" - return self.render() - - new_preset = RulePreset(name=name, description=description, rules=[], dsl="") - self._state.presets.append(new_preset) - self._state.selected_name = name - self._state.ns_mode = "view" - self._editor.set_content("") - self._editor.conf.readonly = False - logger.debug(f"Created preset '{name}'") - return self.render() - - def on_confirm_rename(self, client_response): - name = (client_response.get("name") or "").strip() - description = (client_response.get("description") or "").strip() - - preset = self._get_user_preset(self._state.selected_name) - if not name or preset is None: - self._state.ns_mode = "view" - return self.render() - - if name != preset.name: - all_names = {p.name for p in self._get_all_presets()} - if name in all_names: - logger.debug(f"Cannot rename to '{name}': name already exists") - self._state.ns_mode = "view" - return self.render() - - old_name = preset.name - preset.name = name - preset.description = description - self._state.selected_name = name - self._state.ns_mode = "view" - logger.debug(f"Renamed preset '{old_name}' → '{name}'") - return self.render() - - def on_cancel(self): + def _sync_provider(self): + """Sync all presets (builtin + user) into the session-scoped metadata provider.""" + provider = DatagridMetadataProvider(self) + provider.rule_presets = {p.name: p for p in self._get_all_presets()} + + # === Command handlers === + + def handle_new_preset(self): + self._state.ns_mode = "new" + return self.render() + + def handle_save_preset(self): + if not self._state.selected_name or self._is_builtin(self._state.selected_name): + return self.render() + preset = self._get_user_preset(self._state.selected_name) + if preset is None: + return self.render() + dsl = self._editor.get_content() + preset.dsl = dsl + preset.rules = self._parse_dsl_to_rules(dsl) + self._state.save() + self._sync_provider() + logger.debug(f"Saved preset '{preset.name}' with {len(preset.rules)} rules") + return self.render() + + def handle_rename_preset(self): + if not self._state.selected_name or self._is_builtin(self._state.selected_name): + return self.render() + self._state.ns_mode = "rename" + return self.render() + + def handle_delete_preset(self): + if not self._state.selected_name or self._is_builtin(self._state.selected_name): + return self.render() + deleted_name = self._state.selected_name + self._state.presets = [p for p in self._state.presets if p.name != deleted_name] + self._state.selected_name = None + self._editor.set_content("") + self._editor.conf.readonly = False + self._sync_provider() + logger.debug(f"Deleted preset '{deleted_name}'") + return self.render() + + def handle_select_preset(self, name: str): + preset = None + for p in self._get_all_presets(): + if p.name == name: + preset = p + break + if preset is None: + return None + self._state.selected_name = name + self._state.ns_mode = "view" + self._editor.set_content(preset.dsl) + self._editor.conf.readonly = self._is_builtin(name) + logger.debug(f"Selected preset '{name}', readonly={self._editor.conf.readonly}") + return self.render() # to also update the selected item in the search + + def handle_confirm_new(self, client_response): + name = (client_response.get("name") or "").strip() + description = (client_response.get("description") or "").strip() + + if not name: + self._state.ns_mode = "view" + return self.render() + + all_names = {p.name for p in self._get_all_presets()} + if name in all_names: + logger.debug(f"Cannot create preset '{name}': name already exists") + self._state.ns_mode = "view" + return self.render() + + new_preset = RulePreset(name=name, description=description, rules=[], dsl="") + self._state.presets.append(new_preset) + self._state.selected_name = name + self._state.ns_mode = "view" + self._editor.set_content("") + self._editor.conf.readonly = False + self._sync_provider() + logger.debug(f"Created preset '{name}'") + return self.render() + + def handle_confirm_rename(self, client_response): + name = (client_response.get("name") or "").strip() + description = (client_response.get("description") or "").strip() + + preset = self._get_user_preset(self._state.selected_name) + if not name or preset is None: + self._state.ns_mode = "view" + return self.render() + + if name != preset.name: + all_names = {p.name for p in self._get_all_presets()} + if name in all_names: + logger.debug(f"Cannot rename to '{name}': name already exists") self._state.ns_mode = "view" return self.render() - - # === Rendering === - - def _mk_preset_item(self, preset: RulePreset): - is_active = self._state.selected_name == preset.name - is_builtin = self._is_builtin(preset.name) - - badges = [] - if preset.has_formatter(): - badges.append(Span("format()", cls="badge badge-xs badge-secondary")) - if preset.has_style(): - badges.append(Span("style()", cls="badge badge-xs badge-primary")) - if is_builtin: - badges.append(Span("built-in", cls="badge badge-xs badge-ghost")) - - item_cls = "mf-fmgr-preset-item" - if is_active: - item_cls += " mf-fmgr-preset-item-active" - - select_cmd = Command( - "SelectPreset", - f"Select {preset.name}", - self, - self.on_select_preset, - args=[preset.name], - ).htmx(target=f"#{self._id}", swap="outerHTML") - - return mk.mk( - Div( - Div(preset.name, cls="mf-fmgr-preset-name"), - Div(preset.description, cls="mf-fmgr-preset-desc") if preset.description else None, - Div(*badges, cls="mf-fmgr-preset-badges") if badges else None, - cls=item_cls, - ), - command=select_cmd, - ) - - def _mk_preset_header(self, preset: RulePreset, is_builtin: bool): - badges = [] - if preset.has_formatter(): - badges.append(Span("format()", cls="badge badge-secondary")) - if preset.has_style(): - badges.append(Span("style()", cls="badge badge-primary")) - if is_builtin: - badges.append(Span("built-in", cls="badge badge-ghost")) - - return Div( - Div( - Div(preset.name, cls="text-sm font-semibold"), - Div(preset.description, cls="text-xs opacity-50") if preset.description else None, - ), - Div(*badges, cls="flex gap-1") if badges else None, - cls="mf-fmgr-editor-meta", - ) - - def _mk_editor_view(self): - preset = self._get_selected_preset() - if preset is None: - return Div("Select a preset to edit", cls="mf-fmgr-placeholder p-4 text-sm opacity-50") - is_builtin = self._is_builtin(preset.name) - return Div( - self._mk_preset_header(preset, is_builtin), - self._editor, - cls="mf-fmgr-editor-view", - ) - - def _mk_new_form(self): - return Div( - Form( - Fieldset( - Label("Name"), - Input(name="name", cls="input input-sm w-full"), - Label("Description"), - Input(name="description", cls="input input-sm w-full"), - legend="New preset", - cls="fieldset border-base-300 rounded-box p-4", - ), - mk.dialog_buttons( - on_ok=self.commands.confirm_new(), - on_cancel=self.commands.cancel(), - ), - ), - cls="mf-fmgr-form p-4", - ) - - def _mk_rename_form(self): - preset = self._get_selected_preset() - return Div( - Form( - Fieldset( - Label("Name"), - Input(name="name", value=preset.name if preset else "", cls="input input-sm w-full"), - Label("Description"), - Input(name="description", value=preset.description if preset else "", cls="input input-sm w-full"), - legend="Rename preset", - cls="fieldset border-base-300 rounded-box p-4", - ), - mk.dialog_buttons( - on_ok=self.commands.confirm_rename(), - on_cancel=self.commands.cancel(), - ), - ), - cls="mf-fmgr-form p-4", - ) - - def _mk_main_content(self): - if self._state.ns_mode == "new": - return self._mk_new_form() - elif self._state.ns_mode == "rename": - return self._mk_rename_form() - return self._mk_editor_view() - - def render(self): - self._search.set_items(self._get_all_presets()) - self._panel._main = self._mk_main_content() - self._panel._left = self._search - - return Div( - self._menu, - self._panel, - id=self._id, - cls="mf-formatting-manager", - ) - - def __ft__(self): - return self.render() + + old_name = preset.name + preset.name = name + preset.description = description + self._state.selected_name = name + self._state.ns_mode = "view" + self._sync_provider() + logger.debug(f"Renamed preset '{old_name}' → '{name}'") + return self.render() + + def handle_cancel(self): + self._state.ns_mode = "view" + return self.render() + + # === Rendering === + + def _get_badges(self, preset: RulePreset, is_builtin: bool): + badges = [] + if preset.has_formatter(): + badges.append(self._mk_badge("format")) + if preset.has_style(): + badges.append(self._mk_badge("style")) + if is_builtin: + badges.append(self._mk_badge("built-in")) + + return badges + + def _mk_badge(self, text: Literal["format", "style", "built-in"]): + if text == "built-in": + return Span("built-in", cls="badge badge-xs badge-ghost") + if text == "style": + return Span("style", cls="badge badge-xs badge-primary") + return Span("format", cls="badge badge-xs badge-secondary") + + def _mk_preset_item(self, preset: RulePreset): + is_active = self._state.selected_name == preset.name + is_builtin = self._is_builtin(preset.name) + badges = self._get_badges(preset, is_builtin) + + item_cls = "mf-fmgr-preset-item" + if is_active: + item_cls += " mf-fmgr-preset-item-active" + + return mk.mk( + Div( + Div(preset.name, cls="mf-fmgr-preset-name"), + Div(preset.description, cls="mf-fmgr-preset-desc") if preset.description else None, + Div(*badges, cls="mf-fmgr-preset-badges") if badges else None, + cls=item_cls, + ), + command=self.commands.select_preset(preset.name), + ) + + def _mk_preset_header(self, preset: RulePreset, is_builtin: bool): + badges = self._get_badges(preset, is_builtin) + + return Div( + Div( + Div(preset.name, cls="text-sm font-semibold"), + Div(preset.description, cls="text-xs opacity-50") if preset.description else None, + ), + Div(*badges, cls="flex gap-1") if badges else None, + cls="mf-fmgr-editor-meta mb-2", + ) + + def _mk_editor_view(self): + preset = self._get_selected_preset() + if preset is None: + return Div("Select a preset to edit", cls="mf-fmgr-placeholder p-4 text-sm opacity-50") + is_builtin = self._is_builtin(preset.name) + return Div( + self._mk_preset_header(preset, is_builtin), + self._editor, + cls="mf-fmgr-editor-view p-2", + ) + + def _mk_new_form(self): + return Div( + Form( + Fieldset( + Label("Name"), + Input(name="name", cls="input input-sm w-full"), + Label("Description"), + Input(name="description", cls="input input-sm w-full"), + legend="New preset", + cls="fieldset border-base-300 rounded-box p-2", + ), + mk.dialog_buttons( + on_ok=self.commands.confirm_new(), + on_cancel=self.commands.cancel(), + ), + ), + cls="mf-fmgr-form p-2", + ) + + def _mk_rename_form(self): + preset = self._get_selected_preset() + return Div( + Form( + Fieldset( + Label("Name"), + Input(name="name", value=preset.name if preset else "", cls="input input-sm w-full"), + Label("Description"), + Input(name="description", value=preset.description if preset else "", cls="input input-sm w-full"), + legend="Rename preset", + cls="fieldset border-base-300 rounded-box p-4", + ), + mk.dialog_buttons( + on_ok=self.commands.confirm_rename(), + on_cancel=self.commands.cancel(), + ), + ), + cls="mf-fmgr-form p-2", + ) + + def _mk_main_content(self): + if self._state.ns_mode == "new": + return self._mk_new_form() + elif self._state.ns_mode == "rename": + return self._mk_rename_form() + return self._mk_editor_view() + + def render(self): + self._search.set_items(self._get_all_presets()) + self._panel._main = self._mk_main_content() + self._panel._left = self._search + + return Div( + self._menu, + self._panel, + id=self._id, + cls="mf-formatting-manager", + ) + + def __ft__(self): + return self.render() diff --git a/src/myfasthtml/controls/Menu.py b/src/myfasthtml/controls/Menu.py index 9b99108..5d2ae5f 100644 --- a/src/myfasthtml/controls/Menu.py +++ b/src/myfasthtml/controls/Menu.py @@ -1,3 +1,4 @@ +import inspect from dataclasses import dataclass, field from typing import Optional @@ -35,7 +36,8 @@ class Menu(MultipleInstance): name for name in dir(commands_obj) if not name.startswith("_") - and callable(getattr(commands_obj, name)) + and callable(attr := getattr(commands_obj, name)) + and len(inspect.signature(attr).parameters) == 0 ] return { @@ -59,7 +61,7 @@ class Menu(MultipleInstance): Div("|"), *[self._mk_menu(command_name) for command_name in self._state.last_used[:3]] ) if self._state.last_used else [], - cls="flex" + cls="flex mb-1" ), id=self._id ) diff --git a/src/myfasthtml/controls/Panel.py b/src/myfasthtml/controls/Panel.py index 4207572..7b64045 100644 --- a/src/myfasthtml/controls/Panel.py +++ b/src/myfasthtml/controls/Panel.py @@ -198,7 +198,7 @@ class Panel(MultipleInstance): body = Div( header, Div(content, id=self._ids.content(side), cls="mf-panel-content"), - cls="mf-panel-body" + cls=f"mf-panel-body mf-panel-body-{side}" ) if side == "left": return Div(