polyadmin 0.1.0b1__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 (104) hide show
  1. polyadmin/__init__.py +47 -0
  2. polyadmin/core/__init__.py +0 -0
  3. polyadmin/core/_async.py +21 -0
  4. polyadmin/core/action.py +135 -0
  5. polyadmin/core/admin.py +129 -0
  6. polyadmin/core/audit.py +65 -0
  7. polyadmin/core/auth.py +52 -0
  8. polyadmin/core/authorization.py +48 -0
  9. polyadmin/core/csrf.py +70 -0
  10. polyadmin/core/dashboard.py +41 -0
  11. polyadmin/core/delete.py +146 -0
  12. polyadmin/core/exporter.py +136 -0
  13. polyadmin/core/field.py +201 -0
  14. polyadmin/core/filter.py +305 -0
  15. polyadmin/core/inline.py +97 -0
  16. polyadmin/core/login.py +110 -0
  17. polyadmin/core/model_admin.py +349 -0
  18. polyadmin/core/page.py +62 -0
  19. polyadmin/core/pagination.py +70 -0
  20. polyadmin/core/query.py +243 -0
  21. polyadmin/core/relation.py +40 -0
  22. polyadmin/core/slug.py +57 -0
  23. polyadmin/core/template_context.py +679 -0
  24. polyadmin/core/widget.py +280 -0
  25. polyadmin/fastapi/__init__.py +3 -0
  26. polyadmin/fastapi/audit.py +52 -0
  27. polyadmin/fastapi/auth.py +112 -0
  28. polyadmin/fastapi/csrf.py +93 -0
  29. polyadmin/fastapi/deletes.py +76 -0
  30. polyadmin/fastapi/errors.py +93 -0
  31. polyadmin/fastapi/handlers.py +798 -0
  32. polyadmin/fastapi/inlines.py +177 -0
  33. polyadmin/fastapi/locale.py +128 -0
  34. polyadmin/fastapi/login.py +110 -0
  35. polyadmin/fastapi/pages.py +97 -0
  36. polyadmin/fastapi/relations.py +264 -0
  37. polyadmin/fastapi/responses.py +55 -0
  38. polyadmin/fastapi/router.py +174 -0
  39. polyadmin/fastapi/static.py +21 -0
  40. polyadmin/i18n/__init__.py +50 -0
  41. polyadmin/i18n/context.py +54 -0
  42. polyadmin/i18n/negotiation.py +56 -0
  43. polyadmin/i18n/setup.py +81 -0
  44. polyadmin/i18n/translator.py +124 -0
  45. polyadmin/locale/fr/LC_MESSAGES/polyadmin.mo +0 -0
  46. polyadmin/locale/fr/LC_MESSAGES/polyadmin.po +486 -0
  47. polyadmin/locale/polyadmin.pot +485 -0
  48. polyadmin/locale/ru/LC_MESSAGES/polyadmin.mo +0 -0
  49. polyadmin/locale/ru/LC_MESSAGES/polyadmin.po +496 -0
  50. polyadmin/templates/admin/base.html +91 -0
  51. polyadmin/templates/admin/components/action_confirm_modal.html +86 -0
  52. polyadmin/templates/admin/components/csrf-field.html +5 -0
  53. polyadmin/templates/admin/components/error_fragment.html +6 -0
  54. polyadmin/templates/admin/components/field.html +60 -0
  55. polyadmin/templates/admin/components/form_wrapper.html +131 -0
  56. polyadmin/templates/admin/components/icons.html +67 -0
  57. polyadmin/templates/admin/components/inline.html +251 -0
  58. polyadmin/templates/admin/components/inline_fragment.html +2 -0
  59. polyadmin/templates/admin/components/list_content.html +54 -0
  60. polyadmin/templates/admin/components/lookup_results.html +19 -0
  61. polyadmin/templates/admin/components/search.html +19 -0
  62. polyadmin/templates/admin/components/toasts.html +151 -0
  63. polyadmin/templates/admin/components/ui/breadcrumb.html +30 -0
  64. polyadmin/templates/admin/components/ui/bulk-actions.html +69 -0
  65. polyadmin/templates/admin/components/ui/calendar.html +175 -0
  66. polyadmin/templates/admin/components/ui/combobox.html +82 -0
  67. polyadmin/templates/admin/components/ui/delete-preview.html +37 -0
  68. polyadmin/templates/admin/components/ui/dropdown-menu.html +71 -0
  69. polyadmin/templates/admin/components/ui/field.html +110 -0
  70. polyadmin/templates/admin/components/ui/filter-panel.html +155 -0
  71. polyadmin/templates/admin/components/ui/locale-switcher.html +30 -0
  72. polyadmin/templates/admin/components/ui/multi-select.html +253 -0
  73. polyadmin/templates/admin/components/ui/pagination.html +81 -0
  74. polyadmin/templates/admin/components/ui/radio-group.html +28 -0
  75. polyadmin/templates/admin/components/ui/select.html +165 -0
  76. polyadmin/templates/admin/components/ui/sidebar.html +175 -0
  77. polyadmin/templates/admin/components/ui/slider.html +22 -0
  78. polyadmin/templates/admin/components/ui/switch.html +36 -0
  79. polyadmin/templates/admin/components/ui/table.html +221 -0
  80. polyadmin/templates/admin/components/ui/theme-toggle.html +33 -0
  81. polyadmin/templates/admin/dashboard.html +35 -0
  82. polyadmin/templates/admin/error.html +33 -0
  83. polyadmin/templates/admin/login.html +94 -0
  84. polyadmin/templates/admin/resource/delete.html +29 -0
  85. polyadmin/templates/admin/resource/delete_selected.html +49 -0
  86. polyadmin/templates/admin/resource/detail.html +78 -0
  87. polyadmin/templates/admin/resource/form.html +5 -0
  88. polyadmin/templates/admin/resource/list.html +5 -0
  89. polyadmin/templates/admin/theme.html +372 -0
  90. polyadmin/templates/admin/widgets/activity.html +8 -0
  91. polyadmin/templates/admin/widgets/chart.html +15 -0
  92. polyadmin/templates/admin/widgets/donut.html +59 -0
  93. polyadmin/templates/admin/widgets/metric.html +1 -0
  94. polyadmin/templates/admin/widgets/progress.html +7 -0
  95. polyadmin/templates/admin/widgets/stat.html +22 -0
  96. polyadmin/templates/admin/widgets/table.html +29 -0
  97. polyadmin/templates/admin/widgets/tabs.html +34 -0
  98. polyadmin/templates/admin/widgets/timeline.html +21 -0
  99. polyadmin/templating.py +528 -0
  100. polyadmin/ui.py +817 -0
  101. polyadmin-0.1.0b1.dist-info/METADATA +239 -0
  102. polyadmin-0.1.0b1.dist-info/RECORD +104 -0
  103. polyadmin-0.1.0b1.dist-info/WHEEL +4 -0
  104. polyadmin-0.1.0b1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,496 @@
1
+ # Russian translations of PolyAdmin's framework strings.
2
+ #
3
+ # DRAFT: machine-assisted and not yet reviewed by a native speaker.
4
+ # Corrections are welcome; docs/i18n.md ("Maintaining the framework
5
+ # catalogs") describes how to regenerate and test this file.
6
+ msgid ""
7
+ msgstr ""
8
+ "Project-Id-Version: PolyAdmin 0.1.0\n"
9
+ "Report-Msgid-Bugs-To: https://github.com/MagicRodri/polyadmin/issues\n"
10
+ "POT-Creation-Date: 2026-09-21 18:34+0500\n"
11
+ "PO-Revision-Date: 2026-09-14 22:04+0500\n"
12
+ "Last-Translator: Draft, awaiting native review\n"
13
+ "Language: ru\n"
14
+ "Language-Team: Russian (unreviewed draft)\n"
15
+ "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
16
+ "MIME-Version: 1.0\n"
17
+ "Content-Type: text/plain; charset=utf-8\n"
18
+ "Content-Transfer-Encoding: 8bit\n"
19
+ "Generated-By: Babel 2.18.0\n"
20
+
21
+ #, python-format
22
+ msgid "%(label)s (%(num)d)"
23
+ msgstr "%(label)s (%(num)d)"
24
+
25
+ #, python-format
26
+ msgid "%(label)s applied to %(num)d record."
27
+ msgid_plural "%(label)s applied to %(num)d records."
28
+ msgstr[0] "%(label)s применено к %(num)d записи."
29
+ msgstr[1] "%(label)s применено к %(num)d записям."
30
+ msgstr[2] "%(label)s применено к %(num)d записям."
31
+
32
+ #, python-format
33
+ msgid "%(label)s is required."
34
+ msgstr "«%(label)s» обязательно для заполнения."
35
+
36
+ #, python-format
37
+ msgid "%(n)s of %(num)d row selected"
38
+ msgid_plural "%(n)s of %(num)d rows selected"
39
+ msgstr[0] "%(n)s из %(num)d строки выбрано"
40
+ msgstr[1] "%(n)s из %(num)d строк выбрано"
41
+ msgstr[2] "%(n)s из %(num)d строк выбрано"
42
+
43
+ #. Translators: %(name)s is the model's name. French and Russian
44
+ #. nouns carry gender, so phrase around agreement.
45
+ #, python-format
46
+ msgid "%(name)s created."
47
+ msgstr "«%(name)s»: запись создана."
48
+
49
+ #. Translators: %(name)s is the model's name. French and Russian
50
+ #. nouns carry gender, so phrase around agreement.
51
+ #, python-format
52
+ msgid "%(name)s deleted."
53
+ msgstr "«%(name)s»: запись удалена."
54
+
55
+ #. Translators: %(name)s is the model's name. French and Russian
56
+ #. nouns carry gender, so phrase around agreement.
57
+ #, python-format
58
+ msgid "%(name)s updated."
59
+ msgstr "«%(name)s»: запись обновлена."
60
+
61
+ #, python-format
62
+ msgid "%(num)d row"
63
+ msgid_plural "%(num)d rows"
64
+ msgstr[0] "%(num)d строка"
65
+ msgstr[1] "%(num)d строки"
66
+ msgstr[2] "%(num)d строк"
67
+
68
+ #, python-format
69
+ msgid "%(num)d you can't view"
70
+ msgid_plural "%(num)d you can't view"
71
+ msgstr[0] "%(num)d запись, недоступная вам"
72
+ msgstr[1] "%(num)d записи, недоступные вам"
73
+ msgstr[2] "%(num)d записей, недоступных вам"
74
+
75
+ msgid "Action…"
76
+ msgstr "Действие…"
77
+
78
+ msgid "Add"
79
+ msgstr "Добавить"
80
+
81
+ msgid "Admin"
82
+ msgstr "Администрирование"
83
+
84
+ msgid "All"
85
+ msgstr "Все"
86
+
87
+ msgid "Anonymous"
88
+ msgstr "Аноним"
89
+
90
+ msgid "Any date"
91
+ msgstr "Любая дата"
92
+
93
+ msgid "Apply"
94
+ msgstr "Применить"
95
+
96
+ msgid "Asc"
97
+ msgstr "По возрастанию"
98
+
99
+ msgid "Back to the admin"
100
+ msgstr "Вернуться в панель администратора"
101
+
102
+ msgid "Breadcrumb"
103
+ msgstr "Хлебные крошки"
104
+
105
+ msgid "CSV"
106
+ msgstr "CSV"
107
+
108
+ msgid "Cancel"
109
+ msgstr "Отмена"
110
+
111
+ msgid "Clear all"
112
+ msgstr "Очистить всё"
113
+
114
+ msgid "Clear selection"
115
+ msgstr "Снять выделение"
116
+
117
+ msgid "Close"
118
+ msgstr "Закрыть"
119
+
120
+ msgid "Close filters"
121
+ msgstr "Закрыть фильтры"
122
+
123
+ msgid "Confirm"
124
+ msgstr "Подтвердить"
125
+
126
+ #, python-format
127
+ msgid "Couldn't save %(name)s"
128
+ msgstr "Не удалось сохранить «%(name)s»"
129
+
130
+ msgid "Custom range"
131
+ msgstr "Произвольный период"
132
+
133
+ msgid "Dark mode"
134
+ msgstr "Тёмная тема"
135
+
136
+ msgid "Dashboard"
137
+ msgstr "Панель управления"
138
+
139
+ msgid "Delete"
140
+ msgstr "Удалить"
141
+
142
+ #, python-format
143
+ msgid "Delete %(name)s"
144
+ msgstr "Удалить %(name)s"
145
+
146
+ #, python-format
147
+ msgid "Delete %(num)d record?"
148
+ msgid_plural "Delete %(num)d records?"
149
+ msgstr[0] "Удалить %(num)d запись?"
150
+ msgstr[1] "Удалить %(num)d записи?"
151
+ msgstr[2] "Удалить %(num)d записей?"
152
+
153
+ msgid "Delete selected"
154
+ msgstr "Удалить выбранное"
155
+
156
+ msgid "Delete the selected records? This cannot be undone."
157
+ msgstr "Удалить выбранные записи? Это действие нельзя отменить."
158
+
159
+ #, python-format
160
+ msgid "Delete this %(name)s?"
161
+ msgstr "Удалить «%(name)s»?"
162
+
163
+ #, python-format
164
+ msgid "Delete «%(label)s»?"
165
+ msgstr "Удалить «%(label)s»?"
166
+
167
+ #. Translators: a bulk delete stopped part-way. %(deleted)d of
168
+ #. %(total)d records were deleted; %(error)s is the underlying error.
169
+ #, python-format
170
+ msgid "Deleted %(deleted)d of %(total)d, then failed: %(error)s"
171
+ msgstr "Удалено %(deleted)d из %(total)d, затем сбой: %(error)s"
172
+
173
+ #, python-format
174
+ msgid "Deleted %(num)d record."
175
+ msgid_plural "Deleted %(num)d records."
176
+ msgstr[0] "Удалена %(num)d запись."
177
+ msgstr[1] "Удалено %(num)d записи."
178
+ msgstr[2] "Удалено %(num)d записей."
179
+
180
+ msgid "Desc"
181
+ msgstr "По убыванию"
182
+
183
+ msgid "Drag to reorder"
184
+ msgstr "Перетащите, чтобы изменить порядок"
185
+
186
+ msgid "Drag to reorder (not saved)"
187
+ msgstr "Перетащите, чтобы изменить порядок (не сохраняется)"
188
+
189
+ msgid "Edit"
190
+ msgstr "Изменить"
191
+
192
+ msgid "Email"
193
+ msgstr "Эл. почта"
194
+
195
+ msgid "Empty"
196
+ msgstr "Пусто"
197
+
198
+ msgid "Everything is selected."
199
+ msgstr "Выбрано всё."
200
+
201
+ msgid "Export"
202
+ msgstr "Экспорт"
203
+
204
+ msgid "Filters"
205
+ msgstr "Фильтры"
206
+
207
+ msgid "First page"
208
+ msgstr "Первая страница"
209
+
210
+ msgid "From"
211
+ msgstr "С"
212
+
213
+ msgid "History"
214
+ msgstr "История"
215
+
216
+ msgid "Language"
217
+ msgstr "Язык"
218
+
219
+ msgid "Last page"
220
+ msgstr "Последняя страница"
221
+
222
+ msgid "Light mode"
223
+ msgstr "Светлая тема"
224
+
225
+ msgid "New"
226
+ msgstr "Новый"
227
+
228
+ msgid "Next month"
229
+ msgstr "Следующий месяц"
230
+
231
+ msgid "Next page"
232
+ msgstr "Следующая страница"
233
+
234
+ msgid "No"
235
+ msgstr "Нет"
236
+
237
+ #, python-format
238
+ msgid "No %(label)s."
239
+ msgstr "Нет %(label)s."
240
+
241
+ msgid "No data."
242
+ msgstr "Нет данных."
243
+
244
+ msgid "No items selected."
245
+ msgstr "Ничего не выбрано."
246
+
247
+ msgid "No matches."
248
+ msgstr "Совпадений не найдено."
249
+
250
+ msgid "No options."
251
+ msgstr "Нет вариантов."
252
+
253
+ msgid "No panels configured."
254
+ msgstr "Панели не настроены."
255
+
256
+ msgid "No recent activity."
257
+ msgstr "Нет недавней активности."
258
+
259
+ msgid "No records."
260
+ msgstr "Нет записей."
261
+
262
+ msgid "No resources registered."
263
+ msgstr "Нет зарегистрированных ресурсов."
264
+
265
+ msgid "No widgets configured."
266
+ msgstr "Виджеты не настроены."
267
+
268
+ msgid "Not empty"
269
+ msgstr "Не пусто"
270
+
271
+ msgid "Not found"
272
+ msgstr "Не найдено"
273
+
274
+ msgid "Notifications"
275
+ msgstr "Уведомления"
276
+
277
+ msgid "Open calendar"
278
+ msgstr "Открыть календарь"
279
+
280
+ msgid "Open menu"
281
+ msgstr "Открыть меню"
282
+
283
+ msgid "Page"
284
+ msgstr "Страница"
285
+
286
+ #, python-format
287
+ msgid "Page %(page)d of %(pages)d"
288
+ msgstr "Страница %(page)d из %(pages)d"
289
+
290
+ msgid "Password"
291
+ msgstr "Пароль"
292
+
293
+ msgid "Past 7 days"
294
+ msgstr "Последние 7 дней"
295
+
296
+ msgid "Permission denied"
297
+ msgstr "Доступ запрещён"
298
+
299
+ msgid "Platform"
300
+ msgstr "Платформа"
301
+
302
+ msgid "PolyAdmin"
303
+ msgstr "PolyAdmin"
304
+
305
+ msgid "Previous month"
306
+ msgstr "Предыдущий месяц"
307
+
308
+ msgid "Previous page"
309
+ msgstr "Предыдущая страница"
310
+
311
+ msgid "Remove"
312
+ msgstr "Убрать"
313
+
314
+ #, python-format
315
+ msgid "Remove %(label)s"
316
+ msgstr "Убрать %(label)s"
317
+
318
+ #, python-format
319
+ msgid "Remove this %(label)s?"
320
+ msgstr "Убрать «%(label)s»?"
321
+
322
+ msgid "Rows per page"
323
+ msgstr "Строк на странице"
324
+
325
+ msgid "Save"
326
+ msgstr "Сохранить"
327
+
328
+ msgid "Save and add another"
329
+ msgstr "Сохранить и добавить ещё"
330
+
331
+ msgid "Save and continue editing"
332
+ msgstr "Сохранить и продолжить редактирование"
333
+
334
+ msgid "Save as new"
335
+ msgstr "Сохранить как новый"
336
+
337
+ #, python-format
338
+ msgid "Save to add %(label)s."
339
+ msgstr "Сохраните, чтобы добавить %(label)s."
340
+
341
+ msgid "Search…"
342
+ msgstr "Поиск…"
343
+
344
+ msgid "Security check failed"
345
+ msgstr "Проверка безопасности не пройдена"
346
+
347
+ msgid "Select all"
348
+ msgstr "Выбрать всё"
349
+
350
+ msgid "Select row"
351
+ msgstr "Выбрать строку"
352
+
353
+ #, python-format
354
+ msgid "Select the %(num)d matching"
355
+ msgid_plural "Select all %(num)d matching"
356
+ msgstr[0] "Выбрать %(num)d совпадающую запись"
357
+ msgstr[1] "Выбрать все %(num)d совпадающие записи"
358
+ msgstr[2] "Выбрать все %(num)d совпадающих записей"
359
+
360
+ msgid "Selected records"
361
+ msgstr "Выбранные записи"
362
+
363
+ msgid "Select…"
364
+ msgstr "Выбрать…"
365
+
366
+ msgid "Sign in"
367
+ msgstr "Войти"
368
+
369
+ #, python-format
370
+ msgid "Sign in to %(site)s"
371
+ msgstr "Войти в %(site)s"
372
+
373
+ msgid "Sign out"
374
+ msgstr "Выйти"
375
+
376
+ msgid "Sign-in could not be completed. Please try again."
377
+ msgstr "Не удалось выполнить вход. Попробуйте снова."
378
+
379
+ msgid "Sign-in required"
380
+ msgstr "Требуется вход"
381
+
382
+ msgid "Signed in"
383
+ msgstr "Вход выполнен"
384
+
385
+ msgid "Superuser"
386
+ msgstr "Суперпользователь"
387
+
388
+ msgid "That email and password don't match an account."
389
+ msgstr "Такой адрес электронной почты и пароль не соответствуют ни одной учётной записи."
390
+
391
+ msgid "That record doesn't exist, or it was deleted."
392
+ msgstr "Такой записи не существует, либо она была удалена."
393
+
394
+ #, python-format
395
+ msgid "The %(num)d matching this filter is selected."
396
+ msgid_plural "All %(num)d matching this filter are selected."
397
+ msgstr[0] "%(num)d запись, соответствующая этому фильтру, выбрана."
398
+ msgstr[1] "Все %(num)d записи, соответствующие этому фильтру, выбраны."
399
+ msgstr[2] "Все %(num)d записей, соответствующих этому фильтру, выбраны."
400
+
401
+ #, python-format
402
+ msgid "The %(num)d on this page is selected."
403
+ msgid_plural "All %(num)d on this page are selected."
404
+ msgstr[0] "%(num)d запись на этой странице выбрана."
405
+ msgstr[1] "Все %(num)d записи на этой странице выбраны."
406
+ msgstr[2] "Все %(num)d записей на этой странице выбраны."
407
+
408
+ msgid "The selection changed since you reviewed it."
409
+ msgstr "Выбор изменился с момента проверки."
410
+
411
+ msgid "These records must be removed first:"
412
+ msgstr "Сначала нужно удалить эти записи:"
413
+
414
+ msgid "This action cannot be undone."
415
+ msgstr "Это действие нельзя отменить."
416
+
417
+ msgid "This can't be deleted"
418
+ msgstr "Удаление невозможно"
419
+
420
+ msgid "This month"
421
+ msgstr "В этом месяце"
422
+
423
+ msgid "This page expired before the form was submitted. Reload and try again."
424
+ msgstr "Срок действия страницы истёк до отправки формы. Обновите страницу и попробуйте снова."
425
+
426
+ msgid "This will also delete"
427
+ msgstr "Также будут удалены"
428
+
429
+ msgid "This year"
430
+ msgstr "В этом году"
431
+
432
+ msgid "To"
433
+ msgstr "По"
434
+
435
+ msgid "Today"
436
+ msgstr "Сегодня"
437
+
438
+ msgid "Toggle dark mode"
439
+ msgstr "Переключить тёмную тему"
440
+
441
+ msgid "Toggle sidebar"
442
+ msgstr "Переключить боковую панель"
443
+
444
+ msgid "Total"
445
+ msgstr "Всего"
446
+
447
+ msgid "View"
448
+ msgstr "Просмотр"
449
+
450
+ msgid "Welcome back"
451
+ msgstr "С возвращением"
452
+
453
+ msgid "XLSX"
454
+ msgstr "XLSX"
455
+
456
+ msgid "Yes"
457
+ msgstr "Да"
458
+
459
+ msgid "You have been signed out."
460
+ msgstr "Вы вышли из системы."
461
+
462
+ msgid "You need to be signed in to view this page."
463
+ msgstr "Чтобы просмотреть эту страницу, войдите в систему."
464
+
465
+ #, python-format
466
+ msgid "Your account doesn't have permission to delete: %(types)s"
467
+ msgstr "У вашей учётной записи нет права удалять: %(types)s"
468
+
469
+ msgid "Your account doesn't have permission to do that."
470
+ msgstr "У вашей учётной записи нет прав для этого действия."
471
+
472
+ msgid "create"
473
+ msgstr "создание"
474
+
475
+ msgid "delete"
476
+ msgstr "удаление"
477
+
478
+ msgid "m@example.com"
479
+ msgstr "m@example.com"
480
+
481
+ msgid "no change"
482
+ msgstr "без изменений"
483
+
484
+ msgid "system"
485
+ msgstr "система"
486
+
487
+ msgid "update"
488
+ msgstr "изменение"
489
+
490
+ #, python-format
491
+ msgid "…and %(num)d more"
492
+ msgid_plural "…and %(num)d more"
493
+ msgstr[0] "…и ещё %(num)d"
494
+ msgstr[1] "…и ещё %(num)d"
495
+ msgstr[2] "…и ещё %(num)d"
496
+
@@ -0,0 +1,91 @@
1
+ {% from "admin/components/icons.html" import icon %}
2
+ {% from "admin/components/ui/theme-toggle.html" import theme_toggle %}
3
+ {% from "admin/components/ui/locale-switcher.html" import locale_switcher_menu %}
4
+ {% from "admin/components/ui/calendar.html" import calendar_script %}
5
+ {% from "admin/components/ui/select.html" import select_script %}
6
+ {% from "admin/components/ui/multi-select.html" import multi_select_script %}
7
+ <!doctype html>
8
+ <html lang="{{ locale }}" class="h-full">
9
+ <head>
10
+ <meta charset="utf-8">
11
+ <meta name="viewport" content="width=device-width, initial-scale=1">
12
+ {# The CSRF token, for JS. The cookie is HttpOnly precisely so that
13
+ scripts read it here instead -- see theme.html's htmx:configRequest
14
+ listener. #}
15
+ <meta name="csrf-token" content="{{ csrf_token }}">
16
+ <title>{% block title %}{{ _(site_title) }}{% endblock %} &middot; {{ _(site_title) }}</title>
17
+ {# Design tokens, Tailwind config, Alpine + plugins, HTMX, and the
18
+ theme store -- all of it, in the one load order that works. See
19
+ admin/theme.html. #}
20
+ {% include "admin/theme.html" %}
21
+ {{ calendar_script() }}
22
+ {{ select_script() }}
23
+ {{ multi_select_script() }}
24
+ {% block extra_head %}{% endblock %}
25
+ </head>
26
+ {# These resolve through theme.html's variables, so the admin follows
27
+ the active theme without a template knowing which one it is. #}
28
+ {# SidebarProvider, from shadcn's sidebar-07 block. Its state lives on
29
+ <body> so every descendant can read it: `collapsed` is the desktop
30
+ icon-rail mode, seeded from the sidebar_state cookie so the choice
31
+ survives a reload; `sidebarOpen` is the mobile sheet, a separate
32
+ axis, because a phone has an overlay rather than a rail. Cmd/Ctrl+B
33
+ is wired here too, so it works wherever focus is. #}
34
+ <body class="min-h-full bg-background text-foreground antialiased"
35
+ x-data="{
36
+ sidebarOpen: false,
37
+ collapsed: (document.cookie.match(/(?:^|; )sidebar_state=([^;]*)/) || [])[1] === 'collapsed',
38
+ toggle() {
39
+ this.collapsed = !this.collapsed;
40
+ document.cookie = 'sidebar_state=' + (this.collapsed ? 'collapsed' : 'expanded') +
41
+ '; path=/; max-age=' + 60 * 60 * 24 * 7;
42
+ }
43
+ }"
44
+ @keydown.window.meta.b.prevent="toggle()"
45
+ @keydown.window.ctrl.b.prevent="toggle()">
46
+ {% include "admin/components/toasts.html" %}
47
+ {% include "admin/components/action_confirm_modal.html" %}
48
+ <div class="flex min-h-full">
49
+ {# Below md the sidebar *is* a sheet -- overlay plus off-canvas
50
+ panel -- and from md up it becomes the static collapsible
51
+ column. One element does both jobs, so there is no duplicate nav
52
+ markup to keep in sync. #}
53
+ <div x-cloak x-show="sidebarOpen" x-transition.opacity.duration.300ms=""
54
+ @click="sidebarOpen = false" class="{{ ui('sheet', 'overlay') }} md:hidden"></div>
55
+
56
+ {% include "admin/components/ui/sidebar.html" %}
57
+
58
+ {# SidebarInset. #}
59
+ <div class="{{ ui('sidebar', 'inset') }}">
60
+ {# The block's header: trigger, a short vertical rule, then the
61
+ breadcrumbs. h-16 fixed, so it doesn't jump as content changes. #}
62
+ <header class="{{ ui('sidebar', 'topbar') }}">
63
+ <button type="button" @click="window.innerWidth < 768 ? sidebarOpen = true : toggle()"
64
+ class="-ml-1 {{ ui('button', 'ghost-muted', 'size-icon-sm') }}">
65
+ {{ icon("sidebar", "w-4 h-4") }}
66
+ <span class="sr-only">{{ _("Toggle sidebar") }}</span>
67
+ </button>
68
+ <div class="{{ ui('separator', 'vertical') }} mr-2 !h-4"></div>
69
+ <div class="min-w-0 flex-1">
70
+ {% if breadcrumbs %}
71
+ {% from "admin/components/ui/breadcrumb.html" import render_breadcrumbs %}
72
+ {{ render_breadcrumbs(breadcrumbs, base_path) }}
73
+ {% endif %}
74
+ </div>
75
+ {% if locale_switcher %}{{ locale_switcher_menu(locale_switcher, base_path, csrf_token) }}{% endif %}
76
+ {{ theme_toggle() }}
77
+ </header>
78
+ {# The page itself scrolls -- one scrollbar, the browser's own, for
79
+ however long the table is. So main must NOT become a scroll
80
+ container: overflow-x-clip clips a wide table without the side
81
+ effect overflow-x-hidden has, which is to turn the other axis
82
+ into auto and box the content in a scrollbar of its own. The
83
+ sidebar stays put by being sticky, not by the content scrolling
84
+ underneath it. #}
85
+ <main class="min-w-0 flex-1 overflow-x-clip p-4">
86
+ {% block content %}{% endblock %}
87
+ </main>
88
+ </div>
89
+ </div>
90
+ </body>
91
+ </html>
@@ -0,0 +1,86 @@
1
+ {# Confirmation modal, the Dialog port from shadcn/ui. It covers both
2
+ places an "are you sure" prompt appears: an Action's form, and the
3
+ list's per-row Delete. One teleported instance backs both, opening on
4
+ a window-level `action-confirm-request` event carrying
5
+ `{message, onConfirm}`. `onConfirm` is a plain callback because a
6
+ form submit and htmx's confirm hook need different follow-through
7
+ (`form.submit()` versus `issueRequest(true)`), so the modal stays
8
+ agnostic.
9
+
10
+ The confirmation text is never serialized into a JS or HTML string:
11
+ it arrives as an attribute value or as htmx's own event detail, and
12
+ is handed to the modal through a CustomEvent as a live JS value,
13
+ never re-embedded into markup. #}
14
+ <template x-teleport="body">
15
+ {# Assigning the callback straight from the event in one statement
16
+ makes Alpine invoke it immediately rather than store it: the modal
17
+ would flash open with the action already done. Routing it through a
18
+ plain local first avoids that. #}
19
+ <div x-data="{ open: false, message: '', onConfirm: null }"
20
+ @action-confirm-request.window="let cb = $event.detail.onConfirm; onConfirm = cb; message = $event.detail.message; open = true"
21
+ @keydown.escape.window="open = false"
22
+ x-show="open" x-cloak
23
+ class="fixed inset-0 z-[99]">
24
+ <div x-show="open"
25
+ x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
26
+ x-transition:leave="ease-in duration-200" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
27
+ @click="open = false" class="{{ ui('dialog', 'overlay') }}"></div>
28
+ <div class="{{ ui('dialog', 'container') }}">
29
+ <div x-show="open"
30
+ x-trap.inert.noscroll="open"
31
+ x-transition:enter="ease-out duration-300"
32
+ x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
33
+ x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
34
+ x-transition:leave="ease-in duration-200"
35
+ x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
36
+ x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
37
+ class="{{ ui('dialog', 'content-sm') }}">
38
+ <h3 class="{{ ui('dialog', 'title') }}">{{ _("Confirm") }}</h3>
39
+ <p class="{{ ui('dialog', 'description') }}" x-text="message"></p>
40
+ <div class="{{ ui('dialog', 'footer') }}">
41
+ <button type="button" @click="open = false; onConfirm = null"
42
+ class="{{ ui('button', 'outline', 'size-sm') }}">
43
+ {{ _("Cancel") }}
44
+ </button>
45
+ <button type="button"
46
+ @click="open = false; if (onConfirm) { onConfirm(); onConfirm = null; }"
47
+ class="{{ ui('button', 'size-sm') }}">
48
+ {{ _("Confirm") }}
49
+ </button>
50
+ </div>
51
+ </div>
52
+ </div>
53
+ </div>
54
+ </template>
55
+
56
+ <script>
57
+ // Wired into a form's onsubmit as:
58
+ // onsubmit="return window.adminConfirmSubmit(event, this, this.dataset.confirm)"
59
+ // `message` falsy (no confirmation required for this action) lets the
60
+ // submit through untouched.
61
+ window.adminConfirmSubmit = window.adminConfirmSubmit || function (event, form, message) {
62
+ if (!message) return true;
63
+ event.preventDefault();
64
+ window.dispatchEvent(new CustomEvent("action-confirm-request", {
65
+ detail: { message: message, onConfirm: function () { form.submit(); } },
66
+ }));
67
+ return false;
68
+ };
69
+
70
+ // Replaces htmx's native window.confirm() (triggered by hx-confirm,
71
+ // e.g. the list view's row-level Delete button) with this same
72
+ // modal. htmx fires "htmx:confirm" before every hx-* request and, if
73
+ // preventDefault() is called, waits for evt.detail.issueRequest(true)
74
+ // instead of proceeding immediately -- this is htmx's documented hook
75
+ // for swapping in a custom confirmation dialog.
76
+ document.body.addEventListener("htmx:confirm", function (evt) {
77
+ if (!evt.detail.question) return;
78
+ evt.preventDefault();
79
+ window.dispatchEvent(new CustomEvent("action-confirm-request", {
80
+ detail: {
81
+ message: evt.detail.question,
82
+ onConfirm: function () { evt.detail.issueRequest(true); },
83
+ },
84
+ }));
85
+ });
86
+ </script>
@@ -0,0 +1,5 @@
1
+ {# The CSRF token as a hidden field, for forms that can submit with no
2
+ JavaScript. htmx requests get it as a header instead, so a form that
3
+ only ever submits via hx-post does not need this. Takes the token
4
+ itself, not the whole context: {{ csrf_field(csrf_token) }}. #}
5
+ {% macro csrf_field(token) %}<input type="hidden" name="_csrf" value="{{ token }}">{% endmacro %}
@@ -0,0 +1,6 @@
1
+ {# The same failure as admin/error.html, for an htmx request. A whole
2
+ document swapped into a table cell would be nonsense, so this is just the
3
+ alert -- htmx puts it where the failed interaction was. #}
4
+ <div role="alert" class="{{ ui('alert', 'destructive') }}">
5
+ <p class="{{ ui('alert', 'description') }}">{{ message }}</p>
6
+ </div>