daisyui 2.1.1 → 2.2.1
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 +4 -4
- data/CHANGELOG.md +73 -0
- data/README.md +149 -0
- data/lib/daisy_ui/base.rb +24 -4
- data/lib/daisy_ui/engine.rb +4 -0
- data/lib/daisy_ui/mcp_server/variants_guide.rb +99 -0
- data/lib/daisy_ui/mcp_server.rb +16 -1
- data/lib/daisy_ui/tailwind/app_sources.rb +73 -0
- data/lib/daisy_ui/tailwind/candidate.rb +38 -3
- data/lib/daisy_ui/tailwind/command.rb +21 -4
- data/lib/daisy_ui/tailwind/sources.rb +11 -8
- data/lib/daisy_ui/tailwind.rb +45 -1
- data/lib/daisy_ui/unknown_modifier_error.rb +9 -2
- data/lib/daisy_ui/updated_at.rb +1 -1
- data/lib/daisy_ui/variants/match.rb +89 -0
- data/lib/daisy_ui/variants/rendering.rb +68 -0
- data/lib/daisy_ui/variants/resolution.rb +53 -0
- data/lib/daisy_ui/variants/schema.rb +73 -0
- data/lib/daisy_ui/variants.rb +160 -0
- data/lib/daisy_ui/version.rb +1 -1
- data/lib/daisy_ui.rb +2 -0
- data/lib/tasks/daisyui.rake +12 -0
- metadata +9 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6b5e9f54e5ef6670c715b8b23b7f25e2143b5dff828a17df64cd1c937cd8ea5a
|
|
4
|
+
data.tar.gz: 0ddd4bbef303e8fbbed5248317e107755cd96205ed8acb1e6b5e50b35505ad5c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f0f500a23192a07122b72795bb10c391ac5541d35ab6c5e168d73a8cac84070b46fbfd68de149c4fe4d2e01263c449c903c0172732a4b0245b0083f39f0f234f
|
|
7
|
+
data.tar.gz: d9f72b8aa71f1289efbfc83c7763fe3be6ce9e71b1a9b07ecf8a63c157228772031128eb665e2cf6eefe28c3f30119d5e03d19961f57c00ed88a76e1270b201a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,78 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
### Fixed
|
|
4
|
+
|
|
5
|
+
- The Tailwind sources generator accepts named groups and peers
|
|
6
|
+
(`group/item`, `group-has-[…]/item:block`, `peer-checked/field:block`) and a
|
|
7
|
+
plugin utility's named modifier (`motion-ease-in-out/opacity`) instead of
|
|
8
|
+
raising on them as paths. A bare word before the `/` (`app/components`) and a
|
|
9
|
+
second `/` (`lib/daisy_ui/base.rb`) are still rejected (#71).
|
|
10
|
+
|
|
11
|
+
## [2.2.0] - 2026-10-26
|
|
12
|
+
|
|
13
|
+
### Documentation for the variants API
|
|
14
|
+
|
|
15
|
+
- Docs site: "Variants, states and slots" (when to use `variant`, `flag`,
|
|
16
|
+
`compound`, `state`/`on` and `slot`, the match values, render order and its
|
|
17
|
+
three pitfalls, introspection) and "Tailwind sources for your components"
|
|
18
|
+
(`--app`, the rake task, committing the file, the `stale?` spec, stylelint).
|
|
19
|
+
- MCP server: `get_component` includes the component's `variants.schema`, and a
|
|
20
|
+
new `variants_guide` tool returns a short guide to the DSL with the pitfalls.
|
|
21
|
+
- README: the variants section links the guide and lists the pitfalls.
|
|
22
|
+
|
|
23
|
+
### Variants, states and slots
|
|
24
|
+
|
|
25
|
+
A `variants` block declares what a component that owns its design needs
|
|
26
|
+
beyond `register_modifiers`, as data the gem owns (#67). See the README's
|
|
27
|
+
"Variants, states and slots".
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
class Widget < DaisyUI::Button
|
|
31
|
+
variants do
|
|
32
|
+
base "font-medium"
|
|
33
|
+
default :size, :md
|
|
34
|
+
compound style: :outline, color: nil, class: "[--btn-bg:var(--color-base-100)]"
|
|
35
|
+
state :hover, "hover"
|
|
36
|
+
state :open, "[.dropdown:is(:focus-within,[open],:has(:popover-open))>&]"
|
|
37
|
+
on %i[hover open], style: %i[ghost link], class: "[--btn-bg:transparent]"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- `variant` and `flag` are `register_modifiers` (groups, ClassMerge,
|
|
43
|
+
inheritance unchanged); `base` appends to the inherited base; `default`
|
|
44
|
+
applies when the caller names no member of a group.
|
|
45
|
+
- `compound(**match, class:, set:)` adds classes when modifiers hold together
|
|
46
|
+
or one holds and another is absent (`color: nil`, `shape: [nil, :row]`,
|
|
47
|
+
`color: true`, flags `true`/`false`); `set:` selects members for the
|
|
48
|
+
caller's empty groups, in declaration order.
|
|
49
|
+
- `state :name, "variant"` names a Tailwind variant once; `on(states, …)`
|
|
50
|
+
writes a declaration once and renders it under each state.
|
|
51
|
+
- `slot :name, "classes"`, per-slot Hash values, and `slot_classes` /
|
|
52
|
+
`slot_options` for components with parts.
|
|
53
|
+
- Defaults and `set:` members are resolved, not left to ClassMerge: a default
|
|
54
|
+
never renders beside the member the caller gave, with class merging off too.
|
|
55
|
+
- A typo (a group, member, flag, state or slot the component does not have)
|
|
56
|
+
raises `ArgumentError` where it is declared.
|
|
57
|
+
- `Component.variants.schema` returns plain, frozen data: variants, flags,
|
|
58
|
+
defaults, compounds, `on` rules, states and slots.
|
|
59
|
+
- An unknown modifier's error now lists the component's groups:
|
|
60
|
+
`… has no modifier :huge. Variants: size: xs, sm, …`.
|
|
61
|
+
- Registries are frozen by `DaisyUI.finalize!` like the others.
|
|
62
|
+
|
|
63
|
+
### Tailwind sources for an app's own components
|
|
64
|
+
|
|
65
|
+
State-prefixed classes appear nowhere literally in an app's source, so
|
|
66
|
+
`bin/rails "daisyui:tailwind[app/assets/stylesheets/generated/daisyui_app.css]"`
|
|
67
|
+
(or `daisyui-tailwind --app --require FILE`) writes `@source inline(…)` for
|
|
68
|
+
every `DaisyUI::Base` subclass the app defines: its classes, compounds, slots
|
|
69
|
+
and each `on` class per state. Sorted, versionless output meant to be
|
|
70
|
+
committed; `DaisyUI::Tailwind.stale?(path)` fails a spec when a declaration
|
|
71
|
+
changed and the file was not regenerated. From Ruby:
|
|
72
|
+
`DaisyUI::Tailwind.sources(app: true)`.
|
|
73
|
+
|
|
74
|
+
## [2.1.1] - 2026-10-17
|
|
75
|
+
|
|
3
76
|
### Tailwind sources for only the components an app uses
|
|
4
77
|
|
|
5
78
|
`bundle exec daisyui-tailwind app -o app/assets/stylesheets/generated/daisyui.css`
|
data/README.md
CHANGED
|
@@ -216,6 +216,42 @@ language can read it directly from `$(bundle info daisyui --path)`:
|
|
|
216
216
|
- `classes`: everything it can render: `name`, the modifiers and the class words in its Ruby. Words that are not classes cost nothing.
|
|
217
217
|
- `shared`: base.rb's classes, which every component can render (`:skeleton`, the colour modifiers). Include them with any component.
|
|
218
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 */
|
|
235
|
+
```
|
|
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
|
+
|
|
219
255
|
## Class merging
|
|
220
256
|
|
|
221
257
|
Components merge their classes so the last conflicting class wins, for Tailwind
|
|
@@ -306,6 +342,116 @@ end
|
|
|
306
342
|
Keyword arguments are not validated: `Toggle(primray: true)` renders a
|
|
307
343
|
`primray` attribute, because unknown keywords are HTML attributes.
|
|
308
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
|
+
|
|
309
455
|
## Threads, fibers and Ractors
|
|
310
456
|
|
|
311
457
|
Components and `DaisyUI::ClassMerge` are safe to share between threads (Puma)
|
|
@@ -554,6 +700,9 @@ Add to your Claude Code MCP settings (`~/.claude.json` or project `.claude.json`
|
|
|
554
700
|
- **list_components** - List all available DaisyUI components
|
|
555
701
|
- **get_component** - Get detailed info about a specific component (modifiers, usage examples)
|
|
556
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).
|
|
557
706
|
|
|
558
707
|
# Usage
|
|
559
708
|
|
data/lib/daisy_ui/base.rb
CHANGED
|
@@ -35,6 +35,8 @@ module DaisyUI
|
|
|
35
35
|
# This renders: <div class="[generated classes]" id="..." data-...="..." ...>
|
|
36
36
|
#
|
|
37
37
|
class Base < Phlex::HTML
|
|
38
|
+
include Variants::Rendering
|
|
39
|
+
|
|
38
40
|
BOOLS = [true, false].freeze
|
|
39
41
|
# Shared color modifiers used across multiple components
|
|
40
42
|
# Maps color names to their DaisyUI background/text class combinations
|
|
@@ -145,6 +147,16 @@ module DaisyUI
|
|
|
145
147
|
@component_class = Shareable.share(component_class)
|
|
146
148
|
end
|
|
147
149
|
|
|
150
|
+
# What a `variants` block declared beyond modifiers: base, defaults,
|
|
151
|
+
# compounds, states, `on` rules and slots (see DaisyUI::Variants).
|
|
152
|
+
def variant_definitions
|
|
153
|
+
@variant_definitions || Variants::EMPTY
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def variant_definitions=(definitions)
|
|
157
|
+
@variant_definitions = Shareable.share(definitions)
|
|
158
|
+
end
|
|
159
|
+
|
|
148
160
|
# Positional symbols a component handles itself (e.g. Dropdown's
|
|
149
161
|
# :popover): accepted as modifiers, but they emit no class.
|
|
150
162
|
def flags
|
|
@@ -183,6 +195,7 @@ module DaisyUI
|
|
|
183
195
|
subclass.modifiers = (modifiers || {}).dup
|
|
184
196
|
subclass.modifier_groups = modifier_groups.transform_values(&:dup)
|
|
185
197
|
subclass.flags = flags.dup
|
|
198
|
+
subclass.variant_definitions = variant_definitions
|
|
186
199
|
# Inherit component_class if it was explicitly set
|
|
187
200
|
return unless instance_variable_defined?(:@component_class)
|
|
188
201
|
|
|
@@ -216,11 +229,15 @@ module DaisyUI
|
|
|
216
229
|
ClassMerge.reset!
|
|
217
230
|
end
|
|
218
231
|
|
|
232
|
+
# Declares variants, compounds, defaults, states and slots; returns the
|
|
233
|
+
# declarations, so `Widget.variants.schema` describes them.
|
|
234
|
+
def variants(&) = Variants.build(self, &)
|
|
235
|
+
|
|
219
236
|
# @api private Called by DaisyUI.finalize! for every component: derives
|
|
220
237
|
# component_class on the main Ractor and makes sure every registry is
|
|
221
238
|
# frozen for non-main Ractors.
|
|
222
239
|
def finalize!
|
|
223
|
-
[component_class, modifiers, modifier_groups, flags].each { |registry| Shareable.share(registry) }
|
|
240
|
+
[component_class, modifiers, modifier_groups, flags, variant_definitions].each { |registry| Shareable.share(registry) }
|
|
224
241
|
self
|
|
225
242
|
end
|
|
226
243
|
end
|
|
@@ -246,17 +263,20 @@ module DaisyUI
|
|
|
246
263
|
# Main extension points - override these in your project!
|
|
247
264
|
|
|
248
265
|
# Builds the final CSS class string from:
|
|
249
|
-
# 1. Component base class (e.g., "dropdown")
|
|
266
|
+
# 1. Component base class (e.g., "dropdown"), then a `variants` base
|
|
250
267
|
# 2. Modifier classes (e.g., "dropdown-top")
|
|
251
|
-
# 3.
|
|
252
|
-
# 4.
|
|
268
|
+
# 3. What a `variants` block derives: defaults, compounds, `on` states
|
|
269
|
+
# 4. Responsive classes (e.g., "sm:dropdown-hover")
|
|
270
|
+
# 5. User-provided classes via `class:` option
|
|
253
271
|
#
|
|
254
272
|
# Note: This method DESTRUCTIVELY removes :class from options
|
|
255
273
|
# so it won't appear again in attributes
|
|
256
274
|
def classes
|
|
257
275
|
merge_classes(
|
|
258
276
|
base_class,
|
|
277
|
+
variant_base_classes,
|
|
259
278
|
*modifier_classes,
|
|
279
|
+
*variant_classes,
|
|
260
280
|
*responsive_classes,
|
|
261
281
|
options.delete(:class)
|
|
262
282
|
)
|
data/lib/daisy_ui/engine.rb
CHANGED
|
@@ -11,6 +11,10 @@ module DaisyUI
|
|
|
11
11
|
# * appends the JS dir to the asset load path so Propshaft/Sprockets serve
|
|
12
12
|
# the .min.js and its sourcemap (Sprockets also needs them precompiled).
|
|
13
13
|
#
|
|
14
|
+
# Rails also loads lib/tasks/daisyui.rake from here: `bin/rails
|
|
15
|
+
# daisyui:tailwind[OUTPUT]` writes the Tailwind sources of the app's own
|
|
16
|
+
# components (DaisyUI::Tailwind::AppSources).
|
|
17
|
+
#
|
|
14
18
|
# The controllers are opt-in at the markup level (`Dropdown(:popover, stimulus:
|
|
15
19
|
# true)`, `Tooltip(:popover)`) and not preloaded, so a page that never uses
|
|
16
20
|
# one never fetches it.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DaisyUI
|
|
4
|
+
class McpServer
|
|
5
|
+
# The text of the `variants_guide` tool: the `variants` DSL on one page, with
|
|
6
|
+
# the three ways it surprises. Keep it in step with the README's "Variants,
|
|
7
|
+
# states and slots" and the docs site page.
|
|
8
|
+
module VariantsGuide
|
|
9
|
+
TEXT = <<~MARKDOWN
|
|
10
|
+
# Variants, states and slots
|
|
11
|
+
|
|
12
|
+
A component that owns its design declares it in a `variants` block on a
|
|
13
|
+
`DaisyUI::Base` subclass. `get_component` shows the result as a schema.
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
class Widget < DaisyUI::Button
|
|
17
|
+
variants do
|
|
18
|
+
base "font-medium"
|
|
19
|
+
variant :size, sm: "btn-sm", md: "btn-md"
|
|
20
|
+
default :size, :md
|
|
21
|
+
compound style: :outline, color: nil, class: "[--btn-bg:var(--color-base-100)]"
|
|
22
|
+
state :hover, "hover"
|
|
23
|
+
on :hover, style: %i[ghost link], class: "[--btn-bg:transparent]"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Which declaration
|
|
29
|
+
|
|
30
|
+
| Need | Use |
|
|
31
|
+
|---|---|
|
|
32
|
+
| Alternatives, only one applies (sizes, colors) | `variant :group, member: "classes"` |
|
|
33
|
+
| An on/off modifier | `flag :name, "classes"`; `flag :name` alone has no class and exists to be matched |
|
|
34
|
+
| Classes every instance gets | `base "classes"` (appended to the inherited base) |
|
|
35
|
+
| A member when the caller names none of the group | `default :group, :member` |
|
|
36
|
+
| Classes when modifiers hold together or one is absent | `compound(**match, class:, set: {})` |
|
|
37
|
+
| The same classes under a browser state | `state :name, "<tailwind variant>"` once, then `on(states, **match, class:)` |
|
|
38
|
+
| A part of the component (trigger, menu) | `slot :name, "classes"`, then `slot_classes(name, options)` / `slot_options(name, options)` in the part's method |
|
|
39
|
+
|
|
40
|
+
## Match values (`compound`, `on`)
|
|
41
|
+
|
|
42
|
+
| Value | Holds when |
|
|
43
|
+
|---|---|
|
|
44
|
+
| `:member` | that member is selected: given, defaulted or set by an earlier compound |
|
|
45
|
+
| `[:a, :b]` | any of them is; a `nil` in the list means "or none": `shape: [nil, :row]` |
|
|
46
|
+
| `nil` / `false` | no member of the group is |
|
|
47
|
+
| `true` | any member of the group is: `color: true` |
|
|
48
|
+
| `true` / `false` | for a flag: on / off |
|
|
49
|
+
|
|
50
|
+
`set: { group: :member }` on a compound selects a member for the caller's empty
|
|
51
|
+
groups; later compounds see it. `class` and `set` are reserved keys: a group called that could never be matched.
|
|
52
|
+
A `class:` or a modifier value can be a Hash keyed by slot (`root` is the component).
|
|
53
|
+
|
|
54
|
+
## Render order
|
|
55
|
+
|
|
56
|
+
component class, `base`, the caller's modifiers, defaulted and set members,
|
|
57
|
+
compounds, `on` rules, `responsive:`, then the caller's `class:`. ClassMerge
|
|
58
|
+
resolves conflicts, the last one wins.
|
|
59
|
+
|
|
60
|
+
## Pitfalls
|
|
61
|
+
|
|
62
|
+
1. A `default` renders after the caller's modifiers. A default member's class
|
|
63
|
+
beats a caller modifier that sets the same property (a default size's
|
|
64
|
+
`--fontsize` beats an `:inline` modifier's). Put a value both can set in
|
|
65
|
+
`base`, not in a default.
|
|
66
|
+
2. A compound or `on` rule also renders after the caller's modifiers, so it
|
|
67
|
+
can override a modifier's arbitrary property. Leave the conflicting member
|
|
68
|
+
out of the match (`layout: [nil, :inline, :row]`).
|
|
69
|
+
3. `on` writes `"<state>:<class>"`: the state prefix is outermost. A rule that
|
|
70
|
+
needs a child selector first (`[&_li]:hover:bg-...`) cannot be an `on :hover`
|
|
71
|
+
rule; keep it literal in `base`.
|
|
72
|
+
|
|
73
|
+
Also: do not name a group after an inherited one with a different meaning
|
|
74
|
+
(`Button` already has `shape`); a state selector with a child combinator puts `>`
|
|
75
|
+
in the class attribute, so parse rendered HTML with Nokogiri, not a regexp.
|
|
76
|
+
|
|
77
|
+
## Tailwind sources
|
|
78
|
+
|
|
79
|
+
Tailwind v4 compiles only classes written out literally, and an `on` class is
|
|
80
|
+
written nowhere. Generate sources for your own components, commit the file,
|
|
81
|
+
import it from the Tailwind entry and fail a spec when it is stale:
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
bin/rails "daisyui:tailwind[app/assets/stylesheets/generated/daisyui_app.css]"
|
|
85
|
+
bundle exec daisyui-tailwind --app --require ./config/components.rb -o app/assets/stylesheets/generated/daisyui_app.css
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
expect(DaisyUI::Tailwind.stale?("app/assets/stylesheets/generated/daisyui_app.css")).to be(false)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Introspection
|
|
93
|
+
|
|
94
|
+
`Widget.variants.schema` returns `base`, `variants`, `flags`, `defaults`,
|
|
95
|
+
`compounds`, `on`, `states` and `slots` as frozen data.
|
|
96
|
+
MARKDOWN
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
data/lib/daisy_ui/mcp_server.rb
CHANGED
|
@@ -18,7 +18,7 @@ module DaisyUI
|
|
|
18
18
|
},
|
|
19
19
|
{
|
|
20
20
|
name: "get_component",
|
|
21
|
-
description: "Get detailed information about a specific DaisyUI component including all modifiers and usage examples",
|
|
21
|
+
description: "Get detailed information about a specific DaisyUI component including all modifiers, its variants schema (groups, flags, defaults, compounds, on rules, states, slots) and usage examples",
|
|
22
22
|
inputSchema: {
|
|
23
23
|
type: "object",
|
|
24
24
|
properties: {
|
|
@@ -37,6 +37,11 @@ module DaisyUI
|
|
|
37
37
|
},
|
|
38
38
|
required: ["query"]
|
|
39
39
|
}
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: "variants_guide",
|
|
43
|
+
description: "How to declare a component's design with the variants DSL (variant, flag, default, compound, state/on, slot): when to use which, the match values, render order and the pitfalls",
|
|
44
|
+
inputSchema: { type: "object", properties: {}, required: [] }
|
|
40
45
|
}
|
|
41
46
|
].freeze
|
|
42
47
|
Shareable.make(TOOLS) # the Hashes inside too
|
|
@@ -113,6 +118,8 @@ module DaisyUI
|
|
|
113
118
|
get_component(arguments["component"])
|
|
114
119
|
when "search_components"
|
|
115
120
|
search_components(arguments["query"])
|
|
121
|
+
when "variants_guide"
|
|
122
|
+
VariantsGuide::TEXT
|
|
116
123
|
else
|
|
117
124
|
return { content: [{ type: "text", text: "Unknown tool: #{tool_name}" }], isError: true }
|
|
118
125
|
end
|
|
@@ -160,6 +167,14 @@ module DaisyUI
|
|
|
160
167
|
## Available Modifiers
|
|
161
168
|
#{modifier_list.empty? ? 'No modifiers' : modifier_list}
|
|
162
169
|
|
|
170
|
+
## Variants schema
|
|
171
|
+
|
|
172
|
+
Groups, flags, defaults, compounds, `on` rules, states and slots. `variants_guide` explains them.
|
|
173
|
+
|
|
174
|
+
```json
|
|
175
|
+
#{JSON.pretty_generate(klass.variants.schema)}
|
|
176
|
+
```
|
|
177
|
+
|
|
163
178
|
## Usage Examples
|
|
164
179
|
|
|
165
180
|
```ruby
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DaisyUI
|
|
4
|
+
module Tailwind
|
|
5
|
+
# The Tailwind sources for an app's own components (DaisyUI::Base
|
|
6
|
+
# subclasses outside the gem), read from their declarations rather than
|
|
7
|
+
# the gem's manifest: the component class, every modifier, and what a
|
|
8
|
+
# `variants` block declares, `on` rules written out once per state. Those
|
|
9
|
+
# state-prefixed classes appear nowhere literally in the app's source, so
|
|
10
|
+
# Tailwind only compiles them from here.
|
|
11
|
+
#
|
|
12
|
+
# One `@source inline(…)` per component, sorted by name, under a header
|
|
13
|
+
# with no version, so the file changes only when a declaration does: it
|
|
14
|
+
# is meant to be committed, and DaisyUI::Tailwind.stale? checks it.
|
|
15
|
+
class AppSources
|
|
16
|
+
HEADER = "/* Generated by daisyui-tailwind --app from the app's DaisyUI components: do not edit. */\n"
|
|
17
|
+
|
|
18
|
+
def initialize(components, responsive: [], prefix: nil)
|
|
19
|
+
anonymous = components.reject(&:name)
|
|
20
|
+
raise ArgumentError, "an anonymous class has no name to label its block: #{anonymous.inspect}" unless anonymous.empty?
|
|
21
|
+
|
|
22
|
+
@components = components.sort_by(&:name)
|
|
23
|
+
@breakpoints = responsive.map(&:to_s)
|
|
24
|
+
@prefix = prefix.to_s
|
|
25
|
+
Sources.validate_options!(@breakpoints, @prefix)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def render(format)
|
|
29
|
+
case format.to_s
|
|
30
|
+
when "css" then to_css
|
|
31
|
+
when "json" then to_json
|
|
32
|
+
else raise ArgumentError, "unknown format #{format.inspect}: css or json"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def to_css
|
|
37
|
+
return "#{HEADER}/* No app components. */\n" if @components.empty?
|
|
38
|
+
|
|
39
|
+
lines = @components.flat_map { |component| ["/* #{component.name} */", %(@source inline("#{classes(component).join(' ')}");)] }
|
|
40
|
+
"#{HEADER}#{lines.join("\n")}\n"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def to_json(*)
|
|
44
|
+
require "json"
|
|
45
|
+
|
|
46
|
+
components = @components.to_h do |component|
|
|
47
|
+
[component.name, { "name" => component.component_class&.to_s, "classes" => classes(component) }]
|
|
48
|
+
end
|
|
49
|
+
json = { "components" => components, "classes" => components.values.flat_map { |entry| entry["classes"] }.uniq.sort }
|
|
50
|
+
"#{JSON.pretty_generate(json)}\n"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def classes(component)
|
|
56
|
+
classes = component.variants.literal_classes(prefix: @prefix) + responsive_classes(component)
|
|
57
|
+
invalid = classes.reject { |token| Candidate.valid?(token) }
|
|
58
|
+
return classes.uniq.sort if invalid.empty?
|
|
59
|
+
|
|
60
|
+
raise ArgumentError, "#{component.name} declares #{invalid.map(&:inspect).join(', ')}, " \
|
|
61
|
+
"which Tailwind cannot read from @source inline(…)"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# What `responsive:` renders: the component class and the modifiers.
|
|
65
|
+
def responsive_classes(component)
|
|
66
|
+
return [] if @breakpoints.empty?
|
|
67
|
+
|
|
68
|
+
tokens = [component.component_class&.to_s, *(component.modifiers || {}).values].compact.flat_map(&:split)
|
|
69
|
+
@breakpoints.flat_map { |breakpoint| tokens.map { |token| "#{breakpoint}:#{@prefix}#{token}" } }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -6,18 +6,24 @@ module DaisyUI
|
|
|
6
6
|
# quoted string with brace expansion, and one unbalanced bracket in it
|
|
7
7
|
# makes Tailwind drop every class after it, so a candidate starts and ends
|
|
8
8
|
# like a utility or variant, has balanced () and [], and holds no quote,
|
|
9
|
-
# brace, semicolon, backslash or backtick.
|
|
10
|
-
#
|
|
9
|
+
# brace, semicolon, backslash or backtick.
|
|
10
|
+
#
|
|
11
|
+
# A `/` before a digit or `[` is a fraction or an opacity (`w-1/2`,
|
|
12
|
+
# `bg-base-100/50`). A `/` before anything else is a path, except one
|
|
13
|
+
# trailing `/<name>` on a variant or utility: a named group or peer
|
|
14
|
+
# (`group/item`, `group-hover/item:underline`, `peer-checked/field:block`)
|
|
15
|
+
# or a plugin utility's named modifier (`motion-ease-in-out/opacity`).
|
|
11
16
|
#
|
|
12
17
|
# @api private
|
|
13
18
|
module Candidate
|
|
14
19
|
SHAPE = /\A[a-z@!\[*-][^\s"'`;{}\\]*[a-z0-9\])%!]\z/
|
|
15
20
|
PATH = %r{/(?![0-9\[])}
|
|
21
|
+
NAMED = %r{\A(?<head>.+)/[A-Za-z0-9_-]+!?\z}
|
|
16
22
|
CLOSING = { ")" => "(", "]" => "[" }.freeze
|
|
17
23
|
|
|
18
24
|
class << self
|
|
19
25
|
def valid?(token)
|
|
20
|
-
SHAPE.match?(token) && token.match?(/[a-z]/) &&
|
|
26
|
+
SHAPE.match?(token) && token.match?(/[a-z]/) && balanced?(token) && !paths?(token)
|
|
21
27
|
end
|
|
22
28
|
|
|
23
29
|
# The candidates among the whitespace-separated words of `classes`.
|
|
@@ -38,6 +44,35 @@ module DaisyUI
|
|
|
38
44
|
end
|
|
39
45
|
open.empty?
|
|
40
46
|
end
|
|
47
|
+
|
|
48
|
+
def paths?(token)
|
|
49
|
+
parts(token).any? { |part| path?(part) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def path?(part)
|
|
53
|
+
return false unless PATH.match?(part)
|
|
54
|
+
|
|
55
|
+
head = part[NAMED, :head]
|
|
56
|
+
head.nil? || PATH.match?(head) || !named?(head)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# What may carry a `/<name>`: a group or a peer, or a utility with a
|
|
60
|
+
# `-` in it. A bare word (`app`, `vendor`, `hover`) is a directory.
|
|
61
|
+
def named?(head)
|
|
62
|
+
%w[group peer].include?(head) || (head.include?("-") && SHAPE.match?(head))
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# The token split on its top-level `:`, as Tailwind splits variants
|
|
66
|
+
# from the utility; a `:` inside [] or () belongs to its part.
|
|
67
|
+
def parts(token)
|
|
68
|
+
depth = 0
|
|
69
|
+
slices = token.each_char.slice_when do |char, _|
|
|
70
|
+
depth += 1 if "[(".include?(char)
|
|
71
|
+
depth -= 1 if "])".include?(char)
|
|
72
|
+
char == ":" && depth.zero?
|
|
73
|
+
end
|
|
74
|
+
slices.map { |chars| chars.join.delete_suffix(":") }
|
|
75
|
+
end
|
|
41
76
|
end
|
|
42
77
|
end
|
|
43
78
|
end
|