daisyui 2.1.0 → 2.2.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 41fa98f82c0377c4615fa5e4a1091ae8383688a951acb726122866cefa273665
4
- data.tar.gz: 651713886837f46040942fc397089d1194dc35787ab1ba18308db97f12d94a06
3
+ metadata.gz: b1bf5647c845ed4fbdf7d538e4934c7171b23039cbc9029d8c68330b82f0acd2
4
+ data.tar.gz: 9282c494a1db396b4153fda3c9696040e8c95ac7d495d0bbcf9a0be983a49fb1
5
5
  SHA512:
6
- metadata.gz: be24df2be52d01adbda5f9e257650d788352a0dfd286bee5c734dfce66aee0377dd0ad601269363ffd734d908cd0c2cde612062017dc59fde61086f4f53e716c
7
- data.tar.gz: 2320af028c2539b849de1feb7c1d2690cc6ca0e75da24bd7d838415171c4d922f59b26f5833e1cc1a1e035c330ce532eaae75e21107bd89119f24653f7ab3eb4
6
+ metadata.gz: f2fa7469a01b18c4bb28110ab4da1238ce8c846fef910e3edc488d283ed641e8cee5fb3b2fb69ca528f643283a1339114db14354a0f3d0d306b67f996923e4d8
7
+ data.tar.gz: 282f9add88cc7e82eabd013b81bb4e1470c8809f6687ada460470ef97a472d228182154f644c56a7df000f4714dd8d51eca4bbc5692f5ddbbdba7607e84617f4
data/CHANGELOG.md CHANGED
@@ -1,4 +1,103 @@
1
- ## [2.1.0]
1
+ ## [Unreleased]
2
+
3
+ ### Documentation for the variants API
4
+
5
+ - Docs site: "Variants, states and slots" (when to use `variant`, `flag`,
6
+ `compound`, `state`/`on` and `slot`, the match values, render order and its
7
+ three pitfalls, introspection) and "Tailwind sources for your components"
8
+ (`--app`, the rake task, committing the file, the `stale?` spec, stylelint).
9
+ - MCP server: `get_component` includes the component's `variants.schema`, and a
10
+ new `variants_guide` tool returns a short guide to the DSL with the pitfalls.
11
+ - README: the variants section links the guide and lists the pitfalls.
12
+
13
+ ### Variants, states and slots
14
+
15
+ A `variants` block declares what a component that owns its design needs
16
+ beyond `register_modifiers`, as data the gem owns (#67). See the README's
17
+ "Variants, states and slots".
18
+
19
+ ```ruby
20
+ class Widget < DaisyUI::Button
21
+ variants do
22
+ base "font-medium"
23
+ default :size, :md
24
+ compound style: :outline, color: nil, class: "[--btn-bg:var(--color-base-100)]"
25
+ state :hover, "hover"
26
+ state :open, "[.dropdown:is(:focus-within,[open],:has(:popover-open))>&]"
27
+ on %i[hover open], style: %i[ghost link], class: "[--btn-bg:transparent]"
28
+ end
29
+ end
30
+ ```
31
+
32
+ - `variant` and `flag` are `register_modifiers` (groups, ClassMerge,
33
+ inheritance unchanged); `base` appends to the inherited base; `default`
34
+ applies when the caller names no member of a group.
35
+ - `compound(**match, class:, set:)` adds classes when modifiers hold together
36
+ or one holds and another is absent (`color: nil`, `shape: [nil, :row]`,
37
+ `color: true`, flags `true`/`false`); `set:` selects members for the
38
+ caller's empty groups, in declaration order.
39
+ - `state :name, "variant"` names a Tailwind variant once; `on(states, …)`
40
+ writes a declaration once and renders it under each state.
41
+ - `slot :name, "classes"`, per-slot Hash values, and `slot_classes` /
42
+ `slot_options` for components with parts.
43
+ - Defaults and `set:` members are resolved, not left to ClassMerge: a default
44
+ never renders beside the member the caller gave, with class merging off too.
45
+ - A typo (a group, member, flag, state or slot the component does not have)
46
+ raises `ArgumentError` where it is declared.
47
+ - `Component.variants.schema` returns plain, frozen data: variants, flags,
48
+ defaults, compounds, `on` rules, states and slots.
49
+ - An unknown modifier's error now lists the component's groups:
50
+ `… has no modifier :huge. Variants: size: xs, sm, …`.
51
+ - Registries are frozen by `DaisyUI.finalize!` like the others.
52
+
53
+ ### Tailwind sources for an app's own components
54
+
55
+ State-prefixed classes appear nowhere literally in an app's source, so
56
+ `bin/rails "daisyui:tailwind[app/assets/stylesheets/generated/daisyui_app.css]"`
57
+ (or `daisyui-tailwind --app --require FILE`) writes `@source inline(…)` for
58
+ every `DaisyUI::Base` subclass the app defines: its classes, compounds, slots
59
+ and each `on` class per state. Sorted, versionless output meant to be
60
+ committed; `DaisyUI::Tailwind.stale?(path)` fails a spec when a declaration
61
+ changed and the file was not regenerated. From Ruby:
62
+ `DaisyUI::Tailwind.sources(app: true)`.
63
+
64
+ ## [2.1.1] - 2026-10-17
65
+
66
+ ### Tailwind sources for only the components an app uses
67
+
68
+ `bundle exec daisyui-tailwind app -o app/assets/stylesheets/generated/daisyui.css`
69
+ writes the Tailwind sources for the DaisyUI components an app's Ruby and
70
+ templates reference, plus the components those render (Dropdown renders
71
+ Button and Menu): one `@source inline("…")` per component, for the Tailwind
72
+ entry to `@import`. See the README's "Tailwind sources".
73
+
74
+ - Smaller: pointing `@source` at the whole gem hands Tailwind every class of
75
+ every component and ClassMerge's catalogue of Tailwind utilities. On an app
76
+ using 33 of the 77 components the gem's share of the CSS went from 210 KB to
77
+ 126 KB (32 → 19 KB gzipped).
78
+ - More complete: Tailwind v4 drops Ruby comments when it scans a `.rb` file
79
+ and cannot see a Symbol component class (`self.component_class = :navbar`),
80
+ so a whole-gem scan generated no `.navbar`, `.card` or `.drawer` rule, nor
81
+ the rules daisyUI nests under them (`indicator-item`, `stack-top`), unless
82
+ the app spelled the class out itself. The generated sources carry them.
83
+ - No path in the output, so it is the same on every machine; `--output`
84
+ rewrites the file only when its content changes, so a CSS watcher does not
85
+ loop on it.
86
+ - `--format json` gives a build script the components, their classes and
87
+ their absolute files. From Ruby, `DaisyUI::Tailwind.sources(scan: ["app"])`
88
+ returns the CSS (`format: :json` for the JSON), and
89
+ `DaisyUI::Tailwind.used_components(scan: ["app"])` the component names.
90
+ - `--responsive sm,md,lg` adds breakpoint variants of each component class and
91
+ modifier, for apps that render `responsive:` modifiers (off by default: it
92
+ multiplies the CSS). `--prefix` matches `DaisyUI.configuration.prefix`,
93
+ which `DaisyUI::Tailwind.sources` uses by default.
94
+ - The manifest behind it, `data/daisyui/tailwind_manifest.json`, lists every
95
+ component's class, daisyUI name, file, the components it renders and its
96
+ classes. It is built from the components (`rake tailwind:manifest`) and a
97
+ spec fails when it is stale. It lives in the gem's data dir, not `lib/`:
98
+ an app that keeps scanning the gem's `lib/` builds the same CSS as before.
99
+
100
+ ## [2.1.0] - 2026-10-15
2
101
 
3
102
  ### DaisyUI finalizes itself at boot: class merging in any Ractor (#58)
4
103
 
@@ -35,7 +134,7 @@ then works in any Ractor; each Ractor builds its own merger and LRU.
35
134
  - Rendering components inside a Ractor waits on Phlex
36
135
  ([yippee-fun/phlex#1055](https://github.com/yippee-fun/phlex/pull/1055)).
37
136
 
38
- ## [2.0.2] and earlier
137
+ ## [2.0.2] - 2026-10-11
39
138
 
40
139
  ### Unknown modifiers are reported (#54)
41
140
 
@@ -69,6 +168,30 @@ end
69
168
  (`DaisyUI::Chat#bubble has no modifier :primray`).
70
169
  - Keyword booleans are not validated: unknown keywords stay HTML attributes.
71
170
 
171
+ ### Changed
172
+
173
+ - The gem ships minified Stimulus controllers with linked sourcemaps, and the
174
+ importmap pins them without preloading, so with the lazy registration the
175
+ README shows (`lazyLoadControllersFrom`) a page that never uses one never
176
+ fetches it. `daisy_dropdown_controller` 6.1 KB → 2.8 KB (2.4 → 1.3 KB gzipped),
177
+ `daisy_tooltip_controller` 8.8 KB → 5.2 KB (2.4 → 1.8 KB gzipped). The
178
+ `daisy_ui/controllers/*` specifiers and the bundler import paths are unchanged.
179
+
180
+ ### Fixed
181
+
182
+ - Modifiers added with `config.modifiers.add(component: X)` now apply to
183
+ subclasses of `X` too; the closest class wins.
184
+
185
+ ## [2.0.1] - 2026-10-09
186
+
187
+ ### Fixed
188
+
189
+ - Button renders the `disabled` attribute on `button`/`input` elements when given
190
+ `:disabled` or `disabled: true`, so a disabled submit button no longer submits
191
+ its form from the keyboard (#52).
192
+
193
+ ## [2.0.0] - 2026-09-30
194
+
72
195
  ### Upgrading to 2.0
73
196
 
74
197
  Components now merge their classes: when two classes conflict, the later one
@@ -110,6 +233,35 @@ wins. `Button(:sm, class: "btn-lg")` renders `btn btn-lg` (1.x rendered
110
233
  app components subclassing `DaisyUI::Base` can do the same.
111
234
  `config.modifiers.add` takes `group:` to join one.
112
235
 
236
+ ### Changed
237
+
238
+ - Maintainers cut releases with `bin/release` (`list`, `--dry-run`, `patch`/`minor`/`major`/`X.Y.Z`), which checks the lockfiles for gems that do not accept the new version yet and then drives `rake release`.
239
+
240
+ ## [1.3.1] - 2026-08-31
241
+
242
+ ### Fixed
243
+
244
+ - Popover tooltips no longer clip arrows that extend beyond the tooltip card.
245
+
246
+ ## [1.3.0] - 2026-08-30
247
+
248
+ ### Added
249
+
250
+ - `Tooltip` `:popover` modifier for collision-aware tooltips. It renders real
251
+ tooltip content in the browser top layer, opens on hover or focus, flips and
252
+ shifts within the visual viewport, follows scroll/resize while open, and
253
+ exposes `role="tooltip"`/`aria-describedby` semantics. The bundled
254
+ `daisy-tooltip` Stimulus controller is auto-pinned for importmap-rails apps
255
+ and supports a custom controller identifier via `stimulus:`.
256
+
257
+ ## [1.2.1] - 2026-07-03
258
+
259
+ - No changes to the gem; this release shipped documentation-site and deploy changes only.
260
+
261
+ ## [1.2.0] - 2026-06-26
262
+
263
+ ### Added
264
+
113
265
  - Aura component (DaisyUI 5.6) with style variants (dual, rainbow, holo, gold, silver, glow) and sizes
114
266
  - Megamenu component (DaisyUI 5.6) with wide, full, vertical modifiers, sizes, and active_indicator sub-component
115
267
  - OTP component (DaisyUI 5.6) for one-time password inputs with configurable digit count, joined modifier, sizes, and color variants
@@ -127,34 +279,27 @@ wins. `Button(:sm, class: "btn-lg")` renders `btn btn-lg` (1.x rendered
127
279
  positioning fallback (Safari < 26, Firefox < 147) and optional roving
128
280
  keyboard navigation. The default `:popover` dropdown remains zero-JS, and the
129
281
  gem stays a plain Phlex library when Rails is absent.
130
- - `Tooltip` `:popover` modifier for collision-aware tooltips. It renders real
131
- tooltip content in the browser top layer, opens on hover or focus, flips and
132
- shifts within the visual viewport, follows scroll/resize while open, and
133
- exposes `role="tooltip"`/`aria-describedby` semantics. The bundled
134
- `daisy-tooltip` Stimulus controller is auto-pinned for importmap-rails apps
135
- and supports a custom controller identifier via `stimulus:`.
136
282
 
137
283
  ### Changed
138
284
 
139
- - The gem ships minified Stimulus controllers with linked sourcemaps, and the
140
- importmap pins them without preloading, so with the lazy registration the
141
- README shows (`lazyLoadControllersFrom`) a page that never uses one never
142
- fetches it. `daisy_dropdown_controller` 6.1 KB → 2.8 KB (2.4 → 1.3 KB gzipped),
143
- `daisy_tooltip_controller` 8.8 KB → 5.2 KB (2.4 → 1.8 KB gzipped). The
144
- `daisy_ui/controllers/*` specifiers and the bundler import paths are unchanged.
145
- - Maintainers cut releases with `bin/release` (`list`, `--dry-run`, `patch`/`minor`/`major`/`X.Y.Z`), which checks the lockfiles for gems that do not accept the new version yet and then drives `rake release`.
146
-
147
285
  - Range component: added range-vertical modifier for vertical orientation (DaisyUI 5.6)
148
286
  - Tooltip component: added alignment modifiers (tooltip-start, tooltip-center, tooltip-end) and tooltip-content sub-component (DaisyUI 5.6)
149
287
 
150
- ### Fixed
288
+ ## [1.1.1] - 2026-04-05
151
289
 
152
- - Modifiers added with `config.modifiers.add(component: X)` now apply to
153
- subclasses of `X` too; the closest class wins.
154
- - Popover tooltips no longer clip arrows that extend beyond the tooltip card.
155
- - Button renders the `disabled` attribute on `button`/`input` elements when given
156
- `:disabled` or `disabled: true`, so a disabled submit button no longer submits
157
- its form from the keyboard (#52).
290
+ ### Changed
291
+
292
+ - The gem packages only `exe/`, `lib/`, `CHANGELOG.md`, `LICENSE.txt` and
293
+ `README.md`, listed with `git ls-files` (#14).
294
+
295
+ ## [1.1.0] - 2026-04-05
296
+
297
+ ### Changed
298
+
299
+ - Components aligned with DaisyUI 5 (#13): new `Hover3d` and `TextRotate`
300
+ components; `Link` sets the `link` class; `MenuItem` uses
301
+ `menu-disabled`/`menu-active`/`menu-focus`; Button's `no_animation` and
302
+ `glass`, Mask's `parallelogram` shapes and Menu's color modifiers are removed.
158
303
 
159
304
  ## [0.1.0] - 2024-08-02
160
305
 
data/README.md CHANGED
@@ -92,33 +92,166 @@ Consider not including DaisyUI in ApplicationComponent if:
92
92
 
93
93
  In this scenario, including both DaisyUI and your own component library in `ApplicationComponent` will lead to naming conflicts.
94
94
 
95
- 3. Update your `tailwind.config.js` file to include DaisyUI component styles:
95
+ 3. Tell Tailwind which classes the gem renders: see [Tailwind sources](#tailwind-sources).
96
96
 
97
- ```js
98
- const execSync = require("child_process").execSync;
99
- const outputDaisyUI = execSync("bundle show daisyui", { encoding: "utf-8" });
100
- const daisyUIPath = outputDaisyUI.trim() + "/**/*.rb";
101
- module.exports = {
102
- content: [
103
- // ... other paths
104
- daisyUIPath,
105
- ],
106
- };
97
+ ## Tailwind sources
98
+
99
+ The gem renders daisyUI class names from Ruby, so Tailwind has to be told
100
+ which ones to generate CSS for. Give it the classes of just the components your
101
+ app uses: `daisyui-tailwind` finds the components your Ruby and templates
102
+ reference, adds the components those render (Dropdown renders Button and Menu,
103
+ Menu renders MenuItem), and writes one `@source inline(...)` per component for
104
+ your Tailwind entry to import.
105
+
106
+ ```sh
107
+ bundle exec daisyui-tailwind app -o app/assets/stylesheets/generated/daisyui.css
107
108
  ```
108
109
 
109
- 4. Update your tailwind.config.js file to detect TailwindCSS classes in Ruby files.
110
+ ```css
111
+ /* app/assets/stylesheets/application.tailwind.css */
112
+ @import "tailwindcss";
113
+ @plugin "daisyui";
114
+
115
+ @source "../../views"; /* your own classes, as before */
116
+ @import "./generated/daisyui.css"; /* the DaisyUI components you use */
117
+ ```
118
+
119
+ Run it before Tailwind in your CSS build (it rewrites the file only when its
120
+ content changes, so a watcher does not loop on it). The output names no path,
121
+ so it is the same on every machine: commit it, or generate it on every build.
122
+
123
+ **From a Node build script**, spawn the command, or read JSON:
110
124
 
111
125
  ```js
112
- module.exports = {
113
- content: [
114
- // ... other paths
115
- //
116
- // Note the "rb" extension at the end
117
- "./app/views/**/*.{erb,haml,html,slim,rb}",
118
- ],
119
- };
126
+ // build-css.js (bun)
127
+ Bun.spawnSync(["bundle", "exec", "daisyui-tailwind", "app", "-o", "app/assets/stylesheets/generated/daisyui.css"])
128
+
129
+ // or per bundle, as JSON: { components, classes, files, shared, daisyui }
130
+ const out = Bun.spawnSync(["bundle", "exec", "daisyui-tailwind", "app/views/admin", "--format", "json"])
131
+ const { components, classes, files } = JSON.parse(out.stdout.toString())
132
+ ```
133
+
134
+ **From Ruby** (a rake task, a build script):
135
+
136
+ ```ruby
137
+ DaisyUI::Tailwind.used_components(scan: ["app"]) # => ["Button", "Menu", "MenuItem", ...]
138
+ DaisyUI::Tailwind.sources(scan: ["app"]) # => the CSS partial
139
+ DaisyUI::Tailwind.sources(components: %w[Navbar], format: :json)
140
+ ```
141
+
142
+ | Option | |
143
+ |---|---|
144
+ | `PATH...` | Directories (every `.rb`, `.erb`, `.haml`, `.slim` below) or files to scan. A path that does not exist is an error. |
145
+ | `-c, --component Button,Navbar` | Components to include whether or not a path references them (repeatable). |
146
+ | `-f, --format css\|json` | `css` (default) is the partial to import; `json` is for build scripts. |
147
+ | `-o, --output FILE` | Write to FILE instead of stdout, only when its content changes. |
148
+ | `--responsive sm,md,lg` | Add those breakpoint variants of each component class and modifier, for apps that render `responsive:` modifiers. Off by default: they multiply each component's CSS (sm,md,lg made the sample below 3.7 times larger). |
149
+ | `--prefix d-` | Prefix the classes the way an app that sets `DaisyUI.configuration.prefix` renders them. `DaisyUI::Tailwind.sources` uses the configured prefix by default. |
150
+
151
+ What counts as a use: a kit call with or without parentheses (`Button(:primary)`,
152
+ `Badge :primary do`, `Button variant do`, `Hero do`), an instance (`Card.new`)
153
+ or the constant (`DaisyUI::Navbar`, `class AppNavbar < DaisyUI::Navbar`).
154
+ Comments do not count (`#` lines in Ruby, `<%# %>` in ERB, `-#` in HAML, `/` in
155
+ Slim), nor does a name starting a sentence in a string ("Join thousands of…").
156
+ Matching is textual and errs on the side of including a component: a false hit
157
+ costs a little CSS, a miss would leave a component unstyled. A gem that renders DaisyUI components for you (phlex-forms wraps
158
+ Input, Select, Range...) is scanned like your app:
159
+ `daisyui-tailwind app "$(bundle info phlex-forms --path)/lib"`; the classes it
160
+ writes in its own Ruby still need its own `@source`. Classes your code builds
161
+ by interpolation (`"badge-#{color}"`) are invisible to Tailwind either way:
162
+ spell them out.
163
+
164
+ **The size win.** Measured on [cosmos](https://github.com/zoolutions/cosmos)
165
+ (33 of the gem's 77 components), the CSS the gem's classes add to a bundle
166
+ (Tailwind 4.3.2, daisyUI 5.6.13, minified):
167
+
168
+ | Sources | CSS | gzip |
169
+ |---|---|---|
170
+ | `@source` the gem's whole `lib/` | 210.1 KB | 31.9 KB |
171
+ | `daisyui-tailwind` | 125.7 KB | 19.1 KB |
172
+ | `daisyui-tailwind --responsive sm,md,lg` | 466.5 KB | 64.3 KB |
173
+
174
+ It is also more complete. Tailwind v4 drops Ruby comments when it scans a `.rb`
175
+ file and cannot see a Symbol component class (`self.component_class = :navbar`),
176
+ so the whole-gem scan generated no `.navbar`, `.card`, `.drawer`... rule (the app
177
+ only got them by spelling the class itself) and none of the sub-rules daisyUI
178
+ nests under them (`indicator-item`, `stack-top`). The manifest carries both.
179
+
180
+ **Scanning the whole gem still works** as it did:
181
+
182
+ ```css
183
+ /* bundle info daisyui --path, resolved by your build script */
184
+ @source "/path/to/gems/daisyui-x.y.z/lib";
185
+ ```
186
+
187
+ ### The manifest
188
+
189
+ The command reads `data/daisyui/tailwind_manifest.json` in the gem, built from
190
+ the components themselves and checked in (`bundle exec rake tailwind:manifest`
191
+ in this repo). It sits outside `lib/`, so an `@source` on the gem's `lib/` or
192
+ its `**/*.rb` never sees it. A build script in another
193
+ language can read it directly from `$(bundle info daisyui --path)`:
194
+
195
+ ```json
196
+ {
197
+ "schema": 1,
198
+ "shared": { "files": ["lib/daisy_ui/base.rb"], "classes": ["bg-primary", "skeleton", "..."] },
199
+ "components": {
200
+ "Dropdown": {
201
+ "class": "DaisyUI::Dropdown",
202
+ "name": "dropdown",
203
+ "files": ["lib/daisy_ui/dropdown.rb"],
204
+ "depends_on": ["Button", "Menu"],
205
+ "modifiers": ["dropdown-end", "dropdown-top", "..."],
206
+ "classes": ["dropdown", "dropdown-content", "dropdown-end", "..."]
207
+ }
208
+ }
209
+ }
210
+ ```
211
+
212
+ - `name`: the daisyUI component class (`null` where it depends on the modifiers, as for Label).
213
+ - `files`: relative to the gem root, for a literal `@source` instead of `inline()`.
214
+ - `depends_on`: the components it renders; resolve them transitively.
215
+ - `modifiers`: the classes `responsive:` can prefix (with `name`).
216
+ - `classes`: everything it can render: `name`, the modifiers and the class words in its Ruby. Words that are not classes cost nothing.
217
+ - `shared`: base.rb's classes, which every component can render (`:skeleton`, the colour modifiers). Include them with any component.
218
+
219
+ ### Your own components (`--app`)
220
+
221
+ A component you declare with [`variants`](#variants-states-and-slots) renders
222
+ classes that appear nowhere literally in your source: an `on %i[hover open]`
223
+ rule writes `hover:…` and `[.dropdown…>&]:…` at render time. Tailwind only
224
+ compiles what it finds written out, so hand it your components' classes too:
225
+
226
+ ```sh
227
+ bin/rails "daisyui:tailwind[app/assets/stylesheets/generated/daisyui_app.css]"
228
+ # without Rails: load your components, then walk them
229
+ bundle exec daisyui-tailwind --app --require ./config/components.rb -o app/assets/stylesheets/generated/daisyui_app.css
230
+ ```
231
+
232
+ ```css
233
+ @import "./generated/daisyui.css"; /* the gem's components you use */
234
+ @import "./generated/daisyui_app.css"; /* your own: variants, compounds, states, slots */
120
235
  ```
121
236
 
237
+ With the app loaded (the rake task eager-loads Rails), it walks every
238
+ `DaisyUI::Base` subclass defined outside the gem and writes one
239
+ `@source inline(…)` per component, sorted by name: its component class,
240
+ modifiers, `base`, compound and slot classes, and each `on` class once per
241
+ state. `--responsive` and `--prefix` work as above; `--format json` too. From
242
+ Ruby: `DaisyUI::Tailwind.sources(app: true)`, or `app: [Widget, Picker]` for
243
+ some. The header carries no version and nothing depends on the machine, so the
244
+ file only changes when a declaration does: **commit it**, and let a spec fail
245
+ when it is stale, the way this gem checks its own manifest:
246
+
247
+ ```ruby
248
+ # spec/daisyui_tailwind_spec.rb
249
+ it("has fresh Tailwind sources") { expect(DaisyUI::Tailwind.stale?("app/assets/stylesheets/generated/daisyui_app.css")).to be(false) }
250
+ ```
251
+
252
+ `stale?` eager-loads a Rails app before it compares. The CSS build then never
253
+ boots the app; it only imports the file.
254
+
122
255
  ## Class merging
123
256
 
124
257
  Components merge their classes so the last conflicting class wins, for Tailwind
@@ -209,6 +342,116 @@ end
209
342
  Keyword arguments are not validated: `Toggle(primray: true)` renders a
210
343
  `primray` attribute, because unknown keywords are HTML attributes.
211
344
 
345
+ ## Variants, states and slots
346
+
347
+ The full guide, with when to use which declaration, is on the docs site:
348
+ [Variants, states and slots](https://daisyui.phlex.fun/docs/variants). Four things
349
+ to know before you write one:
350
+
351
+ - A `default` renders after the caller's modifiers, so it beats one that sets the same property. Put such a value in `base`.
352
+ - A `compound` or `on` rule also renders after the caller's modifiers. Leave the member it would override out of the match (`layout: [nil, :row]`).
353
+ - `on` writes `"<state>:<class>"`, the state outermost. A rule that needs a child selector first (`[&_li]:hover:...`) stays literal in `base`.
354
+ - `on` classes appear nowhere literally: generate [Tailwind sources](#your-own-components---app) for your components or Tailwind never compiles them.
355
+
356
+ `register_modifiers` maps a symbol to classes. A component that owns its design
357
+ also needs classes for two modifiers together, the same declarations under a
358
+ browser state, and classes for its parts. Declare them in a `variants` block:
359
+
360
+ ```ruby
361
+ class Widget < DaisyUI::Button
362
+ OPEN = "[.dropdown:is(:focus-within,[open],:has(:popover-open))>&]"
363
+
364
+ variants do
365
+ base "font-medium" # after the inherited base (btn)
366
+ variant :size, sm: "btn-sm [--fontsize:var(--text-sm)]" # overrides one inherited member
367
+ default :size, :md # when the caller names no size
368
+
369
+ compound style: :outline, color: nil, class: "[--btn-bg:var(--color-base-100)]"
370
+ compound style: %i[ghost link], class: "[--btn-border:transparent]"
371
+
372
+ state :hover, "hover"
373
+ state :open, OPEN
374
+ on %i[hover open], style: %i[ghost link], class: "[--btn-bg:color-mix(in_oklab,var(--color-base-content)_8%,transparent)]"
375
+ end
376
+ end
377
+
378
+ Widget() # btn font-medium btn-md
379
+ Widget(:ghost, :sm) # btn font-medium btn-ghost btn-sm [--fontsize:…] [--btn-border:transparent]
380
+ # hover:[--btn-bg:…] [.dropdown…>&]:[--btn-bg:…]
381
+ ```
382
+
383
+ | Declaration | |
384
+ |---|---|
385
+ | `variant :group, member: "classes", …` | Alternatives: the grouped `register_modifiers(group: { … })`. Members merge as one ClassMerge group. |
386
+ | `flag :name, "classes"` | An on/off modifier: an ungrouped `register_modifiers` entry. `flag :name` alone is a `register_flags` flag, no class, for compounds to match. |
387
+ | `base "classes"` | Classes every instance gets, appended to the inherited base. |
388
+ | `default :group, :member` | The member used when the caller names none of the group. Never rendered beside a member the caller gives. |
389
+ | `compound(**match, class:, set: {})` | `class:` when every key of `match` holds. `set: { group: :member }` selects members for the caller's empty groups; later compounds see them. Applied in order, after defaults. |
390
+ | `state :name, "variant"` | Names a Tailwind variant once: `"hover"`, `"focus-visible"`, `"[.dropdown[open]>&]"`. |
391
+ | `on(states, **match, class:)` | Like `compound`, with each class written once per state: `on %i[hover open], class: "a"` renders `hover:a [.dropdown…>&]:a`. |
392
+ | `slot :name, "classes"` | A part of the component (see below). |
393
+
394
+ A `match` value says when a group or flag holds:
395
+
396
+ | Value | Holds when |
397
+ |---|---|
398
+ | `:member` | that member is selected: given, defaulted or set by an earlier compound |
399
+ | `[:a, :b]` | any of them is; a `nil` in the list means "or none": `shape: [nil, :row]` |
400
+ | `nil` / `false` | no member of the group is |
401
+ | `true` | any member of the group is: `color: true` |
402
+ | `true` / `false` | for a flag: it is on / off |
403
+
404
+ Classes render in this order, merged by ClassMerge: the component class, `base`,
405
+ the given modifiers, the defaulted and set members, compounds, `on` rules,
406
+ `responsive:`, then `class:`. Every declared class goes through
407
+ `DaisyUI.configuration.prefix` like a modifier (`hover:d-…` with `prefix = "d-"`).
408
+ A typo fails where it is declared, at boot: a `compound`, `on`, `default` or
409
+ `set:` naming a group, member, flag, state or slot the component does not have
410
+ raises `ArgumentError`. `class` and `set` are reserved keys: a group called that could never be matched, and nothing stops you naming one.
411
+
412
+ **Slots.** A component with parts declares each one; a variant, compound or `on`
413
+ value can be a Hash keyed by slot (`root` is the component itself):
414
+
415
+ ```ruby
416
+ class Picker < DaisyUI::Dropdown
417
+ variants do
418
+ slot :trigger
419
+ slot :menu, "mt-2"
420
+ variant :reach, wide: { root: "picker-wide", menu: "w-96" }
421
+ state :open, "[.dropdown:is(:focus-within,[open],:has(:popover-open))>&]"
422
+ on :open, class: { menu: "shadow-lg" }
423
+ end
424
+
425
+ def trigger(*mods, **options, &) = render DaisyUI::Button.new(*mods, **slot_options(:trigger, options), &)
426
+ def list(*mods, **options, &) = render DaisyUI::Menu.new(*mods, class: slot_classes(:menu, options), **options, &)
427
+ end
428
+ ```
429
+
430
+ `slot_classes(:menu, options)` returns the slot's classes, those of every
431
+ selected member, compound and `on` rule for it, then `options.delete(:class)`,
432
+ merged. `slot_options(:trigger, options)` returns `options` with that `class:`.
433
+
434
+ **Introspection.** `Widget.variants.schema` is plain, frozen data for a docs
435
+ page, a component gallery or a design-tool parity check:
436
+
437
+ ```ruby
438
+ {
439
+ base: "btn font-medium",
440
+ variants: { style: %i[ghost link outline dash soft], size: %i[lg md xs xl sm], … }, # a re-declared member moves last
441
+ flags: %i[active disabled wide block],
442
+ defaults: { size: :md },
443
+ compounds: [{ match: { style: :outline, color: nil }, class: "[--btn-bg:…]", set: {} }, …],
444
+ on: [{ states: %i[hover open], match: { style: %i[ghost link] }, class: "[--btn-bg:…]" }],
445
+ states: { hover: "hover", open: "[.dropdown…>&]" },
446
+ slots: { root: "btn font-medium" }
447
+ }
448
+ ```
449
+
450
+ A component that only calls `register_modifiers` has a schema too (variants and
451
+ flags). An unknown modifier's error names the groups:
452
+ `Widget has no modifier :huge. Variants: style: ghost, link, outline, dash, soft; size: lg, md, xs, xl, sm; …`.
453
+ The state-prefixed classes need [Tailwind sources of their own](#your-own-components---app).
454
+
212
455
  ## Threads, fibers and Ractors
213
456
 
214
457
  Components and `DaisyUI::ClassMerge` are safe to share between threads (Puma)
@@ -457,6 +700,9 @@ Add to your Claude Code MCP settings (`~/.claude.json` or project `.claude.json`
457
700
  - **list_components** - List all available DaisyUI components
458
701
  - **get_component** - Get detailed info about a specific component (modifiers, usage examples)
459
702
  - **search_components** - Search components by name or modifier
703
+ - **variants_guide** - How to declare a component with `variants`: which declaration to use, the match values, render order and the pitfalls
704
+
705
+ `get_component` also returns the component's variants schema (groups, flags, defaults, compounds, `on` rules, states and slots).
460
706
 
461
707
  # Usage
462
708