@dezkareid/components 0.0.0 → 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/.releaserc +18 -0
- package/.turbo/turbo-build.log +7 -0
- package/.turbo/turbo-test.log +17 -0
- package/AGENTS.md +174 -0
- package/CHANGELOG.md +12 -0
- package/README.md +213 -5
- package/dist/_virtual/_commonjsHelpers.js +6 -0
- package/dist/_virtual/_commonjsHelpers.js.map +1 -0
- package/dist/_virtual/index.js +8 -0
- package/dist/_virtual/index.js.map +1 -0
- package/dist/_virtual/index2.js +4 -0
- package/dist/_virtual/index2.js.map +1 -0
- package/dist/astro/index.d.ts +5 -0
- package/dist/astro/index.d.ts.map +1 -0
- package/dist/components.min.css +1 -0
- package/dist/css/button.module.css.js +4 -0
- package/dist/css/button.module.css.js.map +1 -0
- package/dist/css/card.module.css.js +4 -0
- package/dist/css/card.module.css.js.map +1 -0
- package/dist/css/index.d.ts +5 -0
- package/dist/css/index.d.ts.map +1 -0
- package/dist/css/tag.module.css.js +4 -0
- package/dist/css/tag.module.css.js.map +1 -0
- package/dist/css/theme-toggle.module.css.js +4 -0
- package/dist/css/theme-toggle.module.css.js.map +1 -0
- package/dist/node_modules/.pnpm/classnames@2.5.1/node_modules/classnames/index.js +86 -0
- package/dist/node_modules/.pnpm/classnames@2.5.1/node_modules/classnames/index.js.map +1 -0
- package/dist/react/Button/index.d.ts +6 -0
- package/dist/react/Button/index.d.ts.map +1 -0
- package/dist/react/Button/index.js +10 -0
- package/dist/react/Button/index.js.map +1 -0
- package/dist/react/Card/index.d.ts +6 -0
- package/dist/react/Card/index.d.ts.map +1 -0
- package/dist/react/Card/index.js +10 -0
- package/dist/react/Card/index.js.map +1 -0
- package/dist/react/Tag/index.d.ts +6 -0
- package/dist/react/Tag/index.d.ts.map +1 -0
- package/dist/react/Tag/index.js +10 -0
- package/dist/react/Tag/index.js.map +1 -0
- package/dist/react/ThemeToggle/index.d.ts +2 -0
- package/dist/react/ThemeToggle/index.d.ts.map +1 -0
- package/dist/react/ThemeToggle/index.js +25 -0
- package/dist/react/ThemeToggle/index.js.map +1 -0
- package/dist/react/index.d.ts +5 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +5 -0
- package/dist/react/index.js.map +1 -0
- package/dist/shared/js/theme.d.ts +5 -0
- package/dist/shared/js/theme.d.ts.map +1 -0
- package/dist/shared/js/theme.js +22 -0
- package/dist/shared/js/theme.js.map +1 -0
- package/dist/shared/types/button.d.ts +8 -0
- package/dist/shared/types/button.d.ts.map +1 -0
- package/dist/shared/types/card.d.ts +5 -0
- package/dist/shared/types/card.d.ts.map +1 -0
- package/dist/shared/types/tag.d.ts +5 -0
- package/dist/shared/types/tag.d.ts.map +1 -0
- package/dist/shared/types/theme-toggle.d.ts +4 -0
- package/dist/shared/types/theme-toggle.d.ts.map +1 -0
- package/dist/vue/index.d.ts +5 -0
- package/dist/vue/index.d.ts.map +1 -0
- package/done/2026-03-03-design-system-components/osddt.plan.md +233 -0
- package/done/2026-03-03-design-system-components/osddt.spec.md +90 -0
- package/done/2026-03-03-design-system-components/osddt.tasks.md +100 -0
- package/package.json +76 -6
- package/rollup.config.mjs +32 -0
- package/setupTests.ts +1 -0
- package/src/astro/Button/index.astro +35 -0
- package/src/astro/Card/index.astro +23 -0
- package/src/astro/Tag/index.astro +23 -0
- package/src/astro/ThemeToggle/index.astro +63 -0
- package/src/astro/index.ts +4 -0
- package/src/css/button.module.css +90 -0
- package/src/css/card.module.css +30 -0
- package/src/css/index.ts +4 -0
- package/src/css/tag.module.css +33 -0
- package/src/css/theme-toggle.module.css +38 -0
- package/src/declarations.d.ts +19 -0
- package/src/react/Button/index.test.tsx +59 -0
- package/src/react/Button/index.tsx +31 -0
- package/src/react/Card/index.test.tsx +38 -0
- package/src/react/Card/index.tsx +14 -0
- package/src/react/Tag/index.test.tsx +35 -0
- package/src/react/Tag/index.tsx +14 -0
- package/src/react/ThemeToggle/index.test.tsx +84 -0
- package/src/react/ThemeToggle/index.tsx +36 -0
- package/src/react/index.ts +4 -0
- package/src/shared/js/theme.ts +22 -0
- package/src/shared/types/button.ts +8 -0
- package/src/shared/types/card.ts +5 -0
- package/src/shared/types/tag.ts +5 -0
- package/src/shared/types/theme-toggle.ts +5 -0
- package/src/vue/Button/index.vue +27 -0
- package/src/vue/Card/index.vue +18 -0
- package/src/vue/Tag/index.vue +18 -0
- package/src/vue/ThemeToggle/index.vue +39 -0
- package/src/vue/index.ts +4 -0
- package/tsconfig.json +19 -0
- package/vite.config.build.ts +34 -0
- package/vitest.config.ts +12 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Task List: Design System Components
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## Phase 1 — Package Setup
|
|
6
|
+
|
|
7
|
+
> All Phase 2–7 tasks depend on Phase 1 being complete.
|
|
8
|
+
|
|
9
|
+
- [x] [S] Configure `package.json`: add `exports` map for `./react`, `./astro`, `./vue` sub-paths, and add `vue` + `astro` as peer dependencies
|
|
10
|
+
- [x] [S] Create directory skeleton: `src/css/`, `src/shared/js/`, `src/shared/types/`, `src/react/`, `src/astro/`, `src/vue/`
|
|
11
|
+
- [x] [S] Create empty barrel files: `src/react/index.ts`, `src/astro/index.ts`, `src/vue/index.ts`
|
|
12
|
+
- [x] [S] Verify `@dezkareid/design-tokens` is installed and its CSS token file is importable; add as dependency if missing
|
|
13
|
+
- [x] [M] Set up Vitest config targeting `src/react/**/*.test.tsx` with jsdom environment and React Testing Library
|
|
14
|
+
|
|
15
|
+
**Definition of Done**: `package.json` has correct exports and peer deps; all directories exist; Vitest runs (no tests yet, zero failures).
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Phase 2 — Button Component
|
|
20
|
+
|
|
21
|
+
> Depends on: Phase 1
|
|
22
|
+
|
|
23
|
+
- [x] [S] Define `ButtonProps` interface in `src/shared/types/button.ts` (`variant`, `size`, `disabled`, `children`)
|
|
24
|
+
- [x] [M] Write `src/css/button.module.css` — structure (`.button`, size modifiers `.button--sm/md/lg`) and skin (variant modifiers `.button--primary/secondary`, state modifier `.button--disabled`) using BEM + OOCSS
|
|
25
|
+
- [x] [M] Implement `src/react/Button/index.tsx` — composes BEM classes, forwards native button props
|
|
26
|
+
- [x] [M] Write `src/react/Button/index.test.tsx` — renders primary/secondary variants, renders sm/md/lg sizes, disabled prevents click, has accessible button role
|
|
27
|
+
- [x] [M] Implement `src/astro/Button/index.astro`
|
|
28
|
+
- [x] [M] Implement `src/vue/Button/index.vue`
|
|
29
|
+
|
|
30
|
+
**Definition of Done**: Button renders all variants and sizes in all three frameworks; React tests pass; no hardcoded colour or spacing values.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Phase 3 — Tag Component
|
|
35
|
+
|
|
36
|
+
> Depends on: Phase 1
|
|
37
|
+
|
|
38
|
+
- [x] [S] Define `TagProps` interface in `src/shared/types/tag.ts` (`variant`, `children`)
|
|
39
|
+
- [x] [M] Write `src/css/tag.module.css` — structure (`.tag`) and skin (variant modifiers `.tag--default/success/danger`) using BEM + OOCSS; add comment proposing `--color-danger` semantic token
|
|
40
|
+
- [x] [M] Implement `src/react/Tag/index.tsx` — renders `<span>`, accepts `children`, composes BEM classes
|
|
41
|
+
- [x] [M] Write `src/react/Tag/index.test.tsx` — renders default/success/danger variants, renders arbitrary children content
|
|
42
|
+
- [x] [M] Implement `src/astro/Tag/index.astro`
|
|
43
|
+
- [x] [M] Implement `src/vue/Tag/index.vue`
|
|
44
|
+
|
|
45
|
+
**Definition of Done**: Tag renders all variants with correct colours in all three frameworks; accepts arbitrary slot/children; React tests pass.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Phase 4 — Card Component
|
|
50
|
+
|
|
51
|
+
> Depends on: Phase 1
|
|
52
|
+
|
|
53
|
+
- [x] [S] Define `CardProps` interface in `src/shared/types/card.ts` (`elevation`, `children`; default elevation `'raised'`)
|
|
54
|
+
- [x] [M] Write `src/css/card.module.css` — structure (`.card`) and skin (base skin on `.card`, elevation modifiers `.card--raised/flat`) using BEM + OOCSS; add comment proposing `--shadow-raised` token
|
|
55
|
+
- [x] [M] Implement `src/react/Card/index.tsx` — renders `<div>`, accepts `children`, composes BEM classes
|
|
56
|
+
- [x] [M] Write `src/react/Card/index.test.tsx` — renders children, applies `card--raised` by default, applies `card--flat` when set
|
|
57
|
+
- [x] [M] Implement `src/astro/Card/index.astro`
|
|
58
|
+
- [x] [M] Implement `src/vue/Card/index.vue`
|
|
59
|
+
|
|
60
|
+
**Definition of Done**: Card renders children inside a themed surface with correct elevation in all three frameworks; React tests pass.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Phase 5 — ThemeToggle Component
|
|
65
|
+
|
|
66
|
+
> Depends on: Phase 1
|
|
67
|
+
|
|
68
|
+
- [x] [S] Define `ThemeToggleProps` interface in `src/shared/types/theme-toggle.ts` (no required props)
|
|
69
|
+
- [x] [M] Implement shared theme utilities in `src/shared/js/theme.ts`: `getInitialTheme()`, `applyTheme()`, `persistTheme()` — all SSR-safe with `typeof window` guards
|
|
70
|
+
- [x] [M] Write `src/css/theme-toggle.module.css` — structure (`.theme-toggle`) and skin + state modifier (`.theme-toggle--dark`) using BEM + OOCSS
|
|
71
|
+
- [x] [M] Implement `src/react/ThemeToggle/index.tsx` — uses `useState` + `useEffect`, calls shared theme utilities
|
|
72
|
+
- [x] [L] Write `src/react/ThemeToggle/index.test.tsx` — initialises from `localStorage`, falls back to `prefers-color-scheme`, toggles on click, persists to `localStorage`
|
|
73
|
+
- [x] [M] Implement `src/astro/ThemeToggle/index.astro` — static markup + inline `<script>` calling shared utilities
|
|
74
|
+
- [x] [M] Implement `src/vue/ThemeToggle/index.vue` — uses `ref` + `onMounted`, calls shared utilities
|
|
75
|
+
|
|
76
|
+
**Definition of Done**: ThemeToggle reads, applies, and persists theme in all three frameworks; React tests pass; no FOUC in Astro (inline script runs before paint).
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Phase 6 — Exports & Package Configuration
|
|
81
|
+
|
|
82
|
+
> Depends on: Phases 2–5
|
|
83
|
+
|
|
84
|
+
- [x] [S] Update `src/react/index.ts` to re-export `Button`, `Tag`, `Card`, `ThemeToggle`
|
|
85
|
+
- [x] [S] Update `src/astro/index.ts` to re-export all Astro components
|
|
86
|
+
- [x] [S] Update `src/vue/index.ts` to re-export all Vue components
|
|
87
|
+
- [x] [S] Verify `package.json` `exports` paths resolve correctly (smoke check imports)
|
|
88
|
+
|
|
89
|
+
**Definition of Done**: All components importable from `@dezkareid/components/react`, `/astro`, and `/vue`; no TypeScript errors.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Phase 7 — Documentation
|
|
94
|
+
|
|
95
|
+
> Depends on: Phase 6
|
|
96
|
+
|
|
97
|
+
- [x] [M] Update `README.md`: add `@dezkareid/design-tokens` CSS import requirement, add usage examples for Button, Tag, Card, ThemeToggle in all three framework formats
|
|
98
|
+
- [x] [M] Update `AGENTS.md`: add component API summaries (props, variants, slot/children behaviour) for all four components
|
|
99
|
+
|
|
100
|
+
**Definition of Done**: README covers install, token CSS import, and usage for all components; AGENTS.md describes all component APIs for AI tooling.
|
package/package.json
CHANGED
|
@@ -1,7 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dezkareid/components",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
4
5
|
"description": "A package to export UI components in formats like React, Astro, Vue, etc.",
|
|
6
|
+
"exports": {
|
|
7
|
+
"./react": {
|
|
8
|
+
"types": "./dist/react/index.d.ts",
|
|
9
|
+
"import": "./dist/react.js",
|
|
10
|
+
"default": "./dist/react.js"
|
|
11
|
+
},
|
|
12
|
+
"./astro": "./src/astro/index.ts",
|
|
13
|
+
"./vue": "./src/vue/index.ts",
|
|
14
|
+
"./css": {
|
|
15
|
+
"import": "./dist/components.min.css",
|
|
16
|
+
"default": "./dist/components.min.css"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"typesVersions": {
|
|
20
|
+
"*": {
|
|
21
|
+
"react": [
|
|
22
|
+
"./dist/react/index.d.ts"
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
},
|
|
5
26
|
"repository": {
|
|
6
27
|
"type": "git",
|
|
7
28
|
"url": "https://github.com/dezkareid/dezkareid"
|
|
@@ -15,8 +36,57 @@
|
|
|
15
36
|
"access": "public"
|
|
16
37
|
},
|
|
17
38
|
"keywords": [
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
39
|
+
"design-system",
|
|
40
|
+
"components",
|
|
41
|
+
"react",
|
|
42
|
+
"astro",
|
|
43
|
+
"vue"
|
|
44
|
+
],
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"astro": ">=4.0.0",
|
|
47
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
48
|
+
"react-dom": "^18.0.0 || ^19.0.0",
|
|
49
|
+
"vue": "^3.0.0"
|
|
50
|
+
},
|
|
51
|
+
"peerDependenciesMeta": {
|
|
52
|
+
"astro": {
|
|
53
|
+
"optional": true
|
|
54
|
+
},
|
|
55
|
+
"vue": {
|
|
56
|
+
"optional": true
|
|
57
|
+
},
|
|
58
|
+
"react": {
|
|
59
|
+
"optional": true
|
|
60
|
+
},
|
|
61
|
+
"react-dom": {
|
|
62
|
+
"optional": true
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"dependencies": {
|
|
66
|
+
"classnames": "2.5.1",
|
|
67
|
+
"@dezkareid/design-tokens": "0.0.0"
|
|
68
|
+
},
|
|
69
|
+
"devDependencies": {
|
|
70
|
+
"@rollup/plugin-commonjs": "29.0.0",
|
|
71
|
+
"@rollup/plugin-node-resolve": "16.0.3",
|
|
72
|
+
"@rollup/plugin-typescript": "12.3.0",
|
|
73
|
+
"@testing-library/jest-dom": "6.9.1",
|
|
74
|
+
"@testing-library/react": "16.3.2",
|
|
75
|
+
"@testing-library/user-event": "14.5.2",
|
|
76
|
+
"@types/classnames": "^2.3.4",
|
|
77
|
+
"@types/react": "19.2.9",
|
|
78
|
+
"@vitejs/plugin-react": "5.1.4",
|
|
79
|
+
"jsdom": "27.4.0",
|
|
80
|
+
"react": "19.2.4",
|
|
81
|
+
"react-dom": "19.2.4",
|
|
82
|
+
"rollup": "4.56.0",
|
|
83
|
+
"rollup-plugin-postcss": "4.0.2",
|
|
84
|
+
"typescript": "5.9.3",
|
|
85
|
+
"vite": "7.3.1",
|
|
86
|
+
"vitest": "4.0.18"
|
|
87
|
+
},
|
|
88
|
+
"scripts": {
|
|
89
|
+
"build": "rollup -c rollup.config.mjs",
|
|
90
|
+
"test": "vitest --run"
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import typescript from '@rollup/plugin-typescript';
|
|
2
|
+
import resolve from '@rollup/plugin-node-resolve';
|
|
3
|
+
import commonjs from '@rollup/plugin-commonjs';
|
|
4
|
+
import postcss from 'rollup-plugin-postcss';
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
input: 'src/react/index.ts',
|
|
8
|
+
external: ['react', 'react/jsx-runtime'],
|
|
9
|
+
plugins: [
|
|
10
|
+
resolve({ extensions: ['.ts', '.tsx'] }),
|
|
11
|
+
commonjs(),
|
|
12
|
+
postcss({
|
|
13
|
+
autoModules: true,
|
|
14
|
+
extract: 'components.min.css',
|
|
15
|
+
minimize: true,
|
|
16
|
+
}),
|
|
17
|
+
typescript({
|
|
18
|
+
tsconfig: './tsconfig.json',
|
|
19
|
+
declaration: true,
|
|
20
|
+
declarationDir: 'dist',
|
|
21
|
+
exclude: ['**/*.test.tsx', '**/*.test.ts', 'setupTests.ts'],
|
|
22
|
+
}),
|
|
23
|
+
],
|
|
24
|
+
output: {
|
|
25
|
+
dir: 'dist',
|
|
26
|
+
format: 'es',
|
|
27
|
+
preserveModules: true,
|
|
28
|
+
preserveModulesRoot: 'src',
|
|
29
|
+
entryFileNames: '[name].js',
|
|
30
|
+
sourcemap: true,
|
|
31
|
+
},
|
|
32
|
+
};
|
package/setupTests.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { ButtonProps } from '../../shared/types/button';
|
|
3
|
+
import styles from '../../css/button.module.css';
|
|
4
|
+
|
|
5
|
+
type Props = ButtonProps & {
|
|
6
|
+
href?: string;
|
|
7
|
+
class?: string;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
variant = 'primary',
|
|
13
|
+
size = 'md',
|
|
14
|
+
disabled = false,
|
|
15
|
+
href,
|
|
16
|
+
class: className,
|
|
17
|
+
...rest
|
|
18
|
+
} = Astro.props;
|
|
19
|
+
|
|
20
|
+
const Tag = href ? 'a' : 'button';
|
|
21
|
+
|
|
22
|
+
const classes = [
|
|
23
|
+
styles.button,
|
|
24
|
+
styles[`button--${variant}`],
|
|
25
|
+
styles[`button--${size}`],
|
|
26
|
+
disabled ? styles['button--disabled'] : '',
|
|
27
|
+
className ?? '',
|
|
28
|
+
]
|
|
29
|
+
.filter(Boolean)
|
|
30
|
+
.join(' ');
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
<Tag class={classes} href={href} disabled={!href && disabled} {...rest}>
|
|
34
|
+
<slot />
|
|
35
|
+
</Tag>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { CardProps } from '../../shared/types/card';
|
|
3
|
+
import styles from '../../css/card.module.css';
|
|
4
|
+
|
|
5
|
+
type Props = CardProps & {
|
|
6
|
+
class?: string;
|
|
7
|
+
[key: string]: unknown;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const { elevation = 'raised', class: className, ...rest } = Astro.props;
|
|
11
|
+
|
|
12
|
+
const classes = [
|
|
13
|
+
styles.card,
|
|
14
|
+
styles[`card--${elevation}`],
|
|
15
|
+
className ?? '',
|
|
16
|
+
]
|
|
17
|
+
.filter(Boolean)
|
|
18
|
+
.join(' ');
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
<div class={classes} {...rest}>
|
|
22
|
+
<slot />
|
|
23
|
+
</div>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { TagProps } from '../../shared/types/tag';
|
|
3
|
+
import styles from '../../css/tag.module.css';
|
|
4
|
+
|
|
5
|
+
type Props = TagProps & {
|
|
6
|
+
class?: string;
|
|
7
|
+
[key: string]: unknown;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const { variant = 'default', class: className, ...rest } = Astro.props;
|
|
11
|
+
|
|
12
|
+
const classes = [
|
|
13
|
+
styles.tag,
|
|
14
|
+
styles[`tag--${variant}`],
|
|
15
|
+
className ?? '',
|
|
16
|
+
]
|
|
17
|
+
.filter(Boolean)
|
|
18
|
+
.join(' ');
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
<span class={classes} {...rest}>
|
|
22
|
+
<slot />
|
|
23
|
+
</span>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
import styles from '../../css/theme-toggle.module.css';
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
class?: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const { class: className } = Astro.props;
|
|
9
|
+
|
|
10
|
+
const classes = [styles['theme-toggle'], className ?? ''].filter(Boolean).join(' ');
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
<!--
|
|
14
|
+
The inline script runs before first paint to apply the stored theme,
|
|
15
|
+
preventing a flash of unstyled/wrong-theme content (FOUC).
|
|
16
|
+
-->
|
|
17
|
+
<script is:inline>
|
|
18
|
+
(function () {
|
|
19
|
+
var stored = localStorage.getItem('color-scheme');
|
|
20
|
+
var theme =
|
|
21
|
+
stored === 'light' || stored === 'dark'
|
|
22
|
+
? stored
|
|
23
|
+
: window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
24
|
+
? 'dark'
|
|
25
|
+
: 'light';
|
|
26
|
+
document.documentElement.setAttribute('color-scheme', theme);
|
|
27
|
+
})();
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<button
|
|
31
|
+
type="button"
|
|
32
|
+
class={classes}
|
|
33
|
+
data-theme-toggle
|
|
34
|
+
aria-label="Toggle colour scheme"
|
|
35
|
+
>
|
|
36
|
+
<span data-theme-label>Light</span>
|
|
37
|
+
</button>
|
|
38
|
+
|
|
39
|
+
<script>
|
|
40
|
+
import { getInitialTheme, applyTheme, persistTheme } from '../../shared/js/theme';
|
|
41
|
+
|
|
42
|
+
const btn = document.querySelector('[data-theme-toggle]') as HTMLButtonElement | null;
|
|
43
|
+
const label = document.querySelector('[data-theme-label]') as HTMLElement | null;
|
|
44
|
+
|
|
45
|
+
if (btn && label) {
|
|
46
|
+
let current = getInitialTheme();
|
|
47
|
+
|
|
48
|
+
function update(theme: 'light' | 'dark') {
|
|
49
|
+
label!.textContent = theme === 'dark' ? 'Dark' : 'Light';
|
|
50
|
+
btn!.setAttribute('aria-pressed', String(theme === 'dark'));
|
|
51
|
+
btn!.classList.toggle('theme-toggle--dark', theme === 'dark');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
update(current);
|
|
55
|
+
|
|
56
|
+
btn.addEventListener('click', () => {
|
|
57
|
+
current = current === 'light' ? 'dark' : 'light';
|
|
58
|
+
applyTheme(current);
|
|
59
|
+
persistTheme(current);
|
|
60
|
+
update(current);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
</script>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/* =============================================================================
|
|
2
|
+
BUTTON — BEM + OOCSS
|
|
3
|
+
Structure classes: layout, sizing, spacing
|
|
4
|
+
Skin classes: colour, border, typography
|
|
5
|
+
============================================================================= */
|
|
6
|
+
|
|
7
|
+
/* --- Structure: block --- */
|
|
8
|
+
.button {
|
|
9
|
+
display: inline-flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
justify-content: center;
|
|
12
|
+
cursor: pointer;
|
|
13
|
+
border: none;
|
|
14
|
+
border-radius: var(--spacing-4);
|
|
15
|
+
font-family: var(--font-family-base);
|
|
16
|
+
font-weight: var(--font-weight-medium);
|
|
17
|
+
line-height: var(--font-line-height-none);
|
|
18
|
+
text-decoration: none;
|
|
19
|
+
white-space: nowrap;
|
|
20
|
+
transition: opacity 0.15s ease;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* --- Structure: size modifiers --- */
|
|
24
|
+
.button--sm,
|
|
25
|
+
.button--small {
|
|
26
|
+
padding: var(--spacing-4) var(--spacing-12);
|
|
27
|
+
font-size: var(--font-size-200);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.button--md,
|
|
31
|
+
.button--medium {
|
|
32
|
+
padding: var(--spacing-8) var(--spacing-24);
|
|
33
|
+
font-size: var(--font-size-300);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.button--lg,
|
|
37
|
+
.button--large {
|
|
38
|
+
padding: var(--spacing-12) var(--spacing-32);
|
|
39
|
+
font-size: var(--font-size-400);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* --- Skin: variant modifiers --- */
|
|
43
|
+
.button--primary {
|
|
44
|
+
background-color: var(--color-primary);
|
|
45
|
+
color: var(--color-text-inverse);
|
|
46
|
+
border: 1px solid transparent;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.button--primary:hover:not(.button--disabled) {
|
|
50
|
+
opacity: 0.88;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.button--secondary,
|
|
54
|
+
.button--outline {
|
|
55
|
+
background-color: transparent;
|
|
56
|
+
color: var(--color-primary);
|
|
57
|
+
border: 1px solid var(--color-primary);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.button--secondary:hover:not(.button--disabled),
|
|
61
|
+
.button--outline:hover:not(.button--disabled) {
|
|
62
|
+
background-color: var(--color-background-secondary);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.button--success {
|
|
66
|
+
background-color: var(--color-success);
|
|
67
|
+
color: var(--color-text-inverse);
|
|
68
|
+
border: 1px solid transparent;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.button--success:hover:not(.button--disabled) {
|
|
72
|
+
opacity: 0.88;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.button--ghost {
|
|
76
|
+
background-color: transparent;
|
|
77
|
+
color: var(--color-text-primary);
|
|
78
|
+
border: 1px solid transparent;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.button--ghost:hover:not(.button--disabled) {
|
|
82
|
+
background-color: var(--color-background-secondary);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* --- Skin: state modifier --- */
|
|
86
|
+
.button--disabled {
|
|
87
|
+
opacity: 0.4;
|
|
88
|
+
cursor: not-allowed;
|
|
89
|
+
pointer-events: none;
|
|
90
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/* =============================================================================
|
|
2
|
+
CARD — BEM + OOCSS
|
|
3
|
+
Structure classes: layout, sizing, spacing
|
|
4
|
+
Skin classes: colour, shadow
|
|
5
|
+
============================================================================= */
|
|
6
|
+
|
|
7
|
+
/* --- Structure: block --- */
|
|
8
|
+
.card {
|
|
9
|
+
display: block;
|
|
10
|
+
width: 100%;
|
|
11
|
+
border-radius: var(--spacing-8);
|
|
12
|
+
padding: var(--spacing-24);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* --- Skin: base --- */
|
|
16
|
+
.card {
|
|
17
|
+
background-color: var(--color-background-secondary);
|
|
18
|
+
color: var(--color-text-primary);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/* --- Skin: elevation modifiers --- */
|
|
22
|
+
|
|
23
|
+
/* TODO: Propose --shadow-raised token to the design-tokens package. */
|
|
24
|
+
.card--raised {
|
|
25
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.card--flat {
|
|
29
|
+
box-shadow: none;
|
|
30
|
+
}
|
package/src/css/index.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/* =============================================================================
|
|
2
|
+
TAG — BEM + OOCSS
|
|
3
|
+
Structure classes: layout, sizing, spacing
|
|
4
|
+
Skin classes: colour, border, typography
|
|
5
|
+
============================================================================= */
|
|
6
|
+
|
|
7
|
+
/* --- Structure: block --- */
|
|
8
|
+
.tag {
|
|
9
|
+
display: inline-flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
border-radius: var(--spacing-4);
|
|
12
|
+
padding: var(--spacing-4) var(--spacing-8);
|
|
13
|
+
font-family: var(--font-family-base);
|
|
14
|
+
font-size: var(--font-size-100);
|
|
15
|
+
font-weight: var(--font-weight-medium);
|
|
16
|
+
line-height: var(--font-line-height-none);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* --- Skin: variant modifiers --- */
|
|
20
|
+
.tag--default {
|
|
21
|
+
background-color: var(--color-background-secondary);
|
|
22
|
+
color: var(--color-text-primary);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.tag--success {
|
|
26
|
+
background-color: var(--color-success);
|
|
27
|
+
color: var(--color-text-inverse);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.tag--danger {
|
|
31
|
+
background-color: var(--color-danger);
|
|
32
|
+
color: var(--color-text-inverse);
|
|
33
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/* =============================================================================
|
|
2
|
+
THEME TOGGLE — BEM + OOCSS
|
|
3
|
+
Structure classes: layout, sizing, spacing
|
|
4
|
+
Skin classes: colour, state
|
|
5
|
+
============================================================================= */
|
|
6
|
+
|
|
7
|
+
/* --- Structure: block --- */
|
|
8
|
+
.theme-toggle {
|
|
9
|
+
display: inline-flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
justify-content: center;
|
|
12
|
+
gap: var(--spacing-8);
|
|
13
|
+
cursor: pointer;
|
|
14
|
+
padding: var(--spacing-8) var(--spacing-12);
|
|
15
|
+
border: none;
|
|
16
|
+
border-radius: var(--spacing-4);
|
|
17
|
+
font-family: var(--font-family-base);
|
|
18
|
+
font-size: var(--font-size-200);
|
|
19
|
+
font-weight: var(--font-weight-medium);
|
|
20
|
+
line-height: var(--font-line-height-none);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* --- Skin: base --- */
|
|
24
|
+
.theme-toggle {
|
|
25
|
+
background-color: transparent;
|
|
26
|
+
color: var(--color-text-primary);
|
|
27
|
+
border: 1px solid var(--color-background-secondary);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.theme-toggle:hover {
|
|
31
|
+
background-color: var(--color-background-secondary);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* --- Skin: state modifier (dark mode active) --- */
|
|
35
|
+
.theme-toggle--dark {
|
|
36
|
+
color: var(--color-primary);
|
|
37
|
+
border-color: var(--color-primary);
|
|
38
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// CSS Modules
|
|
2
|
+
declare module '*.module.css' {
|
|
3
|
+
const classes: Record<string, string>;
|
|
4
|
+
export default classes;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// Astro components
|
|
8
|
+
declare module '*.astro' {
|
|
9
|
+
import type { AstroComponentFactory } from 'astro/runtime/server/index.js';
|
|
10
|
+
const Component: AstroComponentFactory;
|
|
11
|
+
export default Component;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Vue SFCs
|
|
15
|
+
declare module '*.vue' {
|
|
16
|
+
import type { DefineComponent } from 'vue';
|
|
17
|
+
const Component: DefineComponent;
|
|
18
|
+
export default Component;
|
|
19
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
4
|
+
import { Button } from './index';
|
|
5
|
+
|
|
6
|
+
describe('Button', () => {
|
|
7
|
+
it('renders with default variant and size', () => {
|
|
8
|
+
render(<Button>Click me</Button>);
|
|
9
|
+
expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('renders primary variant', () => {
|
|
13
|
+
render(<Button variant="primary">Primary</Button>);
|
|
14
|
+
const btn = screen.getByRole('button');
|
|
15
|
+
expect(btn.className).toMatch(/button--primary/);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('renders secondary variant', () => {
|
|
19
|
+
render(<Button variant="secondary">Secondary</Button>);
|
|
20
|
+
const btn = screen.getByRole('button');
|
|
21
|
+
expect(btn.className).toMatch(/button--secondary/);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('renders sm size', () => {
|
|
25
|
+
render(<Button size="sm">Small</Button>);
|
|
26
|
+
expect(screen.getByRole('button').className).toMatch(/button--sm/);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('renders md size', () => {
|
|
30
|
+
render(<Button size="md">Medium</Button>);
|
|
31
|
+
expect(screen.getByRole('button').className).toMatch(/button--md/);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('renders lg size', () => {
|
|
35
|
+
render(<Button size="lg">Large</Button>);
|
|
36
|
+
expect(screen.getByRole('button').className).toMatch(/button--lg/);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('is disabled when disabled prop is set', () => {
|
|
40
|
+
render(<Button disabled>Disabled</Button>);
|
|
41
|
+
const btn = screen.getByRole('button');
|
|
42
|
+
expect(btn).toBeDisabled();
|
|
43
|
+
expect(btn.className).toMatch(/button--disabled/);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('does not fire onClick when disabled', async () => {
|
|
47
|
+
const onClick = vi.fn();
|
|
48
|
+
render(<Button disabled onClick={onClick}>Disabled</Button>);
|
|
49
|
+
await userEvent.click(screen.getByRole('button'));
|
|
50
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('fires onClick when enabled', async () => {
|
|
54
|
+
const onClick = vi.fn();
|
|
55
|
+
render(<Button onClick={onClick}>Click</Button>);
|
|
56
|
+
await userEvent.click(screen.getByRole('button'));
|
|
57
|
+
expect(onClick).toHaveBeenCalledOnce();
|
|
58
|
+
});
|
|
59
|
+
});
|