@morze/ui 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -217,7 +217,6 @@ function Button({
217
217
  asChild = false,
218
218
  tone,
219
219
  loading = false,
220
- dot = false,
221
220
  disabled,
222
221
  children,
223
222
  ...props
@@ -225,7 +224,6 @@ function Button({
225
224
  const Comp = asChild ? Slot.Root : "button";
226
225
  const isDisabled = disabled || loading;
227
226
  const decorate = (label) => /* @__PURE__ */ jsxs(Fragment, { children: [
228
- dot ? /* @__PURE__ */ jsx("span", { className: "mz-btn__dot", "aria-hidden": "true" }) : null,
229
227
  label,
230
228
  loading ? /* @__PURE__ */ jsx("span", { className: "mz-btn__spinner", "aria-hidden": "true", children: /* @__PURE__ */ jsx(SpinnerIcon, {}) }) : null
231
229
  ] });
@@ -269,7 +267,6 @@ function Badge({
269
267
  className,
270
268
  variant = "solid",
271
269
  tone,
272
- dot = false,
273
270
  asChild = false,
274
271
  children,
275
272
  ...props
@@ -283,10 +280,7 @@ function Badge({
283
280
  "data-tone": tone,
284
281
  className: cn(badgeVariants({ variant }), className),
285
282
  ...props,
286
- children: asChild ? children : /* @__PURE__ */ jsxs(Fragment, { children: [
287
- dot ? /* @__PURE__ */ jsx("span", { className: "mz-badge__dot", "aria-hidden": "true" }) : null,
288
- children
289
- ] })
283
+ children
290
284
  }
291
285
  );
292
286
  }
@@ -1824,6 +1818,8 @@ var defaultDataTableLabels = {
1824
1818
  previousPage: "Previous page",
1825
1819
  nextPage: "Next page",
1826
1820
  lastPage: "Last page",
1821
+ loadMore: "Load more",
1822
+ loadingMore: "Loading\u2026",
1827
1823
  selectedCount: (count) => `Selected: ${count}`,
1828
1824
  allMatchingSuffix: " (all matching)",
1829
1825
  selectAllMatching: (total) => `Select all ${total}`,
@@ -1848,6 +1844,8 @@ function isFilterActive(value) {
1848
1844
  return Boolean(value.from || value.to);
1849
1845
  case "boolean":
1850
1846
  return true;
1847
+ case "custom":
1848
+ return !(value.value === void 0 || value.value === null || value.value === "" || Array.isArray(value.value) && value.value.length === 0);
1851
1849
  }
1852
1850
  }
1853
1851
  function activeFilters(filters) {
@@ -1918,10 +1916,11 @@ function ColumnFilter({ columnLabel, def, value, onApply, labels: labelsProp })
1918
1916
  draft,
1919
1917
  setDraft,
1920
1918
  labels,
1921
- onSubmit: () => commit(draft)
1919
+ onSubmit: () => commit(draft),
1920
+ onCommit: commit
1922
1921
  }
1923
1922
  ),
1924
- /* @__PURE__ */ jsxs("div", { className: "mz-dt__filter-actions", children: [
1923
+ def.type === "custom" && def.actions === false ? null : /* @__PURE__ */ jsxs("div", { className: "mz-dt__filter-actions", children: [
1925
1924
  /* @__PURE__ */ jsx(Button, { size: "sm", variant: "ghost", onClick: () => commit(void 0), children: labels.reset }),
1926
1925
  /* @__PURE__ */ jsx(Button, { size: "sm", onClick: () => commit(draft), children: labels.apply })
1927
1926
  ] })
@@ -1933,7 +1932,8 @@ function FilterBody({
1933
1932
  draft,
1934
1933
  setDraft,
1935
1934
  labels,
1936
- onSubmit
1935
+ onSubmit,
1936
+ onCommit
1937
1937
  }) {
1938
1938
  const submitOnEnter = (event) => {
1939
1939
  if (event.key === "Enter") {
@@ -2041,6 +2041,17 @@ function FilterBody({
2041
2041
  )
2042
2042
  ] });
2043
2043
  }
2044
+ if (def.type === "custom") {
2045
+ const staged = draft?.type === "custom" ? draft : void 0;
2046
+ const wrap = (value, label) => value === void 0 ? void 0 : { type: "custom", value, label };
2047
+ return /* @__PURE__ */ jsx(Fragment, { children: def.render({
2048
+ value: staged?.value,
2049
+ label: staged?.label,
2050
+ onChange: (value, label) => setDraft(wrap(value, label)),
2051
+ commit: (value, label) => onCommit(wrap(value, label)),
2052
+ labels
2053
+ }) });
2054
+ }
2044
2055
  const current = draft?.type === "boolean" ? draft.value : void 0;
2045
2056
  return /* @__PURE__ */ jsx("div", { className: "mz-dt__filter-options", role: "radiogroup", children: [
2046
2057
  { value: true, label: def.trueLabel ?? labels.yes },
@@ -2183,6 +2194,7 @@ function DataTablePagination({
2183
2194
  onQueryChange
2184
2195
  }) {
2185
2196
  const labels = resolveLabels(labelsProp);
2197
+ const sizes = pageSizeOptions === false ? [] : pageSizeOptions;
2186
2198
  const pages = pageCount(total, query.pageSize);
2187
2199
  const from = total === 0 ? 0 : (query.page - 1) * query.pageSize + 1;
2188
2200
  const to = total === void 0 ? from + rowsOnPage - 1 : Math.min(query.page * query.pageSize, total);
@@ -2199,7 +2211,7 @@ function DataTablePagination({
2199
2211
  total.toLocaleString(locale)
2200
2212
  ] }) : null
2201
2213
  ] }) }),
2202
- /* @__PURE__ */ jsxs("div", { className: "mz-dt__pagination-size", children: [
2214
+ sizes.length > 1 ? /* @__PURE__ */ jsxs("div", { className: "mz-dt__pagination-size", children: [
2203
2215
  /* @__PURE__ */ jsx("span", { children: labels.rowsPerPage }),
2204
2216
  /* @__PURE__ */ jsxs(
2205
2217
  Select,
@@ -2208,11 +2220,11 @@ function DataTablePagination({
2208
2220
  onValueChange: (value) => onQueryChange({ ...query, pageSize: Number(value), page: 1 }),
2209
2221
  children: [
2210
2222
  /* @__PURE__ */ jsx(SelectTrigger, { size: "sm", className: "mz-dt__pagination-select", children: /* @__PURE__ */ jsx(SelectValue, {}) }),
2211
- /* @__PURE__ */ jsx(SelectContent, { children: pageSizeOptions.map((size) => /* @__PURE__ */ jsx(SelectItem, { value: String(size), children: size }, size)) })
2223
+ /* @__PURE__ */ jsx(SelectContent, { children: sizes.map((size) => /* @__PURE__ */ jsx(SelectItem, { value: String(size), children: size }, size)) })
2212
2224
  ]
2213
2225
  }
2214
2226
  )
2215
- ] }),
2227
+ ] }) : null,
2216
2228
  /* @__PURE__ */ jsxs("div", { className: "mz-dt__pagination-nav", children: [
2217
2229
  /* @__PURE__ */ jsxs("span", { className: "mz-dt__pagination-page", children: [
2218
2230
  query.page,
@@ -2386,20 +2398,27 @@ function useColumnLayout({
2386
2398
  () => controlled ? merge(baseline, controlled, baseline) : internal,
2387
2399
  [controlled, internal, baseline]
2388
2400
  );
2401
+ const onLayoutChangeRef = React39.useRef(onLayoutChange);
2402
+ React39.useEffect(() => {
2403
+ onLayoutChangeRef.current = onLayoutChange;
2404
+ });
2405
+ const internalRef = React39.useRef(internal);
2406
+ React39.useEffect(() => {
2407
+ internalRef.current = internal;
2408
+ }, [internal]);
2389
2409
  const update = React39.useCallback(
2390
2410
  (patch, options) => {
2391
- setInternal((current) => {
2392
- const base = controlled ? merge(baseline, controlled, baseline) : current;
2393
- const next = patch(base);
2394
- if (next === base) return current;
2395
- if (options?.persist !== false) {
2396
- writeStored2(persistKey, next);
2397
- onLayoutChange?.(next);
2398
- }
2399
- return next;
2400
- });
2411
+ const base = controlled ? merge(baseline, controlled, baseline) : internalRef.current;
2412
+ const next = patch(base);
2413
+ if (next === base) return;
2414
+ internalRef.current = next;
2415
+ setInternal(next);
2416
+ if (options?.persist !== false) {
2417
+ writeStored2(persistKey, next);
2418
+ onLayoutChangeRef.current?.(next);
2419
+ }
2401
2420
  },
2402
- [controlled, baseline, persistKey, onLayoutChange]
2421
+ [controlled, baseline, persistKey]
2403
2422
  );
2404
2423
  const byId = React39.useMemo(
2405
2424
  () => new Map(columns.map((c) => [c.id, c])),
@@ -2583,6 +2602,7 @@ function useRowSelection(options) {
2583
2602
  }
2584
2603
  var SELECT_WIDTH = 44;
2585
2604
  var EXPAND_WIDTH = 40;
2605
+ var FIT_CYCLE_MS = 250;
2586
2606
  function DataTable({
2587
2607
  columns,
2588
2608
  data,
@@ -2598,6 +2618,8 @@ function DataTable({
2598
2618
  bulkActions,
2599
2619
  renderExpanded,
2600
2620
  onRowClick,
2621
+ rowClassName,
2622
+ rowProps,
2601
2623
  layout: controlledLayout,
2602
2624
  onLayoutChange,
2603
2625
  persistKey = null,
@@ -2607,6 +2629,10 @@ function DataTable({
2607
2629
  stickyHeader = true,
2608
2630
  emptyState,
2609
2631
  pageSizeOptions,
2632
+ pagination,
2633
+ onLoadMore,
2634
+ hasMore,
2635
+ autoLoadMore = true,
2610
2636
  labels: labelsProp,
2611
2637
  locale,
2612
2638
  caption,
@@ -2640,28 +2666,38 @@ function DataTable({
2640
2666
  }),
2641
2667
  pageKeys
2642
2668
  });
2643
- const lastFitWidth = React39.useRef(0);
2669
+ const recentFits = React39.useRef([]);
2670
+ const hiddenKey = layout.hidden.join("\0");
2671
+ const orderKey = layout.order.join("\0");
2644
2672
  React39.useEffect(() => {
2645
2673
  const scroller = scrollerRef.current;
2646
2674
  if (!scroller || !autoFit) return;
2647
2675
  const controls = (selectable ? SELECT_WIDTH : 0) + (renderExpanded ? EXPAND_WIDTH : 0);
2676
+ recentFits.current = [];
2648
2677
  const run = () => {
2649
2678
  const available = scroller.clientWidth - controls - 2;
2650
- if (Math.abs(available - lastFitWidth.current) < 1) return;
2651
- lastFitWidth.current = available;
2679
+ const now = Date.now();
2680
+ const recent = recentFits.current.filter((entry) => now - entry.at < FIT_CYCLE_MS);
2681
+ recentFits.current = recent;
2682
+ if (recent.some((entry) => Math.abs(available - entry.width) < 1)) return;
2683
+ recent.push({ width: available, at: now });
2652
2684
  fitTo(available);
2653
2685
  };
2654
2686
  run();
2655
2687
  const observer = new ResizeObserver(run);
2656
2688
  observer.observe(scroller);
2657
2689
  return () => observer.disconnect();
2658
- }, [autoFit, fitTo, selectable, renderExpanded, layout.hidden, layout.order]);
2690
+ }, [autoFit, fitTo, selectable, renderExpanded, hiddenKey, orderKey]);
2659
2691
  React39.useEffect(() => {
2660
2692
  const scroller = scrollerRef.current;
2661
2693
  if (!scroller) return;
2662
2694
  const update = () => {
2663
2695
  const max = scroller.scrollWidth - scroller.clientWidth;
2664
- setScrolled({ left: scroller.scrollLeft > 1, right: scroller.scrollLeft < max - 1 });
2696
+ const left = scroller.scrollLeft > 1;
2697
+ const right = scroller.scrollLeft < max - 1;
2698
+ setScrolled(
2699
+ (current) => current.left === left && current.right === right ? current : { left, right }
2700
+ );
2665
2701
  };
2666
2702
  update();
2667
2703
  scroller.addEventListener("scroll", update, { passive: true });
@@ -2672,6 +2708,34 @@ function DataTable({
2672
2708
  observer.disconnect();
2673
2709
  };
2674
2710
  }, [visible.length, data.length]);
2711
+ const moreRef = React39.useRef(null);
2712
+ const onLoadMoreRef = React39.useRef(onLoadMore);
2713
+ React39.useEffect(() => {
2714
+ onLoadMoreRef.current = onLoadMore;
2715
+ });
2716
+ const askedAt = React39.useRef(-1);
2717
+ const canLoadMore = Boolean(onLoadMore) && (hasMore ?? (total === void 0 || data.length < total));
2718
+ const showLoadMore = canLoadMore && !error && data.length > 0;
2719
+ const showPager = pagination ?? !onLoadMore;
2720
+ const askForMore = () => {
2721
+ if (askedAt.current === data.length) return;
2722
+ askedAt.current = data.length;
2723
+ onLoadMoreRef.current?.();
2724
+ };
2725
+ React39.useEffect(() => {
2726
+ const sentinel = moreRef.current;
2727
+ const scroller = scrollerRef.current;
2728
+ if (!sentinel || !scroller || !showLoadMore || !autoLoadMore || loading) return;
2729
+ if (typeof IntersectionObserver === "undefined") return;
2730
+ const observer = new IntersectionObserver(
2731
+ (entries) => {
2732
+ if (entries.some((entry) => entry.isIntersecting)) askForMore();
2733
+ },
2734
+ { root: scroller, rootMargin: "120px" }
2735
+ );
2736
+ observer.observe(sentinel);
2737
+ return () => observer.disconnect();
2738
+ }, [showLoadMore, autoLoadMore, loading, data.length]);
2675
2739
  const offsets = React39.useMemo(() => {
2676
2740
  const result = /* @__PURE__ */ new Map();
2677
2741
  let leftParts = selectable ? [`${SELECT_WIDTH}px`] : [];
@@ -2923,14 +2987,21 @@ function DataTable({
2923
2987
  /* @__PURE__ */ jsx("tbody", { children: loading && data.length === 0 ? Array.from({ length: Math.min(query.pageSize, 8) }, (_, index) => /* @__PURE__ */ jsx("tr", { className: "mz-dt__row", children: /* @__PURE__ */ jsx("td", { className: "mz-dt__td", colSpan, children: /* @__PURE__ */ jsx(Skeleton, { style: { height: 14, width: `${60 + index * 13 % 35}%` } }) }) }, `skeleton-${index}`)) : data.map((row, rowIndex) => {
2924
2988
  const key = rowKey(row);
2925
2989
  const isOpen = expanded.has(key);
2990
+ const context = { rowIndex };
2991
+ const hostClass = rowClassName?.(row, context);
2992
+ const hostProps = rowProps?.(row, context);
2926
2993
  return /* @__PURE__ */ jsxs(React39.Fragment, { children: [
2927
2994
  /* @__PURE__ */ jsxs(
2928
2995
  "tr",
2929
2996
  {
2930
- className: "mz-dt__row",
2997
+ ...hostProps,
2998
+ className: cn("mz-dt__row", hostClass, hostProps?.className),
2931
2999
  "data-selected": rows.selected.has(key) || rows.allMatching || void 0,
2932
3000
  "data-clickable": onRowClick ? true : void 0,
2933
- onClick: onRowClick ? () => onRowClick(row) : void 0,
3001
+ onClick: onRowClick || hostProps?.onClick ? (event) => {
3002
+ hostProps?.onClick?.(event);
3003
+ onRowClick?.(row);
3004
+ } : void 0,
2934
3005
  children: [
2935
3006
  selectable ? /* @__PURE__ */ jsx(
2936
3007
  "td",
@@ -2979,7 +3050,11 @@ function DataTable({
2979
3050
  ]
2980
3051
  }
2981
3052
  ),
2982
- isOpen && renderExpanded ? /* @__PURE__ */ jsx("tr", { className: "mz-dt__row mz-dt__row--expanded", children: /* @__PURE__ */ jsx("td", { className: "mz-dt__td mz-dt__td--expanded", colSpan, children: renderExpanded(row) }) }) : null
3053
+ isOpen && renderExpanded ? (
3054
+ // The detail panel belongs to the same record, so a row
3055
+ // tint carries onto it; the attributes do not.
3056
+ /* @__PURE__ */ jsx("tr", { className: cn("mz-dt__row mz-dt__row--expanded", hostClass), children: /* @__PURE__ */ jsx("td", { className: "mz-dt__td mz-dt__td--expanded", colSpan, children: renderExpanded(row) }) })
3057
+ ) : null
2983
3058
  ] }, key);
2984
3059
  }) })
2985
3060
  ] }),
@@ -3001,11 +3076,12 @@ function DataTable({
3001
3076
  /* @__PURE__ */ jsx("p", { children: error }),
3002
3077
  onRetry ? /* @__PURE__ */ jsx(Button, { size: "sm", variant: "secondary", onClick: onRetry, children: labels.retry }) : null
3003
3078
  ] }) : null,
3079
+ showLoadMore ? /* @__PURE__ */ jsx("div", { ref: moreRef, className: "mz-dt__more", "data-slot": "data-table-load-more", children: /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "sm", disabled: loading, onClick: askForMore, children: loading ? labels.loadingMore : labels.loadMore }) }) : null,
3004
3080
  loading && data.length > 0 ? /* @__PURE__ */ jsx("div", { className: "mz-dt__loading-bar", "aria-hidden": "true" }) : null
3005
3081
  ]
3006
3082
  }
3007
3083
  ),
3008
- /* @__PURE__ */ jsx(
3084
+ showPager ? /* @__PURE__ */ jsx(
3009
3085
  DataTablePagination,
3010
3086
  {
3011
3087
  query,
@@ -3016,7 +3092,7 @@ function DataTable({
3016
3092
  locale,
3017
3093
  onQueryChange
3018
3094
  }
3019
- ),
3095
+ ) : null,
3020
3096
  selectable && (rows.count ?? 0) > 0 ? /* @__PURE__ */ jsxs("div", { className: "mz-dt__bulkbar", role: "region", "aria-label": labels.bulkActions, children: [
3021
3097
  /* @__PURE__ */ jsxs("span", { className: "mz-dt__bulkbar-count", children: [
3022
3098
  labels.selectedCount(
@@ -3051,6 +3127,10 @@ function describeFilter(value, def, labels) {
3051
3127
  return `: ${value.from ?? "\u2026"} \u2013 ${value.to ?? "\u2026"}`;
3052
3128
  case "boolean":
3053
3129
  return `: ${value.value ? labels.yes.toLowerCase() : labels.no.toLowerCase()}`;
3130
+ case "custom": {
3131
+ const text = (def?.type === "custom" ? def.describe?.(value.value) : void 0) ?? value.label;
3132
+ return text ? `: ${text}` : "";
3133
+ }
3054
3134
  }
3055
3135
  }
3056
3136