nicegui-shadcn 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. nicegui_shadcn-0.1.0/AGENT.md +325 -0
  2. nicegui_shadcn-0.1.0/LICENSE +21 -0
  3. nicegui_shadcn-0.1.0/PKG-INFO +481 -0
  4. nicegui_shadcn-0.1.0/README.md +458 -0
  5. nicegui_shadcn-0.1.0/examples/demo.py +229 -0
  6. nicegui_shadcn-0.1.0/frontend/tailwind.css +295 -0
  7. nicegui_shadcn-0.1.0/frontend/vendor/reka-entry.js +135 -0
  8. nicegui_shadcn-0.1.0/nicegui_shadcn/__init__.py +46 -0
  9. nicegui_shadcn-0.1.0/nicegui_shadcn/_tw_merge.py +274 -0
  10. nicegui_shadcn-0.1.0/nicegui_shadcn/elements/__init__.py +44 -0
  11. nicegui_shadcn-0.1.0/nicegui_shadcn/elements/base.py +135 -0
  12. nicegui_shadcn-0.1.0/nicegui_shadcn/elements/icon.py +35 -0
  13. nicegui_shadcn-0.1.0/nicegui_shadcn/elements/shadcn_accordion.py +133 -0
  14. nicegui_shadcn-0.1.0/nicegui_shadcn/elements/shadcn_accordion.vue +27 -0
  15. nicegui_shadcn-0.1.0/nicegui_shadcn/elements/shadcn_accordion_content.vue +17 -0
  16. nicegui_shadcn-0.1.0/nicegui_shadcn/elements/shadcn_accordion_item.vue +18 -0
  17. nicegui_shadcn-0.1.0/nicegui_shadcn/elements/shadcn_accordion_trigger.vue +33 -0
  18. nicegui_shadcn-0.1.0/nicegui_shadcn/elements/shadcn_button.py +162 -0
  19. nicegui_shadcn-0.1.0/nicegui_shadcn/elements/shadcn_checkbox.vue +36 -0
  20. nicegui_shadcn-0.1.0/nicegui_shadcn/elements/shadcn_controls.py +185 -0
  21. nicegui_shadcn-0.1.0/nicegui_shadcn/elements/shadcn_dialog.vue +21 -0
  22. nicegui_shadcn-0.1.0/nicegui_shadcn/elements/shadcn_dialog_content.vue +73 -0
  23. nicegui_shadcn-0.1.0/nicegui_shadcn/elements/shadcn_dialog_trigger.vue +17 -0
  24. nicegui_shadcn-0.1.0/nicegui_shadcn/elements/shadcn_display.py +377 -0
  25. nicegui_shadcn-0.1.0/nicegui_shadcn/elements/shadcn_dropdown_menu.vue +74 -0
  26. nicegui_shadcn-0.1.0/nicegui_shadcn/elements/shadcn_form.py +229 -0
  27. nicegui_shadcn-0.1.0/nicegui_shadcn/elements/shadcn_input.vue +28 -0
  28. nicegui_shadcn-0.1.0/nicegui_shadcn/elements/shadcn_layout.py +153 -0
  29. nicegui_shadcn-0.1.0/nicegui_shadcn/elements/shadcn_overlay.py +290 -0
  30. nicegui_shadcn-0.1.0/nicegui_shadcn/elements/shadcn_popover.vue +20 -0
  31. nicegui_shadcn-0.1.0/nicegui_shadcn/elements/shadcn_popover_content.vue +28 -0
  32. nicegui_shadcn-0.1.0/nicegui_shadcn/elements/shadcn_popover_trigger.vue +17 -0
  33. nicegui_shadcn-0.1.0/nicegui_shadcn/elements/shadcn_radio_group.vue +53 -0
  34. nicegui_shadcn-0.1.0/nicegui_shadcn/elements/shadcn_select.py +63 -0
  35. nicegui_shadcn-0.1.0/nicegui_shadcn/elements/shadcn_select.vue +108 -0
  36. nicegui_shadcn-0.1.0/nicegui_shadcn/elements/shadcn_slider.vue +34 -0
  37. nicegui_shadcn-0.1.0/nicegui_shadcn/elements/shadcn_switch.vue +27 -0
  38. nicegui_shadcn-0.1.0/nicegui_shadcn/elements/shadcn_tabs.py +136 -0
  39. nicegui_shadcn-0.1.0/nicegui_shadcn/elements/shadcn_tabs.vue +23 -0
  40. nicegui_shadcn-0.1.0/nicegui_shadcn/elements/shadcn_tabs_content.vue +17 -0
  41. nicegui_shadcn-0.1.0/nicegui_shadcn/elements/shadcn_tabs_list.vue +25 -0
  42. nicegui_shadcn-0.1.0/nicegui_shadcn/elements/shadcn_textarea.vue +26 -0
  43. nicegui_shadcn-0.1.0/nicegui_shadcn/elements/shadcn_toggle.vue +23 -0
  44. nicegui_shadcn-0.1.0/nicegui_shadcn/elements/shadcn_toggle_group.vue +34 -0
  45. nicegui_shadcn-0.1.0/nicegui_shadcn/elements/shadcn_tooltip.vue +35 -0
  46. nicegui_shadcn-0.1.0/nicegui_shadcn/icons.py +108 -0
  47. nicegui_shadcn-0.1.0/nicegui_shadcn/shadcn.py +16 -0
  48. nicegui_shadcn-0.1.0/nicegui_shadcn/static/shadcn.css +6425 -0
  49. nicegui_shadcn-0.1.0/nicegui_shadcn/static/vendor/reka-ui.js +15 -0
  50. nicegui_shadcn-0.1.0/nicegui_shadcn/theme.py +84 -0
  51. nicegui_shadcn-0.1.0/package-lock.json +2179 -0
  52. nicegui_shadcn-0.1.0/package.json +32 -0
  53. nicegui_shadcn-0.1.0/pyproject.toml +68 -0
  54. nicegui_shadcn-0.1.0/tests/audit_classes.py +123 -0
  55. nicegui_shadcn-0.1.0/tests/test_render.py +104 -0
  56. nicegui_shadcn-0.1.0/tests/test_tw_merge.py +105 -0
  57. nicegui_shadcn-0.1.0/tests/visual_check.mjs +322 -0
@@ -0,0 +1,325 @@
1
+ # AGENT.md
2
+
3
+ Notes for anyone — human or agent — changing this repository.
4
+
5
+ `nicegui-shadcn` is a [NiceGUI](https://nicegui.io) extension that provides shadcn/ui
6
+ components. It is unusual in one respect: it has **no build step for the user**, which means
7
+ the Tailwind stylesheet and the Vue component bundle are compiled here and committed. Most
8
+ of the rules below exist to keep that illusion intact.
9
+
10
+ Read `README.md` for what the library does. This file is about how to change it without
11
+ breaking it.
12
+
13
+ ---
14
+
15
+ ## 1. Layout: build inputs vs runtime assets
16
+
17
+ The split is deliberate and load-bearing. `frontend/` is **not** part of the importable
18
+ package, so importing `nicegui_shadcn` never reads a `.css` or a `.js` source file.
19
+
20
+ | Path | Role | Ships in |
21
+ | --- | --- | --- |
22
+ | `nicegui_shadcn/*.py` | Runtime: `theme.py`, `shadcn.py`, `icons.py`, `_tw_merge.py` | wheel + sdist |
23
+ | `nicegui_shadcn/elements/*.py` | Runtime: one module per component group | wheel + sdist |
24
+ | `nicegui_shadcn/elements/*.vue` | Runtime: templates, parsed by NiceGUI's own `VBuild` | wheel + sdist |
25
+ | `nicegui_shadcn/static/shadcn.css` | Runtime: compiled stylesheet (~191 kB) | wheel + sdist |
26
+ | `nicegui_shadcn/static/vendor/reka-ui.js` | Runtime: tree-shaken reka-ui bundle (~283 kB) | wheel + sdist |
27
+ | `frontend/tailwind.css` | **Build input**: Tailwind v4 source, design tokens, `@source` globs | sdist |
28
+ | `frontend/vendor/reka-entry.js` | **Build input**: esbuild entry for the reka-ui bundle | sdist |
29
+ | `examples/demo.py` | **Build input + dev aid**: the demo the browser test drives | sdist |
30
+ | `tests/` | Dev | sdist |
31
+ | `package.json`, `package-lock.json` | Dev: pins the Tailwind CLI and esbuild | sdist |
32
+ | `node_modules/` | Installed, never shipped | — |
33
+ | `AGENT.md` | This file | sdist |
34
+
35
+ If you add a runtime file, check `[tool.poetry].include` in `pyproject.toml`.
36
+
37
+ ---
38
+
39
+ ## 2. The edit → build → verify loop
40
+
41
+ ```bash
42
+ npm install # once
43
+ npm run build # = build:css + build:vendor, ~250 ms total
44
+ ```
45
+
46
+ **Rebuild the CSS after touching any `.py` or `.vue` under `nicegui_shadcn/`.** Tailwind
47
+ only emits utilities it can see, so a new class in a Python constant or a `.vue` template
48
+ does not exist until you rebuild. Forgetting this is the single most common way to ship a
49
+ component that renders unstyled.
50
+
51
+ Then verify, in this order — each layer catches what the one before it cannot:
52
+
53
+ ```bash
54
+ python tests/test_tw_merge.py # 37 cases: cn() conflict resolution, no NiceGUI needed
55
+ python tests/test_render.py # 30 checks: every component renders server-side, no browser
56
+ python tests/audit_classes.py # 223 tokens: every class used in Python exists in the CSS
57
+ python examples/demo.py # start the demo (port 8123), then:
58
+ node tests/visual_check.mjs http://127.0.0.1:8123/ # 34 checks in headless Edge
59
+ ```
60
+
61
+ - `tests/test_render.py` derives one `<stem> registered` check per `*.vue` file, so a new
62
+ component is covered as soon as the file exists.
63
+ - `tests/audit_classes.py` is the only guard against Tailwind's **silent** failure mode: an
64
+ unresolvable candidate is dropped without a warning, and you only see the damage as an
65
+ unstyled element in a browser. Add a component, rebuild, and run it.
66
+ - `tests/visual_check.mjs` asserts computed styles, so it is the only layer that can tell
67
+ whether a design *token* — not merely a class name — reached the element. It writes
68
+ `_shot-light.png` / `_shot-dark.png`; set `SHADCN_SHOT_DIR` to redirect them.
69
+
70
+ `examples/demo.py` runs with `reload=False`, so **restart it after every code change**
71
+ otherwise the browser test checks the previous build.
72
+
73
+ The bundled browser tools are not always available in this environment; the Playwright +
74
+ headless Edge path in `tests/visual_check.mjs` is the supported way to look at a page.
75
+ It already knows the Edge/Chrome paths to try.
76
+
77
+ ---
78
+
79
+ ## 3. NiceGUI extension contract
80
+
81
+ Everything here was read out of NiceGUI 3.x and is easy to get wrong. Paths are under
82
+ `C:\Python\Py313\Lib\site-packages\nicegui` in this environment.
83
+
84
+ ### Registering a component
85
+
86
+ `Element.__init_subclass__(cls, *, component, dependencies, esm, default_classes,
87
+ default_style, default_props)` (`element.py:92-131`). `component` and `dependencies` are
88
+ globs resolved **relative to the file that defines the subclass**, which is why every
89
+ component module carries its own `.vue` next to it.
90
+
91
+ ### What `VBuild` will and will not accept
92
+
93
+ `vbuild.py` is a ~100-line `HTMLParser`, **not** a Vue SFC compiler. Its limits shape every
94
+ `.vue` file in this repo:
95
+
96
+ - Exactly **one top-level tag** inside `<template>`, or it raises
97
+ `ValueError('File has more than one top level tag')`.
98
+ - **`<script setup>` does not work.** Only an Options API `export default {...}` is read.
99
+ - **Nested `<template>` truncates the component.** `handle_endtag` closes the captured
100
+ template at the *first* `</template>` it sees, without checking nesting level, so
101
+ `<template v-for>` / `<template #slot>` silently cuts the rest of the markup off.
102
+ Write `v-for` on a real element, or use `<component :is>`.
103
+ - Void tags (`input`, `img`, `br`, `hr`, …) are not level-counted, so an `<input>` root is
104
+ legal.
105
+ - The `<script>` block is served verbatim as an ES module and **must have a default
106
+ export**; `import` statements work.
107
+ - Scoped `<style>` selectors are rewritten to `*[data-<name>]`.
108
+
109
+ ### Naming rules
110
+
111
+ - The file **stem** is used as a raw JavaScript identifier (`import { default as NAME } from
112
+ …`), so **no hyphens** — hence `shadcn_tabs_list.vue`, never `shadcn-tabs-list.vue`.
113
+ - Stems must be **globally unique** across both `.vue` and `.js` components
114
+ (`Component._names` is a shared `ClassVar` with an `assert`).
115
+ - All components are prefixed `shadcn_` for that reason.
116
+
117
+ ### The `data-<name>` marker
118
+
119
+ `VBuild` puts `data-<stem>` on the **first tag of the template**. If the root is a fragment
120
+ (a reka `*Root`, or a `*Portal`), the attribute is dropped and your mount assertions fail.
121
+ Two consequences:
122
+
123
+ - Put an explicit marker on the meaningful element:
124
+ `data-shadcn_select`, `data-shadcn_dialog_content`, `data-shadcn_popover_content`,
125
+ `data-shadcn_dropdown_menu`.
126
+ - A fragment root needs `inheritAttrs: false` in `export default`, otherwise Vue warns about
127
+ extraneous non-prop attributes. Use `v-bind="$attrs"` on the element that should receive
128
+ the caller's classes.
129
+
130
+ ### Props and events
131
+
132
+ - `element._props` entries become **real Vue props** on the resolved component
133
+ (`static/nicegui.js:230-325`), camelized by Vue: Python `model-value` → `modelValue`.
134
+ - Python event names go through `event_type_to_camel_case`, and the client only
135
+ capitalises the first letter, so `self.on('update:model-value', …)` becomes
136
+ `onUpdate:modelValue` — exactly what `emit('update:modelValue', v)` looks up.
137
+ - A component that emits a **single** value arrives server-side as that value, not a list
138
+ (`client.py:347-349` unwraps `len(args) == 1`).
139
+ - `ValueElement` uses `VALUE_PROP = 'model-value'`; set `LOOPBACK = False` when the
140
+ component writes the value back itself (see `shadcn_form.py`).
141
+ - **Declare every prop you pass.** An undeclared prop falls through to the DOM; NiceGUI's
142
+ own `loopback` prop once leaked onto the select trigger as `<button loopback="true">`
143
+ until `shadcn_select.vue` declared it.
144
+
145
+ ### Text and children
146
+
147
+ `renderRecursively` unshifts `element.text` **before** the children of the default slot.
148
+ So an element's own `_text` always renders first — a Button cannot use `_text` for its
149
+ label if it wants an icon on the left. Build the label as a child element instead.
150
+
151
+ Use `elements/base.py:Text` (a `<span>`) rather than `ui.label` (a `<div>`) for anything
152
+ inside a `<button>`; a `<div>` in a button is invalid HTML.
153
+
154
+ ### Page-level registration
155
+
156
+ `theme.py` does three things at import time, all of which must happen before the first
157
+ component renders:
158
+
159
+ 1. `app.add_static_files('/_nicegui_shadcn', …)`;
160
+ 2. `register_importmap_override('reka-ui', …)` so `.vue` scripts can
161
+ `import { … } from 'reka-ui'`;
162
+ 3. `ui.add_head_html('<link rel="stylesheet" …>', shared=True)`.
163
+
164
+ Use a `<link>`, **not** `ui.add_css`. `add_css` inlines the stylesheet into an
165
+ `addStyle(...)` JavaScript call, so it only lands after the socket handshake — a visible
166
+ flash of unstyled content on every reload. `shared=True` is what makes the call legal
167
+ outside a client context (import time).
168
+
169
+ Keep `vue` **external** in the reka-ui bundle (`esbuild --external:vue`). NiceGUI already
170
+ puts its own Vue 3.5 on the import map; a second copy breaks `provide`/`inject` and
171
+ `Teleport` across components.
172
+
173
+ ---
174
+
175
+ ## 4. Tailwind and the cascade
176
+
177
+ NiceGUI declares the layer order once, in `templates/index.html:13`:
178
+
179
+ ```
180
+ @layer theme, base, quasar, nicegui, components, utilities, overrides, quasar_importants;
181
+ ```
182
+
183
+ Everything we emit joins those layers.
184
+
185
+ - **Import `utilities` with `layer(utilities) important`.** `quasar.important.css` lands in
186
+ the *last* layer and contains `.bg-primary { background: var(--q-primary) !important }`.
187
+ Layer precedence is reversed for important declarations, so making ours important too is
188
+ the only way `shadcn.button()` comes out shadcn-black instead of Quasar-blue.
189
+ - **Do not import Tailwind's preflight.** Quasar already normalises; a second reset fights
190
+ it. The one preflight rule we need — `[hidden] { display: none !important }`, used by
191
+ mounted-but-closed panels — is added by hand in `@layer base`.
192
+ - Bind the dark variant to the class NiceGUI already toggles:
193
+ `@custom-variant dark (&:where(body.body--dark, body.body--dark *));`
194
+ - Expose design tokens through `@theme inline { --color-primary: var(--primary); … }` so
195
+ `bg-primary` / `text-muted-foreground` resolve to the shadcn variables.
196
+ - The build uses `source(none)` plus explicit `@source` globs, so the compiled CSS is a
197
+ pure function of the checkout's `.py`/`.vue` files and does not drift with where it was
198
+ built.
199
+ - Runtime `classes=` strings are covered by `@source inline(...)` blocks. That vocabulary is
200
+ a **deliberate subset**: semantic colours plus common layout/spacing/typography. Raw
201
+ palette colours (`bg-blue-600`) are not generated. Extending it means adding to the
202
+ matrix and rebuilding; the README documents the user-facing workflow.
203
+
204
+ ### Where class strings live
205
+
206
+ **All CSS classes live in Python**, in module-level constants or `default_classes`. The
207
+ `.vue` templates contain no `class="…"` of their own except `:class` bindings fed from a
208
+ prop. That is what makes `classes=` mergeable and auditable.
209
+
210
+ `tests/audit_classes.py` encodes the conventions it depends on:
211
+
212
+ - constants must be named `_?[A-Z][A-Z0-9_]*_(BASE|CLASSES|VARIANTS|SIZES)`;
213
+ - it parses Python with `ast`, not regex, and reads `default_classes=`, `classes=` and
214
+ `.classes(...)` keyword arguments;
215
+ - `CLASS_ATTR_RE` uses a lookbehind so `:class` / `v-bind:class` (JavaScript expressions) are
216
+ ignored.
217
+
218
+ If you rename a constant or move a class into a template, the audit will either go quiet or
219
+ go red — check it.
220
+
221
+ ---
222
+
223
+ ## 5. reka-ui
224
+
225
+ The bundle is built from `frontend/vendor/reka-entry.js`, which re-exports **only** the
226
+ primitives we wrap so esbuild can tree-shake the rest. Rebuild it after changing that file:
227
+
228
+ ```bash
229
+ npm run build:vendor
230
+ ```
231
+
232
+ Two things worth knowing:
233
+
234
+ - **`provide`/`inject` does not survive a NiceGUI slot for `TabsList` → `TabsTrigger`.**
235
+ reka's `TabsTrigger` uses `RovingFocusItem` unconditionally and throws
236
+ `` Injection `Symbol(RovingFocusGroupContext)` not found ``. Every *other* cross-boundary
237
+ inject works (dialog, popover, dropdown, tooltip, select, accordion). The fix was to make
238
+ the tab bar data-driven: `shadcn_tabs_list.vue` takes a `tabs` array prop and generates
239
+ the triggers itself, which keeps reka's real roving-focus/arrow-key behaviour inside one
240
+ component tree. Anything else that needs a context provided by a sibling should follow
241
+ the same pattern.
242
+ - **Force-mounted content is not hidden by reka.** It only sets `data-state`, never
243
+ `hidden`. The children of `TabsContent`/`AccordionContent` stay mounted on purpose (so a
244
+ server-side update always finds its element), and hiding is done with
245
+ `data-[state=inactive]:hidden` / `data-[state=closed]:hidden`. The cost is that the
246
+ accordion has no *closing* animation.
247
+ - `PinInputSeparator` does not exist in reka-ui 2.10.5. Check the export list before adding
248
+ an import; a bad import fails the esbuild step with
249
+ `No matching export in "node_modules/reka-ui/dist/index.js"`.
250
+
251
+ ---
252
+
253
+ ## 6. Python API conventions
254
+
255
+ - **`cn()` semantics are mandatory for components that take `classes=`.** NiceGUI's
256
+ `.classes()` *appends*, so `shadcn.button('Save', classes='bg-destructive')` would emit
257
+ both `bg-primary` and `bg-destructive` and let CSS source order decide. `ShadcnElement`
258
+ merges through `_tw_merge.tw_merge` so the caller wins.
259
+ - In `_tw_merge`, always slice with `b[len(prefix):]`. Two bugs came from hand-counted
260
+ indices (`b[6:]` for `border-`, `b[7:]` for `rounded-`), and both made unrelated utilities
261
+ share a conflict group.
262
+ - Validate enum-ish arguments with `option(kind, value, options)` so the error names the
263
+ valid choices.
264
+ - Expose a lowercase factory (`shadcn.button(...)`) and keep the class in the same module.
265
+ `elements/__init__.py` builds `__all__` from each module's `__all__` **before** the star
266
+ imports, because `from .icon import *` rebinds the name `icon` from the module to the
267
+ factory.
268
+ - Icons are inlined in `icons.py` (39 glyphs), not bundled from `lucide-vue-next`; a new
269
+ glyph is one entry in `_ICONS`.
270
+
271
+ ---
272
+
273
+ ## 7. Packaging
274
+
275
+ Poetry, PEP 621 metadata, `poetry-core` backend.
276
+
277
+ - `requires-python` **must** carry an upper bound (`>=3.10,<4.0`). A bare `>=3.10` makes
278
+ `poetry lock` fail with *"a possible solution would be to set the `python` property to
279
+ `>=3.10,<4`"*.
280
+ - `license = "MIT"` + `license-files = ["LICENSE"]` (PEP 639). Do not add a
281
+ `License ::` classifier; it conflicts.
282
+ - Poetry honours `.gitignore` when building the sdist. Anything you add there also
283
+ disappears from the source distribution — that is how `node_modules/` and `dist/` stay
284
+ out.
285
+ - **Poetry 2.4.1 defines no `testpypi` repository.** `poetry publish -r testpypi` fails with
286
+ `Repository testpypi is not defined` until you run
287
+ `poetry config repositories.testpypi https://test.pypi.org/legacy/`.
288
+ - `poetry publish` uploads whatever is in `dist/`; it does not rebuild unless you pass
289
+ `--build`. Use `poetry publish --dry-run` to check repository resolution and the artifact
290
+ list without credentials.
291
+ - After changing the runtime assets, rebuild the wheel and **verify it in a fresh venv**.
292
+ Run the check with a working directory outside the repository, or `C:\dsh` shadows
293
+ site-packages on `sys.path` and you will happily test the checkout instead of the wheel.
294
+
295
+ ---
296
+
297
+ ## 8. Pitfalls already paid for
298
+
299
+ A short list of mistakes that cost real time here. Most were invisible until a browser
300
+ looked at the result.
301
+
302
+ 1. **Tailwind drops unknown classes silently.** A typo produces no warning and no rule.
303
+ `tests/audit_classes.py` exists for exactly this.
304
+ 2. **`getComputedStyle` reports a blockified `display`.** A button that is a flex item
305
+ reports `flex` even though its class list says `inline-flex`, and removing the class
306
+ gives `block`. Assert `flex` *or* `inline-flex`; the class list is the source of truth.
307
+ 3. **A page-level overflow check misses per-element overflow.** A `size="icon"` button with
308
+ a text label overflowed by 22 px while `document.scrollWidth` was clean; the check had to
309
+ compare each element's `scrollWidth` against its `clientWidth`. The Button now degrades
310
+ to `sr-only` + `aria-label`.
311
+ 4. **Assign text to the element that should show it.** `Avatar.__init__` once did
312
+ `self._text = fallback`, which put "CN" in the outer container next to the circle instead
313
+ of inside it. For a child element, assign to the child.
314
+ 5. **Compare tokens, not colours.** Assertions convert through a probe element
315
+ (`background-color: var(--primary)`) and compare computed strings; Chromium echoes
316
+ `oklch()` back unchanged, so the test proves *which token* was used rather than that some
317
+ dark colour appeared.
318
+ 6. **`Button` has no `.vue`.** It renders a native `<button>` (`tag='button'`). Asserting
319
+ `tpl-shadcn_button` is wrong; the repository has exactly 24 `.vue` files, which is why
320
+ `test_render.py` has 30 checks (6 static + 24).
321
+ 7. **NiceGUI 3.x removed `classes=` / `style=` / `props=` from `Element.__init__`.** Set them
322
+ through `_props` / `.classes()` / `.style()` after construction, as `base.py` does.
323
+ 8. **`add_slot('default', template)` cannot inject markup.** `_collect_slot_dict()` excludes
324
+ the default slot, so a default-slot template is never sent to the client. Build real
325
+ child elements.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 XiangQinxi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.