tinywidgets 1.3.12 → 1.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tinywidgets",
3
- "version": "1.3.12",
3
+ "version": "1.4.0",
4
4
  "author": "jamesgpearce",
5
5
  "repository": "github:tinyplex/tinywidgets",
6
6
  "license": "MIT",
@@ -38,7 +38,7 @@
38
38
  "./css": "./src/index.css.ts"
39
39
  },
40
40
  "dependencies": {
41
- "@vanilla-extract/css": "^1.19.1",
41
+ "@vanilla-extract/css": "^1.20.0",
42
42
  "lucide-react": "^0.577.0",
43
43
  "react": "^19.2.4",
44
44
  "react-dom": "^19.2.4",
@@ -1,5 +1,10 @@
1
1
  import type {StyleRule} from '@vanilla-extract/css';
2
- import {type ComponentType, type ReactNode} from 'react';
2
+ import {
3
+ createElement,
4
+ isValidElement,
5
+ type ComponentType,
6
+ type ReactNode,
7
+ } from 'react';
3
8
  import {screens} from '../css/screens';
4
9
 
5
10
  /**
@@ -30,12 +35,18 @@ export const classNames = (
30
35
  export const renderComponentOrNode = (
31
36
  ComponentOrNode: ComponentType | ReactNode,
32
37
  fallback: ReactNode = null,
33
- ) =>
34
- ComponentOrNode instanceof Function ? (
35
- <ComponentOrNode />
36
- ) : (
37
- (ComponentOrNode ?? fallback)
38
- );
38
+ ): ReactNode => {
39
+ if (ComponentOrNode == null) {
40
+ return fallback;
41
+ }
42
+ if (isValidElement(ComponentOrNode)) {
43
+ return ComponentOrNode;
44
+ }
45
+ if (['string', 'number', 'boolean'].includes(typeof ComponentOrNode)) {
46
+ return ComponentOrNode as string | number | boolean;
47
+ }
48
+ return createElement(ComponentOrNode as ComponentType);
49
+ };
39
50
 
40
51
  export const large = (style: StyleRule) => ({
41
52
  '@media': {[`screen and (min-width: ${screens.large}px)`]: style},
@@ -12,9 +12,7 @@ export const appLayout = style({
12
12
  });
13
13
 
14
14
  export const header = style({
15
- display: 'flex',
16
15
  justifyContent: 'space-between',
17
- alignItems: 'center',
18
16
  gap: dimensions.padding,
19
17
  padding: dimensions.padding,
20
18
  position: 'fixed',
@@ -29,9 +27,7 @@ export const header = style({
29
27
  });
30
28
 
31
29
  export const topNav = style({
32
- display: 'flex',
33
30
  justifyContent: 'space-between',
34
- alignItems: 'center',
35
31
  gap: dimensions.padding,
36
32
  flex: 1,
37
33
  });
@@ -39,9 +35,6 @@ export const topNav = style({
39
35
  export const sideNavButton = style(large({display: 'none!important'}));
40
36
 
41
37
  export const title = style({
42
- display: 'flex',
43
- alignItems: 'center',
44
- gap: dimensions.padding,
45
38
  ...large({
46
39
  width: `calc(${dimensions.sideNavWidth} - 2 * ${dimensions.padding})`,
47
40
  }),
@@ -84,9 +77,7 @@ export const mainHasFooter = style({
84
77
  });
85
78
 
86
79
  export const footer = style({
87
- display: 'flex',
88
80
  justifyContent: 'right',
89
- alignItems: 'center',
90
81
  gap: dimensions.padding,
91
82
  paddingLeft: dimensions.padding,
92
83
  paddingRight: dimensions.padding,
@@ -25,6 +25,7 @@ import {
25
25
  useSideNavIsOpen,
26
26
  useToggleSideNavIsOpenCallback,
27
27
  } from '../../stores/SessionStore.tsx';
28
+ import {Axis} from '../Axis/index.tsx';
28
29
  import {Button} from '../Button/index.tsx';
29
30
  import {
30
31
  app,
@@ -165,7 +166,7 @@ const Layout = ({
165
166
  <LayoutContext.Provider value={{portal: ref.current}}>
166
167
  {sessionStoreIsReady && routeStoreIsReady && localStoreIsReady ? (
167
168
  <>
168
- <header className={header}>
169
+ <Axis as="header" className={header}>
169
170
  {hasSideNav ? (
170
171
  <Button
171
172
  variant="icon"
@@ -177,10 +178,10 @@ const Layout = ({
177
178
  <nav className={title}>
178
179
  {renderComponentOrNode(titleComponentOrNode)}
179
180
  </nav>
180
- <nav className={topNav}>
181
+ <Axis as="nav" className={topNav}>
181
182
  {renderComponentOrNode(topNavLeftComponentOrNode, <div />)}
182
183
  {renderComponentOrNode(topNavRightComponentOrNode, <div />)}
183
- </nav>
184
+ </Axis>
184
185
  <Button
185
186
  variant="icon"
186
187
  onClick={toggleDarkChoice}
@@ -197,7 +198,7 @@ const Layout = ({
197
198
  {renderComponentOrNode(sideNavComponentOrNode)}
198
199
  </nav>
199
200
  ) : null}
200
- </header>
201
+ </Axis>
201
202
  <main
202
203
  className={classNames(
203
204
  main,
@@ -208,9 +209,9 @@ const Layout = ({
208
209
  {renderComponentOrNode(mainComponentOrNode)}
209
210
  </main>
210
211
  {hasFooter ? (
211
- <footer className={footer}>
212
+ <Axis as="footer" className={footer}>
212
213
  {renderComponentOrNode(footerComponentOrNode)}
213
- </footer>
214
+ </Axis>
214
215
  ) : null}
215
216
  </>
216
217
  ) : null}
@@ -0,0 +1,13 @@
1
+ import {style, styleVariants} from '@vanilla-extract/css';
2
+
3
+ export const axis = style({
4
+ display: 'flex',
5
+ alignItems: 'center',
6
+ });
7
+
8
+ export const axisVariants = styleVariants({
9
+ horizontal: {},
10
+ vertical: {
11
+ flexDirection: 'column',
12
+ },
13
+ });
@@ -0,0 +1,82 @@
1
+ import type {MouseEventHandler, ReactNode} from 'react';
2
+ import {classNames} from '../../common/functions';
3
+ import {axis, axisVariants} from './index.css';
4
+
5
+ /**
6
+ * The `Axis` component displays its children along a flex axis, aligning them
7
+ * in the center of the cross-axis.
8
+ *
9
+ * This is useful for compact layouts where icons, avatars, images, and text
10
+ * should share a common visual center line, or for stacked layouts that should
11
+ * remain horizontally centered.
12
+ *
13
+ * @param props The props for the component.
14
+ * @returns The Axis component.
15
+ * @example
16
+ * ```tsx
17
+ * <Axis>
18
+ * <Image src="/favicon.svg" variant="logo" />
19
+ * TinyWidgets
20
+ * </Axis>
21
+ * ```
22
+ * This example shows a logo image and text aligned along the same
23
+ * horizontal axis.
24
+ * @example
25
+ * ```tsx
26
+ * <Axis variant="vertical">
27
+ * <Image src="/favicon.svg" variant="logo" />
28
+ * TinyWidgets
29
+ * </Axis>
30
+ * ```
31
+ * This example shows the same content arranged on a vertical axis.
32
+ * @icon Lucide.AlignCenterHorizontal
33
+ */
34
+ export const Axis = ({
35
+ as: Component = 'div',
36
+ variant = 'horizontal',
37
+ className,
38
+ title,
39
+ onClick,
40
+ children,
41
+ }: {
42
+ /**
43
+ * The HTML element used to wrap the axis, one of:
44
+ * - `div`
45
+ * - `nav`
46
+ * - `span`
47
+ * - `h1`
48
+ * - `header`
49
+ * - `footer`
50
+ */
51
+ readonly as?: 'div' | 'nav' | 'span' | 'h1' | 'header' | 'footer';
52
+ /**
53
+ * A variant of the axis, one of:
54
+ * - `horizontal`
55
+ * - `vertical`
56
+ */
57
+ readonly variant?: keyof typeof axisVariants;
58
+ /**
59
+ * An extra CSS class name for the component.
60
+ */
61
+ readonly className?: string;
62
+ /**
63
+ * Alternative text shown when the user hovers over the component.
64
+ */
65
+ readonly title?: string;
66
+ /**
67
+ * A handler called when the user clicks on the component.
68
+ */
69
+ readonly onClick?: MouseEventHandler<HTMLElement>;
70
+ /**
71
+ * The children of the component, arranged along the selected axis.
72
+ */
73
+ readonly children: ReactNode;
74
+ }) => (
75
+ <Component
76
+ className={classNames(axis, axisVariants[variant], className)}
77
+ title={title}
78
+ onClick={onClick}
79
+ >
80
+ {children}
81
+ </Component>
82
+ );
@@ -0,0 +1,5 @@
1
+ import {style} from '@vanilla-extract/css';
2
+
3
+ export const imageLabel = style({
4
+ gap: '0.5rem',
5
+ });
@@ -0,0 +1,72 @@
1
+ import type {ComponentType, MouseEventHandler, ReactNode} from 'react';
2
+ import {classNames, renderComponentOrNode} from '../../common/functions';
3
+ import {Axis} from '../Axis';
4
+ import {imageLabel} from './index.css';
5
+
6
+ /**
7
+ * The `ImageLabel` component displays an image and a text label along a shared
8
+ * horizontal axis.
9
+ *
10
+ * It is useful for common UI patterns such as brand marks, avatars with names,
11
+ * or any compact media-and-label combination.
12
+ *
13
+ * @param props The props for the component.
14
+ * @returns The ImageLabel component.
15
+ * @example
16
+ * ```tsx
17
+ * <ImageLabel
18
+ * image={<Image src="/favicon.svg" variant="avatar" />}
19
+ * text="TinyWidgets"
20
+ * />
21
+ * ```
22
+ * This example shows a logo image with a text label.
23
+ * @example
24
+ * ```tsx
25
+ * <ImageLabel
26
+ * as="h1"
27
+ * image={<Image src="/favicon.svg" variant="logo"/>}
28
+ * text={<b>TinyWidgets</b>}
29
+ * />
30
+ * ```
31
+ * This example shows the component used as a heading with rich text content.
32
+ * @icon Lucide.Captions
33
+ */
34
+ export const ImageLabel = ({
35
+ as = 'div',
36
+ image,
37
+ text,
38
+ className,
39
+ onClick,
40
+ }: {
41
+ /**
42
+ * The HTML element used to wrap the image label, one of:
43
+ * - `div`
44
+ * - `nav`
45
+ * - `span`
46
+ * - `h1`
47
+ * - `header`
48
+ * - `footer`
49
+ */
50
+ readonly as?: 'div' | 'nav' | 'span' | 'h1' | 'header' | 'footer';
51
+ /**
52
+ * A component or element which renders the image for the label.
53
+ */
54
+ readonly image: ComponentType | ReactNode;
55
+ /**
56
+ * A component, element, or string which renders the label text.
57
+ */
58
+ readonly text: ComponentType | ReactNode;
59
+ /**
60
+ * An extra CSS class name for the component.
61
+ */
62
+ readonly className?: string;
63
+ /**
64
+ * A handler called when the user clicks on the component.
65
+ */
66
+ readonly onClick?: MouseEventHandler<HTMLElement>;
67
+ }) => (
68
+ <Axis as={as} className={classNames(imageLabel, className)} onClick={onClick}>
69
+ {renderComponentOrNode(image)}
70
+ {renderComponentOrNode(text)}
71
+ </Axis>
72
+ );
@@ -7,8 +7,6 @@ export const metric = style({
7
7
  });
8
8
 
9
9
  export const metricLabel = style({
10
- display: 'flex',
11
- alignItems: 'center',
12
10
  gap: '0.5rem',
13
11
  });
14
12
 
@@ -1,6 +1,7 @@
1
1
  import type {ComponentType, ReactNode} from 'react';
2
2
  import {classNames, renderComponentOrNode} from '../../common/functions';
3
3
  import {iconSize} from '../../css/dimensions.css';
4
+ import {Axis} from '../Axis';
4
5
  import {metric, metricLabel, metricNumber} from './index.css';
5
6
 
6
7
  /**
@@ -47,10 +48,10 @@ export const Metric = ({
47
48
  readonly className?: string;
48
49
  }) => (
49
50
  <div className={classNames(metric, className)}>
50
- <div className={metricLabel}>
51
+ <Axis className={metricLabel}>
51
52
  {Icon ? <Icon className={iconSize} /> : null}
52
53
  {renderComponentOrNode(titleComponentOrNode)}
53
- </div>
54
+ </Axis>
54
55
  <div className={metricNumber}>
55
56
  {renderComponentOrNode(numberComponentOrNode)}
56
57
  </div>
@@ -2,8 +2,6 @@ import {style, styleVariants} from '@vanilla-extract/css';
2
2
  import {colors} from '../../css/colors.css';
3
3
 
4
4
  export const tag = style({
5
- display: 'flex',
6
- alignItems: 'center',
7
5
  fontSize: '0.625rem',
8
6
  lineHeight: '0.625rem',
9
7
  padding: '0.1rem 0.25rem',
@@ -1,5 +1,6 @@
1
1
  import type {ComponentType, ReactNode} from 'react';
2
2
  import {classNames, renderComponentOrNode} from '../../common/functions';
3
+ import {Axis} from '../Axis';
3
4
  import {tag, tagIcon, tagVariants} from './index.css';
4
5
 
5
6
  /**
@@ -61,12 +62,12 @@ export const Tag = ({
61
62
  }) => {
62
63
  const icon = Icon ? <Icon className={tagIcon} /> : null;
63
64
  return (
64
- <div
65
+ <Axis
65
66
  className={classNames(tag, tagVariants[variant], className)}
66
67
  title={alt}
67
68
  >
68
69
  {icon}
69
70
  {renderComponentOrNode(title)}
70
- </div>
71
+ </Axis>
71
72
  );
72
73
  };
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export {App} from './components/App/index.tsx';
2
+ export {Axis} from './components/Axis/index.tsx';
2
3
  export {Button} from './components/Button/index.tsx';
3
4
  export {Card} from './components/Card/index.tsx';
4
5
  export {Checkbox} from './components/Checkbox/index.tsx';
@@ -8,6 +9,7 @@ export {Detail} from './components/Detail/index.tsx';
8
9
  export {Flyout} from './components/Flyout/index.tsx';
9
10
  export {Hr} from './components/Hr/index.tsx';
10
11
  export {Image} from './components/Image/index.tsx';
12
+ export {ImageLabel} from './components/ImageLabel/index.tsx';
11
13
  export {Metric} from './components/Metric/index.tsx';
12
14
  export {Row} from './components/Row/index.tsx';
13
15
  export {Select} from './components/Select/index.tsx';