flask-construct-ui 0.9.5__py3-none-any.whl

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.
Files changed (80) hide show
  1. flask_construct_ui/__init__.py +82 -0
  2. flask_construct_ui/auth.py +290 -0
  3. flask_construct_ui/base.py +1259 -0
  4. flask_construct_ui/components.py +812 -0
  5. flask_construct_ui/crud.py +1641 -0
  6. flask_construct_ui/csrf.py +39 -0
  7. flask_construct_ui/data.py +257 -0
  8. flask_construct_ui/data_sqlalchemy.py +304 -0
  9. flask_construct_ui/forms.py +130 -0
  10. flask_construct_ui/headless.py +110 -0
  11. flask_construct_ui/icons.py +158 -0
  12. flask_construct_ui/l10n/en/main.flt +82 -0
  13. flask_construct_ui/l10n/sv/main.flt +82 -0
  14. flask_construct_ui/language.py +100 -0
  15. flask_construct_ui/navbar.py +54 -0
  16. flask_construct_ui/navigation.py +58 -0
  17. flask_construct_ui/orgmode.py +338 -0
  18. flask_construct_ui/pagination.py +39 -0
  19. flask_construct_ui/py.typed +0 -0
  20. flask_construct_ui/sanitize.py +25 -0
  21. flask_construct_ui/schema.py +507 -0
  22. flask_construct_ui/static/css/prosemirror-bundle.min.css +1 -0
  23. flask_construct_ui/static/css/unpoly.min.css +6 -0
  24. flask_construct_ui/static/js/construct-core.min.js +1 -0
  25. flask_construct_ui/static/js/prosemirror-bundle.min.js +34 -0
  26. flask_construct_ui/static/js/unpoly.min.js +1 -0
  27. flask_construct_ui/static/themes/whitelabel/css/whitelabel.css +720 -0
  28. flask_construct_ui/static/themes/whitelabel/designs/adminlte.css +94 -0
  29. flask_construct_ui/static/themes/whitelabel/designs/sandstone.css +64 -0
  30. flask_construct_ui/static/themes/whitelabel/fonts/bootstrap-icons/LICENSE.txt +21 -0
  31. flask_construct_ui/static/themes/whitelabel/fonts/bootstrap-icons/bootstrap-icons.min.css +5 -0
  32. flask_construct_ui/static/themes/whitelabel/fonts/bootstrap-icons/fonts/bootstrap-icons.woff +0 -0
  33. flask_construct_ui/static/themes/whitelabel/fonts/bootstrap-icons/fonts/bootstrap-icons.woff2 +0 -0
  34. flask_construct_ui/static/themes/whitelabel/fonts/material-symbols/LICENSE.txt +202 -0
  35. flask_construct_ui/static/themes/whitelabel/fonts/material-symbols/material-symbols-outlined.woff2 +0 -0
  36. flask_construct_ui/static/themes/whitelabel/fonts/material-symbols/material-symbols.css +26 -0
  37. flask_construct_ui/templates/structure/base.html +5 -0
  38. flask_construct_ui/templates/structure/flashes.html +7 -0
  39. flask_construct_ui/templates/structure/public.html +18 -0
  40. flask_construct_ui/templates/themes/whitelabel/components/_form_field.html +69 -0
  41. flask_construct_ui/templates/themes/whitelabel/components/button.html +16 -0
  42. flask_construct_ui/templates/themes/whitelabel/components/calendar.html +30 -0
  43. flask_construct_ui/templates/themes/whitelabel/components/calendarmini.html +30 -0
  44. flask_construct_ui/templates/themes/whitelabel/components/card.html +25 -0
  45. flask_construct_ui/templates/themes/whitelabel/components/chart.html +5 -0
  46. flask_construct_ui/templates/themes/whitelabel/components/form.html +61 -0
  47. flask_construct_ui/templates/themes/whitelabel/components/grid_layout.html +5 -0
  48. flask_construct_ui/templates/themes/whitelabel/components/horizontal_layout.html +5 -0
  49. flask_construct_ui/templates/themes/whitelabel/components/live.html +40 -0
  50. flask_construct_ui/templates/themes/whitelabel/components/main.html +4 -0
  51. flask_construct_ui/templates/themes/whitelabel/components/markup.html +1 -0
  52. flask_construct_ui/templates/themes/whitelabel/components/rich_text.html +19 -0
  53. flask_construct_ui/templates/themes/whitelabel/components/table.html +30 -0
  54. flask_construct_ui/templates/themes/whitelabel/components/tabs.html +30 -0
  55. flask_construct_ui/templates/themes/whitelabel/components/text.html +26 -0
  56. flask_construct_ui/templates/themes/whitelabel/components/vertical_layout.html +5 -0
  57. flask_construct_ui/templates/themes/whitelabel/crud/_macros.html +962 -0
  58. flask_construct_ui/templates/themes/whitelabel/crud/_many_related_options.html +26 -0
  59. flask_construct_ui/templates/themes/whitelabel/crud/_related_child_form.html +98 -0
  60. flask_construct_ui/templates/themes/whitelabel/crud/_related_child_list.html +136 -0
  61. flask_construct_ui/templates/themes/whitelabel/crud/_related_options.html +15 -0
  62. flask_construct_ui/templates/themes/whitelabel/crud/create.html +45 -0
  63. flask_construct_ui/templates/themes/whitelabel/crud/edit.html +125 -0
  64. flask_construct_ui/templates/themes/whitelabel/crud/listindex.html +238 -0
  65. flask_construct_ui/templates/themes/whitelabel/crud/related_crud_tab_child_view.html +33 -0
  66. flask_construct_ui/templates/themes/whitelabel/crud/related_crud_tab_form.html +125 -0
  67. flask_construct_ui/templates/themes/whitelabel/crud/related_crud_tab_list.html +140 -0
  68. flask_construct_ui/templates/themes/whitelabel/crud/tab_view.html +38 -0
  69. flask_construct_ui/templates/themes/whitelabel/errors/forbidden.html +16 -0
  70. flask_construct_ui/templates/themes/whitelabel/navbar/notifications.html +26 -0
  71. flask_construct_ui/templates/themes/whitelabel/navbar/search_results.html +31 -0
  72. flask_construct_ui/templates/themes/whitelabel/style/_flashes.html +16 -0
  73. flask_construct_ui/templates/themes/whitelabel/style/base_full.html +181 -0
  74. flask_construct_ui/templates/themes/whitelabel/style/base_partial.html +16 -0
  75. flask_construct_ui/validation.py +422 -0
  76. flask_construct_ui-0.9.5.dist-info/METADATA +60 -0
  77. flask_construct_ui-0.9.5.dist-info/RECORD +80 -0
  78. flask_construct_ui-0.9.5.dist-info/WHEEL +5 -0
  79. flask_construct_ui-0.9.5.dist-info/licenses/LICENSE +165 -0
  80. flask_construct_ui-0.9.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,507 @@
1
+ import enum
2
+ import datetime
3
+ from .validation import ValidationError, parse_rule
4
+
5
+
6
+ class SchemaField:
7
+ def __init__(self, field_type, name, **kwargs):
8
+ self.field_type = field_type
9
+ self.name = name
10
+ self.readonly = kwargs.get("readonly", False)
11
+ self.required = kwargs.get("required", False)
12
+ self.nullable = kwargs.get("nullable", not self.required)
13
+ self.skipfilter = kwargs.get("skipfilter", False)
14
+ self.visible_list = kwargs.get("visible_list", True)
15
+ self.visible_form = kwargs.get("visible_form", True)
16
+ self.secret = kwargs.get("secret", False)
17
+ # None = follow the app-wide CONSTRUCT_ENFORCE_MATCHES default;
18
+ # True/False = explicit per-field override for server-side `matches`
19
+ # enforcement. Structural checks (length, type, required) are always
20
+ # enforced server-side regardless of this flag.
21
+ self.enforce = kwargs.get("enforce", None)
22
+ self.default = kwargs.get("default", None)
23
+ self.kwargs = kwargs
24
+ matches = kwargs.get("matches", None)
25
+ self._matches_fn = None
26
+ self._validation_node = None
27
+ if callable(matches):
28
+ self._matches_fn = matches
29
+ elif matches:
30
+ self.matches = matches
31
+ self._validation_node = parse_rule(matches)
32
+
33
+ def form_input_type(self) -> str:
34
+ return "text"
35
+
36
+ def form_extra_attrs(self) -> str:
37
+ return ""
38
+
39
+ def format_value(self, field, row):
40
+ return getattr(row, field)
41
+
42
+ def parse_value(self, value):
43
+ return value
44
+
45
+ def validate_value(self, val, row=None):
46
+ if val is None or val == "":
47
+ if self.required:
48
+ return ValidationError(
49
+ "validation-field-required", {}, "This field is required"
50
+ )
51
+ return None
52
+ return self._validate_matches(val, row)
53
+
54
+ def _validate_matches(self, val, row=None):
55
+ if self._matches_fn is not None:
56
+ rule_str = self._matches_fn(row)
57
+ if rule_str:
58
+ node = parse_rule(rule_str)
59
+ ok, err = node.validate_detail(val)
60
+ if not ok:
61
+ return err
62
+ elif self._validation_node:
63
+ ok, err = self._validation_node.validate_detail(val)
64
+ if not ok:
65
+ return err
66
+ return None
67
+
68
+ def validate_structural(self, val):
69
+ """Type/size invariants that are always enforced server-side."""
70
+ return None
71
+
72
+ def validate_submission(self, val, row=None, enforce_default=False):
73
+ """Validate a submitted form value before it is written.
74
+
75
+ Structural checks and ``required`` always apply; ``matches`` rules
76
+ apply when the field's ``enforce`` flag (or the app default) is on.
77
+ """
78
+ if (
79
+ val is None
80
+ or val == ""
81
+ or (isinstance(val, (set, list, tuple)) and not val)
82
+ ):
83
+ if self.required:
84
+ return ValidationError(
85
+ "validation-field-required", {}, "This field is required"
86
+ )
87
+ return None
88
+ err = self.validate_structural(val)
89
+ if err:
90
+ return err
91
+ enforce = self.enforce if self.enforce is not None else enforce_default
92
+ if enforce:
93
+ return self._validate_matches(val, row)
94
+ return None
95
+
96
+
97
+ class PK(SchemaField):
98
+ def __init__(self, name, **kwargs):
99
+ super().__init__("pk", name, **kwargs)
100
+
101
+
102
+ class String(SchemaField):
103
+ def __init__(self, name, length, **kwargs):
104
+ super().__init__("string", name, **kwargs)
105
+ self.length = length
106
+
107
+ def form_input_type(self):
108
+ return "text"
109
+
110
+ def validate_structural(self, val):
111
+ if self.length and len(str(val)) > self.length:
112
+ return ValidationError(
113
+ "validation-string-len-at-most",
114
+ {"limit": self.length},
115
+ f"Length must be at most {self.length} characters",
116
+ )
117
+ return None
118
+
119
+
120
+ class Text(SchemaField):
121
+ def __init__(self, name, **kwargs):
122
+ super().__init__("text", name, **kwargs)
123
+ self.markdown = kwargs.get("markdown", False)
124
+ self.orgmode = kwargs.get("orgmode", False)
125
+
126
+ def form_input_type(self):
127
+ if self.markdown:
128
+ return "richtext"
129
+ return "textarea"
130
+
131
+
132
+ class Integer(SchemaField):
133
+ def __init__(self, name, **kwargs):
134
+ super().__init__("integer", name, **kwargs)
135
+
136
+ def form_input_type(self):
137
+ return "number"
138
+
139
+ def parse_value(self, value):
140
+ if value == "":
141
+ return None
142
+ return value
143
+
144
+ def validate_structural(self, val):
145
+ try:
146
+ int(str(val))
147
+ except (ValueError, TypeError):
148
+ return ValidationError(
149
+ "validation-must-be-integer", {}, "Must be an integer"
150
+ )
151
+ return None
152
+
153
+
154
+ class Decimal(SchemaField):
155
+ def __init__(self, name, **kwargs):
156
+ super().__init__("decimal", name, **kwargs)
157
+
158
+ def form_input_type(self):
159
+ return "number"
160
+
161
+ def parse_value(self, value):
162
+ if value == "":
163
+ return None
164
+ return value
165
+
166
+ def form_extra_attrs(self):
167
+ return 'step="any"'
168
+
169
+ def validate_structural(self, val):
170
+ try:
171
+ float(val)
172
+ except (ValueError, TypeError):
173
+ return ValidationError("validation-must-be-number", {}, "Must be a number")
174
+ return None
175
+
176
+
177
+ class Boolean(SchemaField):
178
+ def __init__(self, name, **kwargs):
179
+ super().__init__("bool", name, **kwargs)
180
+
181
+ def form_input_type(self):
182
+ return "checkbox"
183
+
184
+
185
+ class DateTime(SchemaField):
186
+ def __init__(self, name, **kwargs):
187
+ super().__init__("datetime", name, **kwargs)
188
+
189
+ def form_input_type(self):
190
+ return "datetime-local"
191
+
192
+ def parse_value(self, value):
193
+ if value == "":
194
+ return None
195
+ return value
196
+
197
+ def validate_structural(self, val):
198
+
199
+ if isinstance(val, (datetime.date, datetime.datetime)):
200
+ return None
201
+ try:
202
+ datetime.datetime.fromisoformat(str(val))
203
+ except (ValueError, TypeError):
204
+ return ValidationError(
205
+ "validation-must-be-datetime", {}, "Must be a valid date/time"
206
+ )
207
+ return None
208
+
209
+
210
+ class Related(SchemaField):
211
+ def __init__(self, name, relcls, relation, **kwargs):
212
+ super().__init__("related", name, **kwargs)
213
+ self.related_class = relcls
214
+ self.related_field = relation
215
+
216
+ def form_input_type(self):
217
+ return "select"
218
+
219
+ def format_value(self, field, row):
220
+ curr = row
221
+ for item in self.related_field.split("."):
222
+ if curr is None:
223
+ return None
224
+ curr = getattr(curr, item, None)
225
+ return curr
226
+
227
+ def form_value(self, row):
228
+ """The related record itself, for form rendering.
229
+
230
+ ``format_value`` resolves ``related_field`` as a (possibly dotted)
231
+ display path for list cells — e.g. ``"category.name"`` yields a
232
+ string. Form templates instead need the related record to compare
233
+ primary keys when preselecting the current option, so this always
234
+ resolves just the relationship attribute (the first path segment).
235
+ A raw primary key on the row (a form re-rendering submitted values
236
+ after a validation failure) is read back into the record.
237
+ """
238
+ value = getattr(row, self.related_field.split(".")[0], None)
239
+ if value is None or value == "":
240
+ return None
241
+ model = getattr(self.related_class, "model", None)
242
+ if model is not None and isinstance(value, model):
243
+ return value
244
+ if isinstance(value, (str, int)):
245
+ return self.related_class().read(value)
246
+ return value
247
+
248
+
249
+ class _RecordMap:
250
+ """Mapping over a record's attributes for ``str.format_map`` — missing
251
+ attributes render as empty strings instead of raising."""
252
+
253
+ def __init__(self, record):
254
+ self._record = record
255
+
256
+ def __getitem__(self, key):
257
+ value = getattr(self._record, key, None)
258
+ return "" if value is None else value
259
+
260
+
261
+ class ManyRelated(SchemaField):
262
+ # Sub-field types allowed in the inline create panel (create="form").
263
+ # Relational types would need recursive pickers and richtext needs a
264
+ # live editor instance — neither works inside a cloned <template>.
265
+ INLINE_FORM_TYPES = (
266
+ "string",
267
+ "text",
268
+ "integer",
269
+ "decimal",
270
+ "datetime",
271
+ "bool",
272
+ "set",
273
+ "enum",
274
+ )
275
+
276
+ def __init__(self, name, relcls, relation, **kwargs):
277
+ super().__init__("manyrelated", name, **kwargs)
278
+ self.related_class = relcls
279
+ self.related_field = relation
280
+ self.search_fields = kwargs.get("search_fields", None)
281
+ self.display = kwargs.get("display", None)
282
+ self.create = kwargs.get("create", None)
283
+ if self.create not in (None, "quick", "form"):
284
+ raise ValueError(
285
+ f"ManyRelated create= must be None, 'quick' or 'form',"
286
+ f" got {self.create!r}"
287
+ )
288
+ self.quick_field = kwargs.get("quick_field", None)
289
+ self._quick_field_resolved = None
290
+
291
+ def form_input_type(self):
292
+ return "manyrelated"
293
+
294
+ def render_display(self, record):
295
+ """Render a related record for chips and search results.
296
+
297
+ ``display`` mirrors the ``matches`` contract: a format string
298
+ interpolating related-record fields (``"{name} ({city})"``) or a
299
+ ``callable(record) -> str``. Defaults to ``str(record)``.
300
+ """
301
+ if record is None:
302
+ return ""
303
+ if callable(self.display):
304
+ return str(self.display(record))
305
+ if self.display:
306
+ return self.display.format_map(_RecordMap(record))
307
+ return str(record)
308
+
309
+ def resolve_quick_field(self):
310
+ """The related field that receives a quick-created value.
311
+
312
+ Resolved lazily — ``related_class._schema`` only exists once
313
+ ``@dataschema`` has run on the related class.
314
+ """
315
+ if self._quick_field_resolved is not None:
316
+ return self._quick_field_resolved
317
+ sub_schema = self.related_class._schema
318
+ if self.quick_field is not None:
319
+ ftype = sub_schema.get(self.quick_field)
320
+ if ftype is None or ftype.field_type not in ("string", "text"):
321
+ raise ValueError(
322
+ f"ManyRelated {self.name}: quick_field"
323
+ f" {self.quick_field!r} must be a string or text field"
324
+ f" of the related schema"
325
+ )
326
+ self._quick_field_resolved = self.quick_field
327
+ return self.quick_field
328
+ candidates = [
329
+ sub_name
330
+ for sub_name, ftype in sub_schema.items()
331
+ if ftype.field_type in ("string", "text")
332
+ and ftype.visible_form
333
+ and not ftype.readonly
334
+ and not ftype.secret
335
+ ]
336
+ required = [c for c in candidates if sub_schema[c].required]
337
+ if len(required) == 1:
338
+ self._quick_field_resolved = required[0]
339
+ elif len(candidates) == 1:
340
+ self._quick_field_resolved = candidates[0]
341
+ else:
342
+ raise ValueError(
343
+ f"ManyRelated {self.name}: quick_field is ambiguous;"
344
+ f" pass quick_field= explicitly"
345
+ )
346
+ return self._quick_field_resolved
347
+
348
+ def inline_form_fields(self):
349
+ """The related-schema fields editable in the inline create panel."""
350
+ return [
351
+ (sub_name, ftype)
352
+ for sub_name, ftype in self.related_class._schema.items()
353
+ if ftype.field_type in self.INLINE_FORM_TYPES
354
+ and ftype.visible_form
355
+ and not ftype.readonly
356
+ and not ftype.secret
357
+ ]
358
+
359
+ def format_value(self, field, row):
360
+ return list(getattr(row, self.related_field, None) or [])
361
+
362
+ def form_value(self, row):
363
+ """The related records, for form rendering (see Related.form_value).
364
+
365
+ Raw primary keys on the row (a form re-rendering submitted values
366
+ after a validation failure) are read back into records.
367
+ """
368
+ model = getattr(self.related_class, "model", None)
369
+ resolved = []
370
+ for item in getattr(row, self.related_field, None) or []:
371
+ if model is not None and isinstance(item, model):
372
+ resolved.append(item)
373
+ elif isinstance(item, (str, int)):
374
+ rec = self.related_class().read(item)
375
+ if rec is not None:
376
+ resolved.append(rec)
377
+ else:
378
+ resolved.append(item)
379
+ return resolved
380
+
381
+
382
+ class Set(SchemaField):
383
+ def __init__(self, name, sets, **kwargs):
384
+ super().__init__("set", name, **kwargs)
385
+ self.sets = sets
386
+
387
+ def parse_value(self, value):
388
+ if isinstance(value, (list, set)):
389
+ return ",".join(sorted(list(value)))
390
+ return value
391
+
392
+ def form_input_type(self):
393
+ return "multiselect"
394
+
395
+ def validate_structural(self, val):
396
+ values = val if isinstance(val, (set, list, tuple)) else str(val).split(",")
397
+ invalid = [v for v in values if v and v not in self.sets]
398
+ if invalid:
399
+ options = ", ".join(f"'{k}'" for k in self.sets)
400
+ return ValidationError(
401
+ "validation-must-be-one-of",
402
+ {"options": options},
403
+ f"Must be one of: {options}",
404
+ )
405
+ return None
406
+
407
+
408
+ class List(SchemaField):
409
+ def __init__(self, name, relcls, relation, columns=None, **kwargs):
410
+ super().__init__("list", name, **kwargs)
411
+ self.list_class = relcls
412
+ self.list_field = relation
413
+ self.columns = columns
414
+ self.layout = kwargs.get("layout", "table")
415
+ self.can_add = kwargs.get("can_add", True)
416
+ self.can_delete = kwargs.get("can_delete", True)
417
+ self.template = kwargs.get("template", None)
418
+
419
+ def form_input_type(self):
420
+ return "list"
421
+
422
+
423
+ class Enum(String):
424
+ class ValueWrapper(dict):
425
+ def __init__(self, v, display=None):
426
+ super().__init__(id=v)
427
+ self.v = v
428
+ self.id = v
429
+ self.display = display if display is not None else v
430
+
431
+ def __str__(self):
432
+ from flask import has_app_context, current_app
433
+
434
+ if has_app_context() and hasattr(current_app, "l10n"):
435
+ translated = current_app.l10n.t(self.v)
436
+ if translated != self.v:
437
+ return str(translated)
438
+ return str(self.display)
439
+
440
+ def __init__(self, name, enum_class, **kwargs):
441
+ super().__init__(name, 50, **kwargs)
442
+ self.enum_class = enum_class
443
+ self.field_type = "enum"
444
+ self._primary_key = "id"
445
+
446
+ def related_class(self):
447
+ return self
448
+
449
+ related_class._primary_key = "id"
450
+
451
+ def search(self, q=None):
452
+ return [self.ValueWrapper(t.name, t.value) for t in self.enum_class]
453
+
454
+ def validate_structural(self, val):
455
+ if str(val) not in {member.name for member in self.enum_class}:
456
+ options = ", ".join(f"'{member.name}'" for member in self.enum_class)
457
+ return ValidationError(
458
+ "validation-must-be-one-of",
459
+ {"options": options},
460
+ f"Must be one of: {options}",
461
+ )
462
+ return None
463
+
464
+ def parse_value(self, value):
465
+ if value == "":
466
+ return None
467
+ return value
468
+
469
+ def format_value(self, field, row):
470
+ val = getattr(row, field, None)
471
+ if val is None:
472
+ return None
473
+
474
+ if isinstance(val, enum.Enum):
475
+ return self.ValueWrapper(val.name, val.value)
476
+ return self.ValueWrapper(str(val), str(val))
477
+
478
+
479
+ class Schema:
480
+ def __init__(self):
481
+ self.PK = PK
482
+ self.String = String
483
+ self.Text = Text
484
+ self.Integer = Integer
485
+ self.Decimal = Decimal
486
+ self.Boolean = Boolean
487
+ self.DateTime = DateTime
488
+ self.Related = Related
489
+ self.ManyRelated = ManyRelated
490
+ self.List = List
491
+ self.Set = Set
492
+ self.Enum = Enum
493
+
494
+
495
+ schema = Schema()
496
+
497
+
498
+ def dataschema(cls):
499
+ cls._schema = {}
500
+ cls._primary_key = None
501
+ for name, attribute in list(cls.__dict__.items()):
502
+ if isinstance(attribute, SchemaField):
503
+ cls._schema[name] = attribute
504
+ delattr(cls, name)
505
+ if isinstance(attribute, PK):
506
+ cls._primary_key = name
507
+ return cls
@@ -0,0 +1 @@
1
+ .ProseMirror{position:relative}.ProseMirror{word-wrap:break-word;white-space:pre-wrap;white-space:break-spaces;-webkit-font-variant-ligatures:none;font-variant-ligatures:none;font-feature-settings:"liga" 0}.ProseMirror pre{white-space:pre-wrap}.ProseMirror li{position:relative}.ProseMirror-hideselection *::selection{background:transparent}.ProseMirror-hideselection *::-moz-selection{background:transparent}.ProseMirror-hideselection{caret-color:transparent}.ProseMirror [draggable][contenteditable=false]{user-select:text}.ProseMirror-selectednode{outline:2px solid #8cf}li.ProseMirror-selectednode{outline:none}li.ProseMirror-selectednode:after{content:"";position:absolute;inset:-2px -2px -2px -32px;border:2px solid #8cf;pointer-events:none}img.ProseMirror-separator{display:inline!important;border:none!important;margin:0!important}
@@ -0,0 +1,6 @@
1
+ [hidden][hidden]{display:none !important}
2
+ up-wrapper{display:inline-block}
3
+ up-bounds{position:absolute}.up-focus-hidden:focus-visible{outline-color:rgba(0,0,0,0) !important;outline-style:none !important}body.up-scrollbar-away{padding-right:calc(var(--up-scrollbar-width) + var(--up-original-padding-right)) !important}body.up-scrollbar-away,html:has(>body.up-scrollbar-away){overflow-y:hidden !important;overflow-y:clip !important}body.up-scrollbar-away .up-scrollbar-away{right:calc(var(--up-scrollbar-width) + var(--up-original-right)) !important}
4
+ .up-request-loader{display:none}up-progress-bar{position:fixed;top:0;left:0;z-index:999999999;height:3px;background-color:#007bff}
5
+ up-focus-trap{position:fixed;top:0;left:0;width:0;height:0}up-cover-viewport,up-drawer-viewport,up-modal-viewport,up-drawer-backdrop,up-modal-backdrop,up-cover,up-drawer,up-modal{top:0;left:0;bottom:0;right:0}up-drawer-box,up-modal-box{box-shadow:0 0 10px 1px rgba(0,0,0,.3)}up-popup{box-shadow:0 0 4px rgba(0,0,0,.3)}up-popup:focus,up-cover-box:focus,up-drawer-box:focus,up-modal-box:focus,up-cover:focus,up-drawer:focus,up-modal:focus,up-popup:focus-visible,up-cover-box:focus-visible,up-drawer-box:focus-visible,up-modal-box:focus-visible,up-cover:focus-visible,up-drawer:focus-visible,up-modal:focus-visible{outline:none}up-cover,up-drawer,up-modal{z-index:2000;position:fixed}up-drawer-backdrop,up-modal-backdrop{position:absolute;background:rgba(0,0,0,.4)}up-cover-viewport,up-drawer-viewport,up-modal-viewport{position:absolute;overflow-y:scroll;overflow-x:hidden;overscroll-behavior:contain;display:flex;align-items:flex-start;justify-content:center}up-popup,up-cover-box,up-drawer-box,up-modal-box{position:relative;box-sizing:border-box;max-width:100%;background-color:#fff;padding:20px;overflow-x:hidden}up-popup-content,up-cover-content,up-drawer-content,up-modal-content{display:block}up-popup{z-index:1000}up-popup-dismiss,up-cover-dismiss,up-drawer-dismiss,up-modal-dismiss{color:#888;position:absolute;top:10px;right:10px;font-size:1.7rem;line-height:.5;cursor:pointer}up-modal[nesting="0"] up-modal-viewport{padding:25px 15px}up-modal[nesting="1"] up-modal-viewport{padding:50px 30px}up-modal[nesting="2"] up-modal-viewport{padding:75px 45px}up-modal[nesting="3"] up-modal-viewport{padding:100px 60px}up-modal[nesting="4"] up-modal-viewport{padding:125px 75px}up-modal[size=small] up-modal-box{width:350px}up-modal[size=medium] up-modal-box{width:650px}up-modal[size=large] up-modal-box{width:1000px}up-modal[size=grow] up-modal-box{width:auto}up-modal[size=full] up-modal-box{width:100%}up-drawer-viewport{justify-content:flex-start}up-drawer[position=right] up-drawer-viewport{justify-content:flex-end}up-drawer-box{min-height:100vh}up-drawer[size=small] up-drawer-box{width:150px}up-drawer[size=medium] up-drawer-box{width:340px}up-drawer[size=large] up-drawer-box{width:600px}up-drawer[size=grow] up-drawer-box{width:auto}up-drawer[size=full] up-drawer-box{width:100%}up-cover-box{width:100%;min-height:100vh;padding:0}up-popup{padding:15px;text-align:left}up-popup[size=small]{width:180px}up-popup[size=medium]{width:300px}up-popup[size=large]{width:550px}up-popup[size=grow] up-popup{width:auto}up-popup[size=full] up-popup{width:100%}
6
+ [up-clickable][role=link]{cursor:pointer}[up-expand]:not([role]),[up-expand][role=link]{cursor:pointer}
@@ -0,0 +1 @@
1
+ (()=>{var x={};if(typeof up<"u"){let d=function(r){return String(r).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")};A=d,up.status&&up.status.config&&up.status.config.currentClasses&&(up.status.config.currentClasses.includes("active")||up.status.config.currentClasses.push("active")),up.feedback&&up.feedback.config&&up.feedback.config.currentClasses&&(up.feedback.config.currentClasses.includes("active")||up.feedback.config.currentClasses.push("active")),up.on("up:location:changed",function(){setTimeout(()=>{document.querySelectorAll(".fcu-submenu-link.active, .fcu-submenu-link.up-current").forEach(function(r){let a=r.closest("details");a&&(a.open=!0)})},0)}),up.compiler(".fcu-menu-link, .fcu-submenu-link",function(r){let a=r.getAttribute("href");if(a&&a!=="#"){let u=a.replace(/\/+$/,"");u&&u!=="/"&&r.setAttribute("up-alias",`${u} ${u}/ ${u}/*`)}}),up.compiler("[data-live-sse-url], [data-live-poll-url]",function(r){let a=r.getAttribute("data-live-sse-url"),u=r.getAttribute("data-live-poll-url"),e=parseInt(r.getAttribute("data-live-interval")||"10000",10),n=r.getAttribute("data-live-content-id")||"live-content-"+r.getAttribute("data-live-source"),o=document.getElementById(n),s=r.getAttribute("data-live-format")||"default",t=r.getAttribute("data-live-limit")?parseInt(r.getAttribute("data-live-limit"),10):200;if(!o)return;let c=null,i=null,p=!1;setTimeout(()=>{p=!0},2*e);function g(l){if(l&&!p)return;r.classList.toggle("live-stale",l);let f=r.querySelector(".live-stale-indicator");f&&(f.style.display=l?"":"none")}function v(l){let f=l||location.href;typeof up<"u"&&up.visit?(up.cache&&up.cache.expire&&up.cache.expire(),up.visit(f,{cache:!1})):window.location.href=f}function T(){let l=e*.15*(Math.random()*2-1);return Math.max(1e3,e+l)}function b(){document.hidden||fetch(u).then(l=>l.status===205?(y(),v(),null):l.text()).then(l=>{if(!l)return;let m=new DOMParser().parseFromString(l,"text/html").querySelector(".live-content");m&&(o.innerHTML=m.innerHTML),g(!1)}).catch(()=>g(!0))}function E(){c=setTimeout(()=>{b(),E()},T())}function w(){document.hidden||b()}function L(){c||!u||(document.addEventListener("visibilitychange",w),E())}function y(){c&&(clearTimeout(c),c=null),document.removeEventListener("visibilitychange",w)}return a?(i=new EventSource(a),i.addEventListener("update",function(l){if(s==="raw"){o.innerHTML=l.data,o.scrollTop=o.scrollHeight,g(!1),r.dispatchEvent(new CustomEvent("fcu:live:update",{detail:l.data}));return}try{let f=JSON.parse(l.data);r.dispatchEvent(new CustomEvent("fcu:live:update",{detail:f}));let m=Object.entries(f).map(([I,h])=>I==="line"?`<div style="padding:2px 0;border-bottom:1px solid rgba(0,0,0,0.07)">${d(h)}</div>`:Array.isArray(h)?h.map(k=>`<div style="padding:2px 0;border-bottom:1px solid rgba(0,0,0,0.07)">${d(k)}</div>`).join(""):`<div><strong>${d(I)}</strong>: ${d(h)}</div>`).join("");if(s==="append"){if(o.insertAdjacentHTML("beforeend",m),t>0)for(;o.children.length>t;)o.removeChild(o.firstChild)}else o.innerHTML=m;o.scrollTop=o.scrollHeight,g(!1)}catch{if(s==="append"){let m=document.createElement("div");if(m.textContent=l.data,o.appendChild(m),t>0)for(;o.children.length>t;)o.removeChild(o.firstChild)}else o.textContent=l.data;o.scrollTop=o.scrollHeight,r.dispatchEvent(new CustomEvent("fcu:live:update",{detail:l.data}))}}),i.addEventListener("done",function(l){i.close(),i=null,y();let f;try{f=JSON.parse(l.data).redirect}catch{}r.dispatchEvent(new CustomEvent("fcu:live:done",{detail:f})),v(f)}),i.onerror=function(){i.close(),i=null,g(!0),L()}):L(),function(){i&&i.close(),y()}})}var A;if(typeof up<"u"){let a=function(e){let n=document.getElementById("manyrelated-results-"+e);n&&(n.innerHTML="");let o=document.getElementById("manyrelated-search-"+e);o&&(o.value="",o.focus())},u=function(e,n,o,s){let t=document.createElement("span");t.style.cssText=`display: inline-flex; align-items: center; gap: 4px; background-color: var(--fcu-surface-alt); border: 1px ${o?"dashed":"solid"} var(--fcu-border); font-size: var(--fcu-font-size-sm); font-weight: 500; padding: 2px 8px; border-radius: var(--fcu-border-radius);`,t.appendChild(document.createTextNode(n));let c=document.createElement("button");c.type="button",c.style.cssText="margin-left: 4px; border: none; background: transparent; padding: 0; cursor: pointer; color: var(--fcu-text-muted); display: inline-flex; align-items: center;";let i=document.getElementById("manyrelated-search-"+e),p=i&&i.parentNode.querySelector("span button");return p?c.innerHTML=p.innerHTML:c.textContent="\xD7",c.addEventListener("click",function(g){g.stopPropagation(),s(t)}),t.appendChild(c),i&&i.parentNode.insertBefore(t,i),t};q=a,C=u,up.compiler(".js-hide",function(e){e.querySelectorAll("input, textarea, select").forEach(n=>n.disabled=!0)}),up.compiler(".secret-toggle-btn",function(e){let n=o=>{let s=e.closest(".form-group"),t=s.querySelector(".secret-input-container"),c=s.querySelector(".secret-change-checkbox");if(!t||!c)return;let i=t.querySelectorAll("input, textarea, select");t.style.display==="none"?(t.style.display="block",i.forEach(p=>p.disabled=!1),c.checked=!0,e.classList.add("active"),e.querySelector(".bi")?e.innerHTML='<i class="bi bi-x-lg"></i>':e.querySelector(".material-symbols-outlined")&&(e.innerHTML='<span class="material-symbols-outlined text-xl">close</span>'),i.length>0&&i[0].focus()):(t.style.display="none",i.forEach(p=>p.disabled=!0),c.checked=!1,e.classList.remove("active"),e.querySelector(".bi")?e.innerHTML='<i class="bi bi-pencil-fill"></i>':e.querySelector(".material-symbols-outlined")&&(e.innerHTML='<span class="material-symbols-outlined text-xl">edit</span>'))};return e.addEventListener("click",n),()=>e.removeEventListener("click",n)}),up.compiler("[data-auto-dismiss]",function(e){let n=(o,s)=>{let t=s||o.target;t&&!t.contains(e)&&document.contains(e)&&(up.destroy(e,{transition:"fade-out"}),up.off("up:fragment:inserted",n))};return up.on("up:fragment:inserted",n),()=>up.off("up:fragment:inserted",n)});let d={},r={};window.addItem=function(e){if(d[e]===void 0){let s=document.querySelectorAll(`[name^="${e}-new-"]`).length;d[e]=s}let n=document.getElementById(`${e}-new-item-template`),o=document.getElementById(`${e}-new-items-container`);if(n&&o){let s=n.content.cloneNode(!0),t=document.createElement("div");t.appendChild(s);let c=t.innerHTML.replace(/__INDEX__/g,d[e]);o.insertAdjacentHTML("beforeend",c),d[e]++}},window.selectItem=function(e,n,o){let s=document.getElementById(e);s&&(s.value=n);let t=document.getElementById(`selected-item-display-${e}`);t&&(t.value=o);let c=document.getElementById(`search-results-${e}`);c&&(c.innerHTML="");let i=document.getElementById(`search-input-${e}`);i&&(i.value="")},window.addManyRelatedItem=function(e,n,o){let s=document.getElementById(e+"-hidden-inputs");if(!s)return;if(s.querySelector(`input[value="${n}"]`)){a(e);return}let t=document.createElement("input");t.type="hidden",t.name=e,t.value=n,s.appendChild(t);let c=u(e,o,!1,function(i){t.remove(),i.remove()});c.id=e+"-badge-"+n,a(e)},window.addManyRelatedQuickItem=function(e,n){if(Array.from(document.querySelectorAll(`input[name="${e}-quicknew"]`)).some(function(p){return!p.disabled&&p.value===n})){a(e);return}let s=document.getElementById(e+"-hidden-inputs"),t=s&&s.dataset.pendingLabel||"",c=document.createElement("input");c.type="hidden",c.name=e+"-quicknew",c.value=n;let i=u(e,n,!0,function(p){p.remove()});if(i.insertBefore(c,i.firstChild),t){let p=document.createElement("span");p.style.cssText="color: var(--fcu-text-muted); font-weight: normal;",p.textContent=t,i.insertBefore(p,i.lastChild)}a(e)},window.addManyRelatedNewPanel=function(e){let n=`${e}-newrel-`;if(r[e]===void 0){let t=-1;document.querySelectorAll(`[name^="${n}"]`).forEach(function(c){let i=parseInt(c.name.slice(n.length).split("-")[0],10);!isNaN(i)&&i>t&&(t=i)}),r[e]=t+1}let o=document.getElementById(`${e}-newrel-item-template`),s=document.getElementById(`${e}-newrel-items-container`);if(o&&s){let t=o.content.cloneNode(!0),c=document.createElement("div");c.appendChild(t);let i=c.innerHTML.replace(/__INDEX__/g,r[e]);s.insertAdjacentHTML("beforeend",i),r[e]++}},window.removeManyRelatedItem=function(e,n){let o=document.getElementById(e+"-hidden-inputs");if(o){let t=o.querySelector(`input[value="${n}"]`);t&&t.remove()}let s=document.getElementById(e+"-badge-"+n);s&&s.remove()},up.compiler("[data-js-enable]",function(e){e.disabled=!1}),up.compiler(".js-hide select",function(e){e.disabled=!0}),up.compiler('input[id^="search-input-"]',function(e){e.addEventListener("keydown",function(n){let o=e.id.replace("search-input-",""),s=document.getElementById("search-results-"+o);if(!s)return;if(n.key==="Escape"){n.preventDefault(),s.innerHTML="",e.blur();return}let t=s.querySelectorAll("a.fcu-dropdown-item, a.list-group-item");if(t.length===0)return;let c=s.querySelector("a.fcu-dropdown-item.active, a.list-group-item.active"),i=c?Array.from(t).indexOf(c):-1;n.key==="ArrowDown"?(n.preventDefault(),c&&c.classList.remove("active"),i=(i+1)%t.length,t[i].classList.add("active"),t[i].scrollIntoView({block:"nearest"})):n.key==="ArrowUp"?(n.preventDefault(),c&&c.classList.remove("active"),i=(i-1+t.length)%t.length,t[i].classList.add("active"),t[i].scrollIntoView({block:"nearest"})):n.key==="Enter"&&(n.preventDefault(),c&&c.click())})}),up.compiler('input[id^="manyrelated-search-"]',function(e){e.addEventListener("keydown",function(n){let o=e.id.replace("manyrelated-search-",""),s=document.getElementById("manyrelated-results-"+o);if(!s)return;let t=s.querySelectorAll("a.fcu-dropdown-item, a.list-group-item");if(t.length===0)return;let c=s.querySelector("a.fcu-dropdown-item.active, a.list-group-item.active"),i=c?Array.from(t).indexOf(c):-1;n.key==="ArrowDown"?(n.preventDefault(),c&&c.classList.remove("active"),i=(i+1)%t.length,t[i].classList.add("active"),t[i].scrollIntoView({block:"nearest"})):n.key==="ArrowUp"?(n.preventDefault(),c&&c.classList.remove("active"),i=(i-1+t.length)%t.length,t[i].classList.add("active"),t[i].scrollIntoView({block:"nearest"})):n.key==="Enter"?(n.preventDefault(),c&&c.click()):n.key==="Escape"&&(s.innerHTML="")})})}var q,C;document.addEventListener("input",function(d){let r=d.target;if(r.matches('input[id^="manyrelated-search-"]')){let a=r.id.replace("manyrelated-search-",""),u=r.getAttribute("up-href"),e=r.value,n=parseInt(r.getAttribute("up-delay"))||0,o=parseInt(r.getAttribute("up-min-length"))||0;if(clearTimeout(window.manyRelatedTimer),u&&e.length>=o)window.manyRelatedTimer=setTimeout(function(){typeof up<"u"&&up.render({url:u,target:"#manyrelated-results-"+a,params:{q:e}})},n);else{let s=document.getElementById("manyrelated-results-"+a);s&&(s.innerHTML="")}return}if(r.matches('input[id^="search-input-"]')){let a=r.id.replace("search-input-",""),u=r.getAttribute("up-href"),e=r.getAttribute("up-target"),n=r.value,o=parseInt(r.getAttribute("up-delay"))||0,s=parseInt(r.getAttribute("up-min-length"))||0;if(clearTimeout(window.searchTimer),u&&e)if(n.length>=s)window.searchTimer=setTimeout(function(){typeof up<"u"&&up.render({url:u,target:e,params:{q:n}})},o);else{let t=document.querySelector(e);t&&(t.innerHTML="")}}});document.addEventListener("focusin",function(d){let r=d.target;if(r.matches('input[id^="manyrelated-search-"]')){let a=r.id.replace("manyrelated-search-","");clearTimeout(x[a]),r.dispatchEvent(new Event("input",{bubbles:!0}))}r.matches('input[id^="search-input-"]')&&r.dispatchEvent(new Event("input",{bubbles:!0}))});document.addEventListener("keydown",function(d){let r=d.target;if(d.key==="ArrowDown"&&r.matches('input[id^="selected-item-display-"]')){let a=document.getElementById("search-input-"+r.id.replace("selected-item-display-",""));a&&(d.preventDefault(),a.focus())}});document.addEventListener("focusout",function(d){let r=d.target;if(r.matches('input[id^="manyrelated-search-"]')){let a=r.id.replace("manyrelated-search-","");x[a]=setTimeout(function(){let u=document.getElementById("manyrelated-results-"+a);u&&(u.innerHTML="")},200)}});})();