@kjun-ui/native 0.8.1 → 0.9.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.cjs CHANGED
@@ -234,6 +234,10 @@ function installLayerKeyboard(document2, state, keyupFallback = false) {
234
234
  var import_react2 = require("react");
235
235
 
236
236
  // shared/package-runtime/layer-state.ts
237
+ function chooseReturnTarget(chosen, fallback2) {
238
+ if (chosen === false) return void 0;
239
+ return chosen && chosen.isConnected !== false ? chosen : fallback2;
240
+ }
237
241
  function createLayerState() {
238
242
  const entries = /* @__PURE__ */ new Map(), listeners = /* @__PURE__ */ new Set();
239
243
  const restoredFocus = /* @__PURE__ */ new WeakSet();
@@ -254,6 +258,7 @@ function createLayerState() {
254
258
  return !layer.blocked && (layer.kind === "window" ? ((_a = topWindow()) == null ? void 0 : _a.id) === layer.id : ((_b = topWindow()) == null ? void 0 : _b.id) === layer.owner);
255
259
  };
256
260
  const top = () => [...entries.values()].filter(active).sort((a, b) => a.order - b.order).at(-1);
261
+ const topPopup = () => [...entries.values()].filter((layer) => layer.kind !== "tooltip" && active(layer)).sort((a, b) => a.order - b.order).at(-1);
257
262
  function dismiss(layer) {
258
263
  if (layer.closeRequested) return;
259
264
  layer.closeRequested = true;
@@ -273,6 +278,7 @@ function createLayerState() {
273
278
  entries,
274
279
  topWindow,
275
280
  top,
281
+ topPopup,
276
282
  windowOf,
277
283
  isRestoredFocus(target) {
278
284
  return !!target && restoredFocus.has(target);
@@ -421,21 +427,31 @@ function focusTrigger(target) {
421
427
  }
422
428
  return target;
423
429
  }
424
- function restoreFocus(target, state, id) {
430
+ function restoreFocus(target, state, id, fallback2) {
431
+ const usable = (item) => (item == null ? void 0 : item.isConnected) !== false && !(typeof HTMLElement !== "undefined" && item instanceof HTMLElement && item.closest("[inert]"));
425
432
  const restore = () => {
426
- var _a, _b, _c, _d;
433
+ var _a, _b, _c, _d, _e;
427
434
  if (state.entries.has(id)) return;
428
- if ((target == null ? void 0 : target.isConnected) !== false && !(typeof HTMLElement !== "undefined" && target instanceof HTMLElement && target.closest("[inert]"))) (_a = target == null ? void 0 : target.focus) == null ? void 0 : _a.call(target, { preventScroll: true });
429
- else (_d = (_c = (_b = state.topWindow()) == null ? void 0 : _b.root) == null ? void 0 : _c.focus) == null ? void 0 : _d.call(_c, { preventScroll: true });
435
+ if (usable(target)) (_a = target == null ? void 0 : target.focus) == null ? void 0 : _a.call(target, { preventScroll: true });
436
+ else if (fallback2 && fallback2 !== target && usable(fallback2)) (_b = fallback2.focus) == null ? void 0 : _b.call(fallback2, { preventScroll: true });
437
+ else (_e = (_d = (_c = state.topWindow()) == null ? void 0 : _c.root) == null ? void 0 : _d.focus) == null ? void 0 : _e.call(_d, { preventScroll: true });
430
438
  };
431
439
  if (typeof requestAnimationFrame !== "undefined") requestAnimationFrame(restore);
432
440
  else queueMicrotask(restore);
433
441
  }
434
- function useLayer(open, present, kind, close, trigger) {
442
+ function useLayer(open, present, kind, close, trigger, returnFocus) {
435
443
  var _a;
436
444
  const state = useLayerState(), parent = (0, import_react2.useContext)(LayerScope), id = (0, import_react2.useId)();
437
445
  const callback = (0, import_react2.useRef)(close);
438
446
  callback.current = close;
447
+ const returnRef = (0, import_react2.useRef)(returnFocus);
448
+ returnRef.current = returnFocus;
449
+ const fallbackFocus = (0, import_react2.useRef)(null);
450
+ const removeLayer = () => {
451
+ const removed = state.remove(id, currentFocus());
452
+ if (removed) fallbackFocus.current = removed;
453
+ return removed && returnRef.current ? chooseReturnTarget(returnRef.current(), removed) : removed;
454
+ };
439
455
  const openingFocus = (0, import_react2.useRef)(null);
440
456
  const wasOpen = (0, import_react2.useRef)(false);
441
457
  const pendingFocus = (0, import_react2.useRef)(null);
@@ -447,17 +463,17 @@ function useLayer(open, present, kind, close, trigger) {
447
463
  state.open(id, kind, parent, () => callback.current(), focusTrigger(trigger == null ? void 0 : trigger.current) || openingFocus.current);
448
464
  } else if (kind === "window" && present) state.markClosing(id);
449
465
  else if (kind !== "window" || !present) {
450
- const target = state.remove(id, currentFocus());
466
+ const target = removeLayer();
451
467
  if (target) pendingFocus.current = target;
452
468
  if (!present && pendingFocus.current) {
453
- restoreFocus(pendingFocus.current, state, id);
469
+ restoreFocus(pendingFocus.current, state, id, fallbackFocus.current);
454
470
  pendingFocus.current = null;
455
471
  }
456
472
  }
457
473
  }, [open, present, state, id, kind, parent, trigger]);
458
474
  (0, import_react2.useLayoutEffect)(() => () => {
459
- const target = state.remove(id, currentFocus());
460
- if (target || pendingFocus.current) restoreFocus(target || pendingFocus.current, state, id);
475
+ const target = removeLayer();
476
+ if (target || pendingFocus.current) restoreFocus(target || pendingFocus.current, state, id, fallbackFocus.current);
461
477
  }, [state, id]);
462
478
  const entry = state.entries.get(id);
463
479
  return {
@@ -3065,7 +3081,7 @@ function KjunRadioGroup({
3065
3081
  accessibilityLabel: ariaLabel,
3066
3082
  style: {
3067
3083
  flexDirection: direction === "vertical" ? "column" : "row",
3068
- flexWrap: "wrap",
3084
+ flexWrap: direction === "vertical" ? "nowrap" : "wrap",
3069
3085
  gap: direction === "vertical" ? import_tokens18.tokens.dimension.value8 : import_tokens18.tokens.dimension.value16
3070
3086
  },
3071
3087
  children: children || options.map((option) => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
@@ -4254,6 +4270,17 @@ function nextOptionValue(value, option, key, multiple) {
4254
4270
  if (!multiple) return id;
4255
4271
  return Array.isArray(value) && value.includes(id) ? value.filter((item) => item !== id) : [...Array.isArray(value) ? value : [], id];
4256
4272
  }
4273
+ function optionMatchRank(option, query, labelKey, searchKeys = []) {
4274
+ const needle = query.trim().toLocaleLowerCase();
4275
+ if (!needle) return 0;
4276
+ const label = optionLabel(option, labelKey).toLocaleLowerCase();
4277
+ if (label.startsWith(needle)) return 0;
4278
+ if (label.includes(needle)) return 1;
4279
+ return typeof option === "object" && searchKeys.some((key) => {
4280
+ var _a;
4281
+ return String((_a = option[key]) != null ? _a : "").toLocaleLowerCase().includes(needle);
4282
+ }) ? 2 : -1;
4283
+ }
4257
4284
  function selectOptions({
4258
4285
  options,
4259
4286
  value,
@@ -4263,10 +4290,13 @@ function selectOptions({
4263
4290
  pageSize,
4264
4291
  limit,
4265
4292
  loading,
4266
- multiple
4293
+ multiple,
4294
+ filterFn,
4295
+ searchKeys = []
4267
4296
  }) {
4268
4297
  const selected = (option) => optionSelected(value, option, valueKey);
4269
- const matching = options.map((option, index) => ({ option, key: optionKey(option, valueKey, index) })).filter(({ option }) => optionLabel(option, labelKey).toLocaleLowerCase().includes(query.trim().toLocaleLowerCase()));
4298
+ const indexed = options.map((option, index) => ({ option, key: optionKey(option, valueKey, index) }));
4299
+ const matching = filterFn ? indexed.filter(({ option }) => filterFn(option, query)) : indexed.map((item, index) => ({ item, index, rank: optionMatchRank(item.option, query, labelKey, searchKeys) })).filter(({ rank }) => rank >= 0).sort((a, b) => a.rank - b.rank || a.index - b.index).map(({ item }) => item);
4270
4300
  const visible = loading ? [] : pageSize > 0 ? matching.slice(0, limit) : matching;
4271
4301
  const selectedOption = options.find(selected);
4272
4302
  const label = multiple && Array.isArray(value) && value.length > 1 ? `${value.length}\uAC1C \uC120\uD0DD\uB428` : selectedOption !== void 0 ? optionLabel(selectedOption, labelKey) : "";
@@ -5001,7 +5031,9 @@ function KjunSelect({
5001
5031
  onChange,
5002
5032
  onOpenChange,
5003
5033
  onSearch,
5004
- onClear
5034
+ onClear,
5035
+ filterFn,
5036
+ searchKeys
5005
5037
  }) {
5006
5038
  const { open, query, limit, changeOpen, search, showMore } = useSelectState(controlled, disabled, optionPageSize, onOpenChange);
5007
5039
  const { colors } = useKjunStyles(), field = useKjunField(), ref = (0, import_react44.useRef)(null), spec = import_tokens30.tokens.input[size];
@@ -5026,7 +5058,9 @@ function KjunSelect({
5026
5058
  pageSize: optionPageSize,
5027
5059
  limit,
5028
5060
  loading,
5029
- multiple
5061
+ multiple,
5062
+ filterFn,
5063
+ searchKeys
5030
5064
  });
5031
5065
  const firstSelected = visible.findIndex(({ option }) => selected(option));
5032
5066
  if (firstSelected < 0) selectedRow.current = null;
@@ -5988,7 +6022,7 @@ function KjunRefreshButton({
5988
6022
  ...props
5989
6023
  }) {
5990
6024
  const reduced = useReducedMotion();
5991
- const label = ariaLabel || [targetName, "\uC0C8\uB85C\uACE0\uCE68"].filter(Boolean).join(" ");
6025
+ const label = ariaLabel || [targetName, mode === "text" ? text : "\uC0C8\uB85C\uACE0\uCE68"].filter(Boolean).join(" ");
5992
6026
  const hint = tooltip != null ? tooltip : mode === "icon" ? label : "";
5993
6027
  const button = /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(KjunTooltip, { content: props.disabled || props.loading ? "" : hint, placement: tooltipPlacement, delay: tooltipDelay, children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
5994
6028
  KjunButton,
@@ -7152,9 +7186,18 @@ function tablePresentation(props, mobile) {
7152
7186
  }
7153
7187
  };
7154
7188
  }
7189
+ var warnedFormats = /* @__PURE__ */ new Set();
7190
+ function columnFormat(column) {
7191
+ if (typeof column.format === "function") return column.format;
7192
+ if (column.format != null && !warnedFormats.has(column.key)) {
7193
+ warnedFormats.add(column.key);
7194
+ console.warn(`KJUN Table: ${column.key} \uC5F4\uC758 format\uC740 \uD568\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4.`);
7195
+ }
7196
+ return void 0;
7197
+ }
7155
7198
  function tableCellValue(props, column, row, index) {
7156
7199
  const value = getValue(row, column.key);
7157
- const body = props.renderCell ? props.renderCell(value, column, row, index) : column.render ? column.render(value, row, index) : column.format ? column.format(value, row) : props.formatValue ? props.formatValue(value, column, row) : value == null ? "-" : typeof value === "number" && column.type === "number" ? new Intl.NumberFormat().format(value) : String(value);
7200
+ const body = props.renderCell ? props.renderCell(value, column, row, index) : column.render ? column.render(value, row, index) : columnFormat(column) ? columnFormat(column)(value, row) : props.formatValue ? props.formatValue(value, column, row) : value == null ? "-" : typeof value === "number" && column.type === "number" ? new Intl.NumberFormat().format(value) : String(value);
7158
7201
  return { value, body };
7159
7202
  }
7160
7203
 
package/dist/index.js CHANGED
@@ -119,6 +119,10 @@ function installLayerKeyboard(document2, state, keyupFallback = false) {
119
119
  import { createContext as createContext2, useContext as useContext2, useId, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
120
120
 
121
121
  // shared/package-runtime/layer-state.ts
122
+ function chooseReturnTarget(chosen, fallback2) {
123
+ if (chosen === false) return void 0;
124
+ return chosen && chosen.isConnected !== false ? chosen : fallback2;
125
+ }
122
126
  function createLayerState() {
123
127
  const entries = /* @__PURE__ */ new Map(), listeners = /* @__PURE__ */ new Set();
124
128
  const restoredFocus = /* @__PURE__ */ new WeakSet();
@@ -139,6 +143,7 @@ function createLayerState() {
139
143
  return !layer.blocked && (layer.kind === "window" ? ((_a = topWindow()) == null ? void 0 : _a.id) === layer.id : ((_b = topWindow()) == null ? void 0 : _b.id) === layer.owner);
140
144
  };
141
145
  const top = () => [...entries.values()].filter(active).sort((a, b) => a.order - b.order).at(-1);
146
+ const topPopup = () => [...entries.values()].filter((layer) => layer.kind !== "tooltip" && active(layer)).sort((a, b) => a.order - b.order).at(-1);
142
147
  function dismiss(layer) {
143
148
  if (layer.closeRequested) return;
144
149
  layer.closeRequested = true;
@@ -158,6 +163,7 @@ function createLayerState() {
158
163
  entries,
159
164
  topWindow,
160
165
  top,
166
+ topPopup,
161
167
  windowOf,
162
168
  isRestoredFocus(target) {
163
169
  return !!target && restoredFocus.has(target);
@@ -306,21 +312,31 @@ function focusTrigger(target) {
306
312
  }
307
313
  return target;
308
314
  }
309
- function restoreFocus(target, state, id) {
315
+ function restoreFocus(target, state, id, fallback2) {
316
+ const usable = (item) => (item == null ? void 0 : item.isConnected) !== false && !(typeof HTMLElement !== "undefined" && item instanceof HTMLElement && item.closest("[inert]"));
310
317
  const restore = () => {
311
- var _a, _b, _c, _d;
318
+ var _a, _b, _c, _d, _e;
312
319
  if (state.entries.has(id)) return;
313
- if ((target == null ? void 0 : target.isConnected) !== false && !(typeof HTMLElement !== "undefined" && target instanceof HTMLElement && target.closest("[inert]"))) (_a = target == null ? void 0 : target.focus) == null ? void 0 : _a.call(target, { preventScroll: true });
314
- else (_d = (_c = (_b = state.topWindow()) == null ? void 0 : _b.root) == null ? void 0 : _c.focus) == null ? void 0 : _d.call(_c, { preventScroll: true });
320
+ if (usable(target)) (_a = target == null ? void 0 : target.focus) == null ? void 0 : _a.call(target, { preventScroll: true });
321
+ else if (fallback2 && fallback2 !== target && usable(fallback2)) (_b = fallback2.focus) == null ? void 0 : _b.call(fallback2, { preventScroll: true });
322
+ else (_e = (_d = (_c = state.topWindow()) == null ? void 0 : _c.root) == null ? void 0 : _d.focus) == null ? void 0 : _e.call(_d, { preventScroll: true });
315
323
  };
316
324
  if (typeof requestAnimationFrame !== "undefined") requestAnimationFrame(restore);
317
325
  else queueMicrotask(restore);
318
326
  }
319
- function useLayer(open, present, kind, close, trigger) {
327
+ function useLayer(open, present, kind, close, trigger, returnFocus) {
320
328
  var _a;
321
329
  const state = useLayerState(), parent = useContext2(LayerScope), id = useId();
322
330
  const callback = useRef(close);
323
331
  callback.current = close;
332
+ const returnRef = useRef(returnFocus);
333
+ returnRef.current = returnFocus;
334
+ const fallbackFocus = useRef(null);
335
+ const removeLayer = () => {
336
+ const removed = state.remove(id, currentFocus());
337
+ if (removed) fallbackFocus.current = removed;
338
+ return removed && returnRef.current ? chooseReturnTarget(returnRef.current(), removed) : removed;
339
+ };
324
340
  const openingFocus = useRef(null);
325
341
  const wasOpen = useRef(false);
326
342
  const pendingFocus = useRef(null);
@@ -332,17 +348,17 @@ function useLayer(open, present, kind, close, trigger) {
332
348
  state.open(id, kind, parent, () => callback.current(), focusTrigger(trigger == null ? void 0 : trigger.current) || openingFocus.current);
333
349
  } else if (kind === "window" && present) state.markClosing(id);
334
350
  else if (kind !== "window" || !present) {
335
- const target = state.remove(id, currentFocus());
351
+ const target = removeLayer();
336
352
  if (target) pendingFocus.current = target;
337
353
  if (!present && pendingFocus.current) {
338
- restoreFocus(pendingFocus.current, state, id);
354
+ restoreFocus(pendingFocus.current, state, id, fallbackFocus.current);
339
355
  pendingFocus.current = null;
340
356
  }
341
357
  }
342
358
  }, [open, present, state, id, kind, parent, trigger]);
343
359
  useLayoutEffect(() => () => {
344
- const target = state.remove(id, currentFocus());
345
- if (target || pendingFocus.current) restoreFocus(target || pendingFocus.current, state, id);
360
+ const target = removeLayer();
361
+ if (target || pendingFocus.current) restoreFocus(target || pendingFocus.current, state, id, fallbackFocus.current);
346
362
  }, [state, id]);
347
363
  const entry = state.entries.get(id);
348
364
  return {
@@ -3031,7 +3047,7 @@ function KjunRadioGroup({
3031
3047
  accessibilityLabel: ariaLabel,
3032
3048
  style: {
3033
3049
  flexDirection: direction === "vertical" ? "column" : "row",
3034
- flexWrap: "wrap",
3050
+ flexWrap: direction === "vertical" ? "nowrap" : "wrap",
3035
3051
  gap: direction === "vertical" ? tokens16.dimension.value8 : tokens16.dimension.value16
3036
3052
  },
3037
3053
  children: children || options.map((option) => /* @__PURE__ */ jsx19(
@@ -4269,6 +4285,17 @@ function nextOptionValue(value, option, key, multiple) {
4269
4285
  if (!multiple) return id;
4270
4286
  return Array.isArray(value) && value.includes(id) ? value.filter((item) => item !== id) : [...Array.isArray(value) ? value : [], id];
4271
4287
  }
4288
+ function optionMatchRank(option, query, labelKey, searchKeys = []) {
4289
+ const needle = query.trim().toLocaleLowerCase();
4290
+ if (!needle) return 0;
4291
+ const label = optionLabel(option, labelKey).toLocaleLowerCase();
4292
+ if (label.startsWith(needle)) return 0;
4293
+ if (label.includes(needle)) return 1;
4294
+ return typeof option === "object" && searchKeys.some((key) => {
4295
+ var _a;
4296
+ return String((_a = option[key]) != null ? _a : "").toLocaleLowerCase().includes(needle);
4297
+ }) ? 2 : -1;
4298
+ }
4272
4299
  function selectOptions({
4273
4300
  options,
4274
4301
  value,
@@ -4278,10 +4305,13 @@ function selectOptions({
4278
4305
  pageSize,
4279
4306
  limit,
4280
4307
  loading,
4281
- multiple
4308
+ multiple,
4309
+ filterFn,
4310
+ searchKeys = []
4282
4311
  }) {
4283
4312
  const selected = (option) => optionSelected(value, option, valueKey);
4284
- const matching = options.map((option, index) => ({ option, key: optionKey(option, valueKey, index) })).filter(({ option }) => optionLabel(option, labelKey).toLocaleLowerCase().includes(query.trim().toLocaleLowerCase()));
4313
+ const indexed = options.map((option, index) => ({ option, key: optionKey(option, valueKey, index) }));
4314
+ const matching = filterFn ? indexed.filter(({ option }) => filterFn(option, query)) : indexed.map((item, index) => ({ item, index, rank: optionMatchRank(item.option, query, labelKey, searchKeys) })).filter(({ rank }) => rank >= 0).sort((a, b) => a.rank - b.rank || a.index - b.index).map(({ item }) => item);
4285
4315
  const visible = loading ? [] : pageSize > 0 ? matching.slice(0, limit) : matching;
4286
4316
  const selectedOption = options.find(selected);
4287
4317
  const label = multiple && Array.isArray(value) && value.length > 1 ? `${value.length}\uAC1C \uC120\uD0DD\uB428` : selectedOption !== void 0 ? optionLabel(selectedOption, labelKey) : "";
@@ -5016,7 +5046,9 @@ function KjunSelect({
5016
5046
  onChange,
5017
5047
  onOpenChange,
5018
5048
  onSearch,
5019
- onClear
5049
+ onClear,
5050
+ filterFn,
5051
+ searchKeys
5020
5052
  }) {
5021
5053
  const { open, query, limit, changeOpen, search, showMore } = useSelectState(controlled, disabled, optionPageSize, onOpenChange);
5022
5054
  const { colors } = useKjunStyles(), field = useKjunField(), ref = useRef23(null), spec = tokens28.input[size];
@@ -5041,7 +5073,9 @@ function KjunSelect({
5041
5073
  pageSize: optionPageSize,
5042
5074
  limit,
5043
5075
  loading,
5044
- multiple
5076
+ multiple,
5077
+ filterFn,
5078
+ searchKeys
5045
5079
  });
5046
5080
  const firstSelected = visible.findIndex(({ option }) => selected(option));
5047
5081
  if (firstSelected < 0) selectedRow.current = null;
@@ -6010,7 +6044,7 @@ function KjunRefreshButton({
6010
6044
  ...props
6011
6045
  }) {
6012
6046
  const reduced = useReducedMotion();
6013
- const label = ariaLabel || [targetName, "\uC0C8\uB85C\uACE0\uCE68"].filter(Boolean).join(" ");
6047
+ const label = ariaLabel || [targetName, mode === "text" ? text : "\uC0C8\uB85C\uACE0\uCE68"].filter(Boolean).join(" ");
6014
6048
  const hint = tooltip != null ? tooltip : mode === "icon" ? label : "";
6015
6049
  const button = /* @__PURE__ */ jsx36(KjunTooltip, { content: props.disabled || props.loading ? "" : hint, placement: tooltipPlacement, delay: tooltipDelay, children: /* @__PURE__ */ jsx36(
6016
6050
  KjunButton,
@@ -7191,9 +7225,18 @@ function tablePresentation(props, mobile) {
7191
7225
  }
7192
7226
  };
7193
7227
  }
7228
+ var warnedFormats = /* @__PURE__ */ new Set();
7229
+ function columnFormat(column) {
7230
+ if (typeof column.format === "function") return column.format;
7231
+ if (column.format != null && !warnedFormats.has(column.key)) {
7232
+ warnedFormats.add(column.key);
7233
+ console.warn(`KJUN Table: ${column.key} \uC5F4\uC758 format\uC740 \uD568\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4.`);
7234
+ }
7235
+ return void 0;
7236
+ }
7194
7237
  function tableCellValue(props, column, row, index) {
7195
7238
  const value = getValue(row, column.key);
7196
- const body = props.renderCell ? props.renderCell(value, column, row, index) : column.render ? column.render(value, row, index) : column.format ? column.format(value, row) : props.formatValue ? props.formatValue(value, column, row) : value == null ? "-" : typeof value === "number" && column.type === "number" ? new Intl.NumberFormat().format(value) : String(value);
7239
+ const body = props.renderCell ? props.renderCell(value, column, row, index) : column.render ? column.render(value, row, index) : columnFormat(column) ? columnFormat(column)(value, row) : props.formatValue ? props.formatValue(value, column, row) : value == null ? "-" : typeof value === "number" && column.type === "number" ? new Intl.NumberFormat().format(value) : String(value);
7197
7240
  return { value, body };
7198
7241
  }
7199
7242
 
@@ -234,6 +234,10 @@ function installLayerKeyboard(document2, state, keyupFallback = false) {
234
234
  var import_react2 = require("react");
235
235
 
236
236
  // shared/package-runtime/layer-state.ts
237
+ function chooseReturnTarget(chosen, fallback2) {
238
+ if (chosen === false) return void 0;
239
+ return chosen && chosen.isConnected !== false ? chosen : fallback2;
240
+ }
237
241
  function createLayerState() {
238
242
  const entries = /* @__PURE__ */ new Map(), listeners = /* @__PURE__ */ new Set();
239
243
  const restoredFocus = /* @__PURE__ */ new WeakSet();
@@ -254,6 +258,7 @@ function createLayerState() {
254
258
  return !layer.blocked && (layer.kind === "window" ? ((_a = topWindow()) == null ? void 0 : _a.id) === layer.id : ((_b = topWindow()) == null ? void 0 : _b.id) === layer.owner);
255
259
  };
256
260
  const top = () => [...entries.values()].filter(active).sort((a, b) => a.order - b.order).at(-1);
261
+ const topPopup = () => [...entries.values()].filter((layer) => layer.kind !== "tooltip" && active(layer)).sort((a, b) => a.order - b.order).at(-1);
257
262
  function dismiss(layer) {
258
263
  if (layer.closeRequested) return;
259
264
  layer.closeRequested = true;
@@ -273,6 +278,7 @@ function createLayerState() {
273
278
  entries,
274
279
  topWindow,
275
280
  top,
281
+ topPopup,
276
282
  windowOf,
277
283
  isRestoredFocus(target) {
278
284
  return !!target && restoredFocus.has(target);
@@ -421,21 +427,31 @@ function focusTrigger(target) {
421
427
  }
422
428
  return target;
423
429
  }
424
- function restoreFocus(target, state, id) {
430
+ function restoreFocus(target, state, id, fallback2) {
431
+ const usable = (item) => (item == null ? void 0 : item.isConnected) !== false && !(typeof HTMLElement !== "undefined" && item instanceof HTMLElement && item.closest("[inert]"));
425
432
  const restore = () => {
426
- var _a, _b, _c, _d;
433
+ var _a, _b, _c, _d, _e;
427
434
  if (state.entries.has(id)) return;
428
- if ((target == null ? void 0 : target.isConnected) !== false && !(typeof HTMLElement !== "undefined" && target instanceof HTMLElement && target.closest("[inert]"))) (_a = target == null ? void 0 : target.focus) == null ? void 0 : _a.call(target, { preventScroll: true });
429
- else (_d = (_c = (_b = state.topWindow()) == null ? void 0 : _b.root) == null ? void 0 : _c.focus) == null ? void 0 : _d.call(_c, { preventScroll: true });
435
+ if (usable(target)) (_a = target == null ? void 0 : target.focus) == null ? void 0 : _a.call(target, { preventScroll: true });
436
+ else if (fallback2 && fallback2 !== target && usable(fallback2)) (_b = fallback2.focus) == null ? void 0 : _b.call(fallback2, { preventScroll: true });
437
+ else (_e = (_d = (_c = state.topWindow()) == null ? void 0 : _c.root) == null ? void 0 : _d.focus) == null ? void 0 : _e.call(_d, { preventScroll: true });
430
438
  };
431
439
  if (typeof requestAnimationFrame !== "undefined") requestAnimationFrame(restore);
432
440
  else queueMicrotask(restore);
433
441
  }
434
- function useLayer(open, present, kind, close, trigger) {
442
+ function useLayer(open, present, kind, close, trigger, returnFocus) {
435
443
  var _a;
436
444
  const state = useLayerState(), parent = (0, import_react2.useContext)(LayerScope), id = (0, import_react2.useId)();
437
445
  const callback = (0, import_react2.useRef)(close);
438
446
  callback.current = close;
447
+ const returnRef = (0, import_react2.useRef)(returnFocus);
448
+ returnRef.current = returnFocus;
449
+ const fallbackFocus = (0, import_react2.useRef)(null);
450
+ const removeLayer = () => {
451
+ const removed = state.remove(id, currentFocus());
452
+ if (removed) fallbackFocus.current = removed;
453
+ return removed && returnRef.current ? chooseReturnTarget(returnRef.current(), removed) : removed;
454
+ };
439
455
  const openingFocus = (0, import_react2.useRef)(null);
440
456
  const wasOpen = (0, import_react2.useRef)(false);
441
457
  const pendingFocus = (0, import_react2.useRef)(null);
@@ -447,17 +463,17 @@ function useLayer(open, present, kind, close, trigger) {
447
463
  state.open(id, kind, parent, () => callback.current(), focusTrigger(trigger == null ? void 0 : trigger.current) || openingFocus.current);
448
464
  } else if (kind === "window" && present) state.markClosing(id);
449
465
  else if (kind !== "window" || !present) {
450
- const target = state.remove(id, currentFocus());
466
+ const target = removeLayer();
451
467
  if (target) pendingFocus.current = target;
452
468
  if (!present && pendingFocus.current) {
453
- restoreFocus(pendingFocus.current, state, id);
469
+ restoreFocus(pendingFocus.current, state, id, fallbackFocus.current);
454
470
  pendingFocus.current = null;
455
471
  }
456
472
  }
457
473
  }, [open, present, state, id, kind, parent, trigger]);
458
474
  (0, import_react2.useLayoutEffect)(() => () => {
459
- const target = state.remove(id, currentFocus());
460
- if (target || pendingFocus.current) restoreFocus(target || pendingFocus.current, state, id);
475
+ const target = removeLayer();
476
+ if (target || pendingFocus.current) restoreFocus(target || pendingFocus.current, state, id, fallbackFocus.current);
461
477
  }, [state, id]);
462
478
  const entry = state.entries.get(id);
463
479
  return {
@@ -3227,7 +3243,7 @@ function KjunRadioGroup({
3227
3243
  accessibilityLabel: ariaLabel,
3228
3244
  style: {
3229
3245
  flexDirection: direction === "vertical" ? "column" : "row",
3230
- flexWrap: "wrap",
3246
+ flexWrap: direction === "vertical" ? "nowrap" : "wrap",
3231
3247
  gap: direction === "vertical" ? import_tokens19.tokens.dimension.value8 : import_tokens19.tokens.dimension.value16
3232
3248
  },
3233
3249
  children: children || options.map((option) => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
@@ -4416,6 +4432,17 @@ function nextOptionValue(value, option, key, multiple) {
4416
4432
  if (!multiple) return id;
4417
4433
  return Array.isArray(value) && value.includes(id) ? value.filter((item) => item !== id) : [...Array.isArray(value) ? value : [], id];
4418
4434
  }
4435
+ function optionMatchRank(option, query, labelKey, searchKeys = []) {
4436
+ const needle = query.trim().toLocaleLowerCase();
4437
+ if (!needle) return 0;
4438
+ const label = optionLabel(option, labelKey).toLocaleLowerCase();
4439
+ if (label.startsWith(needle)) return 0;
4440
+ if (label.includes(needle)) return 1;
4441
+ return typeof option === "object" && searchKeys.some((key) => {
4442
+ var _a;
4443
+ return String((_a = option[key]) != null ? _a : "").toLocaleLowerCase().includes(needle);
4444
+ }) ? 2 : -1;
4445
+ }
4419
4446
  function selectOptions({
4420
4447
  options,
4421
4448
  value,
@@ -4425,10 +4452,13 @@ function selectOptions({
4425
4452
  pageSize,
4426
4453
  limit,
4427
4454
  loading,
4428
- multiple
4455
+ multiple,
4456
+ filterFn,
4457
+ searchKeys = []
4429
4458
  }) {
4430
4459
  const selected = (option) => optionSelected(value, option, valueKey);
4431
- const matching = options.map((option, index) => ({ option, key: optionKey(option, valueKey, index) })).filter(({ option }) => optionLabel(option, labelKey).toLocaleLowerCase().includes(query.trim().toLocaleLowerCase()));
4460
+ const indexed = options.map((option, index) => ({ option, key: optionKey(option, valueKey, index) }));
4461
+ const matching = filterFn ? indexed.filter(({ option }) => filterFn(option, query)) : indexed.map((item, index) => ({ item, index, rank: optionMatchRank(item.option, query, labelKey, searchKeys) })).filter(({ rank }) => rank >= 0).sort((a, b) => a.rank - b.rank || a.index - b.index).map(({ item }) => item);
4432
4462
  const visible = loading ? [] : pageSize > 0 ? matching.slice(0, limit) : matching;
4433
4463
  const selectedOption = options.find(selected);
4434
4464
  const label = multiple && Array.isArray(value) && value.length > 1 ? `${value.length}\uAC1C \uC120\uD0DD\uB428` : selectedOption !== void 0 ? optionLabel(selectedOption, labelKey) : "";
@@ -5163,7 +5193,9 @@ function KjunSelect({
5163
5193
  onChange,
5164
5194
  onOpenChange,
5165
5195
  onSearch,
5166
- onClear
5196
+ onClear,
5197
+ filterFn,
5198
+ searchKeys
5167
5199
  }) {
5168
5200
  const { open, query, limit, changeOpen, search, showMore } = useSelectState(controlled, disabled, optionPageSize, onOpenChange);
5169
5201
  const { colors } = useKjunStyles(), field = useKjunField(), ref = (0, import_react47.useRef)(null), spec = import_tokens31.tokens.input[size];
@@ -5188,7 +5220,9 @@ function KjunSelect({
5188
5220
  pageSize: optionPageSize,
5189
5221
  limit,
5190
5222
  loading,
5191
- multiple
5223
+ multiple,
5224
+ filterFn,
5225
+ searchKeys
5192
5226
  });
5193
5227
  const firstSelected = visible.findIndex(({ option }) => selected(option));
5194
5228
  if (firstSelected < 0) selectedRow.current = null;
@@ -6243,7 +6277,7 @@ function KjunRefreshButton({
6243
6277
  ...props
6244
6278
  }) {
6245
6279
  const reduced = useReducedMotion();
6246
- const label = ariaLabel || [targetName, "\uC0C8\uB85C\uACE0\uCE68"].filter(Boolean).join(" ");
6280
+ const label = ariaLabel || [targetName, mode === "text" ? text : "\uC0C8\uB85C\uACE0\uCE68"].filter(Boolean).join(" ");
6247
6281
  const hint = tooltip != null ? tooltip : mode === "icon" ? label : "";
6248
6282
  const button = /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(KjunTooltip, { content: props.disabled || props.loading ? "" : hint, placement: tooltipPlacement, delay: tooltipDelay, children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
6249
6283
  KjunButton,
@@ -7407,9 +7441,18 @@ function tablePresentation(props, mobile) {
7407
7441
  }
7408
7442
  };
7409
7443
  }
7444
+ var warnedFormats = /* @__PURE__ */ new Set();
7445
+ function columnFormat(column) {
7446
+ if (typeof column.format === "function") return column.format;
7447
+ if (column.format != null && !warnedFormats.has(column.key)) {
7448
+ warnedFormats.add(column.key);
7449
+ console.warn(`KJUN Table: ${column.key} \uC5F4\uC758 format\uC740 \uD568\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4.`);
7450
+ }
7451
+ return void 0;
7452
+ }
7410
7453
  function tableCellValue(props, column, row, index) {
7411
7454
  const value = getValue(row, column.key);
7412
- const body = props.renderCell ? props.renderCell(value, column, row, index) : column.render ? column.render(value, row, index) : column.format ? column.format(value, row) : props.formatValue ? props.formatValue(value, column, row) : value == null ? "-" : typeof value === "number" && column.type === "number" ? new Intl.NumberFormat().format(value) : String(value);
7455
+ const body = props.renderCell ? props.renderCell(value, column, row, index) : column.render ? column.render(value, row, index) : columnFormat(column) ? columnFormat(column)(value, row) : props.formatValue ? props.formatValue(value, column, row) : value == null ? "-" : typeof value === "number" && column.type === "number" ? new Intl.NumberFormat().format(value) : String(value);
7413
7456
  return { value, body };
7414
7457
  }
7415
7458
 
package/dist/index.web.js CHANGED
@@ -119,6 +119,10 @@ function installLayerKeyboard(document2, state, keyupFallback = false) {
119
119
  import { createContext as createContext2, useContext as useContext2, useId, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
120
120
 
121
121
  // shared/package-runtime/layer-state.ts
122
+ function chooseReturnTarget(chosen, fallback2) {
123
+ if (chosen === false) return void 0;
124
+ return chosen && chosen.isConnected !== false ? chosen : fallback2;
125
+ }
122
126
  function createLayerState() {
123
127
  const entries = /* @__PURE__ */ new Map(), listeners = /* @__PURE__ */ new Set();
124
128
  const restoredFocus = /* @__PURE__ */ new WeakSet();
@@ -139,6 +143,7 @@ function createLayerState() {
139
143
  return !layer.blocked && (layer.kind === "window" ? ((_a = topWindow()) == null ? void 0 : _a.id) === layer.id : ((_b = topWindow()) == null ? void 0 : _b.id) === layer.owner);
140
144
  };
141
145
  const top = () => [...entries.values()].filter(active).sort((a, b) => a.order - b.order).at(-1);
146
+ const topPopup = () => [...entries.values()].filter((layer) => layer.kind !== "tooltip" && active(layer)).sort((a, b) => a.order - b.order).at(-1);
142
147
  function dismiss(layer) {
143
148
  if (layer.closeRequested) return;
144
149
  layer.closeRequested = true;
@@ -158,6 +163,7 @@ function createLayerState() {
158
163
  entries,
159
164
  topWindow,
160
165
  top,
166
+ topPopup,
161
167
  windowOf,
162
168
  isRestoredFocus(target) {
163
169
  return !!target && restoredFocus.has(target);
@@ -306,21 +312,31 @@ function focusTrigger(target) {
306
312
  }
307
313
  return target;
308
314
  }
309
- function restoreFocus(target, state, id) {
315
+ function restoreFocus(target, state, id, fallback2) {
316
+ const usable = (item) => (item == null ? void 0 : item.isConnected) !== false && !(typeof HTMLElement !== "undefined" && item instanceof HTMLElement && item.closest("[inert]"));
310
317
  const restore = () => {
311
- var _a, _b, _c, _d;
318
+ var _a, _b, _c, _d, _e;
312
319
  if (state.entries.has(id)) return;
313
- if ((target == null ? void 0 : target.isConnected) !== false && !(typeof HTMLElement !== "undefined" && target instanceof HTMLElement && target.closest("[inert]"))) (_a = target == null ? void 0 : target.focus) == null ? void 0 : _a.call(target, { preventScroll: true });
314
- else (_d = (_c = (_b = state.topWindow()) == null ? void 0 : _b.root) == null ? void 0 : _c.focus) == null ? void 0 : _d.call(_c, { preventScroll: true });
320
+ if (usable(target)) (_a = target == null ? void 0 : target.focus) == null ? void 0 : _a.call(target, { preventScroll: true });
321
+ else if (fallback2 && fallback2 !== target && usable(fallback2)) (_b = fallback2.focus) == null ? void 0 : _b.call(fallback2, { preventScroll: true });
322
+ else (_e = (_d = (_c = state.topWindow()) == null ? void 0 : _c.root) == null ? void 0 : _d.focus) == null ? void 0 : _e.call(_d, { preventScroll: true });
315
323
  };
316
324
  if (typeof requestAnimationFrame !== "undefined") requestAnimationFrame(restore);
317
325
  else queueMicrotask(restore);
318
326
  }
319
- function useLayer(open, present, kind, close, trigger) {
327
+ function useLayer(open, present, kind, close, trigger, returnFocus) {
320
328
  var _a;
321
329
  const state = useLayerState(), parent = useContext2(LayerScope), id = useId();
322
330
  const callback = useRef(close);
323
331
  callback.current = close;
332
+ const returnRef = useRef(returnFocus);
333
+ returnRef.current = returnFocus;
334
+ const fallbackFocus = useRef(null);
335
+ const removeLayer = () => {
336
+ const removed = state.remove(id, currentFocus());
337
+ if (removed) fallbackFocus.current = removed;
338
+ return removed && returnRef.current ? chooseReturnTarget(returnRef.current(), removed) : removed;
339
+ };
324
340
  const openingFocus = useRef(null);
325
341
  const wasOpen = useRef(false);
326
342
  const pendingFocus = useRef(null);
@@ -332,17 +348,17 @@ function useLayer(open, present, kind, close, trigger) {
332
348
  state.open(id, kind, parent, () => callback.current(), focusTrigger(trigger == null ? void 0 : trigger.current) || openingFocus.current);
333
349
  } else if (kind === "window" && present) state.markClosing(id);
334
350
  else if (kind !== "window" || !present) {
335
- const target = state.remove(id, currentFocus());
351
+ const target = removeLayer();
336
352
  if (target) pendingFocus.current = target;
337
353
  if (!present && pendingFocus.current) {
338
- restoreFocus(pendingFocus.current, state, id);
354
+ restoreFocus(pendingFocus.current, state, id, fallbackFocus.current);
339
355
  pendingFocus.current = null;
340
356
  }
341
357
  }
342
358
  }, [open, present, state, id, kind, parent, trigger]);
343
359
  useLayoutEffect(() => () => {
344
- const target = state.remove(id, currentFocus());
345
- if (target || pendingFocus.current) restoreFocus(target || pendingFocus.current, state, id);
360
+ const target = removeLayer();
361
+ if (target || pendingFocus.current) restoreFocus(target || pendingFocus.current, state, id, fallbackFocus.current);
346
362
  }, [state, id]);
347
363
  const entry = state.entries.get(id);
348
364
  return {
@@ -3193,7 +3209,7 @@ function KjunRadioGroup({
3193
3209
  accessibilityLabel: ariaLabel,
3194
3210
  style: {
3195
3211
  flexDirection: direction === "vertical" ? "column" : "row",
3196
- flexWrap: "wrap",
3212
+ flexWrap: direction === "vertical" ? "nowrap" : "wrap",
3197
3213
  gap: direction === "vertical" ? tokens17.dimension.value8 : tokens17.dimension.value16
3198
3214
  },
3199
3215
  children: children || options.map((option) => /* @__PURE__ */ jsx19(
@@ -4431,6 +4447,17 @@ function nextOptionValue(value, option, key, multiple) {
4431
4447
  if (!multiple) return id;
4432
4448
  return Array.isArray(value) && value.includes(id) ? value.filter((item) => item !== id) : [...Array.isArray(value) ? value : [], id];
4433
4449
  }
4450
+ function optionMatchRank(option, query, labelKey, searchKeys = []) {
4451
+ const needle = query.trim().toLocaleLowerCase();
4452
+ if (!needle) return 0;
4453
+ const label = optionLabel(option, labelKey).toLocaleLowerCase();
4454
+ if (label.startsWith(needle)) return 0;
4455
+ if (label.includes(needle)) return 1;
4456
+ return typeof option === "object" && searchKeys.some((key) => {
4457
+ var _a;
4458
+ return String((_a = option[key]) != null ? _a : "").toLocaleLowerCase().includes(needle);
4459
+ }) ? 2 : -1;
4460
+ }
4434
4461
  function selectOptions({
4435
4462
  options,
4436
4463
  value,
@@ -4440,10 +4467,13 @@ function selectOptions({
4440
4467
  pageSize,
4441
4468
  limit,
4442
4469
  loading,
4443
- multiple
4470
+ multiple,
4471
+ filterFn,
4472
+ searchKeys = []
4444
4473
  }) {
4445
4474
  const selected = (option) => optionSelected(value, option, valueKey);
4446
- const matching = options.map((option, index) => ({ option, key: optionKey(option, valueKey, index) })).filter(({ option }) => optionLabel(option, labelKey).toLocaleLowerCase().includes(query.trim().toLocaleLowerCase()));
4475
+ const indexed = options.map((option, index) => ({ option, key: optionKey(option, valueKey, index) }));
4476
+ const matching = filterFn ? indexed.filter(({ option }) => filterFn(option, query)) : indexed.map((item, index) => ({ item, index, rank: optionMatchRank(item.option, query, labelKey, searchKeys) })).filter(({ rank }) => rank >= 0).sort((a, b) => a.rank - b.rank || a.index - b.index).map(({ item }) => item);
4447
4477
  const visible = loading ? [] : pageSize > 0 ? matching.slice(0, limit) : matching;
4448
4478
  const selectedOption = options.find(selected);
4449
4479
  const label = multiple && Array.isArray(value) && value.length > 1 ? `${value.length}\uAC1C \uC120\uD0DD\uB428` : selectedOption !== void 0 ? optionLabel(selectedOption, labelKey) : "";
@@ -5178,7 +5208,9 @@ function KjunSelect({
5178
5208
  onChange,
5179
5209
  onOpenChange,
5180
5210
  onSearch,
5181
- onClear
5211
+ onClear,
5212
+ filterFn,
5213
+ searchKeys
5182
5214
  }) {
5183
5215
  const { open, query, limit, changeOpen, search, showMore } = useSelectState(controlled, disabled, optionPageSize, onOpenChange);
5184
5216
  const { colors } = useKjunStyles(), field = useKjunField(), ref = useRef24(null), spec = tokens29.input[size];
@@ -5203,7 +5235,9 @@ function KjunSelect({
5203
5235
  pageSize: optionPageSize,
5204
5236
  limit,
5205
5237
  loading,
5206
- multiple
5238
+ multiple,
5239
+ filterFn,
5240
+ searchKeys
5207
5241
  });
5208
5242
  const firstSelected = visible.findIndex(({ option }) => selected(option));
5209
5243
  if (firstSelected < 0) selectedRow.current = null;
@@ -6265,7 +6299,7 @@ function KjunRefreshButton({
6265
6299
  ...props
6266
6300
  }) {
6267
6301
  const reduced = useReducedMotion();
6268
- const label = ariaLabel || [targetName, "\uC0C8\uB85C\uACE0\uCE68"].filter(Boolean).join(" ");
6302
+ const label = ariaLabel || [targetName, mode === "text" ? text : "\uC0C8\uB85C\uACE0\uCE68"].filter(Boolean).join(" ");
6269
6303
  const hint = tooltip != null ? tooltip : mode === "icon" ? label : "";
6270
6304
  const button = /* @__PURE__ */ jsx36(KjunTooltip, { content: props.disabled || props.loading ? "" : hint, placement: tooltipPlacement, delay: tooltipDelay, children: /* @__PURE__ */ jsx36(
6271
6305
  KjunButton,
@@ -7446,9 +7480,18 @@ function tablePresentation(props, mobile) {
7446
7480
  }
7447
7481
  };
7448
7482
  }
7483
+ var warnedFormats = /* @__PURE__ */ new Set();
7484
+ function columnFormat(column) {
7485
+ if (typeof column.format === "function") return column.format;
7486
+ if (column.format != null && !warnedFormats.has(column.key)) {
7487
+ warnedFormats.add(column.key);
7488
+ console.warn(`KJUN Table: ${column.key} \uC5F4\uC758 format\uC740 \uD568\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4.`);
7489
+ }
7490
+ return void 0;
7491
+ }
7449
7492
  function tableCellValue(props, column, row, index) {
7450
7493
  const value = getValue(row, column.key);
7451
- const body = props.renderCell ? props.renderCell(value, column, row, index) : column.render ? column.render(value, row, index) : column.format ? column.format(value, row) : props.formatValue ? props.formatValue(value, column, row) : value == null ? "-" : typeof value === "number" && column.type === "number" ? new Intl.NumberFormat().format(value) : String(value);
7494
+ const body = props.renderCell ? props.renderCell(value, column, row, index) : column.render ? column.render(value, row, index) : columnFormat(column) ? columnFormat(column)(value, row) : props.formatValue ? props.formatValue(value, column, row) : value == null ? "-" : typeof value === "number" && column.type === "number" ? new Intl.NumberFormat().format(value) : String(value);
7452
7495
  return { value, body };
7453
7496
  }
7454
7497
 
@@ -25,6 +25,10 @@ export interface KjunSelectProps<T extends SelectOption = SelectOption> {
25
25
  onChange?: (v: OptionValue | OptionValue[] | null) => void;
26
26
  onOpenChange?: (v: boolean) => void;
27
27
  onSearch?: (v: string) => void;
28
+ /** Replaces the default ranked label/searchKeys match; results keep the original order. */
29
+ filterFn?: (option: T, query: string) => boolean;
30
+ /** Extra option fields searched after the label. */
31
+ searchKeys?: string[];
28
32
  onClear?: () => void;
29
33
  }
30
- export declare function KjunSelect<T extends SelectOption = SelectOption>({ value, options, placeholder, ariaLabel, labelKey, valueKey, size, disabled, searchable, open: controlled, loading, searchPlaceholder, optionPageSize, clearable, multiple, error, menuHeader, renderOption, renderSelected, onValueChange, onChange, onOpenChange, onSearch, onClear, }: KjunSelectProps<T>): import("react/jsx-runtime").JSX.Element;
34
+ export declare function KjunSelect<T extends SelectOption = SelectOption>({ value, options, placeholder, ariaLabel, labelKey, valueKey, size, disabled, searchable, open: controlled, loading, searchPlaceholder, optionPageSize, clearable, multiple, error, menuHeader, renderOption, renderSelected, onValueChange, onChange, onOpenChange, onSearch, onClear, filterFn, searchKeys, }: KjunSelectProps<T>): import("react/jsx-runtime").JSX.Element;
@@ -25,6 +25,10 @@ export interface KjunSelectProps<T extends SelectOption = SelectOption> {
25
25
  onChange?: (v: OptionValue | OptionValue[] | null) => void;
26
26
  onOpenChange?: (v: boolean) => void;
27
27
  onSearch?: (v: string) => void;
28
+ /** Replaces the default ranked label/searchKeys match; results keep the original order. */
29
+ filterFn?: (option: T, query: string) => boolean;
30
+ /** Extra option fields searched after the label. */
31
+ searchKeys?: string[];
28
32
  onClear?: () => void;
29
33
  }
30
- export declare function KjunSelect<T extends SelectOption = SelectOption>({ value, options, placeholder, ariaLabel, labelKey, valueKey, size, disabled, searchable, open: controlled, loading, searchPlaceholder, optionPageSize, clearable, multiple, error, menuHeader, renderOption, renderSelected, onValueChange, onChange, onOpenChange, onSearch, onClear, }: KjunSelectProps<T>): import("react/jsx-runtime").JSX.Element;
34
+ export declare function KjunSelect<T extends SelectOption = SelectOption>({ value, options, placeholder, ariaLabel, labelKey, valueKey, size, disabled, searchable, open: controlled, loading, searchPlaceholder, optionPageSize, clearable, multiple, error, menuHeader, renderOption, renderSelected, onValueChange, onChange, onOpenChange, onSearch, onClear, filterFn, searchKeys, }: KjunSelectProps<T>): import("react/jsx-runtime").JSX.Element;
@@ -6,6 +6,8 @@ export interface FocusTarget {
6
6
  }) => void;
7
7
  isConnected?: boolean;
8
8
  }
9
+ /** A caller-chosen return target wins while it is still in the document; false means the caller moves focus itself. */
10
+ export declare function chooseReturnTarget(chosen: FocusTarget | null | false | undefined, fallback: FocusTarget | null | undefined): FocusTarget | null | undefined;
9
11
  export interface LayerEntry {
10
12
  id: string;
11
13
  kind: LayerKind;
@@ -24,6 +26,7 @@ export declare function createLayerState(): {
24
26
  entries: Map<string, LayerEntry>;
25
27
  topWindow: () => LayerEntry | undefined;
26
28
  top: () => LayerEntry | undefined;
29
+ topPopup: () => LayerEntry | undefined;
27
30
  windowOf: (id?: string) => string | undefined;
28
31
  isRestoredFocus(target?: FocusTarget | null): boolean;
29
32
  clearRestoredFocus(target?: FocusTarget | null): void;
@@ -6,6 +6,8 @@ export interface FocusTarget {
6
6
  }) => void;
7
7
  isConnected?: boolean;
8
8
  }
9
+ /** A caller-chosen return target wins while it is still in the document; false means the caller moves focus itself. */
10
+ export declare function chooseReturnTarget(chosen: FocusTarget | null | false | undefined, fallback: FocusTarget | null | undefined): FocusTarget | null | undefined;
9
11
  export interface LayerEntry {
10
12
  id: string;
11
13
  kind: LayerKind;
@@ -24,6 +26,7 @@ export declare function createLayerState(): {
24
26
  entries: Map<string, LayerEntry>;
25
27
  topWindow: () => LayerEntry | undefined;
26
28
  top: () => LayerEntry | undefined;
29
+ topPopup: () => LayerEntry | undefined;
27
30
  windowOf: (id?: string) => string | undefined;
28
31
  isRestoredFocus(target?: FocusTarget | null): boolean;
29
32
  clearRestoredFocus(target?: FocusTarget | null): void;
@@ -8,7 +8,9 @@ export declare const optionDisabled: (option: SelectOption) => boolean;
8
8
  export declare function optionKey(option: SelectOption, key: string, index: number): string;
9
9
  export declare function optionSelected(value: OptionValue | OptionValue[] | null, option: SelectOption, key: string): boolean;
10
10
  export declare function nextOptionValue(value: OptionValue | OptionValue[] | null, option: SelectOption, key: string, multiple: boolean): OptionValue | OptionValue[];
11
- export declare function selectOptions<T extends SelectOption>({ options, value, labelKey, valueKey, query, pageSize, limit, loading, multiple, }: {
11
+ /** 0 label starts with the query, 1 label contains it, 2 a searchKeys field contains it, -1 no match; an empty query matches as 0. */
12
+ export declare function optionMatchRank(option: SelectOption, query: string, labelKey: string, searchKeys?: readonly string[]): 0 | 1 | -1 | 2;
13
+ export declare function selectOptions<T extends SelectOption>({ options, value, labelKey, valueKey, query, pageSize, limit, loading, multiple, filterFn, searchKeys, }: {
12
14
  options: T[];
13
15
  value: OptionValue | OptionValue[] | null;
14
16
  labelKey: string;
@@ -18,6 +20,8 @@ export declare function selectOptions<T extends SelectOption>({ options, value,
18
20
  limit: number;
19
21
  loading: boolean;
20
22
  multiple: boolean;
23
+ filterFn?: (option: T, query: string) => boolean;
24
+ searchKeys?: readonly string[];
21
25
  }): {
22
26
  selected: (option: T) => boolean;
23
27
  matching: {
@@ -8,7 +8,9 @@ export declare const optionDisabled: (option: SelectOption) => boolean;
8
8
  export declare function optionKey(option: SelectOption, key: string, index: number): string;
9
9
  export declare function optionSelected(value: OptionValue | OptionValue[] | null, option: SelectOption, key: string): boolean;
10
10
  export declare function nextOptionValue(value: OptionValue | OptionValue[] | null, option: SelectOption, key: string, multiple: boolean): OptionValue | OptionValue[];
11
- export declare function selectOptions<T extends SelectOption>({ options, value, labelKey, valueKey, query, pageSize, limit, loading, multiple, }: {
11
+ /** 0 label starts with the query, 1 label contains it, 2 a searchKeys field contains it, -1 no match; an empty query matches as 0. */
12
+ export declare function optionMatchRank(option: SelectOption, query: string, labelKey: string, searchKeys?: readonly string[]): 0 | 1 | -1 | 2;
13
+ export declare function selectOptions<T extends SelectOption>({ options, value, labelKey, valueKey, query, pageSize, limit, loading, multiple, filterFn, searchKeys, }: {
12
14
  options: T[];
13
15
  value: OptionValue | OptionValue[] | null;
14
16
  labelKey: string;
@@ -18,6 +20,8 @@ export declare function selectOptions<T extends SelectOption>({ options, value,
18
20
  limit: number;
19
21
  loading: boolean;
20
22
  multiple: boolean;
23
+ filterFn?: (option: T, query: string) => boolean;
24
+ searchKeys?: readonly string[];
21
25
  }): {
22
26
  selected: (option: T) => boolean;
23
27
  matching: {
@@ -11,6 +11,10 @@ export declare function tablePresentation<Row extends object>(props: TableProps<
11
11
  sections: TableCardSection[];
12
12
  columnsForSection(row: Row, section: TableCardSection): TableColumn<Row>[];
13
13
  };
14
+ export declare function columnFormat<Format>(column: {
15
+ key: string;
16
+ format?: Format;
17
+ }): Format | undefined;
14
18
  export declare function tableCellValue<Row extends object>(props: TableProps<Row, unknown>, column: TableColumn<Row>, row: Row, index: number): {
15
19
  value: unknown;
16
20
  body: ReactNode;
@@ -11,6 +11,10 @@ export declare function tablePresentation<Row extends object>(props: TableProps<
11
11
  sections: TableCardSection[];
12
12
  columnsForSection(row: Row, section: TableCardSection): TableColumn<Row>[];
13
13
  };
14
+ export declare function columnFormat<Format>(column: {
15
+ key: string;
16
+ format?: Format;
17
+ }): Format | undefined;
14
18
  export declare function tableCellValue<Row extends object>(props: TableProps<Row, unknown>, column: TableColumn<Row>, row: Row, index: number): {
15
19
  value: unknown;
16
20
  body: ReactNode;
@@ -9,6 +9,7 @@ export declare function useLayerState(): {
9
9
  entries: Map<string, import("./layer-state.cjs").LayerEntry>;
10
10
  topWindow: () => import("./layer-state.cjs").LayerEntry | undefined;
11
11
  top: () => import("./layer-state.cjs").LayerEntry | undefined;
12
+ topPopup: () => import("./layer-state.cjs").LayerEntry | undefined;
12
13
  windowOf: (id?: string) => string | undefined;
13
14
  isRestoredFocus(target?: FocusTarget | null): boolean;
14
15
  clearRestoredFocus(target?: FocusTarget | null): void;
@@ -36,12 +37,13 @@ export declare function useActiveLayerScope(): boolean;
36
37
  export declare function focusTrigger(target: FocusTarget | null | undefined): FocusTarget | HTMLElement | null | undefined;
37
38
  export declare function useLayer(open: boolean, present: boolean, kind: LayerKind, close: () => void, trigger?: {
38
39
  current: FocusTarget | null;
39
- }): {
40
+ }, returnFocus?: () => FocusTarget | null | false | undefined): {
40
41
  id: string;
41
42
  state: {
42
43
  entries: Map<string, import("./layer-state.cjs").LayerEntry>;
43
44
  topWindow: () => import("./layer-state.cjs").LayerEntry | undefined;
44
45
  top: () => import("./layer-state.cjs").LayerEntry | undefined;
46
+ topPopup: () => import("./layer-state.cjs").LayerEntry | undefined;
45
47
  windowOf: (id?: string) => string | undefined;
46
48
  isRestoredFocus(target?: FocusTarget | null): boolean;
47
49
  clearRestoredFocus(target?: FocusTarget | null): void;
@@ -9,6 +9,7 @@ export declare function useLayerState(): {
9
9
  entries: Map<string, import("./layer-state.js").LayerEntry>;
10
10
  topWindow: () => import("./layer-state.js").LayerEntry | undefined;
11
11
  top: () => import("./layer-state.js").LayerEntry | undefined;
12
+ topPopup: () => import("./layer-state.js").LayerEntry | undefined;
12
13
  windowOf: (id?: string) => string | undefined;
13
14
  isRestoredFocus(target?: FocusTarget | null): boolean;
14
15
  clearRestoredFocus(target?: FocusTarget | null): void;
@@ -36,12 +37,13 @@ export declare function useActiveLayerScope(): boolean;
36
37
  export declare function focusTrigger(target: FocusTarget | null | undefined): FocusTarget | HTMLElement | null | undefined;
37
38
  export declare function useLayer(open: boolean, present: boolean, kind: LayerKind, close: () => void, trigger?: {
38
39
  current: FocusTarget | null;
39
- }): {
40
+ }, returnFocus?: () => FocusTarget | null | false | undefined): {
40
41
  id: string;
41
42
  state: {
42
43
  entries: Map<string, import("./layer-state.js").LayerEntry>;
43
44
  topWindow: () => import("./layer-state.js").LayerEntry | undefined;
44
45
  top: () => import("./layer-state.js").LayerEntry | undefined;
46
+ topPopup: () => import("./layer-state.js").LayerEntry | undefined;
45
47
  windowOf: (id?: string) => string | undefined;
46
48
  isRestoredFocus(target?: FocusTarget | null): boolean;
47
49
  clearRestoredFocus(target?: FocusTarget | null): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kjun-ui/native",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "license": "MIT",
5
5
  "homepage": "https://ui.kjun.dev",
6
6
  "repository": {
@@ -47,8 +47,8 @@
47
47
  "./package.json": "./package.json"
48
48
  },
49
49
  "dependencies": {
50
- "@kjun-ui/tokens": "0.8.1",
51
- "@kjun-ui/icons": "0.8.1"
50
+ "@kjun-ui/tokens": "0.9.0",
51
+ "@kjun-ui/icons": "0.9.0"
52
52
  },
53
53
  "peerDependencies": {
54
54
  "react": "^19.2.3",