daisyui 2.0.1 → 2.1.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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +78 -1
  3. data/README.md +81 -1
  4. data/app/javascript/daisy_ui/controllers/daisy_dropdown_controller.min.js +4 -0
  5. data/app/javascript/daisy_ui/controllers/daisy_dropdown_controller.min.js.map +10 -0
  6. data/app/javascript/daisy_ui/controllers/daisy_tooltip_controller.min.js +4 -0
  7. data/app/javascript/daisy_ui/controllers/daisy_tooltip_controller.min.js.map +10 -0
  8. data/config/importmap.rb +12 -3
  9. data/lib/daisy_ui/base.rb +87 -9
  10. data/lib/daisy_ui/chat.rb +1 -0
  11. data/lib/daisy_ui/checkbox.rb +1 -0
  12. data/lib/daisy_ui/class_merge/configuration.rb +18 -3
  13. data/lib/daisy_ui/class_merge/daisy_groups.rb +8 -5
  14. data/lib/daisy_ui/class_merge/tailwind_config.rb +8 -18
  15. data/lib/daisy_ui/class_merge/validators.rb +4 -0
  16. data/lib/daisy_ui/class_merge.rb +23 -12
  17. data/lib/daisy_ui/collapsible_sub_menu.rb +1 -0
  18. data/lib/daisy_ui/configurable/report_log.rb +36 -0
  19. data/lib/daisy_ui/configurable.rb +129 -5
  20. data/lib/daisy_ui/dropdown.rb +1 -0
  21. data/lib/daisy_ui/engine.rb +26 -9
  22. data/lib/daisy_ui/indicator.rb +1 -0
  23. data/lib/daisy_ui/mcp_server.rb +1 -0
  24. data/lib/daisy_ui/modal.rb +1 -0
  25. data/lib/daisy_ui/shareable.rb +56 -0
  26. data/lib/daisy_ui/steps.rb +1 -0
  27. data/lib/daisy_ui/store.rb +71 -0
  28. data/lib/daisy_ui/sub_menu.rb +2 -0
  29. data/lib/daisy_ui/tab.rb +6 -0
  30. data/lib/daisy_ui/tab_with_content.rb +1 -0
  31. data/lib/daisy_ui/tab_without_content.rb +1 -0
  32. data/lib/daisy_ui/tooltip.rb +1 -0
  33. data/lib/daisy_ui/unknown_modifier_error.rb +31 -0
  34. data/lib/daisy_ui/updated_at.rb +1 -1
  35. data/lib/daisy_ui/version.rb +1 -1
  36. metadata +9 -1
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "monitor"
4
-
5
3
  module DaisyUI
6
4
  # Merges CSS class lists so that later classes win over earlier conflicting
7
5
  # ones, for Tailwind utilities and daisyUI modifiers alike:
@@ -16,10 +14,10 @@ module DaisyUI
16
14
  module ClassMerge
17
15
  TAILWIND_MERGE_VERSION = "1.5.6"
18
16
 
19
- # Reentrant, so a reset! triggered while the merger is being built (e.g. a
20
- # component file configuring DaisyUI as it loads) cannot deadlock.
21
- @monitor = Monitor.new
22
-
17
+ # The merger (with its LRU) and the Tailwind class groups live in
18
+ # DaisyUI::Store: one per Ractor, built on first use under the store's
19
+ # reentrant lock, so a reset! triggered while the merger is being built
20
+ # (e.g. a component file configuring DaisyUI as it loads) cannot deadlock.
23
21
  class << self
24
22
  # Accepts strings, nested arrays, nil and false (dropped, as Phlex does
25
23
  # for `class:`). Returns a frozen String, "" when empty.
@@ -27,20 +25,33 @@ module DaisyUI
27
25
  merger.merge(parts.flatten.select(&:itself).join(" "))
28
26
  end
29
27
 
28
+ # Each Ractor's merger remembers the configuration it was built from
29
+ # and is rebuilt once DaisyUI.configure publishes another, also in a
30
+ # Ractor where reset! never ran.
30
31
  def merger
31
- @merger || @monitor.synchronize do
32
- @merger ||= Merger.new(config: DaisyUI.configuration.class_merge.merger_config)
33
- end
32
+ configuration = DaisyUI.configuration
33
+ built_for, merger = Store.fetch(:daisy_ui_class_merger) { build_merger(configuration) }
34
+ return merger if built_for.equal?(configuration)
35
+
36
+ Store.delete(:daisy_ui_class_merger)
37
+ Store.fetch(:daisy_ui_class_merger) { build_merger(configuration) }.last
34
38
  end
35
39
 
36
40
  # @api private The stock Tailwind class groups, for classifying tokens.
37
41
  def tailwind_utils
38
- @tailwind_utils ||= ClassGroupUtils.new(TailwindConfig::DEFAULTS)
42
+ Store.fetch(:daisy_ui_tailwind_utils) { ClassGroupUtils.new(TailwindConfig::DEFAULTS) }
39
43
  end
40
44
 
41
- # Drops the memoized merger so the next merge picks up configuration changes.
45
+ # Drops the current Ractor's merger so its next merge picks up
46
+ # configuration changes.
42
47
  def reset!
43
- @monitor.synchronize { @merger = nil }
48
+ Store.delete(:daisy_ui_class_merger)
49
+ end
50
+
51
+ private
52
+
53
+ def build_merger(configuration)
54
+ [configuration, Merger.new(config: configuration.class_merge.merger_config(configuration))]
44
55
  end
45
56
  end
46
57
  end
@@ -4,6 +4,7 @@ module DaisyUI
4
4
  # @private
5
5
  class CollapsibleSubMenu < Base
6
6
  self.component_class = nil
7
+ register_flags :collapsible, :open
7
8
 
8
9
  def initialize(*, **)
9
10
  super
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DaisyUI
4
+ module Configurable
5
+ # @api private The unknown modifiers already reported, so :warn and a
6
+ # callable fire once per key. Remembers up to `limit` keys, then starts
7
+ # over, so modifiers built from user input cannot grow it without bound.
8
+ # One per Ractor (DaisyUI::Store), so its Mutex is never shared.
9
+ class ReportLog
10
+ def initialize(limit)
11
+ @limit = limit
12
+ @keys = Set.new
13
+ @lock = Mutex.new
14
+ end
15
+
16
+ # True the first time a key is seen.
17
+ def first?(key)
18
+ @lock.synchronize do
19
+ return false if @keys.include?(key)
20
+
21
+ @keys.clear if @keys.size >= @limit
22
+ @keys.add(key)
23
+ true
24
+ end
25
+ end
26
+
27
+ def clear
28
+ @lock.synchronize { @keys.clear }
29
+ end
30
+
31
+ def size
32
+ @lock.synchronize { @keys.size }
33
+ end
34
+ end
35
+ end
36
+ end
@@ -2,15 +2,45 @@
2
2
 
3
3
  module DaisyUI
4
4
  module Configurable
5
+ # Created when DaisyUI extends this module, at load, so `configuration` is
6
+ # a plain read with no check-then-set race.
7
+ def self.extended(base)
8
+ base.instance_variable_set(:@configuration, Configuration.new)
9
+ end
10
+
11
+ # Yields a writable copy of the configuration, then finalizes the copy
12
+ # (see finalize!) and publishes it. Calling configure again keeps
13
+ # working, no reader (another thread, a non-main Ractor) meets a
14
+ # half-changed or writable configuration, and a block that raises
15
+ # publishes nothing. Calls are serialized, so each starts from what the
16
+ # previous one published.
5
17
  def configure
6
- self.configuration ||= Configuration.new
7
- yield(configuration) if block_given?
8
- ClassMerge.reset!
9
- configuration
18
+ configure_lock.synchronize do
19
+ draft = configuration.dup
20
+ yield(draft) if block_given?
21
+ @configuration = draft.finalize!
22
+ ClassMerge.reset!
23
+ finalize!
24
+ configuration
25
+ end
10
26
  end
11
27
 
12
28
  def configuration
13
- @configuration ||= Configuration.new
29
+ @configuration
30
+ end
31
+
32
+ # Freezes what a non-main Ractor reads (this configuration and every
33
+ # component's registries) so DaisyUI::ClassMerge.merge runs in any Ractor.
34
+ # configure calls it, and the Rails engine calls it once the app has
35
+ # booted; an app with neither calls it at the end of boot. Afterwards
36
+ # change settings only through configure: writing to the frozen
37
+ # configuration raises. A configured value that cannot be made shareable
38
+ # is kept as it is (see Shareable.share), never raised on.
39
+ def finalize!
40
+ ClassMerge::DaisyGroups.components.each(&:finalize!)
41
+ configuration.finalize!
42
+ ClassMerge.tailwind_utils
43
+ self
14
44
  end
15
45
 
16
46
  class Modifiers
@@ -19,6 +49,19 @@ module DaisyUI
19
49
  @groups = {}
20
50
  end
21
51
 
52
+ def initialize_copy(source)
53
+ super
54
+ @modifiers = @modifiers.transform_values(&:dup)
55
+ @groups = @groups.dup
56
+ end
57
+
58
+ # @api private Called by DaisyUI.finalize!.
59
+ def finalize!
60
+ Shareable.share(@modifiers)
61
+ Shareable.share(@groups)
62
+ freeze
63
+ end
64
+
22
65
  # `group:` makes the modifier an alternative to the component's other
23
66
  # modifiers in that group, e.g. `add(:huge, classes: "btn-xl",
24
67
  # component: DaisyUI::Button, group: :size)`.
@@ -52,12 +95,67 @@ module DaisyUI
52
95
  end
53
96
 
54
97
  class Configuration
98
+ UNKNOWN_MODIFIER_MODES = %i[raise warn ignore].freeze
99
+ REPORTED_UNKNOWN_MODIFIERS_LIMIT = 1_000
100
+
55
101
  attr_reader :prefix
56
102
 
57
103
  def initialize
58
104
  @prefix = nil
59
105
  end
60
106
 
107
+ def initialize_copy(source)
108
+ super
109
+ @class_merge = @class_merge&.dup
110
+ @modifiers = @modifiers&.dup
111
+ end
112
+
113
+ # @api private Called by DaisyUI.configure and DaisyUI.finalize!. A
114
+ # callable unknown_modifiers that cannot be made shareable (a lambda
115
+ # that closes over app state, an object holding a logger) is kept: the
116
+ # configuration is then frozen but readable on the main Ractor only.
117
+ def finalize!
118
+ class_merge.finalize!
119
+ modifiers.finalize!
120
+ Shareable.share(@prefix)
121
+ Shareable.share(@unknown_modifiers)
122
+ freeze
123
+ end
124
+
125
+ # What a component does with a positional modifier it does not know:
126
+ # :raise, :warn (once per component and modifier), :ignore, or a callable
127
+ # that receives the DaisyUI::UnknownModifierError (once per component and
128
+ # modifier), e.g. `->(error) { Appsignal.report_error(error) }`.
129
+ #
130
+ # Defaults to :raise under Rails in development and test, :warn otherwise.
131
+ def unknown_modifiers
132
+ @unknown_modifiers || default_unknown_modifiers
133
+ end
134
+
135
+ def unknown_modifiers=(mode)
136
+ valid = mode.nil? || mode.respond_to?(:call) || UNKNOWN_MODIFIER_MODES.include?(mode)
137
+ raise ArgumentError, "unknown_modifiers must be :raise, :warn, :ignore or a callable, got #{mode.inspect}" unless valid
138
+
139
+ @unknown_modifiers = mode
140
+ end
141
+
142
+ # @api private Builds the error only when it will be raised or reported.
143
+ def report_unknown_modifier(component:, modifier:, known:, part: nil)
144
+ mode = unknown_modifiers
145
+ return if mode == :ignore
146
+
147
+ error = -> { UnknownModifierError.new(component:, modifier:, known:, part:) }
148
+ raise error.call if mode == :raise
149
+ return unless first_report?([component, part, modifier])
150
+
151
+ mode == :warn ? warn("[DaisyUI] #{error.call.message}") : mode.call(error.call)
152
+ end
153
+
154
+ # @api private
155
+ def reset_reported_unknown_modifiers!
156
+ reported_unknown_modifiers.clear
157
+ end
158
+
61
159
  def prefix=(prefix)
62
160
  @prefix = prefix
63
161
  ClassMerge.reset!
@@ -70,6 +168,32 @@ module DaisyUI
70
168
  def modifiers
71
169
  @modifiers ||= Modifiers.new
72
170
  end
171
+
172
+ private
173
+
174
+ def first_report?(key)
175
+ reported_unknown_modifiers.first?(key)
176
+ end
177
+
178
+ # What was already reported lives per Ractor, outside the configuration,
179
+ # which stays shareable.
180
+ def reported_unknown_modifiers
181
+ Store.fetch(:daisy_ui_reported_unknown_modifiers) { ReportLog.new(REPORTED_UNKNOWN_MODIFIERS_LIMIT) }
182
+ end
183
+
184
+ def default_unknown_modifiers
185
+ return :warn unless defined?(::Rails) && ::Rails.respond_to?(:env)
186
+
187
+ %w[development test].include?(::Rails.env.to_s) ? :raise : :warn
188
+ end
189
+ end
190
+
191
+ private
192
+
193
+ # Reentrant, and one per Ractor (DaisyUI::Store) so it is never shared;
194
+ # configure writes DaisyUI's ivar, so only the main Ractor gets that far.
195
+ def configure_lock
196
+ Store.fetch(:daisy_ui_configure_lock) { Monitor.new }
73
197
  end
74
198
  end
75
199
  end
@@ -5,6 +5,7 @@ require "securerandom"
5
5
  module DaisyUI
6
6
  class Dropdown < Base
7
7
  self.component_class = :dropdown
8
+ register_flags :popover, :tap_to_close
8
9
 
9
10
  # Placement modifiers that, in :popover mode, must live on the popover
10
11
  # element itself (DaisyUI's `position-area` rule is on `.dropdown`).
@@ -4,26 +4,43 @@ module DaisyUI
4
4
  # Optional Rails integration. Loaded only when Rails::Engine is defined (see
5
5
  # lib/daisy_ui.rb), so the gem stays a plain Phlex library outside Rails.
6
6
  #
7
- # It exposes the gem's bundled JavaScript (the opt-in `daisy-dropdown`
8
- # Stimulus controller) to the host app:
9
- # * appends the JS dir to importmap-rails' paths and auto-pins it, so
10
- # consumers get `daisy_ui/controllers/daisy_dropdown_controller` with no
11
- # manual pin;
7
+ # It exposes the gem's bundled Stimulus controllers (`daisy-dropdown`,
8
+ # `daisy-tooltip`) to the host app:
9
+ # * adds config/importmap.rb to importmap-rails' paths, which pins each
10
+ # `daisy_ui/controllers/<name>` to its minified build (no manual pin);
12
11
  # * appends the JS dir to the asset load path so Propshaft/Sprockets serve
13
- # the file.
12
+ # the .min.js and its sourcemap (Sprockets also needs them precompiled).
14
13
  #
15
- # The controller is opt-in at the markup level (`Dropdown(:popover, stimulus:
16
- # true)`), so apps that never opt in ship no extra behavior.
14
+ # The controllers are opt-in at the markup level (`Dropdown(:popover, stimulus:
15
+ # true)`, `Tooltip(:popover)`) and not preloaded, so a page that never uses
16
+ # one never fetches it.
17
17
  # Asset-only engine: no isolate_namespace, since it exposes no routes, models,
18
18
  # or helpers — only static JavaScript — and namespacing would risk surprises in
19
19
  # the host app for no benefit.
20
20
  class Engine < ::Rails::Engine
21
21
  JAVASCRIPT_PATH = root.join("app/javascript")
22
+ # The minified controllers config/importmap.rb pins, and their sourcemaps.
23
+ MINIFIED_ASSETS = Dir[JAVASCRIPT_PATH.join("daisy_ui/controllers/*.min.js{,.map}").to_s]
24
+ .map { |path| Pathname.new(path).relative_path_from(JAVASCRIPT_PATH).to_s }.sort.freeze
22
25
 
23
26
  initializer "daisy_ui.assets" do |app|
24
- app.config.assets.paths << JAVASCRIPT_PATH.to_s if app.config.respond_to?(:assets)
27
+ next unless app.config.respond_to?(:assets)
28
+
29
+ app.config.assets.paths << JAVASCRIPT_PATH.to_s
30
+ # Sprockets compiles only what is listed; Propshaft compiles every file in
31
+ # paths. The list is nil unless Sprockets or another engine created it,
32
+ # and OrderedOptions answers respond_to? for any name, so check the value.
33
+ precompile = app.config.assets.precompile
34
+ precompile.concat(MINIFIED_ASSETS) if precompile.is_a?(Array)
25
35
  end
26
36
 
37
+ # Once the app has booted, freeze the configuration and component
38
+ # registries so a non-main Ractor can merge classes. DaisyUI.configure in
39
+ # an initializer already finalized; this also covers an app that never
40
+ # calls it and the components loaded during boot (eager loading).
41
+ # DaisyUI.configure keeps working afterwards, on a copy.
42
+ config.after_initialize { DaisyUI.finalize! }
43
+
27
44
  initializer "daisy_ui.importmap", before: "importmap" do |app|
28
45
  next unless app.config.respond_to?(:importmap)
29
46
 
@@ -19,6 +19,7 @@ module DaisyUI
19
19
  private
20
20
 
21
21
  def build_item_classes(item_modifiers, options)
22
+ report_unknown_modifiers(item_modifiers, modifier_map.keys, part: :item)
22
23
  # Start with base class
23
24
  base = apply_prefix("indicator-item")
24
25
  # Add modifier classes from position modifiers
@@ -39,6 +39,7 @@ module DaisyUI
39
39
  }
40
40
  }
41
41
  ].freeze
42
+ Shareable.make(TOOLS) # the Hashes inside too
42
43
 
43
44
  EXCLUDED_CONSTANTS = %i[Base Configurable Configuration Modifiers VERSION UPDATED_AT McpServer].freeze
44
45
 
@@ -3,6 +3,7 @@
3
3
  module DaisyUI
4
4
  class Modal < Base
5
5
  self.component_class = :modal
6
+ register_flags :tap_outside_to_close
6
7
 
7
8
  def initialize(*, id:, **)
8
9
  super
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DaisyUI
4
+ # @api private Deep-freezes a value so a non-main Ractor can read it from a
5
+ # constant or a class ivar: `Ractor.make_shareable`, or, on engines without
6
+ # Ractor (JRuby, TruffleRuby), a walk freezing Hash values and Array elements.
7
+ module Shareable
8
+ class << self
9
+ # For the gem's own literals, which must all be shareable: raises if one
10
+ # is not.
11
+ def make(value)
12
+ ractor? ? ::Ractor.make_shareable(value) : deep_freeze(value)
13
+ end
14
+
15
+ # For what an app configures. Freezes Hashes, Arrays and Strings all the
16
+ # way down, makes a capture-free Proc shareable, and keeps anything else
17
+ # (a Proc that closes over app state, a logger) as it is, so whatever
18
+ # holds it is readable on the main Ractor only. Never raises, and never
19
+ # hands an object it does not know to Ractor.make_shareable, which
20
+ # freezes as it goes and would leave a logger frozen before failing.
21
+ def share(value)
22
+ case value
23
+ when Hash
24
+ value.each do |key, member|
25
+ share(key)
26
+ share(member)
27
+ end.freeze
28
+ when Array then value.each { |member| share(member) }.freeze
29
+ when String then value.freeze
30
+ when Proc then share_proc(value)
31
+ else value
32
+ end
33
+ end
34
+
35
+ def ractor?
36
+ defined?(::Ractor) ? ::Ractor.respond_to?(:make_shareable) : false
37
+ end
38
+
39
+ private
40
+
41
+ def share_proc(callable)
42
+ ractor? ? ::Ractor.make_shareable(callable) : callable
43
+ rescue ::Ractor::Error, ArgumentError
44
+ callable
45
+ end
46
+
47
+ def deep_freeze(value)
48
+ case value
49
+ when Hash then value.each_value { |member| deep_freeze(member) }
50
+ when Array then value.each { |member| deep_freeze(member) }
51
+ end
52
+ value.freeze
53
+ end
54
+ end
55
+ end
56
+ end
@@ -27,6 +27,7 @@ module DaisyUI
27
27
  private
28
28
 
29
29
  def build_step_classes(step_modifiers, options)
30
+ report_unknown_modifiers(step_modifiers, step_modifier_map.keys, part: :step)
30
31
  base = apply_prefix("step")
31
32
  mod_classes = step_modifiers.filter_map { |m| apply_prefix(step_modifier_map[m]) }
32
33
  custom = options.delete(:class)
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
4
+
5
+ module DaisyUI
6
+ # @api private Per-Ractor lazy storage for caches that must not live in a
7
+ # class ivar or a constant (the class-merge merger, the Tailwind class
8
+ # groups, the unknown-modifier report log):
9
+ #
10
+ # DaisyUI::Store.fetch(:daisy_ui_merger) { Merger.new(...) }
11
+ # DaisyUI::Store.delete(:daisy_ui_merger)
12
+ #
13
+ # Adapted from conformance-kit's ConformanceKit::Store (MIT, same author),
14
+ # which daisyui cannot depend on at runtime, plus `delete`. Every Ractor has
15
+ # its own Monitor-guarded table: reentrant, so an initializer may fetch or
16
+ # delete other keys. On Ruby 3.4+ the table comes from
17
+ # `Ractor.store_if_absent`. Below 3.4 the main Ractor's table is created at
18
+ # load (never lazily, so there is no check-then-set race), and a non-main
19
+ # Ractor keeps its own in Ractor-local storage.
20
+ module Store
21
+ # One Ractor's values. Reads skip the lock once a key is stored.
22
+ class Table
23
+ def initialize
24
+ @monitor = Monitor.new
25
+ @values = {}
26
+ end
27
+
28
+ def fetch(key)
29
+ @values.fetch(key) do
30
+ @monitor.synchronize { @values.fetch(key) { @values[key] = yield } }
31
+ end
32
+ end
33
+
34
+ def delete(key)
35
+ @monitor.synchronize { @values.delete(key) }
36
+ end
37
+ end
38
+
39
+ class << self
40
+ def fetch(key, &init)
41
+ raise ArgumentError, "DaisyUI::Store.fetch(#{key.inspect}) needs an initializer block" unless init
42
+
43
+ table.fetch(key, &init)
44
+ end
45
+
46
+ def delete(key)
47
+ table.delete(key)
48
+ end
49
+
50
+ def main_ractor?
51
+ !defined?(::Ractor) || ::Ractor.current == ::Ractor.main
52
+ end
53
+
54
+ def native?
55
+ defined?(::Ractor) ? ::Ractor.respond_to?(:store_if_absent) : false
56
+ end
57
+
58
+ private
59
+
60
+ def table
61
+ return ::Ractor.store_if_absent(:daisy_ui_store) { Table.new } if native?
62
+ return ::Ractor.current[:daisy_ui_store] ||= Table.new unless main_ractor?
63
+
64
+ # Set once below, at load, below Ruby 3.4; only the main Ractor reads it.
65
+ @main_table # rubocop:disable ThreadSafety/ClassInstanceVariable
66
+ end
67
+ end
68
+
69
+ @main_table = Table.new unless native?
70
+ end
71
+ end
@@ -3,6 +3,8 @@
3
3
  module DaisyUI
4
4
  # @private
5
5
  class SubMenu < Base
6
+ register_flags :open
7
+
6
8
  def initialize(*, **)
7
9
  super
8
10
  @items = []
data/lib/daisy_ui/tab.rb CHANGED
@@ -36,5 +36,11 @@ module DaisyUI
36
36
  div role: :tabpanel, class: content_classes, **options, &block
37
37
  end
38
38
  end
39
+
40
+ private
41
+
42
+ # Tab only dispatches: TabWithContent or TabWithoutContent validates the
43
+ # modifiers when it is built.
44
+ def validate_modifiers; end
39
45
  end
40
46
  end
@@ -4,6 +4,7 @@ module DaisyUI
4
4
  # @private
5
5
  class TabWithContent < Base
6
6
  self.component_class = :tab
7
+ register_flags :open, :closed
7
8
 
8
9
  def initialize(*, content:, label: nil, id: nil, **)
9
10
  super(*, **)
@@ -4,6 +4,7 @@ module DaisyUI
4
4
  # @private
5
5
  class TabWithoutContent < Base
6
6
  self.component_class = :tab
7
+ register_flags :open, :closed
7
8
 
8
9
  def initialize(*, label: nil, **)
9
10
  super(*, **)
@@ -5,6 +5,7 @@ require "securerandom"
5
5
  module DaisyUI
6
6
  class Tooltip < Base
7
7
  self.component_class = :tooltip
8
+ register_flags :popover
8
9
 
9
10
  PLACEMENTS = %i[top bottom left right].freeze
10
11
  DEFAULT_STIMULUS_IDENTIFIER = "daisy-tooltip"
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "did_you_mean"
4
+
5
+ module DaisyUI
6
+ # Raised (or warned, or reported) when a component is given a positional
7
+ # modifier it does not know. See `DaisyUI.configuration.unknown_modifiers`.
8
+ class UnknownModifierError < ArgumentError
9
+ attr_reader :component, :modifier, :part
10
+
11
+ # `part` names the sub-component method (e.g. :bubble for Chat#bubble).
12
+ def initialize(component:, modifier:, known:, part: nil)
13
+ @component = component
14
+ @modifier = modifier
15
+ @part = part
16
+ super(build_message(known))
17
+ end
18
+
19
+ private
20
+
21
+ def build_message(known)
22
+ name = component.name || component.inspect
23
+ name = "#{name}##{part}" if part
24
+ message = "#{name} has no modifier #{modifier.inspect}"
25
+ suggestions = DidYouMean::SpellChecker.new(dictionary: known.map(&:to_s)).correct(modifier.to_s)
26
+ return message if suggestions.empty?
27
+
28
+ "#{message}. Did you mean? #{suggestions.map { |s| ":#{s}" }.join(', ')}"
29
+ end
30
+ end
31
+ end
@@ -3,5 +3,5 @@
3
3
  module DaisyUI
4
4
  # This timestamp is automatically updated when releasing a new version
5
5
  # Format: YYYY-MM-DD HH:MM:SS UTC
6
- UPDATED_AT = "2026-10-09 17:02:17 UTC"
6
+ UPDATED_AT = "2026-10-15 20:45:37 UTC"
7
7
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DaisyUI
4
- VERSION = "2.0.1"
4
+ VERSION = "2.1.0"
5
5
  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.0.1
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson
@@ -63,7 +63,11 @@ files:
63
63
  - LICENSE.txt
64
64
  - README.md
65
65
  - app/javascript/daisy_ui/controllers/daisy_dropdown_controller.js
66
+ - app/javascript/daisy_ui/controllers/daisy_dropdown_controller.min.js
67
+ - app/javascript/daisy_ui/controllers/daisy_dropdown_controller.min.js.map
66
68
  - app/javascript/daisy_ui/controllers/daisy_tooltip_controller.js
69
+ - app/javascript/daisy_ui/controllers/daisy_tooltip_controller.min.js
70
+ - app/javascript/daisy_ui/controllers/daisy_tooltip_controller.min.js.map
67
71
  - config/importmap.rb
68
72
  - exe/daisyui-mcp
69
73
  - lib/daisy_ui.rb
@@ -94,6 +98,7 @@ files:
94
98
  - lib/daisy_ui/collapse.rb
95
99
  - lib/daisy_ui/collapsible_sub_menu.rb
96
100
  - lib/daisy_ui/configurable.rb
101
+ - lib/daisy_ui/configurable/report_log.rb
97
102
  - lib/daisy_ui/countdown.rb
98
103
  - lib/daisy_ui/diff.rb
99
104
  - lib/daisy_ui/divider.rb
@@ -137,11 +142,13 @@ files:
137
142
  - lib/daisy_ui/range.rb
138
143
  - lib/daisy_ui/rating.rb
139
144
  - lib/daisy_ui/select.rb
145
+ - lib/daisy_ui/shareable.rb
140
146
  - lib/daisy_ui/skeleton.rb
141
147
  - lib/daisy_ui/stack.rb
142
148
  - lib/daisy_ui/stat.rb
143
149
  - lib/daisy_ui/status.rb
144
150
  - lib/daisy_ui/steps.rb
151
+ - lib/daisy_ui/store.rb
145
152
  - lib/daisy_ui/sub_menu.rb
146
153
  - lib/daisy_ui/swap.rb
147
154
  - lib/daisy_ui/tab.rb
@@ -157,6 +164,7 @@ files:
157
164
  - lib/daisy_ui/toast.rb
158
165
  - lib/daisy_ui/toggle.rb
159
166
  - lib/daisy_ui/tooltip.rb
167
+ - lib/daisy_ui/unknown_modifier_error.rb
160
168
  - lib/daisy_ui/updated_at.rb
161
169
  - lib/daisy_ui/validator.rb
162
170
  - lib/daisy_ui/version.rb