pycascadeui 3.1.0__tar.gz → 3.2.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 (129) hide show
  1. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/PKG-INFO +54 -44
  2. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/README.md +53 -41
  3. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/__init__.py +18 -3
  4. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/components/__init__.py +3 -0
  5. pycascadeui-3.2.0/cascadeui/components/base.py +402 -0
  6. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/components/inputs.py +114 -57
  7. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/components/patterns/v2.py +9 -8
  8. pycascadeui-3.2.0/cascadeui/components/selects.py +270 -0
  9. pycascadeui-3.2.0/cascadeui/components/types.py +25 -0
  10. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/components/wrappers.py +3 -1
  11. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/devtools.py +13 -2
  12. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/persistence/manager.py +17 -4
  13. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/state/middleware/persistence.py +22 -0
  14. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/state/middleware/undo.py +34 -1
  15. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/utils/coercion.py +41 -1
  16. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/views/_navigation.py +199 -93
  17. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/views/base.py +20 -4
  18. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/views/layout.py +16 -16
  19. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/views/patterns/__init__.py +3 -0
  20. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/views/patterns/form.py +4 -1
  21. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/views/patterns/leaderboard.py +49 -10
  22. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/views/patterns/menu.py +6 -0
  23. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/views/patterns/paginated.py +106 -25
  24. pycascadeui-3.2.0/cascadeui/views/patterns/roles.py +538 -0
  25. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/views/patterns/tabs.py +3 -0
  26. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/views/patterns/types.py +85 -5
  27. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/views/patterns/wizard.py +9 -7
  28. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/views/persistent.py +27 -36
  29. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/pycascadeui.egg-info/PKG-INFO +54 -44
  30. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/pycascadeui.egg-info/SOURCES.txt +6 -0
  31. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/pycascadeui.egg-info/requires.txt +0 -3
  32. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/pyproject.toml +1 -4
  33. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_components.py +116 -0
  34. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_devtools.py +26 -2
  35. pycascadeui-3.2.0/tests/test_dynamic_persistent_button.py +330 -0
  36. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_form_patterns.py +23 -8
  37. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_input_coercion.py +70 -1
  38. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_inputs.py +100 -15
  39. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_layout_view.py +38 -10
  40. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_leaderboard_patterns.py +164 -10
  41. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_navigation.py +171 -3
  42. pycascadeui-3.2.0/tests/test_navigation_artifact_restore.py +232 -0
  43. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_paginated_patterns.py +210 -0
  44. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_persistence.py +101 -6
  45. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_persistence_middleware.py +2 -2
  46. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_persistent_views.py +46 -4
  47. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_rebuild_callback.py +29 -14
  48. pycascadeui-3.2.0/tests/test_roles_patterns.py +505 -0
  49. pycascadeui-3.2.0/tests/test_selects.py +227 -0
  50. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_undo.py +98 -0
  51. pycascadeui-3.1.0/cascadeui/components/base.py +0 -199
  52. pycascadeui-3.1.0/cascadeui/components/selects.py +0 -106
  53. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/LICENSE +0 -0
  54. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/__main__.py +0 -0
  55. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/components/buttons.py +0 -0
  56. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/components/patterns/__init__.py +0 -0
  57. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/components/patterns/v1.py +0 -0
  58. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/components/v1_composition.py +0 -0
  59. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/exceptions.py +0 -0
  60. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/persistence/__init__.py +0 -0
  61. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/persistence/backends/__init__.py +0 -0
  62. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/persistence/backends/memory.py +0 -0
  63. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/persistence/backends/sqlite.py +0 -0
  64. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/persistence/config.py +0 -0
  65. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/persistence/migrations.py +0 -0
  66. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/persistence/protocols.py +0 -0
  67. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/persistence/schema.py +0 -0
  68. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/py.typed +0 -0
  69. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/setup.py +0 -0
  70. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/state/__init__.py +0 -0
  71. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/state/actions.py +0 -0
  72. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/state/computed.py +0 -0
  73. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/state/middleware/__init__.py +0 -0
  74. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/state/middleware/logging.py +0 -0
  75. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/state/reducers.py +0 -0
  76. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/state/singleton.py +0 -0
  77. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/state/slots.py +0 -0
  78. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/state/store.py +0 -0
  79. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/state/types.py +0 -0
  80. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/theming/__init__.py +0 -0
  81. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/theming/context.py +0 -0
  82. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/theming/core.py +0 -0
  83. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/theming/themes.py +0 -0
  84. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/tracing.py +0 -0
  85. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/utils/__init__.py +0 -0
  86. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/utils/decorators.py +0 -0
  87. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/utils/errors.py +0 -0
  88. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/utils/helpers.py +0 -0
  89. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/utils/logging.py +0 -0
  90. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/utils/tasks.py +0 -0
  91. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/validation.py +0 -0
  92. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/views/__init__.py +0 -0
  93. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/views/_interaction.py +0 -0
  94. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/cascadeui/views/view.py +0 -0
  95. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/pycascadeui.egg-info/dependency_links.txt +0 -0
  96. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/pycascadeui.egg-info/top_level.txt +0 -0
  97. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/setup.cfg +0 -0
  98. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_auto_defer.py +0 -0
  99. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_backends.py +0 -0
  100. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_batching.py +0 -0
  101. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_button_grid.py +0 -0
  102. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_computed.py +0 -0
  103. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_emoji_grid.py +0 -0
  104. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_ephemeral_refresh.py +0 -0
  105. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_five_pillars.py +0 -0
  106. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_hooks.py +0 -0
  107. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_instance_limit.py +0 -0
  108. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_layout_pagination.py +0 -0
  109. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_layout_patterns.py +0 -0
  110. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_layout_persistent.py +0 -0
  111. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_menu_patterns.py +0 -0
  112. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_message_cleanup.py +0 -0
  113. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_middleware.py +0 -0
  114. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_nav_version.py +0 -0
  115. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_owner_only.py +0 -0
  116. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_pagination.py +0 -0
  117. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_pillar_isolation.py +0 -0
  118. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_reducers.py +0 -0
  119. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_scoping.py +0 -0
  120. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_state_slots.py +0 -0
  121. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_state_store.py +0 -0
  122. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_tab_patterns.py +0 -0
  123. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_theming.py +0 -0
  124. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_update_coalescing.py +0 -0
  125. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_v2_helpers.py +0 -0
  126. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_validation.py +0 -0
  127. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_view_init.py +0 -0
  128. {pycascadeui-3.1.0 → pycascadeui-3.2.0}/tests/test_wizard_patterns.py +0 -0
  129. {pycascadeui-3.1.0 → pycascadeui-3.2.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.2.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
@@ -36,8 +36,6 @@ Requires-Dist: mkdocs-material>=9.0; extra == "docs"
36
36
  Requires-Dist: pymdown-extensions>=10.0; extra == "docs"
37
37
  Provides-Extra: sqlite
38
38
  Requires-Dist: aiosqlite>=0.19; extra == "sqlite"
39
- Provides-Extra: redis
40
- Requires-Dist: redis>=5.0; extra == "redis"
41
39
  Dynamic: license-file
42
40
 
43
41
  <p align="center">
@@ -226,22 +224,25 @@ class NotificationPanel(StatefulLayoutView):
226
224
  > 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
225
 
228
226
  ```python
229
- class SettingsHub(StatefulLayoutView):
227
+ class MyFleetView(StatefulLayoutView):
230
228
  state_scope = "user"
231
- subscribed_actions = {"SETTINGS_UPDATED"}
229
+ subscribed_actions = {"FLEET_REROLLED"}
232
230
 
233
231
  def build_ui(self):
234
232
  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
- }),
233
+ grid = emoji_grid(10, 10, fill="\U0001f7e6", row_labels="alpha", col_labels="numeric")
234
+ for row, col in self.ship_cells():
235
+ grid[row, col] = "\U0001f6a2"
236
+
237
+ self.add_item(card("## My Fleet", divider(), grid, color=discord.Color.blue()))
238
+ self.add_item(ActionRow(
239
+ StatefulButton(label="Regenerate", emoji="\U0001f3b2", callback=self._reroll),
242
240
  ))
243
241
 
244
- # build_ui() is called automatically on state changes --
242
+ async def _reroll(self, interaction):
243
+ await self.dispatch("FLEET_REROLLED", {"cells": random_placement()})
244
+
245
+ # build_ui() runs automatically on every FLEET_REROLLED dispatch --
245
246
  # no manual refresh() or on_state_changed() override needed.
246
247
  ```
247
248
 
@@ -300,18 +301,14 @@ class BattleshipView(StatefulLayoutView):
300
301
 
301
302
  ### Lifecycle Control
302
303
 
303
- > Control active sessions per user, guild, or globally with automatic cleanup, replacement policies, and view-capacity caps.
304
+ > 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
305
 
305
306
  ```python
306
- class DashboardView(TabLayoutView):
307
+ class SettingsHubView(MenuLayoutView):
307
308
  instance_limit = 1 # Only one open at a time
308
309
  instance_scope = "user_guild" # Per user per guild
309
310
  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()
311
+ exit_policy = "disable" # Old view's buttons grey out, message stays
315
312
  ```
316
313
 
317
314
  ![V2 Instance Limiting](https://raw.githubusercontent.com/HollowTheSilver/CascadeUI/main/assets/gifs/v2-session-limiting.gif)
@@ -332,12 +329,18 @@ async def setup_hook(self):
332
329
  )
333
330
  ```
334
331
 
335
- Declare `persistent_slots` on any view that should carry application state to disk. The rest stays volatile:
332
+ 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
333
 
337
334
  ```python
338
- class BattleshipView(StatefulLayoutView):
339
- scoped_slot = "battleship_stats" # per-subsystem bucket
340
- persistent_slots = ("battleship_stats",) # opt this slot into persistence
335
+ class GuildRoles(PersistentRolesLayoutView):
336
+ categories = [
337
+ RoleCategory(name="Color Roles", exclusive=True, roles={"Red": 123, "Blue": 456}),
338
+ RoleCategory(name="Pronouns", required=True, roles={"He/Him": 789, "She/Her": 12}),
339
+ ]
340
+
341
+ # Same key in, same panel out -- across bot restarts.
342
+ panel = GuildRoles(context=ctx, persistence_key=f"roles:{ctx.guild.id}")
343
+ await panel.send()
341
344
  ```
342
345
 
343
346
  ![Persistence](https://raw.githubusercontent.com/HollowTheSilver/CascadeUI/main/assets/gifs/v2-persistence-restart.gif)
@@ -349,8 +352,9 @@ class BattleshipView(StatefulLayoutView):
349
352
  > Snapshot-based state history per session with built-in undo and redo support.
350
353
 
351
354
  ```python
352
- class SettingsHub(StatefulLayoutView):
355
+ class NotificationsView(StatefulLayoutView):
353
356
  enable_undo = True # Every dispatch captures a snapshot
357
+ undo_limit = 10 # Stack depth cap (self.undo_depth / self.redo_depth read live)
354
358
 
355
359
  async def _undo(self, interaction):
356
360
  await self.undo() # Restore previous snapshot
@@ -490,7 +494,13 @@ await view.send()
490
494
  # Persistent variant -- admin-posted panel that survives bot restarts
491
495
  # and re-fetches live data on every restore.
492
496
  class PersistentBoard(PersistentLeaderboardLayoutView):
493
- persistence_key = "battleship-leaderboard-main"
497
+ pass
498
+
499
+ panel = PersistentBoard(
500
+ context=ctx,
501
+ persistence_key="battleship-leaderboard-main",
502
+ )
503
+ await panel.send()
494
504
  ```
495
505
 
496
506
  ![Leaderboards](https://raw.githubusercontent.com/HollowTheSilver/CascadeUI/main/assets/gifs/v2-leaderboard.gif)
@@ -602,8 +612,6 @@ for row in rows:
602
612
  view.add_item(row)
603
613
  ```
604
614
 
605
- ![Button Grid](https://raw.githubusercontent.com/HollowTheSilver/CascadeUI/main/assets/pngs/v2-button-grid.PNG)
606
-
607
615
  <div align="center">
608
616
  <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
617
  <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 +638,7 @@ for row in rows:
630
638
  ### Views
631
639
  - V2 layout-based system for structured, container-driven interfaces
632
640
  - Full support for traditional discord.py Views (V1)
633
- - Pre-built patterns: menus, tabs, wizards, forms, pagination, leaderboards
641
+ - Pre-built patterns: menus, tabs, wizards, forms, pagination, leaderboards, roles
634
642
  - `PaginatedView.from_cursor()` for lazy cursor-driven pagination with LRU page cache
635
643
  - `DisplayLayoutView` for one-shot V2 sends from a pre-built container
636
644
  - Automatic state-driven rebuilds: define `build_ui()`, the library handles the edit (≈ React `render()`)
@@ -677,20 +685,6 @@ for row in rows:
677
685
 
678
686
  ---
679
687
 
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
688
  ## Examples
695
689
 
696
690
  > The <a href="https://hollowthesilver.github.io/CascadeUI/examples/"><strong>documentation</strong></a> includes full implementations demonstrating practical usage:
@@ -703,6 +697,22 @@ All core features such as navigation, persistence, and undo/redo are supported.
703
697
  - Multi-user games with shared state, hidden information, and challenge flows (TicTacToe, Battleship)
704
698
  - Open-join lobbies with capacity caps and host-vs-participant authority (Werewolf-style)
705
699
 
700
+ ![Examples](https://raw.githubusercontent.com/HollowTheSilver/CascadeUI/main/assets/gifs/v2-hero.gif)
701
+
702
+ ---
703
+
704
+ ## V1 Components
705
+
706
+ > CascadeUI supports traditional discord.py Views and embeds.
707
+
708
+ Use V1 when you need:
709
+ - Embed-specific features such as fields or timestamps
710
+ - Simpler layouts without containers
711
+
712
+ All core features such as navigation, persistence, and undo/redo are supported.
713
+
714
+ ![Ticket System](https://raw.githubusercontent.com/HollowTheSilver/CascadeUI/main/assets/gifs/v1-ticket-system.gif)
715
+
706
716
  ---
707
717
 
708
718
  ## Documentation
@@ -734,7 +744,7 @@ isort cascadeui/
734
744
 
735
745
  ## Developer's Note
736
746
 
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.
747
+ > 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
748
  >
739
749
  > *-- Hollow*
740
750
 
@@ -184,22 +184,25 @@ class NotificationPanel(StatefulLayoutView):
184
184
  > 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
185
 
186
186
  ```python
187
- class SettingsHub(StatefulLayoutView):
187
+ class MyFleetView(StatefulLayoutView):
188
188
  state_scope = "user"
189
- subscribed_actions = {"SETTINGS_UPDATED"}
189
+ subscribed_actions = {"FLEET_REROLLED"}
190
190
 
191
191
  def build_ui(self):
192
192
  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
- }),
193
+ grid = emoji_grid(10, 10, fill="\U0001f7e6", row_labels="alpha", col_labels="numeric")
194
+ for row, col in self.ship_cells():
195
+ grid[row, col] = "\U0001f6a2"
196
+
197
+ self.add_item(card("## My Fleet", divider(), grid, color=discord.Color.blue()))
198
+ self.add_item(ActionRow(
199
+ StatefulButton(label="Regenerate", emoji="\U0001f3b2", callback=self._reroll),
200
200
  ))
201
201
 
202
- # build_ui() is called automatically on state changes --
202
+ async def _reroll(self, interaction):
203
+ await self.dispatch("FLEET_REROLLED", {"cells": random_placement()})
204
+
205
+ # build_ui() runs automatically on every FLEET_REROLLED dispatch --
203
206
  # no manual refresh() or on_state_changed() override needed.
204
207
  ```
205
208
 
@@ -258,18 +261,14 @@ class BattleshipView(StatefulLayoutView):
258
261
 
259
262
  ### Lifecycle Control
260
263
 
261
- > Control active sessions per user, guild, or globally with automatic cleanup, replacement policies, and view-capacity caps.
264
+ > 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
265
 
263
266
  ```python
264
- class DashboardView(TabLayoutView):
267
+ class SettingsHubView(MenuLayoutView):
265
268
  instance_limit = 1 # Only one open at a time
266
269
  instance_scope = "user_guild" # Per user per guild
267
270
  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()
271
+ exit_policy = "disable" # Old view's buttons grey out, message stays
273
272
  ```
274
273
 
275
274
  ![V2 Instance Limiting](https://raw.githubusercontent.com/HollowTheSilver/CascadeUI/main/assets/gifs/v2-session-limiting.gif)
@@ -290,12 +289,18 @@ async def setup_hook(self):
290
289
  )
291
290
  ```
292
291
 
293
- Declare `persistent_slots` on any view that should carry application state to disk. The rest stays volatile:
292
+ 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
293
 
295
294
  ```python
296
- class BattleshipView(StatefulLayoutView):
297
- scoped_slot = "battleship_stats" # per-subsystem bucket
298
- persistent_slots = ("battleship_stats",) # opt this slot into persistence
295
+ class GuildRoles(PersistentRolesLayoutView):
296
+ categories = [
297
+ RoleCategory(name="Color Roles", exclusive=True, roles={"Red": 123, "Blue": 456}),
298
+ RoleCategory(name="Pronouns", required=True, roles={"He/Him": 789, "She/Her": 12}),
299
+ ]
300
+
301
+ # Same key in, same panel out -- across bot restarts.
302
+ panel = GuildRoles(context=ctx, persistence_key=f"roles:{ctx.guild.id}")
303
+ await panel.send()
299
304
  ```
300
305
 
301
306
  ![Persistence](https://raw.githubusercontent.com/HollowTheSilver/CascadeUI/main/assets/gifs/v2-persistence-restart.gif)
@@ -307,8 +312,9 @@ class BattleshipView(StatefulLayoutView):
307
312
  > Snapshot-based state history per session with built-in undo and redo support.
308
313
 
309
314
  ```python
310
- class SettingsHub(StatefulLayoutView):
315
+ class NotificationsView(StatefulLayoutView):
311
316
  enable_undo = True # Every dispatch captures a snapshot
317
+ undo_limit = 10 # Stack depth cap (self.undo_depth / self.redo_depth read live)
312
318
 
313
319
  async def _undo(self, interaction):
314
320
  await self.undo() # Restore previous snapshot
@@ -448,7 +454,13 @@ await view.send()
448
454
  # Persistent variant -- admin-posted panel that survives bot restarts
449
455
  # and re-fetches live data on every restore.
450
456
  class PersistentBoard(PersistentLeaderboardLayoutView):
451
- persistence_key = "battleship-leaderboard-main"
457
+ pass
458
+
459
+ panel = PersistentBoard(
460
+ context=ctx,
461
+ persistence_key="battleship-leaderboard-main",
462
+ )
463
+ await panel.send()
452
464
  ```
453
465
 
454
466
  ![Leaderboards](https://raw.githubusercontent.com/HollowTheSilver/CascadeUI/main/assets/gifs/v2-leaderboard.gif)
@@ -560,8 +572,6 @@ for row in rows:
560
572
  view.add_item(row)
561
573
  ```
562
574
 
563
- ![Button Grid](https://raw.githubusercontent.com/HollowTheSilver/CascadeUI/main/assets/pngs/v2-button-grid.PNG)
564
-
565
575
  <div align="center">
566
576
  <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
577
  <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 +598,7 @@ for row in rows:
588
598
  ### Views
589
599
  - V2 layout-based system for structured, container-driven interfaces
590
600
  - Full support for traditional discord.py Views (V1)
591
- - Pre-built patterns: menus, tabs, wizards, forms, pagination, leaderboards
601
+ - Pre-built patterns: menus, tabs, wizards, forms, pagination, leaderboards, roles
592
602
  - `PaginatedView.from_cursor()` for lazy cursor-driven pagination with LRU page cache
593
603
  - `DisplayLayoutView` for one-shot V2 sends from a pre-built container
594
604
  - Automatic state-driven rebuilds: define `build_ui()`, the library handles the edit (≈ React `render()`)
@@ -635,20 +645,6 @@ for row in rows:
635
645
 
636
646
  ---
637
647
 
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
648
  ## Examples
653
649
 
654
650
  > The <a href="https://hollowthesilver.github.io/CascadeUI/examples/"><strong>documentation</strong></a> includes full implementations demonstrating practical usage:
@@ -661,6 +657,22 @@ All core features such as navigation, persistence, and undo/redo are supported.
661
657
  - Multi-user games with shared state, hidden information, and challenge flows (TicTacToe, Battleship)
662
658
  - Open-join lobbies with capacity caps and host-vs-participant authority (Werewolf-style)
663
659
 
660
+ ![Examples](https://raw.githubusercontent.com/HollowTheSilver/CascadeUI/main/assets/gifs/v2-hero.gif)
661
+
662
+ ---
663
+
664
+ ## V1 Components
665
+
666
+ > CascadeUI supports traditional discord.py Views and embeds.
667
+
668
+ Use V1 when you need:
669
+ - Embed-specific features such as fields or timestamps
670
+ - Simpler layouts without containers
671
+
672
+ All core features such as navigation, persistence, and undo/redo are supported.
673
+
674
+ ![Ticket System](https://raw.githubusercontent.com/HollowTheSilver/CascadeUI/main/assets/gifs/v1-ticket-system.gif)
675
+
664
676
  ---
665
677
 
666
678
  ## Documentation
@@ -692,7 +704,7 @@ isort cascadeui/
692
704
 
693
705
  ## Developer's Note
694
706
 
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.
707
+ > 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
708
  >
697
709
  > *-- Hollow*
698
710
 
@@ -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,
@@ -39,6 +39,7 @@ from .components.patterns import (
39
39
  toggle_button,
40
40
  toggle_section,
41
41
  )
42
+ from .components.types import EmojiInput
42
43
  from .components.v1_composition import CompositeComponent, get_component, register_component
43
44
  from .components.wrappers import with_confirmation, with_cooldown, with_loading_state
44
45
  from .devtools import DevToolsCog, InspectorView
@@ -105,12 +106,20 @@ from .views.patterns import (
105
106
  PaginatedLayoutView,
106
107
  PaginatedView,
107
108
  PersistentLeaderboardLayoutView,
109
+ PersistentRolesLayoutView,
110
+ RolesLayoutView,
108
111
  TabLayoutView,
109
112
  TabView,
110
113
  WizardLayoutView,
111
114
  WizardView,
112
115
  )
113
- from .views.patterns.types import FormField, FormSchema, WizardSchema, WizardStep
116
+ from .views.patterns.types import (
117
+ FormField,
118
+ FormSchema,
119
+ RoleCategory,
120
+ WizardSchema,
121
+ WizardStep,
122
+ )
114
123
  from .views.persistent import PersistentLayoutView, PersistentView
115
124
  from .views.view import StatefulView
116
125
 
@@ -121,7 +130,7 @@ if "SQLiteBackend" in _persistence_all:
121
130
  # // ========================================( Script )======================================== // #
122
131
 
123
132
 
124
- __version__ = "3.1.0"
133
+ __version__ = "3.2.0"
125
134
 
126
135
  # Export public API
127
136
  __all__ = [
@@ -160,11 +169,14 @@ __all__ = [
160
169
  "MenuLayoutView",
161
170
  "PaginatedLayoutView",
162
171
  "PersistentLeaderboardLayoutView",
172
+ "PersistentRolesLayoutView",
173
+ "RolesLayoutView",
163
174
  "TabLayoutView",
164
175
  "WizardLayoutView",
165
176
  # Components
166
177
  "StatefulButton",
167
178
  "StatefulSelect",
179
+ "DynamicPersistentButton",
168
180
  "CompositeComponent",
169
181
  "ConfirmationButtons",
170
182
  "PaginationControls",
@@ -183,6 +195,8 @@ __all__ = [
183
195
  "with_loading_state",
184
196
  "with_confirmation",
185
197
  "with_cooldown",
198
+ # Type aliases
199
+ "EmojiInput",
186
200
  # V2 Cards & Sections
187
201
  "card",
188
202
  "action_section",
@@ -228,6 +242,7 @@ __all__ = [
228
242
  # Typed schemas
229
243
  "FormField",
230
244
  "FormSchema",
245
+ "RoleCategory",
231
246
  "WizardStep",
232
247
  "WizardSchema",
233
248
  # Validation
@@ -35,6 +35,7 @@ from .patterns import (
35
35
  toggle_section,
36
36
  )
37
37
  from .selects import ChannelSelect, Dropdown, MentionableSelect, RoleSelect, UserSelect
38
+ from .types import EmojiInput
38
39
  from .v1_composition import CompositeComponent, get_component, register_component
39
40
  from .wrappers import with_confirmation, with_cooldown, with_loading_state
40
41
 
@@ -101,4 +102,6 @@ __all__ = [
101
102
  "tab_nav",
102
103
  # V2 media
103
104
  "gallery",
105
+ # Type aliases
106
+ "EmojiInput",
104
107
  ]