@kamaldgrt/ui 0.1.0 → 0.2.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/README.md +10 -1
- package/dist/index.d.ts +18 -2
- package/dist/index.js +121 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,6 +6,7 @@ Shared React UI primitives styled with Tailwind CSS theme tokens:
|
|
|
6
6
|
- **`ConfirmModal`**: message plus Cancel and Confirm, with an optional `danger` style and an optional secondary action.
|
|
7
7
|
- **`PromptModal`**: one or more labeled text inputs plus Cancel and OK. It replaces `window.prompt`.
|
|
8
8
|
- **`Drawer`**: right-side slide-over panel with an enter and exit transition and an optional header icon.
|
|
9
|
+
- **`SearchableSelect`**: single-select with a search box. Ids can be strings or numbers, options can have icons, and an optional `allLabel` is a pinned "no filter" choice.
|
|
9
10
|
|
|
10
11
|
All components are client components (`"use client"` is included in the build).
|
|
11
12
|
|
|
@@ -56,7 +57,7 @@ The components use semantic color tokens, so each app keeps its own look. The ap
|
|
|
56
57
|
## Usage
|
|
57
58
|
|
|
58
59
|
```tsx
|
|
59
|
-
import { ConfirmModal, Drawer, Modal, PromptModal } from "@kamaldgrt/ui";
|
|
60
|
+
import { ConfirmModal, Drawer, Modal, PromptModal, SearchableSelect } from "@kamaldgrt/ui";
|
|
60
61
|
|
|
61
62
|
<ConfirmModal
|
|
62
63
|
isOpen={open}
|
|
@@ -79,6 +80,14 @@ import { ConfirmModal, Drawer, Modal, PromptModal } from "@kamaldgrt/ui";
|
|
|
79
80
|
title="Rename"
|
|
80
81
|
fields={[{ name: "name", label: "Name", defaultValue: current }]}
|
|
81
82
|
/>
|
|
83
|
+
|
|
84
|
+
<SearchableSelect
|
|
85
|
+
ariaLabel="Status"
|
|
86
|
+
options={[{ id: "open", label: "Open" }, { id: "done", label: "Done" }]}
|
|
87
|
+
value={status}
|
|
88
|
+
onChange={setStatus}
|
|
89
|
+
allLabel="Any status"
|
|
90
|
+
/>
|
|
82
91
|
```
|
|
83
92
|
|
|
84
93
|
Behavior worth knowing:
|
package/dist/index.d.ts
CHANGED
|
@@ -46,5 +46,21 @@ declare function PromptModal({ isOpen, onClose, onSubmit, title, fields, submitL
|
|
|
46
46
|
submitLabel?: string;
|
|
47
47
|
}): react.JSX.Element;
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
type SearchableSelectOption<T extends string | number = number> = {
|
|
50
|
+
id: T;
|
|
51
|
+
label: string;
|
|
52
|
+
icon?: ReactNode;
|
|
53
|
+
};
|
|
54
|
+
declare function SearchableSelect<T extends string | number = number>({ options, value, onChange, allLabel, placeholder, className, compact, ariaLabel, }: {
|
|
55
|
+
options: SearchableSelectOption<T>[];
|
|
56
|
+
value: T | null;
|
|
57
|
+
onChange: (id: T | null) => void;
|
|
58
|
+
allLabel?: string;
|
|
59
|
+
placeholder?: string;
|
|
60
|
+
className?: string;
|
|
61
|
+
compact?: boolean;
|
|
62
|
+
ariaLabel?: string;
|
|
63
|
+
}): react.JSX.Element;
|
|
64
|
+
|
|
65
|
+
export { ConfirmModal, Drawer, Modal, PromptModal, SearchableSelect };
|
|
66
|
+
export type { PromptField, SearchableSelectOption };
|
package/dist/index.js
CHANGED
|
@@ -285,9 +285,129 @@ function PromptModal({
|
|
|
285
285
|
}
|
|
286
286
|
) });
|
|
287
287
|
}
|
|
288
|
+
|
|
289
|
+
// src/SearchableSelect.tsx
|
|
290
|
+
import { useEffect as useEffect4, useMemo, useRef as useRef3, useState as useState3 } from "react";
|
|
291
|
+
import { ChevronDown } from "lucide-react";
|
|
292
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
293
|
+
function SearchableSelect({
|
|
294
|
+
options,
|
|
295
|
+
value,
|
|
296
|
+
onChange,
|
|
297
|
+
allLabel,
|
|
298
|
+
placeholder = "Search\u2026",
|
|
299
|
+
className,
|
|
300
|
+
compact = false,
|
|
301
|
+
ariaLabel
|
|
302
|
+
}) {
|
|
303
|
+
var _a;
|
|
304
|
+
const triggerSize = compact ? "py-0.5 text-xs" : "py-1.5 text-sm";
|
|
305
|
+
const rowSize = compact ? "py-0.5 text-xs" : "py-1 text-sm";
|
|
306
|
+
const [open, setOpen] = useState3(false);
|
|
307
|
+
const [query, setQuery] = useState3("");
|
|
308
|
+
const containerRef = useRef3(null);
|
|
309
|
+
const inputRef = useRef3(null);
|
|
310
|
+
useEffect4(() => {
|
|
311
|
+
if (!open) return;
|
|
312
|
+
function handlePointerDown(e) {
|
|
313
|
+
var _a2;
|
|
314
|
+
if (!((_a2 = containerRef.current) == null ? void 0 : _a2.contains(e.target))) {
|
|
315
|
+
setOpen(false);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
document.addEventListener("pointerdown", handlePointerDown);
|
|
319
|
+
return () => document.removeEventListener("pointerdown", handlePointerDown);
|
|
320
|
+
}, [open]);
|
|
321
|
+
useEffect4(() => {
|
|
322
|
+
var _a2;
|
|
323
|
+
if (open) (_a2 = inputRef.current) == null ? void 0 : _a2.focus();
|
|
324
|
+
}, [open]);
|
|
325
|
+
const filtered = useMemo(
|
|
326
|
+
() => options.filter(
|
|
327
|
+
(o) => o.label.toLowerCase().includes(query.trim().toLowerCase())
|
|
328
|
+
),
|
|
329
|
+
[options, query]
|
|
330
|
+
);
|
|
331
|
+
const selected = options.find((o) => o.id === value);
|
|
332
|
+
const selectedLabel = value === null ? allLabel != null ? allLabel : "Select\u2026" : (_a = selected == null ? void 0 : selected.label) != null ? _a : "Select\u2026";
|
|
333
|
+
function toggleOpen() {
|
|
334
|
+
setOpen((wasOpen) => {
|
|
335
|
+
const nowOpen = !wasOpen;
|
|
336
|
+
if (nowOpen) setQuery("");
|
|
337
|
+
return nowOpen;
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
function select(id) {
|
|
341
|
+
onChange(id);
|
|
342
|
+
setOpen(false);
|
|
343
|
+
}
|
|
344
|
+
return /* @__PURE__ */ jsxs5("div", { ref: containerRef, className: `relative ${className != null ? className : ""}`, children: [
|
|
345
|
+
/* @__PURE__ */ jsxs5(
|
|
346
|
+
"button",
|
|
347
|
+
{
|
|
348
|
+
type: "button",
|
|
349
|
+
"aria-label": ariaLabel,
|
|
350
|
+
"aria-haspopup": "listbox",
|
|
351
|
+
"aria-expanded": open,
|
|
352
|
+
onClick: toggleOpen,
|
|
353
|
+
className: `flex w-full items-center justify-between gap-2 rounded-md border border-border bg-surface px-3 ${triggerSize} text-left text-foreground`,
|
|
354
|
+
children: [
|
|
355
|
+
/* @__PURE__ */ jsxs5("span", { className: "flex min-w-0 items-center gap-2", children: [
|
|
356
|
+
selected == null ? void 0 : selected.icon,
|
|
357
|
+
/* @__PURE__ */ jsx5("span", { className: "min-w-0 break-words", children: selectedLabel })
|
|
358
|
+
] }),
|
|
359
|
+
/* @__PURE__ */ jsx5(
|
|
360
|
+
ChevronDown,
|
|
361
|
+
{
|
|
362
|
+
className: `h-4 w-4 shrink-0 text-muted transition-transform ${open ? "rotate-180" : ""}`
|
|
363
|
+
}
|
|
364
|
+
)
|
|
365
|
+
]
|
|
366
|
+
}
|
|
367
|
+
),
|
|
368
|
+
open && /* @__PURE__ */ jsxs5("div", { className: "absolute z-30 mt-1 w-full min-w-48 rounded-md border border-border-subtle bg-surface-elevated p-2 shadow-lg", children: [
|
|
369
|
+
/* @__PURE__ */ jsx5(
|
|
370
|
+
"input",
|
|
371
|
+
{
|
|
372
|
+
ref: inputRef,
|
|
373
|
+
type: "text",
|
|
374
|
+
value: query,
|
|
375
|
+
onChange: (e) => setQuery(e.target.value),
|
|
376
|
+
placeholder,
|
|
377
|
+
className: `mb-2 w-full rounded-md border border-border bg-surface px-2 ${rowSize} text-foreground`
|
|
378
|
+
}
|
|
379
|
+
),
|
|
380
|
+
/* @__PURE__ */ jsxs5("ul", { className: "max-h-56 overflow-y-auto", children: [
|
|
381
|
+
allLabel && !query.trim() && /* @__PURE__ */ jsx5("li", { children: /* @__PURE__ */ jsx5(
|
|
382
|
+
"button",
|
|
383
|
+
{
|
|
384
|
+
type: "button",
|
|
385
|
+
onClick: () => select(null),
|
|
386
|
+
className: `block w-full rounded px-2 ${rowSize} text-left hover:bg-surface-hover ${value === null ? "font-medium text-cta" : "text-foreground"}`,
|
|
387
|
+
children: allLabel
|
|
388
|
+
}
|
|
389
|
+
) }),
|
|
390
|
+
filtered.length === 0 && /* @__PURE__ */ jsx5("li", { className: `px-2 ${rowSize} text-muted`, children: "No matches" }),
|
|
391
|
+
filtered.map((option) => /* @__PURE__ */ jsx5("li", { children: /* @__PURE__ */ jsxs5(
|
|
392
|
+
"button",
|
|
393
|
+
{
|
|
394
|
+
type: "button",
|
|
395
|
+
onClick: () => select(option.id),
|
|
396
|
+
className: `flex w-full items-center gap-2 rounded px-2 ${rowSize} text-left hover:bg-surface-hover ${value === option.id ? "font-medium text-cta" : "text-foreground"}`,
|
|
397
|
+
children: [
|
|
398
|
+
option.icon,
|
|
399
|
+
/* @__PURE__ */ jsx5("span", { className: "min-w-0 break-words", children: option.label })
|
|
400
|
+
]
|
|
401
|
+
}
|
|
402
|
+
) }, option.id))
|
|
403
|
+
] })
|
|
404
|
+
] })
|
|
405
|
+
] });
|
|
406
|
+
}
|
|
288
407
|
export {
|
|
289
408
|
ConfirmModal,
|
|
290
409
|
Drawer,
|
|
291
410
|
Modal,
|
|
292
|
-
PromptModal
|
|
411
|
+
PromptModal,
|
|
412
|
+
SearchableSelect
|
|
293
413
|
};
|