@martinsura/ui 0.1.5 → 0.1.7

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.cjs CHANGED
@@ -517,7 +517,7 @@ var Textarea = ({ rows = 4, ...props }) => {
517
517
  placeholder: props.placeholder,
518
518
  disabled: props.disabled,
519
519
  onChange: (e) => props.onChange?.(e.target.value),
520
- className: tailwindMerge.twMerge(inputBaseClass, "min-h-24 py-2", errorDisplay && "border-(--ui-danger)")
520
+ className: tailwindMerge.twMerge(inputBaseClass, "min-h-24 px-(--ui-px-md) py-2 [font-size:var(--ui-text-md)] rounded-(--ui-radius-md)", errorDisplay && "border-(--ui-danger)")
521
521
  }
522
522
  ),
523
523
  errorDisplay && /* @__PURE__ */ jsxRuntime.jsx(InputError, { error: String(errorDisplay) })
@@ -4500,6 +4500,374 @@ var MultiSelectInput = ({
4500
4500
  errorDisplay && /* @__PURE__ */ jsxRuntime.jsx(InputError, { error: String(errorDisplay), className: props.classNames?.error })
4501
4501
  ] });
4502
4502
  };
4503
+ function calcPosition3(el) {
4504
+ const rect = el.getBoundingClientRect();
4505
+ const maxDropdownHeight = 320;
4506
+ const spaceBelow = window.innerHeight - rect.bottom;
4507
+ const openAbove = spaceBelow < maxDropdownHeight && rect.top > spaceBelow;
4508
+ return {
4509
+ top: openAbove ? rect.top - maxDropdownHeight - 2 : rect.bottom + 2,
4510
+ left: rect.left,
4511
+ width: rect.width,
4512
+ openAbove
4513
+ };
4514
+ }
4515
+ function flattenTree(options, parentLabels = [], parentKey = "") {
4516
+ return options.flatMap((option, index) => {
4517
+ const key = parentKey === "" ? String(index) : `${parentKey}.${index}`;
4518
+ const path = [...parentLabels, option.label];
4519
+ const current = {
4520
+ key,
4521
+ label: option.label,
4522
+ pathLabel: path.join(" / "),
4523
+ value: option.value,
4524
+ selectable: option.selectable
4525
+ };
4526
+ return [current, ...flattenTree(option.children ?? [], path, key)];
4527
+ });
4528
+ }
4529
+ var TreeSelectDropdown = (props) => {
4530
+ const [search, setSearch] = react.useState("");
4531
+ const [pos, setPos] = react.useState(null);
4532
+ const [activePath, setActivePath] = react.useState([]);
4533
+ const searchRef = react.useRef(null);
4534
+ const popupRef = react.useRef(null);
4535
+ const flattenedOptions = react.useMemo(() => flattenTree(props.options), [props.options]);
4536
+ react.useEffect(() => {
4537
+ if (!props.isOpen || !props.anchorRef.current) {
4538
+ return;
4539
+ }
4540
+ setPos(calcPosition3(props.anchorRef.current));
4541
+ setSearch("");
4542
+ setActivePath([]);
4543
+ if (props.showSearch !== false) {
4544
+ setTimeout(() => searchRef.current?.focus(), 0);
4545
+ }
4546
+ }, [props.isOpen, props.anchorRef, props.showSearch]);
4547
+ const handleMouseDown = react.useCallback(
4548
+ (e) => {
4549
+ if (popupRef.current && !popupRef.current.contains(e.target) && props.anchorRef.current && !props.anchorRef.current.contains(e.target)) {
4550
+ props.onClose();
4551
+ }
4552
+ },
4553
+ [props]
4554
+ );
4555
+ react.useEffect(() => {
4556
+ if (!props.isOpen) {
4557
+ return () => document.removeEventListener("mousedown", handleMouseDown);
4558
+ }
4559
+ document.addEventListener("mousedown", handleMouseDown);
4560
+ const handleKeyDown = (e) => {
4561
+ if (e.key === "Escape" && isTopmostFloatingRoot(popupRef.current)) {
4562
+ e.preventDefault();
4563
+ e.stopPropagation();
4564
+ e.stopImmediatePropagation();
4565
+ props.onClose();
4566
+ }
4567
+ };
4568
+ document.addEventListener("keydown", handleKeyDown);
4569
+ return () => {
4570
+ document.removeEventListener("mousedown", handleMouseDown);
4571
+ document.removeEventListener("keydown", handleKeyDown);
4572
+ };
4573
+ }, [props.isOpen, handleMouseDown, props]);
4574
+ if (!props.isOpen || !pos) {
4575
+ return null;
4576
+ }
4577
+ const searchTerm = search.trim().toLowerCase();
4578
+ const isSearching = searchTerm !== "";
4579
+ const filteredOptions = isSearching ? flattenedOptions.filter((option) => option.selectable && option.pathLabel.toLowerCase().includes(searchTerm)) : [];
4580
+ const columns = [];
4581
+ if (!isSearching) {
4582
+ let currentLevel = props.options;
4583
+ columns.push(currentLevel);
4584
+ for (const index of activePath) {
4585
+ const activeOption = currentLevel[index];
4586
+ if (!activeOption?.children?.length) {
4587
+ break;
4588
+ }
4589
+ currentLevel = activeOption.children;
4590
+ columns.push(currentLevel);
4591
+ }
4592
+ }
4593
+ const handleOptionClick = (option, depth, index) => {
4594
+ if (option.children?.length) {
4595
+ setActivePath((prev) => [...prev.slice(0, depth), index]);
4596
+ return;
4597
+ }
4598
+ if (!option.selectable) {
4599
+ return;
4600
+ }
4601
+ props.onSelect(option.value);
4602
+ };
4603
+ return reactDom.createPortal(
4604
+ /* @__PURE__ */ jsxRuntime.jsxs(
4605
+ "div",
4606
+ {
4607
+ ref: popupRef,
4608
+ "data-ui-floating-root": "",
4609
+ style: { top: pos.top, left: pos.left, minWidth: pos.width },
4610
+ className: "fixed z-1002 flex max-h-80 flex-col overflow-hidden rounded-(--ui-radius-lg) border border-(--ui-border) bg-white shadow-lg",
4611
+ children: [
4612
+ props.showSearch !== false && /* @__PURE__ */ jsxRuntime.jsxs(
4613
+ "div",
4614
+ {
4615
+ className: tailwindMerge.twMerge(
4616
+ "flex items-center border-b border-(--ui-border)",
4617
+ popupLayoutClasses.compactHeader,
4618
+ "gap-(--ui-popup-gap)"
4619
+ ),
4620
+ children: [
4621
+ /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconSearch, { size: 13, strokeWidth: 1.5, className: tailwindMerge.twMerge("shrink-0", neutralIconClasses.default) }),
4622
+ /* @__PURE__ */ jsxRuntime.jsx(
4623
+ "input",
4624
+ {
4625
+ ref: searchRef,
4626
+ type: "text",
4627
+ value: search,
4628
+ onChange: (e) => setSearch(e.target.value),
4629
+ placeholder: "Hledat...",
4630
+ className: "flex-1 bg-transparent outline-none [font-size:var(--ui-popup-text)] placeholder:text-(--ui-text-soft)"
4631
+ }
4632
+ )
4633
+ ]
4634
+ }
4635
+ ),
4636
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex max-h-64 overflow-hidden", children: props.loading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex min-h-40 min-w-80 items-center justify-center px-6 py-6", children: /* @__PURE__ */ jsxRuntime.jsx(Spinner, { size: "middle", color: "primary" }) }) : isSearching ? filteredOptions.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: tailwindMerge.twMerge("flex min-h-40 min-w-80 items-center justify-center px-6 py-4 text-xs", neutralTextClasses.soft), children: "\u017D\xE1dn\xE9 v\xFDsledky" }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "max-h-64 min-w-80 overflow-y-auto", children: filteredOptions.map((option) => {
4637
+ const isSelected = props.selectedValue === option.value;
4638
+ return /* @__PURE__ */ jsxRuntime.jsxs(
4639
+ "div",
4640
+ {
4641
+ onMouseDown: (e) => e.preventDefault(),
4642
+ onClick: () => props.onSelect(option.value),
4643
+ className: tailwindMerge.twMerge(
4644
+ "flex cursor-pointer items-center justify-between select-none",
4645
+ popupLayoutClasses.compactRow,
4646
+ isSelected ? "font-medium text-(--ui-text-strong)" : "text-(--ui-text) hover:bg-(--ui-surface-subtle)"
4647
+ ),
4648
+ children: [
4649
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 truncate", children: option.pathLabel }),
4650
+ isSelected && /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconCheck, { size: 13, strokeWidth: 2, className: "ml-3 shrink-0 text-(--ui-text)" })
4651
+ ]
4652
+ },
4653
+ option.key
4654
+ );
4655
+ }) }) : columns.map((column, depth) => /* @__PURE__ */ jsxRuntime.jsx(
4656
+ "div",
4657
+ {
4658
+ className: tailwindMerge.twMerge(
4659
+ "min-w-72 overflow-y-auto",
4660
+ depth > 0 && "border-l border-(--ui-border)"
4661
+ ),
4662
+ children: column.map((option, index) => {
4663
+ const isSelected = props.selectedValue === option.value;
4664
+ const isActiveBranch = activePath[depth] === index;
4665
+ const hasChildren = (option.children?.length ?? 0) > 0;
4666
+ return /* @__PURE__ */ jsxRuntime.jsxs(
4667
+ "div",
4668
+ {
4669
+ onMouseDown: (e) => e.preventDefault(),
4670
+ onMouseEnter: () => {
4671
+ if (hasChildren) {
4672
+ setActivePath((prev) => [...prev.slice(0, depth), index]);
4673
+ }
4674
+ },
4675
+ onClick: () => handleOptionClick(option, depth, index),
4676
+ className: tailwindMerge.twMerge(
4677
+ "flex select-none items-center justify-between",
4678
+ popupLayoutClasses.compactRow,
4679
+ hasChildren || option.selectable ? "cursor-pointer" : "cursor-default",
4680
+ isSelected ? "font-medium text-(--ui-text-strong)" : isActiveBranch ? "bg-(--ui-surface-subtle) text-(--ui-text-strong)" : "text-(--ui-text) hover:bg-(--ui-surface-subtle)",
4681
+ !option.selectable && !hasChildren && "text-(--ui-text-soft)"
4682
+ ),
4683
+ children: [
4684
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 truncate", children: option.label }),
4685
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ml-3 flex shrink-0 items-center gap-2", children: [
4686
+ isSelected && /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconCheck, { size: 13, strokeWidth: 2, className: "text-(--ui-text)" }),
4687
+ hasChildren && /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconChevronRight, { size: 13, strokeWidth: 1.5, className: tailwindMerge.twMerge(neutralIconClasses.default) })
4688
+ ] })
4689
+ ]
4690
+ },
4691
+ `${depth}-${index}-${String(option.value)}`
4692
+ );
4693
+ })
4694
+ },
4695
+ depth
4696
+ )) })
4697
+ ]
4698
+ }
4699
+ ),
4700
+ document.body
4701
+ );
4702
+ };
4703
+ function buildTreeOptions(options, labelKey, valueKey, childrenKey, disableBranchSelection, isOptionSelectable) {
4704
+ return options.map((option) => {
4705
+ const children = option[childrenKey];
4706
+ const nestedOptions = Array.isArray(children) ? buildTreeOptions(children, labelKey, valueKey, childrenKey, disableBranchSelection, isOptionSelectable) : void 0;
4707
+ const hasChildren = (nestedOptions?.length ?? 0) > 0;
4708
+ return {
4709
+ label: String(option[labelKey]),
4710
+ value: option[valueKey],
4711
+ selectable: isOptionSelectable ? isOptionSelectable(option) : !(disableBranchSelection && hasChildren),
4712
+ children: nestedOptions
4713
+ };
4714
+ });
4715
+ }
4716
+ function findSelectedOption(options, value) {
4717
+ for (const option of options) {
4718
+ if (option.value === value) {
4719
+ return option;
4720
+ }
4721
+ const nested = option.children ? findSelectedOption(option.children, value) : null;
4722
+ if (nested) {
4723
+ return nested;
4724
+ }
4725
+ }
4726
+ return null;
4727
+ }
4728
+ function findSelectedPathLabel(options, value, parentLabels = []) {
4729
+ for (const option of options) {
4730
+ const path = [...parentLabels, option.label];
4731
+ if (option.value === value) {
4732
+ return path.join(" / ");
4733
+ }
4734
+ if (option.children?.length) {
4735
+ const nested = findSelectedPathLabel(option.children, value, path);
4736
+ if (nested) {
4737
+ return nested;
4738
+ }
4739
+ }
4740
+ }
4741
+ return null;
4742
+ }
4743
+ var TreeSelectInput = ({
4744
+ options,
4745
+ labelKey = "label",
4746
+ valueKey = "value",
4747
+ childrenKey = "children",
4748
+ size = "middle",
4749
+ showSearch = true,
4750
+ disableBranchSelection = true,
4751
+ selectedLabelMode = "path",
4752
+ ...props
4753
+ }) => {
4754
+ const [isOpen, setIsOpen] = react.useState(false);
4755
+ const triggerRef = react.useRef(null);
4756
+ const allOptions = react.useMemo(
4757
+ () => buildTreeOptions(
4758
+ options ?? [],
4759
+ labelKey,
4760
+ valueKey,
4761
+ childrenKey,
4762
+ disableBranchSelection,
4763
+ props.isOptionSelectable
4764
+ ),
4765
+ [options, labelKey, valueKey, childrenKey, disableBranchSelection, props.isOptionSelectable]
4766
+ );
4767
+ const effectiveValue = props.treatZeroAsEmpty && props.value === 0 ? null : props.value ?? null;
4768
+ const selectedOption = effectiveValue === null || effectiveValue === void 0 ? null : findSelectedOption(allOptions, effectiveValue);
4769
+ const selectedPathLabel = effectiveValue === null || effectiveValue === void 0 ? null : findSelectedPathLabel(allOptions, effectiveValue);
4770
+ const hasValue = effectiveValue !== null && effectiveValue !== void 0;
4771
+ const allowClear = props.allowClear ?? false;
4772
+ const resolveError = useErrorResolver();
4773
+ const resolvedErrors = props.errorName ? resolveError(props.errorName) : [];
4774
+ const errorDisplay = props.error ?? props.customError ?? (resolvedErrors.length > 0 ? resolvedErrors.join(", ") : void 0);
4775
+ const hasError = !!errorDisplay;
4776
+ const handleSelect = (value) => {
4777
+ props.onChange?.(value);
4778
+ setIsOpen(false);
4779
+ };
4780
+ const handleClear = (e) => {
4781
+ e.stopPropagation();
4782
+ props.onChange?.(null);
4783
+ };
4784
+ const handleTriggerClick = () => {
4785
+ if (!props.disabled) {
4786
+ setIsOpen((current) => !current);
4787
+ }
4788
+ };
4789
+ return /* @__PURE__ */ jsxRuntime.jsxs(InputField, { noMargin: props.noMargin, className: props.className, children: [
4790
+ props.label && /* @__PURE__ */ jsxRuntime.jsx(
4791
+ InputLabel,
4792
+ {
4793
+ label: props.label,
4794
+ required: props.required,
4795
+ className: props.classNames?.label,
4796
+ requiredMarkClassName: props.classNames?.requiredMark
4797
+ }
4798
+ ),
4799
+ /* @__PURE__ */ jsxRuntime.jsxs(
4800
+ "div",
4801
+ {
4802
+ ref: triggerRef,
4803
+ onClick: handleTriggerClick,
4804
+ className: tailwindMerge.twMerge(
4805
+ "flex cursor-pointer select-none items-center gap-1 border bg-white transition-colors",
4806
+ triggerSizeClasses[size],
4807
+ isOpen ? triggerBorderClasses.open : triggerBorderClasses.default,
4808
+ props.disabled && triggerBorderClasses.disabled,
4809
+ props.classNames?.trigger
4810
+ ),
4811
+ style: hasError ? { borderColor: "var(--ui-danger)" } : void 0,
4812
+ children: [
4813
+ /* @__PURE__ */ jsxRuntime.jsx(
4814
+ "span",
4815
+ {
4816
+ className: tailwindMerge.twMerge(
4817
+ "flex min-w-0 flex-1 items-center truncate leading-none",
4818
+ !selectedOption && neutralTextClasses.soft,
4819
+ props.classNames?.value
4820
+ ),
4821
+ children: selectedLabelMode === "path" ? selectedPathLabel ?? selectedOption?.label ?? (props.placeholder ?? "") : selectedOption?.label ?? (props.placeholder ?? "")
4822
+ }
4823
+ ),
4824
+ allowClear && hasValue && !props.disabled && /* @__PURE__ */ jsxRuntime.jsx(
4825
+ "button",
4826
+ {
4827
+ type: "button",
4828
+ onMouseDown: (e) => e.preventDefault(),
4829
+ onClick: handleClear,
4830
+ className: tailwindMerge.twMerge(
4831
+ "flex h-4 w-4 shrink-0 cursor-pointer items-center justify-center self-center",
4832
+ neutralIconClasses.default,
4833
+ neutralIconClasses.hover,
4834
+ props.classNames?.clearButton
4835
+ ),
4836
+ children: /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconX, { size: 12, strokeWidth: 2 })
4837
+ }
4838
+ ),
4839
+ /* @__PURE__ */ jsxRuntime.jsx(
4840
+ iconsReact.IconChevronDown,
4841
+ {
4842
+ size: 13,
4843
+ strokeWidth: 1.5,
4844
+ className: tailwindMerge.twMerge(
4845
+ "shrink-0 self-center transition-transform",
4846
+ neutralIconClasses.default,
4847
+ isOpen && "rotate-180",
4848
+ props.classNames?.icon
4849
+ )
4850
+ }
4851
+ )
4852
+ ]
4853
+ }
4854
+ ),
4855
+ /* @__PURE__ */ jsxRuntime.jsx(
4856
+ TreeSelectDropdown,
4857
+ {
4858
+ anchorRef: triggerRef,
4859
+ isOpen,
4860
+ options: allOptions,
4861
+ selectedValue: effectiveValue,
4862
+ onSelect: handleSelect,
4863
+ onClose: () => setIsOpen(false),
4864
+ loading: props.loading,
4865
+ showSearch
4866
+ }
4867
+ ),
4868
+ errorDisplay && /* @__PURE__ */ jsxRuntime.jsx(InputError, { error: String(errorDisplay), className: props.classNames?.error })
4869
+ ] });
4870
+ };
4503
4871
 
4504
4872
  // src/notification/useNotification.ts
4505
4873
  var useNotification = () => {
@@ -4609,7 +4977,7 @@ var SkeletonTable = ({ rows = 5, columns = 4, className }) => /* @__PURE__ */ js
4609
4977
  ] }) });
4610
4978
  var GAP2 = 10;
4611
4979
  var VIEWPORT_MARGIN2 = 8;
4612
- function calcPosition3(trigger, tooltip, placement) {
4980
+ function calcPosition4(trigger, tooltip, placement) {
4613
4981
  let resolvedPlacement = placement;
4614
4982
  if (placement === "top" && trigger.top < tooltip.height + GAP2 + VIEWPORT_MARGIN2) {
4615
4983
  resolvedPlacement = "bottom";
@@ -4693,7 +5061,7 @@ var Tooltip = ({
4693
5061
  }
4694
5062
  const triggerRect = triggerRef.current.getBoundingClientRect();
4695
5063
  const tooltipRect = tooltipRef.current.getBoundingClientRect();
4696
- const next = calcPosition3(triggerRect, tooltipRect, placement);
5064
+ const next = calcPosition4(triggerRect, tooltipRect, placement);
4697
5065
  setPosition(next);
4698
5066
  };
4699
5067
  updatePosition();
@@ -4985,6 +5353,7 @@ exports.Textarea = Textarea;
4985
5353
  exports.Timeline = Timeline;
4986
5354
  exports.Toolbar = Toolbar;
4987
5355
  exports.Tooltip = Tooltip;
5356
+ exports.TreeSelectInput = TreeSelectInput;
4988
5357
  exports.UploadInput = UploadInput;
4989
5358
  exports.UploadProvider = UploadProvider;
4990
5359
  exports.getIcon = getIcon;