@apliteni/apliteni-ui 0.56.0 → 0.57.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apliteni/apliteni-ui",
3
- "version": "0.56.0",
3
+ "version": "0.57.0",
4
4
  "workspaces": [
5
5
  "react"
6
6
  ],
package/react/README.md CHANGED
@@ -28,7 +28,7 @@ import '@apliteni/apliteni-ui/react/css'; // React components' shell styles (mo
28
28
  import { DataTable, Modal, Button } from '@apliteni/apliteni-ui/react';
29
29
  ```
30
30
 
31
- Components: `EmptyState`, `DataTable`, `Pagination`, `StatBand`, `Modal`, `Confirm`, `Drawer`, `CommandPalette`, `Dropdown`, `BackLink`, `Snippet`, `Tabs`, `Button`, `Badge`, `Card`, `Callout`, `Icon`.
31
+ Components: `EmptyState`, `DataTable`, `Pagination`, `StatBand`, `Modal`, `Confirm`, `Drawer`, `CommandPalette`, `Dropdown`, `BackLink`, `Tooltip`, `Snippet`, `Tabs`, `Button`, `Badge`, `Card`, `Callout`, `Icon`.
32
32
 
33
33
  `Snippet` accepts `label`, `code`, `reveal`, and `copyLabel` props. It treats `code`
34
34
  as plain text and copies it exactly as provided. After a successful clipboard write,
@@ -501,3 +501,11 @@ AppShell includes the kit’s rail and top band. Pass the current router pathnam
501
501
  <Reports />
502
502
  </AppShell>
503
503
  ```
504
+
505
+ ## Tooltip
506
+
507
+ Tooltip accepts text and inline children, without nested controls. Hovering or focusing opens it; pressing Escape, moving focus away, or moving the mouse away closes it. On touch devices, tapping toggles it. It uses the kit’s tooltip styles and moves below the trigger when there is not enough space above. Keep the same information elsewhere on the page.
508
+
509
+ ```tsx
510
+ <Tooltip text="Updated daily">Balance</Tooltip>
511
+ ```
@@ -68,6 +68,8 @@
68
68
  border-bottom: 1px solid var(--border);
69
69
  }
70
70
  .rx-modal__title {
71
+ margin: 0;
72
+ font-family: var(--font-sans);
71
73
  font-weight: var(--weight-normal);
72
74
  font-size: var(--text-base);
73
75
  color: var(--text);
@@ -668,3 +670,54 @@
668
670
  align-items: center;
669
671
  gap: var(--space-3);
670
672
  }
673
+
674
+ /* ../src/styles/tooltip.css */
675
+ .ui-tip-host {
676
+ position: relative;
677
+ }
678
+ .ui-tip {
679
+ --ui-tip-gap: 8px;
680
+ --ui-tip-bg: var(--bg-elevated);
681
+ position: absolute;
682
+ top: var(--ui-tip-y, 0px);
683
+ left: var(--ui-tip-x, 50%);
684
+ z-index: var(--z-dropdown);
685
+ translate: calc(-50% + var(--ui-tip-shift, 0px)) calc(-100% - var(--ui-tip-gap));
686
+ display: flex;
687
+ flex-direction: column;
688
+ width: max-content;
689
+ max-width: var(--prose-caption);
690
+ padding: var(--space-2) var(--space-3);
691
+ background: var(--ui-tip-bg);
692
+ border: 1px solid var(--border-strong);
693
+ box-shadow: inset 0 0 0 1px var(--elev-edge, var(--border)), var(--elev-drop);
694
+ border-radius: var(--radius-sm);
695
+ font-family: var(--font-sans);
696
+ font-size: var(--text-xs);
697
+ line-height: 1.45;
698
+ color: var(--text);
699
+ pointer-events: none;
700
+ opacity: 0;
701
+ visibility: hidden;
702
+ transition: opacity var(--dur-fast) var(--ease), visibility var(--dur-fast) linear;
703
+ }
704
+ .ui-tip.is-below {
705
+ translate: calc(-50% + var(--ui-tip-shift, 0px)) var(--ui-tip-gap);
706
+ }
707
+ .ui-tip.is-open {
708
+ opacity: 1;
709
+ visibility: visible;
710
+ }
711
+ .ui-tip__label {
712
+ color: var(--text);
713
+ }
714
+ .ui-tip__value {
715
+ font-size: var(--text-sm);
716
+ font-weight: var(--weight-semibold);
717
+ color: var(--strong);
718
+ font-variant-numeric: tabular-nums;
719
+ }
720
+ .ui-tip__detail {
721
+ color: var(--text);
722
+ font-variant-numeric: tabular-nums;
723
+ }
@@ -14,6 +14,8 @@ type ButtonProps = {
14
14
  block?: boolean;
15
15
  /** In flight: keeps focus, blocks activation, announces progress, and shows dots. */
16
16
  busy?: boolean;
17
+ /** Message after busy ends; an empty string leaves outcome announcements to the caller. */
18
+ completionMessage?: string;
17
19
  children?: ReactNode;
18
20
  } & ButtonHTMLAttributes<HTMLButtonElement>;
19
21
  declare const Button: react.ForwardRefExoticComponent<{
@@ -25,6 +27,8 @@ declare const Button: react.ForwardRefExoticComponent<{
25
27
  block?: boolean;
26
28
  /** In flight: keeps focus, blocks activation, announces progress, and shows dots. */
27
29
  busy?: boolean;
30
+ /** Message after busy ends; an empty string leaves outcome announcements to the caller. */
31
+ completionMessage?: string;
28
32
  children?: ReactNode;
29
33
  } & ButtonHTMLAttributes<HTMLButtonElement> & react.RefAttributes<HTMLButtonElement>>;
30
34
 
@@ -608,7 +612,14 @@ type ThemeToggleProps = {
608
612
  };
609
613
  declare function ThemeToggle({ labelled }: ThemeToggleProps): react.JSX.Element;
610
614
 
615
+ type TooltipProps = {
616
+ text: string;
617
+ /** Inline content without nested controls. */
618
+ children: ReactNode;
619
+ };
620
+ declare function Tooltip({ text, children }: TooltipProps): react.JSX.Element;
621
+
611
622
  declare const PAGE_SIZES: readonly number[];
612
623
  declare const DEFAULT_PAGE_SIZE: number;
613
624
 
614
- export { AppShell, type AppShellProps, type AppShellSection, BackLink, type BackLinkOwnProps, type BackLinkProps, Badge, BusyRegion, type BusyRegionProps, Button, type ButtonProps, Callout, type CalloutProps, type CalloutVariant, Card, type Column, type CommandGroup, type CommandItem, CommandPalette, type CommandPaletteProps, Confirm, type ConfirmProps, DEFAULT_PAGE_SIZE, DataTable, type DataTableProps, DeltaValue, type DeltaValueProps, Denied, type DeniedProps, Drawer, type DrawerProps, DrawerSection, type DrawerSectionProps, Dropdown, type DropdownBadge, type DropdownEntry, type DropdownItem, type DropdownProps, type DropdownRowProps, type DropdownSearch, type DropdownSection, type DropdownSeparator, EmptyState, type EmptyStateProps, type EmptyStateVariant, FileField, type FileFieldProps, type Filter, FilterBar, type FilterBarProps, Icon, KeyValueList, type KeyValueListProps, type KeyValueRow, Modal, type ModalProps, NumericValue, type NumericValueProps, PAGE_SIZES, Pagination, type PaginationProps, RowIdentity, type RowIdentityProps, type SegmentOption, Segmented, type SegmentedProps, SelectField, type SelectFieldProps, Skeleton, type SkeletonProps, SkeletonTable, type SkeletonTableProps, Snippet, type SnippetProps, StatBand, type StatBandProps, type StatDelta, type StatFigure, type StatTone, type StatVariant, THEME_INIT_SCRIPT, type TabItem, type TableSort, Tabs, type TabsProps, TextArea, type TextAreaProps, TextField, type TextFieldProps, ThemeToggle, type ThemeToggleProps, Timeline, type TimelineEvent, type TimelineEventKind, type TimelineProps, Toast, type ToastNotice, type ToastProps, sortTableRows, useToast };
625
+ export { AppShell, type AppShellProps, type AppShellSection, BackLink, type BackLinkOwnProps, type BackLinkProps, Badge, BusyRegion, type BusyRegionProps, Button, type ButtonProps, Callout, type CalloutProps, type CalloutVariant, Card, type Column, type CommandGroup, type CommandItem, CommandPalette, type CommandPaletteProps, Confirm, type ConfirmProps, DEFAULT_PAGE_SIZE, DataTable, type DataTableProps, DeltaValue, type DeltaValueProps, Denied, type DeniedProps, Drawer, type DrawerProps, DrawerSection, type DrawerSectionProps, Dropdown, type DropdownBadge, type DropdownEntry, type DropdownItem, type DropdownProps, type DropdownRowProps, type DropdownSearch, type DropdownSection, type DropdownSeparator, EmptyState, type EmptyStateProps, type EmptyStateVariant, FileField, type FileFieldProps, type Filter, FilterBar, type FilterBarProps, Icon, KeyValueList, type KeyValueListProps, type KeyValueRow, Modal, type ModalProps, NumericValue, type NumericValueProps, PAGE_SIZES, Pagination, type PaginationProps, RowIdentity, type RowIdentityProps, type SegmentOption, Segmented, type SegmentedProps, SelectField, type SelectFieldProps, Skeleton, type SkeletonProps, SkeletonTable, type SkeletonTableProps, Snippet, type SnippetProps, StatBand, type StatBandProps, type StatDelta, type StatFigure, type StatTone, type StatVariant, THEME_INIT_SCRIPT, type TabItem, type TableSort, Tabs, type TabsProps, TextArea, type TextAreaProps, TextField, type TextFieldProps, ThemeToggle, type ThemeToggleProps, Timeline, type TimelineEvent, type TimelineEventKind, type TimelineProps, Toast, type ToastNotice, type ToastProps, Tooltip, type TooltipProps, sortTableRows, useToast };
@@ -56,6 +56,7 @@ var Button = forwardRef(function Button2({
56
56
  iconOnly,
57
57
  block,
58
58
  busy,
59
+ completionMessage,
59
60
  children,
60
61
  type = "button",
61
62
  disabled,
@@ -83,7 +84,7 @@ var Button = forwardRef(function Button2({
83
84
  const prior = previous.current;
84
85
  if ((busy || prior?.busy) && (!prior || prior.text !== text2 || prior.busy !== busy)) {
85
86
  announcer.current ?? (announcer.current = acquireAnnouncer(buttonRef.current.ownerDocument));
86
- announcer.current.announce(`${text2}: ${busy ? "in progress" : "complete"}`);
87
+ announcer.current.announce(busy ? `${text2}: in progress` : completionMessage ?? `${text2}: complete`);
87
88
  }
88
89
  previous.current = { text: text2, busy };
89
90
  });
@@ -472,7 +473,7 @@ function Modal({ open, title, onClose, footer, children, initialFocusRef, role =
472
473
  ref: panel,
473
474
  children: [
474
475
  /* @__PURE__ */ jsxs5("div", { className: "rx-modal__head", children: [
475
- /* @__PURE__ */ jsx7("div", { className: "rx-modal__title", id: titleId, children: title }),
476
+ /* @__PURE__ */ jsx7("h2", { className: "rx-modal__title", id: titleId, children: title }),
476
477
  /* @__PURE__ */ jsx7(Button, { variant: "ghost", size: "sm", iconOnly: true, icon: "x", "aria-label": "Close", onClick: onClose })
477
478
  ] }),
478
479
  /* @__PURE__ */ jsx7("div", { className: "rx-modal__body", id: bodyId, ref: body, children }),
@@ -1943,6 +1944,9 @@ function Timeline({ events, className, ...rest }) {
1943
1944
  import { useEffect as useEffect8, useId as useId8, useRef as useRef13, useState as useState7 } from "react";
1944
1945
  import { esc, icon as icon4 } from "@apliteni/apliteni-ui";
1945
1946
  import { jsx as jsx23, jsxs as jsxs19 } from "react/jsx-runtime";
1947
+ function messages(id, hint, error) {
1948
+ return [error && `${id}-error`, hint && `${id}-hint`].filter(Boolean).join(" ") || void 0;
1949
+ }
1946
1950
  function Frame({ id, label, hint, error, required, children }) {
1947
1951
  return /* @__PURE__ */ jsxs19("div", { className: "ui-field", children: [
1948
1952
  /* @__PURE__ */ jsxs19("label", { className: "ui-field__label", htmlFor: id, children: [
@@ -1950,23 +1954,25 @@ function Frame({ id, label, hint, error, required, children }) {
1950
1954
  required && /* @__PURE__ */ jsx23("span", { className: "ui-field__req", "aria-hidden": "true", children: "*" })
1951
1955
  ] }),
1952
1956
  children,
1953
- (error || hint) && /* @__PURE__ */ jsx23(
1957
+ error && /* @__PURE__ */ jsx23(
1954
1958
  "div",
1955
1959
  {
1956
- id: `${id}-message`,
1957
- className: error ? "ui-field__error" : "ui-field__hint",
1958
- dangerouslySetInnerHTML: { __html: (error ? icon4("alert") : "") + esc(error || hint) }
1960
+ id: `${id}-error`,
1961
+ className: "ui-field__error",
1962
+ role: "alert",
1963
+ dangerouslySetInnerHTML: { __html: icon4("alert") + esc(error) }
1959
1964
  }
1960
- )
1965
+ ),
1966
+ hint && /* @__PURE__ */ jsx23("div", { id: `${id}-hint`, className: "ui-field__hint", children: hint })
1961
1967
  ] });
1962
1968
  }
1963
1969
  function wiring(id, hint, error) {
1964
- return { id, "aria-describedby": error || hint ? `${id}-message` : void 0, "aria-invalid": error ? true : void 0 };
1970
+ return { id, "aria-describedby": messages(id, hint, error), "aria-invalid": error ? true : void 0 };
1965
1971
  }
1966
1972
  function TextField({ label, hint, error, required, type = "text", unit, className = "", ...props }) {
1967
1973
  const id = useId8();
1968
1974
  const hasUnit = type === "number" && !!unit;
1969
- const describedBy = [error || hint ? `${id}-message` : "", hasUnit ? `${id}-unit` : ""].filter(Boolean).join(" ") || void 0;
1975
+ const describedBy = [messages(id, hint, error), hasUnit ? `${id}-unit` : ""].filter(Boolean).join(" ") || void 0;
1970
1976
  const control = /* @__PURE__ */ jsx23(
1971
1977
  "input",
1972
1978
  {
@@ -2074,7 +2080,7 @@ function FileField({ label, hint, error, required, disabled, onFileChange, class
2074
2080
  }
2075
2081
 
2076
2082
  // src/Toast.tsx
2077
- import { createContext as createContext2, useCallback as useCallback3, useContext as useContext2, useEffect as useEffect9, useRef as useRef14, useState as useState8 } from "react";
2083
+ import { createContext as createContext2, useCallback as useCallback3, useContext as useContext2, useEffect as useEffect9, useLayoutEffect as useLayoutEffect4, useRef as useRef14, useState as useState8 } from "react";
2078
2084
  import { createPortal as createPortal4 } from "react-dom";
2079
2085
  import { jsx as jsx24, jsxs as jsxs20 } from "react/jsx-runtime";
2080
2086
  var Context = createContext2(null);
@@ -2169,18 +2175,40 @@ function Notice({ notice, remove }) {
2169
2175
  }
2170
2176
  );
2171
2177
  }
2178
+ var REACH = "--rx-toast-stack";
2179
+ function usePublishedReach(stack2, count) {
2180
+ useLayoutEffect4(() => {
2181
+ const element = stack2.current;
2182
+ const view = element?.ownerDocument.defaultView;
2183
+ const root = element?.ownerDocument.documentElement;
2184
+ if (!element || !view || !root) return;
2185
+ const publish = () => {
2186
+ const box = element.getBoundingClientRect();
2187
+ root.style.setProperty(REACH, `${box.height ? Math.round(view.innerHeight - box.top) : 0}px`);
2188
+ };
2189
+ publish();
2190
+ const observer = typeof view.ResizeObserver === "function" ? new view.ResizeObserver(publish) : void 0;
2191
+ observer?.observe(element);
2192
+ return () => {
2193
+ observer?.disconnect();
2194
+ root.style.removeProperty(REACH);
2195
+ };
2196
+ }, [stack2, count]);
2197
+ }
2172
2198
  function Toast({ children }) {
2173
2199
  const [entries, setEntries] = useState8([]);
2174
2200
  const nextId = useRef14(0);
2201
+ const stack2 = useRef14(null);
2175
2202
  const push = useCallback3((notice) => {
2176
2203
  const id = nextId.current++;
2177
2204
  const remove = () => setEntries((current) => current.filter((entry) => entry.id !== id));
2178
2205
  setEntries((current) => [...current, { id, notice, remove }]);
2179
2206
  }, []);
2207
+ usePublishedReach(stack2, entries.length);
2180
2208
  return /* @__PURE__ */ jsxs20(Context.Provider, { value: push, children: [
2181
2209
  children,
2182
2210
  typeof document !== "undefined" && createPortal4(
2183
- /* @__PURE__ */ jsx24("div", { className: "ui-toast-stack rx-toast-stack", children: entries.map((entry) => /* @__PURE__ */ jsx24(Notice, { notice: entry.notice, remove: entry.remove }, entry.id)) }),
2211
+ /* @__PURE__ */ jsx24("div", { className: "ui-toast-stack rx-toast-stack", ref: stack2, children: entries.map((entry) => /* @__PURE__ */ jsx24(Notice, { notice: entry.notice, remove: entry.remove }, entry.id)) }),
2184
2212
  document.body
2185
2213
  )
2186
2214
  ] });
@@ -2439,6 +2467,106 @@ function ThemeToggle({ labelled = false }) {
2439
2467
  ] }) : button;
2440
2468
  }
2441
2469
 
2470
+ // src/Tooltip.tsx
2471
+ import { useEffect as useEffect11, useId as useId10, useLayoutEffect as useLayoutEffect5, useRef as useRef15, useState as useState10 } from "react";
2472
+ import { jsx as jsx28, jsxs as jsxs24 } from "react/jsx-runtime";
2473
+ function Tooltip({ text: text2, children }) {
2474
+ const id = useId10();
2475
+ const host = useRef15(null);
2476
+ const trigger = useRef15(null);
2477
+ const tip = useRef15(null);
2478
+ const ignoreMouseUntil = useRef15(0);
2479
+ const touchMoved = useRef15(false);
2480
+ const [open, setOpen] = useState10(false);
2481
+ function place() {
2482
+ const h = host.current;
2483
+ const t = tip.current;
2484
+ const mark = trigger.current.getBoundingClientRect();
2485
+ const rect = h.getBoundingClientRect();
2486
+ const view = h.ownerDocument.documentElement;
2487
+ const clip = { top: 0, left: 0, right: view.clientWidth, bottom: view.clientHeight };
2488
+ for (let el = h; el && el !== h.ownerDocument.body; el = el.parentElement) {
2489
+ const style = getComputedStyle(el);
2490
+ if (![style.overflow, style.overflowX, style.overflowY].some((v) => v && v !== "visible")) continue;
2491
+ const bounds = el.getBoundingClientRect();
2492
+ clip.top = Math.max(clip.top, bounds.top);
2493
+ clip.left = Math.max(clip.left, bounds.left);
2494
+ clip.right = Math.min(clip.right, bounds.right);
2495
+ clip.bottom = Math.min(clip.bottom, bounds.bottom);
2496
+ }
2497
+ const gap = parseFloat(getComputedStyle(t).getPropertyValue("--ui-tip-gap")) || 8;
2498
+ const above = mark.top - clip.top;
2499
+ const below = clip.bottom - mark.bottom;
2500
+ const isBelow = above < t.offsetHeight + gap && below > above;
2501
+ t.classList.toggle("is-below", isBelow);
2502
+ const centre = mark.left + mark.width / 2;
2503
+ const ideal = centre - t.offsetWidth / 2;
2504
+ const left = Math.max(clip.left, Math.min(ideal, clip.right - t.offsetWidth));
2505
+ t.style.setProperty("--ui-tip-x", `${centre - rect.left - h.clientLeft + h.scrollLeft}px`);
2506
+ t.style.setProperty("--ui-tip-y", `${(isBelow ? mark.bottom : mark.top) - rect.top - h.clientTop + h.scrollTop}px`);
2507
+ t.style.setProperty("--ui-tip-shift", `${left - ideal}px`);
2508
+ }
2509
+ useLayoutEffect5(() => {
2510
+ if (open) place();
2511
+ }, [open, text2]);
2512
+ useEffect11(() => {
2513
+ if (!open) return;
2514
+ const doc = host.current.ownerDocument;
2515
+ const view = doc.defaultView;
2516
+ const escape = (event) => {
2517
+ if (event.key === "Escape") setOpen(false);
2518
+ };
2519
+ doc.addEventListener("keydown", escape);
2520
+ view.addEventListener("scroll", place, true);
2521
+ view.addEventListener("resize", place);
2522
+ return () => {
2523
+ doc.removeEventListener("keydown", escape);
2524
+ view.removeEventListener("scroll", place, true);
2525
+ view.removeEventListener("resize", place);
2526
+ };
2527
+ }, [open]);
2528
+ return /* @__PURE__ */ jsxs24("span", { ref: host, className: "ui-tip-host", style: { display: "inline-block" }, children: [
2529
+ /* @__PURE__ */ jsx28(
2530
+ "span",
2531
+ {
2532
+ ref: trigger,
2533
+ className: "ui-focusable",
2534
+ tabIndex: 0,
2535
+ "aria-describedby": id,
2536
+ onMouseEnter: () => {
2537
+ if (Date.now() > ignoreMouseUntil.current) setOpen(true);
2538
+ },
2539
+ onMouseLeave: () => {
2540
+ if (Date.now() > ignoreMouseUntil.current) setOpen(false);
2541
+ },
2542
+ onFocus: () => {
2543
+ if (Date.now() > ignoreMouseUntil.current) setOpen(true);
2544
+ },
2545
+ onBlur: () => setOpen(false),
2546
+ onKeyDown: (event) => {
2547
+ if (event.key === "Escape") setOpen(false);
2548
+ },
2549
+ onTouchStart: () => {
2550
+ ignoreMouseUntil.current = Date.now() + 800;
2551
+ touchMoved.current = false;
2552
+ },
2553
+ onTouchMove: () => {
2554
+ touchMoved.current = true;
2555
+ },
2556
+ onTouchCancel: () => {
2557
+ touchMoved.current = true;
2558
+ },
2559
+ onTouchEnd: () => {
2560
+ ignoreMouseUntil.current = Date.now() + 800;
2561
+ if (!touchMoved.current) setOpen((value) => !value);
2562
+ },
2563
+ children
2564
+ }
2565
+ ),
2566
+ /* @__PURE__ */ jsx28("span", { ref: tip, id, role: "tooltip", className: `ui-tip${open ? " is-open" : ""}`, children: /* @__PURE__ */ jsx28("span", { className: "ui-tip__label", children: text2 }) })
2567
+ ] });
2568
+ }
2569
+
2442
2570
  // src/index.ts
2443
2571
  var PAGE_SIZES = KIT_PAGE_SIZES;
2444
2572
  var DEFAULT_PAGE_SIZE3 = KIT_DEFAULT_PAGE_SIZE;
@@ -2482,6 +2610,7 @@ export {
2482
2610
  ThemeToggle,
2483
2611
  Timeline,
2484
2612
  Toast,
2613
+ Tooltip,
2485
2614
  sortTableRows,
2486
2615
  useToast
2487
2616
  };