pycascadeui 3.1.0__tar.gz → 3.3.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 (139) hide show
  1. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/PKG-INFO +62 -47
  2. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/README.md +58 -44
  3. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/__init__.py +24 -4
  4. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/components/__init__.py +6 -0
  5. pycascadeui-3.3.0/cascadeui/components/base.py +402 -0
  6. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/components/inputs.py +114 -57
  7. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/components/patterns/__init__.py +2 -0
  8. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/components/patterns/v2.py +99 -21
  9. pycascadeui-3.3.0/cascadeui/components/selects.py +270 -0
  10. pycascadeui-3.3.0/cascadeui/components/types.py +43 -0
  11. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/components/wrappers.py +3 -1
  12. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/devtools.py +13 -2
  13. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/persistence/__init__.py +5 -0
  14. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/persistence/backends/__init__.py +9 -0
  15. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/persistence/backends/memory.py +40 -1
  16. pycascadeui-3.3.0/cascadeui/persistence/backends/postgres.py +847 -0
  17. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/persistence/backends/sqlite.py +204 -7
  18. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/persistence/manager.py +17 -4
  19. pycascadeui-3.3.0/cascadeui/persistence/protocols.py +301 -0
  20. pycascadeui-3.3.0/cascadeui/persistence/schema_postgres.py +125 -0
  21. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/state/middleware/persistence.py +22 -0
  22. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/state/middleware/undo.py +34 -1
  23. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/utils/__init__.py +3 -1
  24. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/utils/coercion.py +41 -1
  25. pycascadeui-3.3.0/cascadeui/utils/fetch.py +73 -0
  26. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/views/_navigation.py +205 -93
  27. pycascadeui-3.3.0/cascadeui/views/_placement.py +438 -0
  28. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/views/base.py +75 -4
  29. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/views/layout.py +56 -18
  30. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/views/patterns/__init__.py +3 -0
  31. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/views/patterns/form.py +4 -1
  32. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/views/patterns/leaderboard.py +49 -10
  33. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/views/patterns/menu.py +6 -0
  34. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/views/patterns/paginated.py +124 -27
  35. pycascadeui-3.3.0/cascadeui/views/patterns/roles.py +538 -0
  36. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/views/patterns/tabs.py +3 -0
  37. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/views/patterns/types.py +85 -5
  38. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/views/patterns/wizard.py +9 -7
  39. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/views/persistent.py +38 -36
  40. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/views/view.py +23 -1
  41. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/pycascadeui.egg-info/PKG-INFO +62 -47
  42. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/pycascadeui.egg-info/SOURCES.txt +15 -1
  43. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/pycascadeui.egg-info/requires.txt +3 -2
  44. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/pyproject.toml +4 -3
  45. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_auto_defer.py +9 -6
  46. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_backends.py +32 -21
  47. pycascadeui-3.3.0/tests/test_backends_postgres.py +426 -0
  48. pycascadeui-3.3.0/tests/test_backends_raw_sql.py +442 -0
  49. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_components.py +116 -0
  50. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_computed.py +1 -0
  51. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_devtools.py +26 -2
  52. pycascadeui-3.3.0/tests/test_dynamic_persistent_button.py +330 -0
  53. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_ephemeral_refresh.py +9 -8
  54. pycascadeui-3.3.0/tests/test_fetch.py +146 -0
  55. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_five_pillars.py +4 -11
  56. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_form_patterns.py +23 -8
  57. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_hooks.py +1 -0
  58. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_input_coercion.py +70 -1
  59. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_inputs.py +100 -15
  60. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_layout_view.py +102 -11
  61. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_leaderboard_patterns.py +164 -10
  62. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_menu_patterns.py +11 -1
  63. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_message_cleanup.py +5 -1
  64. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_navigation.py +171 -3
  65. pycascadeui-3.3.0/tests/test_navigation_artifact_restore.py +231 -0
  66. pycascadeui-3.3.0/tests/test_paginated_patterns.py +506 -0
  67. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_pagination.py +52 -0
  68. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_persistence.py +120 -23
  69. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_persistence_middleware.py +8 -21
  70. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_persistent_views.py +46 -4
  71. pycascadeui-3.3.0/tests/test_placement.py +534 -0
  72. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_rebuild_callback.py +29 -14
  73. pycascadeui-3.3.0/tests/test_roles_patterns.py +505 -0
  74. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_scoping.py +26 -39
  75. pycascadeui-3.3.0/tests/test_selects.py +226 -0
  76. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_state_slots.py +10 -30
  77. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_theming.py +3 -1
  78. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_undo.py +100 -0
  79. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_v2_helpers.py +115 -0
  80. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_validation.py +3 -0
  81. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_view_init.py +91 -2
  82. pycascadeui-3.1.0/cascadeui/components/base.py +0 -199
  83. pycascadeui-3.1.0/cascadeui/components/selects.py +0 -106
  84. pycascadeui-3.1.0/cascadeui/persistence/protocols.py +0 -171
  85. pycascadeui-3.1.0/tests/test_paginated_patterns.py +0 -246
  86. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/LICENSE +0 -0
  87. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/__main__.py +0 -0
  88. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/components/buttons.py +0 -0
  89. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/components/patterns/v1.py +0 -0
  90. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/components/v1_composition.py +0 -0
  91. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/exceptions.py +0 -0
  92. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/persistence/config.py +0 -0
  93. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/persistence/migrations.py +0 -0
  94. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/persistence/schema.py +0 -0
  95. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/py.typed +0 -0
  96. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/setup.py +0 -0
  97. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/state/__init__.py +0 -0
  98. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/state/actions.py +0 -0
  99. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/state/computed.py +0 -0
  100. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/state/middleware/__init__.py +0 -0
  101. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/state/middleware/logging.py +0 -0
  102. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/state/reducers.py +0 -0
  103. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/state/singleton.py +0 -0
  104. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/state/slots.py +0 -0
  105. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/state/store.py +0 -0
  106. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/state/types.py +0 -0
  107. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/theming/__init__.py +0 -0
  108. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/theming/context.py +0 -0
  109. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/theming/core.py +0 -0
  110. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/theming/themes.py +0 -0
  111. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/tracing.py +0 -0
  112. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/utils/decorators.py +0 -0
  113. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/utils/errors.py +0 -0
  114. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/utils/logging.py +0 -0
  115. pycascadeui-3.1.0/cascadeui/utils/helpers.py → pycascadeui-3.3.0/cascadeui/utils/strings.py +0 -0
  116. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/utils/tasks.py +0 -0
  117. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/validation.py +0 -0
  118. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/views/__init__.py +0 -0
  119. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/cascadeui/views/_interaction.py +0 -0
  120. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/pycascadeui.egg-info/dependency_links.txt +0 -0
  121. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/pycascadeui.egg-info/top_level.txt +0 -0
  122. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/setup.cfg +0 -0
  123. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_batching.py +0 -0
  124. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_button_grid.py +0 -0
  125. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_emoji_grid.py +0 -0
  126. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_instance_limit.py +0 -0
  127. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_layout_pagination.py +0 -0
  128. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_layout_patterns.py +0 -0
  129. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_layout_persistent.py +0 -0
  130. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_middleware.py +0 -0
  131. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_nav_version.py +1 -1
  132. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_owner_only.py +0 -0
  133. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_pillar_isolation.py +1 -1
  134. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_reducers.py +0 -0
  135. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_state_store.py +0 -0
  136. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_tab_patterns.py +0 -0
  137. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_update_coalescing.py +2 -2
  138. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_wizard_patterns.py +0 -0
  139. {pycascadeui-3.1.0 → pycascadeui-3.3.0}/tests/test_wrappers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pycascadeui
3
- Version: 3.1.0
3
+ Version: 3.3.0
4
4
  Summary: Redux-inspired UI framework for discord.py
5
5
  Author-email: HollowTheSilver <hollow@users.noreply.github.com>
6
6
  License-Expression: MIT
@@ -30,14 +30,15 @@ Requires-Dist: pytest>=7.0; extra == "dev"
30
30
  Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
31
31
  Requires-Dist: black>=26.3; extra == "dev"
32
32
  Requires-Dist: isort>=8.0; extra == "dev"
33
+ Requires-Dist: testcontainers[postgres]>=4.0; extra == "dev"
33
34
  Provides-Extra: docs
34
35
  Requires-Dist: mkdocs>=1.5; extra == "docs"
35
36
  Requires-Dist: mkdocs-material>=9.0; extra == "docs"
36
37
  Requires-Dist: pymdown-extensions>=10.0; extra == "docs"
37
38
  Provides-Extra: sqlite
38
39
  Requires-Dist: aiosqlite>=0.19; extra == "sqlite"
39
- Provides-Extra: redis
40
- Requires-Dist: redis>=5.0; extra == "redis"
40
+ Provides-Extra: postgres
41
+ Requires-Dist: asyncpg>=0.30; extra == "postgres"
41
42
  Dynamic: license-file
42
43
 
43
44
  <p align="center">
@@ -151,7 +152,8 @@ pip install pycascadeui
151
152
  Optional dependencies:
152
153
 
153
154
  ```bash
154
- pip install pycascadeui[sqlite]
155
+ pip install pycascadeui[sqlite] # single-process persistence
156
+ pip install pycascadeui[postgres] # multi-process persistence with LISTEN/NOTIFY
155
157
  ```
156
158
 
157
159
  Requirements:
@@ -226,22 +228,25 @@ class NotificationPanel(StatefulLayoutView):
226
228
  > Define `build_ui()` once. The library calls it on every relevant state change and ships the edit for you. No `on_state_changed()` override, no manual `refresh()`, no `message.edit()` plumbing.
227
229
 
228
230
  ```python
229
- class SettingsHub(StatefulLayoutView):
231
+ class MyFleetView(StatefulLayoutView):
230
232
  state_scope = "user"
231
- subscribed_actions = {"SETTINGS_UPDATED"}
233
+ subscribed_actions = {"FLEET_REROLLED"}
232
234
 
233
235
  def build_ui(self):
234
236
  self.clear_items()
235
- settings = self.user_scoped_state()
236
- self.add_item(card(
237
- "## Settings",
238
- key_value({
239
- "Theme": settings.get("theme", "default").title(),
240
- "Notifications": "On" if settings.get("notify") else "Off",
241
- }),
237
+ grid = emoji_grid(10, 10, fill="\U0001f7e6", row_labels="alpha", col_labels="numeric")
238
+ for row, col in self.ship_cells():
239
+ grid[row, col] = "\U0001f6a2"
240
+
241
+ self.add_item(card("## My Fleet", divider(), grid, color=discord.Color.blue()))
242
+ self.add_item(ActionRow(
243
+ StatefulButton(label="Regenerate", emoji="\U0001f3b2", callback=self._reroll),
242
244
  ))
243
245
 
244
- # build_ui() is called automatically on state changes --
246
+ async def _reroll(self, interaction):
247
+ await self.dispatch("FLEET_REROLLED", {"cells": random_placement()})
248
+
249
+ # build_ui() runs automatically on every FLEET_REROLLED dispatch --
245
250
  # no manual refresh() or on_state_changed() override needed.
246
251
  ```
247
252
 
@@ -300,18 +305,14 @@ class BattleshipView(StatefulLayoutView):
300
305
 
301
306
  ### Lifecycle Control
302
307
 
303
- > Control active sessions per user, guild, or globally with automatic cleanup, replacement policies, and view-capacity caps.
308
+ > Cap active sessions per user, guild, or globally. Pick how a collision resolves and how the old view cleans up when a new one opens.
304
309
 
305
310
  ```python
306
- class DashboardView(TabLayoutView):
311
+ class SettingsHubView(MenuLayoutView):
307
312
  instance_limit = 1 # Only one open at a time
308
313
  instance_scope = "user_guild" # Per user per guild
309
314
  instance_policy = "replace" # Exit the old one, open the new one
310
-
311
-
312
- class GameView(StatefulLayoutView):
313
- participant_limit = 8 # Owner + 7 joiners maximum
314
- auto_register_participants = True # Claim slots from allowed_users on send()
315
+ exit_policy = "disable" # Old view's buttons grey out, message stays
315
316
  ```
316
317
 
317
318
  ![V2 Instance Limiting](https://raw.githubusercontent.com/HollowTheSilver/CascadeUI/main/assets/gifs/v2-session-limiting.gif)
@@ -332,12 +333,18 @@ async def setup_hook(self):
332
333
  )
333
334
  ```
334
335
 
335
- Declare `persistent_slots` on any view that should carry application state to disk. The rest stays volatile:
336
+ Subclass `PersistentRolesLayoutView` and declare your categories. The pattern handles button rendering, cardinality enforcement, and restart re-attachment. A stable `persistence_key` is the match identity the middleware uses to find this panel after restart:
336
337
 
337
338
  ```python
338
- class BattleshipView(StatefulLayoutView):
339
- scoped_slot = "battleship_stats" # per-subsystem bucket
340
- persistent_slots = ("battleship_stats",) # opt this slot into persistence
339
+ class GuildRoles(PersistentRolesLayoutView):
340
+ categories = [
341
+ RoleCategory(name="Color Roles", exclusive=True, roles={"Red": 123, "Blue": 456}),
342
+ RoleCategory(name="Pronouns", required=True, roles={"He/Him": 789, "She/Her": 12}),
343
+ ]
344
+
345
+ # Same key in, same panel out -- across bot restarts.
346
+ panel = GuildRoles(context=ctx, persistence_key=f"roles:{ctx.guild.id}")
347
+ await panel.send()
341
348
  ```
342
349
 
343
350
  ![Persistence](https://raw.githubusercontent.com/HollowTheSilver/CascadeUI/main/assets/gifs/v2-persistence-restart.gif)
@@ -349,8 +356,9 @@ class BattleshipView(StatefulLayoutView):
349
356
  > Snapshot-based state history per session with built-in undo and redo support.
350
357
 
351
358
  ```python
352
- class SettingsHub(StatefulLayoutView):
359
+ class NotificationsView(StatefulLayoutView):
353
360
  enable_undo = True # Every dispatch captures a snapshot
361
+ undo_limit = 10 # Stack depth cap (self.undo_depth / self.redo_depth read live)
354
362
 
355
363
  async def _undo(self, interaction):
356
364
  await self.undo() # Restore previous snapshot
@@ -490,7 +498,13 @@ await view.send()
490
498
  # Persistent variant -- admin-posted panel that survives bot restarts
491
499
  # and re-fetches live data on every restore.
492
500
  class PersistentBoard(PersistentLeaderboardLayoutView):
493
- persistence_key = "battleship-leaderboard-main"
501
+ pass
502
+
503
+ panel = PersistentBoard(
504
+ context=ctx,
505
+ persistence_key="battleship-leaderboard-main",
506
+ )
507
+ await panel.send()
494
508
  ```
495
509
 
496
510
  ![Leaderboards](https://raw.githubusercontent.com/HollowTheSilver/CascadeUI/main/assets/gifs/v2-leaderboard.gif)
@@ -602,8 +616,6 @@ for row in rows:
602
616
  view.add_item(row)
603
617
  ```
604
618
 
605
- ![Button Grid](https://raw.githubusercontent.com/HollowTheSilver/CascadeUI/main/assets/pngs/v2-button-grid.PNG)
606
-
607
619
  <div align="center">
608
620
  <img src="https://raw.githubusercontent.com/HollowTheSilver/CascadeUI/main/assets/pngs/v2-emoji-grid.PNG" width="30%" alt="Emoji Grid" style="border-radius: 8px; margin: 5px;" />
609
621
  <img src="https://raw.githubusercontent.com/HollowTheSilver/CascadeUI/main/assets/pngs/v2-button-grid.PNG" width="30%" alt="Button Grid" style="border-radius: 8px; margin: 5px;" />
@@ -630,7 +642,7 @@ for row in rows:
630
642
  ### Views
631
643
  - V2 layout-based system for structured, container-driven interfaces
632
644
  - Full support for traditional discord.py Views (V1)
633
- - Pre-built patterns: menus, tabs, wizards, forms, pagination, leaderboards
645
+ - Pre-built patterns: menus, tabs, wizards, forms, pagination, leaderboards, roles
634
646
  - `PaginatedView.from_cursor()` for lazy cursor-driven pagination with LRU page cache
635
647
  - `DisplayLayoutView` for one-shot V2 sends from a pre-built container
636
648
  - Automatic state-driven rebuilds: define `build_ui()`, the library handles the edit (≈ React `render()`)
@@ -639,7 +651,7 @@ for row in rows:
639
651
  ### Components
640
652
  - Stateful buttons, selects, and modals with state integration
641
653
  - Select callbacks can opt into a `values` second parameter
642
- - V2 builders: `card()`, `stats_card()`, `action_section()`, `toggle_section()`, `image_section()`, `link_section()`, `confirm_section()`, `button_row()`, `cycle_button()`, `toggle_button()`, `tab_nav()`, `key_value()`, `alert()`, `progress_bar()`, `divider()`, `gap()`, `gallery()`
654
+ - V2 builders: `card()`, `stats_card()`, `action_section()`, `toggle_section()`, `image_section()`, `link_section()`, `confirm_section()`, `button_row()`, `cycle_button()`, `toggle_button()`, `tab_nav()`, `key_value()`, `alert()`, `progress_bar()`, `divider()`, `gap()`, `gallery()`, `file_attachment()`
643
655
  - Grid helpers: `emoji_grid()` and `button_grid()`
644
656
  - Typed modal fields (`text`, `integer`, `float`, `date`) with per-field validation
645
657
  - Declarative `FormSchema` and `WizardSchema` base classes
@@ -666,7 +678,8 @@ for row in rows:
666
678
  ### Persistence
667
679
  - Persistent views that survive bot restarts with automatic message re-attachment
668
680
  - Opt-in per slot via `persistent_slots = (...)` (≈ `redux-persist`)
669
- - Built-in SQLite and in-memory backends; custom backends via capability-flag `Protocol`
681
+ - Built-in SQLite, PostgreSQL, and in-memory backends; custom backends via capability-flag `Protocol`
682
+ - Cross-process scoped invalidation through PostgreSQL `LISTEN`/`NOTIFY` (multi-worker bots)
670
683
  - Named scoped buckets via `scoped_slot` for per-subsystem persistence
671
684
  - Two-namespace model (`registry` and `application`) with per-namespace debounce and retry backoff
672
685
  - Undo and redo via snapshot-based state history (opt in with `enable_undo`)
@@ -677,20 +690,6 @@ for row in rows:
677
690
 
678
691
  ---
679
692
 
680
- ## V1 Components
681
-
682
- > CascadeUI supports traditional discord.py Views and embeds.
683
-
684
- Use V1 when you need:
685
- - Embed-specific features such as fields or timestamps
686
- - Simpler layouts without containers
687
-
688
- All core features such as navigation, persistence, and undo/redo are supported.
689
-
690
- ![Ticket System](https://raw.githubusercontent.com/HollowTheSilver/CascadeUI/main/assets/gifs/v1-ticket-system.gif)
691
-
692
- ---
693
-
694
693
  ## Examples
695
694
 
696
695
  > The <a href="https://hollowthesilver.github.io/CascadeUI/examples/"><strong>documentation</strong></a> includes full implementations demonstrating practical usage:
@@ -703,6 +702,22 @@ All core features such as navigation, persistence, and undo/redo are supported.
703
702
  - Multi-user games with shared state, hidden information, and challenge flows (TicTacToe, Battleship)
704
703
  - Open-join lobbies with capacity caps and host-vs-participant authority (Werewolf-style)
705
704
 
705
+ ![Examples](https://raw.githubusercontent.com/HollowTheSilver/CascadeUI/main/assets/gifs/v2-hero.gif)
706
+
707
+ ---
708
+
709
+ ## V1 Components
710
+
711
+ > CascadeUI supports traditional discord.py Views and embeds.
712
+
713
+ Use V1 when you need:
714
+ - Embed-specific features such as fields or timestamps
715
+ - Simpler layouts without containers
716
+
717
+ All core features such as navigation, persistence, and undo/redo are supported.
718
+
719
+ ![Ticket System](https://raw.githubusercontent.com/HollowTheSilver/CascadeUI/main/assets/gifs/v1-ticket-system.gif)
720
+
706
721
  ---
707
722
 
708
723
  ## Documentation
@@ -734,7 +749,7 @@ isort cascadeui/
734
749
 
735
750
  ## Developer's Note
736
751
 
737
- > I built CascadeUI with over **ten years** of Python, and roughly fifteen years of developement experience. All documentation, docstrings and test modules are written and designed using custom **Anthropic Opus** sub-agents. I do not attempt to conceal this fact. I'm a proponent of efficient and responsible agent application in software design. That experience is what makes these tools effective. They're amplifiers, not substitutes.
752
+ > I built CascadeUI with over **ten years** of Python, and roughly fifteen years of development experience. All documentation, docstrings and test modules are written and designed using custom **Anthropic Opus** sub-agents. I do not attempt to conceal this fact. I'm a proponent of efficient and responsible agent application in software design. That experience is what makes these tools effective. They're amplifiers, not substitutes.
738
753
  >
739
754
  > *-- Hollow*
740
755
 
@@ -109,7 +109,8 @@ pip install pycascadeui
109
109
  Optional dependencies:
110
110
 
111
111
  ```bash
112
- pip install pycascadeui[sqlite]
112
+ pip install pycascadeui[sqlite] # single-process persistence
113
+ pip install pycascadeui[postgres] # multi-process persistence with LISTEN/NOTIFY
113
114
  ```
114
115
 
115
116
  Requirements:
@@ -184,22 +185,25 @@ class NotificationPanel(StatefulLayoutView):
184
185
  > Define `build_ui()` once. The library calls it on every relevant state change and ships the edit for you. No `on_state_changed()` override, no manual `refresh()`, no `message.edit()` plumbing.
185
186
 
186
187
  ```python
187
- class SettingsHub(StatefulLayoutView):
188
+ class MyFleetView(StatefulLayoutView):
188
189
  state_scope = "user"
189
- subscribed_actions = {"SETTINGS_UPDATED"}
190
+ subscribed_actions = {"FLEET_REROLLED"}
190
191
 
191
192
  def build_ui(self):
192
193
  self.clear_items()
193
- settings = self.user_scoped_state()
194
- self.add_item(card(
195
- "## Settings",
196
- key_value({
197
- "Theme": settings.get("theme", "default").title(),
198
- "Notifications": "On" if settings.get("notify") else "Off",
199
- }),
194
+ grid = emoji_grid(10, 10, fill="\U0001f7e6", row_labels="alpha", col_labels="numeric")
195
+ for row, col in self.ship_cells():
196
+ grid[row, col] = "\U0001f6a2"
197
+
198
+ self.add_item(card("## My Fleet", divider(), grid, color=discord.Color.blue()))
199
+ self.add_item(ActionRow(
200
+ StatefulButton(label="Regenerate", emoji="\U0001f3b2", callback=self._reroll),
200
201
  ))
201
202
 
202
- # build_ui() is called automatically on state changes --
203
+ async def _reroll(self, interaction):
204
+ await self.dispatch("FLEET_REROLLED", {"cells": random_placement()})
205
+
206
+ # build_ui() runs automatically on every FLEET_REROLLED dispatch --
203
207
  # no manual refresh() or on_state_changed() override needed.
204
208
  ```
205
209
 
@@ -258,18 +262,14 @@ class BattleshipView(StatefulLayoutView):
258
262
 
259
263
  ### Lifecycle Control
260
264
 
261
- > Control active sessions per user, guild, or globally with automatic cleanup, replacement policies, and view-capacity caps.
265
+ > Cap active sessions per user, guild, or globally. Pick how a collision resolves and how the old view cleans up when a new one opens.
262
266
 
263
267
  ```python
264
- class DashboardView(TabLayoutView):
268
+ class SettingsHubView(MenuLayoutView):
265
269
  instance_limit = 1 # Only one open at a time
266
270
  instance_scope = "user_guild" # Per user per guild
267
271
  instance_policy = "replace" # Exit the old one, open the new one
268
-
269
-
270
- class GameView(StatefulLayoutView):
271
- participant_limit = 8 # Owner + 7 joiners maximum
272
- auto_register_participants = True # Claim slots from allowed_users on send()
272
+ exit_policy = "disable" # Old view's buttons grey out, message stays
273
273
  ```
274
274
 
275
275
  ![V2 Instance Limiting](https://raw.githubusercontent.com/HollowTheSilver/CascadeUI/main/assets/gifs/v2-session-limiting.gif)
@@ -290,12 +290,18 @@ async def setup_hook(self):
290
290
  )
291
291
  ```
292
292
 
293
- Declare `persistent_slots` on any view that should carry application state to disk. The rest stays volatile:
293
+ Subclass `PersistentRolesLayoutView` and declare your categories. The pattern handles button rendering, cardinality enforcement, and restart re-attachment. A stable `persistence_key` is the match identity the middleware uses to find this panel after restart:
294
294
 
295
295
  ```python
296
- class BattleshipView(StatefulLayoutView):
297
- scoped_slot = "battleship_stats" # per-subsystem bucket
298
- persistent_slots = ("battleship_stats",) # opt this slot into persistence
296
+ class GuildRoles(PersistentRolesLayoutView):
297
+ categories = [
298
+ RoleCategory(name="Color Roles", exclusive=True, roles={"Red": 123, "Blue": 456}),
299
+ RoleCategory(name="Pronouns", required=True, roles={"He/Him": 789, "She/Her": 12}),
300
+ ]
301
+
302
+ # Same key in, same panel out -- across bot restarts.
303
+ panel = GuildRoles(context=ctx, persistence_key=f"roles:{ctx.guild.id}")
304
+ await panel.send()
299
305
  ```
300
306
 
301
307
  ![Persistence](https://raw.githubusercontent.com/HollowTheSilver/CascadeUI/main/assets/gifs/v2-persistence-restart.gif)
@@ -307,8 +313,9 @@ class BattleshipView(StatefulLayoutView):
307
313
  > Snapshot-based state history per session with built-in undo and redo support.
308
314
 
309
315
  ```python
310
- class SettingsHub(StatefulLayoutView):
316
+ class NotificationsView(StatefulLayoutView):
311
317
  enable_undo = True # Every dispatch captures a snapshot
318
+ undo_limit = 10 # Stack depth cap (self.undo_depth / self.redo_depth read live)
312
319
 
313
320
  async def _undo(self, interaction):
314
321
  await self.undo() # Restore previous snapshot
@@ -448,7 +455,13 @@ await view.send()
448
455
  # Persistent variant -- admin-posted panel that survives bot restarts
449
456
  # and re-fetches live data on every restore.
450
457
  class PersistentBoard(PersistentLeaderboardLayoutView):
451
- persistence_key = "battleship-leaderboard-main"
458
+ pass
459
+
460
+ panel = PersistentBoard(
461
+ context=ctx,
462
+ persistence_key="battleship-leaderboard-main",
463
+ )
464
+ await panel.send()
452
465
  ```
453
466
 
454
467
  ![Leaderboards](https://raw.githubusercontent.com/HollowTheSilver/CascadeUI/main/assets/gifs/v2-leaderboard.gif)
@@ -560,8 +573,6 @@ for row in rows:
560
573
  view.add_item(row)
561
574
  ```
562
575
 
563
- ![Button Grid](https://raw.githubusercontent.com/HollowTheSilver/CascadeUI/main/assets/pngs/v2-button-grid.PNG)
564
-
565
576
  <div align="center">
566
577
  <img src="https://raw.githubusercontent.com/HollowTheSilver/CascadeUI/main/assets/pngs/v2-emoji-grid.PNG" width="30%" alt="Emoji Grid" style="border-radius: 8px; margin: 5px;" />
567
578
  <img src="https://raw.githubusercontent.com/HollowTheSilver/CascadeUI/main/assets/pngs/v2-button-grid.PNG" width="30%" alt="Button Grid" style="border-radius: 8px; margin: 5px;" />
@@ -588,7 +599,7 @@ for row in rows:
588
599
  ### Views
589
600
  - V2 layout-based system for structured, container-driven interfaces
590
601
  - Full support for traditional discord.py Views (V1)
591
- - Pre-built patterns: menus, tabs, wizards, forms, pagination, leaderboards
602
+ - Pre-built patterns: menus, tabs, wizards, forms, pagination, leaderboards, roles
592
603
  - `PaginatedView.from_cursor()` for lazy cursor-driven pagination with LRU page cache
593
604
  - `DisplayLayoutView` for one-shot V2 sends from a pre-built container
594
605
  - Automatic state-driven rebuilds: define `build_ui()`, the library handles the edit (≈ React `render()`)
@@ -597,7 +608,7 @@ for row in rows:
597
608
  ### Components
598
609
  - Stateful buttons, selects, and modals with state integration
599
610
  - Select callbacks can opt into a `values` second parameter
600
- - V2 builders: `card()`, `stats_card()`, `action_section()`, `toggle_section()`, `image_section()`, `link_section()`, `confirm_section()`, `button_row()`, `cycle_button()`, `toggle_button()`, `tab_nav()`, `key_value()`, `alert()`, `progress_bar()`, `divider()`, `gap()`, `gallery()`
611
+ - V2 builders: `card()`, `stats_card()`, `action_section()`, `toggle_section()`, `image_section()`, `link_section()`, `confirm_section()`, `button_row()`, `cycle_button()`, `toggle_button()`, `tab_nav()`, `key_value()`, `alert()`, `progress_bar()`, `divider()`, `gap()`, `gallery()`, `file_attachment()`
601
612
  - Grid helpers: `emoji_grid()` and `button_grid()`
602
613
  - Typed modal fields (`text`, `integer`, `float`, `date`) with per-field validation
603
614
  - Declarative `FormSchema` and `WizardSchema` base classes
@@ -624,7 +635,8 @@ for row in rows:
624
635
  ### Persistence
625
636
  - Persistent views that survive bot restarts with automatic message re-attachment
626
637
  - Opt-in per slot via `persistent_slots = (...)` (≈ `redux-persist`)
627
- - Built-in SQLite and in-memory backends; custom backends via capability-flag `Protocol`
638
+ - Built-in SQLite, PostgreSQL, and in-memory backends; custom backends via capability-flag `Protocol`
639
+ - Cross-process scoped invalidation through PostgreSQL `LISTEN`/`NOTIFY` (multi-worker bots)
628
640
  - Named scoped buckets via `scoped_slot` for per-subsystem persistence
629
641
  - Two-namespace model (`registry` and `application`) with per-namespace debounce and retry backoff
630
642
  - Undo and redo via snapshot-based state history (opt in with `enable_undo`)
@@ -635,20 +647,6 @@ for row in rows:
635
647
 
636
648
  ---
637
649
 
638
- ## V1 Components
639
-
640
- > CascadeUI supports traditional discord.py Views and embeds.
641
-
642
- Use V1 when you need:
643
- - Embed-specific features such as fields or timestamps
644
- - Simpler layouts without containers
645
-
646
- All core features such as navigation, persistence, and undo/redo are supported.
647
-
648
- ![Ticket System](https://raw.githubusercontent.com/HollowTheSilver/CascadeUI/main/assets/gifs/v1-ticket-system.gif)
649
-
650
- ---
651
-
652
650
  ## Examples
653
651
 
654
652
  > The <a href="https://hollowthesilver.github.io/CascadeUI/examples/"><strong>documentation</strong></a> includes full implementations demonstrating practical usage:
@@ -661,6 +659,22 @@ All core features such as navigation, persistence, and undo/redo are supported.
661
659
  - Multi-user games with shared state, hidden information, and challenge flows (TicTacToe, Battleship)
662
660
  - Open-join lobbies with capacity caps and host-vs-participant authority (Werewolf-style)
663
661
 
662
+ ![Examples](https://raw.githubusercontent.com/HollowTheSilver/CascadeUI/main/assets/gifs/v2-hero.gif)
663
+
664
+ ---
665
+
666
+ ## V1 Components
667
+
668
+ > CascadeUI supports traditional discord.py Views and embeds.
669
+
670
+ Use V1 when you need:
671
+ - Embed-specific features such as fields or timestamps
672
+ - Simpler layouts without containers
673
+
674
+ All core features such as navigation, persistence, and undo/redo are supported.
675
+
676
+ ![Ticket System](https://raw.githubusercontent.com/HollowTheSilver/CascadeUI/main/assets/gifs/v1-ticket-system.gif)
677
+
664
678
  ---
665
679
 
666
680
  ## Documentation
@@ -692,7 +706,7 @@ isort cascadeui/
692
706
 
693
707
  ## Developer's Note
694
708
 
695
- > I built CascadeUI with over **ten years** of Python, and roughly fifteen years of developement experience. All documentation, docstrings and test modules are written and designed using custom **Anthropic Opus** sub-agents. I do not attempt to conceal this fact. I'm a proponent of efficient and responsible agent application in software design. That experience is what makes these tools effective. They're amplifiers, not substitutes.
709
+ > I built CascadeUI with over **ten years** of Python, and roughly fifteen years of development experience. All documentation, docstrings and test modules are written and designed using custom **Anthropic Opus** sub-agents. I do not attempt to conceal this fact. I'm a proponent of efficient and responsible agent application in software design. That experience is what makes these tools effective. They're amplifiers, not substitutes.
696
710
  >
697
711
  > *-- Hollow*
698
712
 
@@ -11,7 +11,7 @@ import logging as _logging
11
11
 
12
12
  _logging.getLogger(__name__).addHandler(_logging.NullHandler())
13
13
 
14
- from .components.base import StatefulButton, StatefulSelect
14
+ from .components.base import DynamicPersistentButton, StatefulButton, StatefulSelect
15
15
  from .components.inputs import Checkbox, CheckboxGroup, FileUpload, Modal, RadioGroup, TextInput
16
16
  from .components.patterns import (
17
17
  ConfirmationButtons,
@@ -28,6 +28,7 @@ from .components.patterns import (
28
28
  cycle_button,
29
29
  divider,
30
30
  emoji_grid,
31
+ file_attachment,
31
32
  gallery,
32
33
  gap,
33
34
  image_section,
@@ -39,6 +40,7 @@ from .components.patterns import (
39
40
  toggle_button,
40
41
  toggle_section,
41
42
  )
43
+ from .components.types import EmojiInput, MediaInput
42
44
  from .components.v1_composition import CompositeComponent, get_component, register_component
43
45
  from .components.wrappers import with_confirmation, with_cooldown, with_loading_state
44
46
  from .devtools import DevToolsCog, InspectorView
@@ -81,8 +83,9 @@ from .theming.core import Theme, get_default_theme, get_theme, register_theme, s
81
83
  from .theming.themes import dark_theme, default_theme, light_theme
82
84
  from .utils.decorators import cascade_component, cascade_reducer
83
85
  from .utils.errors import safe_execute, with_error_boundary, with_retry
84
- from .utils.helpers import slugify
86
+ from .utils.fetch import fetch_as_file
85
87
  from .utils.logging import setup_logging
88
+ from .utils.strings import slugify
86
89
  from .utils.tasks import get_task_manager
87
90
  from .validation import (
88
91
  ValidationResult,
@@ -105,12 +108,20 @@ from .views.patterns import (
105
108
  PaginatedLayoutView,
106
109
  PaginatedView,
107
110
  PersistentLeaderboardLayoutView,
111
+ PersistentRolesLayoutView,
112
+ RolesLayoutView,
108
113
  TabLayoutView,
109
114
  TabView,
110
115
  WizardLayoutView,
111
116
  WizardView,
112
117
  )
113
- from .views.patterns.types import FormField, FormSchema, WizardSchema, WizardStep
118
+ from .views.patterns.types import (
119
+ FormField,
120
+ FormSchema,
121
+ RoleCategory,
122
+ WizardSchema,
123
+ WizardStep,
124
+ )
114
125
  from .views.persistent import PersistentLayoutView, PersistentView
115
126
  from .views.view import StatefulView
116
127
 
@@ -121,7 +132,7 @@ if "SQLiteBackend" in _persistence_all:
121
132
  # // ========================================( Script )======================================== // #
122
133
 
123
134
 
124
- __version__ = "3.1.0"
135
+ __version__ = "3.3.0"
125
136
 
126
137
  # Export public API
127
138
  __all__ = [
@@ -160,11 +171,14 @@ __all__ = [
160
171
  "MenuLayoutView",
161
172
  "PaginatedLayoutView",
162
173
  "PersistentLeaderboardLayoutView",
174
+ "PersistentRolesLayoutView",
175
+ "RolesLayoutView",
163
176
  "TabLayoutView",
164
177
  "WizardLayoutView",
165
178
  # Components
166
179
  "StatefulButton",
167
180
  "StatefulSelect",
181
+ "DynamicPersistentButton",
168
182
  "CompositeComponent",
169
183
  "ConfirmationButtons",
170
184
  "PaginationControls",
@@ -183,6 +197,9 @@ __all__ = [
183
197
  "with_loading_state",
184
198
  "with_confirmation",
185
199
  "with_cooldown",
200
+ # Type aliases
201
+ "EmojiInput",
202
+ "MediaInput",
186
203
  # V2 Cards & Sections
187
204
  "card",
188
205
  "action_section",
@@ -206,6 +223,7 @@ __all__ = [
206
223
  "tab_nav",
207
224
  # V2 Media
208
225
  "gallery",
226
+ "file_attachment",
209
227
  # V2 Grids
210
228
  "EmojiGrid",
211
229
  "emoji_grid",
@@ -228,6 +246,7 @@ __all__ = [
228
246
  # Typed schemas
229
247
  "FormField",
230
248
  "FormSchema",
249
+ "RoleCategory",
231
250
  "WizardStep",
232
251
  "WizardSchema",
233
252
  # Validation
@@ -262,6 +281,7 @@ __all__ = [
262
281
  "cascade_reducer",
263
282
  "cascade_component",
264
283
  "slugify",
284
+ "fetch_as_file",
265
285
  # DevTools
266
286
  "InspectorView",
267
287
  "DevToolsCog",
@@ -23,6 +23,7 @@ from .patterns import (
23
23
  confirm_section,
24
24
  cycle_button,
25
25
  divider,
26
+ file_attachment,
26
27
  gallery,
27
28
  gap,
28
29
  image_section,
@@ -35,6 +36,7 @@ from .patterns import (
35
36
  toggle_section,
36
37
  )
37
38
  from .selects import ChannelSelect, Dropdown, MentionableSelect, RoleSelect, UserSelect
39
+ from .types import EmojiInput, MediaInput
38
40
  from .v1_composition import CompositeComponent, get_component, register_component
39
41
  from .wrappers import with_confirmation, with_cooldown, with_loading_state
40
42
 
@@ -101,4 +103,8 @@ __all__ = [
101
103
  "tab_nav",
102
104
  # V2 media
103
105
  "gallery",
106
+ "file_attachment",
107
+ # Type aliases
108
+ "EmojiInput",
109
+ "MediaInput",
104
110
  ]