phlex_kit 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 924514c45499b5f5d021cc5ec5416246c38ccfc0cb40bef9f65a9592e12960b3
4
- data.tar.gz: 9531c44809a4b586547c41a45e2274acf24618436f10ca6d54f368f975ad199e
3
+ metadata.gz: f17bf0029690be129f5e5d77449b9ebb5632d22151ed3508789683b135a7e03f
4
+ data.tar.gz: 2145e97971f4fba1e283d979f454562839a76bae767c29755d3e9ff01639e306
5
5
  SHA512:
6
- metadata.gz: edbe03d8c7b0eafca03b08f4da7e28b0f220be45e3725e18fcc82972d9737341180be65617bcfee479d67a150e3876da29551bf39a46d52459b9a0f794d1672f
7
- data.tar.gz: a25b1d74ac3d5cfb12dfd0f72a20bf1fcf92b115a7f25602cb62dfd0c19c77987db2ee02ee6fd4efd97548306b75d70e188eea2c56bbb4564cb2503d41f592bb
6
+ metadata.gz: 67b7a4e80b51796f3bfc1177be569bf13d064e01b491c5be427baf7275ca87f4fd45d243dfd5f3bdf49fd48735b00fe084ea17e93d48cca62d5b28401c33ca0e
7
+ data.tar.gz: fd0c992227463320e3869ac7917b8ac63c7bbbd5bc468388de7ef00cbec003e0f333c37dde32711c8fed263bb95abbdde2b9bef1789990e8070f4b2b6fc16d29
@@ -110,8 +110,16 @@
110
110
 
111
111
  Dark is the default; light is opt-in via <html data-theme="light"> (set it
112
112
  server-side to avoid a flash). "system" follows the OS via prefers-color-scheme.
113
- Keep the two light token sets in sync. */
114
- :root {
113
+ Keep the two light token sets in sync.
114
+
115
+ .pk-dark forces a SUBTREE dark on any page theme (shadcn's .dark wrapper):
116
+ element-level custom properties beat the :root light overrides for every
117
+ descendant. Panels opened from inside the subtree (popovers, dialogs)
118
+ inherit it — they stay DOM children even in the top layer. Caveat:
119
+ component rules forked on :root[data-theme="light"] still match inside
120
+ the island, but they are written in tokens, so they resolve to the
121
+ island's dark values. */
122
+ :root, .pk-dark {
115
123
  --pk-bg: #0a0a0a;
116
124
  --pk-surface: #171717;
117
125
  --pk-surface-2: #262626;
@@ -10,7 +10,7 @@
10
10
  Anthropic's brand faces (Styrene, Tiempos/Copernicus) are licensed and
11
11
  not shipped; --pk-font-serif falls back through Georgia. A host that has
12
12
  the real fonts re-points the token. */
13
- :root {
13
+ :root, .pk-dark {
14
14
  --pk-bg: #262624;
15
15
  --pk-surface: #30302e;
16
16
  --pk-surface-2: #3a3a37;
@@ -19,7 +19,7 @@
19
19
  - dark text-2 has no shadcn source (every dark *-foreground is 0.985);
20
20
  chart-1's 0.87 step (neutral-300) is used, matching the kit's own dark
21
21
  text-2 weight. */
22
- :root {
22
+ :root, .pk-dark {
23
23
  --pk-bg: oklch(0.145 0 0);
24
24
  --pk-surface: oklch(0.205 0 0);
25
25
  --pk-surface-2: oklch(0.269 0 0);
@@ -15,7 +15,7 @@
15
15
  through to _tokens.css defaults.
16
16
  - dark text-2 has no shadcn source (every dark *-foreground is 0.985);
17
17
  zinc-300 is used, matching the kit's own dark text-2 weight. */
18
- :root {
18
+ :root, .pk-dark {
19
19
  --pk-bg: oklch(0.141 0.005 285.823);
20
20
  --pk-surface: oklch(0.21 0.006 285.885);
21
21
  --pk-surface-2: oklch(0.274 0.006 286.033);
@@ -19,7 +19,9 @@ export default class extends Controller {
19
19
  }
20
20
 
21
21
  onClickOutside(event) {
22
- if (!this.openValue) return;
22
+ // Gate on the live :popover-open state, not the stored flag — a stale
23
+ // flag is how a close on an already-closed panel becomes an open.
24
+ if (!this.contentTarget.matches(":popover-open")) return;
23
25
  if (this.element.contains(event.target)) return;
24
26
 
25
27
  event.preventDefault();
@@ -14,8 +14,11 @@ import { Controller } from "@hotwired/stimulus"
14
14
  export default class extends Controller {
15
15
  static targets = ["menu"]
16
16
 
17
- initialize() {
18
- this.openMenu = null
17
+ // Which menu is open derives from the live :popover-open state, never a
18
+ // stored field — a stale field would make toggle() call showPopover() on
19
+ // an already-open panel (InvalidStateError) or strand an open one.
20
+ get openMenu() {
21
+ return this.menuTargets.find((menu) => this.panel(menu)?.matches(":popover-open")) ?? null
19
22
  }
20
23
 
21
24
  disconnect() {
@@ -57,7 +60,6 @@ export default class extends Controller {
57
60
  this.close()
58
61
  this.panel(menu)?.showPopover()
59
62
  menu.querySelector("[aria-expanded]")?.setAttribute("aria-expanded", "true")
60
- this.openMenu = menu
61
63
  }
62
64
  if (focus) this.items(menu)[0]?.focus()
63
65
  }
@@ -70,7 +72,6 @@ export default class extends Controller {
70
72
  if (panel?.matches(":popover-open")) panel.hidePopover()
71
73
  const trigger = menu.querySelector("[aria-expanded]")
72
74
  trigger?.setAttribute("aria-expanded", "false")
73
- this.openMenu = null
74
75
  if (opts.refocus === true) trigger?.focus()
75
76
  }
76
77
 
@@ -3,8 +3,8 @@
3
3
  (title + muted description) stacks at gap-0.5. `align: :end` flips the
4
4
  panel to the trigger's end edge. Tokens global.
5
5
  The panel is a native [popover] in the top layer, anchor-positioned to
6
- the trigger (Baseline 2026); position-try-fallbacks flips it above the
7
- trigger when it would overflow the viewport bottom. anchor-scope keeps
6
+ the trigger (Baseline 2026); position-try-fallbacks flips it on either
7
+ axis (or both) when it would overflow a viewport edge. anchor-scope keeps
8
8
  sibling popovers from resolving to each other's anchors. */
9
9
  .pk-popover { display: inline-block; anchor-scope: --pk-popover; }
10
10
  .pk-popover-trigger { display: inline-block; anchor-name: --pk-popover; }
@@ -14,7 +14,7 @@
14
14
  inset: auto;
15
15
  position-area: block-end span-inline-end;
16
16
  margin: .25rem 0 0;
17
- position-try-fallbacks: flip-block;
17
+ position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
18
18
  flex-direction: column; gap: .625rem;
19
19
  width: 18rem;
20
20
  border: 1px solid color-mix(in oklab, var(--pk-text) 10%, transparent);
@@ -2,14 +2,37 @@ import { Controller } from "@hotwired/stimulus"
2
2
 
3
3
  // Connects to data-controller="phlex-kit--popover". The panel is a native
4
4
  // [popover=auto] — the browser owns light dismiss + Escape; CSS anchor
5
- // positioning places it (popover.css). Light dismiss fires on pointerdown
6
- // outside the panel, so a click on the trigger would close-then-reopen:
7
- // armToggle records the open state at pointerdown and toggle skips the
8
- // reopen.
5
+ // positioning places it (popover.css). Toggling is native too: connect()
6
+ // points the trigger's button at the panel via popoverTargetElement, so the
7
+ // browser handles mouse + keyboard activation and exposes the expanded
8
+ // state to assistive tech — no imperative toggle, no invoker/light-dismiss
9
+ // race. Button-less triggers (the date picker's input) fall back to a
10
+ // click toggle; wasOpen records the open state at pointerdown because
11
+ // light dismiss fires first and the click would otherwise reopen.
9
12
  export default class extends Controller {
10
13
  static targets = ["trigger", "content"]
11
- armToggle() { this.wasOpen = this.contentTarget.matches(":popover-open") }
12
- toggle(e) { e?.preventDefault(); if (this.wasOpen) return; this.contentTarget.showPopover() }
14
+
15
+ connect() {
16
+ const invoker = this.triggerTarget.querySelector("button")
17
+ if (invoker) {
18
+ invoker.popoverTargetElement = this.contentTarget
19
+ } else {
20
+ this.triggerTarget.addEventListener("pointerdown", this.armToggle)
21
+ this.triggerTarget.addEventListener("click", this.toggle)
22
+ }
23
+ }
24
+
25
+ disconnect() {
26
+ this.triggerTarget.removeEventListener("pointerdown", this.armToggle)
27
+ this.triggerTarget.removeEventListener("click", this.toggle)
28
+ }
29
+
30
+ armToggle = () => { this.wasOpen = this.contentTarget.matches(":popover-open") }
31
+ toggle = () => {
32
+ if (this.wasOpen) { this.wasOpen = false; return }
33
+ this.contentTarget.togglePopover()
34
+ }
35
+
13
36
  contentTargetConnected(el) {
14
37
  el.addEventListener("toggle", (e) => { el.dataset.state = e.newState === "open" ? "open" : "closed" })
15
38
  }
@@ -1,8 +1,11 @@
1
1
  module PhlexKit
2
+ # Wrapper the panel anchors to. Carries no actions: the controller wires
3
+ # the button inside it to the panel via popoverTargetElement, so the
4
+ # browser owns toggling (mouse + keyboard). See popover.rb.
2
5
  class PopoverTrigger < BaseComponent
3
6
  def initialize(**attrs) = (@attrs = attrs)
4
7
  def view_template(&)
5
- div(**mix({ class: "pk-popover-trigger", data: { phlex_kit__popover_target: "trigger", action: "pointerdown->phlex-kit--popover#armToggle click->phlex-kit--popover#toggle" } }, @attrs), &)
8
+ div(**mix({ class: "pk-popover-trigger", data: { phlex_kit__popover_target: "trigger" } }, @attrs), &)
6
9
  end
7
10
  end
8
11
  end
@@ -89,21 +89,30 @@ export default class extends Controller {
89
89
  }
90
90
 
91
91
  clickOutside(event) {
92
- if (!this.openValue) return;
92
+ if (!this.contentTarget.matches(":popover-open")) return;
93
93
  if (this.element.contains(event.target)) return;
94
94
 
95
95
  event.preventDefault();
96
- this.toogleContent();
96
+ this.#hide();
97
97
  }
98
98
 
99
+ // Open/close derive from the live :popover-open state, never a stored
100
+ // flag — a stale flag is how a close on an already-closed panel becomes
101
+ // an open (bit the popover's keyboard toggle).
99
102
  toogleContent() {
100
- this.openValue = !this.openValue;
101
- if (this.openValue) {
102
- this.contentTarget.showPopover();
103
- } else if (this.contentTarget.matches(":popover-open")) {
104
- this.contentTarget.hidePopover();
105
- }
106
- this.triggerTarget.setAttribute("aria-expanded", this.openValue);
103
+ this.contentTarget.matches(":popover-open") ? this.#hide() : this.#show();
104
+ }
105
+
106
+ #show() {
107
+ this.openValue = true;
108
+ this.contentTarget.showPopover();
109
+ this.triggerTarget.setAttribute("aria-expanded", "true");
110
+ }
111
+
112
+ #hide() {
113
+ this.openValue = false;
114
+ if (this.contentTarget.matches(":popover-open")) this.contentTarget.hidePopover();
115
+ this.triggerTarget.setAttribute("aria-expanded", "false");
107
116
  }
108
117
 
109
118
  generateItemsIds() {
@@ -123,7 +132,7 @@ export default class extends Controller {
123
132
  }
124
133
 
125
134
  closeContent() {
126
- this.toogleContent();
135
+ this.#hide();
127
136
  this.resetCurrent();
128
137
 
129
138
  // aria-activedescendant holds an element id; on close it must be removed,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PhlexKit
4
- VERSION = "0.7.0"
4
+ VERSION = "0.8.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phlex_kit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Kennedy