DiscordPYComponents 1.0.0__tar.gz → 1.3.1__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 (22) hide show
  1. discordpycomponents-1.3.1/DiscordPYComponents.egg-info/PKG-INFO +694 -0
  2. {discordpycomponents-1.0.0 → discordpycomponents-1.3.1}/DiscordPYComponents.egg-info/SOURCES.txt +2 -0
  3. discordpycomponents-1.3.1/PKG-INFO +694 -0
  4. discordpycomponents-1.3.1/README.md +682 -0
  5. {discordpycomponents-1.0.0 → discordpycomponents-1.3.1}/py_components/__init__.py +29 -8
  6. {discordpycomponents-1.0.0 → discordpycomponents-1.3.1}/py_components/components.py +325 -42
  7. {discordpycomponents-1.0.0 → discordpycomponents-1.3.1}/py_components/enums.py +7 -9
  8. {discordpycomponents-1.0.0 → discordpycomponents-1.3.1}/py_components/http.py +14 -6
  9. discordpycomponents-1.3.1/py_components/ids.py +146 -0
  10. discordpycomponents-1.3.1/py_components/modal.py +46 -0
  11. {discordpycomponents-1.0.0 → discordpycomponents-1.3.1}/py_components/registry.py +0 -12
  12. discordpycomponents-1.3.1/py_components/ui.py +285 -0
  13. {discordpycomponents-1.0.0 → discordpycomponents-1.3.1}/py_components/view.py +5 -37
  14. {discordpycomponents-1.0.0 → discordpycomponents-1.3.1}/pyproject.toml +2 -2
  15. discordpycomponents-1.0.0/DiscordPYComponents.egg-info/PKG-INFO +0 -257
  16. discordpycomponents-1.0.0/PKG-INFO +0 -257
  17. discordpycomponents-1.0.0/README.md +0 -245
  18. discordpycomponents-1.0.0/py_components/ui.py +0 -134
  19. {discordpycomponents-1.0.0 → discordpycomponents-1.3.1}/DiscordPYComponents.egg-info/dependency_links.txt +0 -0
  20. {discordpycomponents-1.0.0 → discordpycomponents-1.3.1}/DiscordPYComponents.egg-info/requires.txt +0 -0
  21. {discordpycomponents-1.0.0 → discordpycomponents-1.3.1}/DiscordPYComponents.egg-info/top_level.txt +0 -0
  22. {discordpycomponents-1.0.0 → discordpycomponents-1.3.1}/setup.cfg +0 -0
@@ -0,0 +1,694 @@
1
+ Metadata-Version: 2.4
2
+ Name: DiscordPYComponents
3
+ Version: 1.3.1
4
+ Summary: Discord Components V2 for discord.py
5
+ Author: 2gm49
6
+ License: MIT
7
+ Project-URL: Homepage, https://pypi.org/project/DiscordPYComponents/
8
+ Project-URL: Documentation, https://docs.discord.com/developers/components/overview
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: aiohttp
12
+
13
+ # DiscordPYComponents
14
+
15
+ Discord Components V2 for discord.py.
16
+
17
+ PyPI: https://pypi.org/project/DiscordPYComponents/
18
+
19
+ Discord docs:
20
+ - https://docs.discord.com/developers/components/overview
21
+ - https://docs.discord.com/developers/components/reference
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ pip install DiscordPYComponents
27
+ ```
28
+
29
+ Needs:
30
+ - Python 3.10+
31
+ - discord.py (or anything that gives you a token / interaction)
32
+ - aiohttp
33
+
34
+ ```python
35
+ from py_components import (
36
+ UI, TextDisplay, Button, ButtonStyle,
37
+ Container, Section, Separator, ActionRow,
38
+ StringSelect, SelectOption, Modal, Label, TextInput,
39
+ )
40
+ ```
41
+
42
+ If your package folder is named differently, import from that name instead.
43
+
44
+ ## How V2 works
45
+
46
+ Messages with Components V2 set `flags` to `IS_COMPONENTS_V2` (`1 << 15` / `32768`).
47
+
48
+ That means:
49
+ - no top-level `content` or `embeds`
50
+ - no stickers / poll on that message
51
+ - everything is components
52
+ - attachments only show if a `File` / media component points at them
53
+ - max 40 components per message
54
+ - once the flag is on a message, you can't turn it off
55
+
56
+ This library builds those payloads and can send them through discord.py or raw HTTP.
57
+
58
+ ---
59
+
60
+ ## UI
61
+
62
+ Main way to build and send a V2 message.
63
+
64
+ ```python
65
+ ui = UI(
66
+ TextDisplay("hello"),
67
+ Button("ok", custom_id="ok", style=ButtonStyle.SUCCESS),
68
+ )
69
+ await ui.send(ctx)
70
+ ```
71
+
72
+ ### Methods
73
+
74
+ | method | what |
75
+ |--------|------|
76
+ | `UI(*components)` | start a builder |
77
+ | `ui.add(*components)` | append more |
78
+ | `ui.to_payload()` | `{"flags": 32768, "components": [...]}` |
79
+ | `ui.to_components()` | just the components list |
80
+ | `ui.get(id)` | find a component dict by numeric id |
81
+ | `ui.replace(id, component)` | payload with that id swapped |
82
+ | `await ui.send(target, ephemeral=False, files=None)` | channel or interaction |
83
+ | `await ui.reply(interaction, ...)` | same as send on an interaction |
84
+ | `await ui.edit(target, message=None)` | edit interaction original / a Message |
85
+
86
+ `send` / `reply` try discord.py first (`ctx.send`, `interaction.response.send_message`, `followup.send`). Falls back to raw HTTP if that fails.
87
+
88
+ ```python
89
+ await ui.send(ctx)
90
+ await ui.reply(interaction)
91
+ await ui.reply(interaction, ephemeral=True)
92
+ await ui.edit(interaction)
93
+ await ui.edit(message)
94
+ ```
95
+
96
+ Also: `await send_ui(target, ui)` — same as `ui.send(target)`.
97
+
98
+ Loose `Button`s are auto-wrapped into `ActionRow`s (5 per row). Put selects in an `ActionRow` yourself.
99
+
100
+ ---
101
+
102
+ ## Content components
103
+
104
+ ### TextDisplay / Text
105
+
106
+ Markdown text. Alias: `Text`.
107
+
108
+ ```python
109
+ TextDisplay("**bold** and normal", id=1)
110
+ ```
111
+
112
+ | arg | notes |
113
+ |-----|--------|
114
+ | `content` | required, 1–4000 chars |
115
+ | `id` | optional numeric id |
116
+
117
+ ### Thumbnail
118
+
119
+ Small image for a Section accessory. Images only (incl gif/webp).
120
+
121
+ ```python
122
+ Thumbnail("https://cdn.discordapp.com/...", description="icon", spoiler=False)
123
+ ```
124
+
125
+ | arg | notes |
126
+ |-----|--------|
127
+ | `url` | required |
128
+ | `description` | alt text, max 1024 |
129
+ | `spoiler` | blur it |
130
+ | `id` | optional |
131
+
132
+ ### MediaGallery / MediaGalleryItem
133
+
134
+ 1–10 media items.
135
+
136
+ ```python
137
+ MediaGallery(
138
+ "https://example.com/a.png",
139
+ MediaGalleryItem("https://example.com/b.png", description="chart", spoiler=True),
140
+ )
141
+ ```
142
+
143
+ | arg | notes |
144
+ |-----|--------|
145
+ | `*items` | urls and/or `MediaGalleryItem` |
146
+ | `id` | optional |
147
+
148
+ `MediaGalleryItem(url, description=None, spoiler=False)`
149
+
150
+ ### File
151
+
152
+ Shows an uploaded attachment. Upload the file with the message; reference it here.
153
+
154
+ ```python
155
+ File("report.pdf") # -> attachment://report.pdf
156
+ File("attachment://report.pdf", spoiler=True)
157
+ ```
158
+
159
+ | arg | notes |
160
+ |-----|--------|
161
+ | `filename` | name or `attachment://name` |
162
+ | `spoiler` | blur it |
163
+ | `id` | optional |
164
+
165
+ ---
166
+
167
+ ## Layout components
168
+
169
+ ### ActionRow
170
+
171
+ Holds up to 5 buttons **or** one select.
172
+
173
+ ```python
174
+ ActionRow(
175
+ Button("a", custom_id="a"),
176
+ Button("b", custom_id="b"),
177
+ )
178
+ ActionRow(StringSelect("pick", options=[...]))
179
+ ```
180
+
181
+ ### Section
182
+
183
+ 1–3 TextDisplays + optional accessory (Button or Thumbnail).
184
+
185
+ ```python
186
+ Section(
187
+ TextDisplay("**title**"),
188
+ TextDisplay("body text"),
189
+ accessory=Thumbnail("https://..."),
190
+ )
191
+ ```
192
+
193
+ ### Separator
194
+
195
+ Spacing between components.
196
+
197
+ ```python
198
+ Separator() # divider on, small spacing
199
+ Separator(divider=False, spacing=SeparatorSpacing.LARGE)
200
+ ```
201
+
202
+ | arg | notes |
203
+ |-----|--------|
204
+ | `divider` | show a line, default True |
205
+ | `spacing` | `SeparatorSpacing.SMALL` (1) or `LARGE` (2) |
206
+ | `id` | optional |
207
+
208
+ ### Container
209
+
210
+ Groups children. Optional left accent colour (like embeds) and spoiler.
211
+
212
+ ```python
213
+ Container(
214
+ TextDisplay("# header"),
215
+ Separator(),
216
+ TextDisplay("body"),
217
+ accent_color=0x5865F2,
218
+ spoiler=False,
219
+ )
220
+ ```
221
+
222
+ Children can be ActionRow, TextDisplay, Section, MediaGallery, Separator, File.
223
+
224
+ ```python
225
+ c = Container(TextDisplay("x"))
226
+ c.add(Separator())
227
+ ```
228
+
229
+ ---
230
+
231
+ ## Buttons
232
+
233
+ ```python
234
+ Button(
235
+ "label",
236
+ custom_id="id",
237
+ style=ButtonStyle.PRIMARY,
238
+ emoji="🔥", # or make_emoji(...)
239
+ disabled=False,
240
+ id=None,
241
+ )
242
+ ```
243
+
244
+ | style | value | needs |
245
+ |-------|-------|--------|
246
+ | `PRIMARY` | 1 | custom_id |
247
+ | `SECONDARY` | 2 | custom_id |
248
+ | `SUCCESS` | 3 | custom_id |
249
+ | `DANGER` | 4 | custom_id |
250
+ | `LINK` | 5 | url= |
251
+ | `PREMIUM` | 6 | sku_id= |
252
+
253
+ Lowercase aliases work too: `ButtonStyle.primary`, etc.
254
+
255
+ ```python
256
+ Button("docs", style=ButtonStyle.LINK, url="https://discord.com/developers/docs")
257
+ Button("buy", style=ButtonStyle.PREMIUM, sku_id="1234567890")
258
+ Button("nope", custom_id="x", disabled=True)
259
+ ```
260
+
261
+ Must sit in an ActionRow or as a Section accessory. UI auto-wraps loose buttons.
262
+
263
+ Label max 80 chars. custom_id 1–100 chars.
264
+
265
+ ---
266
+
267
+ ## Selects
268
+
269
+ All selects need a `custom_id`. In messages they go inside an ActionRow (one select per row). In modals they go inside a Label.
270
+
271
+ ### SelectOption (StringSelect only)
272
+
273
+ ```python
274
+ SelectOption(
275
+ "Red",
276
+ "red",
277
+ description="the red one",
278
+ emoji="🔴",
279
+ default=False,
280
+ )
281
+ ```
282
+
283
+ ### StringSelect
284
+
285
+ ```python
286
+ StringSelect(
287
+ "colour",
288
+ options=[
289
+ SelectOption("red", "red"),
290
+ SelectOption("blue", "blue"),
291
+ ],
292
+ placeholder="pick one",
293
+ min_values=1,
294
+ max_values=1,
295
+ disabled=False,
296
+ required=None, # modals only
297
+ )
298
+ ```
299
+
300
+ 1–25 options.
301
+
302
+ ### UserSelect / RoleSelect / MentionableSelect / ChannelSelect
303
+
304
+ ```python
305
+ UserSelect(
306
+ "mods",
307
+ placeholder="pick mods",
308
+ min_values=1,
309
+ max_values=3,
310
+ default_values=[
311
+ {"id": "111111111111111111", "type": "user"},
312
+ ],
313
+ required=None,
314
+ disabled=False,
315
+ )
316
+
317
+ RoleSelect("roles", min_values=0, max_values=5)
318
+ MentionableSelect("who")
319
+ ChannelSelect(
320
+ "channel",
321
+ channel_types=[0, 2, 5], # text, voice, announce — discord channel type ints
322
+ )
323
+ ```
324
+
325
+ `default_values` entries: `{"id": "<snowflake>", "type": "user"|"role"|"channel"}`.
326
+
327
+ `required` only matters in modals. `disabled` only in messages (modals error if disabled).
328
+
329
+ ---
330
+
331
+ ## Emoji helper
332
+
333
+ ```python
334
+ from py_components import make_emoji
335
+
336
+ make_emoji("🔥")
337
+ make_emoji("pepe", id=123456789012345678, animated=True)
338
+ ```
339
+
340
+ Buttons and SelectOptions also take a plain unicode string:
341
+
342
+ ```python
343
+ Button("ok", custom_id="ok", emoji="✅")
344
+ SelectOption("fire", "fire", emoji="🔥")
345
+ ```
346
+
347
+ ---
348
+
349
+ ## Component ids
350
+
351
+ Every component can take `id=` (int). If you don't set one, payloads from UI / View / Modal get sequential ids filled in automatically.
352
+
353
+ ```python
354
+ TextDisplay("hi", id=10)
355
+ Button("x", custom_id="x", id=11)
356
+ ```
357
+
358
+ ```python
359
+ from py_components import assign_ids, find_by_id, replace_by_id
360
+
361
+ payload = ui.to_payload()
362
+ node = ui.get(10)
363
+ payload = ui.replace(10, TextDisplay("updated", id=10))
364
+
365
+ # or on raw dict trees
366
+ assign_ids(payload["components"], start=1)
367
+ found = find_by_id(payload["components"], 10)
368
+ replace_by_id(payload["components"], 10, TextDisplay("x", id=10).to_dict())
369
+ ```
370
+
371
+ Use this when editing a message and only changing one piece.
372
+
373
+ ---
374
+
375
+ ## Modals
376
+
377
+ ```python
378
+ from py_components import (
379
+ Modal, Label, TextInput, TextInputStyle,
380
+ RadioGroup, RadioGroupOption, CheckboxGroup, Checkbox,
381
+ FileUpload, StringSelect, SelectOption,
382
+ )
383
+
384
+ modal = Modal(
385
+ "report_modal",
386
+ "bug report",
387
+ Label(
388
+ "what happened",
389
+ TextInput("desc", style=TextInputStyle.PARAGRAPH, max_length=1000),
390
+ description="as much detail as you can",
391
+ ),
392
+ Label(
393
+ "severity",
394
+ RadioGroup("sev", [
395
+ RadioGroupOption("low", "low"),
396
+ RadioGroupOption("high", "high", default=True),
397
+ ]),
398
+ ),
399
+ Label(
400
+ "areas",
401
+ CheckboxGroup("areas", [
402
+ GroupOption("ui", "ui"),
403
+ GroupOption("api", "api"),
404
+ ], min_values=1, max_values=2),
405
+ ),
406
+ Label("i can reproduce this", Checkbox("repro")),
407
+ Label(
408
+ "screenshots",
409
+ FileUpload("shots", min_values=0, max_values=3, required=False, file_types=["image"]),
410
+ ),
411
+ )
412
+
413
+ @bot.tree.command(name="report")
414
+ async def report(interaction):
415
+ await modal.open(interaction)
416
+ ```
417
+
418
+ Max **5** top-level components. Top-level should be `Label` (or `TextDisplay`). Interactive controls go **inside** Label.
419
+
420
+ ### Modal
421
+
422
+ ```python
423
+ Modal(custom_id, title, *components)
424
+ modal.add(*components)
425
+ modal.to_dict() # data object
426
+ modal.to_response() # {"type": 9, "data": ...}
427
+ await modal.open(interaction)
428
+ ```
429
+
430
+ Title max 45 chars. custom_id 1–100.
431
+
432
+ ### Label
433
+
434
+ ```python
435
+ Label("visible label", child_component, description="optional helper text")
436
+ ```
437
+
438
+ Label text max 45. Description max 100. One child.
439
+
440
+ ### TextInput
441
+
442
+ ```python
443
+ TextInput(
444
+ "field_id",
445
+ style=TextInputStyle.SHORT, # or PARAGRAPH (2)
446
+ min_length=0,
447
+ max_length=4000,
448
+ required=True,
449
+ value="prefill",
450
+ placeholder="type here",
451
+ )
452
+ ```
453
+
454
+ ### FileUpload
455
+
456
+ ```python
457
+ FileUpload(
458
+ "files",
459
+ min_values=0,
460
+ max_values=10,
461
+ required=True,
462
+ file_types=["image", "video", "audio", ".pdf"], # optional filter
463
+ )
464
+ ```
465
+
466
+ ### RadioGroup / CheckboxGroup / Checkbox
467
+
468
+ ```python
469
+ # one choice, 2–10 options
470
+ RadioGroup("sev", [RadioGroupOption("low", "low"), RadioGroupOption("high", "high")], required=True)
471
+
472
+ # multi, 1–10 options
473
+ CheckboxGroup(
474
+ "tags",
475
+ [GroupOption("a", "a"), GroupOption("b", "b")],
476
+ min_values=1,
477
+ max_values=2,
478
+ required=True,
479
+ )
480
+
481
+ # single yes/no — label comes from the Label wrapper
482
+ Checkbox("agree", default=False)
483
+ ```
484
+
485
+ `GroupOption` / `RadioGroupOption` / `CheckboxGroupOption` are the same thing:
486
+
487
+ ```python
488
+ GroupOption("label", "value", description="...", default=False)
489
+ ```
490
+
491
+ Selects also work inside Label in modals (StringSelect, UserSelect, etc.).
492
+
493
+ On submit you get a `MODAL_SUBMIT` interaction. Values are under `interaction.data["components"]` (each Label wraps its filled child).
494
+
495
+ ---
496
+
497
+ ## View + callbacks
498
+
499
+ ```python
500
+ from py_components import View, ButtonStyle
501
+
502
+ view = View()
503
+
504
+ async def on_yes(interaction):
505
+ await interaction.response.send_message("ok", ephemeral=True)
506
+
507
+ view.add_button("yes", "yes", style=ButtonStyle.SUCCESS, callback=on_yes)
508
+ view.add_button("no", "no", style=ButtonStyle.SECONDARY)
509
+
510
+ payload = view.to_payload()
511
+ # send payload yourself or merge into a UI
512
+ ```
513
+
514
+ ```python
515
+ view.add_component(some_component, callback=coro) # registers by custom_id
516
+ await view.dispatch(custom_id, interaction)
517
+ ```
518
+
519
+ ### @button decorator
520
+
521
+ ```python
522
+ from py_components import button
523
+
524
+ @button("click me", custom_id="my_btn", style=ButtonStyle.PRIMARY)
525
+ async def on_click(interaction):
526
+ await interaction.response.send_message("clicked")
527
+
528
+ # sets on_click._button_label / _button_custom_id / _button_style
529
+ ```
530
+
531
+ ### Registry
532
+
533
+ Global custom_id → callback map.
534
+
535
+ ```python
536
+ from py_components import Registry, default_registry
537
+
538
+ @default_registry.route("yes")
539
+ async def handle_yes(interaction):
540
+ await interaction.response.send_message("ok")
541
+
542
+ # or
543
+ default_registry.register("no", handle_no)
544
+
545
+ await default_registry.dispatch(interaction.data["custom_id"], interaction)
546
+ default_registry.clear()
547
+ ```
548
+
549
+ You can make your own `Registry()` instance too.
550
+
551
+ Wire it up in your bot:
552
+
553
+ ```python
554
+ @bot.event
555
+ async def on_interaction(interaction):
556
+ if interaction.type.name == "component":
557
+ cid = interaction.data.get("custom_id")
558
+ await default_registry.dispatch(cid, interaction)
559
+ ```
560
+
561
+ ---
562
+
563
+ ## Low-level HTTP
564
+
565
+ Used internally. Available if you're not on discord.py send paths.
566
+
567
+ ```python
568
+ from py_components import send_message, edit_message, respond_interaction, open_modal
569
+
570
+ await send_message(channel_id, bot_token, payload, files=None)
571
+ await edit_message(channel_id, message_id, bot_token, payload)
572
+ await respond_interaction(application_id, interaction_token, bot_token, payload, type=4)
573
+ await open_modal(interaction_id, interaction_token, modal_data)
574
+ ```
575
+
576
+ `files` for `send_message` is a list of dicts: `{"fp": fileobj, "filename": "x.png", "content_type": "..."}`.
577
+
578
+ API base: `https://discord.com/api/v10`.
579
+
580
+ ---
581
+
582
+ ## Enums / constants
583
+
584
+ ```python
585
+ from py_components import (
586
+ ComponentType, # ACTION_ROW=1, BUTTON=2, ... CHECKBOX=23
587
+ ButtonStyle, # PRIMARY..PREMIUM
588
+ TextInputStyle, # SHORT=1, PARAGRAPH=2
589
+ SeparatorSpacing, # SMALL=1, LARGE=2
590
+ IS_COMPONENTS_V2, # 1 << 15
591
+ )
592
+ ```
593
+
594
+ ---
595
+
596
+ ## Full message example
597
+
598
+ ```python
599
+ from py_components import (
600
+ UI, Container, TextDisplay, Section, Thumbnail, Separator,
601
+ SeparatorSpacing, MediaGallery, ActionRow, Button, ButtonStyle,
602
+ StringSelect, SelectOption, File,
603
+ )
604
+
605
+ ui = UI(
606
+ Container(
607
+ TextDisplay("# server status"),
608
+ Separator(spacing=SeparatorSpacing.SMALL),
609
+ Section(
610
+ TextDisplay("**online** — 1204 members"),
611
+ TextDisplay("peak today: 312"),
612
+ accessory=Thumbnail("https://cdn.discordapp.com/icons/ID/icon.png"),
613
+ ),
614
+ Separator(divider=True),
615
+ MediaGallery(
616
+ "https://example.com/chart1.png",
617
+ "https://example.com/chart2.png",
618
+ ),
619
+ accent_color=0x5865F2,
620
+ ),
621
+ ActionRow(
622
+ StringSelect(
623
+ "range",
624
+ options=[
625
+ SelectOption("24h", "24h"),
626
+ SelectOption("7d", "7d"),
627
+ SelectOption("30d", "30d"),
628
+ ],
629
+ placeholder="range",
630
+ )
631
+ ),
632
+ Button("refresh", custom_id="refresh", style=ButtonStyle.PRIMARY, emoji="🔄"),
633
+ Button("docs", style=ButtonStyle.LINK, url="https://example.com/docs"),
634
+ File("report.pdf"),
635
+ )
636
+
637
+ await ui.send(ctx)
638
+ ```
639
+
640
+ ---
641
+
642
+ ## Full modal example
643
+
644
+ ```python
645
+ from py_components import (
646
+ Modal, Label, TextInput, TextInputStyle,
647
+ RadioGroup, RadioGroupOption, CheckboxGroup, GroupOption,
648
+ Checkbox, FileUpload, UserSelect,
649
+ )
650
+
651
+ modal = Modal(
652
+ "apply",
653
+ "staff application",
654
+ Label("why us", TextInput("why", style=TextInputStyle.PARAGRAPH, min_length=50)),
655
+ Label(
656
+ "experience",
657
+ RadioGroup("exp", [
658
+ RadioGroupOption("none", "none"),
659
+ RadioGroupOption("some", "some"),
660
+ RadioGroupOption("lots", "lots"),
661
+ ]),
662
+ ),
663
+ Label(
664
+ "teams",
665
+ CheckboxGroup("teams", [
666
+ GroupOption("mod", "mod"),
667
+ GroupOption("events", "events"),
668
+ GroupOption("tech", "tech"),
669
+ ], min_values=1, max_values=3),
670
+ ),
671
+ Label("referral", UserSelect("ref", min_values=0, max_values=1, required=False)),
672
+ Label("i read the rules", Checkbox("rules")),
673
+ )
674
+
675
+ await modal.open(interaction)
676
+ ```
677
+
678
+ ---
679
+
680
+ ## Limits (discord)
681
+
682
+ - V2 flag is permanent on that message
683
+ - no content / embeds / stickers / poll with V2
684
+ - attachments need a File or media component
685
+ - 40 components max per message
686
+ - buttons: ActionRow or Section accessory
687
+ - one select per ActionRow
688
+ - modals: max 5 top-level components
689
+ - Label label ≤45, description ≤100
690
+ - Button label ≤80, custom_id ≤100
691
+
692
+ ## License
693
+
694
+ MIT
@@ -9,6 +9,8 @@ py_components/__init__.py
9
9
  py_components/components.py
10
10
  py_components/enums.py
11
11
  py_components/http.py
12
+ py_components/ids.py
13
+ py_components/modal.py
12
14
  py_components/registry.py
13
15
  py_components/ui.py
14
16
  py_components/view.py