webadwaita 0.1.0 → 0.2.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/LICENSE +21 -0
- package/README.md +119 -0
- package/babel.config.cjs +23 -0
- package/package.json +34 -32
- package/src/Box/index.tsx +105 -0
- package/src/Button/index.tsx +153 -0
- package/src/Card/index.tsx +67 -0
- package/src/Checkbox/index.tsx +119 -0
- package/src/Entry/index.tsx +94 -0
- package/src/HeaderBar/index.tsx +78 -0
- package/src/Switch/index.tsx +89 -0
- package/src/index.ts +23 -0
- package/src/theme.ts +19 -0
- package/src/tokens.css.ts +91 -0
- package/dist/Button.d.mts +0 -5
- package/dist/Button.d.ts +0 -5
- package/dist/Button.js +0 -63
- package/dist/Button.mjs +0 -41
- package/dist/button.d.mts +0 -5
- package/dist/button.d.ts +0 -5
- package/dist/button.js +0 -63
- package/dist/button.mjs +0 -15
- package/dist/chunk-FWCSY2DS.mjs +0 -37
- package/dist/styles.css +0 -346
- package/dist/styles.d.mts +0 -2
- package/dist/styles.d.ts +0 -2
- package/dist/styles.js +0 -1
- package/dist/styles.mjs +0 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 WebAdwaita contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# WebAdwaita
|
|
2
|
+
|
|
3
|
+
Cross-platform React component library inspired by GNOME's [libadwaita](https://gnome.pages.gitlab.gnome.org/libadwaita/), built on [react-strict-dom](https://github.com/facebook/react-strict-dom). One TypeScript codebase renders on **web**, **iOS**, and **Android**.
|
|
4
|
+
|
|
5
|
+
## Why react-strict-dom?
|
|
6
|
+
|
|
7
|
+
react-strict-dom is Meta's strict subset of `<html.*>` primitives plus a `css.create` API that compiles to **StyleX on web** and **React Native styles on native**. WebAdwaita uses it so every component is platform-neutral by default — no `.web.tsx` / `.native.tsx` forks needed.
|
|
8
|
+
|
|
9
|
+
## Components
|
|
10
|
+
|
|
11
|
+
| Component | Purpose |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| `Box` | Flex layout primitive (direction / gap / padding / align / justify). |
|
|
14
|
+
| `Button` | Variants: `default`, `suggested`, `destructive`, `flat`, `pill`. Sizes: `sm` / `md` / `lg`. |
|
|
15
|
+
| `Entry` | Text input with focus underline (Adwaita `AdwEntry`). |
|
|
16
|
+
| `Switch` | Pill-shaped toggle with sliding thumb. |
|
|
17
|
+
| `Checkbox` | Square checkbox with optional label. |
|
|
18
|
+
| `Card` | Surface with optional title; `boxed` and `flat` variants. |
|
|
19
|
+
| `HeaderBar` | Top bar with leading / centered title / trailing slots. |
|
|
20
|
+
|
|
21
|
+
All design tokens live in [`src/tokens.css.ts`](./src/tokens.css.ts) — palette, radius, spacing, typography, motion. The `.css.ts` filename is required: react-strict-dom's Babel preset configures StyleX to treat `.css` as the token-file extension, so any file holding `css.defineConsts` / `css.defineVars` calls must be named `<name>.css.ts`.
|
|
22
|
+
|
|
23
|
+
[`src/theme.ts`](./src/theme.ts) re-exports the same tokens for runtime use (e.g. computed inline styles in app code).
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
npm install webadwaita react-strict-dom
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Peer deps: `react ^19`, plus `react-dom` (web) or `react-native >= 0.79` (native).
|
|
32
|
+
|
|
33
|
+
## Setup
|
|
34
|
+
|
|
35
|
+
**WebAdwaita ships TypeScript source — not a pre-built bundle.** This is the same pattern Meta's `apps/example-ui` uses, and it's deliberate: the StyleX class names baked into a pre-built bundle would not match the CSS rules extracted by the consumer's PostCSS pipeline. Shipping source lets a single Babel pass at consumer build time produce both the class names *and* the matching CSS.
|
|
36
|
+
|
|
37
|
+
That means your app's bundler must run the `react-strict-dom` Babel preset over `node_modules/webadwaita/src` and your PostCSS config must scan the same files for CSS extraction.
|
|
38
|
+
|
|
39
|
+
**Vite (web):** see [playground/vite.config.ts](./playground/vite.config.ts) and [playground/postcss.config.cjs](./playground/postcss.config.cjs) for the working setup. The key bits:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
// vite.config.ts
|
|
43
|
+
plugins: [
|
|
44
|
+
react({
|
|
45
|
+
babel: { configFile: true },
|
|
46
|
+
include: [/\.[jt]sx?$/, '<...>/node_modules/webadwaita/src/**']
|
|
47
|
+
}),
|
|
48
|
+
babel({ filter: /\.[jt]sx?$/ })
|
|
49
|
+
]
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
// postcss.config.cjs
|
|
54
|
+
'react-strict-dom/postcss-plugin': {
|
|
55
|
+
include: [
|
|
56
|
+
'src/**/*.{ts,tsx}',
|
|
57
|
+
'node_modules/webadwaita/src/**/*.{ts,tsx}'
|
|
58
|
+
],
|
|
59
|
+
babelConfig
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Then add a `strict.css` file with the directive `@react-strict-dom;` and import it from your app's entry point — PostCSS replaces the directive with the generated CSS.
|
|
64
|
+
|
|
65
|
+
**Expo / Metro (native):** Metro picks up the source automatically. Add the `react-strict-dom/babel-preset` to your `babel.config.js` exactly like the [react-strict-dom expo-app example](https://github.com/facebook/react-strict-dom/blob/main/apps/expo-app/babel.config.js).
|
|
66
|
+
|
|
67
|
+
## Use
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
import { Box, Button, Card, Entry, Switch } from 'webadwaita';
|
|
71
|
+
|
|
72
|
+
export function Settings() {
|
|
73
|
+
const [name, setName] = useState('');
|
|
74
|
+
const [enabled, setEnabled] = useState(true);
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<Card title="Profile">
|
|
78
|
+
<Box gap="md">
|
|
79
|
+
<Entry placeholder="Your name" value={name} onChange={setName} />
|
|
80
|
+
<Box direction="row" align="center" gap="sm">
|
|
81
|
+
<Switch checked={enabled} onChange={setEnabled} />
|
|
82
|
+
<span>Enabled</span>
|
|
83
|
+
</Box>
|
|
84
|
+
<Button variant="suggested">Save</Button>
|
|
85
|
+
</Box>
|
|
86
|
+
</Card>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Develop
|
|
92
|
+
|
|
93
|
+
There is no library build step — consumers transform the TypeScript source directly. `npm run typecheck` is the only "build-like" command for the lib itself.
|
|
94
|
+
|
|
95
|
+
Two demo apps live under `apps/`:
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
# library
|
|
99
|
+
npm install
|
|
100
|
+
npm run typecheck
|
|
101
|
+
|
|
102
|
+
# web Storybook (Vite-powered)
|
|
103
|
+
npm --prefix apps/storybook-web install
|
|
104
|
+
npm run storybook:web # opens http://localhost:6006
|
|
105
|
+
|
|
106
|
+
# native Storybook (Expo + @storybook/react-native v8, on-device UI)
|
|
107
|
+
npm --prefix apps/storybook-native install
|
|
108
|
+
npm run storybook:native # then press i / a / w in the Expo CLI
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The native app embeds the Storybook UI directly — open the Expo Go app on a phone (or a simulator) and pick a component from the on-device sidebar. After adding/removing `.stories.tsx` files, regenerate the registry:
|
|
112
|
+
|
|
113
|
+
```sh
|
|
114
|
+
npm --prefix apps/storybook-native run storybook-generate
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
MIT — see [LICENSE](./LICENSE). Inspired by `react-strict-dom`'s `apps/example-ui/` and the libadwaita design system.
|
package/babel.config.cjs
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared babel config used by Vite (web library build) and consumers
|
|
3
|
+
* who want to bundle WebAdwaita source through the react-strict-dom
|
|
4
|
+
* preset (e.g. Metro / Vite / Next.js postcss pipelines).
|
|
5
|
+
*/
|
|
6
|
+
const dev = process.env.NODE_ENV !== 'production';
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
parserOpts: {
|
|
10
|
+
plugins: ['typescript', 'jsx']
|
|
11
|
+
},
|
|
12
|
+
presets: [
|
|
13
|
+
[
|
|
14
|
+
'react-strict-dom/babel-preset',
|
|
15
|
+
{
|
|
16
|
+
debug: dev,
|
|
17
|
+
dev,
|
|
18
|
+
rootDir: process.cwd(),
|
|
19
|
+
platform: process.env.WEBADWAITA_PLATFORM || 'web'
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
]
|
|
23
|
+
};
|
package/package.json
CHANGED
|
@@ -1,43 +1,45 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webadwaita",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Cross-platform Adwaita-styled UI components built on react-strict-dom.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"files": [
|
|
8
|
+
"src",
|
|
9
|
+
"babel.config.cjs",
|
|
10
|
+
"README.md",
|
|
11
|
+
"LICENSE"
|
|
12
|
+
],
|
|
13
|
+
"main": "./src/index.ts",
|
|
6
14
|
"exports": {
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"require": "./dist/button.js"
|
|
11
|
-
},
|
|
12
|
-
"./styles": {
|
|
13
|
-
"import": "./dist/styles.mjs",
|
|
14
|
-
"require": "./dist/styles.js"
|
|
15
|
-
},
|
|
16
|
-
"./styles.css": "./dist/styles.css"
|
|
15
|
+
".": "./src/index.ts",
|
|
16
|
+
"./babel-config": "./babel.config.cjs",
|
|
17
|
+
"./package.json": "./package.json"
|
|
17
18
|
},
|
|
18
19
|
"scripts": {
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"clean": "rm -rf .turbo node_modules dist"
|
|
20
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
21
|
+
"storybook:web": "npm --prefix apps/storybook-web run storybook",
|
|
22
|
+
"storybook:native": "npm --prefix apps/storybook-native run start"
|
|
23
23
|
},
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"typescript": "5.5.4"
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"react": "^19.0.0",
|
|
26
|
+
"react-dom": "^19.0.0",
|
|
27
|
+
"react-native": ">=0.79.0",
|
|
28
|
+
"react-strict-dom": ">=0.0.50"
|
|
29
|
+
},
|
|
30
|
+
"peerDependenciesMeta": {
|
|
31
|
+
"react-dom": { "optional": true },
|
|
32
|
+
"react-native": { "optional": true }
|
|
34
33
|
},
|
|
35
|
-
"
|
|
36
|
-
"react": "^
|
|
37
|
-
"
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/react": "^19.2.5",
|
|
36
|
+
"@types/react-dom": "^19.2.3",
|
|
37
|
+
"react": "^19.2.0",
|
|
38
|
+
"react-dom": "^19.2.0",
|
|
39
|
+
"react-strict-dom": "^0.0.55",
|
|
40
|
+
"typescript": "~5.9.3"
|
|
38
41
|
},
|
|
39
|
-
"
|
|
40
|
-
|
|
41
|
-
"access": "public"
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=20.11.0"
|
|
42
44
|
}
|
|
43
45
|
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { css, html } from 'react-strict-dom';
|
|
3
|
+
import { spacing } from '../tokens.css';
|
|
4
|
+
|
|
5
|
+
type SpacingKey = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl';
|
|
6
|
+
type Direction = 'row' | 'column';
|
|
7
|
+
type Align = 'flex-start' | 'center' | 'flex-end' | 'stretch' | 'baseline';
|
|
8
|
+
type Justify =
|
|
9
|
+
| 'flex-start'
|
|
10
|
+
| 'center'
|
|
11
|
+
| 'flex-end'
|
|
12
|
+
| 'space-between'
|
|
13
|
+
| 'space-around'
|
|
14
|
+
| 'space-evenly';
|
|
15
|
+
|
|
16
|
+
export type BoxProps = {
|
|
17
|
+
children?: React.ReactNode;
|
|
18
|
+
direction?: Direction;
|
|
19
|
+
gap?: SpacingKey;
|
|
20
|
+
padding?: SpacingKey;
|
|
21
|
+
align?: Align;
|
|
22
|
+
justify?: Justify;
|
|
23
|
+
grow?: boolean;
|
|
24
|
+
wrap?: boolean;
|
|
25
|
+
style?: any;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Layout primitive — a flex container, the building block for everything else.
|
|
30
|
+
* Adwaita-inspired apps lean heavily on simple horizontal/vertical stacks.
|
|
31
|
+
*/
|
|
32
|
+
export function Box(props: BoxProps) {
|
|
33
|
+
const {
|
|
34
|
+
children,
|
|
35
|
+
direction = 'column',
|
|
36
|
+
gap,
|
|
37
|
+
padding,
|
|
38
|
+
align,
|
|
39
|
+
justify,
|
|
40
|
+
grow,
|
|
41
|
+
wrap,
|
|
42
|
+
style
|
|
43
|
+
} = props;
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<html.div
|
|
47
|
+
style={[
|
|
48
|
+
styles.base,
|
|
49
|
+
direction === 'row' ? styles.row : styles.column,
|
|
50
|
+
wrap && styles.wrap,
|
|
51
|
+
grow && styles.grow,
|
|
52
|
+
align != null && alignStyles[align],
|
|
53
|
+
justify != null && justifyStyles[justify],
|
|
54
|
+
gap != null && gapStyles[gap],
|
|
55
|
+
padding != null && paddingStyles[padding],
|
|
56
|
+
style
|
|
57
|
+
]}
|
|
58
|
+
>
|
|
59
|
+
{children}
|
|
60
|
+
</html.div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const styles = css.create({
|
|
65
|
+
base: { display: 'flex', minWidth: 0 },
|
|
66
|
+
row: { flexDirection: 'row' },
|
|
67
|
+
column: { flexDirection: 'column' },
|
|
68
|
+
wrap: { flexWrap: 'wrap' },
|
|
69
|
+
grow: { flexGrow: 1 }
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const alignStyles = css.create({
|
|
73
|
+
'flex-start': { alignItems: 'flex-start' },
|
|
74
|
+
center: { alignItems: 'center' },
|
|
75
|
+
'flex-end': { alignItems: 'flex-end' },
|
|
76
|
+
stretch: { alignItems: 'stretch' },
|
|
77
|
+
baseline: { alignItems: 'baseline' }
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const justifyStyles = css.create({
|
|
81
|
+
'flex-start': { justifyContent: 'flex-start' },
|
|
82
|
+
center: { justifyContent: 'center' },
|
|
83
|
+
'flex-end': { justifyContent: 'flex-end' },
|
|
84
|
+
'space-between': { justifyContent: 'space-between' },
|
|
85
|
+
'space-around': { justifyContent: 'space-around' },
|
|
86
|
+
'space-evenly': { justifyContent: 'space-evenly' }
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const gapStyles = css.create({
|
|
90
|
+
xs: { gap: spacing.xs },
|
|
91
|
+
sm: { gap: spacing.sm },
|
|
92
|
+
md: { gap: spacing.md },
|
|
93
|
+
lg: { gap: spacing.lg },
|
|
94
|
+
xl: { gap: spacing.xl },
|
|
95
|
+
xxl: { gap: spacing.xxl }
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const paddingStyles = css.create({
|
|
99
|
+
xs: { padding: spacing.xs },
|
|
100
|
+
sm: { padding: spacing.sm },
|
|
101
|
+
md: { padding: spacing.md },
|
|
102
|
+
lg: { padding: spacing.lg },
|
|
103
|
+
xl: { padding: spacing.xl },
|
|
104
|
+
xxl: { padding: spacing.xxl }
|
|
105
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { css, html } from 'react-strict-dom';
|
|
3
|
+
import { palette, radius, spacing, typography, motion } from '../tokens.css';
|
|
4
|
+
|
|
5
|
+
export type ButtonVariant =
|
|
6
|
+
| 'default'
|
|
7
|
+
| 'suggested'
|
|
8
|
+
| 'destructive'
|
|
9
|
+
| 'flat'
|
|
10
|
+
| 'pill';
|
|
11
|
+
|
|
12
|
+
export type ButtonSize = 'sm' | 'md' | 'lg';
|
|
13
|
+
|
|
14
|
+
export type ButtonProps = {
|
|
15
|
+
children?: React.ReactNode;
|
|
16
|
+
variant?: ButtonVariant;
|
|
17
|
+
size?: ButtonSize;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
fullWidth?: boolean;
|
|
20
|
+
onPress?: () => void;
|
|
21
|
+
type?: 'button' | 'submit';
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Adwaita-style button. Variants follow libadwaita conventions:
|
|
26
|
+
* - default: flat-ish neutral surface
|
|
27
|
+
* - suggested: blue accent (primary)
|
|
28
|
+
* - destructive: red, for irreversible actions
|
|
29
|
+
* - flat: no background, only on hover
|
|
30
|
+
* - pill: fully rounded (Adwaita "circular" buttons)
|
|
31
|
+
*/
|
|
32
|
+
export function Button(props: ButtonProps) {
|
|
33
|
+
const {
|
|
34
|
+
children,
|
|
35
|
+
variant = 'default',
|
|
36
|
+
size = 'md',
|
|
37
|
+
disabled = false,
|
|
38
|
+
fullWidth = false,
|
|
39
|
+
onPress,
|
|
40
|
+
type = 'button'
|
|
41
|
+
} = props;
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<html.button
|
|
45
|
+
type={type}
|
|
46
|
+
disabled={disabled}
|
|
47
|
+
onClick={onPress}
|
|
48
|
+
style={[
|
|
49
|
+
styles.base,
|
|
50
|
+
sizeStyles[size],
|
|
51
|
+
variantStyles[variant],
|
|
52
|
+
fullWidth && styles.fullWidth,
|
|
53
|
+
disabled && styles.disabled
|
|
54
|
+
]}
|
|
55
|
+
>
|
|
56
|
+
<html.span style={[styles.label, labelSizeStyles[size]]}>
|
|
57
|
+
{children}
|
|
58
|
+
</html.span>
|
|
59
|
+
</html.button>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const styles = css.create({
|
|
64
|
+
base: {
|
|
65
|
+
display: 'flex',
|
|
66
|
+
alignSelf: 'flex-start',
|
|
67
|
+
alignItems: 'center',
|
|
68
|
+
justifyContent: 'center',
|
|
69
|
+
flexDirection: 'row',
|
|
70
|
+
borderWidth: 0,
|
|
71
|
+
cursor: 'pointer',
|
|
72
|
+
transitionProperty: 'background-color, box-shadow, color',
|
|
73
|
+
transitionDuration: motion.durationFast,
|
|
74
|
+
transitionTimingFunction: motion.easing,
|
|
75
|
+
userSelect: 'none'
|
|
76
|
+
},
|
|
77
|
+
label: {
|
|
78
|
+
fontFamily: typography.fontFamily,
|
|
79
|
+
fontWeight: typography.weightStrong,
|
|
80
|
+
textAlign: 'center'
|
|
81
|
+
},
|
|
82
|
+
fullWidth: { alignSelf: 'stretch' },
|
|
83
|
+
disabled: { opacity: 0.5, cursor: 'not-allowed' }
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const sizeStyles = css.create({
|
|
87
|
+
sm: {
|
|
88
|
+
paddingInline: spacing.md,
|
|
89
|
+
borderRadius: radius.sm,
|
|
90
|
+
height: 28
|
|
91
|
+
},
|
|
92
|
+
md: {
|
|
93
|
+
paddingInline: spacing.lg,
|
|
94
|
+
borderRadius: radius.md,
|
|
95
|
+
height: 36
|
|
96
|
+
},
|
|
97
|
+
lg: {
|
|
98
|
+
paddingInline: spacing.xl,
|
|
99
|
+
borderRadius: radius.md,
|
|
100
|
+
height: 44
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const labelSizeStyles = css.create({
|
|
105
|
+
sm: { fontSize: typography.captionSize, lineHeight: typography.captionLineHeight },
|
|
106
|
+
md: { fontSize: typography.bodySize, lineHeight: typography.bodyLineHeight },
|
|
107
|
+
lg: { fontSize: typography.title4Size, lineHeight: typography.title4LineHeight }
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const variantStyles = css.create({
|
|
111
|
+
default: {
|
|
112
|
+
backgroundColor: {
|
|
113
|
+
default: palette.subtleBg,
|
|
114
|
+
':hover': palette.subtleBgHover,
|
|
115
|
+
':active': palette.subtleBgActive
|
|
116
|
+
},
|
|
117
|
+
color: palette.fg
|
|
118
|
+
},
|
|
119
|
+
suggested: {
|
|
120
|
+
backgroundColor: {
|
|
121
|
+
default: palette.accentBg,
|
|
122
|
+
':hover': palette.accentBgHover,
|
|
123
|
+
':active': palette.accentBgActive
|
|
124
|
+
},
|
|
125
|
+
color: palette.accentFg
|
|
126
|
+
},
|
|
127
|
+
destructive: {
|
|
128
|
+
backgroundColor: {
|
|
129
|
+
default: palette.destructiveBg,
|
|
130
|
+
':hover': palette.destructiveBgHover,
|
|
131
|
+
':active': palette.destructiveBgActive
|
|
132
|
+
},
|
|
133
|
+
color: palette.destructiveFg
|
|
134
|
+
},
|
|
135
|
+
flat: {
|
|
136
|
+
backgroundColor: {
|
|
137
|
+
default: 'transparent',
|
|
138
|
+
':hover': palette.subtleBg,
|
|
139
|
+
':active': palette.subtleBgHover
|
|
140
|
+
},
|
|
141
|
+
color: palette.fg
|
|
142
|
+
},
|
|
143
|
+
pill: {
|
|
144
|
+
backgroundColor: {
|
|
145
|
+
default: palette.subtleBg,
|
|
146
|
+
':hover': palette.subtleBgHover,
|
|
147
|
+
':active': palette.subtleBgActive
|
|
148
|
+
},
|
|
149
|
+
color: palette.fg,
|
|
150
|
+
borderRadius: radius.pill,
|
|
151
|
+
paddingInline: spacing.xl
|
|
152
|
+
}
|
|
153
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { css, html } from 'react-strict-dom';
|
|
3
|
+
import { palette, radius, spacing, typography } from '../tokens.css';
|
|
4
|
+
|
|
5
|
+
export type CardProps = {
|
|
6
|
+
children?: React.ReactNode;
|
|
7
|
+
title?: React.ReactNode;
|
|
8
|
+
/** "boxed" matches AdwBoxedList — solid surface w/ visible border. */
|
|
9
|
+
variant?: 'boxed' | 'flat';
|
|
10
|
+
padding?: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Adwaita-style card surface. Default has a soft shadow + rounded corners;
|
|
15
|
+
* "flat" drops the shadow for use inside other surfaces.
|
|
16
|
+
*/
|
|
17
|
+
export function Card(props: CardProps) {
|
|
18
|
+
const { children, title, variant = 'boxed', padding = true } = props;
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<html.div style={[styles.card, variant === 'boxed' ? styles.boxed : styles.flat]}>
|
|
22
|
+
{title != null && (
|
|
23
|
+
<html.div style={styles.header}>
|
|
24
|
+
<html.h3 style={styles.titleText}>{title}</html.h3>
|
|
25
|
+
</html.div>
|
|
26
|
+
)}
|
|
27
|
+
<html.div style={padding ? styles.body : null}>{children}</html.div>
|
|
28
|
+
</html.div>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const styles = css.create({
|
|
33
|
+
card: {
|
|
34
|
+
backgroundColor: palette.cardBg,
|
|
35
|
+
borderRadius: radius.lg,
|
|
36
|
+
overflow: 'hidden',
|
|
37
|
+
display: 'flex',
|
|
38
|
+
flexDirection: 'column'
|
|
39
|
+
},
|
|
40
|
+
boxed: {
|
|
41
|
+
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04)',
|
|
42
|
+
borderWidth: 1,
|
|
43
|
+
borderStyle: 'solid',
|
|
44
|
+
borderColor: palette.border
|
|
45
|
+
},
|
|
46
|
+
flat: {
|
|
47
|
+
borderWidth: 1,
|
|
48
|
+
borderStyle: 'solid',
|
|
49
|
+
borderColor: palette.border
|
|
50
|
+
},
|
|
51
|
+
header: {
|
|
52
|
+
paddingBlock: spacing.md,
|
|
53
|
+
paddingInline: spacing.lg,
|
|
54
|
+
borderBottomWidth: 1,
|
|
55
|
+
borderBottomStyle: 'solid',
|
|
56
|
+
borderBottomColor: palette.border
|
|
57
|
+
},
|
|
58
|
+
titleText: {
|
|
59
|
+
margin: 0,
|
|
60
|
+
fontFamily: typography.fontFamily,
|
|
61
|
+
fontSize: typography.title4Size,
|
|
62
|
+
lineHeight: typography.title4LineHeight,
|
|
63
|
+
fontWeight: typography.weightBold,
|
|
64
|
+
color: palette.fg
|
|
65
|
+
},
|
|
66
|
+
body: { padding: spacing.lg }
|
|
67
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { css, html } from 'react-strict-dom';
|
|
3
|
+
import { palette, radius, motion, typography, spacing } from '../tokens.css';
|
|
4
|
+
|
|
5
|
+
export type CheckboxProps = {
|
|
6
|
+
checked: boolean;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
onChange?: (checked: boolean) => void;
|
|
9
|
+
label?: React.ReactNode;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Adwaita-style checkbox. Solid blue square with a unicode check when on,
|
|
14
|
+
* subtle outlined square when off.
|
|
15
|
+
*/
|
|
16
|
+
export function Checkbox(props: CheckboxProps) {
|
|
17
|
+
const { checked, disabled = false, onChange, label } = props;
|
|
18
|
+
|
|
19
|
+
const toggle = () => {
|
|
20
|
+
if (!disabled) onChange?.(!checked);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const box = (
|
|
24
|
+
<html.span style={[styles.box, checked ? styles.boxOn : styles.boxOff]}>
|
|
25
|
+
{checked && <html.span style={styles.check}>{'✓'}</html.span>}
|
|
26
|
+
</html.span>
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
if (label == null) {
|
|
30
|
+
return (
|
|
31
|
+
<html.button
|
|
32
|
+
type="button"
|
|
33
|
+
role="checkbox"
|
|
34
|
+
aria-checked={checked}
|
|
35
|
+
disabled={disabled}
|
|
36
|
+
onClick={toggle}
|
|
37
|
+
style={[styles.bareButton, disabled && styles.disabled]}
|
|
38
|
+
>
|
|
39
|
+
{box}
|
|
40
|
+
</html.button>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<html.button
|
|
46
|
+
type="button"
|
|
47
|
+
role="checkbox"
|
|
48
|
+
aria-checked={checked}
|
|
49
|
+
disabled={disabled}
|
|
50
|
+
onClick={toggle}
|
|
51
|
+
style={[styles.row, disabled && styles.disabled]}
|
|
52
|
+
>
|
|
53
|
+
{box}
|
|
54
|
+
<html.span style={styles.label}>{label}</html.span>
|
|
55
|
+
</html.button>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const SIZE = 18;
|
|
60
|
+
|
|
61
|
+
const styles = css.create({
|
|
62
|
+
bareButton: {
|
|
63
|
+
backgroundColor: 'transparent',
|
|
64
|
+
borderWidth: 0,
|
|
65
|
+
padding: 0,
|
|
66
|
+
cursor: 'pointer'
|
|
67
|
+
},
|
|
68
|
+
row: {
|
|
69
|
+
display: 'flex',
|
|
70
|
+
alignSelf: 'flex-start',
|
|
71
|
+
flexDirection: 'row',
|
|
72
|
+
alignItems: 'center',
|
|
73
|
+
gap: spacing.sm,
|
|
74
|
+
backgroundColor: 'transparent',
|
|
75
|
+
borderWidth: 0,
|
|
76
|
+
padding: 0,
|
|
77
|
+
cursor: 'pointer'
|
|
78
|
+
},
|
|
79
|
+
box: {
|
|
80
|
+
display: 'flex',
|
|
81
|
+
width: SIZE,
|
|
82
|
+
height: SIZE,
|
|
83
|
+
borderRadius: radius.sm,
|
|
84
|
+
alignItems: 'center',
|
|
85
|
+
justifyContent: 'center',
|
|
86
|
+
transitionProperty: 'background-color, border-color',
|
|
87
|
+
transitionDuration: motion.durationFast,
|
|
88
|
+
transitionTimingFunction: motion.easing
|
|
89
|
+
},
|
|
90
|
+
boxOff: {
|
|
91
|
+
backgroundColor: 'transparent',
|
|
92
|
+
borderWidth: 1,
|
|
93
|
+
borderStyle: 'solid',
|
|
94
|
+
borderColor: {
|
|
95
|
+
default: palette.borderStrong,
|
|
96
|
+
':hover': palette.fg
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
boxOn: {
|
|
100
|
+
backgroundColor: {
|
|
101
|
+
default: palette.accentBg,
|
|
102
|
+
':hover': palette.accentBgHover
|
|
103
|
+
},
|
|
104
|
+
borderWidth: 0
|
|
105
|
+
},
|
|
106
|
+
check: {
|
|
107
|
+
color: palette.accentFg,
|
|
108
|
+
fontSize: 14,
|
|
109
|
+
lineHeight: 1,
|
|
110
|
+
fontWeight: 700
|
|
111
|
+
},
|
|
112
|
+
label: {
|
|
113
|
+
color: palette.fg,
|
|
114
|
+
fontFamily: typography.fontFamily,
|
|
115
|
+
fontSize: typography.bodySize,
|
|
116
|
+
lineHeight: typography.bodyLineHeight
|
|
117
|
+
},
|
|
118
|
+
disabled: { opacity: 0.5, cursor: 'not-allowed' }
|
|
119
|
+
});
|