diff --git a/.changeset/full-papayas-press.md b/.changeset/full-papayas-press.md new file mode 100644 index 00000000..933f40b8 --- /dev/null +++ b/.changeset/full-papayas-press.md @@ -0,0 +1,5 @@ +--- +'@solidmaterial/hero': minor +--- + +Add example app with one page for visual hero in README.md. diff --git a/.gitattributes b/.gitattributes index 2ae34428..1c45d9c3 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,4 @@ * text eol=lf +*.png binary packages/**/*.d.ts -linguist-vendored -packages/icons/icons/** linguist-generated \ No newline at end of file +packages/icons/icons/** linguist-generated diff --git a/README.md b/README.md index fa3f6c6b..7796ede0 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,179 @@ -

Solid Material

+
+ + + + + Solid Material logo + + +
+ +
[![NPM status](https://img.shields.io/npm/v/@solidmaterial/material.svg)](https://npmjs.com/package/@solidmaterial/material) [![CI status](https://github.com/mprovok/solid-material/actions/workflows/ci.yml/badge.svg)](https://github.com/mprovok/solid-material/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/mprovok/solid-material/graph/badge.svg)](https://codecov.io/gh/mprovok/solid-material) - -Material 3 Expressive for SolidJS +[![Storybook](https://img.shields.io/badge/%F0%9F%93%96%20Storybook-%236750a4)][url-storybook]
+# Solid Material + +> Material 3 Expressive components built for SolidJS + Solid Material is a component library implementing [Material 3 Expressive][url-m3] using [SolidJS][url-solid]. +It provides components closely following the guidelines of Material 3 Expressive. + +
+Screenshot phone (light mode) +Screenshot phone (dark mode) +
+ +## ✨ Features + +- **Material 3 Expressive:** Components implementing the M3 Expressive spec + +- **Layout Components:** Responsive components for navigation (rail and bar) and content in multiple panes + +- **Dynamic Theming:** Baseline and dynamic color schemes based on a source color + +- **Design Tokens:** Includes tokens for easings, springs, and static palette color tokens for 11 colors + +- **View Transitions:** Animated transitions between pages using the View Transitions API + +- **Few dependencies**: Avoids external dependencies as much as possible by making use of modern CSS features like + scroll-driven animations and anchor positioning + +## 🚀 Quick Start + +Add the components and icons to your SolidJS project: + +```bash +pnpm add @solidmaterial/material @solidmaterial/icons +``` + +Add the Vite plugin to be able to import SVG files as SolidJS components: + +```bash +pnpm add -D @solidmaterial/vite-plugin-solid-svg +``` + +vite.config.ts: + +```ts +import solidSvg from '@solidmaterial/vite-plugin-solid-svg'; +import { defineConfig } from 'vite'; +import solidPlugin from 'vite-plugin-solid'; -## Packages +export default defineConfig({ + plugins: [solidPlugin(), solidSvg()] +}); +``` -| Package | -| --------------------------------------------------- | -| [@solidmaterial/material][url-sm-material] | -| [@solidmaterial/icons][url-sm-icons] | -| [@solidmaterial/vite-plugin-solid-svg][url-sm-vite] | +src/index.tsx: -## License +```tsx +import { MaterialButton } from '@solidmaterial/material/components/button'; +import { MaterialCard, MaterialCardBody } from '@solidmaterial/material/components/card'; +import { H1 } from '@solidmaterial/material/components/typography'; +import { MaterialTheme } from '@solidmaterial/material/styling'; +import { render } from 'solid-js/web'; + +import TotalDissolvedSolidsIcon from '@solidmaterial/icons/400/outlined/total_dissolved_solids.svg'; + +const App = () => { + return ( + +
+ + +
+ +

+ Material 3 Expressive with Solid Material +

+ + Get started + +
+
+
+
+
+ ); +}; + +render(() => , document.querySelector('#root')!); +``` + +## 📖 Documentation + +You can read more detailed instructions on how to display navigation components (like the navigation rail and bar) +or how to put your content in flexible or fixed panes on the [Get Started][url-get-started] page. + +To view all the components you can use, visit the [Storybook][url-storybook] site. + +## 📦 Packages + +| Package | Description | +| --------------------------------------------------- | --------------------------------------------------------------------------------- | +| [@solidmaterial/material][url-sm-material] | Core and layout components for M3 Expressive, design tokens, and view transitions | +| [@solidmaterial/icons][url-sm-icons] | Icons from Material Symbols as SVG files | +| [@solidmaterial/vite-plugin-solid-svg][url-sm-vite] | Vite plugin to import SVG files as SolidJS components | + +## 🛠️ Development + +```bash +pnpm install # Install dependencies +pnpm dev # Run development environment of the landing page + +pnpm build # Create a production build of the packages +pnpm preview # View the production build of the landing page + +pnpm lint # Run oxlint on all packages +pnpm format:check # Check formatting using oxfmt +pnpm format # Format all files using oxfmt + +pnpm install:webkit # Install the WebKit browser needed to run the tests +pnpm test # Run the tests of all packages +``` + +## ❤️ Credits + +This project uses the web components of [@material/web][url-material-web] for +the ripple, focus ring, and elevation effects, and for some of the components +often used in forms (like the checkbox, text field, etc.), these are mere bindings +around the respective web components. +See the [README.md][url-sm-material] file for the exact list. + +The [Material Symbols][url-google-mdi] in [@solidmaterial/icons][url-sm-icons] were made by Google. + +Many of the SolidJS components make use of the primitives from [Solid Primitives][url-solid-primitives]. +Much thanks to their contributors. + +## 🤖 AI? + +The components (TypeScript and CSS) in [@solidmaterial/material][url-sm-material] were handcrafted, +but files which don't end up in its production build (like the calculator demo app) may +or may not have been partially assisted by small LLMs. + +## 📜 License This project is licensed under the Apache 2.0 license. +[url-get-started]: https://solid-material.sites.deploybase.eu/get-started +[url-google-mdi]: https://github.com/google/material-design-icons +[url-material-web]: https://material-web.dev/ [url-m3]: https://m3.material.io/ -[url-solid]: https://www.solidjs.com/ [url-sm-material]: https://github.com/mprovok/solid-material/tree/main/packages/material#readme [url-sm-icons]: https://github.com/mprovok/solid-material/tree/main/packages/icons#readme [url-sm-vite]: https://github.com/mprovok/solid-material/tree/main/packages/vite-plugin-solid-svg#readme +[url-solid]: https://www.solidjs.com/ +[url-solid-primitives]: https://primitives.solidjs.community/ +[url-storybook]: https://solid-material.sites.deploybase.eu/storybook/ diff --git a/apps/hero/README.md b/apps/hero/README.md new file mode 100644 index 00000000..85b19224 --- /dev/null +++ b/apps/hero/README.md @@ -0,0 +1,3 @@ +# Example app using Solid Material for hero section of README.md + +A small app used for a visual hero in the README.md in the root of the repository. diff --git a/apps/hero/index.html b/apps/hero/index.html new file mode 100644 index 00000000..7bf9f625 --- /dev/null +++ b/apps/hero/index.html @@ -0,0 +1,19 @@ + + + + + + + + + + + + + +
+ + + diff --git a/apps/hero/package.json b/apps/hero/package.json new file mode 100644 index 00000000..12df2b71 --- /dev/null +++ b/apps/hero/package.json @@ -0,0 +1,32 @@ +{ + "name": "@solidmaterial/hero", + "version": "1.0.0", + "private": true, + "description": "Example app using Solid Material for hero section of README.md", + "bugs": "https://github.com/mprovok/solid-material/issues", + "license": "Apache-2.0", + "author": "Mark Provo Kluit", + "repository": { + "type": "git", + "url": "git+https://github.com/mprovok/solid-material.git", + "directory": "apps/calculator" + }, + "type": "module", + "scripts": { + "dev": "vite --host", + "lint": "oxlint --report-unused-disable-directives", + "lint:fix": "oxlint --fix --report-unused-disable-directives" + }, + "dependencies": { + "@fontsource/google-sans-flex": "5.2.3", + "@solidjs/router": "catalog:solid", + "@solidmaterial/icons": "workspace:*", + "@solidmaterial/material": "workspace:*", + "solid-js": "catalog:solid" + }, + "devDependencies": { + "@solidmaterial/vite-plugin-solid-svg": "workspace:*", + "vite": "catalog:vite", + "vite-plugin-solid": "catalog:vite" + } +} diff --git a/apps/hero/src/NavigationLayout.tsx b/apps/hero/src/NavigationLayout.tsx new file mode 100644 index 00000000..6b29d346 --- /dev/null +++ b/apps/hero/src/NavigationLayout.tsx @@ -0,0 +1,56 @@ +import type { MaterialNavigationItemType } from '@solidmaterial/material/components/navigation-item'; +import type { FlowComponent } from 'solid-js'; + +import { MaterialNavigationLayout } from '@solidmaterial/material/layouts'; + +import HomeFillIcon from '@solidmaterial/icons/400/outlined/home-fill.svg'; +import HomeIcon from '@solidmaterial/icons/400/outlined/home.svg'; +import SettingsFillIcon from '@solidmaterial/icons/400/outlined/settings-fill.svg'; +import SettingsIcon from '@solidmaterial/icons/400/outlined/settings.svg'; +import ImageFillIcon from '@solidmaterial/icons/400/rounded/image-fill.svg'; +import ImageIcon from '@solidmaterial/icons/400/rounded/image.svg'; +import VideocamFillIcon from '@solidmaterial/icons/400/rounded/videocam-fill.svg'; +import VideocamIcon from '@solidmaterial/icons/400/rounded/videocam.svg'; + +export const NavigationLayout: FlowComponent = props => { + const items: MaterialNavigationItemType[] = [ + { + label: 'Label 1', + icon: HomeIcon, + activeIcon: HomeFillIcon, + href: '/', + end: true + }, + { + label: 'Label 2', + icon: ImageIcon, + activeIcon: ImageFillIcon, + href: '/label-2' + }, + { + label: 'Label 3', + icon: VideocamIcon, + activeIcon: VideocamFillIcon, + href: '/label-3' + }, + { + label: 'Label 4', + icon: SettingsIcon, + activeIcon: SettingsFillIcon, + href: '/label-4' + } + ]; + + return ( + + {props.children} + + ); +}; diff --git a/apps/hero/src/components/examples/ExampleConnectedButtonGroup.tsx b/apps/hero/src/components/examples/ExampleConnectedButtonGroup.tsx new file mode 100644 index 00000000..8877e7c7 --- /dev/null +++ b/apps/hero/src/components/examples/ExampleConnectedButtonGroup.tsx @@ -0,0 +1,33 @@ +import type { VoidComponent } from 'solid-js'; + +import { MaterialButton } from '@solidmaterial/material/components/button'; +import { MaterialButtonGroup } from '@solidmaterial/material/components/button-group'; +import { Breakpoints } from '@solidmaterial/material/utils'; +import { Index, createSignal } from 'solid-js'; + +const LABELS = ['One', 'Two', 'Three']; +const LABELS_MOBILE = ['A', 'B', 'C']; + +export const ExampleConnectedButtonGroup: VoidComponent = () => { + const [toggled, setToggled] = createSignal(0); + + const isMobile = () => Breakpoints.isCompactWidth(); + + return ( + + + {(_, index) => ( + setToggled(index)} + > + {isMobile() ? LABELS_MOBILE[index] : LABELS[index]} + + )} + + + ); +}; diff --git a/apps/hero/src/components/examples/ExampleList.tsx b/apps/hero/src/components/examples/ExampleList.tsx new file mode 100644 index 00000000..ebc81031 --- /dev/null +++ b/apps/hero/src/components/examples/ExampleList.tsx @@ -0,0 +1,27 @@ +import type { VoidComponent } from 'solid-js'; + +import { MaterialIcon } from '@solidmaterial/material/components/icon'; +import { MaterialList, MaterialListItem } from '@solidmaterial/material/components/list'; +import { Index } from 'solid-js'; + +import StarIcon from '@solidmaterial/icons/400/outlined/star.svg'; + +const LABELS = ['Segmented', 'Item', 'List']; + +export const ExampleList: VoidComponent = () => ( + + + {(_, index) => ( + + + + } + > + {LABELS[index]} + + )} + + +); diff --git a/apps/hero/src/components/examples/ExampleSearchBar.tsx b/apps/hero/src/components/examples/ExampleSearchBar.tsx new file mode 100644 index 00000000..55c8cfa4 --- /dev/null +++ b/apps/hero/src/components/examples/ExampleSearchBar.tsx @@ -0,0 +1,36 @@ +import type { VoidComponent } from 'solid-js'; + +import { MaterialIconButton } from '@solidmaterial/material/components/icon-button'; +import { MaterialSearch, MaterialSearchBar } from '@solidmaterial/material/components/search'; +import { Match, Switch, createSignal } from 'solid-js'; + +import FavoriteIcon from '@solidmaterial/icons/400/outlined/favorite.svg'; +import MapIcon from '@solidmaterial/icons/400/outlined/map.svg'; +import StarIcon from '@solidmaterial/icons/400/outlined/star.svg'; + +export const ExampleSearchBar: VoidComponent = () => { + const [searchInput, setSearchInput] = createSignal(''); + + return ( + + ( + + + } /> + + + } /> + } /> + + + )} + /> + + ); +}; diff --git a/apps/hero/src/components/examples/ExampleSplitButtonMenu.tsx b/apps/hero/src/components/examples/ExampleSplitButtonMenu.tsx new file mode 100644 index 00000000..c387d9c6 --- /dev/null +++ b/apps/hero/src/components/examples/ExampleSplitButtonMenu.tsx @@ -0,0 +1,31 @@ +import type { VoidComponent } from 'solid-js'; + +import { showSnack } from '@solidmaterial/material/components/snackbar'; +import { MaterialSplitButtonMenu } from '@solidmaterial/material/components/split-button'; +import { createSignal } from 'solid-js'; + +import EditIcon from '@solidmaterial/icons/400/outlined/edit.svg'; + +const LABELS = ['One', 'Two', 'Three']; + +export const ExampleSplitButtonMenu: VoidComponent = () => { + const [label, setLabel] = createSignal('Split button'); + + const onClick = () => { + showSnack({ + text: `Clicked on ${label()}`, + dismissable: false + }); + }; + + return ( + } + menuItems={LABELS.map(text => ({ label: text, onClick: () => setLabel(text) }))} + onClick={onClick} + > + {label()} + + ); +}; diff --git a/apps/hero/src/index.css b/apps/hero/src/index.css new file mode 100644 index 00000000..5ba86ce4 --- /dev/null +++ b/apps/hero/src/index.css @@ -0,0 +1,7 @@ +:root { + --md-ref-typeface-brand: Google Sans Flex, system-ui; +} + +body { + background-color: var(--md-sys-color-background); +} diff --git a/apps/hero/src/index.tsx b/apps/hero/src/index.tsx new file mode 100644 index 00000000..dfd9dab7 --- /dev/null +++ b/apps/hero/src/index.tsx @@ -0,0 +1,31 @@ +import '@fontsource/google-sans-flex/latin-400.css'; +import '@fontsource/google-sans-flex/latin-500.css'; +import type { ParentComponent } from 'solid-js'; + +import { Route, Router } from '@solidjs/router'; +import { MaterialTheme } from '@solidmaterial/material/styling'; +import { render } from 'solid-js/web'; + +import './index.css'; + +import { NavigationLayout } from './NavigationLayout'; +import RouteHome from './routes'; + +const RootLayout: ParentComponent = props => { + return ( + + {props.children} + + ); +}; + +const root = document.querySelector('#root'); + +render( + () => ( + + + + ), + root! +); diff --git a/apps/hero/src/logo/Logo.module.css b/apps/hero/src/logo/Logo.module.css new file mode 100644 index 00000000..d10131d8 --- /dev/null +++ b/apps/hero/src/logo/Logo.module.css @@ -0,0 +1,31 @@ +:root { + --logo-color: var(--md-sys-color-on-primary); + --logo-background-color: var(--md-sys-color-primary); + + --logo-size: 32rem; + --text-size: 18rem; +} + +.logo { + width: var(--logo-size); + height: var(--logo-size); + background-color: var(--logo-background-color); + border-radius: 50%; + mask-image: url(@solidmaterial/icons/400/outlined/total_dissolved_solids.svg), linear-gradient(#000, #000); + mask-size: 100% 100%; + mask-composite: exclude; +} + +.container { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + + h1 { + font-size: var(--text-size); + line-height: var(--text-size); + font-weight: 500; + color: var(--logo-background-color); + } +} diff --git a/apps/hero/src/logo/Logo.tsx b/apps/hero/src/logo/Logo.tsx new file mode 100644 index 00000000..25d07649 --- /dev/null +++ b/apps/hero/src/logo/Logo.tsx @@ -0,0 +1,33 @@ +import type { VoidComponent } from 'solid-js'; + +import { H1 } from '@solidmaterial/material/components/typography'; + +import styles from './Logo.module.css'; + +/** + * Renders the logo of Solid Material + * + * In Chrome Devtools, right click on the element and then choose "Capture node screenshot" + * to export it to a PNG file. + * + * 1. Open the PNG file in Inkscape + * 2. Select the image with the select tool (S) and then choose Path > Trace Bitmap... + * 3. On tab Multicolor: + * - Detection mode: Colors + * - Enable "Remove background" + * - Speckles: 1000 + * - Smooth corners: 1.0 + * - Optimize: 0.0 + * 4. Delete the image element and save it to an SVG file + * 5. Edit the SVG file, add fill and stroke attributes and clean up the XML + */ +export const Logo: VoidComponent = () => { + return ( +
+
+

+ Solid Material +

+
+ ); +}; diff --git a/apps/hero/src/routes/index.module.css b/apps/hero/src/routes/index.module.css new file mode 100644 index 00000000..efda52a9 --- /dev/null +++ b/apps/hero/src/routes/index.module.css @@ -0,0 +1,22 @@ +.main { + --logo-size: 4rem; + --text-size: 2rem; + + padding: var(--md-user-spacing-md); + display: flex; + flex-direction: column; + gap: var(--md-user-spacing-md); + + sm-card:first-of-type { + h2 { + margin-bottom: var(--md-user-spacing-sm); + font-weight: 600; + } + + .buttons-in-card { + display: flex; + flex-direction: row; + gap: var(--md-user-spacing-sm); + } + } +} diff --git a/apps/hero/src/routes/index.tsx b/apps/hero/src/routes/index.tsx new file mode 100644 index 00000000..b8876a9c --- /dev/null +++ b/apps/hero/src/routes/index.tsx @@ -0,0 +1,110 @@ +import type { Component } from 'solid-js'; + +import { MaterialAppBar } from '@solidmaterial/material/components/app-bar'; +import { MaterialCard, MaterialCardBody } from '@solidmaterial/material/components/card'; +import { MaterialCarousel, MaterialCarouselItem } from '@solidmaterial/material/components/carousel'; +import { MaterialIconButton } from '@solidmaterial/material/components/icon-button'; +import { showSnack } from '@solidmaterial/material/components/snackbar'; +import { H2 } from '@solidmaterial/material/components/typography'; +import { + MaterialBodyLayout, + MaterialNavigationRailLayoutMenuButton, + MaterialPane +} from '@solidmaterial/material/layouts'; +import { onMount } from 'solid-js'; + +import { ExampleConnectedButtonGroup } from '../components/examples/ExampleConnectedButtonGroup'; +import { ExampleList } from '../components/examples/ExampleList'; +import { ExampleSearchBar } from '../components/examples/ExampleSearchBar'; +import { ExampleSplitButtonMenu } from '../components/examples/ExampleSplitButtonMenu'; +import { Logo } from '../logo/Logo'; + +import styles from './index.module.css'; + +import MoreVertFillIcon from '@solidmaterial/icons/400/outlined/more_vert-fill.svg'; + +const PageHome: Component = () => { + const doNothing = () => { + // Empty + }; + + onMount(() => { + showSnack({ + text: 'Snackbar', + dismissable: true, + action: { + label: 'Action', + onClick: doNothing + } + }); + }); + + return ( + + + } + trailingButtons={ + } href="/" transition="forward" /> + } + /> +
+ + +
+ + + Balloons near ground + + + Balloons near ground + + + Balloons near ground + + +
+ + +

+ Card +

+

+ +

+
+ + +
+
+
+
+
+
+ ); +}; + +// oxlint-disable-next-line import/no-default-export +export default PageHome; diff --git a/apps/hero/tsconfig.json b/apps/hero/tsconfig.json new file mode 100644 index 00000000..9d67a024 --- /dev/null +++ b/apps/hero/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "types": ["@solidmaterial/vite-plugin-solid-svg/types", "vite/client"], + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src"] +} diff --git a/apps/hero/vite.config.ts b/apps/hero/vite.config.ts new file mode 100644 index 00000000..f7c5a899 --- /dev/null +++ b/apps/hero/vite.config.ts @@ -0,0 +1,15 @@ +import solidSvg from '@solidmaterial/vite-plugin-solid-svg'; +import { defineConfig } from 'vite'; +import solidPlugin from 'vite-plugin-solid'; + +// oxlint-disable-next-line import/no-default-export +export default defineConfig({ + plugins: [solidPlugin(), solidSvg()], + server: { + port: 4000 + }, + resolve: { + conditions: ['browser'], + extensions: ['.tsx', '.ts', '.js', '.json'] + } +}); diff --git a/docs/images/logo-dark.svg b/docs/images/logo-dark.svg new file mode 100644 index 00000000..a714de91 --- /dev/null +++ b/docs/images/logo-dark.svg @@ -0,0 +1,14 @@ + + + + + diff --git a/docs/images/logo-light.svg b/docs/images/logo-light.svg new file mode 100644 index 00000000..c1deba9c --- /dev/null +++ b/docs/images/logo-light.svg @@ -0,0 +1,14 @@ + + + + + diff --git a/docs/images/screenshot-phone-dark.png b/docs/images/screenshot-phone-dark.png new file mode 100644 index 00000000..e113686e Binary files /dev/null and b/docs/images/screenshot-phone-dark.png differ diff --git a/docs/images/screenshot-phone-light.png b/docs/images/screenshot-phone-light.png new file mode 100644 index 00000000..7775ee6e Binary files /dev/null and b/docs/images/screenshot-phone-light.png differ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dc303469..3b13a9ed 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -345,6 +345,34 @@ importers: specifier: catalog:vitest version: 4.1.11(@types/node@24.13.2)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-istanbul@4.1.11)(vite@8.1.5(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)) + apps/hero: + dependencies: + '@fontsource/google-sans-flex': + specifier: 5.2.3 + version: 5.2.3 + '@solidjs/router': + specifier: catalog:solid + version: 0.16.1(solid-js@1.9.14) + '@solidmaterial/icons': + specifier: workspace:* + version: link:../../packages/icons + '@solidmaterial/material': + specifier: workspace:* + version: link:../../packages/material + solid-js: + specifier: catalog:solid + version: 1.9.14 + devDependencies: + '@solidmaterial/vite-plugin-solid-svg': + specifier: workspace:* + version: link:../../packages/vite-plugin-solid-svg + vite: + specifier: catalog:vite + version: 8.1.5(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1) + vite-plugin-solid: + specifier: catalog:vite + version: 2.11.12(@testing-library/jest-dom@6.9.1)(solid-js@1.9.14)(supports-color@7.2.0)(vite@8.1.5(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)) + apps/landing-page: dependencies: '@fontsource/google-sans-flex':