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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d30cb25a7b084c35be2de937df6e73bb3700a2f5acfb4c985c5e7cb4824cdda0
4
- data.tar.gz: e5527dde4ac27befb95a4073a1facbfe9a19a1552390d0236ba40a7495efbb02
3
+ metadata.gz: 41fa98f82c0377c4615fa5e4a1091ae8383688a951acb726122866cefa273665
4
+ data.tar.gz: 651713886837f46040942fc397089d1194dc35787ab1ba18308db97f12d94a06
5
5
  SHA512:
6
- metadata.gz: 4db77994d5bf816005c30bcac72f43b9e0a699b47d5c58963fca047d63a0d51618df468db7e07e09961efffbdb7f8656da5490765180c59424534f87953b74ff
7
- data.tar.gz: 27c490791fa5006f8670df8f69159c978bad805a93a30dd65740ca6ab6202b76cd92af02ce03599eb1398a9465e66c744172f30de6ad6ff429265935bf2f103e
6
+ metadata.gz: be24df2be52d01adbda5f9e257650d788352a0dfd286bee5c734dfce66aee0377dd0ad601269363ffd734d908cd0c2cde612062017dc59fde61086f4f53e716c
7
+ data.tar.gz: 2320af028c2539b849de1feb7c1d2690cc6ca0e75da24bd7d838415171c4d922f59b26f5833e1cc1a1e035c330ce532eaae75e21107bd89119f24653f7ab3eb4
data/CHANGELOG.md CHANGED
@@ -1,4 +1,73 @@
1
- ## [Unreleased]
1
+ ## [2.1.0]
2
+
3
+ ### DaisyUI finalizes itself at boot: class merging in any Ractor (#58)
4
+
5
+ At boot DaisyUI now freezes its configuration and every component's
6
+ registries, so a non-main Ractor can read them. `DaisyUI::ClassMerge.merge`
7
+ then works in any Ractor; each Ractor builds its own merger and LRU.
8
+
9
+ - Automatic: `DaisyUI.configure` finalizes at its end, and the Rails engine
10
+ finalizes again in `after_initialize`, with no app code. A Rails-less app
11
+ that never calls `configure` calls `DaisyUI.finalize!` at the end of boot.
12
+ - **The configuration is frozen after `DaisyUI.configure` (and, in a Rails
13
+ app, after boot).** Writing to `DaisyUI.configuration` directly now raises
14
+ `FrozenError`, e.g. `DaisyUI.configuration.unknown_modifiers = :ignore` in a
15
+ spec: use `DaisyUI.configure { |config| config.unknown_modifiers = :ignore }`.
16
+ `DaisyUI.configure` can be called again at any time: it yields a writable
17
+ copy and publishes it once that is finalized, so no reader ever meets a
18
+ half-changed configuration, and a block that raises changes nothing. A
19
+ Ractor that already built its merger rebuilds it from the new
20
+ configuration.
21
+ - Finalizing never raises. A configured value that cannot be made shareable
22
+ is kept as it is, unfrozen, and the configuration is then readable on the
23
+ main Ractor only: a callable `unknown_modifiers` or a raw `class_merge`
24
+ validator that closes over app state (such as
25
+ `->(error) { Appsignal.report_error(error) }` in an initializer), or an
26
+ object that holds a logger.
27
+ - Component registries (`register_modifiers`, `register_flags`,
28
+ `self.component_class =`) are frozen as they are written, so a component
29
+ that loads after boot (lazy loading in development) is readable from any
30
+ Ractor too. Writers now replace them instead of mutating them, and
31
+ mutating one in place raises `FrozenError`.
32
+ - Thread safety: the Tailwind class groups behind `ClassMerge` were built
33
+ with an unguarded `||=` (8 threads built them 8 times); they are now built
34
+ once per Ractor.
35
+ - Rendering components inside a Ractor waits on Phlex
36
+ ([yippee-fun/phlex#1055](https://github.com/yippee-fun/phlex/pull/1055)).
37
+
38
+ ## [2.0.2] and earlier
39
+
40
+ ### Unknown modifiers are reported (#54)
41
+
42
+ A positional or responsive modifier a component does not know used to render
43
+ nothing, silently. `config.unknown_modifiers` now decides what happens:
44
+
45
+ ```ruby
46
+ DaisyUI.configure do |config|
47
+ config.unknown_modifiers = :raise # :raise | :warn | :ignore | ->(error) { ... }
48
+ end
49
+ ```
50
+
51
+ - The default is `:raise` under Rails in development and test, and `:warn`
52
+ everywhere else (production, plain Ruby). A Rails app's test suite can go red
53
+ after upgrading if it passes a modifier a component never had, e.g.
54
+ `Toggle(:checked)` or `Radio(:checked)` (use `checked: true`): fix the call,
55
+ register the modifier, or set `:warn`/`:ignore`.
56
+ - `:raise` raises `DaisyUI::UnknownModifierError` (an `ArgumentError`) naming
57
+ the component, the modifier and the closest registered ones.
58
+ - `:warn` and a callable fire once per component (or part, so `Chat` and
59
+ `Chat#bubble` count separately) and modifier. Thread-safe; the memory of what
60
+ was reported resets after 1,000 distinct keys, so a modifier may be reported
61
+ again after that. Use a callable to send it to an
62
+ error tracker:
63
+ `config.unknown_modifiers = ->(error) { Appsignal.report_error(error) }`.
64
+ - Known means the component's `register_modifiers`, `config.modifiers.add`, and
65
+ the new `register_flags` for positional symbols a component handles itself
66
+ without a class (Dropdown's `:popover`, Modal's `:tap_outside_to_close`, ...).
67
+ Flags are not valid in `responsive:`.
68
+ - `Chat#bubble`, `Steps#step` and `Indicator#item` check their modifiers too
69
+ (`DaisyUI::Chat#bubble has no modifier :primray`).
70
+ - Keyword booleans are not validated: unknown keywords stay HTML attributes.
2
71
 
3
72
  ### Upgrading to 2.0
4
73
 
@@ -67,6 +136,12 @@ wins. `Button(:sm, class: "btn-lg")` renders `btn btn-lg` (1.x rendered
67
136
 
68
137
  ### Changed
69
138
 
139
+ - The gem ships minified Stimulus controllers with linked sourcemaps, and the
140
+ importmap pins them without preloading, so with the lazy registration the
141
+ README shows (`lazyLoadControllersFrom`) a page that never uses one never
142
+ fetches it. `daisy_dropdown_controller` 6.1 KB → 2.8 KB (2.4 → 1.3 KB gzipped),
143
+ `daisy_tooltip_controller` 8.8 KB → 5.2 KB (2.4 → 1.8 KB gzipped). The
144
+ `daisy_ui/controllers/*` specifiers and the bundler import paths are unchanged.
70
145
  - Maintainers cut releases with `bin/release` (`list`, `--dry-run`, `patch`/`minor`/`major`/`X.Y.Z`), which checks the lockfiles for gems that do not accept the new version yet and then drives `rake release`.
71
146
 
72
147
  - Range component: added range-vertical modifier for vertical orientation (DaisyUI 5.6)
@@ -74,6 +149,8 @@ wins. `Button(:sm, class: "btn-lg")` renders `btn btn-lg` (1.x rendered
74
149
 
75
150
  ### Fixed
76
151
 
152
+ - Modifiers added with `config.modifiers.add(component: X)` now apply to
153
+ subclasses of `X` too; the closest class wins.
77
154
  - Popover tooltips no longer clip arrows that extend beyond the tooltip card.
78
155
  - Button renders the `disabled` attribute on `button`/`input` elements when given
79
156
  `:disabled` or `disabled: true`, so a disabled submit button no longer submits
data/README.md CHANGED
@@ -177,6 +177,77 @@ arrays, `nil` and `false`; returns a frozen String). Opt out globally with
177
177
  `config.class_merge.enabled = false`, or per component by overriding the
178
178
  private `merge_classes`.
179
179
 
180
+ ## Unknown modifiers
181
+
182
+ A positional or responsive modifier the component does not know
183
+ (`Toggle(:primray)`) is reported instead of silently rendering nothing:
184
+
185
+ ```ruby
186
+ DaisyUI.configure do |config|
187
+ # Default: :raise under Rails in development/test, :warn everywhere else.
188
+ config.unknown_modifiers = :raise # raises DaisyUI::UnknownModifierError
189
+ # Or :warn (Kernel#warn), :ignore, or a callable such as
190
+ # ->(error) { Appsignal.report_error(error) }. :warn and a callable fire once
191
+ # per component (or sub-component part, e.g. Chat#bubble) and modifier; the
192
+ # memory of what was reported resets after 1,000 distinct keys.
193
+ end
194
+ ```
195
+
196
+ Known modifiers are the component's `register_modifiers`, `config.modifiers.add`
197
+ (for the component or any DaisyUI ancestor), and `register_flags`: positional
198
+ symbols your component handles itself without emitting a class. Flags are not
199
+ valid in `responsive:`, since they have no class to prefix. Sub-component
200
+ methods with their own modifiers (`Chat#bubble`, `Steps#step`,
201
+ `Indicator#item`) are checked too.
202
+
203
+ ```ruby
204
+ class Widget < DaisyUI::Base
205
+ register_flags :collapsed # Widget(:collapsed) is valid; read it via `modifiers.include?(:collapsed)`
206
+ end
207
+ ```
208
+
209
+ Keyword arguments are not validated: `Toggle(primray: true)` renders a
210
+ `primray` attribute, because unknown keywords are HTML attributes.
211
+
212
+ ## Threads, fibers and Ractors
213
+
214
+ Components and `DaisyUI::ClassMerge` are safe to share between threads (Puma)
215
+ and fibers (Falcon). For Ractors, DaisyUI finalizes itself at boot: it freezes
216
+ the configuration and every component's registries, so
217
+ `DaisyUI::ClassMerge.merge` runs in any Ractor (as long as the configuration
218
+ is shareable, see below), each Ractor with a merger of its own.
219
+ `DaisyUI.configure` finalizes at its end and the Rails engine
220
+ finalizes again once the app has booted, so there is nothing to call. A
221
+ Rails-less app that never calls `configure` calls `DaisyUI.finalize!` at the
222
+ end of boot.
223
+
224
+ After that the configuration is frozen: writing to `DaisyUI.configuration`
225
+ directly raises `FrozenError`. `DaisyUI.configure` keeps working at any time:
226
+ it yields a writable copy and publishes it once that is finalized, and a
227
+ Ractor that already built its merger rebuilds it from the new configuration.
228
+ A test that changes a setting goes through it too:
229
+
230
+ ```ruby
231
+ around do |example|
232
+ DaisyUI.configure { |config| config.unknown_modifiers = :ignore }
233
+ example.run
234
+ ensure
235
+ DaisyUI.configure { |config| config.unknown_modifiers = :raise }
236
+ end
237
+ ```
238
+
239
+ Finalizing never raises. A configured value that cannot be made shareable is
240
+ kept as it is, and the configuration is then readable on the main Ractor only:
241
+ a callable `unknown_modifiers` or a raw `class_merge` validator that closes
242
+ over app state (`->(error) { Appsignal.report_error(error) }` in an
243
+ initializer), or an object that holds a logger. A lambda is shareable when it
244
+ captures no local variables and is defined inside a module, or made with
245
+ `Ractor.shareable_lambda` on Ruby 4.0. A component that loads after boot (lazy
246
+ loading in development) freezes its registries as it writes them, so any
247
+ Ractor can read it; a Ractor that already built its merger picks up its groups
248
+ after the next `configure`. Rendering components inside a Ractor waits on Phlex
249
+ ([yippee-fun/phlex#1055](https://github.com/yippee-fun/phlex/pull/1055)).
250
+
180
251
  # Compatibility Notes
181
252
 
182
253
  ## @tailwindcss/forms plugin
@@ -270,6 +341,10 @@ import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading"
270
341
  lazyLoadControllersFrom("daisy_ui/controllers", application)
271
342
  ```
272
343
 
344
+ The pin resolves to a minified build with a linked sourcemap (devtools shows the
345
+ readable source) and is not preloaded: a page fetches the controller the first
346
+ time one of its elements appears.
347
+
273
348
  Then opt in per call site:
274
349
 
275
350
  ```ruby
@@ -343,7 +418,7 @@ end
343
418
  Register the gem controllers once with
344
419
  `lazyLoadControllersFrom("daisy_ui/controllers", application)`, as shown in the
345
420
  dropdown controller section above. Importmap-rails applications receive the
346
- controller pin from the gem automatically. Bundler applications can import
421
+ controller pin (minified, not preloaded) from the gem automatically. Bundler applications can import
347
422
  `app/javascript/daisy_ui/controllers/daisy_tooltip_controller.js` and register
348
423
  it as `daisy-tooltip`.
349
424
 
@@ -416,6 +491,11 @@ Which produces:
416
491
 
417
492
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
418
493
 
494
+ After editing a Stimulus controller in `app/javascript/daisy_ui/controllers/`,
495
+ rebuild its minified twin with `bundle exec rake build:js` (needs the bun in
496
+ `.bun-version`) and commit the `.min.js` and `.min.js.map`. CI runs
497
+ `bundle exec rake build:js_check` and fails when they are stale.
498
+
419
499
  # Contributing
420
500
 
421
501
  Bug reports and pull requests are welcome on GitHub at https://github.com/mhenrixon/daisyui. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/mhenrixon/daisyui/blob/main/CODE_OF_CONDUCT.md).
@@ -0,0 +1,4 @@
1
+ import{Controller as l}from"@hotwired/stimulus";var h=4,d='[role="menuitem"]:not([aria-disabled="true"]), a[href]:not([aria-disabled="true"]), button:not([disabled])';class p extends l{static targets=["trigger","menu"];static values={keyboard:{type:Boolean,default:!1}};connect(){if(this.menuEl=this.hasMenuTarget?this.menuTarget:this.element.querySelector("[popover]"),this.triggerEl=this.hasTriggerTarget?this.triggerTarget:this.element.querySelector("[popovertarget]"),!this.menuEl)return;this.needsJsPositioning=!this.#d(),this.onToggle=(t)=>this.#n(t),this.menuEl.addEventListener("toggle",this.onToggle)}disconnect(){this.menuEl?.removeEventListener("toggle",this.onToggle),this.#t()}#n(t){if(t.newState==="open")this.#i();else this.#s()}async#i(){if(this.triggerEl?.setAttribute("aria-expanded","true"),this.needsJsPositioning)await this.#r();if(this.keyboardValue)this.#l()}#s(){this.triggerEl?.setAttribute("aria-expanded","false"),this.#t()}#o(){return this.menuEl?.matches(":popover-open")??!1}#t(){if(this.stopAutoUpdate?.(),this.stopAutoUpdate=null,this.keyHandler)this.menuEl?.removeEventListener("keydown",this.keyHandler),this.keyHandler=null}async#r(){if(!this.triggerEl)return;let t;try{t=await import("@floating-ui/dom")}catch(i){console.warn("[daisy-dropdown] @floating-ui/dom is not available; the popover will open unpositioned. Pin it in your import map to enable the legacy-browser positioning fallback.",i);return}if(!this.#o())return;let{computePosition:e,autoUpdate:n,flip:s,shift:o,offset:r}=t;this.menuEl.style.position="fixed",this.menuEl.style.margin="0",this.stopAutoUpdate=n(this.triggerEl,this.menuEl,()=>{e(this.triggerEl,this.menuEl,{placement:this.#a(),middleware:[r(h),s(),o({padding:8})]}).then(({x:i,y:a})=>{Object.assign(this.menuEl.style,{left:`${i}px`,top:`${a}px`})})})}#a(){let t=this.menuEl.classList,e=t.contains("dropdown-top")?"top":t.contains("dropdown-left")?"left":t.contains("dropdown-right")?"right":"bottom",n=t.contains("dropdown-end")?"-end":t.contains("dropdown-center")?"":"-start";return e==="left"||e==="right"?e:`${e}${n}`}#l(){this.keyHandler=(t)=>this.#h(t),this.menuEl.addEventListener("keydown",this.keyHandler),this.#e()[0]?.focus()}#h(t){let e=this.#e();if(e.length===0)return;let n=e.indexOf(document.activeElement);switch(t.key){case"ArrowDown":t.preventDefault(),e[(n+1+e.length)%e.length].focus();break;case"ArrowUp":t.preventDefault(),e[(n-1+e.length)%e.length].focus();break;case"Home":t.preventDefault(),e[0].focus();break;case"End":t.preventDefault(),e[e.length-1].focus();break}}#e(){return Array.from(this.menuEl.querySelectorAll(d))}#d(){return typeof CSS<"u"&&typeof CSS.supports==="function"&&CSS.supports("anchor-name: --x")}}export{p as default};
2
+
3
+ //# debugId=E14AFCB07E09B55864756E2164756E21
4
+ //# sourceMappingURL=daisy_dropdown_controller.min.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["daisy_dropdown_controller.js"],
4
+ "sourcesContent": [
5
+ "import { Controller } from \"@hotwired/stimulus\"\n\n// daisy-dropdown — OPT-IN enhancement for the :popover Dropdown variant.\n//\n// The native Popover API already handles open/toggle, light-dismiss (outside\n// click), Escape, and focus return — so this controller deliberately does NOT\n// re-implement any of that. Its only jobs are:\n//\n// 1. A positioning fallback for browsers WITHOUT CSS anchor positioning\n// (Safari < 26, Firefox < 147). On modern browsers it does nothing and\n// fetches no extra JavaScript. The fallback lazily imports @floating-ui/dom,\n// which the HOST app must pin (see README) — if it is missing the menu still\n// opens (native popover), just unpositioned, and a console warning is logged.\n// 2. Optional roving keyboard navigation over the menu's focusable items\n// (enable with data-daisy-dropdown-keyboard-value=\"true\").\n//\n// State is driven entirely by the popover's native `toggle` event; there is no\n// internal open flag, no document-level listeners, and no display toggling.\nconst OFFSET = 4\n\n// Focusable, interactive menu items. Prefers explicit role=\"menuitem\", but falls\n// back to the links/buttons DaisyUI menus actually render (<li><a>…</a></li>), so\n// keyboard nav works on stock markup without forcing roles onto caller content.\nconst ITEM_SELECTOR = '[role=\"menuitem\"]:not([aria-disabled=\"true\"]), a[href]:not([aria-disabled=\"true\"]), button:not([disabled])'\n\nexport default class extends Controller {\n static targets = [\"trigger\", \"menu\"]\n static values = { keyboard: { type: Boolean, default: false } }\n\n connect() {\n this.menuEl = this.hasMenuTarget ? this.menuTarget : this.element.querySelector(\"[popover]\")\n this.triggerEl = this.hasTriggerTarget ? this.triggerTarget : this.element.querySelector(\"[popovertarget]\")\n if (!this.menuEl) return\n\n this.needsJsPositioning = !this.#supportsAnchorPositioning()\n this.onToggle = (event) => this.#handleToggle(event)\n this.menuEl.addEventListener(\"toggle\", this.onToggle)\n }\n\n disconnect() {\n this.menuEl?.removeEventListener(\"toggle\", this.onToggle)\n this.#teardownOpen()\n }\n\n #handleToggle(event) {\n if (event.newState === \"open\") this.#onOpen()\n else this.#onClose()\n }\n\n async #onOpen() {\n // The native Popover API does not mirror open state onto the invoker, so\n // sync aria-expanded ourselves for assistive tech.\n this.triggerEl?.setAttribute(\"aria-expanded\", \"true\")\n if (this.needsJsPositioning) await this.#position()\n if (this.keyboardValue) this.#bindKeyboard()\n }\n\n #onClose() {\n this.triggerEl?.setAttribute(\"aria-expanded\", \"false\")\n this.#teardownOpen()\n // Do NOT restore focus — the native popover already returns focus to the trigger.\n }\n\n #isOpen() {\n return this.menuEl?.matches(\":popover-open\") ?? false\n }\n\n #teardownOpen() {\n this.stopAutoUpdate?.()\n this.stopAutoUpdate = null\n if (this.keyHandler) {\n this.menuEl?.removeEventListener(\"keydown\", this.keyHandler)\n this.keyHandler = null\n }\n }\n\n // --- positioning fallback (only when CSS anchor positioning is unavailable) ---\n\n async #position() {\n if (!this.triggerEl) return\n\n // Lazy + feature-gated, so modern browsers never fetch the positioning lib.\n // The host app is responsible for pinning @floating-ui/dom; if it is absent\n // we degrade gracefully (native popover still opens, just unpositioned).\n let floatingUi\n try {\n floatingUi = await import(\"@floating-ui/dom\")\n } catch (error) {\n console.warn(\n \"[daisy-dropdown] @floating-ui/dom is not available; the popover will open unpositioned. \" +\n \"Pin it in your import map to enable the legacy-browser positioning fallback.\",\n error,\n )\n return\n }\n\n // A fast open→close during the dynamic import may have already closed us;\n // bail rather than position (and leak an autoUpdate on) a closed menu.\n if (!this.#isOpen()) return\n\n const { computePosition, autoUpdate, flip, shift, offset } = floatingUi\n\n this.menuEl.style.position = \"fixed\"\n this.menuEl.style.margin = \"0\"\n\n this.stopAutoUpdate = autoUpdate(this.triggerEl, this.menuEl, () => {\n computePosition(this.triggerEl, this.menuEl, {\n placement: this.#placement(),\n middleware: [offset(OFFSET), flip(), shift({ padding: 8 })],\n }).then(({ x, y }) => {\n Object.assign(this.menuEl.style, { left: `${x}px`, top: `${y}px` })\n })\n })\n }\n\n // Derive Floating UI placement from DaisyUI's dropdown-* classes on the menu.\n #placement() {\n const cl = this.menuEl.classList\n const side = cl.contains(\"dropdown-top\")\n ? \"top\"\n : cl.contains(\"dropdown-left\")\n ? \"left\"\n : cl.contains(\"dropdown-right\")\n ? \"right\"\n : \"bottom\"\n const align = cl.contains(\"dropdown-end\") ? \"-end\" : cl.contains(\"dropdown-center\") ? \"\" : \"-start\"\n return (side === \"left\" || side === \"right\") ? side : `${side}${align}`\n }\n\n // --- optional roving keyboard navigation ---\n\n #bindKeyboard() {\n this.keyHandler = (event) => this.#onKeydown(event)\n this.menuEl.addEventListener(\"keydown\", this.keyHandler)\n this.#items()[0]?.focus()\n }\n\n #onKeydown(event) {\n const items = this.#items()\n if (items.length === 0) return\n const index = items.indexOf(document.activeElement)\n\n switch (event.key) {\n case \"ArrowDown\":\n event.preventDefault()\n items[(index + 1 + items.length) % items.length].focus()\n break\n case \"ArrowUp\":\n event.preventDefault()\n items[(index - 1 + items.length) % items.length].focus()\n break\n case \"Home\":\n event.preventDefault()\n items[0].focus()\n break\n case \"End\":\n event.preventDefault()\n items[items.length - 1].focus()\n break\n }\n }\n\n #items() {\n return Array.from(this.menuEl.querySelectorAll(ITEM_SELECTOR))\n }\n\n #supportsAnchorPositioning() {\n return typeof CSS !== \"undefined\" && typeof CSS.supports === \"function\" && CSS.supports(\"anchor-name: --x\")\n }\n}\n"
6
+ ],
7
+ "mappings": "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",
8
+ "debugId": "E14AFCB07E09B55864756E2164756E21",
9
+ "names": []
10
+ }
@@ -0,0 +1,4 @@
1
+ import{Controller as y}from"@hotwired/stimulus";var a=8,c=8,g=10,d=4,f='button, a[href], input, select, textarea, [tabindex]:not([tabindex="-1"])',b={top:"bottom",bottom:"top",left:"right",right:"left"};class T extends y{static targets=["content","arrow"];static values={placement:{type:String,default:"top"}};connect(){if(!this.hasContentTarget)return;if(this.originalMaxWidth=this.contentTarget.style.maxWidth,this.onPointerEnter=()=>this.show(),this.onPointerLeave=()=>this.#m(),this.onFocusIn=()=>this.show(),this.onFocusOut=(t)=>this.#l(t),this.onKeydown=(t)=>this.#u(t),this.onGeometryChange=()=>this.#i(),this.element.addEventListener("pointerenter",this.onPointerEnter),this.element.addEventListener("pointerleave",this.onPointerLeave),this.element.addEventListener("focusin",this.onFocusIn),this.element.addEventListener("focusout",this.onFocusOut),this.#p(),this.element.classList.contains("tooltip-open"))this.show()}disconnect(){if(this.element.removeEventListener("pointerenter",this.onPointerEnter),this.element.removeEventListener("pointerleave",this.onPointerLeave),this.element.removeEventListener("focusin",this.onFocusIn),this.element.removeEventListener("focusout",this.onFocusOut),this.#e(),this.#g(),this.hasContentTarget)this.contentTarget.style.maxWidth=this.originalMaxWidth}show(){if(!this.hasContentTarget||this.#t())return;this.contentTarget.style.visibility="hidden",this.contentTarget.showPopover(),this.#h(),this.#i()}hide(){if(this.#t())this.contentTarget.hidePopover();this.contentTarget.style.visibility="hidden",this.#e()}#h(){window.addEventListener("resize",this.onGeometryChange),window.addEventListener("scroll",this.onGeometryChange,!0),window.addEventListener("keydown",this.onKeydown),window.visualViewport?.addEventListener("resize",this.onGeometryChange),window.visualViewport?.addEventListener("scroll",this.onGeometryChange)}#e(){cancelAnimationFrame(this.frame),window.removeEventListener("resize",this.onGeometryChange),window.removeEventListener("scroll",this.onGeometryChange,!0),window.removeEventListener("keydown",this.onKeydown),window.visualViewport?.removeEventListener("resize",this.onGeometryChange),window.visualViewport?.removeEventListener("scroll",this.onGeometryChange)}#i(){cancelAnimationFrame(this.frame),this.frame=requestAnimationFrame(()=>this.#r())}#r(){if(!this.#t())return;let t=this.#d(),e=Math.max(0,t.width-a*2);this.contentTarget.style.maxWidth=this.originalMaxWidth;let i=Number.parseFloat(getComputedStyle(this.contentTarget).maxWidth),n=Number.isFinite(i)?Math.min(i,e):e;this.contentTarget.style.maxWidth=`${n}px`;let o=this.element.getBoundingClientRect(),h=this.contentTarget.getBoundingClientRect(),r=this.#n(this.placementValue,o,h),s=b[this.placementValue]||"bottom",l=this.#n(s,o,h),m=this.#s(l,h,t)<this.#s(r,h,t),w=m?s:this.placementValue,v=m?l:r,{x:u,y:p}=this.#a(v,h,t);Object.assign(this.contentTarget.style,{left:`${u}px`,top:`${p}px`,visibility:"visible"}),this.#c(w,o,h,u,p)}#n(t,e,i){let n=e.left+e.width/2,o=e.top+e.height/2;switch(t){case"bottom":return{x:n-i.width/2,y:e.bottom+c};case"left":return{x:e.left-i.width-c,y:o-i.height/2};case"right":return{x:e.right+c,y:o-i.height/2};default:return{x:n-i.width/2,y:e.top-i.height-c}}}#s({x:t,y:e},i,n){let o=n.left+a,h=n.left+n.width-a,r=n.top+a,s=n.top+n.height-a;return Math.max(0,o-t)+Math.max(0,t+i.width-h)+Math.max(0,r-e)+Math.max(0,e+i.height-s)}#a({x:t,y:e},i,n){let o=n.left+a,h=n.left+n.width-a,r=n.top+a,s=n.top+n.height-a;return{x:i.width>h-o?o:Math.min(Math.max(t,o),h-i.width),y:i.height>s-r?r:Math.min(Math.max(e,r),s-i.height)}}#c(t,e,i,n,o){if(!this.hasArrowTarget)return;let h=e.left+e.width/2-n,r=e.top+e.height/2-o,s=this.arrowTarget.style;if(Object.assign(s,{inset:"auto",transform:"none"}),t==="top"||t==="bottom"){if(s.left=`${this.#o(h-g/2,4,i.width-g-4)}px`,t==="top")s.bottom=`${-d}px`;else s.top=`${-d}px`,s.transform="rotate(180deg)";return}if(s.top=`${this.#o(r-d/2,4,i.height-d-4)}px`,t==="left")s.right="-7px",s.transform="rotate(-90deg)";else s.left="-7px",s.transform="rotate(90deg)"}#d(){let t=window.visualViewport;return{left:t?.offsetLeft??0,top:t?.offsetTop??0,width:t?.width??window.innerWidth,height:t?.height??window.innerHeight}}#l(t){if(!this.element.contains(t.relatedTarget))this.hide()}#m(){if(!this.element.matches(":focus-within"))this.hide()}#u(t){if(t.key!=="Escape")return;this.hide()}#p(){let t=this.element.matches(f)?this.element:this.element.querySelector(f);if(!t||!this.contentTarget.id)return;this.describedTrigger=t,this.previousDescription=t.getAttribute("aria-describedby");let e=this.previousDescription?.split(/\s+/).filter(Boolean)||[];t.setAttribute("aria-describedby",[...new Set([...e,this.contentTarget.id])].join(" "))}#g(){if(!this.describedTrigger)return;if(this.previousDescription===null)this.describedTrigger.removeAttribute("aria-describedby");else this.describedTrigger.setAttribute("aria-describedby",this.previousDescription)}#t(){return this.hasContentTarget&&this.contentTarget.matches(":popover-open")}#o(t,e,i){return Math.min(Math.max(t,e),Math.max(e,i))}}export{T as default};
2
+
3
+ //# debugId=5716AAA30AA53C0864756E2164756E21
4
+ //# sourceMappingURL=daisy_tooltip_controller.min.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["daisy_tooltip_controller.js"],
4
+ "sourcesContent": [
5
+ "import { Controller } from \"@hotwired/stimulus\"\n\nconst VIEWPORT_PADDING = 8\nconst GAP = 8\nconst ARROW_WIDTH = 10\nconst ARROW_HEIGHT = 4\nconst FOCUSABLE_SELECTOR = 'button, a[href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])'\nconst OPPOSITE = { top: \"bottom\", bottom: \"top\", left: \"right\", right: \"left\" }\n\n// daisy-tooltip — positions a :popover Tooltip in the browser top layer.\n//\n// The Popover API escapes clipping ancestors. This controller supplies the\n// hover/focus lifecycle expected of a tooltip, then flips and shifts the real\n// tooltip element only while it is open so it remains inside the visual\n// viewport. No global listeners remain attached while the tooltip is closed.\nexport default class extends Controller {\n static targets = [\"content\", \"arrow\"]\n static values = { placement: { type: String, default: \"top\" } }\n\n connect() {\n if (!this.hasContentTarget) return\n\n this.originalMaxWidth = this.contentTarget.style.maxWidth\n this.onPointerEnter = () => this.show()\n this.onPointerLeave = () => this.#hideUnlessFocused()\n this.onFocusIn = () => this.show()\n this.onFocusOut = (event) => this.#handleFocusOut(event)\n this.onKeydown = (event) => this.#handleKeydown(event)\n this.onGeometryChange = () => this.#schedulePosition()\n\n this.element.addEventListener(\"pointerenter\", this.onPointerEnter)\n this.element.addEventListener(\"pointerleave\", this.onPointerLeave)\n this.element.addEventListener(\"focusin\", this.onFocusIn)\n this.element.addEventListener(\"focusout\", this.onFocusOut)\n this.#describeTrigger()\n\n if (this.element.classList.contains(\"tooltip-open\")) this.show()\n }\n\n disconnect() {\n this.element.removeEventListener(\"pointerenter\", this.onPointerEnter)\n this.element.removeEventListener(\"pointerleave\", this.onPointerLeave)\n this.element.removeEventListener(\"focusin\", this.onFocusIn)\n this.element.removeEventListener(\"focusout\", this.onFocusOut)\n this.#teardownOpen()\n this.#restoreDescription()\n if (this.hasContentTarget) this.contentTarget.style.maxWidth = this.originalMaxWidth\n }\n\n show() {\n if (!this.hasContentTarget || this.#isOpen()) return\n\n this.contentTarget.style.visibility = \"hidden\"\n this.contentTarget.showPopover()\n this.#setupOpen()\n this.#schedulePosition()\n }\n\n hide() {\n if (this.#isOpen()) this.contentTarget.hidePopover()\n this.contentTarget.style.visibility = \"hidden\"\n this.#teardownOpen()\n }\n\n #setupOpen() {\n window.addEventListener(\"resize\", this.onGeometryChange)\n window.addEventListener(\"scroll\", this.onGeometryChange, true)\n window.addEventListener(\"keydown\", this.onKeydown)\n window.visualViewport?.addEventListener(\"resize\", this.onGeometryChange)\n window.visualViewport?.addEventListener(\"scroll\", this.onGeometryChange)\n }\n\n #teardownOpen() {\n cancelAnimationFrame(this.frame)\n window.removeEventListener(\"resize\", this.onGeometryChange)\n window.removeEventListener(\"scroll\", this.onGeometryChange, true)\n window.removeEventListener(\"keydown\", this.onKeydown)\n window.visualViewport?.removeEventListener(\"resize\", this.onGeometryChange)\n window.visualViewport?.removeEventListener(\"scroll\", this.onGeometryChange)\n }\n\n #schedulePosition() {\n cancelAnimationFrame(this.frame)\n this.frame = requestAnimationFrame(() => this.#position())\n }\n\n #position() {\n if (!this.#isOpen()) return\n\n const viewport = this.#viewport()\n const availableWidth = Math.max(0, viewport.width - VIEWPORT_PADDING * 2)\n this.contentTarget.style.maxWidth = this.originalMaxWidth\n const computedMaxWidth = Number.parseFloat(getComputedStyle(this.contentTarget).maxWidth)\n const maxWidth = Number.isFinite(computedMaxWidth) ? Math.min(computedMaxWidth, availableWidth) : availableWidth\n this.contentTarget.style.maxWidth = `${maxWidth}px`\n\n const trigger = this.element.getBoundingClientRect()\n const tooltip = this.contentTarget.getBoundingClientRect()\n const preferred = this.#coordinates(this.placementValue, trigger, tooltip)\n const oppositePlacement = OPPOSITE[this.placementValue] || \"bottom\"\n const opposite = this.#coordinates(oppositePlacement, trigger, tooltip)\n const useOpposite = this.#overflow(opposite, tooltip, viewport) < this.#overflow(preferred, tooltip, viewport)\n const placement = useOpposite ? oppositePlacement : this.placementValue\n const coordinates = useOpposite ? opposite : preferred\n const { x, y } = this.#shift(coordinates, tooltip, viewport)\n\n Object.assign(this.contentTarget.style, { left: `${x}px`, top: `${y}px`, visibility: \"visible\" })\n this.#positionArrow(placement, trigger, tooltip, x, y)\n }\n\n #coordinates(placement, trigger, tooltip) {\n const centerX = trigger.left + trigger.width / 2\n const centerY = trigger.top + trigger.height / 2\n\n switch (placement) {\n case \"bottom\":\n return { x: centerX - tooltip.width / 2, y: trigger.bottom + GAP }\n case \"left\":\n return { x: trigger.left - tooltip.width - GAP, y: centerY - tooltip.height / 2 }\n case \"right\":\n return { x: trigger.right + GAP, y: centerY - tooltip.height / 2 }\n default:\n return { x: centerX - tooltip.width / 2, y: trigger.top - tooltip.height - GAP }\n }\n }\n\n #overflow({ x, y }, tooltip, viewport) {\n const left = viewport.left + VIEWPORT_PADDING\n const right = viewport.left + viewport.width - VIEWPORT_PADDING\n const top = viewport.top + VIEWPORT_PADDING\n const bottom = viewport.top + viewport.height - VIEWPORT_PADDING\n\n return (\n Math.max(0, left - x) +\n Math.max(0, x + tooltip.width - right) +\n Math.max(0, top - y) +\n Math.max(0, y + tooltip.height - bottom)\n )\n }\n\n #shift({ x, y }, tooltip, viewport) {\n const left = viewport.left + VIEWPORT_PADDING\n const right = viewport.left + viewport.width - VIEWPORT_PADDING\n const top = viewport.top + VIEWPORT_PADDING\n const bottom = viewport.top + viewport.height - VIEWPORT_PADDING\n\n return {\n x: tooltip.width > right - left ? left : Math.min(Math.max(x, left), right - tooltip.width),\n y: tooltip.height > bottom - top ? top : Math.min(Math.max(y, top), bottom - tooltip.height),\n }\n }\n\n #positionArrow(placement, trigger, tooltip, x, y) {\n if (!this.hasArrowTarget) return\n\n const centerX = trigger.left + trigger.width / 2 - x\n const centerY = trigger.top + trigger.height / 2 - y\n const arrow = this.arrowTarget.style\n Object.assign(arrow, { inset: \"auto\", transform: \"none\" })\n\n if (placement === \"top\" || placement === \"bottom\") {\n arrow.left = `${this.#clamp(centerX - ARROW_WIDTH / 2, 4, tooltip.width - ARROW_WIDTH - 4)}px`\n if (placement === \"top\") arrow.bottom = `${-ARROW_HEIGHT}px`\n else {\n arrow.top = `${-ARROW_HEIGHT}px`\n arrow.transform = \"rotate(180deg)\"\n }\n return\n }\n\n arrow.top = `${this.#clamp(centerY - ARROW_HEIGHT / 2, 4, tooltip.height - ARROW_HEIGHT - 4)}px`\n if (placement === \"left\") {\n arrow.right = \"-7px\"\n arrow.transform = \"rotate(-90deg)\"\n } else {\n arrow.left = \"-7px\"\n arrow.transform = \"rotate(90deg)\"\n }\n }\n\n #viewport() {\n const visualViewport = window.visualViewport\n return {\n left: visualViewport?.offsetLeft ?? 0,\n top: visualViewport?.offsetTop ?? 0,\n width: visualViewport?.width ?? window.innerWidth,\n height: visualViewport?.height ?? window.innerHeight,\n }\n }\n\n #handleFocusOut(event) {\n if (!this.element.contains(event.relatedTarget)) this.hide()\n }\n\n #hideUnlessFocused() {\n if (!this.element.matches(\":focus-within\")) this.hide()\n }\n\n #handleKeydown(event) {\n if (event.key !== \"Escape\") return\n\n this.hide()\n }\n\n #describeTrigger() {\n const trigger = this.element.matches(FOCUSABLE_SELECTOR)\n ? this.element\n : this.element.querySelector(FOCUSABLE_SELECTOR)\n if (!trigger || !this.contentTarget.id) return\n\n this.describedTrigger = trigger\n this.previousDescription = trigger.getAttribute(\"aria-describedby\")\n const descriptions = this.previousDescription?.split(/\\s+/).filter(Boolean) || []\n trigger.setAttribute(\"aria-describedby\", [...new Set([...descriptions, this.contentTarget.id])].join(\" \"))\n }\n\n #restoreDescription() {\n if (!this.describedTrigger) return\n\n if (this.previousDescription === null) this.describedTrigger.removeAttribute(\"aria-describedby\")\n else this.describedTrigger.setAttribute(\"aria-describedby\", this.previousDescription)\n }\n\n #isOpen() {\n return this.hasContentTarget && this.contentTarget.matches(\":popover-open\")\n }\n\n #clamp(value, minimum, maximum) {\n return Math.min(Math.max(value, minimum), Math.max(minimum, maximum))\n }\n}\n"
6
+ ],
7
+ "mappings": "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",
8
+ "debugId": "5716AAA30AA53C0864756E2164756E21",
9
+ "names": []
10
+ }
data/config/importmap.rb CHANGED
@@ -7,6 +7,15 @@
7
7
  # // app/javascript/controllers/index.js
8
8
  # import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading"
9
9
  # lazyLoadControllersFrom("daisy_ui/controllers", application)
10
- pin_all_from DaisyUI::Engine.root.join("app/javascript/daisy_ui/controllers"),
11
- under: "daisy_ui/controllers",
12
- to: "daisy_ui/controllers"
10
+ #
11
+ # Each pin resolves to the minified build (`rake build:js`), whose linked
12
+ # sourcemap shows the readable source in devtools. Pinned one by one, not with
13
+ # pin_all_from: the directory also holds the .min.js twins, which would become
14
+ # pins of their own. Not preloaded: a lazily registered controller is fetched
15
+ # the first time its data-controller appears, so a page without one skips it.
16
+ pin "daisy_ui/controllers/daisy_dropdown_controller",
17
+ to: "daisy_ui/controllers/daisy_dropdown_controller.min.js",
18
+ preload: false
19
+ pin "daisy_ui/controllers/daisy_tooltip_controller",
20
+ to: "daisy_ui/controllers/daisy_tooltip_controller.min.js",
21
+ preload: false
data/lib/daisy_ui/base.rb CHANGED
@@ -118,38 +118,77 @@ module DaisyUI
118
118
  error: "bg-error text-error-content"
119
119
  }.freeze
120
120
 
121
+ # Component registries: written by the class body (register_modifiers,
122
+ # register_flags, `self.component_class =`) and by `inherited`, on the main
123
+ # Ractor while the class loads; rendering only reads them. Every writer
124
+ # assigns a new value instead of mutating the old one and freezes it as it
125
+ # writes (Shareable.share), so a non-main Ractor can read the registries of
126
+ # a component that loaded after boot too, and a later register_* still
127
+ # works.
121
128
  class << self
122
- attr_writer :component_class, :modifier_groups
123
- attr_accessor :modifiers
129
+ # rubocop:disable ThreadSafety/ClassInstanceVariable -- see above
130
+ attr_reader :modifiers
131
+
132
+ def modifiers=(modifiers)
133
+ @modifiers = Shareable.share(modifiers)
134
+ end
135
+
136
+ def modifier_groups=(groups)
137
+ @modifier_groups = Shareable.share(groups)
138
+ end
139
+
140
+ def flags=(flags)
141
+ @flags = Shareable.share(flags)
142
+ end
143
+
144
+ def component_class=(component_class)
145
+ @component_class = Shareable.share(component_class)
146
+ end
147
+
148
+ # Positional symbols a component handles itself (e.g. Dropdown's
149
+ # :popover): accepted as modifiers, but they emit no class.
150
+ def flags
151
+ @flags || []
152
+ end
153
+
154
+ def register_flags(*names)
155
+ self.flags = flags | names
156
+ end
124
157
 
125
158
  # { group => [modifier, ...] }: modifiers in one group are alternatives
126
159
  # (sizes, colors, ...), so ClassMerge keeps only the last one.
127
160
  def modifier_groups
128
- @modifier_groups ||= {}
161
+ @modifier_groups || {}
129
162
  end
130
163
 
164
+ # Derived from the class name on first use, the same value every time.
165
+ # Remembered on the main Ractor only (DaisyUI.finalize! derives it at
166
+ # boot); a non-main Ractor cannot set a class ivar, so it derives it.
131
167
  def component_class
132
168
  return @component_class if instance_variable_defined?(:@component_class)
133
169
 
134
- @component_class = begin
170
+ derived = begin
135
171
  # Convert "DaisyUI::Button" -> "button"
136
172
  class_name = name.split("::").last # demodulize
137
173
  class_name.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
138
174
  .gsub(/([a-z\d])([A-Z])/, '\1_\2')
139
175
  .tr("_", "-")
140
- .downcase
176
+ .downcase.freeze
141
177
  end
178
+ Store.main_ractor? ? (@component_class = derived) : derived
142
179
  end
143
180
 
144
181
  def inherited(subclass)
145
182
  super
146
183
  subclass.modifiers = (modifiers || {}).dup
147
184
  subclass.modifier_groups = modifier_groups.transform_values(&:dup)
185
+ subclass.flags = flags.dup
148
186
  # Inherit component_class if it was explicitly set
149
187
  return unless instance_variable_defined?(:@component_class)
150
188
 
151
189
  subclass.component_class = @component_class
152
190
  end
191
+ # rubocop:enable ThreadSafety/ClassInstanceVariable
153
192
 
154
193
  # Registers modifiers. A Hash value declares a group of alternatives:
155
194
  #
@@ -167,15 +206,23 @@ module DaisyUI
167
206
  end
168
207
  end
169
208
 
170
- modifier_groups.each_value { |members| members.delete_if { |member| flat.key?(member) } }
209
+ groups = modifier_groups.transform_values { |members| members.reject { |member| flat.key?(member) } }
171
210
  mods.each do |group, value|
172
- (modifier_groups[group] ||= []).concat(value.keys) if value.is_a?(Hash)
211
+ groups[group] = (groups[group] || []) + value.keys if value.is_a?(Hash)
173
212
  end
174
- modifier_groups.delete_if { |_group, members| members.empty? }
175
213
 
214
+ self.modifier_groups = groups.reject { |_group, members| members.empty? }
176
215
  self.modifiers = (modifiers || {}).merge(flat)
177
216
  ClassMerge.reset!
178
217
  end
218
+
219
+ # @api private Called by DaisyUI.finalize! for every component: derives
220
+ # component_class on the main Ractor and makes sure every registry is
221
+ # frozen for non-main Ractors.
222
+ def finalize!
223
+ [component_class, modifiers, modifier_groups, flags].each { |registry| Shareable.share(registry) }
224
+ self
225
+ end
179
226
  end
180
227
 
181
228
  def initialize(*modifiers, as: :div, id: nil, **options)
@@ -188,6 +235,7 @@ module DaisyUI
188
235
  # Store all keyword arguments (class, data, aria, etc.)
189
236
  # This will be processed by `classes` and `attributes` methods
190
237
  @options = options
238
+ validate_modifiers
191
239
  super()
192
240
  end
193
241
 
@@ -277,11 +325,41 @@ module DaisyUI
277
325
  {
278
326
  skeleton: "skeleton",
279
327
  **(self.class.modifiers || {}),
280
- **DaisyUI.configuration.modifiers.for(component: self.class),
328
+ **configured_modifiers,
281
329
  **DaisyUI.configuration.modifiers.for(component: nil)
282
330
  }
283
331
  end
284
332
 
333
+ # Modifiers added with `config.modifiers.add(component:)` for this class or
334
+ # any DaisyUI ancestor; the closest class wins.
335
+ def configured_modifiers
336
+ self.class.ancestors.take_while { |klass| klass != Base }.reverse.reduce({}) do |result, klass|
337
+ result.merge(DaisyUI.configuration.modifiers.for(component: klass))
338
+ end
339
+ end
340
+
341
+ # Reports positional modifiers that are neither in modifier_map nor a
342
+ # registered flag, and responsive ones not in modifier_map (a flag has no
343
+ # class to prefix). See Configuration#unknown_modifiers.
344
+ def validate_modifiers
345
+ responsive = (options[:responsive]&.values || []).flat_map { |mods| Array(mods) }
346
+ return if modifiers.empty? && responsive.empty?
347
+
348
+ map_keys = modifier_map.keys
349
+ report_unknown_modifiers(modifiers, map_keys | self.class.flags)
350
+ report_unknown_modifiers(responsive, map_keys)
351
+ end
352
+
353
+ # For sub-component methods with their own modifiers, e.g. Chat#bubble:
354
+ # `report_unknown_modifiers(modifiers, BUBBLE_MODIFIERS.keys, part: :bubble)`.
355
+ def report_unknown_modifiers(requested, known, part: nil)
356
+ requested.each do |modifier|
357
+ next if !modifier.is_a?(Symbol) || known.include?(modifier)
358
+
359
+ DaisyUI.configuration.report_unknown_modifier(component: self.class, modifier:, known:, part:)
360
+ end
361
+ end
362
+
285
363
  def extract_boolean_modifiers(options)
286
364
  modifier_keys = modifier_map.keys
287
365
  boolean_mods = []
data/lib/daisy_ui/chat.rb CHANGED
@@ -108,6 +108,7 @@ module DaisyUI
108
108
  private
109
109
 
110
110
  def build_bubble_classes(modifiers, options)
111
+ report_unknown_modifiers(modifiers, BUBBLE_MODIFIERS.keys, part: :bubble)
111
112
  modifier_classes = modifiers.filter_map { |mod| apply_prefix(BUBBLE_MODIFIERS[mod]) }
112
113
  merge_classes(apply_prefix("chat-bubble"), *modifier_classes, options.delete(:class))
113
114
  end
@@ -3,6 +3,7 @@
3
3
  module DaisyUI
4
4
  class Checkbox < Base
5
5
  self.component_class = :checkbox
6
+ register_flags :checked, :disabled
6
7
 
7
8
  def view_template(&)
8
9
  input(
@@ -24,6 +24,20 @@ module DaisyUI
24
24
  @conflicts = {}
25
25
  end
26
26
 
27
+ def initialize_copy(source)
28
+ super
29
+ @theme = @theme.transform_values(&:dup)
30
+ @class_groups = @class_groups.transform_values(&:dup)
31
+ @conflicts = @conflicts.transform_values(&:dup)
32
+ end
33
+
34
+ # @api private Called by DaisyUI.finalize!. A raw class group's
35
+ # validator that closes over app state is kept as it is.
36
+ def finalize!
37
+ [@theme, @class_groups, @conflicts].each { |settings| Shareable.share(settings) }
38
+ freeze
39
+ end
40
+
27
41
  # Registers utilities that behave like an existing one:
28
42
  # `utility("text-display", like: "text-lg")` makes `text-display` a font
29
43
  # size, so it replaces `text-sm` and sits next to `text-error`.
@@ -57,11 +71,12 @@ module DaisyUI
57
71
  append(@conflicts, conflicts)
58
72
  end
59
73
 
60
- # @api private The config passed to Merger.new, including the daisyUI groups.
61
- def merger_config
74
+ # @api private The config passed to Merger.new, including the daisyUI
75
+ # groups of +configuration+ (the DaisyUI configuration holding this one).
76
+ def merger_config(configuration = DaisyUI.configuration)
62
77
  {
63
78
  theme: @theme,
64
- class_groups: DaisyGroups.build.merge(@class_groups) { |_id, existing, added| existing + added },
79
+ class_groups: DaisyGroups.build(configuration).merge(@class_groups) { |_id, existing, added| existing + added },
65
80
  conflicting_class_groups: @conflicts
66
81
  }
67
82
  end
@@ -18,8 +18,9 @@ module DaisyUI
18
18
  # @api private
19
19
  module DaisyGroups
20
20
  class << self
21
- # Class groups for the merger: `{ "daisy:group:btn-size" => ["btn-xs", ...], ... }`.
22
- def build(prefix: DaisyUI.configuration.prefix)
21
+ # Class groups for the merger: `{ "daisy:group:btn-size" => ["btn-xs", ...], ... }`,
22
+ # from one configuration's prefix and modifiers.
23
+ def build(configuration = DaisyUI.configuration, prefix: configuration.prefix)
23
24
  components = self.components
24
25
  groups = {}
25
26
 
@@ -37,7 +38,7 @@ module DaisyUI
37
38
  end
38
39
 
39
40
  # Configured modifiers join the component's own group of that name.
40
- DaisyUI.configuration.modifiers.groups.each do |(component, group), classes|
41
+ configuration.modifiers.groups.each do |(component, group), classes|
41
42
  owner = component ? component.component_class || component.name : "global"
42
43
  add(groups, "daisy:group:#{owner}-#{group}", classes.flat_map(&:split), prefix)
43
44
  end
@@ -51,9 +52,11 @@ module DaisyUI
51
52
 
52
53
  # Every named DaisyUI::Base subclass: the gem's components (eager
53
54
  # loaded) and any app component loaded so far. An app component that
54
- # loads later resets the merger through `register_modifiers`.
55
+ # loads later resets the merger through `register_modifiers`. Only the
56
+ # main Ractor can eager-load; DaisyUI.finalize! did it there before
57
+ # any other Ractor builds a merger.
55
58
  def components
56
- Zeitwerk::Loader.eager_load_namespace(DaisyUI)
59
+ Zeitwerk::Loader.eager_load_namespace(DaisyUI) if Store.main_ractor?
57
60
  descendants(DaisyUI::Base).select(&:name)
58
61
  end
59
62
 
@@ -8,21 +8,6 @@ module DaisyUI
8
8
  module TailwindConfig
9
9
  include Validators
10
10
 
11
- class << self
12
- private
13
-
14
- # Recursively freezes nested Hash/Array structures so shared config
15
- # constants can't be mutated in place. Keys are strings/symbols and are
16
- # already immutable, so only values need walking.
17
- def deep_freeze(object)
18
- case object
19
- when Hash then object.each_value { |value| deep_freeze(value) }
20
- when Array then object.each { |value| deep_freeze(value) }
21
- end
22
- object.freeze
23
- end
24
- end
25
-
26
11
  THEME_KEYS = %w[
27
12
  color
28
13
  font
@@ -46,8 +31,8 @@ module DaisyUI
46
31
  ].freeze
47
32
  THEME_KEYS.each do |key|
48
33
  const_set("THEME_#{key.upcase.tr('-', '_')}", lambda { |config|
49
- config[:theme].fetch(key, nil)
50
- }
34
+ config[:theme].fetch(key, nil)
35
+ }
51
36
  )
52
37
  end
53
38
 
@@ -2465,7 +2450,12 @@ module DaisyUI
2465
2450
  "selection"
2466
2451
  ]
2467
2452
  }.freeze
2468
- deep_freeze(DEFAULTS)
2453
+
2454
+ # Every constant above, DEFAULTS deeply, is made Ractor-shareable so a
2455
+ # non-main Ractor can build its own merger from them: the scale and theme
2456
+ # lambdas are capture-free (THEME_* only reads its frozen key) and their
2457
+ # self is this module.
2458
+ constants(false).each { |name| Shareable.make(const_get(name)) }
2469
2459
 
2470
2460
  # Extends DEFAULTS with incoming_config without mutating either.
2471
2461
  # Theme scales are appended. Unlike upstream, `class_groups` and
@@ -197,6 +197,10 @@ module DaisyUI
197
197
  IS_LABEL_SHADOW = lambda { |label|
198
198
  label == "shadow"
199
199
  }
200
+
201
+ # Ractor-shareable, so a non-main Ractor can read them: every validator
202
+ # is a capture-free lambda whose self is this module.
203
+ constants(false).each { |name| Shareable.make(const_get(name)) }
200
204
  end
201
205
  end
202
206
  end