tinywidgets 1.5.0 → 1.6.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 (33) hide show
  1. package/package.json +14 -14
  2. package/src/components/App/index.css.ts +35 -26
  3. package/src/components/App/index.tsx +31 -6
  4. package/src/components/Axis/index.css.ts +1 -1
  5. package/src/components/Button/index.css.ts +37 -23
  6. package/src/components/Button/index.tsx +14 -0
  7. package/src/components/Card/index.css.ts +69 -3
  8. package/src/components/Card/index.tsx +86 -5
  9. package/src/components/Checkbox/index.css.ts +27 -8
  10. package/src/components/Checkbox/index.tsx +17 -2
  11. package/src/components/Code/index.css.ts +2 -2
  12. package/src/components/Collapsible/index.css.ts +6 -6
  13. package/src/components/Detail/index.css.ts +2 -2
  14. package/src/components/Flyout/index.css.ts +5 -5
  15. package/src/components/Hr/index.css.ts +1 -1
  16. package/src/components/Image/index.css.ts +12 -12
  17. package/src/components/Loading/index.css.ts +28 -28
  18. package/src/components/Row/index.css.ts +8 -8
  19. package/src/components/Select/index.css.ts +20 -9
  20. package/src/components/Select/index.tsx +22 -3
  21. package/src/components/Summary/index.css.ts +1 -1
  22. package/src/components/Table/index.css.ts +8 -8
  23. package/src/components/Tag/index.css.ts +8 -8
  24. package/src/components/TextInput/index.css.ts +48 -17
  25. package/src/components/TextInput/index.tsx +39 -5
  26. package/src/css/classes.css.ts +42 -0
  27. package/src/css/code.css.ts +18 -18
  28. package/src/css/colors.css.ts +17 -17
  29. package/src/css/dimensions.css.ts +2 -2
  30. package/src/css/global.css.ts +2 -2
  31. package/src/index.css.ts +1 -0
  32. package/src/index.ts +5 -1
  33. package/src/stores/RouteStore.tsx +52 -14
@@ -1,7 +1,14 @@
1
1
  import {useCallback, useEffect, useState, type ComponentType} from 'react';
2
2
  import {classNames} from '../../common/functions.tsx';
3
3
  import {iconSize} from '../../css/dimensions.css.ts';
4
- import {icon, input, inputWithIcon, wrapper} from './index.css.ts';
4
+ import {
5
+ icon,
6
+ iconVariants,
7
+ input,
8
+ inputVariants,
9
+ inputWithIconVariants,
10
+ wrapper,
11
+ } from './index.css.ts';
5
12
 
6
13
  /**
7
14
  * The `TextInput` component displays a managed text input with an existing
@@ -23,6 +30,17 @@ import {icon, input, inputWithIcon, wrapper} from './index.css.ts';
23
30
  * ```
24
31
  * This example shows the TextInput component with an inset icon and
25
32
  * placeholder.
33
+ * @example
34
+ * ```tsx
35
+ * <TextInput initialText="42" variant="small" />
36
+ * ```
37
+ * This example shows the `small` variant of the TextInput component.
38
+ * @example
39
+ * ```tsx
40
+ * <TextInput icon={Lucide.Search} placeholder="Search..." variant="small" />
41
+ * ```
42
+ * This example shows the `small` variant of the TextInput component with an
43
+ * inset icon and placeholder.
26
44
  * @icon Lucide.TextCursorInput
27
45
  */
28
46
  export const TextInput = ({
@@ -32,6 +50,7 @@ export const TextInput = ({
32
50
  icon: Icon,
33
51
  alt,
34
52
  className,
53
+ variant = 'default',
35
54
  ref,
36
55
  }: {
37
56
  /**
@@ -59,6 +78,15 @@ export const TextInput = ({
59
78
  * An extra CSS class name for the component.
60
79
  */
61
80
  readonly className?: string;
81
+ /**
82
+ * A variant of the input, one of:
83
+ * - `default`
84
+ * - `small`
85
+ */
86
+ readonly variant?: keyof typeof inputVariants;
87
+ /**
88
+ * A ref to the underlying input element.
89
+ */
62
90
  ref?: React.RefObject<HTMLInputElement | null>;
63
91
  }) => {
64
92
  const [text, setText] = useState(initialText ?? '');
@@ -76,15 +104,21 @@ export const TextInput = ({
76
104
  [change],
77
105
  );
78
106
 
79
- useEffect(() => change(initialText ?? ''), [change, initialText]);
107
+ useEffect(() => setText(initialText ?? ''), [initialText]);
80
108
 
81
109
  return (
82
- <div className={wrapper}>
83
- {Icon ? <Icon className={classNames(iconSize, icon)} /> : null}
110
+ <div className={classNames(wrapper, className)}>
111
+ {Icon ? (
112
+ <Icon className={classNames(iconSize, icon, iconVariants[variant])} />
113
+ ) : null}
84
114
  <input
85
115
  value={text}
86
116
  placeholder={placeholder}
87
- className={classNames(input, Icon && inputWithIcon, className)}
117
+ className={classNames(
118
+ input,
119
+ inputVariants[variant],
120
+ Icon && inputWithIconVariants[variant],
121
+ )}
88
122
  onChange={handleChange}
89
123
  title={alt}
90
124
  ref={ref}
@@ -0,0 +1,42 @@
1
+ import {table} from '../components/Table/index.css.ts';
2
+
3
+ /**
4
+ * The `classes` object exposes reusable TinyWidgets CSS classes so they can be
5
+ * applied to markup rendered by other libraries.
6
+ *
7
+ * The current members are:
8
+ * - `table`
9
+ *
10
+ * @example
11
+ * ```tsx
12
+ * <table className={classes.table}>
13
+ * <thead>
14
+ * <tr>
15
+ * <th>Name</th>
16
+ * <th>Species</th>
17
+ * <th>Color</th>
18
+ * </tr>
19
+ * </thead>
20
+ * <tbody>
21
+ * <tr>
22
+ * <td>Fido</td>
23
+ * <td>Dog</td>
24
+ * <td>Brown</td>
25
+ * </tr>
26
+ * <tr>
27
+ * <td>Felix</td>
28
+ * <td>Cat</td>
29
+ * <td>Black</td>
30
+ * </tr>
31
+ * <tr>
32
+ * <td>Cujo</td>
33
+ * <td>Wolf</td>
34
+ * <td>Gray</td>
35
+ * </tr>
36
+ * </tbody>
37
+ * </table>
38
+ * ```
39
+ * This example applies the TinyWidgets table style to existing table markup.
40
+ * @icon Lucide.Shapes
41
+ */
42
+ export const classes = {table};
@@ -54,9 +54,6 @@ import {createTheme, createThemeContract} from '@vanilla-extract/css';
54
54
  * @icon Lucide.Palette
55
55
  */
56
56
  export const code = createThemeContract({
57
- 'mono-1': null,
58
- 'mono-2': null,
59
- 'mono-3': null,
60
57
  'hue-1': null,
61
58
  'hue-2': null,
62
59
  'hue-3': null,
@@ -65,17 +62,17 @@ export const code = createThemeContract({
65
62
  'hue-5-2': null,
66
63
  'hue-6': null,
67
64
  'hue-6-2': null,
68
- 'syntax-fg': null,
65
+ 'mono-1': null,
66
+ 'mono-2': null,
67
+ 'mono-3': null,
68
+ 'syntax-accent': null,
69
69
  'syntax-bg': null,
70
- 'syntax-gutter': null,
70
+ 'syntax-fg': null,
71
71
  'syntax-guide': null,
72
- 'syntax-accent': null,
72
+ 'syntax-gutter': null,
73
73
  });
74
74
 
75
75
  export const codeLight = createTheme(code, {
76
- 'mono-1': `hsl(230, 8%, 24%)`,
77
- 'mono-2': `hsl(230, 6%, 44%)`,
78
- 'mono-3': `hsl(230, 4%, 64%)`,
79
76
  'hue-1': `hsl(198, 99%, 37%)`,
80
77
  'hue-2': `hsl(221, 87%, 60%)`,
81
78
  'hue-3': `hsl(301, 63%, 40%)`,
@@ -84,17 +81,17 @@ export const codeLight = createTheme(code, {
84
81
  'hue-5-2': `hsl(344, 84%, 43%)`,
85
82
  'hue-6': `hsl(35, 99%, 36%)`,
86
83
  'hue-6-2': `hsl(35, 99%, 40%)`,
87
- 'syntax-fg': `hsl(230, 8%, 24%)`,
84
+ 'mono-1': `hsl(230, 8%, 24%)`,
85
+ 'mono-2': `hsl(230, 6%, 44%)`,
86
+ 'mono-3': `hsl(230, 4%, 64%)`,
87
+ 'syntax-accent': `hsl(230, 100%, 66%)`,
88
88
  'syntax-bg': `hsl(230, 1%, 98%)`,
89
- 'syntax-gutter': `hsl(230, 1%, 62%)`,
89
+ 'syntax-fg': `hsl(230, 8%, 24%)`,
90
90
  'syntax-guide': `hsla(230, 8%, 24%, 0.2)`,
91
- 'syntax-accent': `hsl(230, 100%, 66%)`,
91
+ 'syntax-gutter': `hsl(230, 1%, 62%)`,
92
92
  });
93
93
 
94
94
  export const codeDark = createTheme(code, {
95
- 'mono-1': `hsl(220, 14%, 71%)`,
96
- 'mono-2': `hsl(220, 9%, 55%)`,
97
- 'mono-3': `hsl(220, 10%, 40%)`,
98
95
  'hue-1': `hsl(187, 47%, 55%)`,
99
96
  'hue-2': `hsl(207, 82%, 66%)`,
100
97
  'hue-3': `hsl(286, 60%, 67%)`,
@@ -103,9 +100,12 @@ export const codeDark = createTheme(code, {
103
100
  'hue-5-2': `hsl(5, 48%, 51%)`,
104
101
  'hue-6': `hsl(29, 54%, 61%)`,
105
102
  'hue-6-2': `hsl(39, 67%, 69%)`,
106
- 'syntax-fg': `hsl(220, 14%, 71%)`,
103
+ 'mono-1': `hsl(220, 14%, 71%)`,
104
+ 'mono-2': `hsl(220, 9%, 55%)`,
105
+ 'mono-3': `hsl(220, 10%, 40%)`,
106
+ 'syntax-accent': `hsl(220, 100%, 66%)`,
107
107
  'syntax-bg': `hsl(220, 13%, 18%)`,
108
- 'syntax-gutter': `hsl(220, 14%, 45%)`,
108
+ 'syntax-fg': `hsl(220, 14%, 71%)`,
109
109
  'syntax-guide': `hsla(220, 14%, 71%, 0.15)`,
110
- 'syntax-accent': `hsl(220, 100%, 66%)`,
110
+ 'syntax-gutter': `hsl(220, 14%, 45%)`,
111
111
  });
@@ -66,53 +66,53 @@ import {
66
66
  * @icon Lucide.Palette
67
67
  */
68
68
  export const colors = createThemeContract({
69
- accentHue: null,
70
- backgroundHue: null,
71
69
  accent: null,
72
- accentLight: null,
73
- accentHover: null,
74
- accentContrast: null,
75
70
  accentBright: null,
71
+ accentContrast: null,
72
+ accentHover: null,
73
+ accentHue: null,
74
+ accentLight: null,
76
75
  background: null,
77
76
  background2: null,
77
+ backgroundExtreme: null,
78
78
  backgroundHaze: null,
79
79
  backgroundHover: null,
80
- backgroundExtreme: null,
80
+ backgroundHue: null,
81
+ border: null,
82
+ borderColor: null,
81
83
  foreground: null,
82
84
  foregroundBright: null,
83
85
  foregroundDim: null,
84
86
  foregroundExtreme: null,
85
- borderColor: null,
86
- border: null,
87
87
  shadow: null,
88
88
  });
89
89
 
90
90
  const common = {
91
+ accent: `oklch(50% .11 ${colors.accentHue})`,
92
+ accentBright: `oklch(57.37% 0.2178 ${colors.accentHue})`,
93
+ accentContrast: '#fff',
94
+ accentHover: `oklch(45% .1 ${colors.accentHue})`,
91
95
  accentHue: fallbackVar('var(--tinyWidgets-accentHue)', '8'),
96
+ accentLight: `oklch(71% .16 ${colors.accentHue})`,
92
97
  backgroundHue: fallbackVar(
93
98
  'var(--tinyWidgets-backgroundHue)',
94
99
  `calc(${colors.accentHue} + 180)`,
95
100
  ),
96
101
  border: `1px solid ${colors.borderColor}`,
97
- accent: `oklch(50% .11 ${colors.accentHue})`,
98
- accentLight: `oklch(71% .16 ${colors.accentHue})`,
99
- accentHover: `oklch(45% .1 ${colors.accentHue})`,
100
- accentBright: `oklch(57.37% 0.2178 ${colors.accentHue})`,
101
- accentContrast: '#fff',
102
102
  };
103
103
 
104
104
  export const colorsLight = createTheme(colors, {
105
105
  ...common,
106
106
  background: `oklch(99% .01 ${colors.backgroundHue})`,
107
107
  background2: `oklch(95% .01 ${colors.backgroundHue})`,
108
+ backgroundExtreme: '#fff',
108
109
  backgroundHaze: `oklch(99% .01 ${colors.backgroundHue} / .5)`,
109
110
  backgroundHover: `oklch(90% .01 ${colors.backgroundHue})`,
110
- backgroundExtreme: '#fff',
111
+ borderColor: `oklch(90% .01 ${colors.backgroundHue})`,
111
112
  foreground: `oklch(50% .01 ${colors.accentHue})`,
112
113
  foregroundBright: `oklch(10% .01 ${colors.accentHue})`,
113
114
  foregroundDim: `oklch(60% .01 ${colors.accentHue})`,
114
115
  foregroundExtreme: '#000',
115
- borderColor: `oklch(90% .01 ${colors.backgroundHue})`,
116
116
  shadow: '0 1px 4px 0 hsl(0 0 20 / .1)',
117
117
  });
118
118
  globalStyle(`html:has(${colorsLight})`, {
@@ -123,14 +123,14 @@ export const colorsDark = createTheme(colors, {
123
123
  ...common,
124
124
  background: `oklch(20% .01 ${colors.backgroundHue})`,
125
125
  background2: `oklch(15% .01 ${colors.backgroundHue})`,
126
+ backgroundExtreme: '#000',
126
127
  backgroundHaze: `oklch(21% 0% ${colors.backgroundHue} / .5)`,
127
128
  backgroundHover: `oklch(25% .01 ${colors.backgroundHue})`,
128
- backgroundExtreme: '#000',
129
+ borderColor: `oklch(30% .01 ${colors.backgroundHue})`,
129
130
  foreground: `oklch(85% .01 ${colors.accentHue})`,
130
131
  foregroundBright: `oklch(95% .01 ${colors.accentHue})`,
131
132
  foregroundDim: `oklch(60% .01 ${colors.accentHue})`,
132
133
  foregroundExtreme: '#fff',
133
- borderColor: `oklch(30% .01 ${colors.backgroundHue})`,
134
134
  shadow: '0 1px 4px 0 #000',
135
135
  });
136
136
  globalStyle(`html:has(${colorsDark})`, {
@@ -1,14 +1,14 @@
1
1
  import {createTheme, fallbackVar, style} from '@vanilla-extract/css';
2
2
 
3
3
  const classAndObject = createTheme({
4
- logo: fallbackVar('var(--tinyWidgets-logo)', '2rem'),
5
4
  avatar: fallbackVar('var(--tinyWidgets-avatar)', '2rem'),
5
+ footerHeight: fallbackVar('var(--tinyWidgets-footerHeight)', '2rem'),
6
6
  icon: fallbackVar('var(--tinyWidgets-icon)', '1rem'),
7
+ logo: fallbackVar('var(--tinyWidgets-logo)', '2rem'),
7
8
  padding: fallbackVar('var(--tinyWidgets-padding)', '1rem'),
8
9
  radius: fallbackVar('var(--tinyWidgets-radius)', '0.5rem'),
9
10
  sideNavWidth: fallbackVar('var(--tinyWidgets-sideNavWidth)', '20rem'),
10
11
  topNavHeight: fallbackVar('var(--tinyWidgets-topNavHeight)', '4rem'),
11
- footerHeight: fallbackVar('var(--tinyWidgets-footerHeight)', '2rem'),
12
12
  });
13
13
  export const dimensionsClass = classAndObject[0];
14
14
 
@@ -2,10 +2,10 @@ import {globalStyle} from '@vanilla-extract/css';
2
2
  import {small} from '../common/functions';
3
3
 
4
4
  globalStyle('*', {
5
- margin: 0,
6
- padding: 0,
7
5
  boxSizing: 'border-box',
8
6
  color: 'inherit',
9
7
  fontSize: 'inherit',
8
+ margin: 0,
9
+ padding: 0,
10
10
  ...small({fontSize: '0.9rem'}),
11
11
  });
package/src/index.css.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import './css/global.css.ts';
2
2
 
3
+ export {classes} from './css/classes.css.ts';
3
4
  export {code} from './css/code.css.ts';
4
5
  export {colors} from './css/colors.css.ts';
5
6
  export {dimensions} from './css/dimensions.css.ts';
package/src/index.ts CHANGED
@@ -21,4 +21,8 @@ export {TextInput} from './components/TextInput/index.tsx';
21
21
 
22
22
  export {classNames} from './common/functions.tsx';
23
23
  export {useDark} from './stores/LocalStore.tsx';
24
- export {useRoute, useSetRouteCallback} from './stores/RouteStore.tsx';
24
+ export {
25
+ useRoute,
26
+ useRouteParts,
27
+ useSetRouteCallback,
28
+ } from './stores/RouteStore.tsx';
@@ -1,25 +1,43 @@
1
1
  import {createCustomPersister} from 'tinybase/persisters/with-schemas';
2
2
  import * as UiReact from 'tinybase/ui-react/with-schemas';
3
- import {type NoTablesSchema, createStore} from 'tinybase/with-schemas';
3
+ import {createStore, type Store} from 'tinybase/with-schemas';
4
4
  import {READY, READY_SCHEMA} from './common';
5
5
 
6
6
  const ROUTE_STORE = 'tinywidgets/Route';
7
- const ROUTE = 'route';
7
+ const ROUTE_PARTS = 'routeParts';
8
+ const PARTS = 'parts';
8
9
 
10
+ const TABLES_SCHEMA = {
11
+ [ROUTE_PARTS]: {[PARTS]: {type: 'string', default: ''}},
12
+ } as const;
9
13
  const VALUES_SCHEMA = {
10
14
  ...READY_SCHEMA,
11
- [ROUTE]: {type: 'string', default: ''},
12
15
  } as const;
13
- type Schemas = [NoTablesSchema, typeof VALUES_SCHEMA];
16
+ type Schemas = [typeof TABLES_SCHEMA, typeof VALUES_SCHEMA];
17
+ type RouteStore = Store<Schemas>;
14
18
 
15
19
  const {
20
+ useCell,
16
21
  useCreateStore,
17
- useProvideStore,
18
22
  useCreatePersister,
23
+ useSetTablesCallback,
24
+ useProvideStore,
19
25
  useValue,
20
- useSetValueCallback,
21
26
  } = UiReact as UiReact.WithSchemas<Schemas>;
22
27
 
28
+ const getRouteTables = (route: string) => {
29
+ const parts = route.split('/');
30
+ return {
31
+ [ROUTE_PARTS]: Object.fromEntries([
32
+ ['0', {[PARTS]: route}],
33
+ ...parts.map((_, index) => [
34
+ index + 1 + '',
35
+ {[PARTS]: parts.slice(0, index + 1).join('/')},
36
+ ]),
37
+ ]),
38
+ };
39
+ };
40
+
23
41
  /**
24
42
  * The useRoute hook returns the current route, assuming the app is using the
25
43
  * basic TinyWidgets routing system.
@@ -37,7 +55,30 @@ const {
37
55
  * ```
38
56
  * This example shows the hook returning the current route.
39
57
  */
40
- export const useRoute = () => useValue(ROUTE, ROUTE_STORE);
58
+ export const useRoute = () => useRouteParts(0).join('/');
59
+
60
+ /**
61
+ * The useRouteParts hook returns the current route truncated to a number of
62
+ * slash-separated parts.
63
+ *
64
+ * This hook will only cause a rerender when the relevant part of the route
65
+ * changes. For example, if the route is `a/b/c/d`, then `useRouteParts(2)` will
66
+ * return `['a', 'b']` and will only cause a rerender when the first or second
67
+ * part of the route changes, but not when the third or fourth part changes.
68
+ *
69
+ * @param length The number of route parts to include. Use 0 for the full route.
70
+ * @returns The route path, or an empty string if it does not exist.
71
+ * @example
72
+ * ```tsx
73
+ * <ul>
74
+ * <li>{useRouteParts(1).join(' » ')}</li>
75
+ * <li>{useRouteParts(2).join(' » ')}</li>
76
+ * <li>{useRouteParts(0).join(' » ')}</li>
77
+ * </ul>
78
+ * ```
79
+ */
80
+ export const useRouteParts = (length: number) =>
81
+ useCell(ROUTE_PARTS, length + '', PARTS, ROUTE_STORE)?.split('/') ?? [];
41
82
 
42
83
  /**
43
84
  * The useSetRouteCallback hook a callback for setting the current route,
@@ -60,14 +101,14 @@ export const useRoute = () => useValue(ROUTE, ROUTE_STORE);
60
101
  * route when called as a click handler.
61
102
  */
62
103
  export const useSetRouteCallback = () =>
63
- useSetValueCallback(ROUTE, (route: string) => route, [], ROUTE_STORE);
104
+ useSetTablesCallback(getRouteTables, [], ROUTE_STORE);
64
105
 
65
106
  export const useRouteStoreIsReady = () =>
66
107
  useValue(READY, ROUTE_STORE) as boolean;
67
108
 
68
109
  export const RouteStore = () => {
69
110
  const routeStore = useCreateStore(() =>
70
- createStore().setValuesSchema(VALUES_SCHEMA),
111
+ createStore().setSchema(TABLES_SCHEMA, VALUES_SCHEMA),
71
112
  );
72
113
  useProvideStore(ROUTE_STORE, routeStore);
73
114
 
@@ -76,12 +117,9 @@ export const RouteStore = () => {
76
117
  (routeStore) =>
77
118
  createCustomPersister(
78
119
  routeStore,
79
- async () => [{}, {route: location.hash.slice(1), ready: true}],
120
+ async () => [getRouteTables(location.hash.slice(1)), {ready: true}],
80
121
  async (getContent) => {
81
- const route = getContent()[1].route;
82
- if (route) {
83
- location.hash = route;
84
- }
122
+ location.hash = getContent()[0][ROUTE_PARTS]?.['0']?.[PARTS] ?? '';
85
123
  },
86
124
  (listener) => {
87
125
  const hashListener = () => listener();