tinywidgets 1.3.4 → 1.3.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tinywidgets",
3
- "version": "1.3.4",
3
+ "version": "1.3.6",
4
4
  "author": "jamesgpearce",
5
5
  "repository": "github:tinyplex/tinywidgets",
6
6
  "license": "MIT",
@@ -19,28 +19,29 @@
19
19
  "prePublishPackage": "prettier --check . && eslint . && cspell --quiet . && tsc && cp ../README.md ./README.md"
20
20
  },
21
21
  "devDependencies": {
22
- "@eslint/js": "^9.27.0",
23
- "@types/react": "^19.1.4",
24
- "cspell": "^9.0.1",
25
- "eslint": "^9.27.0",
26
- "eslint-plugin-import": "^2.31.0",
22
+ "@eslint/js": "^9.30.1",
23
+ "@types/react": "^19.1.8",
24
+ "@types/react-dom": "^19.1.6",
25
+ "cspell": "^9.1.3",
26
+ "eslint": "^9.30.1",
27
+ "eslint-plugin-import": "^2.32.0",
27
28
  "eslint-plugin-react": "^7.37.5",
28
29
  "eslint-plugin-react-hooks": "^5.2.0",
29
- "globals": "^16.1.0",
30
- "prettier": "^3.5.3",
30
+ "globals": "^16.3.0",
31
+ "prettier": "^3.6.2",
31
32
  "prettier-plugin-organize-imports": "^4.1.0",
32
33
  "typescript": "^5.8.3",
33
- "typescript-eslint": "^8.32.1"
34
+ "typescript-eslint": "^8.36.0"
34
35
  },
35
36
  "exports": {
36
37
  ".": "./src/index.ts",
37
38
  "./css": "./src/index.css.ts"
38
39
  },
39
40
  "dependencies": {
40
- "@vanilla-extract/css": "^1.17.2",
41
- "lucide-react": "^0.511.0",
41
+ "@vanilla-extract/css": "^1.17.4",
42
+ "lucide-react": "^0.525.0",
42
43
  "react": "^19.1.0",
43
44
  "react-dom": "^19.1.0",
44
- "tinybase": "^6.2.0-beta.1"
45
+ "tinybase": "^6.4.0"
45
46
  }
46
47
  }
@@ -1,5 +1,11 @@
1
1
  import {Menu, Moon, Sun, SunMoon, X} from 'lucide-react';
2
- import type {ComponentType, ReactNode} from 'react';
2
+ import {
3
+ createContext,
4
+ useContext,
5
+ useRef,
6
+ type ComponentType,
7
+ type ReactNode,
8
+ } from 'react';
3
9
  import * as UiReact from 'tinybase/ui-react/with-schemas';
4
10
  import type {OptionalSchemas} from 'tinybase/with-schemas';
5
11
  import {classNames, renderComponentOrNode} from '../../common/functions.tsx';
@@ -105,6 +111,11 @@ export const App = (props: {
105
111
  );
106
112
  };
107
113
 
114
+ const LayoutContext = createContext<{readonly portal: HTMLDivElement | null}>({
115
+ portal: null,
116
+ });
117
+ export const usePortal = () => useContext(LayoutContext).portal;
118
+
108
119
  const Layout = ({
109
120
  title: titleComponentOrNode,
110
121
  topNavLeft: topNavLeftComponentOrNode,
@@ -137,8 +148,11 @@ const Layout = ({
137
148
  const hasSideNav = sideNavComponentOrNode != null;
138
149
  const hasFooter = footerComponentOrNode != null;
139
150
 
140
- return sessionStoreIsReady && routeStoreIsReady && localStoreIsReady ? (
151
+ const ref = useRef<HTMLDivElement>(null);
152
+
153
+ return (
141
154
  <div
155
+ ref={ref}
142
156
  className={classNames(
143
157
  app,
144
158
  hasLayout && appLayout,
@@ -148,53 +162,60 @@ const Layout = ({
148
162
  )}
149
163
  >
150
164
  {hasLayout ? (
151
- <>
152
- <header className={header}>
153
- {hasSideNav ? (
154
- <Button
155
- variant="icon"
156
- onClick={toggleSideNavIsOpen}
157
- icon={sideNavIsOpen ? X : Menu}
158
- className={sideNavButton}
159
- />
160
- ) : null}
161
- <nav className={title}>
162
- {renderComponentOrNode(titleComponentOrNode)}
163
- </nav>
164
- <nav className={topNav}>
165
- {renderComponentOrNode(topNavLeftComponentOrNode, <div />)}
166
- {renderComponentOrNode(topNavRightComponentOrNode, <div />)}
167
- </nav>
168
- <Button
169
- variant="icon"
170
- onClick={toggleDarkChoice}
171
- icon={darkIcons[darkChoice]}
172
- alt={darkChoices[darkChoice]}
173
- />
174
- {hasSideNav ? (
175
- <nav
176
- className={classNames(sideNav, sideNavIsOpen && sideNavOpen)}
165
+ <LayoutContext.Provider value={{portal: ref.current}}>
166
+ {sessionStoreIsReady && routeStoreIsReady && localStoreIsReady ? (
167
+ <>
168
+ <header className={header}>
169
+ {hasSideNav ? (
170
+ <Button
171
+ variant="icon"
172
+ onClick={toggleSideNavIsOpen}
173
+ icon={sideNavIsOpen ? X : Menu}
174
+ className={sideNavButton}
175
+ />
176
+ ) : null}
177
+ <nav className={title}>
178
+ {renderComponentOrNode(titleComponentOrNode)}
179
+ </nav>
180
+ <nav className={topNav}>
181
+ {renderComponentOrNode(topNavLeftComponentOrNode, <div />)}
182
+ {renderComponentOrNode(topNavRightComponentOrNode, <div />)}
183
+ </nav>
184
+ <Button
185
+ variant="icon"
186
+ onClick={toggleDarkChoice}
187
+ icon={darkIcons[darkChoice]}
188
+ alt={darkChoices[darkChoice]}
189
+ />
190
+ {hasSideNav ? (
191
+ <nav
192
+ className={classNames(
193
+ sideNav,
194
+ sideNavIsOpen && sideNavOpen,
195
+ )}
196
+ >
197
+ {renderComponentOrNode(sideNavComponentOrNode)}
198
+ </nav>
199
+ ) : null}
200
+ </header>
201
+ <main
202
+ className={classNames(
203
+ main,
204
+ hasSideNav && mainHasSideNav,
205
+ hasFooter && mainHasFooter,
206
+ )}
177
207
  >
178
- {renderComponentOrNode(sideNavComponentOrNode)}
179
- </nav>
180
- ) : null}
181
- </header>
182
- <main
183
- className={classNames(
184
- main,
185
- hasSideNav && mainHasSideNav,
186
- hasFooter && mainHasFooter,
187
- )}
188
- >
189
- {renderComponentOrNode(mainComponentOrNode)}
190
- </main>
191
- {hasFooter ? (
192
- <footer className={footer}>
193
- {renderComponentOrNode(footerComponentOrNode)}
194
- </footer>
208
+ {renderComponentOrNode(mainComponentOrNode)}
209
+ </main>
210
+ {hasFooter ? (
211
+ <footer className={footer}>
212
+ {renderComponentOrNode(footerComponentOrNode)}
213
+ </footer>
214
+ ) : null}
215
+ </>
195
216
  ) : null}
196
- </>
217
+ </LayoutContext.Provider>
197
218
  ) : null}
198
219
  </div>
199
- ) : null;
220
+ );
200
221
  };
@@ -114,6 +114,7 @@ export const Button = ({
114
114
  href,
115
115
  alt,
116
116
  className,
117
+ anchorName,
117
118
  ref,
118
119
  }: {
119
120
  /**
@@ -167,6 +168,10 @@ export const Button = ({
167
168
  * An extra CSS class name for the component.
168
169
  */
169
170
  readonly className?: string;
171
+ /**
172
+ * An name for the component to be used as an anchor for other elements.
173
+ */
174
+ readonly anchorName?: string;
170
175
  ref?: React.RefObject<HTMLButtonElement | null>;
171
176
  }) => {
172
177
  const hrefClick = useCallback(
@@ -175,6 +180,7 @@ export const Button = ({
175
180
  );
176
181
 
177
182
  return (
183
+ // @ts-expect-error anchorName not typed for React yet
178
184
  <button
179
185
  className={classNames(
180
186
  button,
@@ -185,6 +191,7 @@ export const Button = ({
185
191
  onClick={onClick ?? hrefClick}
186
192
  title={alt}
187
193
  ref={ref}
194
+ {...(anchorName ? {style: {anchorName}} : {})}
188
195
  >
189
196
  {Icon ? <Icon className={iconSize} /> : null}
190
197
  {titleComponentOrNode ? (
@@ -6,7 +6,7 @@ import {card} from './index.css';
6
6
  * The `Card` component displays a simple rectangular container.
7
7
  *
8
8
  * @param props The props for the component.
9
- * @returns Card Row component.
9
+ * @returns The Card component.
10
10
  * @example
11
11
  * ```tsx
12
12
  * <Card>
@@ -27,7 +27,7 @@ export const Card = ({
27
27
  */
28
28
  readonly className?: string;
29
29
  /**
30
- * The children of the component, that go inside the card.
30
+ * The children of the component that go inside the card.
31
31
  */
32
32
  readonly children: ReactNode;
33
33
  }) => <div className={classNames(card, className)}>{children}</div>;
@@ -0,0 +1,14 @@
1
+ import {style} from '@vanilla-extract/css';
2
+ import {dimensions} from '../../css/dimensions.css';
3
+ import {colors} from '../../index.css';
4
+
5
+ export const checkbox = style({
6
+ display: 'inline-block',
7
+ boxShadow: colors.shadow + ' inset',
8
+ borderRadius: dimensions.radius,
9
+ border: colors.border,
10
+ padding: '0.5rem',
11
+ height: '2rem',
12
+ width: '2rem',
13
+ margin: '5px',
14
+ });
@@ -0,0 +1,67 @@
1
+ import {LucideCheck} from 'lucide-react';
2
+ import {useCallback, useState} from 'react';
3
+ import {classNames} from '../../common/functions.tsx';
4
+ import {Button} from '../Button/index.tsx';
5
+ import {checkbox} from './index.css.ts';
6
+
7
+ /**
8
+ * The `Checkbox` component displays a managed checkbox with an optional default
9
+ * checked state.
10
+ *
11
+ * @param props The props for the component.
12
+ * @returns The Checkbox component.
13
+ * @example
14
+ * ```tsx
15
+ * <Checkbox onChange={(value) => console.log(value)} />
16
+ * ```
17
+ * This example shows the Checkbox component without a default checked state.
18
+ * @example
19
+ * ```tsx
20
+ * <Checkbox
21
+ * initialChecked={true}
22
+ * onChange={(value) => console.log(value)}
23
+ * />
24
+ * ```
25
+ * This example shows the Checkbox component with a default checked state.
26
+ * @icon Lucide.LucideCheckSquare
27
+ */
28
+ export const Checkbox = ({
29
+ initialChecked,
30
+ onChange,
31
+ alt,
32
+ className,
33
+ ref,
34
+ }: {
35
+ /**
36
+ * An optional value for whether the box is checked.
37
+ */
38
+ readonly initialChecked?: boolean;
39
+ /**
40
+ * A handler called when the text is changed.
41
+ */
42
+ readonly onChange?: (checked: boolean) => void;
43
+ /**
44
+ * Alternative text shown when the user hovers over the input.
45
+ */
46
+ readonly alt?: string;
47
+ /**
48
+ * An extra CSS class name for the component.
49
+ */
50
+ readonly className?: string;
51
+ ref?: React.RefObject<HTMLButtonElement | null>;
52
+ }) => {
53
+ const [checked, setChecked] = useState(initialChecked ?? false);
54
+ const handleClick = useCallback(() => {
55
+ setChecked(!checked);
56
+ onChange?.(!checked);
57
+ }, [checked, onChange]);
58
+ return (
59
+ <Button
60
+ className={classNames(checkbox, className)}
61
+ onClick={handleClick}
62
+ alt={alt}
63
+ ref={ref}
64
+ icon={checked ? LucideCheck : undefined}
65
+ />
66
+ );
67
+ };
@@ -93,7 +93,7 @@ export const Collapsible = ({
93
93
  */
94
94
  readonly className?: string;
95
95
  /**
96
- * The children of the component, that go inside the collapsible section.
96
+ * The children of the component that go inside the collapsible section.
97
97
  */
98
98
  readonly children: ReactNode;
99
99
  }) => {
@@ -19,7 +19,7 @@ import {detailCell, detailRow, detailTable} from './index.css';
19
19
  * />
20
20
  * ```
21
21
  * This example shows the basic Collapsible component.
22
- * @icon Lucide.Table
22
+ * @icon Lucide.TableProperties
23
23
  */
24
24
  export const Detail = ({
25
25
  data,
@@ -0,0 +1,25 @@
1
+ import {style} from '@vanilla-extract/css';
2
+ import {colors} from '../../css/colors.css';
3
+ import {dimensions} from '../../css/dimensions.css';
4
+
5
+ export const wrapper = style({
6
+ position: 'relative',
7
+ });
8
+
9
+ export const flyout = style({
10
+ padding: dimensions.padding,
11
+ borderRadius: dimensions.radius,
12
+ boxShadow: colors.shadow,
13
+ border: colors.border,
14
+ height: 'fit-content',
15
+ overflow: 'auto',
16
+ backgroundColor: colors.background,
17
+ position: 'absolute',
18
+ top: 'calc(2rem + 2px)',
19
+ left: 0,
20
+ });
21
+
22
+ export const anchoredFlyout = style({
23
+ top: 'anchor(bottom)',
24
+ left: 'anchor(left)',
25
+ });
@@ -0,0 +1,148 @@
1
+ import {
2
+ ComponentType,
3
+ useCallback,
4
+ useMemo,
5
+ useState,
6
+ type ReactNode,
7
+ } from 'react';
8
+ import {createPortal} from 'react-dom';
9
+ import {getUniqueId} from 'tinybase';
10
+ import {classNames} from '../../common/functions';
11
+ import {
12
+ useCollapsibleIsOpen,
13
+ useSetCollapsibleIsOpenCallback,
14
+ } from '../../stores/SessionStore';
15
+ import {usePortal} from '../App';
16
+ import {Button} from '../Button';
17
+ import {buttonVariants} from '../Button/index.css';
18
+ import {anchoredFlyout, flyout, wrapper} from './index.css';
19
+
20
+ const supportsAnchors = CSS.supports('anchor-name', '--');
21
+
22
+ /**
23
+ * The `Flyout` component displays a simple rectangular container that pops up
24
+ * out of an icon.
25
+ *
26
+ * @param props The props for the component.
27
+ * @returns Flyout Row component.
28
+ * @example
29
+ * ```tsx
30
+ * <Flyout icon={Lucide.LucideHelpCircle}>
31
+ * <h1>Welcome</h1>
32
+ * <Hr />
33
+ * <p>We hope you enjoy TinyWidgets</p>
34
+ * </Flyout>
35
+ * ```
36
+ * This example shows a flyout from a simple button.
37
+ * @example
38
+ * ```tsx
39
+ * <Flyout
40
+ * title="Toggle"
41
+ * icon={Lucide.LucidePanelTopOpen}
42
+ * openIcon={Lucide.LucidePanelTopClose}
43
+ * startOpen={true}
44
+ * >
45
+ * <p>We hope you enjoy TinyWidgets</p>
46
+ * </Flyout>
47
+ * ```
48
+ * This example shows a flyout, starting off open, with a title on the button,
49
+ * and with a different icon for open and closed states.
50
+ * @icon Lucide.LucideArrowDownSquare
51
+ */
52
+ export const Flyout = ({
53
+ icon,
54
+ openIcon,
55
+ title,
56
+ variant,
57
+ startOpen,
58
+ id = '',
59
+ className,
60
+ children,
61
+ }: {
62
+ /**
63
+ * An icon to click on to open up the flyout, and which must accept a
64
+ * className prop.
65
+ */
66
+ readonly icon: ComponentType<{className?: string}>;
67
+ /**
68
+ * An optional icon to show when the flyout is open, and which must accept a
69
+ * className prop.
70
+ */
71
+ readonly openIcon?: ComponentType<{className?: string}>;
72
+ /**
73
+ * An optional component, element, or string which renders the title of
74
+ * the button.
75
+ */
76
+ readonly title?: ComponentType | ReactNode;
77
+ /**
78
+ * A variant of the button used for the flyout, one of:
79
+ * - `default`
80
+ * - `icon`
81
+ * - `accent`
82
+ * - `ghost`
83
+ * - `item`
84
+ */
85
+ readonly variant?: keyof typeof buttonVariants;
86
+ /**
87
+ * Whether the flyout should start opened up.
88
+ */
89
+ readonly startOpen?: boolean;
90
+ /**
91
+ * An Id which will allow the state to be preserved between page reloads.
92
+ */
93
+ readonly id?: string;
94
+ /**
95
+ * An extra CSS class name for the component.
96
+ */
97
+ readonly className?: string;
98
+ /**
99
+ * The children of the component that go inside the card.
100
+ */
101
+ readonly children: ReactNode;
102
+ }) => {
103
+ const storedIsOpen = useCollapsibleIsOpen(id) ?? startOpen;
104
+ const setStoredIsOpen = useSetCollapsibleIsOpenCallback(id);
105
+ const [stateIsOpen, setStateIsOpen] = useState(startOpen);
106
+
107
+ const isOpen = id ? storedIsOpen : stateIsOpen;
108
+ const setIsOpen = id ? setStoredIsOpen : setStateIsOpen;
109
+
110
+ const handleClick = useCallback(
111
+ () => setIsOpen(!isOpen),
112
+ [setIsOpen, isOpen],
113
+ );
114
+
115
+ const anchor = useMemo(() => '--' + getUniqueId(5), []);
116
+ const portal = usePortal();
117
+
118
+ const buttonProps = {
119
+ icon: isOpen ? (openIcon ?? icon) : icon,
120
+ title,
121
+ variant,
122
+ onClick: handleClick,
123
+ };
124
+ return supportsAnchors ? (
125
+ <>
126
+ <Button {...buttonProps} anchorName={anchor} />
127
+ {isOpen && portal
128
+ ? createPortal(
129
+ <div
130
+ className={classNames(flyout, anchoredFlyout, className)}
131
+ // @ts-expect-error positionAnchor not typed for React yet
132
+ style={{positionAnchor: anchor}}
133
+ >
134
+ {children}
135
+ </div>,
136
+ portal,
137
+ )
138
+ : null}
139
+ </>
140
+ ) : (
141
+ <div className={wrapper}>
142
+ <Button {...buttonProps} />
143
+ {isOpen ? (
144
+ <div className={classNames(flyout, className)}>{children}</div>
145
+ ) : null}
146
+ </div>
147
+ );
148
+ };
@@ -12,4 +12,6 @@ export const select = style({
12
12
  boxShadow: colors.shadow,
13
13
  border: colors.border,
14
14
  backgroundColor: colors.background,
15
+ boxSizing: 'content-box',
16
+ height: '1.5rem',
15
17
  });
@@ -58,17 +58,14 @@ export const Select = ({
58
58
  );
59
59
  return (
60
60
  <select
61
+ value={option}
61
62
  className={classNames(select, className)}
62
63
  onChange={handleChange}
63
64
  title={alt}
64
65
  ref={ref}
65
66
  >
66
67
  {Object.entries(options).map(([eachOption, label]) => (
67
- <option
68
- key={eachOption}
69
- value={eachOption}
70
- {...(eachOption === option ? {selected: true} : {})}
71
- >
68
+ <option key={eachOption} value={eachOption}>
72
69
  {label}
73
70
  </option>
74
71
  ))}
@@ -0,0 +1,26 @@
1
+ import {globalStyle, style} from '@vanilla-extract/css';
2
+ import {colors} from '../../css/colors.css';
3
+
4
+ export const table = style({
5
+ width: '100%',
6
+ height: '100%',
7
+ padding: 0,
8
+ borderCollapse: 'collapse',
9
+ tableLayout: 'fixed',
10
+ });
11
+
12
+ globalStyle(`${table} th, ${table} td`, {
13
+ overflow: 'hidden',
14
+ padding: '0.25rem 1rem',
15
+ whiteSpace: 'nowrap',
16
+ textOverflow: 'ellipsis',
17
+ borderWidth: '1px 0',
18
+ borderStyle: 'solid',
19
+ borderColor: colors.borderColor,
20
+ textAlign: 'left',
21
+ });
22
+
23
+ globalStyle(`${table} th`, {
24
+ fontWeight: 'inherit',
25
+ backgroundColor: colors.backgroundHover,
26
+ });
@@ -0,0 +1,33 @@
1
+ import type {ReactNode} from 'react';
2
+ import {classNames} from '../../common/functions';
3
+ import {table} from './index.css';
4
+
5
+ /**
6
+ * The `Table` component displays a table with some simple default styling.
7
+ *
8
+ * @param props The props for the component.
9
+ * @returns The Table component.
10
+ * @example
11
+ * ```tsx
12
+ * <Table>
13
+ * <tr><th>Column 1</th><th>Column 2</th></tr>
14
+ * <tr><td>Cell A</td><td>Cell B</td></tr>
15
+ * <tr><td>Cell C</td><td>Cell D</td></tr>
16
+ * </Table>
17
+ * ```
18
+ * This example shows a simple table.
19
+ * @icon Lucide.LucideTable2
20
+ */
21
+ export const Table = ({
22
+ className,
23
+ children,
24
+ }: {
25
+ /**
26
+ * An extra CSS class name for the component.
27
+ */
28
+ readonly className?: string;
29
+ /**
30
+ * The children (`tr` rows) that go inside the table.
31
+ */
32
+ readonly children: ReactNode;
33
+ }) => <table className={classNames(table, className)}>{children}</table>;
@@ -6,10 +6,9 @@ export const wrapper = style({
6
6
  flexShrink: 0,
7
7
  alignSelf: 'center',
8
8
  position: 'relative',
9
+ display: 'inline-block',
9
10
  });
10
11
 
11
- export const wrapperWithIcon = style({});
12
-
13
12
  export const input = style({
14
13
  borderRadius: dimensions.radius,
15
14
  padding: '0.5rem',
@@ -20,6 +19,7 @@ export const input = style({
20
19
  boxShadow: colors.shadow + ' inset',
21
20
  border: colors.border,
22
21
  backgroundColor: colors.background,
22
+ lineHeight: '1.5rem',
23
23
  });
24
24
 
25
25
  export const inputWithIcon = style({
@@ -29,7 +29,7 @@ export const inputWithIcon = style({
29
29
  export const icon = style({
30
30
  position: 'absolute',
31
31
  left: `calc(${dimensions.icon} * .5)`,
32
- top: `calc(${dimensions.icon} * .65)`,
32
+ top: `calc(${dimensions.icon} * .8)`,
33
33
  color: colors.foregroundDim,
34
34
  backgroundColor: colors.background,
35
35
  borderRight: `calc(${dimensions.icon} * .25) solid ${colors.background}`,
@@ -1,13 +1,7 @@
1
1
  import {useCallback, useState, type ComponentType} from 'react';
2
2
  import {classNames} from '../../common/functions.tsx';
3
3
  import {iconSize} from '../../css/dimensions.css.ts';
4
- import {
5
- icon,
6
- input,
7
- inputWithIcon,
8
- wrapper,
9
- wrapperWithIcon,
10
- } from './index.css.ts';
4
+ import {icon, input, inputWithIcon, wrapper} from './index.css.ts';
11
5
 
12
6
  /**
13
7
  * The `TextInput` component displays a managed text input with an existing
@@ -76,7 +70,7 @@ export const TextInput = ({
76
70
  [onChange],
77
71
  );
78
72
  return (
79
- <div className={classNames(wrapper, Icon && wrapperWithIcon)}>
73
+ <div className={wrapper}>
80
74
  {Icon ? <Icon className={classNames(iconSize, icon)} /> : null}
81
75
  <input
82
76
  value={text}