@rufous/ui 0.3.0 → 0.3.11

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/main.cjs CHANGED
@@ -2252,10 +2252,12 @@ function AutocompleteInner(props, _ref) {
2252
2252
  className = "",
2253
2253
  style,
2254
2254
  sx,
2255
+ open: openProp,
2255
2256
  onOpen,
2256
2257
  onClose
2257
2258
  } = props;
2258
- const [open, setOpen] = (0, import_react19.useState)(false);
2259
+ const [internalOpen, setInternalOpen] = (0, import_react19.useState)(false);
2260
+ const open = openProp !== void 0 ? openProp : internalOpen;
2259
2261
  const [inputStr, setInputStr] = (0, import_react19.useState)("");
2260
2262
  const [filterQuery, setFilterQuery] = (0, import_react19.useState)("");
2261
2263
  const [focusedIdx, setFocusedIdx] = (0, import_react19.useState)(-1);
@@ -2341,20 +2343,20 @@ function AutocompleteInner(props, _ref) {
2341
2343
  const openPopup = (0, import_react19.useCallback)(() => {
2342
2344
  if (disabled) return;
2343
2345
  calcPopupStyle();
2344
- setOpen(true);
2346
+ if (openProp === void 0) setInternalOpen(true);
2345
2347
  setFocusedIdx(-1);
2346
2348
  setFilterQuery("");
2347
2349
  onOpen?.();
2348
- }, [disabled, calcPopupStyle, onOpen]);
2350
+ }, [disabled, calcPopupStyle, onOpen, openProp]);
2349
2351
  const closePopup = (0, import_react19.useCallback)((justSelected = false) => {
2350
- setOpen(false);
2352
+ if (openProp === void 0) setInternalOpen(false);
2351
2353
  setFocusedIdx(-1);
2352
2354
  onClose?.();
2353
2355
  if (!justSelected && !freeSolo && !multiple && value == null) {
2354
2356
  setInputStr("");
2355
2357
  onInputChange?.(null, "");
2356
2358
  }
2357
- }, [freeSolo, multiple, value, onInputChange, onClose]);
2359
+ }, [openProp, freeSolo, multiple, value, onInputChange, onClose]);
2358
2360
  (0, import_react19.useEffect)(() => {
2359
2361
  if (!open) return;
2360
2362
  const handleOutside = (e) => {
@@ -3014,6 +3016,11 @@ var parseDisplay = (str, fmt = "MM/DD/YYYY") => {
3014
3016
  };
3015
3017
  var isoToDate = (iso) => {
3016
3018
  if (!iso) return null;
3019
+ if (iso.endsWith("Z") || /[+-]\d{2}:?\d{2}$/.test(iso)) {
3020
+ const d2 = new Date(iso);
3021
+ if (isNaN(d2.getTime())) return null;
3022
+ return new Date(d2.getFullYear(), d2.getMonth(), d2.getDate());
3023
+ }
3017
3024
  const [datePart] = iso.split("T");
3018
3025
  const [y, mo, d] = datePart.split("-").map(Number);
3019
3026
  if (!y || !mo || !d) return null;
@@ -3026,7 +3033,7 @@ var today = () => {
3026
3033
  var isSameDay = (a, b) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
3027
3034
  var normaliseBoundary = (d) => {
3028
3035
  if (!d) return null;
3029
- const base = d instanceof Date ? d : isoToDate(typeof d === "string" ? d.split("T")[0] : d);
3036
+ const base = d instanceof Date ? d : isoToDate(d);
3030
3037
  if (!base) return null;
3031
3038
  return new Date(base.getFullYear(), base.getMonth(), base.getDate());
3032
3039
  };
@@ -5133,6 +5140,7 @@ var Select = import_react24.default.forwardRef(function Select2(props, ref) {
5133
5140
  disabled = false,
5134
5141
  required = false,
5135
5142
  multiple = false,
5143
+ groupBy,
5136
5144
  className = "",
5137
5145
  style,
5138
5146
  sx
@@ -5312,32 +5320,51 @@ var Select = import_react24.default.forwardRef(function Select2(props, ref) {
5312
5320
  ),
5313
5321
  helperText && /* @__PURE__ */ import_react24.default.createElement("div", { className: `rf-text-field__helper-text${error ? " rf-text-field__helper-text--error" : ""}` }, helperText),
5314
5322
  open && !disabled && import_react_dom5.default.createPortal(
5315
- /* @__PURE__ */ import_react24.default.createElement("div", { ref: popupRef, className: "rf-select__popup", role: "presentation", style: popupStyle }, /* @__PURE__ */ import_react24.default.createElement("ul", { ref: listRef, className: "rf-select__listbox", role: "listbox", "aria-multiselectable": multiple }, options.map((opt, idx) => {
5316
- const selected = isSelected(opt.value);
5317
- const focused = focusedIdx === idx;
5318
- return /* @__PURE__ */ import_react24.default.createElement(
5319
- "li",
5320
- {
5321
- key: opt.value,
5322
- "data-idx": idx,
5323
- role: "option",
5324
- "aria-selected": selected,
5325
- "aria-disabled": opt.disabled,
5326
- className: [
5327
- "rf-select__option",
5328
- selected ? "rf-select__option--selected" : "",
5329
- focused ? "rf-select__option--focused" : "",
5330
- opt.disabled ? "rf-select__option--disabled" : ""
5331
- ].filter(Boolean).join(" "),
5332
- onMouseEnter: () => setFocusedIdx(idx),
5333
- onMouseLeave: () => setFocusedIdx(-1),
5334
- onMouseDown: (e) => e.preventDefault(),
5335
- onClick: (e) => selectOption(opt, e)
5336
- },
5337
- /* @__PURE__ */ import_react24.default.createElement("span", { className: "rf-select__option-label" }, opt.label),
5338
- /* @__PURE__ */ import_react24.default.createElement("span", { className: "rf-select__option-check", "aria-hidden": "true" }, /* @__PURE__ */ import_react24.default.createElement(CheckIcon2, null))
5339
- );
5340
- }))),
5323
+ /* @__PURE__ */ import_react24.default.createElement("div", { ref: popupRef, className: "rf-select__popup", role: "presentation", style: popupStyle }, /* @__PURE__ */ import_react24.default.createElement("ul", { ref: listRef, className: "rf-select__listbox", role: "listbox", "aria-multiselectable": multiple }, (() => {
5324
+ const renderOpt = (opt, selectableIdx) => {
5325
+ const selected = isSelected(opt.value);
5326
+ const focused = focusedIdx === selectableIdx;
5327
+ return /* @__PURE__ */ import_react24.default.createElement(
5328
+ "li",
5329
+ {
5330
+ key: opt.value,
5331
+ "data-idx": selectableIdx,
5332
+ role: "option",
5333
+ "aria-selected": selected,
5334
+ "aria-disabled": opt.disabled,
5335
+ className: [
5336
+ "rf-select__option",
5337
+ selected ? "rf-select__option--selected" : "",
5338
+ focused ? "rf-select__option--focused" : "",
5339
+ opt.disabled ? "rf-select__option--disabled" : ""
5340
+ ].filter(Boolean).join(" "),
5341
+ onMouseEnter: () => setFocusedIdx(selectableIdx),
5342
+ onMouseLeave: () => setFocusedIdx(-1),
5343
+ onMouseDown: (e) => e.preventDefault(),
5344
+ onClick: (e) => selectOption(opt, e)
5345
+ },
5346
+ /* @__PURE__ */ import_react24.default.createElement("span", { className: "rf-select__option-label" }, opt.label),
5347
+ /* @__PURE__ */ import_react24.default.createElement("span", { className: "rf-select__option-check", "aria-hidden": "true" }, /* @__PURE__ */ import_react24.default.createElement(CheckIcon2, null))
5348
+ );
5349
+ };
5350
+ let sCounter = 0;
5351
+ const selectableIdxMap = /* @__PURE__ */ new Map();
5352
+ options.forEach((o) => {
5353
+ if (!o.disabled) selectableIdxMap.set(o, sCounter++);
5354
+ });
5355
+ const resolveGroup = groupBy ? groupBy : (o) => o.group ?? "";
5356
+ const hasGroups = groupBy != null || options.some((o) => o.group != null);
5357
+ if (!hasGroups) {
5358
+ return options.map((opt) => renderOpt(opt, selectableIdxMap.get(opt) ?? -1));
5359
+ }
5360
+ const groupMap = /* @__PURE__ */ new Map();
5361
+ options.forEach((opt) => {
5362
+ const g = resolveGroup(opt);
5363
+ if (!groupMap.has(g)) groupMap.set(g, []);
5364
+ groupMap.get(g).push(opt);
5365
+ });
5366
+ return Array.from(groupMap.entries()).map(([groupName, groupOpts]) => /* @__PURE__ */ import_react24.default.createElement("li", { key: groupName, role: "presentation" }, /* @__PURE__ */ import_react24.default.createElement("div", { className: "rf-select__group-header" }, groupName), /* @__PURE__ */ import_react24.default.createElement("ul", { className: "rf-select__group-items", role: "group" }, groupOpts.map((opt) => renderOpt(opt, selectableIdxMap.get(opt) ?? -1)))));
5367
+ })())),
5341
5368
  document.body
5342
5369
  )
5343
5370
  );
package/dist/main.css CHANGED
@@ -2727,6 +2727,23 @@ pre {
2727
2727
  .rf-select__option--selected .rf-select__option-check {
2728
2728
  opacity: 1;
2729
2729
  }
2730
+ .rf-select__group-header {
2731
+ padding: 6px 16px 4px;
2732
+ font-size: 0.7rem;
2733
+ font-weight: 700;
2734
+ letter-spacing: 0.07em;
2735
+ text-transform: uppercase;
2736
+ color: var(--primary-color, #a41b06);
2737
+ background: #fff5f5;
2738
+ position: sticky;
2739
+ top: 0;
2740
+ z-index: 1;
2741
+ }
2742
+ .rf-select__group-items {
2743
+ padding: 0;
2744
+ list-style: none;
2745
+ margin: 0;
2746
+ }
2730
2747
 
2731
2748
  /* lib/styles/slider.css */
2732
2749
  .rf-slider {
package/dist/main.d.cts CHANGED
@@ -762,6 +762,15 @@ interface AutocompleteProps<T = string> {
762
762
  style?: CSSProperties;
763
763
  /** Scoped style overrides. Supports nested CSS selectors with & */
764
764
  sx?: SxProp;
765
+ /**
766
+ * Controlled open state. When provided the dropdown respects this value.
767
+ * Pair with `onOpen` and `onClose` to sync your own state:
768
+ * ```
769
+ * const [open, setOpen] = useState(false);
770
+ * <Autocomplete open={open} onOpen={() => setOpen(true)} onClose={() => setOpen(false)} />
771
+ * ```
772
+ */
773
+ open?: boolean;
765
774
  /** Called when the popup opens */
766
775
  onOpen?: () => void;
767
776
  /** Called when the popup closes */
@@ -919,6 +928,7 @@ type SelectOption = {
919
928
  value: string | number;
920
929
  label: string;
921
930
  disabled?: boolean;
931
+ group?: string;
922
932
  };
923
933
  interface SelectProps {
924
934
  options: SelectOption[] | string[];
@@ -934,6 +944,8 @@ interface SelectProps {
934
944
  disabled?: boolean;
935
945
  required?: boolean;
936
946
  multiple?: boolean;
947
+ /** Group options by returning a category string */
948
+ groupBy?: (option: SelectOption) => string;
937
949
  className?: string;
938
950
  style?: CSSProperties;
939
951
  sx?: SxProp;
package/dist/main.d.ts CHANGED
@@ -762,6 +762,15 @@ interface AutocompleteProps<T = string> {
762
762
  style?: CSSProperties;
763
763
  /** Scoped style overrides. Supports nested CSS selectors with & */
764
764
  sx?: SxProp;
765
+ /**
766
+ * Controlled open state. When provided the dropdown respects this value.
767
+ * Pair with `onOpen` and `onClose` to sync your own state:
768
+ * ```
769
+ * const [open, setOpen] = useState(false);
770
+ * <Autocomplete open={open} onOpen={() => setOpen(true)} onClose={() => setOpen(false)} />
771
+ * ```
772
+ */
773
+ open?: boolean;
765
774
  /** Called when the popup opens */
766
775
  onOpen?: () => void;
767
776
  /** Called when the popup closes */
@@ -919,6 +928,7 @@ type SelectOption = {
919
928
  value: string | number;
920
929
  label: string;
921
930
  disabled?: boolean;
931
+ group?: string;
922
932
  };
923
933
  interface SelectProps {
924
934
  options: SelectOption[] | string[];
@@ -934,6 +944,8 @@ interface SelectProps {
934
944
  disabled?: boolean;
935
945
  required?: boolean;
936
946
  multiple?: boolean;
947
+ /** Group options by returning a category string */
948
+ groupBy?: (option: SelectOption) => string;
937
949
  className?: string;
938
950
  style?: CSSProperties;
939
951
  sx?: SxProp;
package/dist/main.js CHANGED
@@ -2098,10 +2098,12 @@ function AutocompleteInner(props, _ref) {
2098
2098
  className = "",
2099
2099
  style,
2100
2100
  sx,
2101
+ open: openProp,
2101
2102
  onOpen,
2102
2103
  onClose
2103
2104
  } = props;
2104
- const [open, setOpen] = useState5(false);
2105
+ const [internalOpen, setInternalOpen] = useState5(false);
2106
+ const open = openProp !== void 0 ? openProp : internalOpen;
2105
2107
  const [inputStr, setInputStr] = useState5("");
2106
2108
  const [filterQuery, setFilterQuery] = useState5("");
2107
2109
  const [focusedIdx, setFocusedIdx] = useState5(-1);
@@ -2187,20 +2189,20 @@ function AutocompleteInner(props, _ref) {
2187
2189
  const openPopup = useCallback2(() => {
2188
2190
  if (disabled) return;
2189
2191
  calcPopupStyle();
2190
- setOpen(true);
2192
+ if (openProp === void 0) setInternalOpen(true);
2191
2193
  setFocusedIdx(-1);
2192
2194
  setFilterQuery("");
2193
2195
  onOpen?.();
2194
- }, [disabled, calcPopupStyle, onOpen]);
2196
+ }, [disabled, calcPopupStyle, onOpen, openProp]);
2195
2197
  const closePopup = useCallback2((justSelected = false) => {
2196
- setOpen(false);
2198
+ if (openProp === void 0) setInternalOpen(false);
2197
2199
  setFocusedIdx(-1);
2198
2200
  onClose?.();
2199
2201
  if (!justSelected && !freeSolo && !multiple && value == null) {
2200
2202
  setInputStr("");
2201
2203
  onInputChange?.(null, "");
2202
2204
  }
2203
- }, [freeSolo, multiple, value, onInputChange, onClose]);
2205
+ }, [openProp, freeSolo, multiple, value, onInputChange, onClose]);
2204
2206
  useEffect5(() => {
2205
2207
  if (!open) return;
2206
2208
  const handleOutside = (e) => {
@@ -2865,6 +2867,11 @@ var parseDisplay = (str, fmt = "MM/DD/YYYY") => {
2865
2867
  };
2866
2868
  var isoToDate = (iso) => {
2867
2869
  if (!iso) return null;
2870
+ if (iso.endsWith("Z") || /[+-]\d{2}:?\d{2}$/.test(iso)) {
2871
+ const d2 = new Date(iso);
2872
+ if (isNaN(d2.getTime())) return null;
2873
+ return new Date(d2.getFullYear(), d2.getMonth(), d2.getDate());
2874
+ }
2868
2875
  const [datePart] = iso.split("T");
2869
2876
  const [y, mo, d] = datePart.split("-").map(Number);
2870
2877
  if (!y || !mo || !d) return null;
@@ -2877,7 +2884,7 @@ var today = () => {
2877
2884
  var isSameDay = (a, b) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
2878
2885
  var normaliseBoundary = (d) => {
2879
2886
  if (!d) return null;
2880
- const base = d instanceof Date ? d : isoToDate(typeof d === "string" ? d.split("T")[0] : d);
2887
+ const base = d instanceof Date ? d : isoToDate(d);
2881
2888
  if (!base) return null;
2882
2889
  return new Date(base.getFullYear(), base.getMonth(), base.getDate());
2883
2890
  };
@@ -5010,6 +5017,7 @@ var Select = React76.forwardRef(function Select2(props, ref) {
5010
5017
  disabled = false,
5011
5018
  required = false,
5012
5019
  multiple = false,
5020
+ groupBy,
5013
5021
  className = "",
5014
5022
  style,
5015
5023
  sx
@@ -5189,32 +5197,51 @@ var Select = React76.forwardRef(function Select2(props, ref) {
5189
5197
  ),
5190
5198
  helperText && /* @__PURE__ */ React76.createElement("div", { className: `rf-text-field__helper-text${error ? " rf-text-field__helper-text--error" : ""}` }, helperText),
5191
5199
  open && !disabled && ReactDOM4.createPortal(
5192
- /* @__PURE__ */ React76.createElement("div", { ref: popupRef, className: "rf-select__popup", role: "presentation", style: popupStyle }, /* @__PURE__ */ React76.createElement("ul", { ref: listRef, className: "rf-select__listbox", role: "listbox", "aria-multiselectable": multiple }, options.map((opt, idx) => {
5193
- const selected = isSelected(opt.value);
5194
- const focused = focusedIdx === idx;
5195
- return /* @__PURE__ */ React76.createElement(
5196
- "li",
5197
- {
5198
- key: opt.value,
5199
- "data-idx": idx,
5200
- role: "option",
5201
- "aria-selected": selected,
5202
- "aria-disabled": opt.disabled,
5203
- className: [
5204
- "rf-select__option",
5205
- selected ? "rf-select__option--selected" : "",
5206
- focused ? "rf-select__option--focused" : "",
5207
- opt.disabled ? "rf-select__option--disabled" : ""
5208
- ].filter(Boolean).join(" "),
5209
- onMouseEnter: () => setFocusedIdx(idx),
5210
- onMouseLeave: () => setFocusedIdx(-1),
5211
- onMouseDown: (e) => e.preventDefault(),
5212
- onClick: (e) => selectOption(opt, e)
5213
- },
5214
- /* @__PURE__ */ React76.createElement("span", { className: "rf-select__option-label" }, opt.label),
5215
- /* @__PURE__ */ React76.createElement("span", { className: "rf-select__option-check", "aria-hidden": "true" }, /* @__PURE__ */ React76.createElement(CheckIcon2, null))
5216
- );
5217
- }))),
5200
+ /* @__PURE__ */ React76.createElement("div", { ref: popupRef, className: "rf-select__popup", role: "presentation", style: popupStyle }, /* @__PURE__ */ React76.createElement("ul", { ref: listRef, className: "rf-select__listbox", role: "listbox", "aria-multiselectable": multiple }, (() => {
5201
+ const renderOpt = (opt, selectableIdx) => {
5202
+ const selected = isSelected(opt.value);
5203
+ const focused = focusedIdx === selectableIdx;
5204
+ return /* @__PURE__ */ React76.createElement(
5205
+ "li",
5206
+ {
5207
+ key: opt.value,
5208
+ "data-idx": selectableIdx,
5209
+ role: "option",
5210
+ "aria-selected": selected,
5211
+ "aria-disabled": opt.disabled,
5212
+ className: [
5213
+ "rf-select__option",
5214
+ selected ? "rf-select__option--selected" : "",
5215
+ focused ? "rf-select__option--focused" : "",
5216
+ opt.disabled ? "rf-select__option--disabled" : ""
5217
+ ].filter(Boolean).join(" "),
5218
+ onMouseEnter: () => setFocusedIdx(selectableIdx),
5219
+ onMouseLeave: () => setFocusedIdx(-1),
5220
+ onMouseDown: (e) => e.preventDefault(),
5221
+ onClick: (e) => selectOption(opt, e)
5222
+ },
5223
+ /* @__PURE__ */ React76.createElement("span", { className: "rf-select__option-label" }, opt.label),
5224
+ /* @__PURE__ */ React76.createElement("span", { className: "rf-select__option-check", "aria-hidden": "true" }, /* @__PURE__ */ React76.createElement(CheckIcon2, null))
5225
+ );
5226
+ };
5227
+ let sCounter = 0;
5228
+ const selectableIdxMap = /* @__PURE__ */ new Map();
5229
+ options.forEach((o) => {
5230
+ if (!o.disabled) selectableIdxMap.set(o, sCounter++);
5231
+ });
5232
+ const resolveGroup = groupBy ? groupBy : (o) => o.group ?? "";
5233
+ const hasGroups = groupBy != null || options.some((o) => o.group != null);
5234
+ if (!hasGroups) {
5235
+ return options.map((opt) => renderOpt(opt, selectableIdxMap.get(opt) ?? -1));
5236
+ }
5237
+ const groupMap = /* @__PURE__ */ new Map();
5238
+ options.forEach((opt) => {
5239
+ const g = resolveGroup(opt);
5240
+ if (!groupMap.has(g)) groupMap.set(g, []);
5241
+ groupMap.get(g).push(opt);
5242
+ });
5243
+ return Array.from(groupMap.entries()).map(([groupName, groupOpts]) => /* @__PURE__ */ React76.createElement("li", { key: groupName, role: "presentation" }, /* @__PURE__ */ React76.createElement("div", { className: "rf-select__group-header" }, groupName), /* @__PURE__ */ React76.createElement("ul", { className: "rf-select__group-items", role: "group" }, groupOpts.map((opt) => renderOpt(opt, selectableIdxMap.get(opt) ?? -1)))));
5244
+ })())),
5218
5245
  document.body
5219
5246
  )
5220
5247
  );
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rufous/ui",
3
3
  "private": false,
4
- "version": "0.3.0",
4
+ "version": "0.3.11",
5
5
  "type": "module",
6
6
  "description": "Experimental: A lightweight React UI component library (Beta)",
7
7
  "style": "./dist/main.css",