@lotics/ui 1.26.0 → 2.0.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 +34 -0
- package/package.json +15 -11
- package/src/app_icon.tsx +59 -0
- package/src/card.tsx +7 -23
- package/src/card_select_item.tsx +52 -0
- package/src/column_filter.tsx +0 -1
- package/src/combobox.tsx +365 -109
- package/src/dynamic_icon.tsx +20 -0
- package/src/dynamic_icon.web.tsx +40 -0
- package/src/legend_item.tsx +2 -2
- package/src/picker.tsx +2 -17
- package/src/picker_menu.tsx +84 -121
- package/src/switcher.tsx +4 -3
- package/src/chart_area.tsx +0 -105
- package/src/chart_bar.tsx +0 -154
- package/src/chart_internals.tsx +0 -43
- package/src/custom_option.test.ts +0 -50
- package/src/custom_option.ts +0 -30
- package/src/search_select.tsx +0 -348
- package/src/select_item.tsx +0 -55
package/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# @lotics/ui
|
|
2
|
+
|
|
3
|
+
Cross-platform (react-native-web) UI primitives with no Lotics domain coupling —
|
|
4
|
+
no i18n, analytics, or domain types. Consumed by `frontend`, `browser_extension`,
|
|
5
|
+
and custom-code apps. Lotics-specific compositions live in `frontend` or
|
|
6
|
+
`@lotics/ui-internal`, not here.
|
|
7
|
+
|
|
8
|
+
The package ships `.tsx` source directly (no build step); consumers import
|
|
9
|
+
subpaths, e.g. `import { Button } from "@lotics/ui/button"`.
|
|
10
|
+
|
|
11
|
+
## Dev harness
|
|
12
|
+
|
|
13
|
+
A standalone Vite app for developing the primitives in isolation — no Expo app,
|
|
14
|
+
no backend.
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm run dev # from packages/ui — serves http://localhost:5173
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
It lives in `dev/`: `main.tsx` mounts a hash-routed sidebar, `registry.ts` lists
|
|
21
|
+
the showcase pages, and `dev/pages/*` are the per-primitive demos. `vite.config.ts`
|
|
22
|
+
wires the react-native-web aliases (the same ones a consuming app's bundler sets up).
|
|
23
|
+
|
|
24
|
+
To add a page: create `dev/pages/<name>.tsx` exporting a component (use the
|
|
25
|
+
`Page`/`Section`/`Row` helpers from `dev/showcase.tsx`) and add one entry to the
|
|
26
|
+
`pages` array in `dev/registry.ts`.
|
|
27
|
+
|
|
28
|
+
## Checks
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm run typecheck # tsgo (covers src + dev)
|
|
32
|
+
npm run lint # oxlint
|
|
33
|
+
npm run test # vitest
|
|
34
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotics/ui",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./tokens": "./src/tokens.ts",
|
|
@@ -35,8 +35,6 @@
|
|
|
35
35
|
"./stacked_progress_bar": "./src/stacked_progress_bar.tsx",
|
|
36
36
|
"./legend_item": "./src/legend_item.tsx",
|
|
37
37
|
"./trend_footer": "./src/trend_footer.tsx",
|
|
38
|
-
"./chart_area": "./src/chart_area.tsx",
|
|
39
|
-
"./chart_bar": "./src/chart_bar.tsx",
|
|
40
38
|
"./spacing": "./src/spacing.ts",
|
|
41
39
|
"./theme": "./src/theme.tsx",
|
|
42
40
|
"./progress_bar": "./src/progress_bar.tsx",
|
|
@@ -48,11 +46,15 @@
|
|
|
48
46
|
"./text": "./src/text.tsx",
|
|
49
47
|
"./activity_indicator": "./src/activity_indicator.tsx",
|
|
50
48
|
"./icon": "./src/icon.tsx",
|
|
49
|
+
"./dynamic_icon": {
|
|
50
|
+
"react-native": "./src/dynamic_icon.tsx",
|
|
51
|
+
"default": "./src/dynamic_icon.web.tsx"
|
|
52
|
+
},
|
|
53
|
+
"./app_icon": "./src/app_icon.tsx",
|
|
51
54
|
"./tooltip": "./src/tooltip.tsx",
|
|
52
55
|
"./button": "./src/button.tsx",
|
|
53
56
|
"./checkbox": "./src/checkbox.tsx",
|
|
54
57
|
"./combobox": "./src/combobox.tsx",
|
|
55
|
-
"./search_select": "./src/search_select.tsx",
|
|
56
58
|
"./portal": "./src/portal.tsx",
|
|
57
59
|
"./popover_nav": "./src/popover_nav.tsx",
|
|
58
60
|
"./popover": "./src/popover.tsx",
|
|
@@ -63,7 +65,7 @@
|
|
|
63
65
|
"./pressable_highlight": "./src/pressable_highlight.tsx",
|
|
64
66
|
"./icon_button": "./src/icon_button.tsx",
|
|
65
67
|
"./info_popover": "./src/info_popover.tsx",
|
|
66
|
-
"./
|
|
68
|
+
"./card_select_item": "./src/card_select_item.tsx",
|
|
67
69
|
"./badge": "./src/badge.tsx",
|
|
68
70
|
"./divider": "./src/divider.tsx",
|
|
69
71
|
"./spacer": "./src/spacer.tsx",
|
|
@@ -179,8 +181,7 @@
|
|
|
179
181
|
"react-dom": "^19.2.0",
|
|
180
182
|
"react-native": ">=0.85.0",
|
|
181
183
|
"react-native-svg": ">=15.0.0",
|
|
182
|
-
"react-native-web": ">=0.20.0"
|
|
183
|
-
"recharts": ">=3.0.0"
|
|
184
|
+
"react-native-web": ">=0.20.0"
|
|
184
185
|
},
|
|
185
186
|
"peerDependenciesMeta": {
|
|
186
187
|
"@lotics/docx": {
|
|
@@ -200,12 +201,10 @@
|
|
|
200
201
|
},
|
|
201
202
|
"react-native-svg": {
|
|
202
203
|
"optional": true
|
|
203
|
-
},
|
|
204
|
-
"recharts": {
|
|
205
|
-
"optional": true
|
|
206
204
|
}
|
|
207
205
|
},
|
|
208
206
|
"scripts": {
|
|
207
|
+
"dev": "vite",
|
|
209
208
|
"typecheck": "tsgo --noEmit",
|
|
210
209
|
"lint": "oxlint",
|
|
211
210
|
"test": "vitest run"
|
|
@@ -213,6 +212,11 @@
|
|
|
213
212
|
"devDependencies": {
|
|
214
213
|
"@lotics/docx": "^0.1.0",
|
|
215
214
|
"@lotics/xlsx": "^0.1.0",
|
|
216
|
-
"
|
|
215
|
+
"@types/react-dom": "~19.2.2",
|
|
216
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
217
|
+
"lucide-react": "^0.562.0",
|
|
218
|
+
"react-dom": "^19.2.0",
|
|
219
|
+
"react-native-web": "^0.21.0",
|
|
220
|
+
"vite": "^7.2.4"
|
|
217
221
|
}
|
|
218
222
|
}
|
package/src/app_icon.tsx
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { View, StyleSheet } from "react-native";
|
|
2
|
+
import { DynamicIcon } from "./dynamic_icon";
|
|
3
|
+
import { colors } from "./colors";
|
|
4
|
+
|
|
5
|
+
interface AppIconProps {
|
|
6
|
+
/**
|
|
7
|
+
* The app's icon — a Lucide icon name (the full library, via `DynamicIcon`).
|
|
8
|
+
* Empty/absent renders the neutral default.
|
|
9
|
+
*/
|
|
10
|
+
icon?: string | null;
|
|
11
|
+
/** Palette token name (e.g. "blue"); unknown/empty → neutral zinc. */
|
|
12
|
+
themeColor?: string | null;
|
|
13
|
+
size?: "sm" | "md";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const DEFAULT_ICON = "layout-grid";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The app's launcher tile: a brand-tinted square holding the app's Lucide icon
|
|
20
|
+
* (full library) or its uploaded image. Shared by every surface that shows an app
|
|
21
|
+
* (launcher, list, picker) — frontend and extension alike.
|
|
22
|
+
*/
|
|
23
|
+
export function AppIcon({ icon, themeColor, size = "sm" }: AppIconProps) {
|
|
24
|
+
const gradientStart = shade(themeColor, "400") ?? colors.zinc["400"];
|
|
25
|
+
const gradientEnd = shade(themeColor, "500") ?? colors.zinc["500"];
|
|
26
|
+
const iconColor = themeColor ? colors.white : colors.zinc["900"];
|
|
27
|
+
const isMd = size === "md";
|
|
28
|
+
const iconSize = isMd ? 20 : 16;
|
|
29
|
+
const name = icon && icon.trim() !== "" ? icon : DEFAULT_ICON;
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<View
|
|
33
|
+
style={[
|
|
34
|
+
styles.container,
|
|
35
|
+
isMd ? styles.md : styles.sm,
|
|
36
|
+
// Solid color is the cross-platform base; web layers the gradient on top.
|
|
37
|
+
{ backgroundColor: gradientEnd, backgroundImage: `linear-gradient(135deg, ${gradientStart} 0%, ${gradientEnd} 100%)` },
|
|
38
|
+
]}
|
|
39
|
+
>
|
|
40
|
+
<DynamicIcon name={name} size={iconSize} color={iconColor} />
|
|
41
|
+
</View>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Safe palette-token → shade lookup (mirrors the option-color resolver). */
|
|
46
|
+
function shade(token: string | null | undefined, level: "400" | "500"): string | undefined {
|
|
47
|
+
if (!token) return undefined;
|
|
48
|
+
const palette = colors[token as keyof typeof colors];
|
|
49
|
+
if (palette && typeof palette === "object" && level in palette) {
|
|
50
|
+
return (palette as Record<string, string>)[level];
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const styles = StyleSheet.create({
|
|
56
|
+
container: { borderRadius: 6, alignItems: "center", justifyContent: "center", overflow: "hidden" },
|
|
57
|
+
sm: { width: 32, height: 32 },
|
|
58
|
+
md: { width: 40, height: 40, borderRadius: 8, boxShadow: "0px 2px 4px rgba(0, 0, 0, 0.1)" },
|
|
59
|
+
});
|
package/src/card.tsx
CHANGED
|
@@ -1,32 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { StyleProp, StyleSheet, View, ViewStyle } from "react-native";
|
|
2
2
|
import { colors } from "./colors";
|
|
3
3
|
|
|
4
4
|
interface CardProps {
|
|
5
5
|
children: React.ReactNode;
|
|
6
6
|
testID?: string;
|
|
7
|
-
onPress?: () => void;
|
|
8
7
|
style?: StyleProp<ViewStyle>;
|
|
9
8
|
}
|
|
10
9
|
|
|
10
|
+
/**
|
|
11
|
+
* A bordered presentational surface — view only. It never handles interaction:
|
|
12
|
+
* for a pressable/selectable card use CardSelectItem (a real focusable button),
|
|
13
|
+
* or compose PressableHighlight for a bespoke action.
|
|
14
|
+
*/
|
|
11
15
|
export function Card(props: CardProps) {
|
|
12
|
-
const { children, testID,
|
|
13
|
-
|
|
14
|
-
if (onPress) {
|
|
15
|
-
return (
|
|
16
|
-
<Pressable
|
|
17
|
-
testID={testID}
|
|
18
|
-
onPress={() => {
|
|
19
|
-
onPress();
|
|
20
|
-
}}
|
|
21
|
-
style={(state) => {
|
|
22
|
-
const hovered = (state as { hovered?: boolean }).hovered;
|
|
23
|
-
return [styles.container, hovered && styles.hovered, style];
|
|
24
|
-
}}
|
|
25
|
-
>
|
|
26
|
-
{children}
|
|
27
|
-
</Pressable>
|
|
28
|
-
);
|
|
29
|
-
}
|
|
16
|
+
const { children, testID, style } = props;
|
|
30
17
|
|
|
31
18
|
return (
|
|
32
19
|
<View testID={testID} style={[styles.container, style]}>
|
|
@@ -55,7 +42,4 @@ const styles = StyleSheet.create({
|
|
|
55
42
|
"0 1px 2px 0 rgba(38,38,38,0.06), 0 4px 12px -2px rgba(38,38,38,0.06)",
|
|
56
43
|
} as ViewStyle),
|
|
57
44
|
},
|
|
58
|
-
hovered: {
|
|
59
|
-
borderColor: colors.zinc["900"],
|
|
60
|
-
},
|
|
61
45
|
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { PressableStateCallbackType, StyleProp, StyleSheet, ViewStyle } from "react-native";
|
|
2
|
+
import { colors } from "./colors";
|
|
3
|
+
import { PressableHighlight } from "./pressable_highlight";
|
|
4
|
+
|
|
5
|
+
interface CardSelectItemProps {
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
onPress: () => void;
|
|
8
|
+
testID?: string;
|
|
9
|
+
style?: StyleProp<ViewStyle>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A bordered, card-shaped button — the selectable row used on auth screens
|
|
14
|
+
* (organization picker, login choices). Always interactive: a real focusable
|
|
15
|
+
* `button` that shows a 2px ring on hover/press, matching the global keyboard
|
|
16
|
+
* focus ring. For a static surface use Card instead.
|
|
17
|
+
*/
|
|
18
|
+
export function CardSelectItem(props: CardSelectItemProps) {
|
|
19
|
+
const { children, onPress, testID, style } = props;
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<PressableHighlight
|
|
23
|
+
testID={testID}
|
|
24
|
+
accessibilityRole="button"
|
|
25
|
+
onPress={onPress}
|
|
26
|
+
style={(state: PressableStateCallbackType) => {
|
|
27
|
+
const hovered = (state as { hovered?: boolean }).hovered;
|
|
28
|
+
const active = hovered || state.pressed;
|
|
29
|
+
return [styles.container, active && styles.ring, style];
|
|
30
|
+
}}
|
|
31
|
+
>
|
|
32
|
+
{children}
|
|
33
|
+
</PressableHighlight>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const styles = StyleSheet.create({
|
|
38
|
+
container: {
|
|
39
|
+
padding: 16,
|
|
40
|
+
borderRadius: 8,
|
|
41
|
+
backgroundColor: colors.background,
|
|
42
|
+
borderColor: colors.border,
|
|
43
|
+
borderWidth: 1,
|
|
44
|
+
},
|
|
45
|
+
// A 2px ring flush against the border (spread 2, no offset/blur) — the same
|
|
46
|
+
// weight and color as the global `:focus-visible` outline, so hover, press,
|
|
47
|
+
// and keyboard focus all read as one consistent ring rather than a thin
|
|
48
|
+
// border-darken. boxShadow keeps it layout-neutral (no 1px→2px reflow).
|
|
49
|
+
ring: {
|
|
50
|
+
...({ boxShadow: `0 0 0 2px ${colors.zinc["900"]}` } as ViewStyle),
|
|
51
|
+
},
|
|
52
|
+
});
|
package/src/column_filter.tsx
CHANGED
|
@@ -155,7 +155,6 @@ export function ColumnFilter(props: ColumnFilterProps) {
|
|
|
155
155
|
) : (
|
|
156
156
|
<PickerMenu
|
|
157
157
|
multi
|
|
158
|
-
enableSearch
|
|
159
158
|
options={column.options ?? []}
|
|
160
159
|
value={value?.kind === "select" ? value.selected : []}
|
|
161
160
|
onValueChange={(selected) => onChange({ kind: "select", selected })}
|