@djangocfg/ui-core 2.1.556 → 2.1.558

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 (37) hide show
  1. package/README.md +3 -3
  2. package/package.json +5 -5
  3. package/src/components/data/calendar/calendar.tsx +1 -1
  4. package/src/components/data/toggle/index.tsx +1 -1
  5. package/src/components/forms/button/index.tsx +6 -1
  6. package/src/components/forms/checkbox/index.tsx +1 -1
  7. package/src/components/forms/filter-button/index.tsx +145 -0
  8. package/src/components/forms/filter-menu/index.tsx +203 -0
  9. package/src/components/forms/mask-input/index.tsx +1 -1
  10. package/src/components/forms/segmented-input/index.tsx +1 -1
  11. package/src/components/forms/tags-input/index.tsx +1 -1
  12. package/src/components/index.ts +6 -0
  13. package/src/components/layout/filter-bar/index.tsx +48 -0
  14. package/src/components/navigation/command/index.tsx +19 -40
  15. package/src/components/navigation/tabs/index.tsx +102 -17
  16. package/src/components/navigation/tabs/use-tab-indicator.ts +68 -0
  17. package/src/components/overlay/drawer/index.tsx +113 -15
  18. package/src/components/overlay/popover/index.tsx +8 -2
  19. package/src/components/overlay/side-panel/index.tsx +77 -8
  20. package/src/components/select/combobox-async.tsx +26 -32
  21. package/src/components/select/combobox.tsx +36 -41
  22. package/src/components/select/country-select.tsx +21 -9
  23. package/src/components/select/language-select.tsx +21 -9
  24. package/src/components/select/multi-select-pro-async.tsx +3 -2
  25. package/src/components/select/multi-select-pro.tsx +3 -2
  26. package/src/components/select/multi-select.tsx +9 -30
  27. package/src/components/select/select.tsx +1 -1
  28. package/src/components/select/trigger.ts +23 -0
  29. package/src/hooks/dom/index.ts +1 -0
  30. package/src/hooks/dom/useHighlight.ts +68 -0
  31. package/src/styles/css/presets/dense.css +12 -0
  32. package/src/styles/css/presets/ios.css +12 -0
  33. package/src/styles/css/presets/soft.css +12 -0
  34. package/src/styles/css/utilities/filter-bar.css +61 -0
  35. package/src/styles/css/utilities/overlay.css +78 -0
  36. package/src/styles/css/utilities/tabs.css +53 -0
  37. package/src/styles/css/utilities.css +2 -0
@@ -0,0 +1,68 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+
5
+ /**
6
+ * The active-row state for a cmdk list inside a popover, and the scroll that
7
+ * has to accompany it.
8
+ *
9
+ * Two things are going on, and neither works alone:
10
+ *
11
+ * 1. **The highlight is controlled.** cmdk otherwise activates the FIRST row,
12
+ * so a picker with a selection opens showing the top of the list rather than
13
+ * the value it holds. `onValueChange` is mandatory with a controlled value —
14
+ * cmdk then stores nothing itself and delegates every move to the caller, so
15
+ * omitting it freezes the arrow keys.
16
+ *
17
+ * 2. **Opening has to scroll.** cmdk schedules its own `scrollIntoView` when
18
+ * the value changes, but the popover keeps its list MOUNTED between opens
19
+ * (`sameNodeAcrossOpens: true`), so on the second open nothing re-mounts and
20
+ * nothing re-scrolls; on the first, the value is set before 252 rows have
21
+ * finished registering and there is nothing to scroll to yet. Measured both
22
+ * ways: selection at index 84, `scrollTop` 0. So the scroll is driven from
23
+ * the OPEN transition, after a paint, rather than left to the library.
24
+ *
25
+ * Ongoing arrow-key movement stays cmdk's own job; this only handles the jump
26
+ * that opening owes the user.
27
+ *
28
+ * ```tsx
29
+ * const highlight = useHighlight(open, value)
30
+ * <Command value={highlight.value} onValueChange={highlight.setValue}>
31
+ * <CommandList ref={highlight.listRef}>…</CommandList>
32
+ * </Command>
33
+ * ```
34
+ */
35
+ export function useHighlight(open: boolean, selected: string | null | undefined) {
36
+ const [value, setValue] = React.useState("")
37
+ const listRef = React.useRef<HTMLDivElement | null>(null)
38
+
39
+ // `selected` is read through a ref so that changing the selection does not
40
+ // re-run the open effect — only the open transition itself should scroll.
41
+ const selectedRef = React.useRef(selected)
42
+ selectedRef.current = selected
43
+
44
+ React.useEffect(() => {
45
+ if (!open) return
46
+ setValue(selectedRef.current ?? "")
47
+ }, [open])
48
+
49
+ // Deliberately NOT `requestAnimationFrame`: a background tab never runs one,
50
+ // so a popover opened in an inactive window would stay parked at the top
51
+ // (this is how the bug first showed up — `document.hidden` and no scroll).
52
+ // A layout effect keyed on the resolved highlight runs as soon as React has
53
+ // committed the rows, in any tab, and before the browser paints.
54
+ React.useLayoutEffect(() => {
55
+ if (!open || !value) return
56
+ const list = listRef.current
57
+ if (!list) return
58
+
59
+ // The row is found by VALUE rather than by `aria-selected`, which cmdk sets
60
+ // in its own scheduled pass — by then this effect has already run.
61
+ const row = list.querySelector(
62
+ `[cmdk-item][data-value="${CSS.escape(value)}"]`
63
+ )
64
+ row?.scrollIntoView({ block: 'nearest' })
65
+ }, [open, value])
66
+
67
+ return { value, setValue, listRef }
68
+ }
@@ -15,6 +15,18 @@
15
15
  --radius-2xl: calc(0.25rem + 8px);
16
16
  --radius-3xl: calc(0.25rem + 12px);
17
17
  --radius-4xl: calc(0.25rem + 16px);
18
+ /* Type scale — a notch tighter than every other preset, which is the whole
19
+ point of this one. It previously set radii and colours only, so "dense"
20
+ rendered text at exactly the same size as the rest: the name promised a
21
+ density the tokens never delivered.
22
+ In `:root` only; a size does not change with the theme. */
23
+ --font-size-base: 0.75rem;
24
+ --font-size-sm: 0.6875rem;
25
+ --font-size-xs: 0.625rem;
26
+ --font-size-lg: 0.875rem;
27
+ --font-size-xl: 1rem;
28
+ --line-height-base: 1.4;
29
+
18
30
  }
19
31
 
20
32
  .dark {
@@ -57,6 +57,18 @@
57
57
  --radius-2xl: calc(0.75rem + 8px);
58
58
  --radius-3xl: calc(0.75rem + 12px);
59
59
  --radius-4xl: calc(0.75rem + 16px);
60
+ /* Type scale. Lives in `:root` only — a size is not a colour and does not
61
+ change with the theme; `macos`/`windows` duplicate it into `.dark`, which
62
+ is two places to edit for one value.
63
+ Without these a preset silently inherits base.css (14px body / 13px
64
+ `text-sm`) while `macos` renders 13/12, so the same component measured a
65
+ pixel apart depending on which preset an app loaded. */
66
+ --font-size-base: 0.8125rem;
67
+ --font-size-sm: 0.75rem;
68
+ --font-size-xs: 0.6875rem;
69
+ --font-size-lg: 0.9375rem;
70
+ --font-size-xl: 1.0625rem;
71
+
60
72
  }
61
73
 
62
74
  .dark {
@@ -23,6 +23,18 @@
23
23
  --radius-2xl: calc(1rem + 8px);
24
24
  --radius-3xl: calc(1rem + 12px);
25
25
  --radius-4xl: calc(1rem + 16px);
26
+ /* Type scale. Lives in `:root` only — a size is not a colour and does not
27
+ change with the theme; `macos`/`windows` duplicate it into `.dark`, which
28
+ is two places to edit for one value.
29
+ Without these a preset silently inherits base.css (14px body / 13px
30
+ `text-sm`) while `macos` renders 13/12, so the same component measured a
31
+ pixel apart depending on which preset an app loaded. */
32
+ --font-size-base: 0.8125rem;
33
+ --font-size-sm: 0.75rem;
34
+ --font-size-xs: 0.6875rem;
35
+ --font-size-lg: 0.9375rem;
36
+ --font-size-xl: 1.0625rem;
37
+
26
38
  }
27
39
 
28
40
  .dark {
@@ -0,0 +1,61 @@
1
+ /**
2
+ * A row of filter controls, and the pills in it.
3
+ *
4
+ * The row owns the height. Every control in it — a pill, a select, a sort menu —
5
+ * reads `--filter-control-h`, which is what stops a caller restyling somebody
6
+ * else's component to make one row line up. Both product frontends were doing
7
+ * exactly that: one overrode `SelectTrigger`, the other `Button size="sm"`.
8
+ */
9
+ .filter-bar {
10
+ /* Named so a host can set the row's density once, here, rather than per
11
+ control. 38px is the height both frontends independently arrived at. */
12
+ --filter-control-h: 2.375rem;
13
+
14
+ container-type: inline-size;
15
+ container-name: filter-bar;
16
+
17
+ display: flex;
18
+ flex-wrap: wrap;
19
+ align-items: center;
20
+ gap: 0.5rem;
21
+ }
22
+
23
+ /* Horizontal scroll instead of wrapping, for a row that must stay one line. */
24
+ .filter-bar[data-overflow="scroll"] {
25
+ flex-wrap: nowrap;
26
+ overflow-x: auto;
27
+ overscroll-behavior-x: contain;
28
+ scrollbar-width: none;
29
+ }
30
+
31
+ .filter-bar[data-overflow="scroll"]::-webkit-scrollbar {
32
+ display: none;
33
+ }
34
+
35
+ .filter-bar[data-overflow="scroll"] > * {
36
+ flex: none;
37
+ }
38
+
39
+ /**
40
+ * The label inside a pill, and the rule that hides it.
41
+ *
42
+ * It collapses when the ROW runs out of room, not when the window does: a
43
+ * toolbar in a narrow column on a wide monitor has the same problem as a phone,
44
+ * and a viewport media query answers neither. Hiding it is `display: none`
45
+ * rather than the `clip-path: inset(50%)` both projects used — that trick keeps
46
+ * the text in the accessibility tree while removing it from sight, which reads
47
+ * fine and measures wrong. The button carries an `aria-label` instead, so the
48
+ * name survives the collapse by design rather than by accident.
49
+ *
50
+ * A pill showing a CHOSEN value keeps its label: "SUV" is the answer, and an
51
+ * icon alone cannot say which question it answered.
52
+ */
53
+ @container filter-bar (max-width: 32rem) {
54
+ .filter-pill:not([data-answered="true"]) .filter-pill-label {
55
+ display: none;
56
+ }
57
+
58
+ .filter-pill:not([data-answered="true"]) {
59
+ padding-inline: 0.625rem;
60
+ }
61
+ }
@@ -22,3 +22,81 @@
22
22
  backdrop-filter: blur(var(--overlay-blur, 0px));
23
23
  -webkit-backdrop-filter: blur(var(--overlay-blur, 0px));
24
24
  }
25
+
26
+ /**
27
+ * The scrolling list inside a popover — a combobox's options, a command menu's
28
+ * results, any panel long enough to need a limit.
29
+ *
30
+ * It is a ROLE, not a size. The 300px ceiling is what reads comfortably; the
31
+ * real constraint is the gap the popover has to the edge of the window, which
32
+ * Radix publishes as `--radix-popover-content-available-height` on the content
33
+ * element. Taking the smaller of the two is what keeps a list opened near the
34
+ * bottom of the screen from flipping above its trigger and being clipped by the
35
+ * viewport — the failure this class exists to prevent.
36
+ *
37
+ * Outside a popover the Radix variable is unset and the fallback leaves the
38
+ * plain ceiling, so the class is safe anywhere.
39
+ *
40
+ * Override `--popover-scroll-max` (per theme, or on one instance) to raise or
41
+ * lower the ceiling; the viewport clamp always still applies.
42
+ *
43
+ * It must sit on the element that ACTUALLY SCROLLS — for cmdk that is
44
+ * `CommandList`, whose highlight moves by `scrollIntoView({block:'nearest'})`
45
+ * and therefore acts on the nearest scrollable ancestor. Capping a wrapper
46
+ * instead hands the scrolling to a container cmdk cannot see, and the arrow
47
+ * keys walk the highlight out of view.
48
+ *
49
+ * Plain CSS rather than a JIT-scanned utility, for the same reason as
50
+ * `.bg-overlay` above: it must compile no matter whose source the Tailwind
51
+ * content scan covers.
52
+ */
53
+ .popover-scroll-region {
54
+ /* The VIEWPORT clamp deliberately lives on the panel (`.popover-panel`), not
55
+ here: this element is only part of the panel, and capping it alone lets the
56
+ siblings above it — a search input, a header — push the panel past the edge
57
+ anyway. Measured: available 193px, list 193px, panel 236px, top −35px.
58
+ Here the job is only the comfortable ceiling plus the actual scrolling;
59
+ `min-height: 0` is what lets the flex parent shrink it below its content
60
+ when the viewport, not the ceiling, is the binding constraint. */
61
+ max-height: var(--popover-scroll-max, 18.75rem);
62
+ min-height: 0;
63
+ overflow-y: auto;
64
+ overflow-x: hidden;
65
+ overscroll-behavior: contain;
66
+ }
67
+
68
+ /**
69
+ * The part of a snapped drawer that is actually on screen.
70
+ *
71
+ * With snap points the panel is FULL height and vaul slides it down to the
72
+ * active point, so everything below that point sits under the bottom edge of
73
+ * the window. A footer pinned with `mt-auto` therefore pins itself to the
74
+ * bottom of the PANEL — measured 344px below the viewport, unreachable even
75
+ * with the body scrolled to its end, which is what made a drawer's submit
76
+ * button impossible to reach.
77
+ *
78
+ * `--snap-point-height` is vaul's offset — how much of the panel is hidden —
79
+ * so the visible height is what remains. Constraining the content column to it
80
+ * puts the footer back at the edge the reader can see, at every snap point,
81
+ * with no JavaScript following the drag.
82
+ */
83
+ .drawer-snap-viewport {
84
+ display: flex;
85
+ flex-direction: column;
86
+ height: calc(100% - var(--snap-point-height, 0px));
87
+ min-height: 0;
88
+ }
89
+
90
+ /**
91
+ * A popover panel that must never grow past the window.
92
+ *
93
+ * `PopoverContent` carries this by default. The height Radix publishes already
94
+ * accounts for `collisionPadding`, so the panel keeps its gap from the edge,
95
+ * and the column layout lets a `.popover-scroll-region` child absorb whatever
96
+ * is left after fixed siblings (search field, footer) have taken their share.
97
+ */
98
+ .popover-panel {
99
+ display: flex;
100
+ flex-direction: column;
101
+ max-height: var(--radix-popover-content-available-height, none);
102
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The sliding indicator behind a segmented tab list.
3
+ *
4
+ * One pill that travels to the selected tab, rather than a highlight that
5
+ * disappears from one tab and reappears on the next. The movement is what
6
+ * carries the meaning: these options are mutually exclusive, and the pill is
7
+ * the single thing being moved between them.
8
+ *
9
+ * Position and size arrive as `--tab-indicator-x` / `--tab-indicator-w`, which
10
+ * the list measures from the active trigger — tabs here are labelled with real
11
+ * words and are never equal width, so a fixed `--step` (the usual CSS-only
12
+ * trick) would drift the moment a label changed. Only `transform` and `width`
13
+ * animate, so the pill stays on the compositor.
14
+ *
15
+ * The indicator is a pseudo-element on the LIST rather than a background on the
16
+ * trigger: a background cannot animate between two different elements, which is
17
+ * exactly why the default variant popped instead of sliding.
18
+ */
19
+ .tabs-sliding {
20
+ position: relative;
21
+ isolation: isolate;
22
+ }
23
+
24
+ .tabs-sliding::before {
25
+ content: "";
26
+ position: absolute;
27
+ z-index: -1;
28
+ top: var(--tab-indicator-inset, 0.25rem);
29
+ bottom: var(--tab-indicator-inset, 0.25rem);
30
+ left: 0;
31
+ width: var(--tab-indicator-w, 0);
32
+ transform: translate3d(var(--tab-indicator-x, 0), 0, 0);
33
+ border-radius: var(--tab-indicator-radius, var(--radius-sm));
34
+ background: var(--tab-indicator-bg, var(--background));
35
+ box-shadow: var(--tab-indicator-shadow, 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1));
36
+ transition:
37
+ transform 260ms cubic-bezier(0.32, 0.72, 0, 1),
38
+ width 260ms cubic-bezier(0.32, 0.72, 0, 1);
39
+ }
40
+
41
+ /* Before the first measurement the pill has no width, so there is nothing to
42
+ see travelling in from the left edge on mount. */
43
+ .tabs-sliding[data-tab-indicator="idle"]::before {
44
+ opacity: 0;
45
+ }
46
+
47
+ /* A reader who has asked for less motion gets the destination, not the
48
+ journey — the pill still marks the right tab, it simply stops sliding. */
49
+ @media (prefers-reduced-motion: reduce) {
50
+ .tabs-sliding::before {
51
+ transition: none;
52
+ }
53
+ }
@@ -12,6 +12,8 @@
12
12
  @import './utilities/controls.css';
13
13
  @import './utilities/datetime-field.css';
14
14
  @import './utilities/step.css';
15
+ @import './utilities/filter-bar.css';
16
+ @import './utilities/tabs.css';
15
17
  @import './utilities/animations.css';
16
18
  @import './utilities/glass.css';
17
19
  @import './utilities/marquee.css';