flask-construct-ui 0.12.0__tar.gz → 0.12.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/PKG-INFO +1 -1
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/base.py +11 -1
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/components/pivot.py +38 -1
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/contrast.py +34 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/l10n/en/main.flt +2 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/l10n/sv/main.flt +2 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/menu.py +143 -19
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/rcrud.py +26 -1
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/static/css/whitelabel.css +81 -22
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/static/designs/sandstone.css +3 -6
- flask_construct_ui-0.12.1/flask_construct_ui/static/js/construct-core.min.js +2 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/components/pivot_form.html +13 -6
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/style/_menu.html +18 -8
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui.egg-info/PKG-INFO +1 -1
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/pyproject.toml +2 -2
- flask_construct_ui-0.12.0/flask_construct_ui/static/js/construct-core.min.js +0 -2
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/LICENSE +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/README.md +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/__init__.py +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/app_hooks.py +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/auth.py +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/capabilities.py +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/cli.py +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/components/__init__.py +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/components/calendar.py +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/components/chart.py +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/components/chart_native.py +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/components/content.py +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/components/core.py +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/components/grid.py +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/components/kanban.py +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/components/layout.py +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/components/live.py +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/components/period.py +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/crud.py +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/crud_forms.py +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/csrf.py +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/data.py +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/data_sqlalchemy.py +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/dragdrop.py +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/errors.py +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/flashes.py +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/forms.py +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/grid.py +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/headless.py +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/icons.py +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/language.py +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/layout.py +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/live.py +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/navbar.py +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/navigation.py +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/orgmode.py +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/pagination.py +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/py.typed +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/resume.py +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/sanitize.py +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/schema.py +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/static/css/grid.css +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/static/css/prosemirror-bundle.min.css +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/static/css/unpoly.min.css +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/static/designs/adminlte.css +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/static/fonts/bootstrap-icons/LICENSE.txt +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/static/fonts/bootstrap-icons/bootstrap-icons.min.css +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/static/fonts/bootstrap-icons/fonts/bootstrap-icons.woff +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/static/fonts/bootstrap-icons/fonts/bootstrap-icons.woff2 +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/static/fonts/material-symbols/LICENSE.txt +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/static/fonts/material-symbols/material-symbols-outlined.woff2 +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/static/fonts/material-symbols/material-symbols.css +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/static/fonts/material-symbols/names.txt +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/static/js/construct-grid.min.js +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/static/js/kanban-dnd.js +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/static/js/prosemirror-bundle.min.js +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/static/js/unpoly.min.js +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/components/_form_field.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/components/button.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/components/calendar.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/components/calendarmini.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/components/card.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/components/chart.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/components/chart_native.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/components/form.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/components/grid.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/components/grid_layout.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/components/grid_notice.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/components/horizontal_layout.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/components/kanban.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/components/live.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/components/live_refresh.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/components/main.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/components/markup.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/components/period_nav.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/components/rich_text.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/components/table.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/components/tabs.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/components/text.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/components/vertical_layout.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/crud/_macros.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/crud/_many_related_options.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/crud/_related_options.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/crud/create.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/crud/edit.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/crud/listindex.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/crud/related_crud_tab_form.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/crud/related_crud_tab_list.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/crud/tab_view.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/crud/view.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/errors/error.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/errors/forbidden.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/navbar/notifications.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/navbar/search_results.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/structure/base.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/structure/public.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/style/_flashes.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/style/_footer.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/style/base_full.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/templates/style/base_partial.html +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/testing.py +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/urls.py +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/validation.py +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui.egg-info/SOURCES.txt +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui.egg-info/dependency_links.txt +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui.egg-info/requires.txt +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui.egg-info/top_level.txt +0 -0
- {flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/setup.cfg +0 -0
|
@@ -1131,6 +1131,7 @@ class FlaskConstructUI(MenuMixin):
|
|
|
1131
1131
|
groups=None,
|
|
1132
1132
|
visible=None,
|
|
1133
1133
|
weight=None,
|
|
1134
|
+
match=None,
|
|
1134
1135
|
):
|
|
1135
1136
|
"""Register a page and add it to the navigation menu.
|
|
1136
1137
|
|
|
@@ -1152,13 +1153,22 @@ class FlaskConstructUI(MenuMixin):
|
|
|
1152
1153
|
``weight`` orders the entry among its siblings (default 0, ties keep
|
|
1153
1154
|
registration order), which is what lets a provider place entries around
|
|
1154
1155
|
statically registered pages.
|
|
1156
|
+
|
|
1157
|
+
``match`` names other URLs the entry stands for, so it is shown as
|
|
1158
|
+
current there too: a pattern string (``"/user/diary/*"``; ``*`` is
|
|
1159
|
+
anything, ``:name`` one path segment), a list of them, or a callable
|
|
1160
|
+
returning either per request — for an entry that redirects somewhere
|
|
1161
|
+
that depends on who is asking. The paths below the entry's own link
|
|
1162
|
+
always count; see :attr:`menu_current`.
|
|
1155
1163
|
"""
|
|
1156
1164
|
self._pages.append(page)
|
|
1157
1165
|
self._blueprint_names.add(page.endpoint)
|
|
1158
1166
|
self._pages_by_endpoint[page.endpoint] = page
|
|
1159
1167
|
if self.app is not None:
|
|
1160
1168
|
self._register_page_blueprint(page)
|
|
1161
|
-
self._add_page_menu_entry(
|
|
1169
|
+
self._add_page_menu_entry(
|
|
1170
|
+
page, submenu, submenu_icon, groups, visible, weight, match
|
|
1171
|
+
)
|
|
1162
1172
|
|
|
1163
1173
|
def _apply_l10n_paths(self):
|
|
1164
1174
|
"""(Re)build the translator's search path.
|
{flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/components/pivot.py
RENAMED
|
@@ -37,6 +37,11 @@ from .core import Component, _t, jinja_env
|
|
|
37
37
|
#: Tones a cell or column may carry; each maps to ``fcu-pivot--<tone>``.
|
|
38
38
|
TONES = ("success", "warning", "danger", "info", "muted")
|
|
39
39
|
|
|
40
|
+
#: What ``unsaved=`` may ask ``core.js`` to do while boxes differ from what
|
|
41
|
+
#: the server rendered: mark the boxes and totals, say so beside Save, warn
|
|
42
|
+
#: before the page or the pivot's fragment is left.
|
|
43
|
+
UNSAVED = ("mark", "status", "warn")
|
|
44
|
+
|
|
40
45
|
# Separates the name from the keys and the keys from each other. Keys are
|
|
41
46
|
# percent-encoded with nothing kept safe, so neither can contain it.
|
|
42
47
|
_SEP = ":"
|
|
@@ -192,7 +197,18 @@ class PivotFormComponent(Component):
|
|
|
192
197
|
or a :class:`PivotCell` whose ``tone`` / ``css_class`` / ``title`` style
|
|
193
198
|
that total; ``grand_total`` may be a zero-argument callable).
|
|
194
199
|
``total_extra`` is a slot under the totals heading, ``footer_label``
|
|
195
|
-
heads the totals row (default: ``total_label``).
|
|
200
|
+
heads the totals row (default: ``total_label``). The built-in sums
|
|
201
|
+
follow typing in the browser (``core.js``); an app total changes only
|
|
202
|
+
when the server renders it again. ``live_totals=False`` keeps every
|
|
203
|
+
total as rendered.
|
|
204
|
+
* ``unsaved`` picks how the page shows typing that is not saved yet, any
|
|
205
|
+
of :data:`UNSAVED` (``()`` shows nothing): ``"mark"`` dashes the
|
|
206
|
+
changed boxes and the totals they move, ``"status"`` adds a live
|
|
207
|
+
"Unsaved changes" line beside Save (``unsaved_label``), ``"warn"``
|
|
208
|
+
asks before leaving the page or replacing the pivot. Whatever is
|
|
209
|
+
chosen, the form carries ``data-fcu-unsaved`` while anything is
|
|
210
|
+
unsaved and fires ``fcu:pivot-unsaved`` as that flips, for an app
|
|
211
|
+
that handles it itself.
|
|
196
212
|
* ``controls`` render inside the form under the table (a row picker,
|
|
197
213
|
extra submit buttons with ``formaction``), after the Save button, which
|
|
198
214
|
stays the form's first submit button so Enter always means save.
|
|
@@ -214,6 +230,9 @@ class PivotFormComponent(Component):
|
|
|
214
230
|
total_label: Optional[str] = None
|
|
215
231
|
total_extra: Any = None
|
|
216
232
|
footer_label: Optional[str] = None
|
|
233
|
+
live_totals: bool = True
|
|
234
|
+
unsaved: Any = ()
|
|
235
|
+
unsaved_label: Optional[str] = None
|
|
217
236
|
controls: List[Any] = field(default_factory=list)
|
|
218
237
|
save: bool = True
|
|
219
238
|
save_label: Optional[str] = None
|
|
@@ -223,6 +242,12 @@ class PivotFormComponent(Component):
|
|
|
223
242
|
|
|
224
243
|
def __post_init__(self):
|
|
225
244
|
_check_name(self.name, "name")
|
|
245
|
+
if isinstance(self.unsaved, str):
|
|
246
|
+
self.unsaved = (self.unsaved,)
|
|
247
|
+
self.unsaved = tuple(self.unsaved)
|
|
248
|
+
for mode in self.unsaved:
|
|
249
|
+
if mode not in UNSAVED:
|
|
250
|
+
raise ValueError(f"PivotForm unsaved {mode!r} is not one of {UNSAVED}")
|
|
226
251
|
for tone in [c.tone for c in self.columns]:
|
|
227
252
|
if tone is not None and tone not in TONES:
|
|
228
253
|
raise ValueError(f"PivotColumn tone {tone!r} is not one of {TONES}")
|
|
@@ -352,6 +377,13 @@ class PivotFormComponent(Component):
|
|
|
352
377
|
row_totals=row_totals,
|
|
353
378
|
column_totals=column_totals,
|
|
354
379
|
grand=grand,
|
|
380
|
+
# Which totals are the built-in sums, which core.js keeps live
|
|
381
|
+
# while typing; an app total is left as rendered.
|
|
382
|
+
live={
|
|
383
|
+
"row": self.live_totals and self.row_total is None,
|
|
384
|
+
"column": self.live_totals and self.column_total is None,
|
|
385
|
+
"grand": self.live_totals and self.grand_total is None,
|
|
386
|
+
},
|
|
355
387
|
input_name=lambda row, column: self.input_name(
|
|
356
388
|
self.name, row.key, column.key
|
|
357
389
|
),
|
|
@@ -360,6 +392,11 @@ class PivotFormComponent(Component):
|
|
|
360
392
|
"total": total_label,
|
|
361
393
|
"footer": self.footer_label or total_label,
|
|
362
394
|
"save": self.save_label or _t("pivot-save", default="Save"),
|
|
395
|
+
"unsaved": self.unsaved_label
|
|
396
|
+
or _t("pivot-unsaved", default="Unsaved changes"),
|
|
397
|
+
"leave": _t(
|
|
398
|
+
"pivot-unsaved-leave", default="Leave without saving your changes?"
|
|
399
|
+
),
|
|
363
400
|
},
|
|
364
401
|
)
|
|
365
402
|
|
|
@@ -294,6 +294,40 @@ PAIRS = [
|
|
|
294
294
|
("input border on info tint", "--fcu-border-input", "--fcu-alert-info-bg", UI),
|
|
295
295
|
("pivot row heading", "--fcu-text", "--fcu-pivot-rowhead-bg", TEXT),
|
|
296
296
|
("pivot row heading link", "--fcu-link", "--fcu-pivot-rowhead-bg", TEXT),
|
|
297
|
+
# PivotForm unsaved="mark" / "status": a changed box's border and the
|
|
298
|
+
# "Unsaved changes" line; muted (weekend) columns sit on the alt surface.
|
|
299
|
+
("pivot unsaved box border", "--fcu-pivot-unsaved-color", "--fcu-surface", UI),
|
|
300
|
+
(
|
|
301
|
+
"pivot unsaved box border on alt surface",
|
|
302
|
+
"--fcu-pivot-unsaved-color",
|
|
303
|
+
"--fcu-surface-alt",
|
|
304
|
+
UI,
|
|
305
|
+
),
|
|
306
|
+
("pivot unsaved status", "--fcu-pivot-unsaved-text", "--fcu-surface", TEXT),
|
|
307
|
+
(
|
|
308
|
+
"pivot unsaved box border on success tint",
|
|
309
|
+
"--fcu-pivot-unsaved-color",
|
|
310
|
+
"--fcu-alert-success-bg",
|
|
311
|
+
UI,
|
|
312
|
+
),
|
|
313
|
+
(
|
|
314
|
+
"pivot unsaved box border on warning tint",
|
|
315
|
+
"--fcu-pivot-unsaved-color",
|
|
316
|
+
"--fcu-alert-warning-bg",
|
|
317
|
+
UI,
|
|
318
|
+
),
|
|
319
|
+
(
|
|
320
|
+
"pivot unsaved box border on danger tint",
|
|
321
|
+
"--fcu-pivot-unsaved-color",
|
|
322
|
+
"--fcu-alert-danger-bg",
|
|
323
|
+
UI,
|
|
324
|
+
),
|
|
325
|
+
(
|
|
326
|
+
"pivot unsaved box border on info tint",
|
|
327
|
+
"--fcu-pivot-unsaved-color",
|
|
328
|
+
"--fcu-alert-info-bg",
|
|
329
|
+
UI,
|
|
330
|
+
),
|
|
297
331
|
# Grid: the selection ring is the only indicator of the keyboard cursor,
|
|
298
332
|
# and the error border doubles as the grid's error text colour.
|
|
299
333
|
("grid selection ring", "--fcu-grid-selected-border", "--fcu-surface", UI),
|
|
@@ -10,6 +10,9 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
import hashlib
|
|
12
12
|
import json
|
|
13
|
+
import re
|
|
14
|
+
from functools import lru_cache
|
|
15
|
+
from urllib.parse import urlparse
|
|
13
16
|
|
|
14
17
|
from flask import g, has_request_context, request
|
|
15
18
|
|
|
@@ -55,12 +58,63 @@ def _menu_fingerprint(items):
|
|
|
55
58
|
entry.get("title"),
|
|
56
59
|
entry.get("link"),
|
|
57
60
|
entry.get("icon"),
|
|
61
|
+
entry.get("match"),
|
|
58
62
|
_menu_fingerprint(entry.get("subitems", [])),
|
|
59
63
|
]
|
|
60
64
|
for entry in items
|
|
61
65
|
]
|
|
62
66
|
|
|
63
67
|
|
|
68
|
+
# --- Which entry is current -------------------------------------------------
|
|
69
|
+
#
|
|
70
|
+
# An entry matches its own link, everything below it (``link/*``) and any
|
|
71
|
+
# ``match`` patterns the app gave it. Of the entries matching a path, the one
|
|
72
|
+
# with the most specific pattern is current: an exact pattern beats a wildcard,
|
|
73
|
+
# then the longer literal prefix wins, then menu order. construct-core.js
|
|
74
|
+
# implements the same rule (`currentMenuLink`) for Unpoly navigations, which
|
|
75
|
+
# never redraw the sidebar; keep the two in step.
|
|
76
|
+
#
|
|
77
|
+
# Pattern syntax, shared with the client: ``*`` matches anything (slashes
|
|
78
|
+
# included), ``:name`` matches one path segment, everything else is literal.
|
|
79
|
+
# Paths and patterns are compared without trailing slashes.
|
|
80
|
+
|
|
81
|
+
_PATTERN_TOKEN = re.compile(r"(\*|:[A-Za-z][\w-]*)")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _normalize_path(path):
|
|
85
|
+
"""``path`` without query, fragment or trailing slashes ("/" stays)."""
|
|
86
|
+
path = urlparse(path).path
|
|
87
|
+
return path.rstrip("/") or "/"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@lru_cache(maxsize=512)
|
|
91
|
+
def _pattern_regex(pattern):
|
|
92
|
+
parts = []
|
|
93
|
+
for part in _PATTERN_TOKEN.split(pattern):
|
|
94
|
+
if part == "*":
|
|
95
|
+
parts.append(".*")
|
|
96
|
+
elif part.startswith(":") and len(part) > 1:
|
|
97
|
+
parts.append("[^/]+")
|
|
98
|
+
else:
|
|
99
|
+
parts.append(re.escape(part))
|
|
100
|
+
return re.compile("^" + "".join(parts) + "$")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _pattern_rank(pattern):
|
|
104
|
+
"""How specific ``pattern`` is: (exact, length of its literal prefix)."""
|
|
105
|
+
literal = _PATTERN_TOKEN.split(pattern, maxsplit=1)[0]
|
|
106
|
+
return (len(literal) == len(pattern), len(literal))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _link_entries(items):
|
|
110
|
+
"""Every entry in ``items`` that links somewhere, submenus flattened."""
|
|
111
|
+
for entry in items:
|
|
112
|
+
if entry.get("type") == "submenu":
|
|
113
|
+
yield from _link_entries(entry.get("subitems", []))
|
|
114
|
+
elif entry.get("link"):
|
|
115
|
+
yield entry
|
|
116
|
+
|
|
117
|
+
|
|
64
118
|
class MenuMixin:
|
|
65
119
|
"""Menu state and behaviour of :class:`~.base.FlaskConstructUI`."""
|
|
66
120
|
|
|
@@ -80,7 +134,7 @@ class MenuMixin:
|
|
|
80
134
|
self._menu_poll = poll
|
|
81
135
|
|
|
82
136
|
def _add_page_menu_entry(
|
|
83
|
-
self, page, submenu, submenu_icon, groups, visible, weight
|
|
137
|
+
self, page, submenu, submenu_icon, groups, visible, weight, match=None
|
|
84
138
|
):
|
|
85
139
|
"""Add ``page``'s menu entry; see :meth:`FlaskConstructUI.add_page`."""
|
|
86
140
|
if not hasattr(page, "menu") or page.menu is False:
|
|
@@ -106,6 +160,14 @@ class MenuMixin:
|
|
|
106
160
|
self._menu_predicates = True
|
|
107
161
|
if weight is not None:
|
|
108
162
|
menu["weight"] = weight
|
|
163
|
+
if match is not None:
|
|
164
|
+
menu["match"] = match
|
|
165
|
+
if callable(menu.get("match")):
|
|
166
|
+
# Like `visible`: it answers to the request, so the menu it renders
|
|
167
|
+
# (the patterns are on the links) can change on nothing a stamp
|
|
168
|
+
# key or the auth stamp describes.
|
|
169
|
+
self._menu_gated = True
|
|
170
|
+
self._menu_predicates = True
|
|
109
171
|
if menu.get("groups") or menu.get("visible") is not None:
|
|
110
172
|
self._menu_gated = True
|
|
111
173
|
if submenu is not None:
|
|
@@ -144,14 +206,16 @@ class MenuMixin:
|
|
|
144
206
|
self._menu_providers.append((provider, key))
|
|
145
207
|
return provider
|
|
146
208
|
|
|
147
|
-
def menu_provider(self, key=None):
|
|
209
|
+
def menu_provider(self, provider=None, *, key=None):
|
|
148
210
|
"""Decorator form of :meth:`add_menu_provider`.
|
|
149
211
|
|
|
150
212
|
Usable bare (``@construct.menu_provider``) or called with a key
|
|
151
|
-
(``@construct.menu_provider(key=lambda: feed.version)``).
|
|
213
|
+
(``@construct.menu_provider(key=lambda: feed.version)``). The bare
|
|
214
|
+
form arrives as ``provider``: a key is callable too, so telling the
|
|
215
|
+
two apart by "is it callable" registered the key as the provider.
|
|
152
216
|
"""
|
|
153
|
-
if
|
|
154
|
-
return self.add_menu_provider(
|
|
217
|
+
if provider is not None:
|
|
218
|
+
return self.add_menu_provider(provider)
|
|
155
219
|
|
|
156
220
|
def wrap(fn):
|
|
157
221
|
return self.add_menu_provider(fn, key=key)
|
|
@@ -207,9 +271,77 @@ class MenuMixin:
|
|
|
207
271
|
self._place_entry(items, dict(entry))
|
|
208
272
|
items = self._visible_entries(items)
|
|
209
273
|
_sort_menu(items)
|
|
274
|
+
for entry in _link_entries(items):
|
|
275
|
+
# Resolved once per request, so a callable runs once however often
|
|
276
|
+
# the patterns are read, and the fingerprint sees its result.
|
|
277
|
+
entry["match"] = self._extra_patterns(entry)
|
|
210
278
|
g._construct_menu = items
|
|
211
279
|
return items
|
|
212
280
|
|
|
281
|
+
def _extra_patterns(self, entry):
|
|
282
|
+
"""``entry``'s own ``match`` patterns, as a list of absolute patterns.
|
|
283
|
+
|
|
284
|
+
``match`` is a string (whitespace-separated patterns), a list of them,
|
|
285
|
+
or a zero-argument callable returning either, run per request. A
|
|
286
|
+
pattern not starting with "/" is relative to the construct URL, like a
|
|
287
|
+
page's link.
|
|
288
|
+
"""
|
|
289
|
+
match = entry.get("match")
|
|
290
|
+
if callable(match):
|
|
291
|
+
match = match()
|
|
292
|
+
if not match:
|
|
293
|
+
return []
|
|
294
|
+
if isinstance(match, str):
|
|
295
|
+
match = match.split()
|
|
296
|
+
patterns = []
|
|
297
|
+
for pattern in match:
|
|
298
|
+
if not pattern.startswith("/"):
|
|
299
|
+
pattern = self.url.rstrip("/") + "/" + pattern
|
|
300
|
+
patterns.append(_normalize_path(pattern))
|
|
301
|
+
return patterns
|
|
302
|
+
|
|
303
|
+
def menu_patterns(self, entry):
|
|
304
|
+
"""Every pattern that makes ``entry`` current: its link, the paths
|
|
305
|
+
below it, and its ``match`` patterns.
|
|
306
|
+
|
|
307
|
+
The link at the root (of the site or of the construct URL) does not
|
|
308
|
+
claim the paths below it — that would be every page.
|
|
309
|
+
"""
|
|
310
|
+
link = entry.get("link")
|
|
311
|
+
if not link:
|
|
312
|
+
return []
|
|
313
|
+
base = _normalize_path(link)
|
|
314
|
+
patterns = [base]
|
|
315
|
+
if base not in ("/", _normalize_path(self.url)):
|
|
316
|
+
patterns.append(base + "/*")
|
|
317
|
+
patterns.extend(self._extra_patterns(entry))
|
|
318
|
+
return patterns
|
|
319
|
+
|
|
320
|
+
def _current_entry(self, items, path):
|
|
321
|
+
"""The entry in ``items`` that ``path`` belongs to, or None."""
|
|
322
|
+
path = _normalize_path(path)
|
|
323
|
+
best, best_rank = None, None
|
|
324
|
+
for entry in _link_entries(items):
|
|
325
|
+
for pattern in self.menu_patterns(entry):
|
|
326
|
+
if not _pattern_regex(pattern).match(path):
|
|
327
|
+
continue
|
|
328
|
+
rank = _pattern_rank(pattern)
|
|
329
|
+
if best_rank is None or rank > best_rank:
|
|
330
|
+
best, best_rank = entry, rank
|
|
331
|
+
return best
|
|
332
|
+
|
|
333
|
+
@property
|
|
334
|
+
def menu_current(self):
|
|
335
|
+
"""The entry of :attr:`menu` the current request belongs to, or None.
|
|
336
|
+
|
|
337
|
+
See the matching rule above ``_normalize_path``. Fragment endpoints get
|
|
338
|
+
None for free (nothing matches ``/_construct/menu``); the client marks
|
|
339
|
+
a reloaded menu against the browser's location instead.
|
|
340
|
+
"""
|
|
341
|
+
if not has_request_context():
|
|
342
|
+
return None
|
|
343
|
+
return self._current_entry(self.menu, request.path)
|
|
344
|
+
|
|
213
345
|
def _place_entry(self, items, entry):
|
|
214
346
|
"""Insert a provider-supplied ``entry`` into the working menu."""
|
|
215
347
|
entry.setdefault("type", "item")
|
|
@@ -330,10 +462,11 @@ class MenuMixin:
|
|
|
330
462
|
|
|
331
463
|
@property
|
|
332
464
|
def titleFromLink(self):
|
|
333
|
-
"""Title of the menu entry
|
|
465
|
+
"""Title of the menu entry the current route belongs to, or "".
|
|
334
466
|
|
|
335
|
-
|
|
336
|
-
|
|
467
|
+
Uses the same rule as the sidebar's current link (:attr:`menu_current`),
|
|
468
|
+
so a record's edit view is named after its list's entry and a page an
|
|
469
|
+
entry claims through ``match`` after that entry. (This used to read
|
|
337
470
|
a ``self.page`` attribute that has never existed on the extension, so
|
|
338
471
|
it raised on every call — harmless while nothing used it, and the cause
|
|
339
472
|
of an empty ``<title>`` once ``page_title`` started depending on it.)
|
|
@@ -347,17 +480,8 @@ class MenuMixin:
|
|
|
347
480
|
"""
|
|
348
481
|
if not has_request_context():
|
|
349
482
|
return ""
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
if item.get("type") == "submenu":
|
|
353
|
-
for subitem in item.get("subitems", []):
|
|
354
|
-
link = subitem.get("link")
|
|
355
|
-
if link and link.rstrip("/") == here:
|
|
356
|
-
return subitem["title"]
|
|
357
|
-
link = item.get("link")
|
|
358
|
-
if link and link.rstrip("/") == here:
|
|
359
|
-
return item["title"]
|
|
360
|
-
return ""
|
|
483
|
+
entry = self._current_entry(self._menu, request.path)
|
|
484
|
+
return entry["title"] if entry else ""
|
|
361
485
|
|
|
362
486
|
def _add_menu_stamp(self, response):
|
|
363
487
|
# Same problem as the auth stamp, one level down: the sidebar is
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
from functools import cached_property
|
|
5
5
|
from math import ceil
|
|
6
6
|
from flask import (
|
|
7
|
+
abort,
|
|
7
8
|
current_app,
|
|
8
9
|
flash,
|
|
9
10
|
g,
|
|
@@ -210,6 +211,25 @@ class RelatedCrudTab:
|
|
|
210
211
|
return verdict
|
|
211
212
|
return rule(page, action, row, parent)
|
|
212
213
|
|
|
214
|
+
def can_list(self, construct) -> bool | str:
|
|
215
|
+
"""Whether the tab's list may be shown under the current parent.
|
|
216
|
+
|
|
217
|
+
The list is part of the parent's page, so it follows the parent
|
|
218
|
+
page's own access — its ``can_edit`` or ``can_view`` for the parent —
|
|
219
|
+
whatever ``capabilities`` says about the children: a reader the
|
|
220
|
+
parent page turns away must not get the children by asking the tab
|
|
221
|
+
route directly. A denial returns the parent's ``can_edit`` answer, so
|
|
222
|
+
its message reaches the 403 page.
|
|
223
|
+
"""
|
|
224
|
+
parent = self.parent_record()
|
|
225
|
+
if parent is None:
|
|
226
|
+
return False
|
|
227
|
+
page = self._parent_view
|
|
228
|
+
verdict = _capability(page, "edit", parent)
|
|
229
|
+
if verdict is True or _capability(page, "view", parent) is True:
|
|
230
|
+
return True
|
|
231
|
+
return verdict
|
|
232
|
+
|
|
213
233
|
def can_add(self, construct) -> bool:
|
|
214
234
|
return self._child_capability("add", construct)
|
|
215
235
|
|
|
@@ -229,7 +249,7 @@ class RelatedCrudTab:
|
|
|
229
249
|
overriding one of those changes the controls and the routes alike.
|
|
230
250
|
"""
|
|
231
251
|
hook = getattr(self, f"can_{action}")
|
|
232
|
-
if action
|
|
252
|
+
if action in ("add", "list"):
|
|
233
253
|
return hook(construct) is True
|
|
234
254
|
return hook(row, construct) is True
|
|
235
255
|
|
|
@@ -373,6 +393,11 @@ class RelatedCrudTab:
|
|
|
373
393
|
return ds, rec
|
|
374
394
|
|
|
375
395
|
def list_view(id):
|
|
396
|
+
if rct.parent_record() is None:
|
|
397
|
+
abort(404)
|
|
398
|
+
allowed = rct.can_list(construct)
|
|
399
|
+
if allowed is not True:
|
|
400
|
+
return forbidden(parent_view, allowed)
|
|
376
401
|
ds = child_dataclass()
|
|
377
402
|
page, per_page, limit = get_pagination(
|
|
378
403
|
getattr(rct.child_crud_class, "page_size", None)
|
{flask_construct_ui-0.12.0 → flask_construct_ui-0.12.1}/flask_construct_ui/static/css/whitelabel.css
RENAMED
|
@@ -159,6 +159,8 @@
|
|
|
159
159
|
--fcu-pivot-input-width: 5em;
|
|
160
160
|
--fcu-pivot-rowhead-bg: var(--fcu-surface);
|
|
161
161
|
--fcu-pivot-total-weight: var(--fcu-font-weight-semibold);
|
|
162
|
+
--fcu-pivot-unsaved-color: var(--fcu-alert-warning-text);
|
|
163
|
+
--fcu-pivot-unsaved-text: var(--fcu-alert-warning-text);
|
|
162
164
|
|
|
163
165
|
/* Value badges: Set members and ManyRelated items in lists and read-only
|
|
164
166
|
views (.fcu-badge). Their ink is in contrast.py's PAIRS. */
|
|
@@ -213,6 +215,30 @@
|
|
|
213
215
|
--fcu-sidebar-active-bg: var(--fcu-primary);
|
|
214
216
|
--fcu-sidebar-active-text: var(--fcu-on-primary);
|
|
215
217
|
--fcu-sidebar-focus: var(--fcu-focus);
|
|
218
|
+
/* The current page's mark beyond its colours: a box-shadow (so a bar costs
|
|
219
|
+
no layout, e.g. `inset -3px 0 0 var(--fcu-primary)`), the link's corner
|
|
220
|
+
radius and its weight. */
|
|
221
|
+
--fcu-sidebar-active-indicator: none;
|
|
222
|
+
--fcu-sidebar-active-radius: var(--fcu-menu-link-radius);
|
|
223
|
+
--fcu-sidebar-active-weight: var(--fcu-font-weight-medium);
|
|
224
|
+
|
|
225
|
+
/* Main menu geometry, so a design can make the sidebar denser, rounder or
|
|
226
|
+
plainer without moving the spacing scale every other control uses. A
|
|
227
|
+
submenu's toggle takes the top-level link metrics. */
|
|
228
|
+
--fcu-sidebar-padding: var(--fcu-space-xl);
|
|
229
|
+
--fcu-menu-gap: var(--fcu-space-2xs);
|
|
230
|
+
--fcu-menu-link-padding: var(--fcu-space-md) var(--fcu-space-lg);
|
|
231
|
+
--fcu-menu-link-gap: var(--fcu-space-lg);
|
|
232
|
+
--fcu-menu-link-radius: var(--fcu-radius-control);
|
|
233
|
+
--fcu-submenu-indent: 20px;
|
|
234
|
+
--fcu-submenu-gap: var(--fcu-space-3xs);
|
|
235
|
+
--fcu-submenu-link-padding: var(--fcu-space-sm) var(--fcu-space-lg);
|
|
236
|
+
--fcu-submenu-link-gap: var(--fcu-space-md);
|
|
237
|
+
--fcu-menu-heading-size: 11px;
|
|
238
|
+
--fcu-menu-heading-weight: var(--fcu-font-weight-bold);
|
|
239
|
+
--fcu-menu-heading-transform: uppercase;
|
|
240
|
+
--fcu-menu-heading-tracking: 0.05em;
|
|
241
|
+
--fcu-menu-heading-padding: var(--fcu-space-lg) var(--fcu-space-sm) var(--fcu-space-2xs);
|
|
216
242
|
|
|
217
243
|
/* Data grid (grid.css). Derived from the semantic tokens rather than a
|
|
218
244
|
fixed blue, so the grid takes the design's primary and follows dark mode
|
|
@@ -573,7 +599,7 @@ a:hover {
|
|
|
573
599
|
background-color: var(--fcu-sidebar-bg);
|
|
574
600
|
border-right: 1px solid var(--fcu-sidebar-border);
|
|
575
601
|
color: var(--fcu-sidebar-text);
|
|
576
|
-
padding: var(--fcu-
|
|
602
|
+
padding: var(--fcu-sidebar-padding);
|
|
577
603
|
box-sizing: border-box;
|
|
578
604
|
}
|
|
579
605
|
|
|
@@ -812,15 +838,15 @@ a:hover {
|
|
|
812
838
|
margin: 0;
|
|
813
839
|
display: flex;
|
|
814
840
|
flex-direction: column;
|
|
815
|
-
gap: var(--fcu-
|
|
841
|
+
gap: var(--fcu-menu-gap);
|
|
816
842
|
}
|
|
817
843
|
.fcu-menu-header {
|
|
818
|
-
font-size:
|
|
819
|
-
font-weight: var(--fcu-
|
|
820
|
-
text-transform:
|
|
844
|
+
font-size: var(--fcu-menu-heading-size);
|
|
845
|
+
font-weight: var(--fcu-menu-heading-weight);
|
|
846
|
+
text-transform: var(--fcu-menu-heading-transform);
|
|
821
847
|
color: var(--fcu-sidebar-text-muted);
|
|
822
|
-
letter-spacing:
|
|
823
|
-
padding: var(--fcu-
|
|
848
|
+
letter-spacing: var(--fcu-menu-heading-tracking);
|
|
849
|
+
padding: var(--fcu-menu-heading-padding);
|
|
824
850
|
/* The sidebar group label is an <h2> for semantics; reset the UA heading
|
|
825
851
|
margins so it keeps the flat look the class was written for. */
|
|
826
852
|
margin: 0;
|
|
@@ -1001,13 +1027,13 @@ fieldset.fcu-form-section__body {
|
|
|
1001
1027
|
.fcu-menu-link {
|
|
1002
1028
|
display: flex;
|
|
1003
1029
|
align-items: center;
|
|
1004
|
-
padding: var(--fcu-
|
|
1030
|
+
padding: var(--fcu-menu-link-padding);
|
|
1005
1031
|
text-decoration: none;
|
|
1006
1032
|
color: var(--fcu-sidebar-text);
|
|
1007
|
-
border-radius: var(--fcu-radius
|
|
1033
|
+
border-radius: var(--fcu-menu-link-radius);
|
|
1008
1034
|
transition: background-color var(--fcu-duration) var(--fcu-easing);
|
|
1009
1035
|
font-weight: var(--fcu-font-weight-medium);
|
|
1010
|
-
gap: var(--fcu-
|
|
1036
|
+
gap: var(--fcu-menu-link-gap);
|
|
1011
1037
|
}
|
|
1012
1038
|
.fcu-menu-link:hover {
|
|
1013
1039
|
background-color: var(--fcu-sidebar-hover-bg);
|
|
@@ -1017,28 +1043,38 @@ fieldset.fcu-form-section__body {
|
|
|
1017
1043
|
.fcu-submenu-link.active {
|
|
1018
1044
|
background-color: var(--fcu-sidebar-active-bg);
|
|
1019
1045
|
color: var(--fcu-sidebar-active-text);
|
|
1046
|
+
box-shadow: var(--fcu-sidebar-active-indicator);
|
|
1047
|
+
border-radius: var(--fcu-sidebar-active-radius);
|
|
1048
|
+
font-weight: var(--fcu-sidebar-active-weight);
|
|
1020
1049
|
}
|
|
1021
1050
|
/* The shared --fcu-focus is only held to 3:1 against the content surfaces; a
|
|
1022
1051
|
sidebar with its own ground needs its own ring. */
|
|
1023
1052
|
.fcu-sidebar :focus-visible {
|
|
1024
1053
|
outline-color: var(--fcu-sidebar-focus);
|
|
1025
1054
|
}
|
|
1055
|
+
/* A submenu's toggle is a row of the menu like any link, not a group label,
|
|
1056
|
+
so it takes the link's metrics and hover rather than .fcu-menu-header's
|
|
1057
|
+
small caps; its icon then lines up with the plain links' icons. */
|
|
1026
1058
|
.fcu-menu-toggle {
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1059
|
+
padding: var(--fcu-menu-link-padding);
|
|
1060
|
+
color: var(--fcu-sidebar-text);
|
|
1061
|
+
border-radius: var(--fcu-menu-link-radius);
|
|
1062
|
+
transition: background-color var(--fcu-duration) var(--fcu-easing);
|
|
1063
|
+
font-weight: var(--fcu-font-weight-medium);
|
|
1031
1064
|
cursor: pointer;
|
|
1032
1065
|
box-sizing: border-box;
|
|
1033
1066
|
}
|
|
1067
|
+
.fcu-menu-toggle:hover {
|
|
1068
|
+
background-color: var(--fcu-sidebar-hover-bg);
|
|
1069
|
+
color: var(--fcu-sidebar-hover-text);
|
|
1070
|
+
}
|
|
1034
1071
|
/* No-JS collapsible submenu via native <details>. */
|
|
1035
1072
|
.fcu-submenu-details > summary {
|
|
1036
1073
|
list-style: none;
|
|
1037
1074
|
cursor: pointer;
|
|
1038
1075
|
display: flex;
|
|
1039
1076
|
align-items: center;
|
|
1040
|
-
|
|
1041
|
-
gap: var(--fcu-space-sm);
|
|
1077
|
+
gap: var(--fcu-menu-link-gap);
|
|
1042
1078
|
}
|
|
1043
1079
|
.fcu-submenu-details > summary::-webkit-details-marker {
|
|
1044
1080
|
display: none;
|
|
@@ -1047,28 +1083,31 @@ fieldset.fcu-form-section__body {
|
|
|
1047
1083
|
color: var(--fcu-sidebar-text-muted);
|
|
1048
1084
|
transition: transform var(--fcu-duration) var(--fcu-easing);
|
|
1049
1085
|
flex: none;
|
|
1086
|
+
/* Only the arrow goes to the far edge; space-between on the summary would
|
|
1087
|
+
centre the title between the icon and the arrow. */
|
|
1088
|
+
margin-left: auto;
|
|
1050
1089
|
}
|
|
1051
1090
|
.fcu-submenu-details[open] > summary .fcu-submenu-arrow {
|
|
1052
1091
|
transform: rotate(90deg);
|
|
1053
1092
|
}
|
|
1054
1093
|
.fcu-submenu {
|
|
1055
1094
|
list-style: none;
|
|
1056
|
-
padding-left:
|
|
1057
|
-
margin: var(--fcu-
|
|
1095
|
+
padding-left: var(--fcu-submenu-indent);
|
|
1096
|
+
margin: var(--fcu-menu-gap) 0 0 0;
|
|
1058
1097
|
display: flex;
|
|
1059
1098
|
flex-direction: column;
|
|
1060
|
-
gap: var(--fcu-
|
|
1099
|
+
gap: var(--fcu-submenu-gap);
|
|
1061
1100
|
}
|
|
1062
1101
|
.fcu-submenu-link {
|
|
1063
1102
|
display: flex;
|
|
1064
1103
|
align-items: center;
|
|
1065
|
-
padding: var(--fcu-
|
|
1104
|
+
padding: var(--fcu-submenu-link-padding);
|
|
1066
1105
|
text-decoration: none;
|
|
1067
1106
|
color: var(--fcu-sidebar-text-muted);
|
|
1068
1107
|
font-size: var(--fcu-font-size-sm);
|
|
1069
|
-
border-radius: var(--fcu-radius
|
|
1108
|
+
border-radius: var(--fcu-menu-link-radius);
|
|
1070
1109
|
transition: all var(--fcu-duration) var(--fcu-easing);
|
|
1071
|
-
gap: var(--fcu-
|
|
1110
|
+
gap: var(--fcu-submenu-link-gap);
|
|
1072
1111
|
}
|
|
1073
1112
|
.fcu-submenu-link:hover {
|
|
1074
1113
|
color: var(--fcu-sidebar-hover-text);
|
|
@@ -1759,6 +1798,26 @@ fieldset.fcu-form-section__body {
|
|
|
1759
1798
|
.fcu-pivot-input[data-fcu-unsent] {
|
|
1760
1799
|
border-style: dashed;
|
|
1761
1800
|
}
|
|
1801
|
+
/* PivotForm unsaved="mark": changed boxes, and totals that moved with them
|
|
1802
|
+
(or, for an app total, whose boxes changed), until the save. Not colour
|
|
1803
|
+
alone: the border and underline are dashed, the total italic. */
|
|
1804
|
+
.fcu-pivot-input[data-fcu-unsaved] {
|
|
1805
|
+
border-style: dashed;
|
|
1806
|
+
border-color: var(--fcu-pivot-unsaved-color);
|
|
1807
|
+
}
|
|
1808
|
+
.fcu-pivot-total[data-fcu-unsaved] {
|
|
1809
|
+
font-style: italic;
|
|
1810
|
+
text-decoration: underline dashed var(--fcu-pivot-unsaved-color);
|
|
1811
|
+
text-underline-offset: 0.2em;
|
|
1812
|
+
}
|
|
1813
|
+
/* PivotForm unsaved="status": the "Unsaved changes" line beside Save. */
|
|
1814
|
+
.fcu-pivot-status {
|
|
1815
|
+
color: var(--fcu-pivot-unsaved-text);
|
|
1816
|
+
font-weight: var(--fcu-font-weight-semibold);
|
|
1817
|
+
}
|
|
1818
|
+
.fcu-pivot-status:empty {
|
|
1819
|
+
display: none;
|
|
1820
|
+
}
|
|
1762
1821
|
.fcu-pivot-controls {
|
|
1763
1822
|
display: flex;
|
|
1764
1823
|
flex-wrap: wrap;
|