daisyui 2.1.1 → 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 +4 -4
- data/CHANGELOG.md +63 -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/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: b1bf5647c845ed4fbdf7d538e4934c7171b23039cbc9029d8c68330b82f0acd2
|
|
4
|
+
data.tar.gz: 9282c494a1db396b4153fda3c9696040e8c95ac7d495d0bbcf9a0be983a49fb1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f2fa7469a01b18c4bb28110ab4da1238ce8c846fef910e3edc488d283ed641e8cee5fb3b2fb69ca528f643283a1339114db14354a0f3d0d306b67f996923e4d8
|
|
7
|
+
data.tar.gz: 282f9add88cc7e82eabd013b81bb4e1470c8809f6687ada460470ef97a472d228182154f644c56a7df000f4714dd8d51eca4bbc5692f5ddbbdba7607e84617f4
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,68 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
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
|
+
|
|
3
66
|
### Tailwind sources for only the components an app uses
|
|
4
67
|
|
|
5
68
|
`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
|
|
@@ -13,6 +13,10 @@ module DaisyUI
|
|
|
13
13
|
that the Ruby and templates under each PATH use, plus the components those
|
|
14
14
|
render. @import the output from your Tailwind entry.
|
|
15
15
|
|
|
16
|
+
With --app, prints the sources of the app's own components (DaisyUI::Base
|
|
17
|
+
subclasses, after requiring each --require FILE) instead: their variants,
|
|
18
|
+
compounds and state-prefixed classes, for a file you commit.
|
|
19
|
+
|
|
16
20
|
TEXT
|
|
17
21
|
|
|
18
22
|
def self.run(argv, out: $stdout, err: $stderr) = new(out:, err:).run(argv)
|
|
@@ -25,16 +29,16 @@ module DaisyUI
|
|
|
25
29
|
def run(argv)
|
|
26
30
|
require "optparse"
|
|
27
31
|
|
|
28
|
-
options = { components: [], format: "css", responsive: [] }
|
|
32
|
+
options = { components: [], format: "css", responsive: [], require: [] }
|
|
29
33
|
parser = parser(options)
|
|
30
34
|
paths = parser.parse(argv)
|
|
31
35
|
return help(parser) if options[:help]
|
|
32
|
-
return usage(parser) if paths.empty? && options[:components].empty?
|
|
36
|
+
return usage(parser) if paths.empty? && options[:components].empty? && !options[:app]
|
|
33
37
|
|
|
34
|
-
result = Tailwind.sources(scan: paths, **options
|
|
38
|
+
result = Tailwind.sources(scan: paths, **sources_options(options))
|
|
35
39
|
options[:output] ? write(options[:output], result) : @out.write(result)
|
|
36
40
|
0
|
|
37
|
-
rescue OptionParser::ParseError, ArgumentError, SystemCallError => e
|
|
41
|
+
rescue OptionParser::ParseError, ArgumentError, SystemCallError, LoadError => e
|
|
38
42
|
@err.puts "daisyui-tailwind: #{e.message}"
|
|
39
43
|
1
|
|
40
44
|
end
|
|
@@ -53,10 +57,23 @@ module DaisyUI
|
|
|
53
57
|
options[:responsive].concat(breakpoints)
|
|
54
58
|
end
|
|
55
59
|
opts.on("--prefix PREFIX", "The DaisyUI.configuration.prefix your app sets") { |prefix| options[:prefix] = prefix }
|
|
60
|
+
opts.on("--app", "The app's own components instead of the gem's") { options[:app] = true }
|
|
61
|
+
opts.on("-r", "--require FILE", "With --app: load FILE first, e.g. config/environment.rb (repeatable)") do |file|
|
|
62
|
+
options[:require] << file
|
|
63
|
+
end
|
|
56
64
|
opts.on("-h", "--help", "Show this help") { options[:help] = true }
|
|
57
65
|
end
|
|
58
66
|
end
|
|
59
67
|
|
|
68
|
+
# With --app: load the app's files, then freeze DaisyUI as boot does.
|
|
69
|
+
def sources_options(options)
|
|
70
|
+
if options[:app]
|
|
71
|
+
options[:require].each { |file| require File.expand_path(file) }
|
|
72
|
+
DaisyUI.finalize!
|
|
73
|
+
end
|
|
74
|
+
options.slice(:components, :format, :responsive, :prefix, :app)
|
|
75
|
+
end
|
|
76
|
+
|
|
60
77
|
def help(parser)
|
|
61
78
|
@out.puts parser
|
|
62
79
|
0
|
|
@@ -22,6 +22,16 @@ module DaisyUI
|
|
|
22
22
|
|
|
23
23
|
attr_reader :components
|
|
24
24
|
|
|
25
|
+
# @api private Shared with AppSources.
|
|
26
|
+
def self.validate_options!(breakpoints, prefix)
|
|
27
|
+
breakpoints.each do |breakpoint|
|
|
28
|
+
raise ArgumentError, "invalid breakpoint #{breakpoint.inspect}: give the variant name, e.g. sm or @md" unless BREAKPOINT.match?(breakpoint)
|
|
29
|
+
end
|
|
30
|
+
return if prefix.empty? || PREFIX.match?(prefix)
|
|
31
|
+
|
|
32
|
+
raise ArgumentError, "invalid prefix #{prefix.inspect}"
|
|
33
|
+
end
|
|
34
|
+
|
|
25
35
|
def initialize(manifest, components, responsive: [], prefix: nil)
|
|
26
36
|
@manifest = manifest
|
|
27
37
|
@components = components
|
|
@@ -90,14 +100,7 @@ module DaisyUI
|
|
|
90
100
|
|
|
91
101
|
def inline(classes) = %(@source inline("#{classes.join(' ')}");)
|
|
92
102
|
|
|
93
|
-
def validate!
|
|
94
|
-
@breakpoints.each do |breakpoint|
|
|
95
|
-
raise ArgumentError, "invalid breakpoint #{breakpoint.inspect}: give the variant name, e.g. sm or @md" unless BREAKPOINT.match?(breakpoint)
|
|
96
|
-
end
|
|
97
|
-
return if @prefix.empty? || PREFIX.match?(@prefix)
|
|
98
|
-
|
|
99
|
-
raise ArgumentError, "invalid prefix #{@prefix.inspect}"
|
|
100
|
-
end
|
|
103
|
+
def validate! = self.class.validate_options!(@breakpoints, @prefix)
|
|
101
104
|
end
|
|
102
105
|
end
|
|
103
106
|
end
|
data/lib/daisy_ui/tailwind.rb
CHANGED
|
@@ -37,11 +37,55 @@ module DaisyUI
|
|
|
37
37
|
# components. `responsive:` adds breakpoint variants (`%w[sm md]`) of the
|
|
38
38
|
# classes `responsive:` renders; `prefix:` defaults to the
|
|
39
39
|
# DaisyUI.configuration.prefix the components render with.
|
|
40
|
-
|
|
40
|
+
#
|
|
41
|
+
# `app: true` gives the app's own components instead (app_components),
|
|
42
|
+
# or `app: [Widget, …]` the ones named: see AppSources. Their file is
|
|
43
|
+
# separate from the gem components' and meant to be committed.
|
|
44
|
+
def sources(scan: [], components: [], app: false, format: :css, responsive: [], prefix: DaisyUI.configuration.prefix)
|
|
45
|
+
return app_sources(app, scan:, components:).new(app == true ? app_components : app, responsive:, prefix:).render(format) if app
|
|
46
|
+
|
|
41
47
|
manifest = self.manifest
|
|
42
48
|
used = used_components(scan:, components:, manifest:)
|
|
43
49
|
Sources.new(manifest, used, responsive:, prefix:).render(format)
|
|
44
50
|
end
|
|
51
|
+
|
|
52
|
+
# The app's DaisyUI::Base subclasses, sorted by name: every named one
|
|
53
|
+
# whose constant still names it (a class a code reload replaced is
|
|
54
|
+
# skipped), defined outside the gem's lib/ or a gem component the app
|
|
55
|
+
# reopened to declare variants (the gem's own never do). A Rails app is
|
|
56
|
+
# eager-loaded first.
|
|
57
|
+
def app_components
|
|
58
|
+
::Rails.application.eager_load! if defined?(::Rails) && ::Rails.respond_to?(:application) && ::Rails.application
|
|
59
|
+
ClassMerge::DaisyGroups.components.select { |component| current?(component) && app_owned?(component) }.sort_by(&:name)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Whether the committed app sources at `path` differ from what the
|
|
63
|
+
# declarations give now (or the file is missing): a spec can fail on it,
|
|
64
|
+
# the way the gem's own manifest spec does.
|
|
65
|
+
def stale?(path, app: true, **)
|
|
66
|
+
!File.file?(path) || File.read(path) != sources(app:, **)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def app_sources(app, scan:, components:)
|
|
72
|
+
return AppSources if scan.empty? && components.empty?
|
|
73
|
+
|
|
74
|
+
raise ArgumentError, "app: gives the app's own components, a file of its own; " \
|
|
75
|
+
"scan: and components: give the gem's (#{app.inspect} with #{(scan + components).inspect})"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def app_owned?(component)
|
|
79
|
+
return true unless Object.const_source_location(component.name)&.first.to_s.start_with?(File.join(ROOT, "lib", ""))
|
|
80
|
+
|
|
81
|
+
!component.variant_definitions.equal?(Variants::EMPTY)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def current?(component)
|
|
85
|
+
Object.const_get(component.name).equal?(component)
|
|
86
|
+
rescue NameError
|
|
87
|
+
false
|
|
88
|
+
end
|
|
45
89
|
end
|
|
46
90
|
end
|
|
47
91
|
end
|
|
@@ -23,9 +23,16 @@ module DaisyUI
|
|
|
23
23
|
name = "#{name}##{part}" if part
|
|
24
24
|
message = "#{name} has no modifier #{modifier.inspect}"
|
|
25
25
|
suggestions = DidYouMean::SpellChecker.new(dictionary: known.map(&:to_s)).correct(modifier.to_s)
|
|
26
|
-
|
|
26
|
+
message = "#{message}. Did you mean? #{suggestions.map { |s| ":#{s}" }.join(', ')}" unless suggestions.empty?
|
|
27
|
+
groups.empty? ? message : "#{message}. Variants: #{groups}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The component's groups and their members, "size: sm, lg; color: …";
|
|
31
|
+
# a sub-component part has modifiers of its own, so none for it.
|
|
32
|
+
def groups
|
|
33
|
+
return "" if part || !component.respond_to?(:modifier_groups)
|
|
27
34
|
|
|
28
|
-
|
|
35
|
+
component.modifier_groups.map { |group, members| "#{group}: #{members.join(', ')}" }.join("; ")
|
|
29
36
|
end
|
|
30
37
|
end
|
|
31
38
|
end
|
data/lib/daisy_ui/updated_at.rb
CHANGED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DaisyUI
|
|
4
|
+
class Variants
|
|
5
|
+
# What a `compound` or `on` match means, and the checks that make a typo
|
|
6
|
+
# fail where it is declared (at boot) rather than never match:
|
|
7
|
+
#
|
|
8
|
+
# | value | matches when |
|
|
9
|
+
# |------------------|-----------------------------------------------------|
|
|
10
|
+
# | `:member` | that member is selected (given, defaulted or set) |
|
|
11
|
+
# | `[:a, :b]` | any of them is; a `nil` in the list means "or none" |
|
|
12
|
+
# | `nil` / `false` | no member of that group is |
|
|
13
|
+
# | `true` | any member of that group is |
|
|
14
|
+
# | `true` / `false` | a flag is on / off |
|
|
15
|
+
#
|
|
16
|
+
# @api private
|
|
17
|
+
class Match
|
|
18
|
+
def initialize(klass)
|
|
19
|
+
@klass = klass
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# `selection` is { group => member or nil }, `on` the flags given.
|
|
23
|
+
def self.holds?(match, selection, on)
|
|
24
|
+
match.all? do |key, value|
|
|
25
|
+
if selection.key?(key)
|
|
26
|
+
member = selection[key]
|
|
27
|
+
case value
|
|
28
|
+
when true then !member.nil?
|
|
29
|
+
when false, nil then member.nil?
|
|
30
|
+
when Array then value.include?(member)
|
|
31
|
+
else member == value
|
|
32
|
+
end
|
|
33
|
+
else
|
|
34
|
+
on.include?(key) == value
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def match!(match)
|
|
40
|
+
match.each do |key, value|
|
|
41
|
+
if groups.key?(key)
|
|
42
|
+
value_members(value).each { |member| member!(key, member) }
|
|
43
|
+
elsif flags.include?(key)
|
|
44
|
+
raise ArgumentError, "#{owner}: flag #{key.inspect} matches true or false, got #{value.inspect}" unless BOOLS.include?(value)
|
|
45
|
+
else
|
|
46
|
+
raise ArgumentError, "#{owner} has no variant or flag #{key.inspect}"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def member!(group, member)
|
|
52
|
+
raise ArgumentError, "#{owner} has no variant #{group.inspect}" unless groups.key?(group)
|
|
53
|
+
return if groups[group].include?(member)
|
|
54
|
+
|
|
55
|
+
raise ArgumentError, "#{owner}: #{group.inspect} has no member #{member.inspect}; #{group}: #{groups[group].join(', ')}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def state!(name)
|
|
59
|
+
return if @klass.variant_definitions[:states].key?(name)
|
|
60
|
+
|
|
61
|
+
raise ArgumentError, "#{owner} has no state #{name.inspect}: declare it with `state #{name.inspect}, \"<variant>\"` first"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def slot!(name)
|
|
65
|
+
return if name == :root || @klass.variant_definitions[:slots].key?(name)
|
|
66
|
+
|
|
67
|
+
raise ArgumentError, "#{owner} has no slot #{name.inspect}: declare it with `slot #{name.inspect}` first"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
BOOLS = [true, false].freeze
|
|
73
|
+
private_constant :BOOLS
|
|
74
|
+
|
|
75
|
+
def value_members(value)
|
|
76
|
+
return [] if value.nil? || value == true || value == false
|
|
77
|
+
|
|
78
|
+
Array(value).compact
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def groups = @klass.modifier_groups
|
|
82
|
+
|
|
83
|
+
# Ungrouped modifiers and registered flags.
|
|
84
|
+
def flags = Schema.flags(@klass)
|
|
85
|
+
|
|
86
|
+
def owner = @klass.name || @klass.inspect
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DaisyUI
|
|
4
|
+
class Variants
|
|
5
|
+
# The instance side of the Variants API, included in DaisyUI::Base. A
|
|
6
|
+
# component without declarations (every gem component) skips it all.
|
|
7
|
+
module Rendering
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
# The `base` classes a `variants` block declared.
|
|
11
|
+
def variant_base_classes
|
|
12
|
+
apply_prefix(self.class.variant_definitions[:base])
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# For `slot` (:root is the component itself): the classes of the
|
|
16
|
+
# members a default or compound selected (the root's given members are
|
|
17
|
+
# modifier_classes already), of every compound that holds, and of every
|
|
18
|
+
# `on` rule that holds, prefixed with its states.
|
|
19
|
+
def variant_classes(slot = :root)
|
|
20
|
+
definitions = self.class.variant_definitions
|
|
21
|
+
return [] if definitions.equal?(EMPTY)
|
|
22
|
+
|
|
23
|
+
resolution = variant_resolution
|
|
24
|
+
[
|
|
25
|
+
*variant_member_classes(resolution, slot, definitions),
|
|
26
|
+
*resolution.compounds.map { |compound| apply_prefix(compound[:class][slot]) },
|
|
27
|
+
*resolution.rules.flat_map { |rule| state_classes(rule, slot, definitions[:states]) }
|
|
28
|
+
].compact
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Classes for one of the component's parts: the slot's own, those its
|
|
32
|
+
# variants, compounds and `on` rules give it, then `options[:class]`
|
|
33
|
+
# (removed from `options`, as component_classes does).
|
|
34
|
+
def slot_classes(name, options = self.options)
|
|
35
|
+
slots = self.class.variant_definitions[:slots]
|
|
36
|
+
raise ArgumentError, "#{self.class.name || self.class.inspect} has no slot #{name.inspect}" unless slots.key?(name)
|
|
37
|
+
|
|
38
|
+
merge_classes(apply_prefix(slots[name]), *variant_classes(name), options.delete(:class))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# `options` with the slot's classes, to splat into the part.
|
|
42
|
+
def slot_options(name, options = self.options)
|
|
43
|
+
classes = slot_classes(name, options)
|
|
44
|
+
classes ? options.merge(class: classes) : options
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def variant_resolution
|
|
48
|
+
@variant_resolution ||= Resolution.new(self.class, modifiers)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def variant_member_classes(resolution, slot, definitions)
|
|
52
|
+
if slot == :root
|
|
53
|
+
resolution.implied_members.map { |member| apply_prefix(modifier_map[member]) }
|
|
54
|
+
else
|
|
55
|
+
resolution.active.map { |member| apply_prefix(definitions[:slot_classes].dig(member, slot)) }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def state_classes(rule, slot, states)
|
|
60
|
+
return [] unless (classes = rule[:class][slot])
|
|
61
|
+
|
|
62
|
+
rule[:states].flat_map do |state|
|
|
63
|
+
classes.split.map { |token| "#{states.fetch(state)}:#{apply_prefix(token)}" }
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DaisyUI
|
|
4
|
+
class Variants
|
|
5
|
+
# Which members, compounds and `on` rules hold for one instance, worked
|
|
6
|
+
# out explicitly rather than left to ClassMerge, so a default or a set
|
|
7
|
+
# member never renders beside the member the caller gave, with class
|
|
8
|
+
# merging on or off:
|
|
9
|
+
#
|
|
10
|
+
# 1. the caller's members (the last one per group, as ClassMerge keeps)
|
|
11
|
+
# 2. defaults, for groups the caller left empty
|
|
12
|
+
# 3. compounds in declaration order; each one's `set` fills or replaces a
|
|
13
|
+
# member the caller did not name, and later compounds see it
|
|
14
|
+
# 4. `on` rules, against the final selection
|
|
15
|
+
#
|
|
16
|
+
# @api private
|
|
17
|
+
class Resolution
|
|
18
|
+
attr_reader :compounds, :rules
|
|
19
|
+
|
|
20
|
+
def initialize(klass, given)
|
|
21
|
+
definitions = klass.variant_definitions
|
|
22
|
+
@given = given
|
|
23
|
+
@selection = klass.modifier_groups.to_h { |group, members| [group, given.reverse.find { |m| members.include?(m) }] }
|
|
24
|
+
@named = @selection.compact.keys
|
|
25
|
+
definitions[:defaults].each { |group, member| @selection[group] ||= member }
|
|
26
|
+
@compounds = matching_compounds(definitions[:compounds])
|
|
27
|
+
@rules = definitions[:rules].select { |rule| holds?(rule[:match]) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Members a default or a compound selected, not the caller.
|
|
31
|
+
def implied_members
|
|
32
|
+
@selection.filter_map { |group, member| member unless @named.include?(group) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Every member and flag in effect, for the slots' per-member classes.
|
|
36
|
+
def active = (@given | implied_members)
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
# One at a time: a compound's `set` is seen by the compounds after it.
|
|
41
|
+
def matching_compounds(compounds)
|
|
42
|
+
compounds.each_with_object([]) do |compound, matched|
|
|
43
|
+
next unless holds?(compound[:match])
|
|
44
|
+
|
|
45
|
+
matched << compound
|
|
46
|
+
compound[:set].each { |group, member| @selection[group] = member unless @named.include?(group) }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def holds?(match) = Match.holds?(match, @selection, @given)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DaisyUI
|
|
4
|
+
class Variants
|
|
5
|
+
# `Component.variants.schema`: a component's declarations as plain,
|
|
6
|
+
# frozen data. A component that only calls `register_modifiers` gets its
|
|
7
|
+
# variants and flags, with the rest empty.
|
|
8
|
+
#
|
|
9
|
+
# @api private
|
|
10
|
+
class Schema
|
|
11
|
+
# Ungrouped modifiers and the flags registered without a class.
|
|
12
|
+
def self.flags(klass)
|
|
13
|
+
grouped = klass.modifier_groups.values.flatten
|
|
14
|
+
((klass.modifiers || {}).keys - grouped) | klass.flags
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initialize(klass)
|
|
18
|
+
@klass = klass
|
|
19
|
+
@definitions = klass.variant_definitions
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def to_h
|
|
23
|
+
Shareable.share(
|
|
24
|
+
base:,
|
|
25
|
+
variants: @klass.modifier_groups.transform_values(&:dup),
|
|
26
|
+
flags: self.class.flags(@klass),
|
|
27
|
+
defaults: @definitions[:defaults].dup,
|
|
28
|
+
compounds: @definitions[:compounds].map { |entry| { match: entry[:match], class: shown(entry[:class]), set: entry[:set] } },
|
|
29
|
+
on: @definitions[:rules].map { |entry| { states: entry[:states], match: entry[:match], class: shown(entry[:class]) } },
|
|
30
|
+
states: @definitions[:states].dup,
|
|
31
|
+
slots: { root: base, **@definitions[:slots] }
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Every class token the declarations can render, `prefix` included the
|
|
36
|
+
# way Base#apply_prefix adds it (after any state), sorted.
|
|
37
|
+
def literal_classes(prefix)
|
|
38
|
+
plain = [base, *modifier_classes, *@definitions[:slots].values, *slot_member_classes, *compound_classes]
|
|
39
|
+
plain = plain.compact.flat_map(&:split).map { |token| "#{prefix}#{token}" }
|
|
40
|
+
(plain + state_classes(prefix)).uniq.sort
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def base
|
|
46
|
+
classes = [@klass.component_class&.to_s, @definitions[:base]].compact.join(" ")
|
|
47
|
+
classes.empty? ? nil : classes
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def modifier_classes = (@klass.modifiers || {}).values
|
|
51
|
+
|
|
52
|
+
def slot_member_classes = @definitions[:slot_classes].values.flat_map(&:values)
|
|
53
|
+
|
|
54
|
+
def compound_classes = @definitions[:compounds].flat_map { |entry| entry[:class].values }
|
|
55
|
+
|
|
56
|
+
def state_classes(prefix)
|
|
57
|
+
@definitions[:rules].flat_map do |rule|
|
|
58
|
+
tokens = rule[:class].values.flat_map(&:split)
|
|
59
|
+
rule[:states].flat_map do |state|
|
|
60
|
+
tokens.map { |token| "#{@definitions[:states].fetch(state)}:#{prefix}#{token}" }
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# A root-only class shows as its String, per-slot classes as the Hash.
|
|
66
|
+
def shown(classes)
|
|
67
|
+
return nil if classes.empty?
|
|
68
|
+
|
|
69
|
+
classes.keys == [:root] ? classes[:root] : classes
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DaisyUI
|
|
4
|
+
# The declarative layer over `register_modifiers`: compound looks, defaults,
|
|
5
|
+
# browser states and parts (slots), declared once in the class body.
|
|
6
|
+
#
|
|
7
|
+
# class Widget < DaisyUI::Button
|
|
8
|
+
# variants do
|
|
9
|
+
# base "font-medium"
|
|
10
|
+
# variant :size, sm: "btn-sm [--fontsize:var(--text-sm)]"
|
|
11
|
+
# default :size, :md
|
|
12
|
+
# compound style: :outline, color: nil, class: "[--btn-bg:var(--color-base-100)]"
|
|
13
|
+
# state :hover, "hover"
|
|
14
|
+
# on :hover, style: %i[ghost link], class: "[--btn-bg:transparent]"
|
|
15
|
+
# end
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# `variant` and `flag` write through `register_modifiers` (so ClassMerge
|
|
19
|
+
# groups, inheritance and finalize! stay as they are); everything else lands
|
|
20
|
+
# in the class's `variant_definitions`, a frozen Hash replaced on every
|
|
21
|
+
# write. `Widget.variants.schema` describes it all as plain data. Matching,
|
|
22
|
+
# validation and rendering live in Variants::Match, Variants::Resolution and
|
|
23
|
+
# Variants::Rendering.
|
|
24
|
+
class Variants
|
|
25
|
+
EMPTY = Shareable.make(
|
|
26
|
+
{ base: nil, defaults: {}, compounds: [], rules: [], states: {}, slots: {}, slot_classes: {} }
|
|
27
|
+
)
|
|
28
|
+
STATE_PREFIX = /\A[^\s"'`;{}\\]+\z/
|
|
29
|
+
|
|
30
|
+
# The DSL for `klass`, with `block` evaluated against it.
|
|
31
|
+
def self.build(klass, &block)
|
|
32
|
+
new(klass).tap { |dsl| dsl.instance_exec(&block) if block }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def initialize(klass)
|
|
36
|
+
@klass = klass
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Classes every instance gets, after the inherited base.
|
|
40
|
+
def base(classes)
|
|
41
|
+
write(base: [definitions[:base], classes].compact.join(" "))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# A group of alternatives: `variant :size, sm: "btn-sm", lg: "btn-lg"`. A
|
|
45
|
+
# Hash value gives classes per slot (`sm: { root: "…", menu: "…" }`).
|
|
46
|
+
def variant(group, **members)
|
|
47
|
+
register(group => members.transform_values { |classes| root_classes(classes) })
|
|
48
|
+
members.each { |member, classes| write_slot_classes(member, classes) }
|
|
49
|
+
self
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# An on/off modifier: `flag :wide, "btn-wide"`, or a flag with no class of
|
|
53
|
+
# its own (`flag :collapsed`) that compounds and `on` rules can match.
|
|
54
|
+
def flag(name, classes = nil)
|
|
55
|
+
return tap { @klass.register_flags(name) } if classes.nil?
|
|
56
|
+
|
|
57
|
+
register(name => root_classes(classes))
|
|
58
|
+
write_slot_classes(name, classes)
|
|
59
|
+
self
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The member used when the caller names no member of `group`.
|
|
63
|
+
def default(group, member)
|
|
64
|
+
check.member!(group, member)
|
|
65
|
+
write(defaults: definitions[:defaults].merge(group => member))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Adds `class:` and sets the variants in `set:` (members the caller did
|
|
69
|
+
# not name) when every key of `match` holds. See Variants::Match.
|
|
70
|
+
def compound(**match)
|
|
71
|
+
classes = match.delete(:class)
|
|
72
|
+
set = match.delete(:set) || {}
|
|
73
|
+
raise ArgumentError, "#{owner}: compound #{match.inspect} needs class: or set:" if classes.nil? && set.empty?
|
|
74
|
+
|
|
75
|
+
check.match!(match)
|
|
76
|
+
set.each { |group, member| check.member!(group, member) }
|
|
77
|
+
add(:compounds, match:, class: slot_hash(classes), set:)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Names a Tailwind variant prefix once: `state :open, "[.dropdown[open]>&]"`.
|
|
81
|
+
def state(name, prefix)
|
|
82
|
+
raise ArgumentError, "#{owner}: state #{name.inspect} needs a Tailwind variant, got #{prefix.inspect}" unless STATE_PREFIX.match?(prefix.to_s)
|
|
83
|
+
|
|
84
|
+
write(states: definitions[:states].merge(name => prefix.to_s))
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Like compound, with every class prefixed by each of the `states`.
|
|
88
|
+
def on(states, **match)
|
|
89
|
+
states = Array(states)
|
|
90
|
+
classes = match.delete(:class)
|
|
91
|
+
raise ArgumentError, "#{owner}: on #{states.inspect} needs class:" if classes.nil?
|
|
92
|
+
|
|
93
|
+
states.each { |name| check.state!(name) }
|
|
94
|
+
check.match!(match)
|
|
95
|
+
add(:rules, states:, match:, class: slot_hash(classes))
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# A part of the component (`root` is the component itself), with the
|
|
99
|
+
# classes it always gets. Read with Base#slot_classes / #slot_options.
|
|
100
|
+
def slot(name, classes = nil)
|
|
101
|
+
raise ArgumentError, "#{owner}: :root is the component itself, not a slot to declare" if name == :root
|
|
102
|
+
|
|
103
|
+
write(slots: definitions[:slots].merge(name => classes))
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Plain, frozen data describing the component, for docs pages, galleries
|
|
107
|
+
# and design-tool parity checks.
|
|
108
|
+
def schema
|
|
109
|
+
Schema.new(@klass).to_h
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Every class the component's declarations can render, prefixed as
|
|
113
|
+
# DaisyUI.configuration.prefix renders them: what Tailwind needs to see.
|
|
114
|
+
def literal_classes(prefix: nil)
|
|
115
|
+
Schema.new(@klass).literal_classes(prefix.to_s)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
def definitions = @klass.variant_definitions
|
|
121
|
+
|
|
122
|
+
def owner = @klass.name || @klass.inspect
|
|
123
|
+
|
|
124
|
+
def check = Match.new(@klass)
|
|
125
|
+
|
|
126
|
+
def write(**changes)
|
|
127
|
+
@klass.variant_definitions = definitions.merge(changes)
|
|
128
|
+
self
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def add(key, **entry)
|
|
132
|
+
write(key => [*definitions[key], entry])
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def register(modifiers)
|
|
136
|
+
@klass.register_modifiers(modifiers)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def root_classes(classes)
|
|
140
|
+
classes.is_a?(Hash) ? slot_hash(classes)[:root].to_s : classes.to_s
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Slot parts of a member, kept apart from `modifiers`, which only ever
|
|
144
|
+
# holds the root's String.
|
|
145
|
+
def write_slot_classes(member, classes)
|
|
146
|
+
parts = classes.is_a?(Hash) ? slot_hash(classes).except(:root) : {}
|
|
147
|
+
slot_classes = definitions[:slot_classes].except(member)
|
|
148
|
+
write(slot_classes: parts.empty? ? slot_classes : slot_classes.merge(member => parts))
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# `"a b"` or `{ root: "a", menu: "b" }` as `{ slot => classes }`.
|
|
152
|
+
def slot_hash(classes)
|
|
153
|
+
return {} if classes.nil?
|
|
154
|
+
return { root: classes.to_s } unless classes.is_a?(Hash)
|
|
155
|
+
|
|
156
|
+
classes.each_key { |slot| check.slot!(slot) }
|
|
157
|
+
classes.transform_values(&:to_s)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
data/lib/daisy_ui/version.rb
CHANGED
data/lib/daisy_ui.rb
CHANGED
|
@@ -14,6 +14,8 @@ loader.ignore("#{__dir__}/daisy_ui/updated_at.rb")
|
|
|
14
14
|
# The Rails engine references Rails::Engine, so it is required explicitly below
|
|
15
15
|
# (only when Rails is present) rather than autoloaded.
|
|
16
16
|
loader.ignore("#{__dir__}/daisy_ui/engine.rb")
|
|
17
|
+
# lib/tasks holds the engine's rake tasks, no constants.
|
|
18
|
+
loader.ignore("#{__dir__}/tasks")
|
|
17
19
|
loader.setup # ready!
|
|
18
20
|
loader.load_file("#{__dir__}/daisy_ui/base.rb")
|
|
19
21
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Loaded by the Rails engine (Rails loads an engine's lib/tasks/*.rake).
|
|
4
|
+
namespace :daisyui do
|
|
5
|
+
desc "Write the Tailwind sources of the app's own DaisyUI components to OUTPUT (stdout without one)"
|
|
6
|
+
task :tailwind, [:output] => :environment do |_task, args|
|
|
7
|
+
argv = ["--app"]
|
|
8
|
+
argv.push("--output", args[:output]) if args[:output]
|
|
9
|
+
status = DaisyUI::Tailwind::Command.run(argv)
|
|
10
|
+
exit(status) unless status.zero?
|
|
11
|
+
end
|
|
12
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: daisyui
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mikael Henriksson
|
|
@@ -128,6 +128,7 @@ files:
|
|
|
128
128
|
- lib/daisy_ui/loading.rb
|
|
129
129
|
- lib/daisy_ui/mask.rb
|
|
130
130
|
- lib/daisy_ui/mcp_server.rb
|
|
131
|
+
- lib/daisy_ui/mcp_server/variants_guide.rb
|
|
131
132
|
- lib/daisy_ui/megamenu.rb
|
|
132
133
|
- lib/daisy_ui/menu.rb
|
|
133
134
|
- lib/daisy_ui/menu_item.rb
|
|
@@ -161,6 +162,7 @@ files:
|
|
|
161
162
|
- lib/daisy_ui/table_row.rb
|
|
162
163
|
- lib/daisy_ui/tabs.rb
|
|
163
164
|
- lib/daisy_ui/tailwind.rb
|
|
165
|
+
- lib/daisy_ui/tailwind/app_sources.rb
|
|
164
166
|
- lib/daisy_ui/tailwind/candidate.rb
|
|
165
167
|
- lib/daisy_ui/tailwind/command.rb
|
|
166
168
|
- lib/daisy_ui/tailwind/lexer.rb
|
|
@@ -179,8 +181,14 @@ files:
|
|
|
179
181
|
- lib/daisy_ui/unknown_modifier_error.rb
|
|
180
182
|
- lib/daisy_ui/updated_at.rb
|
|
181
183
|
- lib/daisy_ui/validator.rb
|
|
184
|
+
- lib/daisy_ui/variants.rb
|
|
185
|
+
- lib/daisy_ui/variants/match.rb
|
|
186
|
+
- lib/daisy_ui/variants/rendering.rb
|
|
187
|
+
- lib/daisy_ui/variants/resolution.rb
|
|
188
|
+
- lib/daisy_ui/variants/schema.rb
|
|
182
189
|
- lib/daisy_ui/version.rb
|
|
183
190
|
- lib/daisyui.rb
|
|
191
|
+
- lib/tasks/daisyui.rake
|
|
184
192
|
homepage: https://github.com/mhenrixon/daisyui
|
|
185
193
|
licenses:
|
|
186
194
|
- MIT
|