@tanishqkancharla/maui 0.0.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.
Files changed (209) hide show
  1. package/README.md +6 -0
  2. package/dist/MauiProvider.d.ts +10 -0
  3. package/dist/MauiProvider.js +45 -0
  4. package/dist/UIDatabase/Focus.d.ts +52 -0
  5. package/dist/UIDatabase/Focus.js +173 -0
  6. package/dist/UIDatabase/UIDatabase.d.ts +11 -0
  7. package/dist/UIDatabase/UIDatabase.js +24 -0
  8. package/dist/components/Avatar.d.ts +8 -0
  9. package/dist/components/Avatar.js +77 -0
  10. package/dist/components/Badge.d.ts +4 -0
  11. package/dist/components/Badge.js +27 -0
  12. package/dist/components/Button.d.ts +13 -0
  13. package/dist/components/Button.js +112 -0
  14. package/dist/components/Checkbox.d.ts +7 -0
  15. package/dist/components/Checkbox.js +54 -0
  16. package/dist/components/CollectionPopover.d.ts +4 -0
  17. package/dist/components/CollectionPopover.js +17 -0
  18. package/dist/components/Dialog.d.ts +7 -0
  19. package/dist/components/Dialog.js +36 -0
  20. package/dist/components/FuzzyString.d.ts +5 -0
  21. package/dist/components/FuzzyString.js +13 -0
  22. package/dist/components/Icons.d.ts +28 -0
  23. package/dist/components/Icons.js +52 -0
  24. package/dist/components/Input.d.ts +7 -0
  25. package/dist/components/Input.js +193 -0
  26. package/dist/components/ListBox.d.ts +9 -0
  27. package/dist/components/ListBox.js +58 -0
  28. package/dist/components/Menu.d.ts +13 -0
  29. package/dist/components/Menu.js +28 -0
  30. package/dist/components/Overlay.d.ts +5 -0
  31. package/dist/components/Overlay.js +30 -0
  32. package/dist/components/Panel.d.ts +4 -0
  33. package/dist/components/Panel.js +15 -0
  34. package/dist/components/Prose.d.ts +24 -0
  35. package/dist/components/Prose.js +43 -0
  36. package/dist/components/Radio.d.ts +12 -0
  37. package/dist/components/Radio.js +64 -0
  38. package/dist/components/Select.d.ts +13 -0
  39. package/dist/components/Select.js +85 -0
  40. package/dist/components/Slider.d.ts +6 -0
  41. package/dist/components/Slider.js +84 -0
  42. package/dist/components/Switch.d.ts +7 -0
  43. package/dist/components/Switch.js +55 -0
  44. package/dist/components/Table.d.ts +20 -0
  45. package/dist/components/Table.js +42 -0
  46. package/dist/components/Tooltip.d.ts +14 -0
  47. package/dist/components/Tooltip.js +165 -0
  48. package/dist/components/Typography.d.ts +36 -0
  49. package/dist/components/Typography.js +116 -0
  50. package/dist/components/Utils.d.ts +35 -0
  51. package/dist/components/Utils.js +43 -0
  52. package/dist/components/navigationItem.d.ts +1 -0
  53. package/dist/components/navigationItem.js +19 -0
  54. package/dist/hooks/useFocus.d.ts +18 -0
  55. package/dist/hooks/useFocus.js +47 -0
  56. package/dist/hooks/useRefCurrent.d.ts +1 -0
  57. package/dist/hooks/useRefCurrent.js +8 -0
  58. package/dist/hooks/useTupleDatabase.d.ts +7 -0
  59. package/dist/hooks/useTupleDatabase.js +10 -0
  60. package/dist/maui.d.ts +42 -0
  61. package/dist/maui.js +46 -0
  62. package/dist/patterns/Loader.d.ts +9 -0
  63. package/dist/patterns/Loader.js +100 -0
  64. package/dist/patterns/Sidebar.d.ts +19 -0
  65. package/dist/patterns/Sidebar.js +96 -0
  66. package/dist/patterns/Tree.d.ts +32 -0
  67. package/dist/patterns/Tree.js +173 -0
  68. package/dist/theme/ThemeContext.d.ts +14 -0
  69. package/dist/theme/ThemeContext.js +51 -0
  70. package/dist/theme/dataTheme.d.ts +2 -0
  71. package/dist/theme/dataTheme.js +2 -0
  72. package/dist/theme/themeFoucScript.d.ts +5 -0
  73. package/dist/theme/themeFoucScript.js +28 -0
  74. package/dist/tokens/avatar.d.ts +32 -0
  75. package/dist/tokens/avatar.js +34 -0
  76. package/dist/tokens/background.d.ts +15 -0
  77. package/dist/tokens/background.js +34 -0
  78. package/dist/tokens/borders.d.ts +7 -0
  79. package/dist/tokens/borders.js +21 -0
  80. package/dist/tokens/colors.d.ts +202 -0
  81. package/dist/tokens/colors.js +65 -0
  82. package/dist/tokens/focusRing.d.ts +5 -0
  83. package/dist/tokens/focusRing.js +23 -0
  84. package/dist/tokens/layout.d.ts +32 -0
  85. package/dist/tokens/layout.js +79 -0
  86. package/dist/tokens/motion.d.ts +3 -0
  87. package/dist/tokens/motion.js +11 -0
  88. package/dist/tokens/prose.d.ts +30 -0
  89. package/dist/tokens/prose.js +117 -0
  90. package/dist/tokens/radius.d.ts +10 -0
  91. package/dist/tokens/radius.js +11 -0
  92. package/dist/tokens/shadow.d.ts +10 -0
  93. package/dist/tokens/shadow.js +24 -0
  94. package/dist/tokens/sizing.d.ts +5 -0
  95. package/dist/tokens/sizing.js +6 -0
  96. package/dist/tokens/spacing.d.ts +39 -0
  97. package/dist/tokens/spacing.js +39 -0
  98. package/dist/tokens/text.d.ts +865 -0
  99. package/dist/tokens/text.js +59 -0
  100. package/dist/tokens/visuallyHidden.d.ts +1 -0
  101. package/dist/tokens/visuallyHidden.js +12 -0
  102. package/dist/utils/fuzzyMatch.d.ts +9 -0
  103. package/dist/utils/fuzzyMatch.js +101 -0
  104. package/dist/utils/memoize.d.ts +8 -0
  105. package/dist/utils/memoize.js +20 -0
  106. package/package.json +67 -0
  107. package/src/MauiProvider.tsx +83 -0
  108. package/src/UIDatabase/Focus.ts +263 -0
  109. package/src/UIDatabase/UIDatabase.tsx +49 -0
  110. package/src/apps/EmailClient/EmailClient.tsx +129 -0
  111. package/src/apps/EmailClient/data.tsx +254 -0
  112. package/src/components/Avatar.tsx +99 -0
  113. package/src/components/Badge.tsx +38 -0
  114. package/src/components/Button.tsx +175 -0
  115. package/src/components/Checkbox.tsx +99 -0
  116. package/src/components/CodeBlock.tsx +78 -0
  117. package/src/components/CollectionPopover.tsx +41 -0
  118. package/src/components/Dialog.tsx +60 -0
  119. package/src/components/FuzzyString.tsx +23 -0
  120. package/src/components/Icons.tsx +320 -0
  121. package/src/components/Input.tsx +274 -0
  122. package/src/components/ListBox.tsx +81 -0
  123. package/src/components/Menu.tsx +68 -0
  124. package/src/components/Overlay.tsx +45 -0
  125. package/src/components/Panel.tsx +32 -0
  126. package/src/components/Prose.tsx +59 -0
  127. package/src/components/Radio.tsx +121 -0
  128. package/src/components/Select.tsx +166 -0
  129. package/src/components/Slider.tsx +126 -0
  130. package/src/components/Switch.tsx +94 -0
  131. package/src/components/Table.tsx +61 -0
  132. package/src/components/Tooltip.tsx +269 -0
  133. package/src/components/Typography.tsx +161 -0
  134. package/src/components/Utils.tsx +100 -0
  135. package/src/components/navigationItem.ts +25 -0
  136. package/src/hooks/useCommand.ts +98 -0
  137. package/src/hooks/useFocus.tsx +87 -0
  138. package/src/hooks/useRefCurrent.ts +11 -0
  139. package/src/hooks/useTupleDatabase.ts +22 -0
  140. package/src/index.html +52 -0
  141. package/src/index.tsx +64 -0
  142. package/src/maui.ts +104 -0
  143. package/src/pages/AvatarPage.tsx +52 -0
  144. package/src/pages/BackgroundColorTokenPage.tsx +142 -0
  145. package/src/pages/BadgePage.tsx +41 -0
  146. package/src/pages/BordersTokenPage.tsx +205 -0
  147. package/src/pages/ButtonsPage.tsx +160 -0
  148. package/src/pages/CodeBlockPage.tsx +120 -0
  149. package/src/pages/ColorTokenPage.tsx +126 -0
  150. package/src/pages/CornerRadiusTokenPage.tsx +164 -0
  151. package/src/pages/EmailClientPage.tsx +25 -0
  152. package/src/pages/FlexTokenPage.tsx +167 -0
  153. package/src/pages/FocusRingTokenPage.tsx +77 -0
  154. package/src/pages/FormControlsPage.tsx +112 -0
  155. package/src/pages/FuzzyStringPage.tsx +155 -0
  156. package/src/pages/IconsPage.tsx +62 -0
  157. package/src/pages/InboxPage.tsx +29 -0
  158. package/src/pages/LayoutTokenPage.tsx +150 -0
  159. package/src/pages/LayoutUtilitiesPage.tsx +34 -0
  160. package/src/pages/ListBoxPage.tsx +44 -0
  161. package/src/pages/LoaderPage.tsx +74 -0
  162. package/src/pages/Maui.tsx +298 -0
  163. package/src/pages/MenuPage.tsx +49 -0
  164. package/src/pages/MessageListPage.tsx +30 -0
  165. package/src/pages/MotionTokenPage.tsx +107 -0
  166. package/src/pages/ProsePage.tsx +107 -0
  167. package/src/pages/SelectPage.tsx +28 -0
  168. package/src/pages/ShadowTokenPage.tsx +128 -0
  169. package/src/pages/SidebarPage.tsx +178 -0
  170. package/src/pages/SizingTokenPage.tsx +150 -0
  171. package/src/pages/SpacingTokenPage.tsx +169 -0
  172. package/src/pages/TextTokenPage.tsx +182 -0
  173. package/src/pages/TooltipPage.tsx +127 -0
  174. package/src/pages/TreePage.tsx +156 -0
  175. package/src/patterns/Inbox.tsx +487 -0
  176. package/src/patterns/Loader.tsx +161 -0
  177. package/src/patterns/MessageList.tsx +194 -0
  178. package/src/patterns/Sidebar.tsx +184 -0
  179. package/src/patterns/Tree.tsx +354 -0
  180. package/src/theme/ThemeContext.tsx +88 -0
  181. package/src/theme/dataTheme.ts +2 -0
  182. package/src/theme/themeFoucScript.ts +29 -0
  183. package/src/tokens/avatar.ts +35 -0
  184. package/src/tokens/background.ts +39 -0
  185. package/src/tokens/borders.ts +33 -0
  186. package/src/tokens/colors.ts +79 -0
  187. package/src/tokens/focusRing.ts +29 -0
  188. package/src/tokens/layout.ts +130 -0
  189. package/src/tokens/motion.ts +15 -0
  190. package/src/tokens/prose.ts +164 -0
  191. package/src/tokens/radius.ts +12 -0
  192. package/src/tokens/shadow.ts +28 -0
  193. package/src/tokens/sizing.ts +7 -0
  194. package/src/tokens/spacing.ts +58 -0
  195. package/src/tokens/text.ts +79 -0
  196. package/src/tokens/visuallyHidden.ts +13 -0
  197. package/src/types/index.d.ts +8 -0
  198. package/src/utils/assertUtils.ts +4 -0
  199. package/src/utils/cls.ts +5 -0
  200. package/src/utils/debug.ts +20 -0
  201. package/src/utils/fuzzyMatch.ts +123 -0
  202. package/src/utils/isEqualToDepth.ts +22 -0
  203. package/src/utils/mauiShikiTheme.ts +195 -0
  204. package/src/utils/memoize.ts +25 -0
  205. package/src/utils/randomId.ts +3 -0
  206. package/src/utils/shiki.ts +63 -0
  207. package/src/utils/useMemoShallowEqual.ts +13 -0
  208. package/src/utils/useRerender.ts +11 -0
  209. package/src/utils/useShortcut.tsx +111 -0
package/README.md ADDED
@@ -0,0 +1,6 @@
1
+ Frontend app boilerplate built with esbuild, typescript, vercel.
2
+
3
+ ```
4
+ npm run dev
5
+ npm run build
6
+ ```
@@ -0,0 +1,10 @@
1
+ import type { ReactNode } from "react";
2
+ /**
3
+ * Root provider for consuming Maui. Wraps theme (`data-theme`), purse-styles,
4
+ * design-system globals, and the focus UI database used by Button/Dialog.
5
+ *
6
+ * Pair with {@link themeFoucScript} in `<head>` to avoid a flash of wrong theme.
7
+ */
8
+ export declare function MauiProvider(props: {
9
+ children: ReactNode;
10
+ }): import("react").JSX.Element;
@@ -0,0 +1,45 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { PurseProvider, useInjectGlobalStyles } from "purse-styles";
3
+ import { ThemeProvider } from "./theme/ThemeContext";
4
+ import { UIDatabaseProvider } from "./UIDatabase/UIDatabase";
5
+ import { backgroundColor } from "./tokens/background";
6
+ import { baseTextStyle } from "./tokens/text";
7
+ function MauiGlobalStyles() {
8
+ useInjectGlobalStyles("*, *::after, *::before", {
9
+ boxSizing: "border-box",
10
+ }, []);
11
+ useInjectGlobalStyles("html, body", {
12
+ margin: 0,
13
+ backgroundColor: backgroundColor.app,
14
+ ...baseTextStyle,
15
+ }, []);
16
+ useInjectGlobalStyles("html", {
17
+ WebkitTextSizeAdjust: "100%",
18
+ textSizeAdjust: "100%",
19
+ }, []);
20
+ useInjectGlobalStyles("body", {
21
+ WebkitFontSmoothing: "antialiased",
22
+ MozOsxFontSmoothing: "grayscale",
23
+ fontSynthesis: "none",
24
+ textRendering: "optimizeLegibility",
25
+ }, []);
26
+ useInjectGlobalStyles("h1, h2, h3, h4, h5, h6, p, blockquote, figure, pre, dl, dd", {
27
+ margin: 0,
28
+ }, []);
29
+ useInjectGlobalStyles("button, input, select, textarea", {
30
+ font: "inherit",
31
+ }, []);
32
+ useInjectGlobalStyles('button, [role="button"]', {
33
+ cursor: "default",
34
+ }, []);
35
+ return null;
36
+ }
37
+ /**
38
+ * Root provider for consuming Maui. Wraps theme (`data-theme`), purse-styles,
39
+ * design-system globals, and the focus UI database used by Button/Dialog.
40
+ *
41
+ * Pair with {@link themeFoucScript} in `<head>` to avoid a flash of wrong theme.
42
+ */
43
+ export function MauiProvider(props) {
44
+ return (_jsx(ThemeProvider, { children: _jsxs(PurseProvider, { children: [_jsx(MauiGlobalStyles, {}), _jsx(UIDatabaseProvider, { children: props.children })] }) }));
45
+ }
@@ -0,0 +1,52 @@
1
+ import { ReadOnlyUIDatabase } from "./UIDatabase";
2
+ export type FocusScopeProps = {
3
+ containBehavior: "lock" | "cycle" | "no-contain";
4
+ autoFocus: boolean;
5
+ };
6
+ type FocusScope = {
7
+ type: "scope";
8
+ id: string;
9
+ } & FocusScopeProps;
10
+ type FocusElement = {
11
+ type: "element";
12
+ id: string;
13
+ };
14
+ export type FocusTreeKey = ["focusTree", ...string[]];
15
+ export type UIFocusSchema = {
16
+ key: FocusTreeKey;
17
+ value: FocusElement | FocusScope;
18
+ } | {
19
+ key: ["activeFocus"];
20
+ value: FocusTreeKey | undefined;
21
+ };
22
+ export declare const focusActions: {
23
+ registerFocusElement: (dbOrTx: import("tuple-database").TupleTransactionApi<UIFocusSchema> | import("tuple-database").TupleDatabaseClientApi<UIFocusSchema>, elementKey: FocusTreeKey) => void;
24
+ unregisterFocusElement: (dbOrTx: import("tuple-database").TupleTransactionApi<UIFocusSchema> | import("tuple-database").TupleDatabaseClientApi<UIFocusSchema>, elementKey: FocusTreeKey) => void;
25
+ registerFocusScope: (dbOrTx: import("tuple-database").TupleTransactionApi<UIFocusSchema> | import("tuple-database").TupleDatabaseClientApi<UIFocusSchema>, scopeKey: FocusTreeKey, scopeProps: FocusScopeProps) => void;
26
+ unregisterFocusScope: (dbOrTx: import("tuple-database").TupleTransactionApi<UIFocusSchema> | import("tuple-database").TupleDatabaseClientApi<UIFocusSchema>, scopeKey: FocusTreeKey) => void;
27
+ focus: (dbOrTx: import("tuple-database").TupleTransactionApi<UIFocusSchema> | import("tuple-database").TupleDatabaseClientApi<UIFocusSchema>, key: FocusTreeKey) => void;
28
+ blur: (dbOrTx: import("tuple-database").TupleTransactionApi<UIFocusSchema> | import("tuple-database").TupleDatabaseClientApi<UIFocusSchema>, key: FocusTreeKey) => void;
29
+ moveToNextFocus: (dbOrTx: import("tuple-database").TupleTransactionApi<UIFocusSchema> | import("tuple-database").TupleDatabaseClientApi<UIFocusSchema>) => void;
30
+ moveToPreviousFocus: (dbOrTx: import("tuple-database").TupleTransactionApi<UIFocusSchema> | import("tuple-database").TupleDatabaseClientApi<UIFocusSchema>) => void;
31
+ };
32
+ type FocusElementAndKey = {
33
+ key: ["focusTree", ...string[]];
34
+ value: FocusElement;
35
+ };
36
+ type FocusScopeAndKey = {
37
+ key: ["focusTree", ...string[]];
38
+ value: FocusScope;
39
+ };
40
+ export declare const focusQueries: {
41
+ getPreviousFocusElement(db: ReadOnlyUIDatabase, elementKey: FocusTreeKey): FocusElementAndKey | undefined;
42
+ getNextFocusElement(db: ReadOnlyUIDatabase, elementKey: FocusTreeKey): FocusElementAndKey | undefined;
43
+ getFirstElementInScope(db: ReadOnlyUIDatabase, scopeKey: FocusTreeKey): FocusElementAndKey | undefined;
44
+ getLastElementInScope(db: ReadOnlyUIDatabase, scopeKey: FocusTreeKey): FocusElementAndKey | undefined;
45
+ getAllElementsInScope(db: ReadOnlyUIDatabase, scopeKey: FocusTreeKey): FocusElementAndKey[];
46
+ isElementFirstInScope(db: ReadOnlyUIDatabase, elementKey: FocusTreeKey): boolean;
47
+ isElementLastInScope(db: ReadOnlyUIDatabase, elementKey: FocusTreeKey): boolean;
48
+ getParentScope(db: ReadOnlyUIDatabase, elementKey: FocusTreeKey): FocusScopeAndKey;
49
+ isElementFocused(db: ReadOnlyUIDatabase, elementKey: FocusTreeKey): boolean;
50
+ getActiveFocus(db: ReadOnlyUIDatabase): FocusTreeKey | undefined;
51
+ };
52
+ export {};
@@ -0,0 +1,173 @@
1
+ import { isEqual } from "lodash";
2
+ import { uiQuery } from "./UIDatabase";
3
+ export const focusActions = {
4
+ registerFocusElement: uiQuery((tx, elementKey) => {
5
+ if (tx.get(elementKey)) {
6
+ throw new Error(`Focusable element with key ${elementKey} already exists`);
7
+ }
8
+ const focusElement = {
9
+ type: "element",
10
+ id: elementKey.last,
11
+ };
12
+ tx.set(elementKey, focusElement);
13
+ }),
14
+ unregisterFocusElement: uiQuery((tx, elementKey) => {
15
+ if (!tx.get(elementKey)) {
16
+ throw new Error(`Focusable element with key ${elementKey} doesn't exist`);
17
+ }
18
+ tx.remove(elementKey);
19
+ }),
20
+ registerFocusScope: uiQuery((tx, scopeKey, scopeProps) => {
21
+ if (tx.get(scopeKey)) {
22
+ throw new Error(`Focus scope with key ${scopeKey} already exists`);
23
+ }
24
+ const scope = {
25
+ type: "scope",
26
+ id: scopeKey.last,
27
+ ...scopeProps,
28
+ };
29
+ tx.set(scopeKey, scope);
30
+ if (scopeProps.autoFocus) {
31
+ const firstChild = focusQueries.getFirstElementInScope(tx, scopeKey);
32
+ if (firstChild) {
33
+ focusActions.focus(tx, firstChild.key);
34
+ }
35
+ }
36
+ }),
37
+ unregisterFocusScope: uiQuery((tx, scopeKey) => {
38
+ if (!tx.get(scopeKey)) {
39
+ throw new Error(`Focus scope with key ${scopeKey} doesn't exist`);
40
+ }
41
+ // TODO: respect restoreFocus
42
+ tx.remove(scopeKey);
43
+ }),
44
+ focus: uiQuery((tx, key) => {
45
+ tx.set(["activeFocus"], key);
46
+ }),
47
+ blur: uiQuery((tx, key) => {
48
+ if (isEqual(tx.get(["activeFocus"]), key)) {
49
+ tx.set(["activeFocus"], undefined);
50
+ }
51
+ }),
52
+ moveToNextFocus: uiQuery((tx) => {
53
+ const activeFocus = tx.get(["activeFocus"]);
54
+ if (!activeFocus) {
55
+ const firstFocusableElement = focusQueries.getFirstElementInScope(tx, [
56
+ "focusTree",
57
+ ]);
58
+ if (firstFocusableElement)
59
+ tx.set(["activeFocus"], firstFocusableElement.key);
60
+ return;
61
+ }
62
+ const isLast = focusQueries.isElementLastInScope(tx, activeFocus);
63
+ if (!isLast) {
64
+ const nextElement = focusQueries.getNextFocusElement(tx, activeFocus);
65
+ if (nextElement)
66
+ tx.set(["activeFocus"], nextElement.key);
67
+ return;
68
+ }
69
+ const { key: scopeKey, value: scope } = focusQueries.getParentScope(tx, activeFocus);
70
+ if (scope.containBehavior === "no-contain") {
71
+ const nextElement = focusQueries.getNextFocusElement(tx, activeFocus);
72
+ if (nextElement)
73
+ tx.set(["activeFocus"], nextElement.key);
74
+ }
75
+ else if (scope.containBehavior === "cycle") {
76
+ const firstElement = focusQueries.getFirstElementInScope(tx, scopeKey);
77
+ if (firstElement)
78
+ tx.set(["activeFocus"], firstElement.key);
79
+ }
80
+ else if (scope.containBehavior === "lock") {
81
+ // Don't change active focus
82
+ return;
83
+ }
84
+ }),
85
+ moveToPreviousFocus: uiQuery((tx) => {
86
+ const activeFocus = tx.get(["activeFocus"]);
87
+ if (!activeFocus) {
88
+ const firstFocusableElement = focusQueries.getLastElementInScope(tx, [
89
+ "focusTree",
90
+ ]);
91
+ if (firstFocusableElement)
92
+ tx.set(["activeFocus"], firstFocusableElement.key);
93
+ return;
94
+ }
95
+ const isFirst = focusQueries.isElementFirstInScope(tx, activeFocus);
96
+ if (!isFirst) {
97
+ const previousElement = focusQueries.getPreviousFocusElement(tx, activeFocus);
98
+ if (previousElement)
99
+ tx.set(["activeFocus"], previousElement.key);
100
+ return;
101
+ }
102
+ const { key: scopeKey, value: scope } = focusQueries.getParentScope(tx, activeFocus);
103
+ if (scope.containBehavior === "no-contain") {
104
+ const previousElement = focusQueries.getPreviousFocusElement(tx, activeFocus);
105
+ if (previousElement)
106
+ tx.set(["activeFocus"], previousElement.key);
107
+ }
108
+ else if (scope.containBehavior === "cycle") {
109
+ const lastElement = focusQueries.getLastElementInScope(tx, scopeKey);
110
+ if (lastElement)
111
+ tx.set(["activeFocus"], lastElement.key);
112
+ }
113
+ else if (scope.containBehavior === "lock") {
114
+ // Don't change active focus
115
+ return;
116
+ }
117
+ }),
118
+ };
119
+ export const focusQueries = {
120
+ getPreviousFocusElement(db, elementKey) {
121
+ const previousElements = db.scan({
122
+ lt: elementKey,
123
+ reverse: true,
124
+ });
125
+ const previous = previousElements.find(({ value }) => value.type === "element");
126
+ return previous;
127
+ },
128
+ getNextFocusElement(db, elementKey) {
129
+ const nextElements = db.scan({ gt: elementKey });
130
+ const nextElement = nextElements.find(({ value }) => value.type === "element");
131
+ return nextElement;
132
+ },
133
+ getFirstElementInScope(db, scopeKey) {
134
+ return focusQueries.getAllElementsInScope(db, scopeKey).first;
135
+ },
136
+ getLastElementInScope(db, scopeKey) {
137
+ return focusQueries.getAllElementsInScope(db, scopeKey).last;
138
+ },
139
+ // TODO: Could have a different index just for elements that would make
140
+ // this more efficient
141
+ getAllElementsInScope(db, scopeKey) {
142
+ const elements = db
143
+ .scan({ prefix: scopeKey })
144
+ .filter(({ value }) => value.type === "element");
145
+ return elements;
146
+ },
147
+ isElementFirstInScope(db, elementKey) {
148
+ const parentScopeKey = elementKey.slice(0, -1);
149
+ const elementsInScope = focusQueries.getAllElementsInScope(db, parentScopeKey);
150
+ const firstElementInScopeKey = elementsInScope[0].key;
151
+ return isEqual(firstElementInScopeKey, elementKey);
152
+ },
153
+ isElementLastInScope(db, elementKey) {
154
+ const parentScopeKey = elementKey.slice(0, -1);
155
+ const elementsInScope = focusQueries.getAllElementsInScope(db, parentScopeKey);
156
+ const lastElementInScopeKey = elementsInScope[elementsInScope.length - 1].key;
157
+ return isEqual(lastElementInScopeKey, elementKey);
158
+ },
159
+ getParentScope(db, elementKey) {
160
+ const parentScopeKey = elementKey.slice(0, -1);
161
+ const parentScope = db.get(parentScopeKey);
162
+ if (parentScope?.type !== "scope") {
163
+ throw new Error(`Expected parent scope here`);
164
+ }
165
+ return { key: parentScopeKey, value: parentScope };
166
+ },
167
+ isElementFocused(db, elementKey) {
168
+ return isEqual(focusQueries.getActiveFocus(db), elementKey);
169
+ },
170
+ getActiveFocus(db) {
171
+ return db.get(["activeFocus"]);
172
+ },
173
+ };
@@ -0,0 +1,11 @@
1
+ import React from "react";
2
+ import { ReadOnlyTupleDatabaseClientApi, TupleDatabaseClientApi } from "tuple-database";
3
+ import { UIFocusSchema } from "./Focus";
4
+ export type UIDatabaseSchema = UIFocusSchema;
5
+ export declare const uiQuery: <I extends any[], O>(fn: (tx: import("tuple-database").TupleTransactionApi<UIFocusSchema>, ...args: I) => O) => (dbOrTx: import("tuple-database").TupleTransactionApi<UIFocusSchema> | TupleDatabaseClientApi<UIFocusSchema>, ...args: I) => O;
6
+ export type UIDatabase = TupleDatabaseClientApi<UIDatabaseSchema>;
7
+ export type ReadOnlyUIDatabase = ReadOnlyTupleDatabaseClientApi<UIDatabaseSchema>;
8
+ export declare function UIDatabaseProvider(props: {
9
+ children: React.ReactNode;
10
+ }): React.JSX.Element;
11
+ export declare function useUIDatabase(): UIDatabase;
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import React, { useContext, useMemo } from "react";
3
+ import { InMemoryTupleStorage, transactionalReadWrite, TupleDatabase, TupleDatabaseClient, } from "tuple-database";
4
+ export const uiQuery = transactionalReadWrite();
5
+ const UIDatabaseContext = React.createContext(undefined);
6
+ export function UIDatabaseProvider(props) {
7
+ const db = useMemo(() => {
8
+ return new TupleDatabaseClient(new TupleDatabase(new InMemoryTupleStorage()));
9
+ }, []);
10
+ // useEffect(() => {
11
+ // ;(window as any).db = db
12
+ // db.subscribe({ prefix: [] }, (writes) => {
13
+ // console.log(writes)
14
+ // })
15
+ // }, [])
16
+ return (_jsx(UIDatabaseContext.Provider, { value: db, children: props.children }));
17
+ }
18
+ export function useUIDatabase() {
19
+ const db = useContext(UIDatabaseContext);
20
+ if (!db) {
21
+ throw new Error("UI Database Context not found");
22
+ }
23
+ return db;
24
+ }
@@ -0,0 +1,8 @@
1
+ import { type TextSize } from "../tokens/text";
2
+ type AvatarProps = {
3
+ name: string;
4
+ size?: TextSize;
5
+ className?: string;
6
+ };
7
+ export declare function Avatar({ name, size, className }: AvatarProps): import("react").JSX.Element;
8
+ export {};
@@ -0,0 +1,77 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { style, useStyles } from "purse-styles";
3
+ import { avatar } from "../tokens/avatar";
4
+ import { colors } from "../tokens/colors";
5
+ import { radius } from "../tokens/radius";
6
+ import { text } from "../tokens/text";
7
+ import { memoize } from "../utils/memoize";
8
+ export function Avatar({ name, size = "sm", className }) {
9
+ const avatarClassName = useStyles(avatarClass(name, size));
10
+ return (_jsx("span", { className: joinClassNames(avatarClassName, className), "aria-hidden": "true", children: initials(name) }));
11
+ }
12
+ const avatarPalette = [
13
+ { background: colors.accent[4], color: colors.accent[11] },
14
+ {
15
+ background: avatar.green.background,
16
+ color: avatar.green.foreground,
17
+ },
18
+ {
19
+ background: avatar.orange.background,
20
+ color: avatar.orange.foreground,
21
+ },
22
+ {
23
+ background: avatar.pink.background,
24
+ color: avatar.pink.foreground,
25
+ },
26
+ ];
27
+ // Avatar dimensions correspond to the text-size scale, so an avatar sits
28
+ // naturally next to text of the same size. "sm" is the common 18px size.
29
+ const avatarDimensions = {
30
+ "2xs": "14px",
31
+ xs: "16px",
32
+ sm: "18px",
33
+ md: "20px",
34
+ lg: "24px",
35
+ xl: "32px",
36
+ };
37
+ // Initials sit well below the box size so they read as a compact monogram
38
+ // rather than filling the circle.
39
+ const avatarInitialsFontSize = {
40
+ "2xs": "6px",
41
+ xs: "7px",
42
+ sm: "8px",
43
+ md: "9px",
44
+ lg: "10px",
45
+ xl: "13px",
46
+ };
47
+ function initials(name) {
48
+ return name
49
+ .split(/\s+/)
50
+ .slice(0, 2)
51
+ .map((part) => part[0]?.toUpperCase() ?? "")
52
+ .join("");
53
+ }
54
+ function avatarColorIndex(seed) {
55
+ let hash = 0;
56
+ for (let index = 0; index < seed.length; index += 1) {
57
+ hash = seed.charCodeAt(index) + ((hash << 5) - hash);
58
+ }
59
+ return Math.abs(hash) % avatarPalette.length;
60
+ }
61
+ const avatarClass = memoize((seed, size) => {
62
+ const palette = avatarPalette[avatarColorIndex(seed)];
63
+ return style(text("2xs", 600, "highContrast"), radius.circle, {
64
+ display: "grid",
65
+ placeItems: "center",
66
+ flexShrink: 0,
67
+ width: avatarDimensions[size],
68
+ height: avatarDimensions[size],
69
+ fontSize: avatarInitialsFontSize[size],
70
+ lineHeight: 1,
71
+ backgroundColor: palette.background,
72
+ color: palette.color,
73
+ });
74
+ });
75
+ function joinClassNames(...classNames) {
76
+ return classNames.filter(Boolean).join(" ");
77
+ }
@@ -0,0 +1,4 @@
1
+ import type React from "react";
2
+ type BadgeProps = React.HTMLAttributes<HTMLSpanElement>;
3
+ export declare function Badge({ className, children, ...props }: BadgeProps): React.JSX.Element;
4
+ export {};
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { style, useStyles } from "purse-styles";
3
+ import { colors } from "../tokens/colors";
4
+ import { radius } from "../tokens/radius";
5
+ export function Badge({ className, children, ...props }) {
6
+ const badgeClassName = useStyles(badgeClass);
7
+ return (_jsx("span", { ...props, className: joinClassNames(badgeClassName, className), children: children }));
8
+ }
9
+ const badgeClass = style(radius.pill, {
10
+ display: "inline-flex",
11
+ alignItems: "center",
12
+ justifyContent: "center",
13
+ minWidth: "20px",
14
+ height: "18px",
15
+ paddingInline: "6px",
16
+ backgroundColor: colors.grayAlpha[3],
17
+ color: colors.gray[11],
18
+ fontFamily: '"SF Compact", "SF Compact Text", "SF Pro Text", system-ui, sans-serif',
19
+ fontSize: "12px",
20
+ fontWeight: 500,
21
+ fontVariantNumeric: "tabular-nums",
22
+ lineHeight: "16px",
23
+ whiteSpace: "nowrap",
24
+ });
25
+ function joinClassNames(...classNames) {
26
+ return classNames.filter(Boolean).join(" ");
27
+ }
@@ -0,0 +1,13 @@
1
+ import React from "react";
2
+ type ButtonAttributes = React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>;
3
+ type ButtonData = {
4
+ id: string;
5
+ focused: boolean;
6
+ };
7
+ type ButtonProps = Omit<ButtonAttributes, "children" | "ref"> & {
8
+ children: React.ReactNode;
9
+ variant?: "default" | "quiet";
10
+ };
11
+ export declare function useButton(props: ButtonProps): [ButtonData, ButtonAttributes];
12
+ export declare function Button(props: ButtonProps): React.JSX.Element;
13
+ export {};
@@ -0,0 +1,112 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import React, { useCallback, useRef } from "react";
3
+ import { useId } from "react-aria";
4
+ import { style, useStyles } from "purse-styles";
5
+ import { useFocus } from "../hooks/useFocus";
6
+ import { useRefCurrent } from "../hooks/useRefCurrent";
7
+ import { backgroundColor } from "../tokens/background";
8
+ import { colors } from "../tokens/colors";
9
+ import { focusRing } from "../tokens/focusRing";
10
+ import { motion } from "../tokens/motion";
11
+ import { shadow, shadowVars } from "../tokens/shadow";
12
+ import { spacing } from "../tokens/spacing";
13
+ import { text } from "../tokens/text";
14
+ const buttonBaseClass = style(text("xs", 400, "highContrast"), focusRing("&:focus-visible", shadowVars.subtle), motion.standard("box-shadow", "background-color", "color"), shadow.subtle, spacing.padding({ x: 6, y: 4 }), {
15
+ display: "inline-flex",
16
+ alignItems: "center",
17
+ justifyContent: "center",
18
+ gap: spacing.value(3),
19
+ borderRadius: "4px",
20
+ width: "fit-content",
21
+ border: "none",
22
+ textOverflow: "ellipsis",
23
+ overflow: "hidden",
24
+ whiteSpace: "nowrap",
25
+ "& > svg": {
26
+ width: "16px",
27
+ height: "16px",
28
+ flexShrink: 0,
29
+ },
30
+ "&:has(> svg)": {
31
+ paddingBlock: spacing.value(3),
32
+ },
33
+ "&:has(> svg:first-child:not(:only-child))": {
34
+ paddingInlineStart: spacing.value(4),
35
+ },
36
+ "&:has(> svg:last-child:not(:only-child))": {
37
+ paddingInlineEnd: spacing.value(4),
38
+ },
39
+ "&:has(> svg:only-child)": {
40
+ paddingInline: spacing.value(3),
41
+ },
42
+ });
43
+ const buttonTextClass = style({
44
+ display: "block",
45
+ minWidth: 0,
46
+ textBox: "trim-both cap alphabetic",
47
+ });
48
+ const buttonClass = style(buttonBaseClass, {
49
+ backgroundColor: backgroundColor.element,
50
+ "&:hover": {
51
+ backgroundColor: backgroundColor.elementHover,
52
+ },
53
+ "&:active": {
54
+ backgroundColor: backgroundColor.elementActive,
55
+ },
56
+ });
57
+ const quietButtonClass = style(buttonBaseClass, focusRing("&:focus-visible"), {
58
+ color: colors.gray[11],
59
+ backgroundColor: "transparent",
60
+ boxShadow: "none",
61
+ "&:hover": {
62
+ color: colors.gray[12],
63
+ backgroundColor: backgroundColor.elementHover,
64
+ },
65
+ });
66
+ export function useButton(props) {
67
+ const ref = useRef(null);
68
+ const id = useId();
69
+ const [focused, focusProps] = useFocus(id, ref);
70
+ const onClickRef = useRefCurrent(props.onClick);
71
+ const onFocusRef = useRefCurrent(props.onFocus);
72
+ const onClick = useCallback((event) => {
73
+ focusProps.onFocus(event);
74
+ onClickRef.current?.(event);
75
+ }, []);
76
+ const onFocus = useCallback((event) => {
77
+ focusProps.onFocus(event);
78
+ onFocusRef.current?.(event);
79
+ }, []);
80
+ return [
81
+ { focused, id },
82
+ { ref, onClick, onFocus },
83
+ ];
84
+ }
85
+ export function Button(props) {
86
+ const { children, className: classNameProp, onClick, onFocus, type = "button", variant = "default", ...buttonProps } = props;
87
+ const [data, attributes] = useButton({ children, onClick, onFocus });
88
+ const className = useStyles(variant === "quiet" ? quietButtonClass : buttonClass);
89
+ const textClassName = useStyles(buttonTextClass);
90
+ const mergedClassName = [className, classNameProp].filter(Boolean).join(" ");
91
+ return (_jsx("button", { ...buttonProps, ...attributes, type: type, className: mergedClassName, children: renderButtonChildren(children, textClassName) }));
92
+ }
93
+ function renderButtonChildren(children, className) {
94
+ const rendered = [];
95
+ let textRun = [];
96
+ const flushTextRun = () => {
97
+ if (textRun.length === 0)
98
+ return;
99
+ rendered.push(_jsx("span", { className: className, children: textRun }, `text-${rendered.length}`));
100
+ textRun = [];
101
+ };
102
+ for (const child of React.Children.toArray(children)) {
103
+ if (typeof child === "string" || typeof child === "number") {
104
+ textRun.push(child);
105
+ continue;
106
+ }
107
+ flushTextRun();
108
+ rendered.push(child);
109
+ }
110
+ flushTextRun();
111
+ return rendered;
112
+ }
@@ -0,0 +1,7 @@
1
+ type CheckboxProps = {
2
+ label?: string;
3
+ checked: boolean;
4
+ setChecked: (checked: boolean) => void;
5
+ };
6
+ export declare function Checkbox(props: CheckboxProps): import("react").JSX.Element;
7
+ export {};
@@ -0,0 +1,54 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useRef } from "react";
3
+ import { useCheckbox } from "react-aria";
4
+ import { useToggleState } from "react-stately";
5
+ import { style, useStyles } from "purse-styles";
6
+ import { colors } from "../tokens/colors";
7
+ import { focusRing } from "../tokens/focusRing";
8
+ import { flex } from "../tokens/layout";
9
+ import { motion } from "../tokens/motion";
10
+ import { radius } from "../tokens/radius";
11
+ import { shadow, shadowVars } from "../tokens/shadow";
12
+ import { visuallyHidden } from "../tokens/visuallyHidden";
13
+ import { labelText } from "./Typography";
14
+ const checkboxClass = style(flex({ align: "center", gap: 3 }), focusRing("& .checkbox-input:focus-visible + .checkbox-toggle", shadowVars.subtle), {
15
+ position: "relative",
16
+ width: "fit-content",
17
+ "&:hover .checkbox-toggle": {
18
+ backgroundColor: colors.gray[8],
19
+ },
20
+ "& .checkbox-input:checked + .checkbox-toggle": {
21
+ backgroundColor: colors.accent[9],
22
+ },
23
+ "& .checkbox-input:checked + .checkbox-toggle .checkbox-icon": {
24
+ opacity: 1,
25
+ transform: "scale(1)",
26
+ },
27
+ });
28
+ const checkboxToggleClass = style(flex({ align: "center", justify: "center" }), radius["2xs"], motion.standard("background-color"), shadow.subtle, {
29
+ pointerEvents: "none",
30
+ width: "14px",
31
+ height: "14px",
32
+ backgroundColor: colors.gray[7],
33
+ });
34
+ const checkboxIconClass = style(motion.standard("opacity", "transform"), {
35
+ width: "10px",
36
+ height: "10px",
37
+ paddingTop: "0.5px",
38
+ paddingLeft: "0.5px",
39
+ fill: colors.gray[3],
40
+ opacity: 0,
41
+ transform: "scale(0)",
42
+ });
43
+ export function Checkbox(props) {
44
+ const { label, checked, setChecked } = props;
45
+ const state = useToggleState({ isSelected: checked, onChange: setChecked });
46
+ const ref = useRef(null);
47
+ const { inputProps } = useCheckbox({ "aria-label": props.label }, state, ref);
48
+ const className = useStyles(checkboxClass);
49
+ const inputClassName = useStyles(visuallyHidden);
50
+ const toggleClassName = useStyles(checkboxToggleClass);
51
+ const iconClassName = useStyles(checkboxIconClass);
52
+ const labelClassName = useStyles(labelText);
53
+ return (_jsxs("label", { className: className, children: [_jsx("input", { ...inputProps, className: `${inputClassName} checkbox-input`, ref: ref }), _jsx("span", { className: `${toggleClassName} checkbox-toggle`, children: _jsx("svg", { focusable: "false", "aria-hidden": "true", className: `${iconClassName} checkbox-icon`, viewBox: "0 0 11 11", children: _jsx("path", { d: "M3.788 9A.999.999 0 0 1 3 8.615l-2.288-3a1 1 0 1 1 1.576-1.23l1.5 1.991 3.924-4.991a1 1 0 1 1 1.576 1.23l-4.712 6A.999.999 0 0 1 3.788 9z" }) }) }), label && _jsx("span", { className: labelClassName, children: label })] }));
54
+ }
@@ -0,0 +1,4 @@
1
+ import { type PopoverProps } from "react-aria-components";
2
+ export interface CollectionPopoverProps extends Omit<PopoverProps, "className"> {
3
+ }
4
+ export declare function CollectionPopover({ placement, offset, ...props }: CollectionPopoverProps): import("react").JSX.Element;
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Popover, } from "react-aria-components";
3
+ import { style, useStyles } from "purse-styles";
4
+ import { backgroundColor } from "../tokens/background";
5
+ import { radius } from "../tokens/radius";
6
+ import { shadow } from "../tokens/shadow";
7
+ import { spacing } from "../tokens/spacing";
8
+ const collectionPopoverStyle = style(radius.sm, shadow.strong, spacing.padding({ y: 1 }), {
9
+ minWidth: "var(--trigger-width)",
10
+ maxHeight: "280px",
11
+ overflow: "hidden",
12
+ background: backgroundColor.element,
13
+ });
14
+ export function CollectionPopover({ placement = "bottom start", offset = 6, ...props }) {
15
+ const className = useStyles(collectionPopoverStyle);
16
+ return (_jsx(Popover, { ...props, className: className, placement: placement, offset: offset }));
17
+ }