tinywidgets 0.0.10 → 1.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/.eslintrc.json +2 -1
- package/bun.lockb +0 -0
- package/package.json +19 -14
- package/src/common/functions.tsx +54 -0
- package/src/components/App/index.css.ts +81 -0
- package/src/components/App/index.tsx +177 -0
- package/src/components/Button/index.css.ts +66 -0
- package/src/components/Button/index.tsx +206 -0
- package/src/components/Card/index.css.ts +12 -0
- package/src/components/Card/index.tsx +34 -0
- package/src/components/Code/index.css.ts +49 -0
- package/src/components/Code/index.tsx +89 -0
- package/src/components/Collapsible/index.css.ts +37 -0
- package/src/components/Collapsible/index.tsx +132 -0
- package/src/{Detail → components/Detail}/index.css.ts +3 -3
- package/src/components/Detail/index.tsx +48 -0
- package/src/{Hr → components/Hr}/index.css.ts +3 -2
- package/src/components/Hr/index.tsx +26 -0
- package/src/components/Image/index.css.ts +38 -0
- package/src/components/Image/index.tsx +85 -0
- package/src/components/Metric/index.tsx +59 -0
- package/src/components/Row/index.css.ts +30 -0
- package/src/components/Row/index.tsx +104 -0
- package/src/components/Summary/index.css.ts +16 -0
- package/src/components/Summary/index.tsx +59 -0
- package/src/{Tag → components/Tag}/index.css.ts +9 -6
- package/src/components/Tag/index.tsx +73 -0
- package/src/css/code.css.ts +112 -0
- package/src/css/colors.css.ts +124 -0
- package/src/css/dimensions.css.ts +67 -0
- package/src/css/global.css.ts +11 -0
- package/src/css/screens.ts +15 -0
- package/src/index.css.ts +4 -137
- package/src/index.ts +16 -24
- package/src/stores/LocalStore.tsx +52 -19
- package/src/stores/RouteStore.tsx +103 -0
- package/src/stores/SessionStore.tsx +40 -25
- package/src/stores/common.ts +6 -0
- package/tsconfig.json +19 -0
- package/.prettierrc +0 -5
- package/index.css.ts +0 -1
- package/index.ts +0 -12
- package/src/App/index.tsx +0 -18
- package/src/Avatar/index.css.ts +0 -17
- package/src/Avatar/index.tsx +0 -25
- package/src/Axis/index.css.ts +0 -19
- package/src/Axis/index.tsx +0 -36
- package/src/Button/index.css.ts +0 -57
- package/src/Button/index.tsx +0 -63
- package/src/Card/index.css.ts +0 -9
- package/src/Card/index.tsx +0 -13
- package/src/Collapsible/index.css.ts +0 -34
- package/src/Collapsible/index.tsx +0 -75
- package/src/Detail/index.tsx +0 -26
- package/src/Hr/index.tsx +0 -8
- package/src/Metric/index.tsx +0 -26
- package/src/Summary/index.css.ts +0 -17
- package/src/Summary/index.tsx +0 -32
- package/src/Tag/index.tsx +0 -26
- package/src/Ui/Header/DarkButton/index.tsx +0 -19
- package/src/Ui/Header/SideNav/index.css.ts +0 -23
- package/src/Ui/Header/SideNav/index.tsx +0 -14
- package/src/Ui/Header/SideNavButton/index.css.ts +0 -4
- package/src/Ui/Header/SideNavButton/index.tsx +0 -19
- package/src/Ui/Header/Title/index.css.ts +0 -10
- package/src/Ui/Header/Title/index.tsx +0 -9
- package/src/Ui/Header/TopNav/index.css.ts +0 -9
- package/src/Ui/Header/TopNav/index.tsx +0 -18
- package/src/Ui/Header/index.css.ts +0 -18
- package/src/Ui/Header/index.tsx +0 -32
- package/src/Ui/Main/Article/index.css.ts +0 -13
- package/src/Ui/Main/Article/index.tsx +0 -9
- package/src/Ui/Main/Footer/index.css.ts +0 -12
- package/src/Ui/Main/Footer/index.tsx +0 -9
- package/src/Ui/Main/index.css.ts +0 -16
- package/src/Ui/Main/index.tsx +0 -24
- package/src/Ui/index.css.ts +0 -9
- package/src/Ui/index.tsx +0 -50
- package/utils.ts +0 -1
- /package/src/{Metric → components/Metric}/index.css.ts +0 -0
package/.eslintrc.json
CHANGED
package/bun.lockb
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,33 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tinywidgets",
|
|
3
|
-
"
|
|
3
|
+
"description": "A collection of tiny, reusable, React components",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"prePublishPackage": "eslint . && tsc"
|
|
7
|
+
},
|
|
4
8
|
"author": "jamesgpearce",
|
|
5
9
|
"repository": "github:tinyplex/tinywidgets",
|
|
6
|
-
"module": "index.ts",
|
|
10
|
+
"module": "./src/index.ts",
|
|
7
11
|
"devDependencies": {
|
|
8
|
-
"@types/react": "^18.3.
|
|
12
|
+
"@types/react": "^18.3.8",
|
|
9
13
|
"@types/react-dom": "^18.3.0",
|
|
10
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
11
|
-
"@typescript-eslint/parser": "^8.
|
|
14
|
+
"@typescript-eslint/eslint-plugin": "^8.7.0",
|
|
15
|
+
"@typescript-eslint/parser": "^8.7.0",
|
|
12
16
|
"eslint": "^8.57.0",
|
|
13
17
|
"eslint-config-prettier": "^9.1.0",
|
|
14
18
|
"eslint-plugin-react": "^7.36.1",
|
|
15
19
|
"eslint-plugin-react-hooks": "^4.6.2",
|
|
16
20
|
"prettier": "^3.3.3",
|
|
17
21
|
"typescript": "^5.6.2",
|
|
18
|
-
"@vanilla-extract/css": "^1.15.5"
|
|
19
|
-
"react": "^18.3.1",
|
|
20
|
-
"react-dom": "^18.3.1",
|
|
21
|
-
"tinybase": "^5.3.0-beta.3"
|
|
22
|
+
"@vanilla-extract/css": "^1.15.5"
|
|
22
23
|
},
|
|
23
24
|
"exports": {
|
|
24
|
-
".": "./index.ts",
|
|
25
|
-
"./
|
|
26
|
-
"./css": "./index.css.ts"
|
|
25
|
+
".": "./src/index.ts",
|
|
26
|
+
"./css": "./src/index.css.ts"
|
|
27
27
|
},
|
|
28
|
-
"description": "reserved",
|
|
29
28
|
"license": "MIT",
|
|
30
29
|
"dependencies": {
|
|
31
|
-
"
|
|
30
|
+
"eslint-plugin-react-refresh": "^0.4.12",
|
|
31
|
+
"lucide-react": "^0.445.0",
|
|
32
|
+
"prism-react-renderer": "^2.4.0",
|
|
33
|
+
"prismjs": "^1.29.0",
|
|
34
|
+
"react": "^18.3.1",
|
|
35
|
+
"react-dom": "^18.3.1",
|
|
36
|
+
"tinybase": "^5.3.1"
|
|
32
37
|
}
|
|
33
38
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React, {type ComponentType, type ReactNode} from 'react';
|
|
2
|
+
import type {StyleRule} from '@vanilla-extract/css';
|
|
3
|
+
import {screens} from '../css/screens';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The `classNames` function returns a concatenated list of class names,
|
|
7
|
+
* filtering out any `null` or `undefined` values.
|
|
8
|
+
*
|
|
9
|
+
* This allows you to easily toggle classes based on conditions.
|
|
10
|
+
*
|
|
11
|
+
* @returns A string of space-separated class names.
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* const classes = classNames(
|
|
15
|
+
* 'class1',
|
|
16
|
+
* false && 'class2',
|
|
17
|
+
* true && 'class3',
|
|
18
|
+
* );
|
|
19
|
+
* // ...
|
|
20
|
+
* <Card className={classes}>
|
|
21
|
+
* <code>{classes}</code>
|
|
22
|
+
* </Card>
|
|
23
|
+
* ```
|
|
24
|
+
* This example shows the function returning a class name list.
|
|
25
|
+
*/
|
|
26
|
+
export const classNames = (
|
|
27
|
+
...classes: (string | boolean | null | undefined)[]
|
|
28
|
+
) => classes.filter(Boolean).join(' ');
|
|
29
|
+
|
|
30
|
+
export const renderComponentOrNode = (
|
|
31
|
+
ComponentOrNode: ComponentType | ReactNode,
|
|
32
|
+
fallback: ReactNode = null,
|
|
33
|
+
) =>
|
|
34
|
+
ComponentOrNode instanceof Function ? (
|
|
35
|
+
<ComponentOrNode />
|
|
36
|
+
) : (
|
|
37
|
+
(ComponentOrNode ?? fallback)
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
export const large = (style: StyleRule) => ({
|
|
41
|
+
'@media': {[`screen and (min-width: ${screens.large}px)`]: style},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export const notLarge = (style: StyleRule) => ({
|
|
45
|
+
'@media': {[`screen and (max-width: ${screens.large}px)`]: style},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export const small = (style: StyleRule) => ({
|
|
49
|
+
'@media': {[`screen and (max-width: ${screens.small}px)`]: style},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export const notSmall = (style: StyleRule) => ({
|
|
53
|
+
'@media': {[`screen and (min-width: ${screens.small}px)`]: style},
|
|
54
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import {dimensions, dimensionsClass} from '../../css/dimensions.css.ts';
|
|
2
|
+
import {colors} from '../../css/colors.css.ts';
|
|
3
|
+
import {large} from '../../common/functions.tsx';
|
|
4
|
+
import {style} from '@vanilla-extract/css';
|
|
5
|
+
|
|
6
|
+
export const app = style([dimensionsClass, {color: colors.foreground}]);
|
|
7
|
+
|
|
8
|
+
export const appLayout = style({
|
|
9
|
+
display: 'flex',
|
|
10
|
+
height: '100dvh',
|
|
11
|
+
width: '100dvw',
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export const header = style({
|
|
15
|
+
display: 'flex',
|
|
16
|
+
justifyContent: 'space-between',
|
|
17
|
+
alignItems: 'center',
|
|
18
|
+
gap: dimensions.padding,
|
|
19
|
+
padding: dimensions.padding,
|
|
20
|
+
position: 'fixed',
|
|
21
|
+
boxShadow: colors.shadow,
|
|
22
|
+
backdropFilter: 'blur(8px)',
|
|
23
|
+
backgroundColor: colors.backgroundHaze,
|
|
24
|
+
left: 0,
|
|
25
|
+
right: 0,
|
|
26
|
+
height: dimensions.topNavHeight,
|
|
27
|
+
borderBottom: colors.border,
|
|
28
|
+
zIndex: 2,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export const topNav = style({
|
|
32
|
+
display: 'flex',
|
|
33
|
+
justifyContent: 'space-between',
|
|
34
|
+
alignItems: 'center',
|
|
35
|
+
gap: dimensions.padding,
|
|
36
|
+
|
|
37
|
+
flex: 1,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export const sideNavButton = style(large({display: 'none'}));
|
|
41
|
+
|
|
42
|
+
export const title = style({
|
|
43
|
+
display: 'flex',
|
|
44
|
+
alignItems: 'center',
|
|
45
|
+
gap: dimensions.padding,
|
|
46
|
+
...large({
|
|
47
|
+
width: `calc(${dimensions.sideNavWidth} - 2 * ${dimensions.padding})`,
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export const sideNav = style({
|
|
52
|
+
position: 'fixed',
|
|
53
|
+
padding: dimensions.padding,
|
|
54
|
+
backgroundColor: colors.background2,
|
|
55
|
+
overflow: 'auto',
|
|
56
|
+
borderRight: colors.border,
|
|
57
|
+
width: dimensions.sideNavWidth,
|
|
58
|
+
bottom: 0,
|
|
59
|
+
left: `calc(-1.2 * ${dimensions.sideNavWidth})`,
|
|
60
|
+
top: dimensions.topNavHeight,
|
|
61
|
+
height: `calc(100dvh - ${dimensions.topNavHeight})`,
|
|
62
|
+
overscrollBehavior: 'contain',
|
|
63
|
+
transition: 'left .2s ease-in-out',
|
|
64
|
+
...large({left: 0}),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export const sideNavOpen = style({left: 0});
|
|
68
|
+
|
|
69
|
+
export const main = style({
|
|
70
|
+
flex: 1,
|
|
71
|
+
backgroundColor: colors.background,
|
|
72
|
+
overflow: 'auto',
|
|
73
|
+
padding: dimensions.padding,
|
|
74
|
+
paddingTop: `calc(${dimensions.topNavHeight} + ${dimensions.padding})`,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
export const mainHasSideNav = style(
|
|
78
|
+
large({
|
|
79
|
+
paddingLeft: `calc(${dimensions.sideNavWidth} + ${dimensions.padding})`,
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import type {ComponentType, ReactNode} from 'react';
|
|
2
|
+
import {
|
|
3
|
+
LocalStore,
|
|
4
|
+
useDark,
|
|
5
|
+
useDarkChoice,
|
|
6
|
+
useLocalStoreIsReady,
|
|
7
|
+
useToggleDarkChoiceCallback,
|
|
8
|
+
} from '../../stores/LocalStore.tsx';
|
|
9
|
+
import {Menu, Moon, Sun, SunMoon, X} from 'lucide-react';
|
|
10
|
+
import {RouteStore, useRouteStoreIsReady} from '../../stores/RouteStore.tsx';
|
|
11
|
+
import {
|
|
12
|
+
SessionStore,
|
|
13
|
+
useSessionStoreIsReady,
|
|
14
|
+
useSideNavIsOpen,
|
|
15
|
+
useToggleSideNavIsOpenCallback,
|
|
16
|
+
} from '../../stores/SessionStore.tsx';
|
|
17
|
+
import {
|
|
18
|
+
app,
|
|
19
|
+
appLayout,
|
|
20
|
+
header,
|
|
21
|
+
main,
|
|
22
|
+
mainHasSideNav,
|
|
23
|
+
sideNav,
|
|
24
|
+
sideNavButton,
|
|
25
|
+
sideNavOpen,
|
|
26
|
+
title,
|
|
27
|
+
topNav,
|
|
28
|
+
} from './index.css.ts';
|
|
29
|
+
import {classNames, renderComponentOrNode} from '../../common/functions.tsx';
|
|
30
|
+
import {codeDark, codeLight} from '../../css/code.css.ts';
|
|
31
|
+
import {colorsDark, colorsLight} from '../../css/colors.css.ts';
|
|
32
|
+
import {Button} from '../Button/index.tsx';
|
|
33
|
+
import {Provider} from 'tinybase/ui-react';
|
|
34
|
+
import React from 'react';
|
|
35
|
+
|
|
36
|
+
const darkIcons = [Sun, Moon, SunMoon];
|
|
37
|
+
const darkChoices = ['Light always', 'Dark always', 'Auto'];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The `App` component is the root component of a TinyWidgets application.
|
|
41
|
+
*
|
|
42
|
+
* It can be provided with optional components for different parts of the app
|
|
43
|
+
* layout, including the top left title, the side bar and the main content.
|
|
44
|
+
*
|
|
45
|
+
* If none of these props are present, the TinyWidgets layout won't be used, but
|
|
46
|
+
* you will still be able to enjoy its state management features, and any
|
|
47
|
+
* components within it will be correctly styled.
|
|
48
|
+
* @param props The props for the component.
|
|
49
|
+
* @returns The App component.
|
|
50
|
+
* @example
|
|
51
|
+
* ```tsx
|
|
52
|
+
* <App />
|
|
53
|
+
* ```
|
|
54
|
+
* This shows an empty App, but if you visit [the TinyWidgets
|
|
55
|
+
* website](https://tinywidgets.org) you'll see one in its full glory!
|
|
56
|
+
* @icon Lucide.PanelsTopLeft
|
|
57
|
+
*/
|
|
58
|
+
export const App = (props: {
|
|
59
|
+
/**
|
|
60
|
+
* An optional component, element, or string which renders the top left title
|
|
61
|
+
* of the application.
|
|
62
|
+
*/
|
|
63
|
+
readonly title?: ComponentType | ReactNode;
|
|
64
|
+
/**
|
|
65
|
+
* An optional component, element, or string which renders navigation on the
|
|
66
|
+
* left side of the top navigation bar of the application.
|
|
67
|
+
*/
|
|
68
|
+
readonly topNavLeft?: ComponentType | ReactNode;
|
|
69
|
+
/**
|
|
70
|
+
* An optional component, element, or string which renders navigation on the
|
|
71
|
+
* right side of the top navigation bar of the application.
|
|
72
|
+
*/
|
|
73
|
+
readonly topNavRight?: ComponentType | ReactNode;
|
|
74
|
+
/**
|
|
75
|
+
* An optional component, element, or string which renders the left side bar
|
|
76
|
+
* of the application.
|
|
77
|
+
*/
|
|
78
|
+
readonly sideNav?: ComponentType | ReactNode;
|
|
79
|
+
/**
|
|
80
|
+
* An optional component, element, or string which renders the main part of
|
|
81
|
+
* the application.
|
|
82
|
+
*/
|
|
83
|
+
readonly main?: ComponentType | ReactNode;
|
|
84
|
+
/**
|
|
85
|
+
* An extra CSS class name for the component.
|
|
86
|
+
*/
|
|
87
|
+
readonly className?: string;
|
|
88
|
+
}) => {
|
|
89
|
+
return (
|
|
90
|
+
<Provider>
|
|
91
|
+
<Layout {...props} />
|
|
92
|
+
<SessionStore />
|
|
93
|
+
<LocalStore />
|
|
94
|
+
<RouteStore />
|
|
95
|
+
</Provider>
|
|
96
|
+
);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const Layout = ({
|
|
100
|
+
title: titleComponentOrNode,
|
|
101
|
+
topNavLeft: topNavLeftComponentOrNode,
|
|
102
|
+
topNavRight: topNavRightComponentOrNode,
|
|
103
|
+
sideNav: sideNavComponentOrNode,
|
|
104
|
+
main: mainComponentOrNode,
|
|
105
|
+
className,
|
|
106
|
+
}: Parameters<typeof App>[0]) => {
|
|
107
|
+
const sessionStoreIsReady = useSessionStoreIsReady();
|
|
108
|
+
const routeStoreIsReady = useRouteStoreIsReady();
|
|
109
|
+
const localStoreIsReady = useLocalStoreIsReady();
|
|
110
|
+
|
|
111
|
+
const toggleDarkChoice = useToggleDarkChoiceCallback();
|
|
112
|
+
|
|
113
|
+
const toggleSideNavIsOpen = useToggleSideNavIsOpenCallback();
|
|
114
|
+
const sideNavIsOpen = useSideNavIsOpen();
|
|
115
|
+
|
|
116
|
+
const darkChoice = useDarkChoice();
|
|
117
|
+
const dark = useDark();
|
|
118
|
+
|
|
119
|
+
const hasLayout = [
|
|
120
|
+
titleComponentOrNode,
|
|
121
|
+
topNavLeftComponentOrNode,
|
|
122
|
+
topNavRightComponentOrNode,
|
|
123
|
+
sideNavComponentOrNode,
|
|
124
|
+
mainComponentOrNode,
|
|
125
|
+
].some((componentOrNode) => componentOrNode);
|
|
126
|
+
const hasSideNav = sideNavComponentOrNode != null;
|
|
127
|
+
|
|
128
|
+
return sessionStoreIsReady && routeStoreIsReady && localStoreIsReady ? (
|
|
129
|
+
<div
|
|
130
|
+
className={classNames(
|
|
131
|
+
app,
|
|
132
|
+
hasLayout && appLayout,
|
|
133
|
+
dark ? colorsDark : colorsLight,
|
|
134
|
+
dark ? codeDark : codeLight,
|
|
135
|
+
className,
|
|
136
|
+
)}
|
|
137
|
+
>
|
|
138
|
+
{hasLayout ? (
|
|
139
|
+
<>
|
|
140
|
+
<header className={header}>
|
|
141
|
+
{hasSideNav ? (
|
|
142
|
+
<Button
|
|
143
|
+
variant="icon"
|
|
144
|
+
onClick={toggleSideNavIsOpen}
|
|
145
|
+
icon={sideNavIsOpen ? X : Menu}
|
|
146
|
+
className={sideNavButton}
|
|
147
|
+
/>
|
|
148
|
+
) : null}
|
|
149
|
+
<nav className={title}>
|
|
150
|
+
{renderComponentOrNode(titleComponentOrNode)}
|
|
151
|
+
</nav>
|
|
152
|
+
<nav className={topNav}>
|
|
153
|
+
{renderComponentOrNode(topNavLeftComponentOrNode, <div />)}
|
|
154
|
+
{renderComponentOrNode(topNavRightComponentOrNode, <div />)}
|
|
155
|
+
</nav>
|
|
156
|
+
<Button
|
|
157
|
+
variant="icon"
|
|
158
|
+
onClick={toggleDarkChoice}
|
|
159
|
+
icon={darkIcons[darkChoice]}
|
|
160
|
+
alt={darkChoices[darkChoice]}
|
|
161
|
+
/>
|
|
162
|
+
{hasSideNav ? (
|
|
163
|
+
<nav
|
|
164
|
+
className={classNames(sideNav, sideNavIsOpen && sideNavOpen)}
|
|
165
|
+
>
|
|
166
|
+
{renderComponentOrNode(sideNavComponentOrNode)}
|
|
167
|
+
</nav>
|
|
168
|
+
) : null}
|
|
169
|
+
</header>
|
|
170
|
+
<main className={classNames(main, hasSideNav && mainHasSideNav)}>
|
|
171
|
+
{renderComponentOrNode(mainComponentOrNode)}
|
|
172
|
+
</main>
|
|
173
|
+
</>
|
|
174
|
+
) : null}
|
|
175
|
+
</div>
|
|
176
|
+
) : null;
|
|
177
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {style, styleVariants} from '@vanilla-extract/css';
|
|
2
|
+
import {colors} from '../../css/colors.css';
|
|
3
|
+
import {dimensions} from '../../css/dimensions.css';
|
|
4
|
+
|
|
5
|
+
export const button = style({
|
|
6
|
+
display: 'inline-flex',
|
|
7
|
+
justifyContent: 'space-between',
|
|
8
|
+
alignItems: 'center',
|
|
9
|
+
gap: dimensions.padding,
|
|
10
|
+
borderRadius: dimensions.radius,
|
|
11
|
+
textAlign: 'left',
|
|
12
|
+
cursor: 'pointer',
|
|
13
|
+
padding: '0.5rem 1rem',
|
|
14
|
+
outlineOffset: '-2px',
|
|
15
|
+
color: 'inherit',
|
|
16
|
+
overflow: 'hidden',
|
|
17
|
+
whiteSpace: 'nowrap',
|
|
18
|
+
transition: 'background-color 0.1s,border-color 0.1s',
|
|
19
|
+
flexShrink: 0,
|
|
20
|
+
border: 0,
|
|
21
|
+
alignSelf: 'center',
|
|
22
|
+
background: 'none',
|
|
23
|
+
selectors: {
|
|
24
|
+
'&:hover': {
|
|
25
|
+
backgroundColor: colors.backgroundHover,
|
|
26
|
+
color: colors.foregroundBright,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export const buttonVariants = styleVariants({
|
|
32
|
+
default: {
|
|
33
|
+
boxShadow: colors.shadow,
|
|
34
|
+
border: colors.border,
|
|
35
|
+
backgroundColor: colors.background,
|
|
36
|
+
},
|
|
37
|
+
accent: {
|
|
38
|
+
boxShadow: colors.shadow,
|
|
39
|
+
backgroundColor: colors.accent,
|
|
40
|
+
color: colors.accentContrast,
|
|
41
|
+
selectors: {
|
|
42
|
+
'&:hover': {
|
|
43
|
+
backgroundColor: colors.accentHover,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
ghost: {},
|
|
48
|
+
item: {width: '100%'},
|
|
49
|
+
icon: {padding: '0.25rem'},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export const currentStyle = style({
|
|
53
|
+
backgroundColor: colors.backgroundHover,
|
|
54
|
+
color: colors.foregroundBright,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export const titleStyle = style({
|
|
58
|
+
flex: '1 1 auto',
|
|
59
|
+
overflow: 'hidden',
|
|
60
|
+
textOverflow: 'ellipsis',
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export const titleStyleRight = style({
|
|
64
|
+
flex: '0 0 auto',
|
|
65
|
+
overflow: 'hidden',
|
|
66
|
+
});
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import type {ComponentType, ReactNode, Ref} from 'react';
|
|
2
|
+
import React, {forwardRef, useCallback} from 'react';
|
|
3
|
+
import {
|
|
4
|
+
button,
|
|
5
|
+
buttonVariants,
|
|
6
|
+
currentStyle,
|
|
7
|
+
titleStyle,
|
|
8
|
+
titleStyleRight,
|
|
9
|
+
} from './index.css.ts';
|
|
10
|
+
import {classNames, renderComponentOrNode} from '../../common/functions.tsx';
|
|
11
|
+
import {iconSize} from '../../css/dimensions.css.ts';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The `Button` component displays an button, with a number of common variants.
|
|
15
|
+
*
|
|
16
|
+
* @param props The props for the component.
|
|
17
|
+
* @returns The Button component.
|
|
18
|
+
* @example
|
|
19
|
+
* ```tsx
|
|
20
|
+
* <Button
|
|
21
|
+
* title="TinyWidgets"
|
|
22
|
+
* onClick={() => alert('Clicked!')}
|
|
23
|
+
* />
|
|
24
|
+
* ```
|
|
25
|
+
* This example shows the `default` variant of the Button component.
|
|
26
|
+
* @example
|
|
27
|
+
* ```tsx
|
|
28
|
+
* <Button
|
|
29
|
+
* title="TinyWidgets"
|
|
30
|
+
* icon={Lucide.Grid3x3}
|
|
31
|
+
* />
|
|
32
|
+
* ```
|
|
33
|
+
* This example shows the `default` variant of the Button component with a left
|
|
34
|
+
* icon.
|
|
35
|
+
* @example
|
|
36
|
+
* ```tsx
|
|
37
|
+
* <Button
|
|
38
|
+
* title="TinyWidgets"
|
|
39
|
+
* iconRight={Lucide.LogIn}
|
|
40
|
+
* />
|
|
41
|
+
* ```
|
|
42
|
+
* This example shows the `default` variant of the Button component with a right
|
|
43
|
+
* icon.
|
|
44
|
+
* @example
|
|
45
|
+
* ```tsx
|
|
46
|
+
* <Button
|
|
47
|
+
* icon={Lucide.Sun}
|
|
48
|
+
* variant="icon"
|
|
49
|
+
* />
|
|
50
|
+
* ```
|
|
51
|
+
* This example shows the `icon` variant of the Button component. It is best
|
|
52
|
+
* used without a title.
|
|
53
|
+
* @example
|
|
54
|
+
* ```tsx
|
|
55
|
+
* <Button
|
|
56
|
+
* title="TinyWidgets"
|
|
57
|
+
* variant="accent"
|
|
58
|
+
* />
|
|
59
|
+
* ```
|
|
60
|
+
* This example shows the `accent` variant of the Button component.
|
|
61
|
+
* @example
|
|
62
|
+
* ```tsx
|
|
63
|
+
* <Button
|
|
64
|
+
* title="tinybase.org"
|
|
65
|
+
* iconRight={Lucide.SquareArrowOutUpRight}
|
|
66
|
+
* variant="accent"
|
|
67
|
+
* href="https://tinybase.org"
|
|
68
|
+
* />
|
|
69
|
+
* ```
|
|
70
|
+
* This example shows the `accent` variant of the Button component with an icon,
|
|
71
|
+
* and that launches a new link.
|
|
72
|
+
* @example
|
|
73
|
+
* ```tsx
|
|
74
|
+
* <Button
|
|
75
|
+
* title="TinyWidgets"
|
|
76
|
+
* variant="ghost"
|
|
77
|
+
* />
|
|
78
|
+
* ```
|
|
79
|
+
* This example shows the `ghost` variant of the Button component.
|
|
80
|
+
* @example
|
|
81
|
+
* ```tsx
|
|
82
|
+
* <Button
|
|
83
|
+
* variant="item"
|
|
84
|
+
* icon={Lucide.Grid3x3}
|
|
85
|
+
* title="TinyWidgets"
|
|
86
|
+
* titleRight={<i>4</i>}
|
|
87
|
+
* iconRight={Lucide.CircleHelp}
|
|
88
|
+
* />
|
|
89
|
+
* ```
|
|
90
|
+
* This example shows the `item` variant of the Button component with both left
|
|
91
|
+
* and right titles and icons. This is suitable for use as a link in a
|
|
92
|
+
* navigational list, such as a side bar.
|
|
93
|
+
* @example
|
|
94
|
+
* ```tsx
|
|
95
|
+
* <Button
|
|
96
|
+
* variant="item"
|
|
97
|
+
* icon={Lucide.Grid3x3}
|
|
98
|
+
* title="TinyWidgets"
|
|
99
|
+
* current={true}
|
|
100
|
+
* />
|
|
101
|
+
* ```
|
|
102
|
+
* This example shows the `item` variant of the Button component, marked as
|
|
103
|
+
* 'current'.
|
|
104
|
+
* @icon Lucide.RectangleHorizontal
|
|
105
|
+
*/
|
|
106
|
+
export const Button = forwardRef(
|
|
107
|
+
(
|
|
108
|
+
{
|
|
109
|
+
icon: Icon,
|
|
110
|
+
title: titleComponentOrNode,
|
|
111
|
+
titleRight: titleRightComponentOrNode,
|
|
112
|
+
iconRight: IconRight,
|
|
113
|
+
onClick,
|
|
114
|
+
variant = 'default',
|
|
115
|
+
current,
|
|
116
|
+
href,
|
|
117
|
+
alt,
|
|
118
|
+
className,
|
|
119
|
+
}: {
|
|
120
|
+
/**
|
|
121
|
+
* An optional component which renders an icon for the button, and which
|
|
122
|
+
* must accept a className prop.
|
|
123
|
+
*/
|
|
124
|
+
readonly icon?: ComponentType<{className?: string}>;
|
|
125
|
+
/**
|
|
126
|
+
* An optional component, element, or string which renders the title of
|
|
127
|
+
* the button.
|
|
128
|
+
*/
|
|
129
|
+
readonly title?: ComponentType | ReactNode;
|
|
130
|
+
/**
|
|
131
|
+
* An optional component, element, or string which renders a second title
|
|
132
|
+
* on the right side of the button.
|
|
133
|
+
*/
|
|
134
|
+
readonly titleRight?: ComponentType | ReactNode;
|
|
135
|
+
/**
|
|
136
|
+
* An optional component which renders a second icon for the button, and
|
|
137
|
+
* which must accept a className prop.
|
|
138
|
+
*/
|
|
139
|
+
readonly iconRight?: ComponentType<{className?: string}>;
|
|
140
|
+
/**
|
|
141
|
+
* A handler called when the user clicks on the button.
|
|
142
|
+
*/
|
|
143
|
+
readonly onClick?: () => void;
|
|
144
|
+
/**
|
|
145
|
+
* A variant of the button, one of:
|
|
146
|
+
* - `default`
|
|
147
|
+
* - `icon`
|
|
148
|
+
* - `accent`
|
|
149
|
+
* - `ghost`
|
|
150
|
+
* - `item`
|
|
151
|
+
*/
|
|
152
|
+
readonly variant?: keyof typeof buttonVariants;
|
|
153
|
+
/**
|
|
154
|
+
* A flag that indicates that an `item` button is 'current' and therefore
|
|
155
|
+
* highlighted.
|
|
156
|
+
*/
|
|
157
|
+
readonly current?: boolean;
|
|
158
|
+
/**
|
|
159
|
+
* A URL that can be used instead of an `onClick` to launch a new web
|
|
160
|
+
* page, much like a link.
|
|
161
|
+
*/
|
|
162
|
+
readonly href?: string;
|
|
163
|
+
/**
|
|
164
|
+
* Alternative text shown when the user hovers over the button.
|
|
165
|
+
*/
|
|
166
|
+
readonly alt?: string;
|
|
167
|
+
/**
|
|
168
|
+
* An extra CSS class name for the component.
|
|
169
|
+
*/
|
|
170
|
+
readonly className?: string;
|
|
171
|
+
},
|
|
172
|
+
ref: Ref<HTMLButtonElement>,
|
|
173
|
+
) => {
|
|
174
|
+
const hrefClick = useCallback(
|
|
175
|
+
() => (href ? open(href, '_blank', 'noreferrer') : null),
|
|
176
|
+
[href],
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<button
|
|
181
|
+
className={classNames(
|
|
182
|
+
button,
|
|
183
|
+
buttonVariants[variant],
|
|
184
|
+
current && currentStyle,
|
|
185
|
+
className,
|
|
186
|
+
)}
|
|
187
|
+
onClick={onClick ?? hrefClick}
|
|
188
|
+
title={alt}
|
|
189
|
+
ref={ref}
|
|
190
|
+
>
|
|
191
|
+
{Icon ? <Icon className={iconSize} /> : null}
|
|
192
|
+
{titleComponentOrNode ? (
|
|
193
|
+
<span className={titleStyle}>
|
|
194
|
+
{renderComponentOrNode(titleComponentOrNode)}
|
|
195
|
+
</span>
|
|
196
|
+
) : null}
|
|
197
|
+
{titleRightComponentOrNode ? (
|
|
198
|
+
<span className={titleStyleRight}>
|
|
199
|
+
{renderComponentOrNode(titleRightComponentOrNode)}
|
|
200
|
+
</span>
|
|
201
|
+
) : null}
|
|
202
|
+
{IconRight ? <IconRight className={iconSize} /> : null}
|
|
203
|
+
</button>
|
|
204
|
+
);
|
|
205
|
+
},
|
|
206
|
+
);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {colors} from '../../css/colors.css';
|
|
2
|
+
import {dimensions} from '../../css/dimensions.css';
|
|
3
|
+
import {style} from '@vanilla-extract/css';
|
|
4
|
+
|
|
5
|
+
export const card = style({
|
|
6
|
+
padding: dimensions.padding,
|
|
7
|
+
borderRadius: dimensions.radius,
|
|
8
|
+
boxShadow: colors.shadow,
|
|
9
|
+
border: colors.border,
|
|
10
|
+
height: 'fit-content',
|
|
11
|
+
overflow: 'auto',
|
|
12
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type {ReactNode} from 'react';
|
|
3
|
+
import {card} from './index.css';
|
|
4
|
+
import {classNames} from '../../common/functions';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The `Card` component displays a simple rectangular container.
|
|
8
|
+
*
|
|
9
|
+
* @param props The props for the component.
|
|
10
|
+
* @returns Card Row component.
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* <Card>
|
|
14
|
+
* <h1>Welcome</h1>
|
|
15
|
+
* <Hr />
|
|
16
|
+
* <p>We hope you enjoy TinyWidgets</p>
|
|
17
|
+
* </Card>
|
|
18
|
+
* ```
|
|
19
|
+
* This example shows a simple card.
|
|
20
|
+
* @icon Lucide.Square
|
|
21
|
+
*/
|
|
22
|
+
export const Card = ({
|
|
23
|
+
className,
|
|
24
|
+
children,
|
|
25
|
+
}: {
|
|
26
|
+
/**
|
|
27
|
+
* An extra CSS class name for the component.
|
|
28
|
+
*/
|
|
29
|
+
readonly className?: string;
|
|
30
|
+
/**
|
|
31
|
+
* The children of the component, that go inside the card.
|
|
32
|
+
*/
|
|
33
|
+
readonly children: ReactNode;
|
|
34
|
+
}) => <div className={classNames(card, className)}>{children}</div>;
|