@stll/ui 0.55.1 → 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.
Files changed (71) hide show
  1. package/README.md +3 -0
  2. package/THIRD_PARTY_NOTICES +24 -0
  3. package/dist/components/badge.d.ts +28 -0
  4. package/dist/components/badge.js +64 -0
  5. package/dist/components/button.d.ts +2 -6
  6. package/dist/components/button.js +33 -25
  7. package/dist/components/button.native.d.ts +15 -0
  8. package/dist/components/button.native.js +165 -0
  9. package/dist/components/button.shared.d.ts +65 -0
  10. package/dist/components/button.shared.js +61 -0
  11. package/dist/components/dialog.d.ts +20 -2
  12. package/dist/components/dialog.js +31 -9
  13. package/dist/components/horizontal-scroller.js +31 -34
  14. package/dist/components/input.native.d.ts +23 -0
  15. package/dist/components/input.native.js +66 -0
  16. package/dist/components/input.shared.d.ts +19 -0
  17. package/dist/components/input.shared.js +0 -0
  18. package/dist/components/loader.d.ts +1 -6
  19. package/dist/components/loader.native.d.ts +28 -0
  20. package/dist/components/loader.native.js +23 -0
  21. package/dist/components/loader.shared.d.ts +17 -0
  22. package/dist/components/loader.shared.js +9 -0
  23. package/dist/components/matter-icon.d.ts +2 -8
  24. package/dist/components/matter-icon.js +10 -30
  25. package/dist/components/matter-icon.native.d.ts +25 -0
  26. package/dist/components/matter-icon.native.js +78 -0
  27. package/dist/components/matter-icon.shared.d.ts +21 -0
  28. package/dist/components/matter-icon.shared.js +18 -0
  29. package/dist/components/separator.native.d.ts +14 -0
  30. package/dist/components/separator.native.js +16 -0
  31. package/dist/components/separator.shared.d.ts +6 -0
  32. package/dist/components/separator.shared.js +0 -0
  33. package/dist/components/sheet.native.d.ts +27 -0
  34. package/dist/components/sheet.native.js +69 -0
  35. package/dist/components/sheet.shared.d.ts +9 -0
  36. package/dist/components/sheet.shared.js +0 -0
  37. package/dist/components/sidebar.js +1 -1
  38. package/dist/components/stella-wordmark.js +5 -13
  39. package/dist/components/stella-wordmark.native.d.ts +9 -0
  40. package/dist/components/stella-wordmark.native.js +27 -0
  41. package/dist/components/stella-wordmark.shared.d.ts +6 -0
  42. package/dist/components/stella-wordmark.shared.js +16 -0
  43. package/dist/components/switch.native.d.ts +13 -0
  44. package/dist/components/switch.native.js +33 -0
  45. package/dist/components/switch.shared.d.ts +9 -0
  46. package/dist/components/switch.shared.js +0 -0
  47. package/dist/components/table.d.ts +86 -8
  48. package/dist/components/table.js +46 -20
  49. package/dist/components/text.d.ts +11 -0
  50. package/dist/components/text.js +25 -0
  51. package/dist/components/text.native.d.ts +15 -0
  52. package/dist/components/text.native.js +46 -0
  53. package/dist/components/text.shared.d.ts +78 -0
  54. package/dist/components/text.shared.js +71 -0
  55. package/dist/components/textarea.native.d.ts +10 -0
  56. package/dist/components/textarea.native.js +28 -0
  57. package/dist/components/textarea.shared.d.ts +4 -0
  58. package/dist/components/textarea.shared.js +0 -0
  59. package/dist/components/tooltip.d.ts +15 -2
  60. package/dist/components/tooltip.js +18 -2
  61. package/dist/components/truncated-text.d.ts +33 -0
  62. package/dist/components/truncated-text.js +111 -0
  63. package/dist/index.d.ts +6 -5
  64. package/dist/index.js +5 -3
  65. package/dist/inspector/dock.js +1 -1
  66. package/dist/lib/scroll-edge-fade.d.ts +23 -0
  67. package/dist/lib/scroll-edge-fade.js +62 -0
  68. package/dist/native-env.d.ts +1 -0
  69. package/package.json +112 -7
  70. package/dist/calendar/index.d.ts +0 -4
  71. package/dist/calendar/index.js +0 -4
@@ -14,13 +14,26 @@ import { Dialog as Dialog$1 } from "@base-ui/react/dialog";
14
14
  const DialogCreateHandle = Dialog$1.createHandle;
15
15
  const DialogLabelsContext = createContext({
16
16
  close: "Close",
17
- unsavedChanges: "Unsaved changes. Press Esc again to discard"
17
+ unsavedChanges: "Unsaved changes. Press Esc again to discard",
18
+ unsavedChangesClose: "Unsaved changes. Close again to discard"
18
19
  });
19
20
  const DialogProvider = ({ labels, children }) => /* @__PURE__ */ jsx(DialogLabelsContext, {
20
21
  value: labels,
21
22
  children
22
23
  });
23
24
  const DialogDismissalContext = createContext("idle");
25
+ /**
26
+ * The close requests the guard holds back. A programmatic close
27
+ * (`imperative-action`, or a controlled `open` set to false after a save) is
28
+ * the owner's decision and passes straight through.
29
+ */
30
+ const GUARDED_CLOSE_REASONS = /* @__PURE__ */ new Set([
31
+ "close-press",
32
+ "escape-key",
33
+ "focus-out",
34
+ "outside-press",
35
+ "trigger-press"
36
+ ]);
24
37
  const Dialog = ({ dirty = false, onOpenChange, onOpenChangeComplete, ...props }) => {
25
38
  const [dismissal, setDismissal] = useState("idle");
26
39
  const pendingDiscard = useRef([]);
@@ -36,9 +49,11 @@ const Dialog = ({ dirty = false, onOpenChange, onOpenChangeComplete, ...props })
36
49
  ...props,
37
50
  onOpenChange: (open, details) => {
38
51
  const hasChanges = dirty || [...forms.sources.values()].some((source) => source.dirty);
39
- if (!open && details.reason === "escape-key" && hasChanges && (dismissal === "idle" || details.event.repeat)) {
52
+ const isEscape = details.reason === "escape-key";
53
+ const isRepeat = isEscape && details.event instanceof KeyboardEvent && details.event.repeat;
54
+ if (!open && hasChanges && GUARDED_CLOSE_REASONS.has(details.reason) && (dismissal === "idle" || isRepeat)) {
40
55
  details.cancel();
41
- setDismissal("confirm-discard");
56
+ setDismissal(isEscape ? "confirm-escape" : "confirm-close");
42
57
  return;
43
58
  }
44
59
  onOpenChange?.(open, details);
@@ -111,7 +126,13 @@ const DIALOG_MOBILE_POPUP_CLASS_NAMES = {
111
126
  centered: void 0,
112
127
  fullscreen: "max-sm:h-full max-sm:max-w-none max-sm:rounded-none max-sm:border-0 max-sm:ps-(--safe-area-inline-start) max-sm:pe-(--safe-area-inline-end) max-sm:pt-(--safe-area-top) max-sm:pb-(--safe-area-bottom) max-sm:opacity-[calc(1-min(var(--nested-dialogs),1))] max-sm:before:hidden"
113
128
  };
114
- const DialogPopup = ({ backdropClassName, className, children, showCloseButton = true, mobileLayout = DIALOG_MOBILE_LAYOUT.bottomSheet, layer = "default", viewportClassName, ...props }) => {
129
+ const DIALOG_SIZE_CLASS_NAMES = {
130
+ sm: "max-w-sm",
131
+ default: "max-w-lg",
132
+ lg: "max-w-2xl",
133
+ xl: "max-w-4xl"
134
+ };
135
+ const DialogPopup = ({ backdropClassName, className, children, showCloseButton = true, mobileLayout = DIALOG_MOBILE_LAYOUT.bottomSheet, layer = "default", size = "default", viewportClassName, ...props }) => {
115
136
  const dismissal = useContext(DialogDismissalContext);
116
137
  const labels = useContext(DialogLabelsContext);
117
138
  return /* @__PURE__ */ jsxs(DialogPortal, { children: [/* @__PURE__ */ jsx(DialogBackdrop, {
@@ -121,20 +142,21 @@ const DialogPopup = ({ backdropClassName, className, children, showCloseButton =
121
142
  className: cn(DIALOG_MOBILE_VIEWPORT_CLASS_NAMES[mobileLayout], viewportClassName),
122
143
  layer,
123
144
  children: /* @__PURE__ */ jsxs(Dialog$1.Popup, {
124
- className: cn("bg-popover text-popover-foreground relative row-start-2 flex max-h-full min-h-0 w-full max-w-lg min-w-0 -translate-y-[calc(1.25rem*var(--nested-dialogs))] scale-[calc(1-0.1*var(--nested-dialogs))] flex-col rounded-2xl border opacity-[calc(1-0.1*var(--nested-dialogs))] shadow-lg/5 transition-[scale,opacity,translate] duration-200 ease-out will-change-transform not-dark:bg-clip-padding before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-2xl)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] data-ending-style:scale-98 data-ending-style:opacity-0 data-ending-style:duration-150 data-nested:data-ending-style:translate-y-8 data-nested-dialog-open:origin-top data-starting-style:scale-98 data-starting-style:opacity-0 data-nested:data-starting-style:translate-y-8 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]", DIALOG_MOBILE_POPUP_CLASS_NAMES[mobileLayout], className),
145
+ className: cn("bg-popover text-popover-foreground relative row-start-2 flex max-h-full min-h-0 w-full min-w-0 -translate-y-[calc(1.25rem*var(--nested-dialogs))] scale-[calc(1-0.1*var(--nested-dialogs))] flex-col rounded-2xl border opacity-[calc(1-0.1*var(--nested-dialogs))] shadow-lg/5 transition-[scale,opacity,translate] duration-200 ease-out will-change-transform not-dark:bg-clip-padding before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-2xl)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] data-ending-style:scale-98 data-ending-style:opacity-0 data-ending-style:duration-150 data-nested:data-ending-style:translate-y-8 data-nested-dialog-open:origin-top data-starting-style:scale-98 data-starting-style:opacity-0 data-nested:data-starting-style:translate-y-8 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]", DIALOG_SIZE_CLASS_NAMES[size], DIALOG_MOBILE_POPUP_CLASS_NAMES[mobileLayout], className),
146
+ "data-size": size,
125
147
  "data-slot": "dialog-popup",
126
148
  ...props,
127
149
  children: [
128
- dismissal === "confirm-discard" && /* @__PURE__ */ jsx("div", {
150
+ dismissal !== "idle" && /* @__PURE__ */ jsx("div", {
129
151
  className: "flex shrink-0 justify-end ps-2 pe-12 pt-2",
130
152
  children: /* @__PURE__ */ jsx("span", {
131
153
  className: "text-muted-foreground max-w-64 px-2 py-1 text-xs",
132
154
  role: "status",
133
- children: labels.unsavedChanges
155
+ children: dismissal === "confirm-escape" ? labels.unsavedChanges : labels.unsavedChangesClose
134
156
  })
135
157
  }),
136
158
  children,
137
- (showCloseButton || dismissal === "confirm-discard") && /* @__PURE__ */ jsx(Dialog$1.Close, {
159
+ (showCloseButton || dismissal !== "idle") && /* @__PURE__ */ jsx(Dialog$1.Close, {
138
160
  "aria-label": labels.close,
139
161
  className: "absolute end-2 top-2",
140
162
  render: /* @__PURE__ */ jsx(Button, {
@@ -176,4 +198,4 @@ const DialogPanel = ({ className, scrollFade = true, ...props }) => /* @__PURE__
176
198
  })
177
199
  });
178
200
  //#endregion
179
- export { DIALOG_MOBILE_LAYOUT, Dialog, DialogBackdrop, DialogBackdrop as DialogOverlay, DialogClose, DialogPopup as DialogContent, DialogPopup, DialogCreateHandle, DialogDescription, DialogFooter, DialogFormState, DialogHeader, DialogPanel, DialogPortal, DialogProvider, DialogTitle, DialogTrigger, DialogViewport };
201
+ export { DIALOG_MOBILE_LAYOUT, DIALOG_SIZE_CLASS_NAMES, Dialog, DialogBackdrop, DialogBackdrop as DialogOverlay, DialogClose, DialogPopup as DialogContent, DialogPopup, DialogCreateHandle, DialogDescription, DialogFooter, DialogFormState, DialogHeader, DialogPanel, DialogPortal, DialogProvider, DialogTitle, DialogTrigger, DialogViewport };
@@ -2,6 +2,7 @@
2
2
  import { ChevronLeftIcon, ChevronRightIcon } from "../icons.js";
3
3
  import { cn } from "../lib/utils.js";
4
4
  import { containedEventHandler } from "../hooks/use-contained-handler.js";
5
+ import { SCROLL_EDGE_FADE_CLASS, writeScrollEdgeFade } from "../lib/scroll-edge-fade.js";
5
6
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
7
  import { useCallback, useState } from "react";
7
8
  //#region src/components/horizontal-scroller.tsx
@@ -13,6 +14,7 @@ const HorizontalScroller = ({ children, className, nextLabel, previousLabel, con
13
14
  canScrollStart: false
14
15
  });
15
16
  const measure = useCallback((node) => {
17
+ writeScrollEdgeFade(node);
16
18
  setState(scrollState(node.scrollLeft, node.scrollWidth, node.clientWidth, getComputedStyle(node).direction === "rtl" ? "rtl" : "ltr"));
17
19
  }, []);
18
20
  const viewportRef = useCallback((node) => {
@@ -55,40 +57,35 @@ const HorizontalScroller = ({ children, className, nextLabel, previousLabel, con
55
57
  });
56
58
  }),
57
59
  ...props,
58
- children: [
59
- /* @__PURE__ */ jsx("div", {
60
- className: cn("scrollbar-subtle overflow-x-auto overscroll-x-contain", viewportClassName),
61
- onScroll,
62
- ref: viewportRef,
63
- children: /* @__PURE__ */ jsx("div", {
64
- className: cn("w-max min-w-full", contentClassName),
65
- children
66
- })
67
- }),
68
- state.canScrollStart && /* @__PURE__ */ jsx("div", { className: "from-background pointer-events-none absolute inset-y-0 start-0 w-10 bg-linear-to-r to-transparent rtl:bg-linear-to-l" }),
69
- state.canScrollEnd && /* @__PURE__ */ jsx("div", { className: "from-background pointer-events-none absolute inset-y-0 end-0 w-10 bg-linear-to-l to-transparent rtl:bg-linear-to-r" }),
70
- (state.canScrollStart || state.canScrollEnd) && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
71
- "aria-disabled": !state.canScrollStart,
72
- tabIndex: state.canScrollStart ? void 0 : -1,
73
- "aria-label": previousLabel,
74
- className: cn(CONTROL_CLASS, "start-1", !state.canScrollStart && "pointer-events-none opacity-0 focus-visible:opacity-100"),
75
- onClick: () => {
76
- if (state.canScrollStart) scroll(-1);
77
- },
78
- type: "button",
79
- children: /* @__PURE__ */ jsx(ChevronLeftIcon, { className: "size-4 rtl:rotate-180" })
80
- }), /* @__PURE__ */ jsx("button", {
81
- "aria-disabled": !state.canScrollEnd,
82
- tabIndex: state.canScrollEnd ? void 0 : -1,
83
- "aria-label": nextLabel,
84
- className: cn(CONTROL_CLASS, "end-1", !state.canScrollEnd && "pointer-events-none opacity-0 focus-visible:opacity-100"),
85
- onClick: () => {
86
- if (state.canScrollEnd) scroll(1);
87
- },
88
- type: "button",
89
- children: /* @__PURE__ */ jsx(ChevronRightIcon, { className: "size-4 rtl:rotate-180" })
90
- })] })
91
- ]
60
+ children: [/* @__PURE__ */ jsx("div", {
61
+ className: cn("scrollbar-subtle overflow-x-auto overscroll-x-contain [--fade-size:2.5rem]", SCROLL_EDGE_FADE_CLASS, viewportClassName),
62
+ onScroll,
63
+ ref: viewportRef,
64
+ children: /* @__PURE__ */ jsx("div", {
65
+ className: cn("w-max min-w-full", contentClassName),
66
+ children
67
+ })
68
+ }), (state.canScrollStart || state.canScrollEnd) && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
69
+ "aria-disabled": !state.canScrollStart,
70
+ tabIndex: state.canScrollStart ? void 0 : -1,
71
+ "aria-label": previousLabel,
72
+ className: cn(CONTROL_CLASS, "start-1", !state.canScrollStart && "pointer-events-none opacity-0 focus-visible:opacity-100"),
73
+ onClick: () => {
74
+ if (state.canScrollStart) scroll(-1);
75
+ },
76
+ type: "button",
77
+ children: /* @__PURE__ */ jsx(ChevronLeftIcon, { className: "size-4 rtl:rotate-180" })
78
+ }), /* @__PURE__ */ jsx("button", {
79
+ "aria-disabled": !state.canScrollEnd,
80
+ tabIndex: state.canScrollEnd ? void 0 : -1,
81
+ "aria-label": nextLabel,
82
+ className: cn(CONTROL_CLASS, "end-1", !state.canScrollEnd && "pointer-events-none opacity-0 focus-visible:opacity-100"),
83
+ onClick: () => {
84
+ if (state.canScrollEnd) scroll(1);
85
+ },
86
+ type: "button",
87
+ children: /* @__PURE__ */ jsx(ChevronRightIcon, { className: "size-4 rtl:rotate-180" })
88
+ })] })]
92
89
  });
93
90
  };
94
91
  const scrollState = (scrollLeft, scrollWidth, clientWidth, direction) => {
@@ -0,0 +1,23 @@
1
+ import { InputContract, TextFieldContract } from "./input.shared.js";
2
+ import { TextInputProps } from "react-native";
3
+ //#region src/components/input.native.d.ts
4
+ type NativeFieldProps = Omit<TextInputProps, keyof TextFieldContract> & TextFieldContract & {
5
+ className?: string | undefined;
6
+ };
7
+ /**
8
+ * A text field in the web's type and colours: the caret, placeholder and
9
+ * selection read the theme tokens, and `disabled` stops editing.
10
+ */
11
+ export declare const NativeField: ({ box, className, disabled, editable, onChangeText, onValueChange, size, style, ...props }: NativeFieldProps & {
12
+ box: string;
13
+ }) => import("react").JSX.Element;
14
+ type InputProps = NativeFieldProps & {
15
+ type?: InputContract["type"];
16
+ };
17
+ /**
18
+ * The web's `Input` (./input.tsx) on a phone: one line of text, in the web's
19
+ * box, or iOS's filled search field for `type="search"`.
20
+ */
21
+ export declare const Input: ({ type, unstyled, size, ...props }: InputProps) => import("react").JSX.Element;
22
+ //#endregion
23
+ export {};
@@ -0,0 +1,66 @@
1
+ import { textFontFamily } from "./text.native.js";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { TextInput } from "react-native";
4
+ //#region src/components/input.native.tsx
5
+ /** The field's text at each control size, as the web sets it on a phone. */
6
+ const TEXT_SIZE = {
7
+ sm: "text-sm",
8
+ default: "text-base",
9
+ lg: "text-base"
10
+ };
11
+ /** The web input's box (hairline border, card fill) at each control size. */
12
+ const BOX = {
13
+ sm: "min-h-9 rounded-lg border border-input bg-card px-3",
14
+ default: "min-h-11 rounded-lg border border-input bg-card px-3",
15
+ lg: "min-h-12 rounded-lg border border-input bg-card px-3"
16
+ };
17
+ /** A search field, filled and borderless, as iOS sets one over a list. */
18
+ const SEARCH_BOX = "min-h-10 rounded-xl bg-secondary px-3";
19
+ /**
20
+ * A text field in the web's type and colours: the caret, placeholder and
21
+ * selection read the theme tokens, and `disabled` stops editing.
22
+ */
23
+ const NativeField = ({ box, className, disabled = false, editable = true, onChangeText, onValueChange, size = "default", style, ...props }) => /* @__PURE__ */ jsx(TextInput, {
24
+ cursorColorClassName: "accent-foreground",
25
+ placeholderTextColorClassName: "accent-muted-foreground",
26
+ selectionColorClassName: "accent-ring",
27
+ ...props,
28
+ className: [
29
+ "text-foreground",
30
+ box,
31
+ TEXT_SIZE[size],
32
+ disabled ? "opacity-64" : "",
33
+ className ?? ""
34
+ ].filter((name) => name !== "").join(" "),
35
+ editable: editable && !disabled,
36
+ onChangeText: (text) => {
37
+ onChangeText?.(text);
38
+ onValueChange?.(text);
39
+ },
40
+ style: [{ fontFamily: textFontFamily("body") }, style]
41
+ });
42
+ /** What each field type asks the platform's keyboard for. */
43
+ const INPUT_MODE = {
44
+ email: "email",
45
+ search: "search",
46
+ tel: "tel",
47
+ text: "text",
48
+ url: "url"
49
+ };
50
+ /** The field's own box: none, iOS's search field, or the web's box. */
51
+ const inputBox = (type, size, unstyled) => {
52
+ if (unstyled) return "min-h-9 py-2";
53
+ return type === "search" ? SEARCH_BOX : BOX[size];
54
+ };
55
+ /**
56
+ * The web's `Input` (./input.tsx) on a phone: one line of text, in the web's
57
+ * box, or iOS's filled search field for `type="search"`.
58
+ */
59
+ const Input = ({ type = "text", unstyled = false, size = "default", ...props }) => /* @__PURE__ */ jsx(NativeField, {
60
+ inputMode: INPUT_MODE[type],
61
+ ...props,
62
+ box: inputBox(type, size, unstyled),
63
+ size
64
+ });
65
+ //#endregion
66
+ export { Input, NativeField };
@@ -0,0 +1,19 @@
1
+ import { ControlSize } from "../lib/control-size.js";
2
+ //#region src/components/input.shared.d.ts
3
+ /** The props every text field renderer accepts, with one meaning. */
4
+ export type TextFieldContract = {
5
+ size?: ControlSize;
6
+ /** No box of its own, for a field inside a container that draws one. */
7
+ unstyled?: boolean;
8
+ value?: string;
9
+ defaultValue?: string;
10
+ placeholder?: string;
11
+ disabled?: boolean;
12
+ /** The field's new text, as typed. */
13
+ onValueChange?: (value: string) => void;
14
+ };
15
+ /** A single-line field also says what it holds; `search` draws a search field. */
16
+ export type InputContract = TextFieldContract & {
17
+ type?: "email" | "search" | "tel" | "text" | "url";
18
+ };
19
+ //#endregion
File without changes
@@ -1,4 +1,5 @@
1
1
  import { ComponentProps } from "react";
2
+ import { LoaderSize } from "./loader.shared.js";
2
3
  //#region src/components/loader.d.ts
3
4
  /**
4
5
  * The indeterminate loading indicator for a control or a row: a plain
@@ -15,12 +16,6 @@ declare const Loader: ({ label, variant, size, animation, className, ...props }:
15
16
  * Progress is stated in the detail text, never drawn as a bar that guesses.
16
17
  */
17
18
  declare const LoaderState: ({ label, detail, hint, className }: LoaderStateProps) => import("react").JSX.Element;
18
- declare const LOADER_SIZE: {
19
- readonly sm: "size-4";
20
- readonly md: "size-6";
21
- readonly lg: "size-8";
22
- };
23
- type LoaderSize = keyof typeof LOADER_SIZE;
24
19
  type LoaderProps = Omit<ComponentProps<"span">, "children" | "role" | "aria-label" | "aria-hidden" | "aria-busy"> & {
25
20
  size?: LoaderSize;
26
21
  animation?: "none" | "spin";
@@ -0,0 +1,28 @@
1
+ import { ActivityIndicatorProps } from "react-native";
2
+ import { LoaderContract, LoaderSize } from "./loader.shared.js";
3
+ //#region src/components/loader.native.d.ts
4
+ type LoaderProps = Omit<ActivityIndicatorProps, "size"> & {
5
+ className?: string | undefined;
6
+ /** The colour is the loader's own: the muted foreground, as on the web. */
7
+ color?: never;
8
+ /** Named through `label`, as on the web. */
9
+ accessibilityLabel?: never;
10
+ size?: LoaderSize | undefined;
11
+ } & ({
12
+ variant?: "status";
13
+ /**
14
+ * What is pending. Optional on a phone, unlike the web: the
15
+ * platform's indicator already announces itself as in progress.
16
+ */
17
+ label?: string | undefined;
18
+ } | {
19
+ variant: "decorative";
20
+ label?: never;
21
+ });
22
+ /**
23
+ * The indeterminate loading indicator, as the web's `Loader` (./loader.tsx):
24
+ * the platform's spinner in the muted foreground, at the web's sizes.
25
+ */
26
+ export declare const Loader: ({ label, size, variant, ...props }: LoaderProps) => import("react").JSX.Element;
27
+ //#endregion
28
+ export type { LoaderContract };
@@ -0,0 +1,23 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { ActivityIndicator } from "react-native";
3
+ //#region src/components/loader.native.tsx
4
+ /** The platform's indicator comes in two sizes; `lg` is a region's. */
5
+ const INDICATOR_SIZE = {
6
+ sm: "small",
7
+ md: "small",
8
+ lg: "large"
9
+ };
10
+ /**
11
+ * The indeterminate loading indicator, as the web's `Loader` (./loader.tsx):
12
+ * the platform's spinner in the muted foreground, at the web's sizes.
13
+ */
14
+ const Loader = ({ label, size = "md", variant = "status", ...props }) => /* @__PURE__ */ jsx(ActivityIndicator, {
15
+ ...props,
16
+ accessibilityElementsHidden: variant === "decorative",
17
+ accessibilityLabel: label,
18
+ colorClassName: "accent-muted-foreground",
19
+ importantForAccessibility: variant === "decorative" ? "no-hide-descendants" : "auto",
20
+ size: INDICATOR_SIZE[size]
21
+ });
22
+ //#endregion
23
+ export { Loader };
@@ -0,0 +1,17 @@
1
+ //#region src/components/loader.shared.d.ts
2
+ /** Inline beside a busy control (`sm`), in a row or panel (`md`), or a region (`lg`). */
3
+ export declare const LOADER_SIZES: readonly ["sm", "md", "lg"];
4
+ export type LoaderSize = (typeof LOADER_SIZES)[number];
5
+ /** The props every loader renderer accepts, with one meaning. */
6
+ export type LoaderContract = {
7
+ size?: LoaderSize;
8
+ } & ({
9
+ variant?: "status";
10
+ /** What is pending, for assistive technology ("Loading matters"). */
11
+ label: string;
12
+ } | {
13
+ /** The surrounding control or text announces the pending state. */
14
+ variant: "decorative";
15
+ label?: never;
16
+ });
17
+ //#endregion
@@ -0,0 +1,9 @@
1
+ //#region src/components/loader.shared.ts
2
+ /** Inline beside a busy control (`sm`), in a row or panel (`md`), or a region (`lg`). */
3
+ const LOADER_SIZES = [
4
+ "sm",
5
+ "md",
6
+ "lg"
7
+ ];
8
+ //#endregion
9
+ export { LOADER_SIZES };
@@ -1,14 +1,8 @@
1
+ import { MatterIconContract } from "./matter-icon.shared.js";
1
2
  //#region src/components/matter-icon.d.ts
2
3
  type MatterIconProps = {
3
4
  className?: string | undefined;
4
- } & ({
5
- matter: {
6
- id: string;
7
- color: string | null;
8
- };
9
- } | {
10
- variant: "none" | "all";
11
- });
5
+ } & MatterIconContract;
12
6
  export declare const MatterIcon: (props: MatterIconProps) => import("react").JSX.Element;
13
7
  export declare const MattersNavIcon: ({ className }: {
14
8
  className?: string | undefined;
@@ -1,47 +1,27 @@
1
1
  import { LayersIcon } from "../icons.js";
2
2
  import { cn } from "../lib/utils.js";
3
3
  import { resolveMatterColor } from "../lib/matter-colors.js";
4
- import { jsx, jsxs } from "react/jsx-runtime";
4
+ import { ALL_MATTERS_STACK } from "./matter-icon.shared.js";
5
+ import { jsx } from "react/jsx-runtime";
5
6
  //#region src/components/matter-icon.tsx
6
- const ALL_MATTERS_STACK_COLORS = [
7
- "var(--option-red)",
8
- "var(--option-blue)",
9
- "var(--option-green)"
10
- ];
11
7
  const MatterIcon = (props) => {
12
8
  if ("matter" in props) return /* @__PURE__ */ jsx(LayersIcon, {
13
9
  "aria-hidden": "true",
14
10
  className: cn(props.className),
15
11
  style: { color: resolveMatterColor(props.matter.id, props.matter.color) }
16
12
  });
17
- if (props.variant === "all") return /* @__PURE__ */ jsxs("svg", {
13
+ if (props.variant === "all") return /* @__PURE__ */ jsx("svg", {
18
14
  "aria-hidden": "true",
19
15
  className: cn(props.className),
20
16
  fill: "none",
21
17
  viewBox: "0 0 24 24",
22
- children: [
23
- /* @__PURE__ */ jsx("path", {
24
- d: "M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z",
25
- stroke: ALL_MATTERS_STACK_COLORS[0],
26
- strokeLinecap: "round",
27
- strokeLinejoin: "round",
28
- strokeWidth: "2"
29
- }),
30
- /* @__PURE__ */ jsx("path", {
31
- d: "M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12",
32
- stroke: ALL_MATTERS_STACK_COLORS[1],
33
- strokeLinecap: "round",
34
- strokeLinejoin: "round",
35
- strokeWidth: "2"
36
- }),
37
- /* @__PURE__ */ jsx("path", {
38
- d: "M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17",
39
- stroke: ALL_MATTERS_STACK_COLORS[2],
40
- strokeLinecap: "round",
41
- strokeLinejoin: "round",
42
- strokeWidth: "2"
43
- })
44
- ]
18
+ children: ALL_MATTERS_STACK.map(({ d, option }) => /* @__PURE__ */ jsx("path", {
19
+ d,
20
+ stroke: `var(--option-${option})`,
21
+ strokeLinecap: "round",
22
+ strokeLinejoin: "round",
23
+ strokeWidth: "2"
24
+ }, option))
45
25
  });
46
26
  return /* @__PURE__ */ jsx(LayersIcon, {
47
27
  "aria-hidden": "true",
@@ -0,0 +1,25 @@
1
+ import { MatterIconContract } from "./matter-icon.shared.js";
2
+ //#region src/components/matter-icon.native.d.ts
3
+ /**
4
+ * The shared contract, with a matter's colour optional: the app's matter
5
+ * records leave it out when none is stored, which the web reads as null.
6
+ */
7
+ type MatterIconProps = ({
8
+ matter: {
9
+ id: string;
10
+ color?: string | null | undefined;
11
+ };
12
+ } | Exclude<MatterIconContract, {
13
+ matter: unknown;
14
+ }>) & {
15
+ size?: number | undefined;
16
+ };
17
+ /**
18
+ * The web's `MatterIcon` (./matter-icon.tsx) on a phone: lucide's layers in
19
+ * the matter's own colour, resolved by the web's `resolveMatterColor` (the
20
+ * stored colour, else a swatch hashed from the id); the muted glyph for no
21
+ * matter; the tri-plane stack for all of them.
22
+ */
23
+ export declare const MatterIcon: (props: MatterIconProps) => import("react").JSX.Element;
24
+ //#endregion
25
+ export {};
@@ -0,0 +1,78 @@
1
+ import { resolveMatterColor } from "../lib/matter-colors.js";
2
+ import { ALL_MATTERS_STACK } from "./matter-icon.shared.js";
3
+ import { jsx } from "react/jsx-runtime";
4
+ import { LayersIcon } from "lucide-react-native";
5
+ import Svg, { Path } from "react-native-svg";
6
+ import { useCSSVariable } from "uniwind";
7
+ //#region src/components/matter-icon.native.tsx
8
+ /**
9
+ * Every palette option a matter can be painted in, as a class. uniwind keeps
10
+ * only the theme variables some class uses, so naming each option here keeps
11
+ * its colour resolvable from a matter's stored `var(--option-<name>)`.
12
+ */
13
+ const MATTER_OPTION_CLASS_NAMES = {
14
+ amber: "accent-option-amber",
15
+ blue: "accent-option-blue",
16
+ cyan: "accent-option-cyan",
17
+ emerald: "accent-option-emerald",
18
+ fuchsia: "accent-option-fuchsia",
19
+ gray: "accent-option-gray",
20
+ green: "accent-option-green",
21
+ indigo: "accent-option-indigo",
22
+ lime: "accent-option-lime",
23
+ orange: "accent-option-orange",
24
+ purple: "accent-option-purple",
25
+ red: "accent-option-red",
26
+ sky: "accent-option-sky",
27
+ teal: "accent-option-teal",
28
+ violet: "accent-option-violet",
29
+ yellow: "accent-option-yellow"
30
+ };
31
+ const MUTED = "--color-muted-foreground";
32
+ /**
33
+ * The theme variable a CSS colour from `resolveMatterColor` names
34
+ * (`var(--option-blue)` is `--color-option-blue`), or null for a literal
35
+ * colour such as a stored hex.
36
+ */
37
+ const themeVariable = (css) => {
38
+ const option = /^var\(--option-([a-z]+)\)$/u.exec(css)?.[1];
39
+ if (option === void 0) return css.startsWith("var(") ? MUTED : null;
40
+ return option in MATTER_OPTION_CLASS_NAMES ? `--color-option-${option}` : MUTED;
41
+ };
42
+ /**
43
+ * A theme variable's value as a colour. `currentColor` stands in only while
44
+ * the theme has not resolved the variable, which a class in this file keeps
45
+ * from happening.
46
+ */
47
+ const asColour = (value) => typeof value === "string" ? value : "currentColor";
48
+ /**
49
+ * The web's `MatterIcon` (./matter-icon.tsx) on a phone: lucide's layers in
50
+ * the matter's own colour, resolved by the web's `resolveMatterColor` (the
51
+ * stored colour, else a swatch hashed from the id); the muted glyph for no
52
+ * matter; the tri-plane stack for all of them.
53
+ */
54
+ const MatterIcon = (props) => {
55
+ const size = props.size ?? 16;
56
+ const css = "matter" in props ? resolveMatterColor(props.matter.id, props.matter.color ?? null) : null;
57
+ const variable = css === null ? MUTED : themeVariable(css);
58
+ const [resolved, ...stack] = useCSSVariable([variable ?? MUTED, ...ALL_MATTERS_STACK.map(({ option }) => `--color-option-${option}`)]);
59
+ if ("variant" in props && props.variant === "all") return /* @__PURE__ */ jsx(Svg, {
60
+ fill: "none",
61
+ height: size,
62
+ viewBox: "0 0 24 24",
63
+ width: size,
64
+ children: ALL_MATTERS_STACK.map(({ d, option }, index) => /* @__PURE__ */ jsx(Path, {
65
+ d,
66
+ stroke: asColour(stack[index]),
67
+ strokeLinecap: "round",
68
+ strokeLinejoin: "round",
69
+ strokeWidth: 2
70
+ }, option))
71
+ });
72
+ return /* @__PURE__ */ jsx(LayersIcon, {
73
+ color: variable === null && css !== null ? css : asColour(resolved),
74
+ size
75
+ });
76
+ };
77
+ //#endregion
78
+ export { MatterIcon };
@@ -0,0 +1,21 @@
1
+ //#region src/components/matter-icon.shared.d.ts
2
+ export type MatterIconContract = {
3
+ matter: {
4
+ id: string;
5
+ color: string | null;
6
+ };
7
+ } | {
8
+ variant: "none" | "all";
9
+ };
10
+ /** The three planes of the "all matters" glyph, top to bottom, in their tints. */
11
+ export declare const ALL_MATTERS_STACK: readonly [{
12
+ readonly d: "M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z";
13
+ readonly option: "red";
14
+ }, {
15
+ readonly d: "M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12";
16
+ readonly option: "blue";
17
+ }, {
18
+ readonly d: "M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17";
19
+ readonly option: "green";
20
+ }];
21
+ //#endregion
@@ -0,0 +1,18 @@
1
+ //#region src/components/matter-icon.shared.ts
2
+ /** The three planes of the "all matters" glyph, top to bottom, in their tints. */
3
+ const ALL_MATTERS_STACK = [
4
+ {
5
+ d: "M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z",
6
+ option: "red"
7
+ },
8
+ {
9
+ d: "M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12",
10
+ option: "blue"
11
+ },
12
+ {
13
+ d: "M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17",
14
+ option: "green"
15
+ }
16
+ ];
17
+ //#endregion
18
+ export { ALL_MATTERS_STACK };
@@ -0,0 +1,14 @@
1
+ import { SeparatorContract } from "./separator.shared.js";
2
+ import { ViewProps } from "react-native";
3
+ //#region src/components/separator.native.d.ts
4
+ /**
5
+ * The web's `Separator` (./separator.tsx) on a phone: a hairline in the
6
+ * border colour, across the container or, `vertical`, down it. A list passes
7
+ * it as the divider between its rows.
8
+ */
9
+ export declare const Separator: ({ className, orientation, ...props }: ViewProps & SeparatorContract & {
10
+ className?: string | undefined;
11
+ /** A hairline holds nothing. */
12
+ children?: never;
13
+ }) => import("react").JSX.Element;
14
+ //#endregion
@@ -0,0 +1,16 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { View } from "react-native";
3
+ //#region src/components/separator.native.tsx
4
+ /**
5
+ * The web's `Separator` (./separator.tsx) on a phone: a hairline in the
6
+ * border colour, across the container or, `vertical`, down it. A list passes
7
+ * it as the divider between its rows.
8
+ */
9
+ const Separator = ({ className, orientation = "horizontal", ...props }) => /* @__PURE__ */ jsx(View, {
10
+ ...props,
11
+ accessibilityElementsHidden: true,
12
+ className: `bg-border ${orientation === "vertical" ? "w-px self-stretch" : "h-px w-full"} ${className ?? ""}`,
13
+ importantForAccessibility: "no-hide-descendants"
14
+ });
15
+ //#endregion
16
+ export { Separator };