@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.
- package/README.md +3 -0
- package/THIRD_PARTY_NOTICES +24 -0
- package/dist/components/badge.d.ts +28 -0
- package/dist/components/badge.js +64 -0
- package/dist/components/button.d.ts +2 -6
- package/dist/components/button.js +33 -25
- package/dist/components/button.native.d.ts +15 -0
- package/dist/components/button.native.js +165 -0
- package/dist/components/button.shared.d.ts +65 -0
- package/dist/components/button.shared.js +61 -0
- package/dist/components/dialog.d.ts +20 -2
- package/dist/components/dialog.js +31 -9
- package/dist/components/horizontal-scroller.js +31 -34
- package/dist/components/input.native.d.ts +23 -0
- package/dist/components/input.native.js +66 -0
- package/dist/components/input.shared.d.ts +19 -0
- package/dist/components/input.shared.js +0 -0
- package/dist/components/loader.d.ts +1 -6
- package/dist/components/loader.native.d.ts +28 -0
- package/dist/components/loader.native.js +23 -0
- package/dist/components/loader.shared.d.ts +17 -0
- package/dist/components/loader.shared.js +9 -0
- package/dist/components/matter-icon.d.ts +2 -8
- package/dist/components/matter-icon.js +10 -30
- package/dist/components/matter-icon.native.d.ts +25 -0
- package/dist/components/matter-icon.native.js +78 -0
- package/dist/components/matter-icon.shared.d.ts +21 -0
- package/dist/components/matter-icon.shared.js +18 -0
- package/dist/components/separator.native.d.ts +14 -0
- package/dist/components/separator.native.js +16 -0
- package/dist/components/separator.shared.d.ts +6 -0
- package/dist/components/separator.shared.js +0 -0
- package/dist/components/sheet.native.d.ts +27 -0
- package/dist/components/sheet.native.js +69 -0
- package/dist/components/sheet.shared.d.ts +9 -0
- package/dist/components/sheet.shared.js +0 -0
- package/dist/components/sidebar.js +1 -1
- package/dist/components/stella-wordmark.js +5 -13
- package/dist/components/stella-wordmark.native.d.ts +9 -0
- package/dist/components/stella-wordmark.native.js +27 -0
- package/dist/components/stella-wordmark.shared.d.ts +6 -0
- package/dist/components/stella-wordmark.shared.js +16 -0
- package/dist/components/switch.native.d.ts +13 -0
- package/dist/components/switch.native.js +33 -0
- package/dist/components/switch.shared.d.ts +9 -0
- package/dist/components/switch.shared.js +0 -0
- package/dist/components/table.d.ts +86 -8
- package/dist/components/table.js +46 -20
- package/dist/components/text.d.ts +11 -0
- package/dist/components/text.js +25 -0
- package/dist/components/text.native.d.ts +15 -0
- package/dist/components/text.native.js +46 -0
- package/dist/components/text.shared.d.ts +78 -0
- package/dist/components/text.shared.js +71 -0
- package/dist/components/textarea.native.d.ts +10 -0
- package/dist/components/textarea.native.js +28 -0
- package/dist/components/textarea.shared.d.ts +4 -0
- package/dist/components/textarea.shared.js +0 -0
- package/dist/components/tooltip.d.ts +15 -2
- package/dist/components/tooltip.js +18 -2
- package/dist/components/truncated-text.d.ts +33 -0
- package/dist/components/truncated-text.js +111 -0
- package/dist/index.d.ts +6 -5
- package/dist/index.js +5 -3
- package/dist/inspector/dock.js +1 -1
- package/dist/lib/scroll-edge-fade.d.ts +23 -0
- package/dist/lib/scroll-edge-fade.js +62 -0
- package/dist/native-env.d.ts +1 -0
- package/package.json +112 -7
- package/dist/calendar/index.d.ts +0 -4
- 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
|
-
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
state.canScrollStart
|
|
69
|
-
|
|
70
|
-
(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
@@ -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 {
|
|
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__ */
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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 };
|