daisyui 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fe7d742ea5321a80e5f6689bd3df51ad59aae0a5ee9ee0617f8f237a654ebad4
4
- data.tar.gz: 9ff82a86a33f3d01165f59b3b94e9e6867bd43c43ab190c0bfbc7f14fa49e546
3
+ metadata.gz: 784f97cabf197c081daba3827d5d7cc67a4bfe68b1be95fd038cecaae9da127c
4
+ data.tar.gz: e1cc4be0135efdf03ed592a8c0d73e2b12d81c4cee47dacd7008ac34aa8bdbbe
5
5
  SHA512:
6
- metadata.gz: fe53957f3b594373cfdbd7865a3e143f5526e679c9dcbb8d8da1eef10a95a0cccf0d55dcf5c33889ad268ed382b3dd21f56b8b74e28f2adc289697e8cf10fedc
7
- data.tar.gz: 3bee4c79945a177706b8f30231b5f4f0a2aab66aa3dd4031842a9d3e603cc09621ebefce8a7cd79062f5e9a9f0bc1823a1701ba10953c687a3d5c03a0b36bd10
6
+ metadata.gz: d6331604dbf5bdd7b419dfcbb5494cf7569a1709b3d9b17dd7fe08d56d1e3fed14b16c4d1925534a4efdce23e4e85347901b5687580f466e02aa9081f5ca646f
7
+ data.tar.gz: 002c0ad206f54764209eb6ae752c852a91369adb97c84910fe39daf19ca993842b773f6f60ad27da12cc88d25e6d7df8cc6415018408a541c99533d214432193
data/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ### Unknown modifiers are reported (#54)
4
+
5
+ A positional or responsive modifier a component does not know used to render
6
+ nothing, silently. `config.unknown_modifiers` now decides what happens:
7
+
8
+ ```ruby
9
+ DaisyUI.configure do |config|
10
+ config.unknown_modifiers = :raise # :raise | :warn | :ignore | ->(error) { ... }
11
+ end
12
+ ```
13
+
14
+ - The default is `:raise` under Rails in development and test, and `:warn`
15
+ everywhere else (production, plain Ruby). A Rails app's test suite can go red
16
+ after upgrading if it passes a modifier a component never had, e.g.
17
+ `Toggle(:checked)` or `Radio(:checked)` (use `checked: true`): fix the call,
18
+ register the modifier, or set `:warn`/`:ignore`.
19
+ - `:raise` raises `DaisyUI::UnknownModifierError` (an `ArgumentError`) naming
20
+ the component, the modifier and the closest registered ones.
21
+ - `:warn` and a callable fire once per component (or part, so `Chat` and
22
+ `Chat#bubble` count separately) and modifier. Thread-safe; the memory of what
23
+ was reported resets after 1,000 distinct keys, so a modifier may be reported
24
+ again after that. Use a callable to send it to an
25
+ error tracker:
26
+ `config.unknown_modifiers = ->(error) { Appsignal.report_error(error) }`.
27
+ - Known means the component's `register_modifiers`, `config.modifiers.add`, and
28
+ the new `register_flags` for positional symbols a component handles itself
29
+ without a class (Dropdown's `:popover`, Modal's `:tap_outside_to_close`, ...).
30
+ Flags are not valid in `responsive:`.
31
+ - `Chat#bubble`, `Steps#step` and `Indicator#item` check their modifiers too
32
+ (`DaisyUI::Chat#bubble has no modifier :primray`).
33
+ - Keyword booleans are not validated: unknown keywords stay HTML attributes.
34
+
3
35
  ### Upgrading to 2.0
4
36
 
5
37
  Components now merge their classes: when two classes conflict, the later one
@@ -67,6 +99,12 @@ wins. `Button(:sm, class: "btn-lg")` renders `btn btn-lg` (1.x rendered
67
99
 
68
100
  ### Changed
69
101
 
102
+ - The gem ships minified Stimulus controllers with linked sourcemaps, and the
103
+ importmap pins them without preloading, so with the lazy registration the
104
+ README shows (`lazyLoadControllersFrom`) a page that never uses one never
105
+ fetches it. `daisy_dropdown_controller` 6.1 KB → 2.8 KB (2.4 → 1.3 KB gzipped),
106
+ `daisy_tooltip_controller` 8.8 KB → 5.2 KB (2.4 → 1.8 KB gzipped). The
107
+ `daisy_ui/controllers/*` specifiers and the bundler import paths are unchanged.
70
108
  - 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
109
 
72
110
  - Range component: added range-vertical modifier for vertical orientation (DaisyUI 5.6)
@@ -74,7 +112,12 @@ wins. `Button(:sm, class: "btn-lg")` renders `btn btn-lg` (1.x rendered
74
112
 
75
113
  ### Fixed
76
114
 
115
+ - Modifiers added with `config.modifiers.add(component: X)` now apply to
116
+ subclasses of `X` too; the closest class wins.
77
117
  - Popover tooltips no longer clip arrows that extend beyond the tooltip card.
118
+ - Button renders the `disabled` attribute on `button`/`input` elements when given
119
+ `:disabled` or `disabled: true`, so a disabled submit button no longer submits
120
+ its form from the keyboard (#52).
78
121
 
79
122
  ## [0.1.0] - 2024-08-02
80
123
 
data/README.md CHANGED
@@ -177,6 +177,38 @@ 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
+
180
212
  # Compatibility Notes
181
213
 
182
214
  ## @tailwindcss/forms plugin
@@ -270,6 +302,10 @@ import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading"
270
302
  lazyLoadControllersFrom("daisy_ui/controllers", application)
271
303
  ```
272
304
 
305
+ The pin resolves to a minified build with a linked sourcemap (devtools shows the
306
+ readable source) and is not preloaded: a page fetches the controller the first
307
+ time one of its elements appears.
308
+
273
309
  Then opt in per call site:
274
310
 
275
311
  ```ruby
@@ -343,7 +379,7 @@ end
343
379
  Register the gem controllers once with
344
380
  `lazyLoadControllersFrom("daisy_ui/controllers", application)`, as shown in the
345
381
  dropdown controller section above. Importmap-rails applications receive the
346
- controller pin from the gem automatically. Bundler applications can import
382
+ controller pin (minified, not preloaded) from the gem automatically. Bundler applications can import
347
383
  `app/javascript/daisy_ui/controllers/daisy_tooltip_controller.js` and register
348
384
  it as `daisy-tooltip`.
349
385
 
@@ -416,6 +452,11 @@ Which produces:
416
452
 
417
453
  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
454
 
455
+ After editing a Stimulus controller in `app/javascript/daisy_ui/controllers/`,
456
+ rebuild its minified twin with `bundle exec rake build:js` (needs the bun in
457
+ `.bun-version`) and commit the `.min.js` and `.min.js.map`. CI runs
458
+ `bundle exec rake build:js_check` and fails when they are stale.
459
+
419
460
  # Contributing
420
461
 
421
462
  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
@@ -119,9 +119,19 @@ module DaisyUI
119
119
  }.freeze
120
120
 
121
121
  class << self
122
- attr_writer :component_class, :modifier_groups
122
+ attr_writer :component_class, :modifier_groups, :flags
123
123
  attr_accessor :modifiers
124
124
 
125
+ # Positional symbols a component handles itself (e.g. Dropdown's
126
+ # :popover): accepted as modifiers, but they emit no class.
127
+ def flags
128
+ @flags ||= []
129
+ end
130
+
131
+ def register_flags(*names)
132
+ self.flags = flags | names
133
+ end
134
+
125
135
  # { group => [modifier, ...] }: modifiers in one group are alternatives
126
136
  # (sizes, colors, ...), so ClassMerge keeps only the last one.
127
137
  def modifier_groups
@@ -145,6 +155,7 @@ module DaisyUI
145
155
  super
146
156
  subclass.modifiers = (modifiers || {}).dup
147
157
  subclass.modifier_groups = modifier_groups.transform_values(&:dup)
158
+ subclass.flags = flags.dup
148
159
  # Inherit component_class if it was explicitly set
149
160
  return unless instance_variable_defined?(:@component_class)
150
161
 
@@ -188,6 +199,7 @@ module DaisyUI
188
199
  # Store all keyword arguments (class, data, aria, etc.)
189
200
  # This will be processed by `classes` and `attributes` methods
190
201
  @options = options
202
+ validate_modifiers
191
203
  super()
192
204
  end
193
205
 
@@ -277,11 +289,41 @@ module DaisyUI
277
289
  {
278
290
  skeleton: "skeleton",
279
291
  **(self.class.modifiers || {}),
280
- **DaisyUI.configuration.modifiers.for(component: self.class),
292
+ **configured_modifiers,
281
293
  **DaisyUI.configuration.modifiers.for(component: nil)
282
294
  }
283
295
  end
284
296
 
297
+ # Modifiers added with `config.modifiers.add(component:)` for this class or
298
+ # any DaisyUI ancestor; the closest class wins.
299
+ def configured_modifiers
300
+ self.class.ancestors.take_while { |klass| klass != Base }.reverse.reduce({}) do |result, klass|
301
+ result.merge(DaisyUI.configuration.modifiers.for(component: klass))
302
+ end
303
+ end
304
+
305
+ # Reports positional modifiers that are neither in modifier_map nor a
306
+ # registered flag, and responsive ones not in modifier_map (a flag has no
307
+ # class to prefix). See Configuration#unknown_modifiers.
308
+ def validate_modifiers
309
+ responsive = (options[:responsive]&.values || []).flat_map { |mods| Array(mods) }
310
+ return if modifiers.empty? && responsive.empty?
311
+
312
+ map_keys = modifier_map.keys
313
+ report_unknown_modifiers(modifiers, map_keys | self.class.flags)
314
+ report_unknown_modifiers(responsive, map_keys)
315
+ end
316
+
317
+ # For sub-component methods with their own modifiers, e.g. Chat#bubble:
318
+ # `report_unknown_modifiers(modifiers, BUBBLE_MODIFIERS.keys, part: :bubble)`.
319
+ def report_unknown_modifiers(requested, known, part: nil)
320
+ requested.each do |modifier|
321
+ next if !modifier.is_a?(Symbol) || known.include?(modifier)
322
+
323
+ DaisyUI.configuration.report_unknown_modifier(component: self.class, modifier:, known:, part:)
324
+ end
325
+ end
326
+
285
327
  def extract_boolean_modifiers(options)
286
328
  modifier_keys = modifier_map.keys
287
329
  boolean_mods = []
@@ -6,6 +6,8 @@ module DaisyUI
6
6
  class Button < Base
7
7
  self.component_class = :btn
8
8
 
9
+ FORM_CONTROLS = %i[button input].freeze
10
+
9
11
  def initialize(*, as: :button, modal: nil, **)
10
12
  super(*, as:, **)
11
13
  @modal = modal
@@ -29,6 +31,16 @@ module DaisyUI
29
31
 
30
32
  attr_reader :modal
31
33
 
34
+ # `disabled` is consumed as a modifier (btn-disabled), which only blocks
35
+ # pointer events. Form controls also need the attribute so keyboard
36
+ # activation and implicit form submission are blocked too.
37
+ def attributes
38
+ attrs = super
39
+ return attrs unless FORM_CONTROLS.include?(as.to_sym) && modifiers.include?(:disabled)
40
+
41
+ attrs.merge(disabled: true)
42
+ end
43
+
32
44
  register_modifiers(
33
45
  style: {
34
46
  # "sm:btn-ghost"
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(
@@ -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
@@ -52,10 +52,49 @@ module DaisyUI
52
52
  end
53
53
 
54
54
  class Configuration
55
+ UNKNOWN_MODIFIER_MODES = %i[raise warn ignore].freeze
56
+ REPORTED_UNKNOWN_MODIFIERS_LIMIT = 1_000
57
+
55
58
  attr_reader :prefix
56
59
 
57
60
  def initialize
58
61
  @prefix = nil
62
+ @reported_unknown_modifiers = Set.new
63
+ @reported_unknown_modifiers_lock = Mutex.new
64
+ end
65
+
66
+ # What a component does with a positional modifier it does not know:
67
+ # :raise, :warn (once per component and modifier), :ignore, or a callable
68
+ # that receives the DaisyUI::UnknownModifierError (once per component and
69
+ # modifier), e.g. `->(error) { Appsignal.report_error(error) }`.
70
+ #
71
+ # Defaults to :raise under Rails in development and test, :warn otherwise.
72
+ def unknown_modifiers
73
+ @unknown_modifiers || default_unknown_modifiers
74
+ end
75
+
76
+ def unknown_modifiers=(mode)
77
+ valid = mode.nil? || mode.respond_to?(:call) || UNKNOWN_MODIFIER_MODES.include?(mode)
78
+ raise ArgumentError, "unknown_modifiers must be :raise, :warn, :ignore or a callable, got #{mode.inspect}" unless valid
79
+
80
+ @unknown_modifiers = mode
81
+ end
82
+
83
+ # @api private Builds the error only when it will be raised or reported.
84
+ def report_unknown_modifier(component:, modifier:, known:, part: nil)
85
+ mode = unknown_modifiers
86
+ return if mode == :ignore
87
+
88
+ error = -> { UnknownModifierError.new(component:, modifier:, known:, part:) }
89
+ raise error.call if mode == :raise
90
+ return unless first_report?([component, part, modifier])
91
+
92
+ mode == :warn ? warn("[DaisyUI] #{error.call.message}") : mode.call(error.call)
93
+ end
94
+
95
+ # @api private
96
+ def reset_reported_unknown_modifiers!
97
+ @reported_unknown_modifiers_lock.synchronize { @reported_unknown_modifiers.clear }
59
98
  end
60
99
 
61
100
  def prefix=(prefix)
@@ -70,6 +109,26 @@ module DaisyUI
70
109
  def modifiers
71
110
  @modifiers ||= Modifiers.new
72
111
  end
112
+
113
+ private
114
+
115
+ # Remembers up to REPORTED_UNKNOWN_MODIFIERS_LIMIT keys, then starts over,
116
+ # so modifiers built from user input cannot grow it without bound.
117
+ def first_report?(key)
118
+ @reported_unknown_modifiers_lock.synchronize do
119
+ return false if @reported_unknown_modifiers.include?(key)
120
+
121
+ @reported_unknown_modifiers.clear if @reported_unknown_modifiers.size >= REPORTED_UNKNOWN_MODIFIERS_LIMIT
122
+ @reported_unknown_modifiers.add(key)
123
+ true
124
+ end
125
+ end
126
+
127
+ def default_unknown_modifiers
128
+ return :warn unless defined?(::Rails) && ::Rails.respond_to?(:env)
129
+
130
+ %w[development test].include?(::Rails.env.to_s) ? :raise : :warn
131
+ end
73
132
  end
74
133
  end
75
134
  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,24 +4,34 @@ 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
 
27
37
  initializer "daisy_ui.importmap", before: "importmap" do |app|
@@ -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
@@ -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
@@ -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)
@@ -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-09-30 16:38:52 UTC"
6
+ UPDATED_AT = "2026-10-11 16:15: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.0"
4
+ VERSION = "2.0.2"
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.0
4
+ version: 2.0.2
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
@@ -157,6 +161,7 @@ files:
157
161
  - lib/daisy_ui/toast.rb
158
162
  - lib/daisy_ui/toggle.rb
159
163
  - lib/daisy_ui/tooltip.rb
164
+ - lib/daisy_ui/unknown_modifier_error.rb
160
165
  - lib/daisy_ui/updated_at.rb
161
166
  - lib/daisy_ui/validator.rb
162
167
  - lib/daisy_ui/version.rb