django-trellis 0.1.0__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.
Files changed (200) hide show
  1. django_trellis-0.1.0/.gitignore +28 -0
  2. django_trellis-0.1.0/LICENSE +21 -0
  3. django_trellis-0.1.0/PKG-INFO +16 -0
  4. django_trellis-0.1.0/README.md +1 -0
  5. django_trellis-0.1.0/pyproject.toml +88 -0
  6. django_trellis-0.1.0/trellis_ui/__init__.py +30 -0
  7. django_trellis-0.1.0/trellis_ui/apps.py +6 -0
  8. django_trellis-0.1.0/trellis_ui/context_processors.py +6 -0
  9. django_trellis-0.1.0/trellis_ui/events.py +15 -0
  10. django_trellis-0.1.0/trellis_ui/filters/__init__.py +3 -0
  11. django_trellis-0.1.0/trellis_ui/filters/rules.py +148 -0
  12. django_trellis-0.1.0/trellis_ui/forms/__init__.py +22 -0
  13. django_trellis-0.1.0/trellis_ui/forms/base.py +23 -0
  14. django_trellis-0.1.0/trellis_ui/forms/boundfield.py +33 -0
  15. django_trellis-0.1.0/trellis_ui/forms/formsets.py +20 -0
  16. django_trellis-0.1.0/trellis_ui/forms/presets/__init__.py +3 -0
  17. django_trellis-0.1.0/trellis_ui/forms/presets/dates.py +164 -0
  18. django_trellis-0.1.0/trellis_ui/forms/renderers.py +21 -0
  19. django_trellis-0.1.0/trellis_ui/forms/widgets.py +698 -0
  20. django_trellis-0.1.0/trellis_ui/generated_styles.py +5 -0
  21. django_trellis-0.1.0/trellis_ui/static/trellis_ui/css/styles.css +4382 -0
  22. django_trellis-0.1.0/trellis_ui/static/trellis_ui/js/index.js +10 -0
  23. django_trellis-0.1.0/trellis_ui/stream.py +356 -0
  24. django_trellis-0.1.0/trellis_ui/targets.py +10 -0
  25. django_trellis-0.1.0/trellis_ui/templates/cotton/icons/add.html +17 -0
  26. django_trellis-0.1.0/trellis_ui/templates/cotton/icons/calendar.html +18 -0
  27. django_trellis-0.1.0/trellis_ui/templates/cotton/icons/checkmark.html +18 -0
  28. django_trellis-0.1.0/trellis_ui/templates/cotton/icons/chevron__down.html +17 -0
  29. django_trellis-0.1.0/trellis_ui/templates/cotton/icons/chevron__right.html +17 -0
  30. django_trellis-0.1.0/trellis_ui/templates/cotton/icons/close.html +19 -0
  31. django_trellis-0.1.0/trellis_ui/templates/cotton/icons/cloud__upload.html +8 -0
  32. django_trellis-0.1.0/trellis_ui/templates/cotton/icons/document__blank.html +10 -0
  33. django_trellis-0.1.0/trellis_ui/templates/cotton/icons/grip.html +21 -0
  34. django_trellis-0.1.0/trellis_ui/templates/cotton/icons/image.html +9 -0
  35. django_trellis-0.1.0/trellis_ui/templates/cotton/icons/menu.html +18 -0
  36. django_trellis-0.1.0/trellis_ui/templates/cotton/icons/moon.html +10 -0
  37. django_trellis-0.1.0/trellis_ui/templates/cotton/icons/overflow_menu__vertical.html +20 -0
  38. django_trellis-0.1.0/trellis_ui/templates/cotton/icons/search.html +18 -0
  39. django_trellis-0.1.0/trellis_ui/templates/cotton/icons/side_panel.html +15 -0
  40. django_trellis-0.1.0/trellis_ui/templates/cotton/icons/side_panel__close.html +17 -0
  41. django_trellis-0.1.0/trellis_ui/templates/cotton/icons/side_panel__open.html +17 -0
  42. django_trellis-0.1.0/trellis_ui/templates/cotton/icons/spinner.html +13 -0
  43. django_trellis-0.1.0/trellis_ui/templates/cotton/icons/subtract.html +17 -0
  44. django_trellis-0.1.0/trellis_ui/templates/cotton/icons/sun.html +10 -0
  45. django_trellis-0.1.0/trellis_ui/templates/cotton/icons/theme.html +13 -0
  46. django_trellis-0.1.0/trellis_ui/templates/cotton/icons/trash_can.html +18 -0
  47. django_trellis-0.1.0/trellis_ui/templates/cotton/layouts/app.html +57 -0
  48. django_trellis-0.1.0/trellis_ui/templates/cotton/layouts/base.html +88 -0
  49. django_trellis-0.1.0/trellis_ui/templates/cotton/layouts/guest.html +50 -0
  50. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/accordion/content.html +14 -0
  51. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/accordion/index.html +30 -0
  52. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/accordion/item.html +40 -0
  53. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/accordion/trigger.html +49 -0
  54. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/action/index.html +16 -0
  55. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/app/aside.html +7 -0
  56. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/app/footer.html +7 -0
  57. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/app/header.html +10 -0
  58. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/app/index.html +19 -0
  59. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/app/main.html +13 -0
  60. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/badge/close.html +24 -0
  61. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/badge/index.html +87 -0
  62. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/box/index.html +34 -0
  63. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/brand/index.html +66 -0
  64. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/button/group.html +7 -0
  65. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/button/index.html +92 -0
  66. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/card/action.html +7 -0
  67. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/card/content.html +7 -0
  68. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/card/description.html +12 -0
  69. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/card/footer.html +11 -0
  70. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/card/header.html +7 -0
  71. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/card/index.html +42 -0
  72. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/card/title.html +13 -0
  73. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/checkbox/control.html +104 -0
  74. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/checkbox/group.html +28 -0
  75. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/checkbox/index.html +62 -0
  76. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/checkbox/indicator.html +18 -0
  77. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/combobox/empty.html +13 -0
  78. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/combobox/group.html +7 -0
  79. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/combobox/impl.html +141 -0
  80. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/combobox/index.html +32 -0
  81. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/combobox/label.html +13 -0
  82. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/combobox/option.html +70 -0
  83. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/combobox/separator.html +11 -0
  84. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/date_picker/impl.html +287 -0
  85. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/date_picker/index.html +33 -0
  86. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/date_picker/preset.html +20 -0
  87. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/date_picker/presets.html +10 -0
  88. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/date_picker/trigger.html +13 -0
  89. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/dropdown/index.html +25 -0
  90. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/field/description.html +10 -0
  91. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/field/error.html +17 -0
  92. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/field/group.html +10 -0
  93. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/field/index.html +47 -0
  94. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/field/label.html +15 -0
  95. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/fieldset/index.html +37 -0
  96. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/fieldset/legend.html +14 -0
  97. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/file_upload/control.html +151 -0
  98. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/file_upload/dropzone.html +39 -0
  99. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/file_upload/index.html +36 -0
  100. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/file_upload/item.html +45 -0
  101. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/file_upload/list.html +7 -0
  102. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/file_upload/remove.html +16 -0
  103. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/frame/body.html +17 -0
  104. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/frame/footer.html +13 -0
  105. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/frame/header.html +13 -0
  106. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/frame/index.html +26 -0
  107. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/heading/index.html +7 -0
  108. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/image/index.html +20 -0
  109. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/input/control.html +1 -0
  110. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/input/group.html +28 -0
  111. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/input/impl.html +122 -0
  112. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/input/index.html +27 -0
  113. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/input/prefix.html +5 -0
  114. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/input/suffix.html +5 -0
  115. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/menu/group.html +7 -0
  116. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/menu/index.html +17 -0
  117. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/menu/item.html +87 -0
  118. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/menu/label.html +14 -0
  119. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/menu/separator.html +24 -0
  120. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/modal/body.html +7 -0
  121. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/modal/close.html +21 -0
  122. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/modal/description.html +12 -0
  123. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/modal/footer.html +7 -0
  124. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/modal/index.html +78 -0
  125. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/modal/outlet.html +11 -0
  126. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/modal/title.html +12 -0
  127. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/modal/trigger.html +13 -0
  128. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/overlay/above.html +7 -0
  129. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/overlay/index.html +15 -0
  130. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/radio/group.html +32 -0
  131. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/radio/index.html +93 -0
  132. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/radio/indicator.html +11 -0
  133. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/scroll_area/index.html +78 -0
  134. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/select/button.html +12 -0
  135. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/select/group.html +7 -0
  136. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/select/impl.html +254 -0
  137. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/select/index.html +33 -0
  138. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/select/input.html +13 -0
  139. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/select/label.html +12 -0
  140. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/select/option/create.html +14 -0
  141. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/select/option/empty.html +11 -0
  142. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/select/option/selected.html +3 -0
  143. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/select/option.html +76 -0
  144. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/select/search.html +30 -0
  145. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/select/separator.html +11 -0
  146. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/select/trigger.html +11 -0
  147. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/separator/index.html +42 -0
  148. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/sidebar/content.html +10 -0
  149. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/sidebar/footer.html +7 -0
  150. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/sidebar/group.html +73 -0
  151. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/sidebar/header.html +7 -0
  152. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/sidebar/index.html +75 -0
  153. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/sidebar/item.html +108 -0
  154. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/sidebar/nav.html +11 -0
  155. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/sidebar/toggle.html +31 -0
  156. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/slider/index.html +174 -0
  157. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/slider/tick.html +25 -0
  158. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/spacer/index.html +6 -0
  159. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/stream/index.html +3 -0
  160. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/switch/control.html +3 -0
  161. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/switch/impl.html +117 -0
  162. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/switch/index.html +54 -0
  163. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/table/body.html +7 -0
  164. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/table/caption.html +10 -0
  165. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/table/cell.html +11 -0
  166. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/table/column/filter.html +10 -0
  167. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/table/column.html +54 -0
  168. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/table/empty.html +11 -0
  169. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/table/footer.html +7 -0
  170. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/table/header.html +7 -0
  171. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/table/index.html +34 -0
  172. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/table/row/header.html +12 -0
  173. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/table/row.html +13 -0
  174. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/tabs/index.html +27 -0
  175. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/tabs/list.html +28 -0
  176. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/tabs/panel.html +28 -0
  177. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/tabs/panels.html +12 -0
  178. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/tabs/tab.html +98 -0
  179. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/text/index.html +7 -0
  180. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/textarea/control.html +1 -0
  181. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/textarea/impl.html +51 -0
  182. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/textarea/index.html +27 -0
  183. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/theme/text.html +5 -0
  184. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/toast/index.html +65 -0
  185. django_trellis-0.1.0/trellis_ui/templates/cotton/ui/toast/outlet.html +28 -0
  186. django_trellis-0.1.0/trellis_ui/templates/trellis_ui/forms/field.html +22 -0
  187. django_trellis-0.1.0/trellis_ui/templates/trellis_ui/forms/form.html +15 -0
  188. django_trellis-0.1.0/trellis_ui/templates/trellis_ui/forms/formset.html +7 -0
  189. django_trellis-0.1.0/trellis_ui/templates/trellis_ui/forms/widgets/checkbox.html +8 -0
  190. django_trellis-0.1.0/trellis_ui/templates/trellis_ui/forms/widgets/checkbox_select.html +37 -0
  191. django_trellis-0.1.0/trellis_ui/templates/trellis_ui/forms/widgets/combobox_select.html +41 -0
  192. django_trellis-0.1.0/trellis_ui/templates/trellis_ui/forms/widgets/date_picker.html +59 -0
  193. django_trellis-0.1.0/trellis_ui/templates/trellis_ui/forms/widgets/file_upload.html +13 -0
  194. django_trellis-0.1.0/trellis_ui/templates/trellis_ui/forms/widgets/input.html +10 -0
  195. django_trellis-0.1.0/trellis_ui/templates/trellis_ui/forms/widgets/radio_select.html +38 -0
  196. django_trellis-0.1.0/trellis_ui/templates/trellis_ui/forms/widgets/select.html +95 -0
  197. django_trellis-0.1.0/trellis_ui/templates/trellis_ui/forms/widgets/textarea.html +10 -0
  198. django_trellis-0.1.0/trellis_ui/templatetags/__init__.py +1 -0
  199. django_trellis-0.1.0/trellis_ui/templatetags/ui_classes.py +93 -0
  200. django_trellis-0.1.0/trellis_ui/templatetags/ui_props.py +108 -0
@@ -0,0 +1,28 @@
1
+ node_modules/
2
+ .pnpm-store/
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .vite/
8
+ .ruff_cache/
9
+ .pytest_cache/
10
+ __pycache__/
11
+ *.pyc
12
+ .DS_Store
13
+ .idea/
14
+ .coverage
15
+ coverage/
16
+ htmlcov/
17
+ playwright-report/
18
+ test-results/
19
+ /types/
20
+ /trellis_ui/static/trellis_ui/css/styles.css
21
+ /trellis_ui/static/trellis_ui/js/
22
+ /trellis_ui/static/trellis_ui/manifests/
23
+ /trellis_ui/generated_styles.py
24
+ /docs/static/docs/
25
+ *.map
26
+ .agents/
27
+ docs/
28
+ /vite.docs.config.ts
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tristan Robinette
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-trellis
3
+ Version: 0.1.0
4
+ Summary: Token-driven Django Cotton UI component kit.
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Classifier: Framework :: Django
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Requires-Python: >=3.13
11
+ Requires-Dist: django-cotton>=2.7.2
12
+ Requires-Dist: django-eventstream>=5.3.4
13
+ Requires-Dist: django>=6.0
14
+ Description-Content-Type: text/markdown
15
+
16
+ # Trellis UI
@@ -0,0 +1 @@
1
+ # Trellis UI
@@ -0,0 +1,88 @@
1
+ [project]
2
+ name = "django-trellis"
3
+ version = "0.1.0"
4
+ description = "Token-driven Django Cotton UI component kit."
5
+ readme = "README.md"
6
+ requires-python = ">=3.13"
7
+ license = "MIT"
8
+ classifiers = [
9
+ "Framework :: Django",
10
+ "Intended Audience :: Developers",
11
+ "License :: OSI Approved :: MIT License",
12
+ ]
13
+ dependencies = [
14
+ "django>=6.0",
15
+ "django-cotton>=2.7.2",
16
+ "django-eventstream>=5.3.4",
17
+ ]
18
+
19
+ [build-system]
20
+ requires = ["hatchling>=1.26"]
21
+ build-backend = "hatchling.build"
22
+
23
+ [tool.hatch.build]
24
+ artifacts = [
25
+ "/trellis_ui/static/trellis_ui/css/styles.css",
26
+ "/trellis_ui/static/trellis_ui/js/index.js",
27
+ "/trellis_ui/generated_styles.py",
28
+ ]
29
+ exclude = [
30
+ "/.gitignore",
31
+ "**/__pycache__",
32
+ "**/*.pyc",
33
+ ]
34
+
35
+ [dependency-groups]
36
+ dev = [
37
+ "daphne>=4.2.1",
38
+ "djlint>=1.40.1",
39
+ "prek>=0.4.6",
40
+ "pytest>=9.1.0",
41
+ "pytest-django>=4.12.0",
42
+ "ruff>=0.15.17",
43
+ ]
44
+
45
+ [tool.hatch.build.targets.wheel]
46
+ packages = ["trellis_ui"]
47
+ exclude = [
48
+ "/trellis_ui/static/trellis_ui/js/**/*.map",
49
+ "/trellis_ui/static/trellis_ui/js/esm",
50
+ "/trellis_ui/static/trellis_ui/manifests",
51
+ ]
52
+
53
+ [tool.hatch.build.targets.sdist]
54
+ only-include = [
55
+ "trellis_ui",
56
+ "README.md",
57
+ "LICENSE",
58
+ "pyproject.toml",
59
+ ]
60
+ exclude = [
61
+ "/.gitignore",
62
+ "/trellis_ui/static/trellis_ui/js/**/*.map",
63
+ "/trellis_ui/static/trellis_ui/js/esm",
64
+ "/trellis_ui/static/trellis_ui/manifests",
65
+ ]
66
+
67
+ [tool.ruff]
68
+ line-length = 100
69
+ target-version = "py313"
70
+
71
+ [tool.ruff.lint]
72
+ select = ["E", "F", "I", "UP", "B", "DJ", "RUF"]
73
+ ignore = []
74
+
75
+ [tool.djlint]
76
+ profile = "django"
77
+ extension = "html"
78
+ ignore = "H006,H021,H026,H030,H031,H037"
79
+ custom_html = "c-[\\w-\\.]+"
80
+ blank_line_after_tag = "c-vars,endpartialdef,load,extends,include,c-slot"
81
+ blank_line_before_tag = "partialdef"
82
+ max_attribute_length = 80
83
+
84
+ [tool.pytest.ini_options]
85
+ DJANGO_SETTINGS_MODULE = "tests.testapp.settings"
86
+ pythonpath = ["."]
87
+ python_files = ["test_*.py", "*_test.py"]
88
+ testpaths = ["tests/components"]
@@ -0,0 +1,30 @@
1
+ from .forms import (
2
+ BaseForm,
3
+ BaseFormMixin,
4
+ BaseFormSet,
5
+ BaseModelForm,
6
+ BaseModelFormSet,
7
+ ComboboxSelect,
8
+ DatePickerInput,
9
+ DateRangePickerInput,
10
+ WidgetContext,
11
+ widget_adapter,
12
+ )
13
+ from .stream import UI, ctx
14
+ from .targets import UITargets
15
+
16
+ __all__ = [
17
+ "UI",
18
+ "BaseForm",
19
+ "BaseFormMixin",
20
+ "BaseFormSet",
21
+ "BaseModelForm",
22
+ "BaseModelFormSet",
23
+ "ComboboxSelect",
24
+ "DatePickerInput",
25
+ "DateRangePickerInput",
26
+ "UITargets",
27
+ "WidgetContext",
28
+ "ctx",
29
+ "widget_adapter",
30
+ ]
@@ -0,0 +1,6 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class TrellisUIConfig(AppConfig):
5
+ name = "trellis_ui"
6
+ verbose_name = "Trellis UI"
@@ -0,0 +1,6 @@
1
+ from .stream import ctx
2
+
3
+
4
+ def ui(_) -> dict:
5
+ return ctx()
6
+
@@ -0,0 +1,15 @@
1
+
2
+ from enum import StrEnum
3
+
4
+
5
+ class UIEvents(StrEnum):
6
+ MODAL_CLOSE = "ui:modal-close"
7
+ RESIZE = "ui:resize"
8
+ RESIZE_END = "ui:resize-end"
9
+ RESIZE_START = "ui:resize-start"
10
+ SIDEBAR_CLOSE = "ui:sidebar-close"
11
+ SIDEBAR_OPEN = "ui:sidebar-open"
12
+ TOAST = "ui:toast"
13
+
14
+
15
+ __all__ = ["UIEvents"]
@@ -0,0 +1,3 @@
1
+ from trellis_ui.filters import rules
2
+
3
+ __all__ = ["rules"]
@@ -0,0 +1,148 @@
1
+ from collections.abc import Mapping, Sequence
2
+ from dataclasses import dataclass, field
3
+ from types import MappingProxyType
4
+ from typing import Any, Self
5
+
6
+ type JSONValue = str | int | float | bool | None | list[JSONValue] | dict[str, JSONValue]
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class RuleSpec[TValue]:
11
+ kind: str
12
+ field_path: str | None = None
13
+ operation_value: TValue | None = None
14
+ model: str | None = None
15
+ negate: bool = False
16
+ metadata: Mapping[str, Any] = field(default_factory=dict)
17
+
18
+ def __post_init__(self) -> None:
19
+ object.__setattr__(self, "kind", _validate_non_empty_string("kind", self.kind))
20
+ object.__setattr__(
21
+ self,
22
+ "field_path",
23
+ _validate_optional_non_empty_string("field_path", self.field_path),
24
+ )
25
+ object.__setattr__(self, "model", _validate_optional_string("model", self.model))
26
+
27
+ if not isinstance(self.negate, bool):
28
+ raise TypeError("negate must be a boolean.")
29
+
30
+ object.__setattr__(self, "metadata", _immutable_metadata(self.metadata))
31
+
32
+ def to_dict(self) -> dict[str, JSONValue]:
33
+ return {
34
+ "kind": self.kind,
35
+ "field_path": self.field_path,
36
+ "operation_value": _json_value(self.operation_value, name="operation_value"),
37
+ "model": self.model,
38
+ "negate": self.negate,
39
+ "metadata": _json_object(self.metadata, name="metadata"),
40
+ }
41
+
42
+ @classmethod
43
+ def from_dict(cls, data: Mapping[str, Any]) -> Self:
44
+ if not isinstance(data, Mapping):
45
+ raise TypeError("RuleSpec.from_dict() expects a mapping.")
46
+
47
+ try:
48
+ kind = data["kind"]
49
+ except KeyError as error:
50
+ raise ValueError("RuleSpec data must include kind.") from error
51
+
52
+ field_path = data.get("field_path")
53
+ operation_value = data.get("operation_value")
54
+ model = data.get("model")
55
+ negate = data.get("negate", False)
56
+ metadata = data.get("metadata", {})
57
+
58
+ _json_value(operation_value, name="operation_value")
59
+ _json_object(metadata, name="metadata")
60
+
61
+ return cls(
62
+ kind=kind,
63
+ field_path=field_path,
64
+ operation_value=operation_value,
65
+ model=model,
66
+ negate=negate,
67
+ metadata=metadata,
68
+ )
69
+
70
+
71
+ def spec[TValue](
72
+ kind: str,
73
+ *,
74
+ field_path: str | None = None,
75
+ operation_value: TValue | None = None,
76
+ model: str | None = None,
77
+ negate: bool = False,
78
+ metadata: Mapping[str, Any] | None = None,
79
+ **metadata_values: Any,
80
+ ) -> RuleSpec[TValue]:
81
+ merged_metadata: dict[str, Any] = {}
82
+ if metadata is not None:
83
+ merged_metadata.update(_json_object(metadata, name="metadata"))
84
+ merged_metadata.update(metadata_values)
85
+
86
+ return RuleSpec(
87
+ kind=kind,
88
+ field_path=field_path,
89
+ operation_value=operation_value,
90
+ model=model,
91
+ negate=negate,
92
+ metadata=merged_metadata,
93
+ )
94
+
95
+
96
+ def _validate_non_empty_string(name: str, value: Any) -> str:
97
+ if not isinstance(value, str):
98
+ raise TypeError(f"{name} must be a string.")
99
+ if value == "":
100
+ raise ValueError(f"{name} must not be empty.")
101
+ return value
102
+
103
+
104
+ def _validate_optional_non_empty_string(name: str, value: Any) -> str | None:
105
+ if value is None:
106
+ return None
107
+ return _validate_non_empty_string(name, value)
108
+
109
+
110
+ def _validate_optional_string(name: str, value: Any) -> str | None:
111
+ if value is None:
112
+ return None
113
+ if not isinstance(value, str):
114
+ raise TypeError(f"{name} must be a string.")
115
+ return value
116
+
117
+
118
+ def _immutable_metadata(metadata: Mapping[str, Any]) -> Mapping[str, Any]:
119
+ return MappingProxyType(_json_object(metadata, name="metadata"))
120
+
121
+
122
+ def _json_object(value: Any, *, name: str) -> dict[str, JSONValue]:
123
+ if not isinstance(value, Mapping):
124
+ raise TypeError(f"{name} must be a mapping.")
125
+
126
+ output: dict[str, JSONValue] = {}
127
+ for key, item in value.items():
128
+ if not isinstance(key, str):
129
+ raise TypeError(f"{name} keys must be strings.")
130
+ output[key] = _json_value(item, name=f"{name}.{key}")
131
+ return output
132
+
133
+
134
+ def _json_value(value: Any, *, name: str) -> JSONValue:
135
+ if value is None or isinstance(value, str | int | float | bool):
136
+ return value
137
+
138
+ if isinstance(value, Mapping):
139
+ return _json_object(value, name=name)
140
+
141
+ if _is_json_sequence(value):
142
+ return [_json_value(item, name=name) for item in value]
143
+
144
+ raise TypeError(f"{name} must be JSON-compatible.")
145
+
146
+
147
+ def _is_json_sequence(value: Any) -> bool:
148
+ return isinstance(value, Sequence) and not isinstance(value, str | bytes | bytearray)
@@ -0,0 +1,22 @@
1
+ from trellis_ui.forms.base import BaseForm, BaseFormMixin, BaseModelForm
2
+ from trellis_ui.forms.formsets import BaseFormSet, BaseModelFormSet
3
+ from trellis_ui.forms.widgets import (
4
+ ComboboxSelect,
5
+ DatePickerInput,
6
+ DateRangePickerInput,
7
+ WidgetContext,
8
+ widget_adapter,
9
+ )
10
+
11
+ __all__ = [
12
+ "BaseForm",
13
+ "BaseFormMixin",
14
+ "BaseFormSet",
15
+ "BaseModelForm",
16
+ "BaseModelFormSet",
17
+ "ComboboxSelect",
18
+ "DatePickerInput",
19
+ "DateRangePickerInput",
20
+ "WidgetContext",
21
+ "widget_adapter",
22
+ ]
@@ -0,0 +1,23 @@
1
+ from django import forms
2
+
3
+ from trellis_ui.forms.renderers import TrellisUIRenderer
4
+
5
+
6
+ class BaseFormMixin:
7
+ default_renderer = TrellisUIRenderer
8
+ template_name_div = TrellisUIRenderer.form_template_name
9
+ template_name_p = TrellisUIRenderer.form_template_name
10
+ template_name_table = TrellisUIRenderer.form_template_name
11
+ template_name_ul = TrellisUIRenderer.form_template_name
12
+
13
+ def __init__(self, *args, **kwargs):
14
+ kwargs.setdefault("label_suffix", "")
15
+ super().__init__(*args, **kwargs)
16
+
17
+
18
+ class BaseForm(BaseFormMixin, forms.Form):
19
+ pass
20
+
21
+
22
+ class BaseModelForm(BaseFormMixin, forms.ModelForm):
23
+ pass
@@ -0,0 +1,33 @@
1
+ from django.forms.boundfield import BoundField
2
+
3
+ from trellis_ui.forms.widgets import render_bound_widget
4
+
5
+
6
+ class UIBoundField(BoundField):
7
+ def as_widget(self, widget=None, attrs=None, only_initial=False):
8
+ widget = widget or self.field.widget
9
+ if self.field.localize:
10
+ widget.is_localized = True
11
+ attrs = attrs or {}
12
+ attrs = self.build_widget_attrs(attrs, widget)
13
+ if self.auto_id and "id" not in widget.attrs:
14
+ attrs.setdefault(
15
+ "id",
16
+ self.html_initial_id if only_initial else self.auto_id,
17
+ )
18
+ if only_initial and self.html_initial_name in self.form.data:
19
+ value = self.form._widget_data_value(
20
+ self.field.hidden_widget(),
21
+ self.html_initial_name,
22
+ )
23
+ else:
24
+ value = self.value()
25
+
26
+ return render_bound_widget(
27
+ bound_field=self,
28
+ widget=widget,
29
+ name=self.html_initial_name if only_initial else self.html_name,
30
+ value=value,
31
+ attrs=attrs,
32
+ renderer=self.form.renderer,
33
+ )
@@ -0,0 +1,20 @@
1
+ from django.forms.formsets import BaseFormSet as DjangoBaseFormSet
2
+ from django.forms.models import BaseModelFormSet as DjangoBaseModelFormSet
3
+
4
+ from trellis_ui.forms.renderers import TrellisUIRenderer
5
+
6
+
7
+ class BaseFormSet(DjangoBaseFormSet):
8
+ renderer = TrellisUIRenderer()
9
+ template_name_div = TrellisUIRenderer.formset_template_name
10
+ template_name_p = TrellisUIRenderer.formset_template_name
11
+ template_name_table = TrellisUIRenderer.formset_template_name
12
+ template_name_ul = TrellisUIRenderer.formset_template_name
13
+
14
+
15
+ class BaseModelFormSet(DjangoBaseModelFormSet):
16
+ renderer = TrellisUIRenderer()
17
+ template_name_div = TrellisUIRenderer.formset_template_name
18
+ template_name_p = TrellisUIRenderer.formset_template_name
19
+ template_name_table = TrellisUIRenderer.formset_template_name
20
+ template_name_ul = TrellisUIRenderer.formset_template_name
@@ -0,0 +1,3 @@
1
+ from trellis_ui.forms.presets import dates
2
+
3
+ __all__ = ["dates"]
@@ -0,0 +1,164 @@
1
+ from calendar import monthrange
2
+ from collections.abc import Mapping
3
+ from dataclasses import dataclass, field
4
+ from datetime import date, timedelta
5
+ from types import MappingProxyType
6
+ from typing import Any
7
+
8
+ from django.utils import timezone
9
+
10
+ from trellis_ui.filters import rules
11
+ from trellis_ui.filters.rules import RuleSpec
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class DatePreset:
16
+ value: str
17
+ label: str
18
+ start: date
19
+ end: date
20
+ rule: RuleSpec[dict[str, int]] | None = None
21
+ disabled: bool = False
22
+ metadata: Mapping[str, Any] = field(default_factory=dict)
23
+
24
+ def __post_init__(self) -> None:
25
+ if not self.value:
26
+ raise ValueError("DatePreset value must not be empty.")
27
+ if not self.label:
28
+ raise ValueError("DatePreset label must not be empty.")
29
+ if self.start > self.end:
30
+ raise ValueError("DatePreset start must be before or equal to end.")
31
+ object.__setattr__(self, "metadata", MappingProxyType(dict(self.metadata)))
32
+
33
+ def to_dict(self) -> dict[str, str | bool]:
34
+ data: dict[str, str | bool] = {
35
+ "value": self.value,
36
+ "label": self.label,
37
+ "start": self.start.isoformat(),
38
+ "end": self.end.isoformat(),
39
+ }
40
+ if self.disabled:
41
+ data["disabled"] = True
42
+ return data
43
+
44
+
45
+ def today(
46
+ *,
47
+ label: str = "Today",
48
+ value: str = "today",
49
+ today: date | None = None,
50
+ ) -> DatePreset:
51
+ current = _today(today)
52
+ return DatePreset(value=value, label=label, start=current, end=current)
53
+
54
+
55
+ def yesterday(
56
+ *,
57
+ label: str = "Yesterday",
58
+ value: str = "yesterday",
59
+ today: date | None = None,
60
+ ) -> DatePreset:
61
+ current = _today(today) - timedelta(days=1)
62
+ return DatePreset(value=value, label=label, start=current, end=current)
63
+
64
+
65
+ def since(
66
+ *,
67
+ days: int | None = None,
68
+ months: int | None = None,
69
+ years: int | None = None,
70
+ label: str | None = None,
71
+ value: str | None = None,
72
+ include_today: bool = True,
73
+ today: date | None = None,
74
+ field: str | None = None,
75
+ model: str | None = None,
76
+ ) -> DatePreset:
77
+ units = {
78
+ key: amount
79
+ for key, amount in {"days": days, "months": months, "years": years}.items()
80
+ if amount is not None
81
+ }
82
+ if len(units) != 1:
83
+ raise ValueError("dates.since() expects exactly one of days, months, or years.")
84
+
85
+ unit, amount = next(iter(units.items()))
86
+ if amount <= 0:
87
+ raise ValueError("dates.since() amount must be greater than zero.")
88
+
89
+ end = _today(today) if include_today else _today(today) - timedelta(days=1)
90
+ if unit == "days":
91
+ start = end - timedelta(days=amount - 1)
92
+ elif unit == "months":
93
+ start = _add_months(end, -amount)
94
+ else:
95
+ start = _add_years(end, -amount)
96
+
97
+ preset_value = value or f"last_{amount}_{unit}"
98
+ preset_label = label or f"Last {amount} {unit}"
99
+ rule = None
100
+ if field is not None or model is not None:
101
+ rule = rules.spec(
102
+ kind="since",
103
+ field_path=field,
104
+ operation_value={unit: amount},
105
+ model=model,
106
+ metadata={"preset": preset_value},
107
+ )
108
+
109
+ return DatePreset(
110
+ value=preset_value,
111
+ label=preset_label,
112
+ start=start,
113
+ end=end,
114
+ rule=rule,
115
+ )
116
+
117
+
118
+ def normalize_preset(preset: DatePreset | Mapping[str, Any]) -> dict[str, str | bool]:
119
+ if isinstance(preset, DatePreset):
120
+ return preset.to_dict()
121
+ if not isinstance(preset, Mapping):
122
+ raise ValueError("Date picker presets must be DatePreset instances or mappings.")
123
+
124
+ output: dict[str, str | bool] = {}
125
+ for key in ("value", "label", "start", "end"):
126
+ value = preset.get(key)
127
+ if value in (None, ""):
128
+ raise ValueError(f"Date picker preset mappings must include {key}.")
129
+ output[key] = _iso_value(value, key=key)
130
+
131
+ disabled = preset.get("disabled", False)
132
+ if disabled:
133
+ output["disabled"] = True
134
+ return output
135
+
136
+
137
+ def _today(today: date | None) -> date:
138
+ return today or timezone.localdate()
139
+
140
+
141
+ def _iso_value(value: Any, *, key: str) -> str:
142
+ if isinstance(value, date):
143
+ return value.isoformat()
144
+ if isinstance(value, str):
145
+ return value
146
+ raise ValueError(f"Date picker preset {key} must be a date or ISO date string.")
147
+
148
+
149
+ def _add_months(value: date, months: int) -> date:
150
+ month_index = value.month - 1 + months
151
+ year = value.year + month_index // 12
152
+ month = month_index % 12 + 1
153
+ day = min(value.day, monthrange(year, month)[1])
154
+ return value.replace(year=year, month=month, day=day)
155
+
156
+
157
+ def _add_years(value: date, years: int) -> date:
158
+ try:
159
+ return value.replace(year=value.year + years)
160
+ except ValueError:
161
+ return value.replace(year=value.year + years, day=28)
162
+
163
+
164
+ __all__ = ["DatePreset", "normalize_preset", "since", "today", "yesterday"]
@@ -0,0 +1,21 @@
1
+ from django.forms.renderers import DjangoTemplates, TemplatesSetting
2
+ from django.template.exceptions import TemplateDoesNotExist
3
+
4
+ from trellis_ui.forms.boundfield import UIBoundField
5
+
6
+ BUILTIN_RENDERER = DjangoTemplates()
7
+
8
+
9
+ class TrellisUIRenderer(TemplatesSetting):
10
+ form_template_name = "trellis_ui/forms/form.html"
11
+ formset_template_name = "trellis_ui/forms/formset.html"
12
+ field_template_name = "trellis_ui/forms/field.html"
13
+ bound_field_class = UIBoundField
14
+
15
+ def get_template(self, template_name):
16
+ try:
17
+ return super().get_template(template_name)
18
+ except TemplateDoesNotExist:
19
+ if template_name.startswith("django/forms/"):
20
+ return BUILTIN_RENDERER.get_template(template_name)
21
+ raise