@ladder-ui/select 0.4.0 → 0.5.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.
package/dist/index.js CHANGED
@@ -1,191 +1 @@
1
- 'use strict';
2
-
3
- Object.defineProperty(exports, '__esModule', { value: true });
4
-
5
- var jsxRuntime = require('react/jsx-runtime');
6
- var react = require('react');
7
- var reactDom = require('react-dom');
8
- var core = require('@ladder-ui/core');
9
- var primitives = require('@ladder-ui/primitives');
10
-
11
- const useIsomorphicLayoutEffect = typeof window !== "undefined" ? react.useLayoutEffect : react.useEffect;
12
- // ─── Icons ──────────────────────────────────────────────────────────────────
13
- function ChevronDownIcon({ className, style }) {
14
- return (jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", className: className, style: style, children: jsxRuntime.jsx("path", { d: "m6 9 6 6 6-6" }) }));
15
- }
16
- function CheckIcon({ className }) {
17
- return (jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", className: className, children: jsxRuntime.jsx("path", { d: "M20 6 9 17l-5-5" }) }));
18
- }
19
- // ─── Helper: Label Extraction ────────────────────────────────────────────────
20
- function extractTextContent(children) {
21
- if (typeof children === "string")
22
- return children;
23
- if (typeof children === "number")
24
- return String(children);
25
- if (Array.isArray(children))
26
- return children.map(extractTextContent).join("");
27
- if (react.isValidElement(children))
28
- return extractTextContent(children.props.children);
29
- return "";
30
- }
31
- function scanItemLabels(children, registry) {
32
- react.Children.forEach(children, (child) => {
33
- if (!react.isValidElement(child))
34
- return;
35
- // Check for SelectItem based on type or name
36
- const type = child.type;
37
- const dn = type.displayName || type.name;
38
- if (dn === "SelectItem") {
39
- const key = child.props.value ?? "";
40
- const label = extractTextContent(child.props.children);
41
- if (label)
42
- registry.set(key, label);
43
- }
44
- if (child.props.children) {
45
- scanItemLabels(child.props.children, registry);
46
- }
47
- });
48
- }
49
- const SelectContext = react.createContext(null);
50
- // ─── Variants ────────────────────────────────────────────────────────────────
51
- const selectTriggerVariants = core.cva({
52
- base: "lui-select-trigger",
53
- variants: {
54
- size: { sm: "lui-select-trigger--sm", md: "lui-select-trigger--md" },
55
- status: { default: "", error: "lui-select-trigger--error" },
56
- fullWidth: { true: "lui-select-trigger--full-width" },
57
- },
58
- defaultVariants: { size: "md", status: "default" },
59
- });
60
- function Select({ children, items, ...props }) {
61
- const select = primitives.useSelect(props);
62
- const labelsMap = react.useMemo(() => {
63
- const map = new Map();
64
- // 1. Scan children (Static JSX)
65
- scanItemLabels(children, map);
66
- // 2. Add explicit items (Dynamic/SDUI)
67
- items?.forEach(item => map.set(item.value, item.label));
68
- return map;
69
- }, [children, items]);
70
- const displayLabel = (select.value !== undefined ? labelsMap.get(select.value) : undefined) ?? "";
71
- return (jsxRuntime.jsx(SelectContext, { value: { ...select, displayLabel }, children: children }));
72
- }
73
- Select.displayName = "Select";
74
- const SelectTrigger = react.forwardRef(({ className, size, status, fullWidth, children, ...props }, ref) => {
75
- const ctx = react.use(SelectContext);
76
- if (!ctx)
77
- return null;
78
- const setRefs = (el) => {
79
- ctx.triggerRef.current = el;
80
- if (typeof ref === "function")
81
- ref(el);
82
- else if (ref)
83
- ref.current = el;
84
- };
85
- return (jsxRuntime.jsxs("button", { ref: setRefs, type: "button", role: "combobox", id: ctx.triggerId, "aria-expanded": ctx.open, "aria-haspopup": "listbox", "aria-controls": ctx.open ? ctx.contentId : undefined, disabled: ctx.disabled, className: selectTriggerVariants({ size, status, fullWidth, className }), onClick: (e) => {
86
- ctx.setOpen(!ctx.open);
87
- props.onClick?.(e);
88
- }, onKeyDown: (e) => {
89
- if (e.key === "Escape" && ctx.open) {
90
- e.preventDefault();
91
- ctx.setOpen(false);
92
- }
93
- if ((e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") && !ctx.open) {
94
- e.preventDefault();
95
- ctx.setOpen(true);
96
- }
97
- props.onKeyDown?.(e);
98
- }, ...props, children: [children, jsxRuntime.jsx(ChevronDownIcon, { className: "lui-select-trigger__chevron" })] }));
99
- });
100
- SelectTrigger.displayName = "SelectTrigger";
101
- const SelectValue = ({ placeholder, className }) => {
102
- const ctx = react.use(SelectContext);
103
- return (jsxRuntime.jsx("span", { className: core.concatClassNames("lui-select-value", className), "data-slot": "select-value", children: ctx?.displayLabel || placeholder }));
104
- };
105
- SelectValue.displayName = "SelectValue";
106
- const SelectContent = react.forwardRef(({ children, className, ...props }, ref) => {
107
- const ctx = react.use(SelectContext);
108
- const contentRef = react.useRef(null);
109
- const [coords, setCoords] = react.useState({ top: 0, left: 0, width: 0 });
110
- const [isPositioned, setIsPositioned] = react.useState(false);
111
- const { addRef } = primitives.useOutsideClick(() => ctx?.setOpen(false), ctx?.open);
112
- // Two-pass positioning strategy
113
- useIsomorphicLayoutEffect(() => {
114
- if (!ctx?.open || !ctx.triggerRef.current || !contentRef.current) {
115
- setIsPositioned(false);
116
- return;
117
- }
118
- const triggerRect = ctx.triggerRef.current.getBoundingClientRect();
119
- const content = contentRef.current;
120
- const contentHeight = content.offsetHeight;
121
- const viewportHeight = window.innerHeight;
122
- // Popper strategy: open below trigger
123
- let top = triggerRect.bottom + window.scrollY + 4;
124
- const left = triggerRect.left + window.scrollX;
125
- // Flip if no space below
126
- if (triggerRect.bottom + contentHeight + 8 > viewportHeight) {
127
- top = triggerRect.top + window.scrollY - contentHeight - 4;
128
- }
129
- setCoords({ top, left, width: triggerRect.width });
130
- setIsPositioned(true);
131
- }, [ctx?.open]);
132
- if (!ctx?.open)
133
- return null;
134
- return reactDom.createPortal(jsxRuntime.jsx("div", { ref: (node) => {
135
- contentRef.current = node;
136
- if (typeof ref === "function")
137
- ref(node);
138
- else if (ref)
139
- ref.current = node;
140
- if (node)
141
- addRef({ current: node });
142
- addRef(ctx.triggerRef);
143
- }, id: ctx.contentId, role: "listbox", "aria-labelledby": ctx.triggerId, "data-slot": "select-content", className: core.concatClassNames("lui-select-content", className), style: {
144
- position: "absolute",
145
- top: coords.top,
146
- left: coords.left,
147
- width: coords.width,
148
- minWidth: coords.width,
149
- zIndex: 9999,
150
- visibility: isPositioned ? "visible" : "hidden",
151
- }, onKeyDown: (e) => {
152
- if (e.key === "Escape") {
153
- ctx.setOpen(false);
154
- ctx.triggerRef.current?.focus();
155
- }
156
- props.onKeyDown?.(e);
157
- }, ...props, children: jsxRuntime.jsx("div", { className: "lui-select-content__viewport", children: children }) }), document.body);
158
- });
159
- SelectContent.displayName = "SelectContent";
160
- const SelectItem = react.forwardRef(({ value, children, disabled, className, ...props }, ref) => {
161
- const ctx = react.use(SelectContext);
162
- const isSelected = ctx?.value === value;
163
- return (jsxRuntime.jsxs("div", { ref: ref, role: "option", "aria-selected": isSelected, "aria-disabled": disabled, className: core.concatClassNames("lui-select-item", isSelected && "lui-select-item--selected", disabled && "lui-select-item--disabled", className), onClick: (e) => {
164
- if (!disabled) {
165
- ctx?.onValueChange(value === null ? "" : value);
166
- ctx?.setOpen(false);
167
- }
168
- props.onClick?.(e);
169
- }, ...props, children: [jsxRuntime.jsx("span", { className: "lui-select-item__indicator", "aria-hidden": "true", children: isSelected && jsxRuntime.jsx(CheckIcon, { className: "lui-select-item__check" }) }), jsxRuntime.jsx("span", { className: "lui-select-item__text", children: children })] }));
170
- });
171
- SelectItem.displayName = "SelectItem";
172
- const SelectGroup = react.forwardRef(({ className, ...props }, ref) => (jsxRuntime.jsx("div", { ref: ref, role: "group", className: core.concatClassNames("lui-select-group", className), ...props })));
173
- SelectGroup.displayName = "SelectGroup";
174
- const SelectLabel = react.forwardRef(({ className, ...props }, ref) => (jsxRuntime.jsx("div", { ref: ref, className: core.concatClassNames("lui-select-label", className), ...props })));
175
- SelectLabel.displayName = "SelectLabel";
176
- const SelectSeparator = react.forwardRef(({ className, ...props }, ref) => (jsxRuntime.jsx("hr", { ref: ref, className: core.concatClassNames("lui-select-separator", className), ...props })));
177
- SelectSeparator.displayName = "SelectSeparator";
178
- const SelectScrollUpButton = react.forwardRef(({ className, ...props }, ref) => (jsxRuntime.jsx("div", { ref: ref, className: core.concatClassNames("lui-select-scroll-button", className), "aria-hidden": "true", ...props, children: jsxRuntime.jsx(ChevronDownIcon, { className: "lui-select-scroll-button__icon", style: { transform: "rotate(180deg)" } }) })));
179
- SelectScrollUpButton.displayName = "SelectScrollUpButton";
180
- const SelectScrollDownButton = react.forwardRef(({ className, ...props }, ref) => (jsxRuntime.jsx("div", { ref: ref, className: core.concatClassNames("lui-select-scroll-button", className), "aria-hidden": "true", ...props, children: jsxRuntime.jsx(ChevronDownIcon, { className: "lui-select-scroll-button__icon" }) })));
181
- SelectScrollDownButton.displayName = "SelectScrollDownButton";
182
-
183
- exports.Select = Select;
184
- exports.SelectContent = SelectContent;
185
- exports.SelectGroup = SelectGroup;
186
- exports.SelectItem = SelectItem;
187
- exports.SelectLabel = SelectLabel;
188
- exports.SelectSeparator = SelectSeparator;
189
- exports.SelectTrigger = SelectTrigger;
190
- exports.SelectValue = SelectValue;
191
- exports.default = Select;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("react/jsx-runtime"),t=require("react"),s=require("react-dom"),l=require("@ladder-ui/core"),r=require("@ladder-ui/primitives");const a="undefined"!=typeof window?t.useLayoutEffect:t.useEffect;function n({className:t,style:s}){return e.jsx("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":"true",className:t,style:s,children:e.jsx("path",{d:"m6 9 6 6 6-6"})})}function i({className:t}){return e.jsx("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2.5,strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":"true",className:t,children:e.jsx("path",{d:"M20 6 9 17l-5-5"})})}function o(e){return"string"==typeof e?e:"number"==typeof e?String(e):Array.isArray(e)?e.map(o).join(""):t.isValidElement(e)?o(e.props.children):""}function c(e,s){t.Children.forEach(e,e=>{if(!t.isValidElement(e))return;const l=e.type;if("SelectItem"===(l.displayName||l.name)){const t=e.props.value??"",l=o(e.props.children);l&&s.set(t,l)}e.props.children&&c(e.props.children,s)})}const d=t.createContext(null),u=l.cva({base:"lui-select-trigger",variants:{size:{sm:"lui-select-trigger--sm",md:"lui-select-trigger--md"},status:{default:"",error:"lui-select-trigger--error"},fullWidth:{true:"lui-select-trigger--full-width"}},defaultVariants:{size:"md",status:"default"}});function p({children:s,items:l,...a}){const n=r.useSelect(a),i=t.useMemo(()=>{const e=new Map;return c(s,e),l?.forEach(t=>e.set(t.value,t.label)),e},[s,l]),o=(void 0!==n.value?i.get(n.value):void 0)??"";return e.jsx(d,{value:{...n,displayLabel:o},children:s})}p.displayName="Select";const m=t.forwardRef(({className:s,size:l,status:r,fullWidth:a,children:i,...o},c)=>{const p=t.use(d);return p?e.jsxs("button",{ref:e=>{p.triggerRef.current=e,"function"==typeof c?c(e):c&&(c.current=e)},type:"button",role:"combobox",id:p.triggerId,"aria-expanded":p.open,"aria-haspopup":"listbox","aria-controls":p.open?p.contentId:void 0,disabled:p.disabled,className:u({size:l,status:r,fullWidth:a,className:s}),onClick:e=>{p.setOpen(!p.open),o.onClick?.(e)},onKeyDown:e=>{"Escape"===e.key&&p.open&&(e.preventDefault(),p.setOpen(!1)),"ArrowDown"!==e.key&&"Enter"!==e.key&&" "!==e.key||p.open||(e.preventDefault(),p.setOpen(!0)),o.onKeyDown?.(e)},...o,children:[i,e.jsx(n,{className:"lui-select-trigger__chevron"})]}):null});m.displayName="SelectTrigger";const f=({placeholder:s,className:r})=>{const a=t.use(d);return e.jsx("span",{className:l.concatClassNames("lui-select-value",r),"data-slot":"select-value",children:a?.displayLabel||s})};f.displayName="SelectValue";const h=t.forwardRef(({children:n,className:i,...o},c)=>{const u=t.use(d),p=t.useRef(null),[m,f]=t.useState({top:0,left:0,width:0}),[h,N]=t.useState(!1),{addRef:g}=r.useOutsideClick(()=>u?.setOpen(!1),u?.open);return a(()=>{if(!u?.open||!u.triggerRef.current||!p.current)return void N(!1);const e=u.triggerRef.current.getBoundingClientRect(),t=p.current.offsetHeight,s=window.innerHeight;let l=e.bottom+window.scrollY+4;const r=e.left+window.scrollX;e.bottom+t+8>s&&(l=e.top+window.scrollY-t-4),f({top:l,left:r,width:e.width}),N(!0)},[u?.open]),u?.open?s.createPortal(e.jsx("div",{ref:e=>{p.current=e,"function"==typeof c?c(e):c&&(c.current=e),e&&g({current:e}),g(u.triggerRef)},id:u.contentId,role:"listbox","aria-labelledby":u.triggerId,"data-slot":"select-content",className:l.concatClassNames("lui-select-content",i),style:{position:"absolute",top:m.top,left:m.left,width:m.width,minWidth:m.width,zIndex:9999,visibility:h?"visible":"hidden"},onKeyDown:e=>{"Escape"===e.key&&(u.setOpen(!1),u.triggerRef.current?.focus()),o.onKeyDown?.(e)},...o,children:e.jsx("div",{className:"lui-select-content__viewport",children:n})}),document.body):null});h.displayName="SelectContent";const N=t.forwardRef(({value:s,children:r,disabled:a,className:n,...o},c)=>{const u=t.use(d),p=u?.value===s;return e.jsxs("div",{ref:c,role:"option","aria-selected":p,"aria-disabled":a,className:l.concatClassNames("lui-select-item",p&&"lui-select-item--selected",a&&"lui-select-item--disabled",n),onClick:e=>{a||(u?.onValueChange(null===s?"":s),u?.setOpen(!1)),o.onClick?.(e)},...o,children:[e.jsx("span",{className:"lui-select-item__indicator","aria-hidden":"true",children:p&&e.jsx(i,{className:"lui-select-item__check"})}),e.jsx("span",{className:"lui-select-item__text",children:r})]})});N.displayName="SelectItem";const g=t.forwardRef(({className:t,...s},r)=>e.jsx("div",{ref:r,role:"group",className:l.concatClassNames("lui-select-group",t),...s}));g.displayName="SelectGroup";const w=t.forwardRef(({className:t,...s},r)=>e.jsx("div",{ref:r,className:l.concatClassNames("lui-select-label",t),...s}));w.displayName="SelectLabel";const x=t.forwardRef(({className:t,...s},r)=>e.jsx("hr",{ref:r,className:l.concatClassNames("lui-select-separator",t),...s}));x.displayName="SelectSeparator",t.forwardRef(({className:t,...s},r)=>e.jsx("div",{ref:r,className:l.concatClassNames("lui-select-scroll-button",t),"aria-hidden":"true",...s,children:e.jsx(n,{className:"lui-select-scroll-button__icon",style:{transform:"rotate(180deg)"}})})).displayName="SelectScrollUpButton",t.forwardRef(({className:t,...s},r)=>e.jsx("div",{ref:r,className:l.concatClassNames("lui-select-scroll-button",t),"aria-hidden":"true",...s,children:e.jsx(n,{className:"lui-select-scroll-button__icon"})})).displayName="SelectScrollDownButton",exports.Select=p,exports.SelectContent=h,exports.SelectGroup=g,exports.SelectItem=N,exports.SelectLabel=w,exports.SelectSeparator=x,exports.SelectTrigger=m,exports.SelectValue=f,exports.default=p;
package/dist/index.mjs CHANGED
@@ -1,179 +1 @@
1
- import { jsxs, jsx } from 'react/jsx-runtime';
2
- import { createContext, forwardRef, use, useRef, useState, useLayoutEffect, useEffect, useMemo, Children, isValidElement } from 'react';
3
- import { createPortal } from 'react-dom';
4
- import { cva, concatClassNames } from '@ladder-ui/core';
5
- import { useOutsideClick, useSelect } from '@ladder-ui/primitives';
6
-
7
- const useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
8
- // ─── Icons ──────────────────────────────────────────────────────────────────
9
- function ChevronDownIcon({ className, style }) {
10
- return (jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", className: className, style: style, children: jsx("path", { d: "m6 9 6 6 6-6" }) }));
11
- }
12
- function CheckIcon({ className }) {
13
- return (jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", className: className, children: jsx("path", { d: "M20 6 9 17l-5-5" }) }));
14
- }
15
- // ─── Helper: Label Extraction ────────────────────────────────────────────────
16
- function extractTextContent(children) {
17
- if (typeof children === "string")
18
- return children;
19
- if (typeof children === "number")
20
- return String(children);
21
- if (Array.isArray(children))
22
- return children.map(extractTextContent).join("");
23
- if (isValidElement(children))
24
- return extractTextContent(children.props.children);
25
- return "";
26
- }
27
- function scanItemLabels(children, registry) {
28
- Children.forEach(children, (child) => {
29
- if (!isValidElement(child))
30
- return;
31
- // Check for SelectItem based on type or name
32
- const type = child.type;
33
- const dn = type.displayName || type.name;
34
- if (dn === "SelectItem") {
35
- const key = child.props.value ?? "";
36
- const label = extractTextContent(child.props.children);
37
- if (label)
38
- registry.set(key, label);
39
- }
40
- if (child.props.children) {
41
- scanItemLabels(child.props.children, registry);
42
- }
43
- });
44
- }
45
- const SelectContext = createContext(null);
46
- // ─── Variants ────────────────────────────────────────────────────────────────
47
- const selectTriggerVariants = cva({
48
- base: "lui-select-trigger",
49
- variants: {
50
- size: { sm: "lui-select-trigger--sm", md: "lui-select-trigger--md" },
51
- status: { default: "", error: "lui-select-trigger--error" },
52
- fullWidth: { true: "lui-select-trigger--full-width" },
53
- },
54
- defaultVariants: { size: "md", status: "default" },
55
- });
56
- function Select({ children, items, ...props }) {
57
- const select = useSelect(props);
58
- const labelsMap = useMemo(() => {
59
- const map = new Map();
60
- // 1. Scan children (Static JSX)
61
- scanItemLabels(children, map);
62
- // 2. Add explicit items (Dynamic/SDUI)
63
- items?.forEach(item => map.set(item.value, item.label));
64
- return map;
65
- }, [children, items]);
66
- const displayLabel = (select.value !== undefined ? labelsMap.get(select.value) : undefined) ?? "";
67
- return (jsx(SelectContext, { value: { ...select, displayLabel }, children: children }));
68
- }
69
- Select.displayName = "Select";
70
- const SelectTrigger = forwardRef(({ className, size, status, fullWidth, children, ...props }, ref) => {
71
- const ctx = use(SelectContext);
72
- if (!ctx)
73
- return null;
74
- const setRefs = (el) => {
75
- ctx.triggerRef.current = el;
76
- if (typeof ref === "function")
77
- ref(el);
78
- else if (ref)
79
- ref.current = el;
80
- };
81
- return (jsxs("button", { ref: setRefs, type: "button", role: "combobox", id: ctx.triggerId, "aria-expanded": ctx.open, "aria-haspopup": "listbox", "aria-controls": ctx.open ? ctx.contentId : undefined, disabled: ctx.disabled, className: selectTriggerVariants({ size, status, fullWidth, className }), onClick: (e) => {
82
- ctx.setOpen(!ctx.open);
83
- props.onClick?.(e);
84
- }, onKeyDown: (e) => {
85
- if (e.key === "Escape" && ctx.open) {
86
- e.preventDefault();
87
- ctx.setOpen(false);
88
- }
89
- if ((e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") && !ctx.open) {
90
- e.preventDefault();
91
- ctx.setOpen(true);
92
- }
93
- props.onKeyDown?.(e);
94
- }, ...props, children: [children, jsx(ChevronDownIcon, { className: "lui-select-trigger__chevron" })] }));
95
- });
96
- SelectTrigger.displayName = "SelectTrigger";
97
- const SelectValue = ({ placeholder, className }) => {
98
- const ctx = use(SelectContext);
99
- return (jsx("span", { className: concatClassNames("lui-select-value", className), "data-slot": "select-value", children: ctx?.displayLabel || placeholder }));
100
- };
101
- SelectValue.displayName = "SelectValue";
102
- const SelectContent = forwardRef(({ children, className, ...props }, ref) => {
103
- const ctx = use(SelectContext);
104
- const contentRef = useRef(null);
105
- const [coords, setCoords] = useState({ top: 0, left: 0, width: 0 });
106
- const [isPositioned, setIsPositioned] = useState(false);
107
- const { addRef } = useOutsideClick(() => ctx?.setOpen(false), ctx?.open);
108
- // Two-pass positioning strategy
109
- useIsomorphicLayoutEffect(() => {
110
- if (!ctx?.open || !ctx.triggerRef.current || !contentRef.current) {
111
- setIsPositioned(false);
112
- return;
113
- }
114
- const triggerRect = ctx.triggerRef.current.getBoundingClientRect();
115
- const content = contentRef.current;
116
- const contentHeight = content.offsetHeight;
117
- const viewportHeight = window.innerHeight;
118
- // Popper strategy: open below trigger
119
- let top = triggerRect.bottom + window.scrollY + 4;
120
- const left = triggerRect.left + window.scrollX;
121
- // Flip if no space below
122
- if (triggerRect.bottom + contentHeight + 8 > viewportHeight) {
123
- top = triggerRect.top + window.scrollY - contentHeight - 4;
124
- }
125
- setCoords({ top, left, width: triggerRect.width });
126
- setIsPositioned(true);
127
- }, [ctx?.open]);
128
- if (!ctx?.open)
129
- return null;
130
- return createPortal(jsx("div", { ref: (node) => {
131
- contentRef.current = node;
132
- if (typeof ref === "function")
133
- ref(node);
134
- else if (ref)
135
- ref.current = node;
136
- if (node)
137
- addRef({ current: node });
138
- addRef(ctx.triggerRef);
139
- }, id: ctx.contentId, role: "listbox", "aria-labelledby": ctx.triggerId, "data-slot": "select-content", className: concatClassNames("lui-select-content", className), style: {
140
- position: "absolute",
141
- top: coords.top,
142
- left: coords.left,
143
- width: coords.width,
144
- minWidth: coords.width,
145
- zIndex: 9999,
146
- visibility: isPositioned ? "visible" : "hidden",
147
- }, onKeyDown: (e) => {
148
- if (e.key === "Escape") {
149
- ctx.setOpen(false);
150
- ctx.triggerRef.current?.focus();
151
- }
152
- props.onKeyDown?.(e);
153
- }, ...props, children: jsx("div", { className: "lui-select-content__viewport", children: children }) }), document.body);
154
- });
155
- SelectContent.displayName = "SelectContent";
156
- const SelectItem = forwardRef(({ value, children, disabled, className, ...props }, ref) => {
157
- const ctx = use(SelectContext);
158
- const isSelected = ctx?.value === value;
159
- return (jsxs("div", { ref: ref, role: "option", "aria-selected": isSelected, "aria-disabled": disabled, className: concatClassNames("lui-select-item", isSelected && "lui-select-item--selected", disabled && "lui-select-item--disabled", className), onClick: (e) => {
160
- if (!disabled) {
161
- ctx?.onValueChange(value === null ? "" : value);
162
- ctx?.setOpen(false);
163
- }
164
- props.onClick?.(e);
165
- }, ...props, children: [jsx("span", { className: "lui-select-item__indicator", "aria-hidden": "true", children: isSelected && jsx(CheckIcon, { className: "lui-select-item__check" }) }), jsx("span", { className: "lui-select-item__text", children: children })] }));
166
- });
167
- SelectItem.displayName = "SelectItem";
168
- const SelectGroup = forwardRef(({ className, ...props }, ref) => (jsx("div", { ref: ref, role: "group", className: concatClassNames("lui-select-group", className), ...props })));
169
- SelectGroup.displayName = "SelectGroup";
170
- const SelectLabel = forwardRef(({ className, ...props }, ref) => (jsx("div", { ref: ref, className: concatClassNames("lui-select-label", className), ...props })));
171
- SelectLabel.displayName = "SelectLabel";
172
- const SelectSeparator = forwardRef(({ className, ...props }, ref) => (jsx("hr", { ref: ref, className: concatClassNames("lui-select-separator", className), ...props })));
173
- SelectSeparator.displayName = "SelectSeparator";
174
- const SelectScrollUpButton = forwardRef(({ className, ...props }, ref) => (jsx("div", { ref: ref, className: concatClassNames("lui-select-scroll-button", className), "aria-hidden": "true", ...props, children: jsx(ChevronDownIcon, { className: "lui-select-scroll-button__icon", style: { transform: "rotate(180deg)" } }) })));
175
- SelectScrollUpButton.displayName = "SelectScrollUpButton";
176
- const SelectScrollDownButton = forwardRef(({ className, ...props }, ref) => (jsx("div", { ref: ref, className: concatClassNames("lui-select-scroll-button", className), "aria-hidden": "true", ...props, children: jsx(ChevronDownIcon, { className: "lui-select-scroll-button__icon" }) })));
177
- SelectScrollDownButton.displayName = "SelectScrollDownButton";
178
-
179
- export { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectSeparator, SelectTrigger, SelectValue, Select as default };
1
+ import{jsxs as e,jsx as t}from"react/jsx-runtime";import{createContext as l,forwardRef as r,use as s,useRef as i,useState as n,useLayoutEffect as a,useEffect as o,useMemo as c,Children as d,isValidElement as u}from"react";import{createPortal as p}from"react-dom";import{cva as m,concatClassNames as f}from"@ladder-ui/core";import{useOutsideClick as h,useSelect as g}from"@ladder-ui/primitives";const N="undefined"!=typeof window?a:o;function y({className:e,style:l}){return t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":"true",className:e,style:l,children:t("path",{d:"m6 9 6 6 6-6"})})}function w({className:e}){return t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2.5,strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":"true",className:e,children:t("path",{d:"M20 6 9 17l-5-5"})})}function v(e){return"string"==typeof e?e:"number"==typeof e?String(e):Array.isArray(e)?e.map(v).join(""):u(e)?v(e.props.children):""}function b(e,t){d.forEach(e,e=>{if(!u(e))return;const l=e.type;if("SelectItem"===(l.displayName||l.name)){const l=e.props.value??"",r=v(e.props.children);r&&t.set(l,r)}e.props.children&&b(e.props.children,t)})}const k=l(null),S=m({base:"lui-select-trigger",variants:{size:{sm:"lui-select-trigger--sm",md:"lui-select-trigger--md"},status:{default:"",error:"lui-select-trigger--error"},fullWidth:{true:"lui-select-trigger--full-width"}},defaultVariants:{size:"md",status:"default"}});function _({children:e,items:l,...r}){const s=g(r),i=c(()=>{const t=new Map;return b(e,t),l?.forEach(e=>t.set(e.value,e.label)),t},[e,l]),n=(void 0!==s.value?i.get(s.value):void 0)??"";return t(k,{value:{...s,displayLabel:n},children:e})}_.displayName="Select";const x=r(({className:l,size:r,status:i,fullWidth:n,children:a,...o},c)=>{const d=s(k);return d?e("button",{ref:e=>{d.triggerRef.current=e,"function"==typeof c?c(e):c&&(c.current=e)},type:"button",role:"combobox",id:d.triggerId,"aria-expanded":d.open,"aria-haspopup":"listbox","aria-controls":d.open?d.contentId:void 0,disabled:d.disabled,className:S({size:r,status:i,fullWidth:n,className:l}),onClick:e=>{d.setOpen(!d.open),o.onClick?.(e)},onKeyDown:e=>{"Escape"===e.key&&d.open&&(e.preventDefault(),d.setOpen(!1)),"ArrowDown"!==e.key&&"Enter"!==e.key&&" "!==e.key||d.open||(e.preventDefault(),d.setOpen(!0)),o.onKeyDown?.(e)},...o,children:[a,t(y,{className:"lui-select-trigger__chevron"})]}):null});x.displayName="SelectTrigger";const C=({placeholder:e,className:l})=>{const r=s(k);return t("span",{className:f("lui-select-value",l),"data-slot":"select-value",children:r?.displayLabel||e})};C.displayName="SelectValue";const D=r(({children:e,className:l,...r},a)=>{const o=s(k),c=i(null),[d,u]=n({top:0,left:0,width:0}),[m,g]=n(!1),{addRef:y}=h(()=>o?.setOpen(!1),o?.open);return N(()=>{if(!o?.open||!o.triggerRef.current||!c.current)return void g(!1);const e=o.triggerRef.current.getBoundingClientRect(),t=c.current.offsetHeight,l=window.innerHeight;let r=e.bottom+window.scrollY+4;const s=e.left+window.scrollX;e.bottom+t+8>l&&(r=e.top+window.scrollY-t-4),u({top:r,left:s,width:e.width}),g(!0)},[o?.open]),o?.open?p(t("div",{ref:e=>{c.current=e,"function"==typeof a?a(e):a&&(a.current=e),e&&y({current:e}),y(o.triggerRef)},id:o.contentId,role:"listbox","aria-labelledby":o.triggerId,"data-slot":"select-content",className:f("lui-select-content",l),style:{position:"absolute",top:d.top,left:d.left,width:d.width,minWidth:d.width,zIndex:9999,visibility:m?"visible":"hidden"},onKeyDown:e=>{"Escape"===e.key&&(o.setOpen(!1),o.triggerRef.current?.focus()),r.onKeyDown?.(e)},...r,children:t("div",{className:"lui-select-content__viewport",children:e})}),document.body):null});D.displayName="SelectContent";const I=r(({value:l,children:r,disabled:i,className:n,...a},o)=>{const c=s(k),d=c?.value===l;return e("div",{ref:o,role:"option","aria-selected":d,"aria-disabled":i,className:f("lui-select-item",d&&"lui-select-item--selected",i&&"lui-select-item--disabled",n),onClick:e=>{i||(c?.onValueChange(null===l?"":l),c?.setOpen(!1)),a.onClick?.(e)},...a,children:[t("span",{className:"lui-select-item__indicator","aria-hidden":"true",children:d&&t(w,{className:"lui-select-item__check"})}),t("span",{className:"lui-select-item__text",children:r})]})});I.displayName="SelectItem";const L=r(({className:e,...l},r)=>t("div",{ref:r,role:"group",className:f("lui-select-group",e),...l}));L.displayName="SelectGroup";const R=r(({className:e,...l},r)=>t("div",{ref:r,className:f("lui-select-label",e),...l}));R.displayName="SelectLabel";const O=r(({className:e,...l},r)=>t("hr",{ref:r,className:f("lui-select-separator",e),...l}));O.displayName="SelectSeparator",r(({className:e,...l},r)=>t("div",{ref:r,className:f("lui-select-scroll-button",e),"aria-hidden":"true",...l,children:t(y,{className:"lui-select-scroll-button__icon",style:{transform:"rotate(180deg)"}})})).displayName="SelectScrollUpButton",r(({className:e,...l},r)=>t("div",{ref:r,className:f("lui-select-scroll-button",e),"aria-hidden":"true",...l,children:t(y,{className:"lui-select-scroll-button__icon"})})).displayName="SelectScrollDownButton";export{_ as Select,D as SelectContent,L as SelectGroup,I as SelectItem,R as SelectLabel,O as SelectSeparator,x as SelectTrigger,C as SelectValue,_ as default};
package/dist/select.css CHANGED
@@ -1,147 +1 @@
1
- @charset "UTF-8";
2
- @layer components {
3
- .lui-select-trigger {
4
- position: relative;
5
- display: inline-flex;
6
- align-items: center;
7
- justify-content: space-between;
8
- gap: 0.5rem;
9
- width: fit-content;
10
- height: var(--lui-select-trigger-height);
11
- padding: var(--lui-select-trigger-py) var(--lui-select-trigger-px);
12
- background-color: var(--lui-select-trigger-bg);
13
- color: var(--lui-select-trigger-text);
14
- border: 1px solid var(--lui-select-trigger-border);
15
- border-radius: var(--lui-select-trigger-radius);
16
- font-family: var(--lui-font-family);
17
- font-size: 0.875rem;
18
- line-height: 1.25;
19
- white-space: nowrap;
20
- text-align: left;
21
- cursor: pointer;
22
- transition: var(--lui-transition);
23
- vertical-align: middle;
24
- outline: none;
25
- user-select: none;
26
- }
27
- .lui-select-trigger:focus-visible {
28
- border-color: var(--lui-select-focus-border);
29
- box-shadow: 0 0 0 2px var(--lui-bg-surface), 0 0 0 4px var(--lui-select-focus-ring-color);
30
- }
31
- .lui-select-trigger:disabled, .lui-select-trigger[aria-disabled=true] {
32
- opacity: var(--lui-disabled-opacity);
33
- cursor: var(--lui-disabled-cursor);
34
- pointer-events: none;
35
- }
36
- .lui-select-trigger--sm {
37
- height: var(--lui-select-trigger-height-sm);
38
- font-size: 0.8125rem;
39
- }
40
- .lui-select-trigger--full-width {
41
- width: 100%;
42
- }
43
- .lui-select-trigger__chevron {
44
- width: 1rem;
45
- height: 1rem;
46
- opacity: 0.5;
47
- flex-shrink: 0;
48
- pointer-events: none;
49
- }
50
- .lui-select-value {
51
- overflow: hidden;
52
- text-overflow: ellipsis;
53
- white-space: nowrap;
54
- flex: 1;
55
- min-width: 0;
56
- }
57
- .lui-select-value:empty::before {
58
- content: "​";
59
- }
60
- .lui-select-content {
61
- background-color: var(--lui-select-content-bg);
62
- border: 1px solid var(--lui-select-content-border);
63
- border-radius: var(--lui-select-content-radius);
64
- box-shadow: var(--lui-select-content-shadow);
65
- overflow: hidden;
66
- animation: lui-select-fade-in 0.12s ease-out;
67
- }
68
- .lui-select-content__viewport {
69
- overflow-y: auto;
70
- max-height: var(--lui-select-content-max-height);
71
- overscroll-behavior: contain;
72
- padding: 0.25rem;
73
- scroll-padding-block: 0.25rem;
74
- }
75
- @keyframes lui-select-fade-in {
76
- from {
77
- opacity: 0;
78
- transform: scale(0.97);
79
- }
80
- to {
81
- opacity: 1;
82
- transform: scale(1);
83
- }
84
- }
85
- .lui-select-label {
86
- padding: 0.375rem 0.5rem 0.25rem;
87
- font-size: 0.75rem;
88
- font-weight: 500;
89
- color: var(--lui-select-label-text);
90
- user-select: none;
91
- font-family: var(--lui-font-family);
92
- }
93
- .lui-select-item {
94
- position: relative;
95
- display: flex;
96
- align-items: center;
97
- gap: 0.375rem;
98
- padding: var(--lui-select-item-py) var(--lui-select-item-px);
99
- padding-right: calc(var(--lui-select-item-px) + var(--lui-select-item-indicator-size) + 0.375rem);
100
- border-radius: var(--lui-select-item-radius);
101
- font-size: 0.875rem;
102
- color: var(--lui-select-item-text);
103
- cursor: pointer;
104
- user-select: none;
105
- outline: none;
106
- transition: background-color 0.1s ease, color 0.1s ease;
107
- font-family: var(--lui-font-family);
108
- }
109
- .lui-select-item:hover:not(.lui-select-item--disabled) {
110
- background-color: var(--lui-select-item-hover-bg);
111
- color: var(--lui-select-item-hover-text);
112
- }
113
- .lui-select-item--selected {
114
- color: var(--lui-select-item-selected-text);
115
- }
116
- .lui-select-item--disabled {
117
- opacity: var(--lui-disabled-opacity);
118
- cursor: not-allowed;
119
- pointer-events: none;
120
- }
121
- .lui-select-item__indicator {
122
- position: absolute;
123
- right: var(--lui-select-item-px);
124
- display: flex;
125
- align-items: center;
126
- justify-content: center;
127
- width: var(--lui-select-item-indicator-size);
128
- height: var(--lui-select-item-indicator-size);
129
- flex-shrink: 0;
130
- }
131
- .lui-select-item__check {
132
- width: 0.875rem;
133
- height: 0.875rem;
134
- color: var(--lui-select-item-selected-text);
135
- }
136
- .lui-select-item__text {
137
- overflow: hidden;
138
- text-overflow: ellipsis;
139
- white-space: nowrap;
140
- flex: 1;
141
- }
142
- .lui-select-separator {
143
- border: none;
144
- border-top: 1px solid var(--lui-select-separator-color);
145
- margin: 0.25rem -0.25rem;
146
- }
147
- }
1
+ @charset "UTF-8";@layer components{.lui-select-trigger{align-items:center;background-color:var(--lui-select-trigger-bg);border:1px solid var(--lui-select-trigger-border);border-radius:var(--lui-select-trigger-radius);color:var(--lui-select-trigger-text);cursor:pointer;display:inline-flex;font-family:var(--lui-font-family);font-size:.875rem;gap:.5rem;height:var(--lui-select-trigger-height);justify-content:space-between;line-height:1.25;outline:none;padding:var(--lui-select-trigger-py) var(--lui-select-trigger-px);position:relative;text-align:left;transition:var(--lui-transition);user-select:none;vertical-align:middle;white-space:nowrap;width:fit-content}.lui-select-trigger:focus-visible{border-color:var(--lui-select-focus-border);box-shadow:0 0 0 2px var(--lui-bg-surface),0 0 0 4px var(--lui-select-focus-ring-color)}.lui-select-trigger:disabled,.lui-select-trigger[aria-disabled=true]{cursor:var(--lui-disabled-cursor);opacity:var(--lui-disabled-opacity);pointer-events:none}.lui-select-trigger--sm{font-size:.8125rem;height:var(--lui-select-trigger-height-sm)}.lui-select-trigger--full-width{width:100%}.lui-select-trigger__chevron{flex-shrink:0;height:1rem;opacity:.5;pointer-events:none;width:1rem}.lui-select-value{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.lui-select-value:empty:before{content:"​"}.lui-select-content{animation:lui-select-fade-in .12s ease-out;background-color:var(--lui-select-content-bg);border:1px solid var(--lui-select-content-border);border-radius:var(--lui-select-content-radius);box-shadow:var(--lui-select-content-shadow);overflow:hidden}.lui-select-content__viewport{max-height:var(--lui-select-content-max-height);overflow-y:auto;overscroll-behavior:contain;padding:.25rem;scroll-padding-block:.25rem}@keyframes lui-select-fade-in{0%{opacity:0;transform:scale(.97)}to{opacity:1;transform:scale(1)}}.lui-select-label{color:var(--lui-select-label-text);font-size:.75rem;font-weight:500;padding:.375rem .5rem .25rem}.lui-select-item,.lui-select-label{font-family:var(--lui-font-family);user-select:none}.lui-select-item{align-items:center;border-radius:var(--lui-select-item-radius);color:var(--lui-select-item-text);cursor:pointer;display:flex;font-size:.875rem;gap:.375rem;outline:none;padding:var(--lui-select-item-py) var(--lui-select-item-px);padding-right:calc(var(--lui-select-item-px) + var(--lui-select-item-indicator-size) + .375rem);position:relative;transition:background-color .1s ease,color .1s ease}.lui-select-item:hover:not(.lui-select-item--disabled){background-color:var(--lui-select-item-hover-bg);color:var(--lui-select-item-hover-text)}.lui-select-item--selected{color:var(--lui-select-item-selected-text)}.lui-select-item--disabled{cursor:not-allowed;opacity:var(--lui-disabled-opacity);pointer-events:none}.lui-select-item__indicator{align-items:center;display:flex;flex-shrink:0;height:var(--lui-select-item-indicator-size);justify-content:center;position:absolute;right:var(--lui-select-item-px);width:var(--lui-select-item-indicator-size)}.lui-select-item__check{color:var(--lui-select-item-selected-text);height:.875rem;width:.875rem}.lui-select-item__text{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.lui-select-separator{border:none;border-top:1px solid var(--lui-select-separator-color);margin:.25rem -.25rem}}
@@ -1,28 +1 @@
1
- :root {
2
- --lui-select-trigger-bg: var(--lui-bg-surface);
3
- --lui-select-trigger-text: var(--lui-text-primary);
4
- --lui-select-trigger-border: var(--lui-border-default);
5
- --lui-select-trigger-radius: var(--lui-radius-sm);
6
- --lui-select-trigger-height: 2.25rem;
7
- --lui-select-trigger-height-sm: 2rem;
8
- --lui-select-trigger-px: 0.75rem;
9
- --lui-select-trigger-py: 0.5rem;
10
- --lui-select-focus-border: var(--lui-bg-interactive);
11
- --lui-select-focus-ring-color: color-mix(in srgb, var(--lui-bg-interactive) 40%, transparent);
12
- --lui-select-content-bg: var(--lui-bg-surface);
13
- --lui-select-content-border: var(--lui-border-default);
14
- --lui-select-content-radius: var(--lui-radius-sm);
15
- --lui-select-content-shadow: 0 4px 16px rgba(0, 0, 0, 0.12), 0 1px 4px rgba(0, 0, 0, 0.08);
16
- --lui-select-content-max-height: 20rem;
17
- --lui-select-item-text: var(--lui-text-primary);
18
- --lui-select-item-hover-bg: color-mix(in srgb, var(--lui-bg-interactive) 10%, transparent);
19
- --lui-select-item-hover-text: var(--lui-text-primary);
20
- --lui-select-item-selected-text: var(--lui-bg-interactive);
21
- --lui-select-item-focus-bg: color-mix(in srgb, var(--lui-bg-interactive) 10%, transparent);
22
- --lui-select-item-radius: var(--lui-radius-sm);
23
- --lui-select-item-px: 0.5rem;
24
- --lui-select-item-py: 0.375rem;
25
- --lui-select-item-indicator-size: 1rem;
26
- --lui-select-label-text: color-mix(in srgb, var(--lui-text-primary) 60%, transparent);
27
- --lui-select-separator-color: var(--lui-border-default);
28
- }
1
+ :root{--lui-select-trigger-bg:var(--lui-bg-surface);--lui-select-trigger-text:var(--lui-text-primary);--lui-select-trigger-border:var(--lui-border-default);--lui-select-trigger-radius:var(--lui-radius-sm);--lui-select-trigger-height:2.25rem;--lui-select-trigger-height-sm:2rem;--lui-select-trigger-px:0.75rem;--lui-select-trigger-py:0.5rem;--lui-select-focus-border:var(--lui-bg-interactive);--lui-select-focus-ring-color:color-mix(in srgb,var(--lui-bg-interactive) 40%,transparent);--lui-select-content-bg:var(--lui-bg-surface);--lui-select-content-border:var(--lui-border-default);--lui-select-content-radius:var(--lui-radius-sm);--lui-select-content-shadow:0 4px 16px rgba(0,0,0,.12),0 1px 4px rgba(0,0,0,.08);--lui-select-content-max-height:20rem;--lui-select-item-text:var(--lui-text-primary);--lui-select-item-hover-bg:color-mix(in srgb,var(--lui-bg-interactive) 10%,transparent);--lui-select-item-hover-text:var(--lui-text-primary);--lui-select-item-selected-text:var(--lui-bg-interactive);--lui-select-item-focus-bg:color-mix(in srgb,var(--lui-bg-interactive) 10%,transparent);--lui-select-item-radius:var(--lui-radius-sm);--lui-select-item-px:0.5rem;--lui-select-item-py:0.375rem;--lui-select-item-indicator-size:1rem;--lui-select-label-text:color-mix(in srgb,var(--lui-text-primary) 60%,transparent);--lui-select-separator-color:var(--lui-border-default)}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ladder-ui/select",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -26,9 +26,9 @@
26
26
  "sass": "^1.90.0",
27
27
  "tslib": "^2.6.2",
28
28
  "typescript": "^5.3.3",
29
- "@ladder-ui/primitives": "0.4.0",
30
- "@ladder-ui/layout": "0.4.0",
31
- "@ladder-ui/core": "0.4.0"
29
+ "@ladder-ui/core": "0.5.0",
30
+ "@ladder-ui/layout": "0.5.0",
31
+ "@ladder-ui/primitives": "0.5.0"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "@ladder-ui/core": ">=0.0.0",