({
28 | ref,
29 | isDisabled: () => ref()?.classList.contains("max-lg:-translate-x-full"),
30 | callback: (sidebar) => {
31 | if (!sidebar) return;
32 |
33 | if (sidebar.classList.contains("max-lg:-translate-x-full")) {
34 | sidebar.classList.remove("max-lg:-translate-x-full");
35 | return;
36 | }
37 |
38 | sidebar.classList.add("max-lg:-translate-x-full");
39 | },
40 | });
41 |
42 | return (
43 |
79 | );
80 | }
81 |
--------------------------------------------------------------------------------
/apps/docs/src/components/toc.astro:
--------------------------------------------------------------------------------
1 | ---
2 | export interface Props {
3 | headings: { depth: number; text: string; slug: string }[];
4 | }
5 |
6 | const { headings } = Astro.props;
7 | ---
8 |
9 | On this page
10 |
11 | {
12 | headings
13 | .filter((item) => item.depth !== 1)
14 | .map((item) => (
15 |
19 |
20 | {item.text}
21 |
22 |
23 | ))
24 | }
25 |
26 |
--------------------------------------------------------------------------------
/apps/docs/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/apps/docs/src/layouts/docs.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import "@fontsource/poppins/300.css";
3 | import "@fontsource/poppins/400.css";
4 | import "@fontsource/poppins/500.css";
5 |
6 | import type { SidebarEntry } from "~/components/sidebar";
7 |
8 | import Footer from "~/components/footer.astro";
9 | import { Header } from "~/components/header";
10 | import { Sidebar } from "~/components/sidebar";
11 | import TOC from "~/components/toc.astro";
12 |
13 | export interface Props {
14 | title: string;
15 | }
16 |
17 | const { frontmatter, headings } = Astro.props;
18 |
19 | const entries = [
20 | ["Guide", await Astro.glob("../pages/guide/*.mdx")],
21 | ["Components", await Astro.glob("../pages/components/*.mdx")],
22 | ] satisfies SidebarEntry[];
23 | ---
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | {frontmatter.title} - Solid Material
33 |
34 |
35 |
36 |
37 |
38 |
41 |
44 |
45 |
46 |
47 |
50 |
51 |
52 |
53 |
54 |
71 |
72 |
89 |
--------------------------------------------------------------------------------
/apps/docs/src/layouts/layout.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import "@fontsource/poppins/300.css";
3 | import "@fontsource/poppins/400.css";
4 | import "@fontsource/poppins/500.css";
5 |
6 | import Footer from "../components/footer.astro";
7 |
8 | export interface Props {
9 | title: string;
10 | description: string;
11 | }
12 |
13 | const { title, description } = Astro.props;
14 | ---
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | {title}
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
41 |
--------------------------------------------------------------------------------
/apps/docs/src/pages/components/button.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Button
3 | layout: ~/layouts/docs.astro
4 | ---
5 |
6 | import Playground from "~/components/playground.astro";
7 | import { BasicExample, DisabledExample, IconsExample } from "~/components/examples/button";
8 |
9 | # {frontmatter.title}
10 |
11 | Buttons allow users to take actions with a single tap.
12 |
13 | ## Basic usage
14 |
15 | The `Button` comes with four variants: filled, tonal, outlined and text.
16 |
17 |
18 |
19 |
20 | ```tsx
21 | Save
22 | Save
23 | Save
24 | Save
25 | ```
26 |
27 |
28 |
29 | ## Adding icons
30 |
31 | You can add icons by supplying `startIcon` and `endIcon` props:
32 |
33 |
34 |
35 |
36 | ```tsx
37 | }>Save
38 | }>Save
39 | ```
40 |
41 |
42 |
43 | ## States
44 |
45 | ### Disabled
46 |
47 | You can disable the `Button` using the `isDisabled` prop:
48 |
49 |
50 |
51 |
52 | ```tsx
53 | Save
54 |
55 | Save
56 |
57 |
58 | Save
59 |
60 |
61 | Save
62 |
63 | ```
64 |
65 |
66 |
--------------------------------------------------------------------------------
/apps/docs/src/pages/components/card.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Card
3 | layout: ~/layouts/docs.astro
4 | ---
5 |
6 | import Playground from "~/components/playground.astro";
7 | import { BasicExample, CompositionExample } from "~/components/examples/card";
8 |
9 | # {frontmatter.title}
10 |
11 | Cards contain content and actions about a single subject.
12 |
13 | ## Basic usage
14 |
15 | The `Card` comes with two variants: filled, outlined.
16 |
17 |
18 |
19 |
20 | ```tsx
21 | Some content
22 |
23 | Some content
24 |
25 | ```
26 |
27 |
28 |
29 | ## Composition
30 |
31 | Add buttons to `Card`:
32 |
33 |
34 |
35 |
36 | ```tsx
37 |
38 | Some content
39 |
40 | Cancel
41 | Cool
42 |
43 |
44 | ```
45 |
46 |
47 |
--------------------------------------------------------------------------------
/apps/docs/src/pages/components/checkbox.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Checkbox
3 | layout: ~/layouts/docs.astro
4 | ---
5 |
6 | import Playground from "~/components/playground.astro";
7 | import { BasicExample, IndeterminateExample, DisabledExample } from "~/components/examples/checkbox";
8 |
9 | # {frontmatter.title}
10 |
11 | Checkboxes allow the user to select one or more items from a set.
12 |
13 | ## Basic usage
14 |
15 | The `Checkbox` needs a label and icons props.
16 |
17 |
18 |
19 |
20 | ```tsx
21 | } indeterminateIcon={ } />
22 | ```
23 |
24 |
25 |
26 | ## States
27 |
28 | ### Indeterminate
29 |
30 | `Checkbox` also accepts `isIndeterminate` prop.
31 |
32 |
33 |
34 |
35 | ```tsx
36 | } indeterminateIcon={ } isIndeterminate />
37 | ```
38 |
39 |
40 |
41 | ### Disabled
42 |
43 | You can disable the `Checkbox` using the `isDisabled` prop:
44 |
45 |
46 |
47 |
48 | ```tsx
49 | } indeterminateIcon={ } isDisabled />
50 | } indeterminateIcon={ } isDisabled defaultIsChecked />
51 | ```
52 |
53 |
54 |
--------------------------------------------------------------------------------
/apps/docs/src/pages/components/divider.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Divider
3 | layout: ~/layouts/docs.astro
4 | ---
5 |
6 | import Playground from "~/components/playground.astro";
7 | import { BasicExample, VerticalExample } from "~/components/examples/divider";
8 |
9 | # {frontmatter.title}
10 |
11 | A divider is a thin line that groups content in lists and layouts.
12 |
13 | ## Basic usage
14 |
15 | By default the `Divider` is horizontal:
16 |
17 |
18 |
19 |
20 | ```tsx
21 |
22 | ```
23 |
24 |
25 |
26 | ## Vertical
27 |
28 | Also, you could add `orientation="vertical"` prop to `Divider`:
29 |
30 |
31 |
32 |
33 | ```tsx
34 |
37 | ```
38 |
39 |
40 |
--------------------------------------------------------------------------------
/apps/docs/src/pages/components/fab.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: FAB
3 | layout: ~/layouts/docs.astro
4 | ---
5 |
6 | import Playground from "~/components/playground.astro";
7 | import { BasicExample, SizeExample, ExtendedExample } from "~/components/examples/fab";
8 |
9 | # {frontmatter.title}
10 |
11 | A Floating Action Button (FAB) performs the primary, or most common, action on a screen.
12 |
13 | ## Basic usage
14 |
15 | The `FAB` comes with three variants: surface, secondary and tertiary.
16 |
17 |
18 |
19 |
20 | ```tsx
21 | } />
22 | } variant="secondary" />
23 | } variant="tertiary" />
24 | ```
25 |
26 |
27 |
28 | ## Extended mode
29 |
30 | When you add a `children`, `FAB` will behave in extended mode:
31 |
32 |
33 |
34 |
35 | ```tsx
36 | }>Add
37 | ```
38 |
39 |
40 |
41 | _Note: extended mode intentionally does not respect `size` prop_
42 |
43 | ## Sizes
44 |
45 | Also `FAB` can accept a `size` prop:
46 |
47 |
48 |
49 |
50 | ```tsx
51 | } size="small" />
52 | } size="medium" />
53 | } size="large" />
54 | ```
55 |
56 |
57 |
--------------------------------------------------------------------------------
/apps/docs/src/pages/components/icon-button.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: IconButton
3 | layout: ~/layouts/docs.astro
4 | ---
5 |
6 | import Playground from "~/components/playground.astro";
7 | import { BasicExample, DisabledExample } from "~/components/examples/icon-button";
8 |
9 | # {frontmatter.title}
10 |
11 | Icon buttons are commonly found in app bars and toolbars.
12 |
13 | ## Basic usage
14 |
15 | The `IconButton` comes with four variants: filled, tonal, outlined and standard.
16 |
17 |
18 |
19 |
20 | ```tsx
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | ```
34 |
35 |
36 |
37 | ## States
38 |
39 | ### Disabled
40 |
41 | You can disable the `IconButton` using the `isDisabled` prop:
42 |
43 |
44 |
45 |
46 | ```tsx
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | ```
60 |
61 |
62 |
--------------------------------------------------------------------------------
/apps/docs/src/pages/components/navigation-drawer.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: NavigationDrawer
3 | layout: ~/layouts/docs.astro
4 | ---
5 |
6 | import Playground from "~/components/playground.astro";
7 | import { BasicExample, ActiveExample, AdornmentExample } from "~/components/examples/navigation-drawer";
8 |
9 | # {frontmatter.title}
10 |
11 | Navigation drawers provide access to destinations in your app in large sized devices.
12 |
13 | ## Basic usage
14 |
15 | The `NavigationDrawer` must be composed with `NavigationDrawerItem`:
16 |
17 |
18 |
19 |
20 | ```tsx
21 |
22 | }>Home
23 | }>Settings
24 |
25 | ```
26 |
27 |
28 |
29 | ## Setting active location
30 |
31 | By default `NavigationDrawerItem` will listen to `active` class, but you could also supply `active` prop:
32 |
33 |
34 |
35 |
36 | ```tsx
37 |
38 | }>
39 | Home
40 |
41 | }>Settings
42 |
43 | ```
44 |
45 |
46 |
47 | ## Adding adornments
48 |
49 | You could also supply `startAdornment` prop to add leading components:
50 |
51 |
52 |
53 |
54 | ```tsx
55 |
(
58 | <>
59 |
60 |
61 |
62 | }>
63 | Add
64 |
65 | >
66 | )}
67 | >
68 | }>Home
69 | }>Settings
70 |
71 | ```
72 |
73 |
74 |
--------------------------------------------------------------------------------
/apps/docs/src/pages/components/navigation-rail.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: NavigationRail
3 | layout: ~/layouts/docs.astro
4 | ---
5 |
6 | import Playground from "~/components/playground.astro";
7 | import { BasicExample, ActiveExample, AdornmentExample } from "~/components/examples/navigation-rail";
8 |
9 | # {frontmatter.title}
10 |
11 | Navigation rails provide access to destinations in your app in medium sized devices.
12 |
13 | ## Basic usage
14 |
15 | The `NavigationRail` must be composed with `NavigationItem`:
16 |
17 |
18 |
19 |
20 | ```tsx
21 |
22 | }>Home
23 | }>Settings
24 |
25 | ```
26 |
27 |
28 |
29 | ## Setting active location
30 |
31 | By default `NavigationItem` will listen to `active` class, but you could also supply `active` prop:
32 |
33 |
34 |
35 |
36 | ```tsx
37 |
38 | }>
39 | Home
40 |
41 | }>Settings
42 |
43 | ```
44 |
45 |
46 |
47 | ## Adding adornments
48 |
49 | You could also supply `startAdornment` prop to add leading components:
50 |
51 |
52 |
53 |
54 | ```tsx
55 |
(
57 | <>
58 |
59 |
60 |
61 | } />
62 | >
63 | )}
64 | >
65 | }>Home
66 | }>Settings
67 |
68 | ```
69 |
70 |
71 |
--------------------------------------------------------------------------------
/apps/docs/src/pages/components/switch.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Switch
3 | layout: ~/layouts/docs.astro
4 | ---
5 |
6 | import Playground from "~/components/playground.astro";
7 | import { BasicExample, DisabledExample } from "~/components/examples/switch";
8 |
9 | # {frontmatter.title}
10 |
11 | Switches toggle the state of a single setting on or off.
12 |
13 | ## Basic usage
14 |
15 | The `Switch` needs a label.
16 |
17 |
18 |
19 |
20 | ```tsx
21 |
22 | ```
23 |
24 |
25 |
26 | ## States
27 |
28 | ### Disabled
29 |
30 | You can disable the `Switch` using the `isDisabled` prop:
31 |
32 |
33 |
34 |
35 | ```tsx
36 |
37 |
38 | ```
39 |
40 |
41 |
--------------------------------------------------------------------------------
/apps/docs/src/pages/components/tabs.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Tabs
3 | layout: ~/layouts/docs.astro
4 | ---
5 |
6 | import Playground from "~/components/playground.astro";
7 | import { BasicExample, WithoutIconsExample, SecondaryExample } from "~/components/examples/tabs";
8 |
9 | # {frontmatter.title}
10 |
11 | Tabs make it easy to explore and switch between different views.
12 |
13 | ## Basic usage
14 |
15 | The `Tabs` comes with two variants: primary and secondary.
16 |
17 | ### Primary
18 |
19 | With icons:
20 |
21 |
22 |
23 |
24 |
25 | Without icons:
26 |
27 |
28 |
29 |
30 |
31 | ### Secondary
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/apps/docs/src/pages/components/text-field.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: TextField
3 | layout: ~/layouts/docs.astro
4 | ---
5 |
6 | import Playground from "~/components/playground.astro";
7 | import {
8 | BasicExample,
9 | AdornmentExample,
10 | DescriptionExample,
11 | ErrorExample,
12 | DisabledExample,
13 | } from "~/components/examples/text-field";
14 |
15 | # {frontmatter.title}
16 |
17 | Text Fields let users enter and edit text.
18 |
19 | ## Basic usage
20 |
21 | The `TextField` needs a label.
22 |
23 |
24 |
25 |
26 | ```tsx
27 |
28 | ```
29 |
30 |
31 |
32 | ## Adding adornments
33 |
34 | You can add adornments by supplying `startAdornment` and `endAdornment` props:
35 |
36 |
37 |
38 |
39 | ```tsx
40 | } />
41 | (
44 |
45 |
46 |
47 | )}
48 | />
49 | ```
50 |
51 |
52 |
53 | ## Description
54 |
55 | You can add description to `TextField` using the `description` prop:
56 |
57 |
58 |
59 |
60 | ```tsx
61 |
62 | ```
63 |
64 |
65 |
66 | ## States
67 |
68 | ### Error
69 |
70 | You can set `validationState` prop to `invalid` to get a nice visual representation of an error:
71 |
72 |
73 |
74 |
75 | ```tsx
76 |
77 | ```
78 |
79 |
80 |
81 | ### Disabled
82 |
83 | You can disable the `TextField` using the `isDisabled` prop:
84 |
85 |
86 |
87 |
88 | ```tsx
89 |
90 | ```
91 |
92 |
93 |
--------------------------------------------------------------------------------
/apps/docs/src/pages/guide/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Getting Started
3 | layout: ~/layouts/docs.astro
4 | ---
5 |
6 | # {frontmatter.title}
7 |
8 | Quick guide to start using Solid Material.
9 |
10 | ## Installation
11 |
12 | Install the base package which contains every component:
13 |
14 | ```bash
15 | npm install @solidjs-material/core
16 | ```
17 |
18 | Install tailwind package which contains the design system:
19 |
20 | ```bash
21 | npm install @solidjs-material/tailwind --save-dev
22 | ```
23 |
24 | ## Adding Tailwindcss preset
25 |
26 | Tailwindcss need to find Solid Material components in package dist folder.
27 |
28 | ```js
29 | const { designSystem } = require("@solidjs-material/tailwind");
30 |
31 | module.exports = {
32 | content: ["./node_modules/@solidjs-material/core/dist/*.{js,cjs}", "./src/**/*.tsx"],
33 | presets: [designSystem({ baseColor: "#FACADA" })], // Try another colors!
34 | };
35 | ```
36 |
37 | ## Adding icons
38 |
39 | You can use any icon library that you want.
40 |
41 | ### Using [Solid Icons](https://solid-icons.vercel.app/)
42 |
43 | Start adding solid-icons to your project:
44 |
45 | ```bash
46 | npm install solid-icons
47 | ```
48 |
49 | Import some icons to your source code:
50 |
51 | ```tsx
52 | import { Button } from "@solidjs-material/core";
53 | import { FiHeart } from "solid-icons/fi";
54 |
55 | function App() {
56 | return }>Sponsor ;
57 | }
58 | ```
59 |
60 | ### Using [Unplugin Icons](https://github.com/antfu/unplugin-icons)
61 |
62 | Follow [unplugin guides](https://github.com/antfu/unplugin-icons#configuration) to add support to your bundler/framework.
63 |
64 | Install you desired icon pack:
65 |
66 | ```bash
67 | npm install @iconify-json/iconoir --save-dev
68 | ```
69 |
70 | Use `compiler: solid` option in plugin:
71 |
72 | ```js
73 | icons({ compiler: "solid" });
74 | ```
75 |
76 | If you use typescript, add the following configuration on your tsconfig.json:
77 |
78 | ```json
79 | {
80 | "compilerOptions": {
81 | "types": ["unplugin-icons/types/solid"]
82 | }
83 | }
84 | ```
85 |
86 | Import some icons to your source code:
87 |
88 | ```tsx
89 | import { Button } from "@solidjs-material/core";
90 | import Heart from "virtual:icons/iconoir/heart";
91 |
92 | function App() {
93 | return }>Sponsor ;
94 | }
95 | ```
96 |
97 | ## Adding custom webfonts
98 |
99 | Solid Material is designed to use any google font that you may want. You can install them using [Fontsource](https://fontsource.org/).
100 |
101 | ```bash
102 | npm install @fontsource/poppins
103 | ```
104 |
105 | Import font variants in your source code:
106 |
107 | ```tsx
108 | import "@fontsource/poppins";
109 | ```
110 |
111 | Modify Tailwindcss configuration to use the installed webfont:
112 |
113 | ```js
114 | const { designSystem } = require("@solidjs-material/tailwind");
115 | const defaultTheme = require("tailwindcss/defaultTheme");
116 |
117 | module.exports = {
118 | content: ["./node_modules/@solidjs-material/core/dist/*.{js,cjs}", "./src/**/*.tsx"],
119 | presets: [designSystem({ baseColor: "#FACADA" })], // Try another colors!
120 | theme: {
121 | extend: {
122 | fontFamily: {
123 | sans: ["Poppins", ...defaultTheme.fontFamily.sans],
124 | },
125 | },
126 | },
127 | };
128 | ```
129 |
--------------------------------------------------------------------------------
/apps/docs/src/pages/guide/usage.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Usage
3 | layout: ~/layouts/docs.astro
4 | ---
5 |
6 | # {frontmatter.title}
7 |
8 | Learn the basics of working with Solid Material components.
9 |
10 | ## Quickstart
11 |
12 | Start importing a component from `@solidjs-material/core`:
13 |
14 | ```tsx
15 | import { Button } from "@solidjs-material/core";
16 |
17 | export function App() {
18 | return Hello ;
19 | }
20 | ```
21 |
22 | ## Global styles
23 |
24 | Solid Material automatically apply global styles such as body background and foreground colors. Every single color is a `css variable` too! You could use like so:
25 |
26 | ```css
27 | .some-component {
28 | background-color: rgb(var(--primary) / 1); /* Plain css */
29 | }
30 |
31 | .some-component {
32 | @apply bg-primary; /* Tailwindcss */
33 | }
34 | ```
35 |
--------------------------------------------------------------------------------
/apps/docs/src/pages/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { Button, Card } from "@solidjs-material/core";
3 |
4 | import Layout from "~/layouts/layout.astro";
5 |
6 | const cards = [
7 | {
8 | title: "Made with SolidJS",
9 | content: "Simple and performant reactivity for building user interfaces.",
10 | image: "/solid.svg",
11 | alt: "SolidJS",
12 | },
13 | {
14 | title: "Following Material You",
15 | content: "Design and build beautiful, usable products with Material 3.",
16 | image: "/material.svg",
17 | alt: "Material Design",
18 | },
19 | {
20 | title: "TailwindCSS Powered",
21 | content: "Rapidly build modern websites without ever leaving your HTML.",
22 | image: "/tailwindcss.svg",
23 | alt: "Tailwindcss",
24 | },
25 | ];
26 | ---
27 |
28 |
29 |
30 |
31 |
32 | SolidMaterial
33 | High quality components at ease.
34 |
35 | Quickly build interfaces following Material You guidelines.
36 |
37 | Get Started
38 |
46 | View on GitHub
47 |
48 |
49 |
50 |
51 | {
52 | cards.map((card) => (
53 |
54 | {card.image && (
55 |
56 |
57 |
58 | )}
59 | {card.title}
60 | {card.content}
61 |
62 | ))
63 | }
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/apps/docs/src/utils/click-outside.ts:
--------------------------------------------------------------------------------
1 | import type { Accessor } from "solid-js";
2 |
3 | import { onCleanup, onMount } from "solid-js";
4 |
5 | const events = ["mousedown", "touchstart"];
6 |
7 | type CreateClickOutsideOptions = {
8 | ref: Accessor;
9 | isDisabled?: Accessor;
10 | callback: (el: T) => void;
11 | };
12 |
13 | export function createClickOutside(options: CreateClickOutsideOptions) {
14 | function onClick(event: Event) {
15 | if (options.isDisabled?.()) return;
16 |
17 | const el = options.ref();
18 | if (el && !el.contains(event.target as HTMLElement)) options.callback(el);
19 | }
20 |
21 | onMount(() => events.forEach((event) => document.addEventListener(event, onClick)));
22 | onCleanup(() => events.forEach((event) => document.removeEventListener(event, onClick)));
23 | }
24 |
--------------------------------------------------------------------------------
/apps/docs/tailwind.config.cjs:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-extraneous-dependencies, global-require */
2 | const { designSystem } = require("@solidjs-material/tailwind");
3 | const defaultTheme = require("tailwindcss/defaultTheme");
4 | const plugin = require("tailwindcss/plugin");
5 |
6 | /** @type {import('tailwindcss').Config} */
7 | module.exports = {
8 | content: [
9 | "./node_modules/@solidjs-material/core/dist/*.{js,cjs}",
10 | "./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}",
11 | ],
12 | presets: [designSystem({ baseColor: "#00FFCC" })],
13 | theme: {
14 | extend: {
15 | fontFamily: {
16 | sans: ["Poppins", "Poppins override", ...defaultTheme.fontFamily.sans],
17 | },
18 | },
19 | },
20 | plugins: [
21 | require("@tailwindcss/typography"),
22 | plugin(({ addBase, addUtilities }) => {
23 | addBase({
24 | ":root": {
25 | "--astro-code-color-text": "rgb(var(--on-surface) / 1)",
26 | "--astro-code-color-background": "rgb(var(--surface-variant) / 1)",
27 | "--astro-code-token-constant": "rgb(var(--on-secondary-container) / 1)",
28 | "--astro-code-token-string": "rgb(var(--on-secondary-container) / 1)",
29 | "--astro-code-token-comment": "rgb(var(--outline) / 1)",
30 | "--astro-code-token-keyword": "rgb(var(--primary) / 1)",
31 | "--astro-code-token-parameter": "#AA0000",
32 | "--astro-code-token-function": "rgb(var(--tertiary) / 1)",
33 | "--astro-code-token-string-expression": "rgb(var(--error) / 1)",
34 | "--astro-code-token-punctuation": "rgb(var(--tertiary) / 1)",
35 | "--astro-code-token-link": "rgb(var(--tertiary) / 1)",
36 | },
37 | });
38 |
39 | addUtilities({
40 | ".reset-svg svg": {
41 | width: "1em",
42 | height: "1em",
43 | },
44 | });
45 | }),
46 | ],
47 | };
48 |
--------------------------------------------------------------------------------
/apps/docs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "astro/tsconfigs/strict",
3 | "compilerOptions": {
4 | "allowJs": true,
5 | "jsx": "preserve",
6 | "jsxImportSource": "solid-js",
7 | "baseUrl": ".",
8 | "paths": {
9 | "~/*": ["src/*"]
10 | }
11 | },
12 | "include": ["src", ".eslintrc.cjs", "tailwind.config.cjs", "astro.config.mjs"]
13 | }
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "solidjs-material",
3 | "version": "1.0.0",
4 | "private": true,
5 | "description": "",
6 | "keywords": [],
7 | "license": "MIT",
8 | "author": {
9 | "name": "Carlos Eduardo de Oliveira Paludetto",
10 | "email": "ceo.paludetto@gmail.com"
11 | },
12 | "main": "index.js",
13 | "scripts": {
14 | "build": "turbo build",
15 | "ci:publish": "turbo run lint build --filter=\"./packages/*\" && git status && changeset publish",
16 | "ci:version": "changeset version && pnpm install --no-frozen-lockfile && git add .",
17 | "dev": "turbo dev --parallel",
18 | "lint": "turbo lint",
19 | "lint:fix": "turbo lint:fix",
20 | "test": "turbo test"
21 | },
22 | "devDependencies": {
23 | "@changesets/cli": "^2.26.0",
24 | "prettier": "^2.8.4",
25 | "prettier-plugin-astro": "^0.8.0",
26 | "prettier-plugin-packagejson": "^2.4.3",
27 | "turbo": "^1.8.1",
28 | "typescript": "^4.9.5"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/packages/configuration/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @solidjs-material/configuration
2 |
3 | ## 0.0.7
4 |
5 | ### Patch Changes
6 |
7 | - 423f693: Fix CI
8 |
9 | ## 0.0.6
10 |
11 | ### Patch Changes
12 |
13 | - Add tabs component
14 | Add tests
15 | Bump kobalte version
16 | Fix components interactions
17 |
18 | ## 0.0.5
19 |
20 | ### Patch Changes
21 |
22 | - f7f74e3: Add new docs
23 | - 5a012bf: Fix some style inconsistances
24 |
25 | ## 0.0.4
26 |
27 | ### Patch Changes
28 |
29 | - 656fac5: Add dialog component
30 | Fix ref handling
31 |
32 | ## 0.0.3
33 |
34 | ### Patch Changes
35 |
36 | - c00fd89: Bump depedencies
37 |
38 | ## 0.0.2
39 |
40 | ### Patch Changes
41 |
42 | - fix changeset configurations
43 |
44 | ## 0.0.1
45 |
46 | ### Patch Changes
47 |
48 | - 029b1d1: Base release
49 |
--------------------------------------------------------------------------------
/packages/configuration/eslint.astro.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: [require.resolve("./eslint.solid.cjs"), "plugin:astro/recommended"],
3 | parserOptions: { extraFileExtensions: [".astro"] },
4 | overrides: [
5 | {
6 | files: ["*.astro"],
7 | parser: "astro-eslint-parser",
8 | rules: {
9 | // solid
10 | "solid/no-unknown-namespaces": "off",
11 | "solid/prefer-for": "off",
12 | "solid/style-prop": "off",
13 | // import
14 | "import/no-unresolved": "off",
15 | },
16 | },
17 | ],
18 | };
19 |
--------------------------------------------------------------------------------
/packages/configuration/eslint.base.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ["airbnb-base", "airbnb-typescript/base", "prettier"],
3 | plugins: ["prettier"],
4 | rules: {
5 | "prettier/prettier": "warn",
6 | // typescript
7 | "@typescript-eslint/no-throw-literal": "off",
8 | "@typescript-eslint/consistent-type-imports": "warn",
9 | "@typescript-eslint/sort-type-union-intersection-members": "warn",
10 | // import
11 | "import/extensions": "off",
12 | "import/prefer-default-export": "off",
13 | "import/order": [
14 | "warn",
15 | {
16 | alphabetize: { order: "asc", caseInsensitive: true },
17 | groups: ["type", "builtin", "external", "index", ["internal", "sibling", "parent"], "object"],
18 | pathGroups: [
19 | {
20 | pattern: "~/**",
21 | group: "internal",
22 | },
23 | ],
24 | pathGroupsExcludedImportTypes: ["builtin", "type", "external", "object"],
25 | "newlines-between": "always",
26 | },
27 | ],
28 | // misc
29 | "no-restricted-syntax": "off",
30 | "no-continue": "off",
31 | },
32 | };
33 |
--------------------------------------------------------------------------------
/packages/configuration/eslint.solid.cjs:
--------------------------------------------------------------------------------
1 | const { resolve } = require("path");
2 |
3 | module.exports = {
4 | extends: [require.resolve("./eslint.base.cjs"), "plugin:tailwindcss/recommended", "plugin:solid/recommended"],
5 | plugins: ["tailwindcss", "solid"],
6 | rules: {},
7 | settings: {
8 | tailwindcss: {
9 | // These are the default values but feel free to customize
10 | callees: ["classnames", "clsx", "ctl", "cva"],
11 | config: resolve(__dirname, "..", "..", "apps", "docs", "tailwind.config.cjs"),
12 | },
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/packages/configuration/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@solidjs-material/configuration",
3 | "version": "0.0.7",
4 | "description": "Solid material base configuration",
5 | "keywords": [],
6 | "license": "MIT",
7 | "author": {
8 | "name": "Carlos Eduardo de Oliveira Paludetto",
9 | "email": "ceo.paludetto@gmail.com"
10 | },
11 | "type": "module",
12 | "files": [
13 | "tsconfig.base.json",
14 | "eslint.base.cjs",
15 | "eslint.solid.cjs",
16 | "eslint.astro.cjs",
17 | "utils.ts"
18 | ],
19 | "dependencies": {
20 | "@typescript-eslint/eslint-plugin": "^5.52.0",
21 | "@typescript-eslint/parser": "^5.52.0",
22 | "eslint": "^8.34.0",
23 | "eslint-config-airbnb-base": "^15.0.0",
24 | "eslint-config-airbnb-typescript": "^17.0.0",
25 | "eslint-config-prettier": "^8.6.0",
26 | "eslint-plugin-astro": "^0.23.0",
27 | "eslint-plugin-import": "^2.27.5",
28 | "eslint-plugin-prettier": "^4.2.1",
29 | "eslint-plugin-solid": "^0.10.0",
30 | "eslint-plugin-tailwindcss": "^3.9.0"
31 | },
32 | "publishConfig": {
33 | "access": "public"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/packages/configuration/tsconfig.base.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["ESNext"],
4 | "module": "ESNext",
5 | "target": "ESNext",
6 | "moduleResolution": "Node",
7 | "importsNotUsedAsValues": "error",
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "skipLibCheck": true,
11 | "allowJs": true,
12 | "strict": true
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/packages/configuration/utils.ts:
--------------------------------------------------------------------------------
1 | export function buildExternal(pkg: Record, keep: string[]) {
2 | return [...Object.keys(pkg.dependencies), ...Object.keys(pkg.devDependencies)].filter((item) => !keep.includes(item));
3 | }
4 |
--------------------------------------------------------------------------------
/packages/core/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-extraneous-dependencies */
2 | const { resolve } = require("path");
3 |
4 | require("@rushstack/eslint-patch/modern-module-resolution");
5 |
6 | module.exports = {
7 | extends: [require.resolve("@solidjs-material/configuration/eslint.solid.cjs")],
8 | parserOptions: { project: resolve(__dirname, "tsconfig.json") },
9 | overrides: [{ files: ["./test/*.{ts,tsx}"], rules: { "import/no-extraneous-dependencies": "off" } }],
10 | };
11 |
--------------------------------------------------------------------------------
/packages/core/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @solidjs-material/core
2 |
3 | ## 0.0.12
4 |
5 | ### Patch Changes
6 |
7 | - 423f693: Fix CI
8 |
9 | ## 0.0.11
10 |
11 | ### Patch Changes
12 |
13 | - 986c180: Fix lint script
14 |
15 | ## 0.0.10
16 |
17 | ### Patch Changes
18 |
19 | - Add tabs component
20 | Add tests
21 | Bump kobalte version
22 | Fix components interactions
23 |
24 | ## 0.0.9
25 |
26 | ### Patch Changes
27 |
28 | - d544306: Add topbar component
29 |
30 | ## 0.0.8
31 |
32 | ### Patch Changes
33 |
34 | - f7f74e3: Add new docs
35 | - 62ac49f: Fix dialog styles
36 | - 62ac49f: Improve ripple usage by leveraging ref logic inside primitive
37 | - 5a012bf: Fix some style inconsistances
38 |
39 | ## 0.0.7
40 |
41 | ### Patch Changes
42 |
43 | - 4d6e807: Add missing types
44 | Fix storybook stories
45 |
46 | ## 0.0.6
47 |
48 | ### Patch Changes
49 |
50 | - 656fac5: Add dialog component
51 | Fix ref handling
52 | - a99edda: Add checkbox component
53 |
54 | ## 0.0.5
55 |
56 | ### Patch Changes
57 |
58 | - c00fd89: Bump depedencies
59 | - 2efedad: Add w-full to TextField
60 |
61 | ## 0.0.4
62 |
63 | ### Patch Changes
64 |
65 | - fix text-field splitProps logic
66 |
67 | ## 0.0.3
68 |
69 | ### Patch Changes
70 |
71 | - fix text field styles
72 |
73 | ## 0.0.2
74 |
75 | ### Patch Changes
76 |
77 | - fix changeset configurations
78 |
79 | ## 0.0.1
80 |
81 | ### Patch Changes
82 |
83 | - 029b1d1: Base release
84 |
--------------------------------------------------------------------------------
/packages/core/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@solidjs-material/core",
3 | "version": "0.0.12",
4 | "description": "Solid Material core components",
5 | "keywords": [
6 | "material you",
7 | "material design",
8 | "solid-js"
9 | ],
10 | "license": "MIT",
11 | "author": {
12 | "name": "Carlos Eduardo de Oliveira Paludetto",
13 | "email": "ceo.paludetto@gmail.com"
14 | },
15 | "type": "module",
16 | "exports": {
17 | "solid": "./dist/index.jsx",
18 | "import": {
19 | "types": "./dist/index.d.ts",
20 | "default": "./dist/index.js"
21 | },
22 | "require": "./dist/index.cjs"
23 | },
24 | "main": "./dist/index.cjs",
25 | "module": "./dist/index.js",
26 | "browser": {},
27 | "types": "./dist/index.d.ts",
28 | "typesVersions": {},
29 | "files": [
30 | "./dist"
31 | ],
32 | "scripts": {
33 | "build": "tsup",
34 | "build:storybook": "sb build",
35 | "dev": "tsup --watch",
36 | "lint": "eslint \"src/**/*.{ts,tsx}\"",
37 | "lint:fix": "eslint \"src/**/*.{ts,tsx}\" --fix",
38 | "test": "vitest run"
39 | },
40 | "dependencies": {
41 | "@kobalte/core": "^0.6.1",
42 | "@kobalte/utils": "^0.5.1",
43 | "class-variance-authority": "^0.4.0",
44 | "solid-js": "^1.6.11"
45 | },
46 | "devDependencies": {
47 | "@rushstack/eslint-patch": "^1.2.0",
48 | "@solidjs-material/configuration": "workspace:^0.0.7",
49 | "@solidjs/testing-library": "^0.6.1",
50 | "@types/testing-library__jest-dom": "github:zoontek/types-testing-library-vitest-dom",
51 | "babel-preset-solid": "^1.6.10",
52 | "eslint": "^8.34.0",
53 | "happy-dom": "^8.6.0",
54 | "solid-icons": "^1.0.4",
55 | "tsup": "^6.6.3",
56 | "tsup-preset-solid": "^0.1.8",
57 | "vite": "^4.1.3",
58 | "vite-plugin-solid": "^2.5.0",
59 | "vitest": "^0.28.5"
60 | },
61 | "peerDependencies": {
62 | "solid-js": "^1.6.0"
63 | },
64 | "publishConfig": {
65 | "access": "public"
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/packages/core/src/components/button/index.tsx:
--------------------------------------------------------------------------------
1 | import type { PolymorphicProps, As } from "@kobalte/utils";
2 | import type { VariantProps } from "class-variance-authority";
3 | import type { JSX } from "solid-js";
4 |
5 | import { Button as KButton } from "@kobalte/core";
6 | import { Show, splitProps } from "solid-js";
7 |
8 | import { icon, root } from "./styles";
9 | import { createRipples } from "~/primitives";
10 | import { mergeWithRefs } from "~/utils/refs";
11 |
12 | type ButtonOwnProps = KButton.ButtonRootOptions &
13 | Omit, "hasEndIcon" | "hasStartIcon"> & {
14 | class?: string;
15 | children: JSX.Element;
16 | startIcon?: JSX.Element;
17 | endIcon?: JSX.Element;
18 | };
19 |
20 | export type ButtonProps = PolymorphicProps;
21 |
22 | export function Button(props: ButtonProps) {
23 | const [trigger] = createRipples({ disabled: props.isDisabled });
24 | const [local, rest] = splitProps(props, ["class", "children", "variant", "startIcon", "endIcon"]);
25 |
26 | return (
27 |
36 |
37 | {local.startIcon}
38 |
39 | {local.children}
40 |
41 | {local.endIcon}
42 |
43 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/packages/core/src/components/button/styles.ts:
--------------------------------------------------------------------------------
1 | import { cva } from "class-variance-authority";
2 |
3 | export const root = cva(
4 | [
5 | "inline-flex h-10 items-center justify-center gap-x-2 rounded-full text-label-large outline-none transition state-layer ripple",
6 | "focus:focus-state-layer focus-visible:hover-state-layer ui-not-disabled:hover:hover-state-layer ui-not-disabled:active:press-state-layer",
7 | ],
8 | {
9 | variants: {
10 | variant: {
11 | filled: ["bg-primary text-on-primary ui-disabled:bg-on-surface/12 ui-disabled:text-on-surface/38"],
12 | tonal: [
13 | "bg-secondary-container text-on-secondary-container ui-disabled:bg-on-surface/12 ui-disabled:text-on-surface/38",
14 | ],
15 | outlined: [
16 | "border border-outline bg-surface text-primary focus:border-primary ui-disabled:border-on-surface/12 ui-disabled:text-on-surface/38",
17 | ],
18 | text: ["bg-transparent text-primary ui-disabled:text-on-surface/38"],
19 | },
20 | hasStartIcon: {
21 | true: ["pl-4"],
22 | false: ["pl-6"],
23 | },
24 | hasEndIcon: {
25 | true: ["pr-4"],
26 | false: ["pr-6"],
27 | },
28 | },
29 | },
30 | );
31 |
32 | export const icon = cva(["inline-flex text-icon-small reset-svg"]);
33 |
--------------------------------------------------------------------------------
/packages/core/src/components/card/index.tsx:
--------------------------------------------------------------------------------
1 | import type { As, PolymorphicProps } from "@kobalte/utils";
2 | import type { VariantProps } from "class-variance-authority";
3 | import type { JSX } from "solid-js";
4 |
5 | import { splitProps } from "solid-js";
6 | import { Dynamic } from "solid-js/web";
7 |
8 | import { root } from "./styles";
9 |
10 | type CardOwnProps = VariantProps & {
11 | class?: string;
12 | children?: JSX.Element;
13 | };
14 |
15 | export type CardProps = PolymorphicProps;
16 |
17 | export function Card(props: CardProps) {
18 | const [local, rest] = splitProps(props, ["as", "children", "class", "variant"]);
19 |
20 | return (
21 |
26 | {local.children}
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/packages/core/src/components/card/styles.ts:
--------------------------------------------------------------------------------
1 | import { cva } from "class-variance-authority";
2 |
3 | export const root = cva(["rounded-md"], {
4 | variants: {
5 | variant: {
6 | filled: ["bg-surface-variant"],
7 | outlined: ["border border-outline bg-surface"],
8 | },
9 | },
10 | });
11 |
--------------------------------------------------------------------------------
/packages/core/src/components/checkbox/index.tsx:
--------------------------------------------------------------------------------
1 | import type { As, PolymorphicProps } from "@kobalte/utils";
2 | import type { VariantProps } from "class-variance-authority";
3 | import type { JSX } from "solid-js";
4 |
5 | import { Checkbox as KCheckbox } from "@kobalte/core";
6 | import { Show, splitProps } from "solid-js";
7 |
8 | import { control, indicator, label, root } from "./styles";
9 | import { createRipples } from "~/primitives";
10 | import { mergeWithRefs } from "~/utils/refs";
11 |
12 | type CheckboxOwnProps = KCheckbox.CheckboxRootOptions &
13 | VariantProps & {
14 | label: string;
15 | checkIcon: JSX.Element;
16 | indeterminateIcon: JSX.Element;
17 | class?: string;
18 | };
19 |
20 | export type CheckboxProps = PolymorphicProps;
21 |
22 | export function Checkbox(props: CheckboxProps) {
23 | const [trigger, positioner] = createRipples({ size: 40, center: true, disabled: props.isDisabled });
24 | const [local, rest] = splitProps(props, ["class", "label", "checkIcon", "indeterminateIcon", "labelPlacement"]);
25 |
26 | return (
27 |
31 |
32 | {local.label}
33 |
34 |
35 | {local.checkIcon}
36 | {local.indeterminateIcon}
37 |
38 |
39 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/packages/core/src/components/checkbox/styles.ts:
--------------------------------------------------------------------------------
1 | import { cva } from "class-variance-authority";
2 |
3 | export const root = cva(
4 | [
5 | "flex items-center gap-4",
6 | "focus:group-focus-state-layer ui-not-disabled:hover:group-hover-state-layer ui-not-disabled:active:group-press-state-layer",
7 | ],
8 | {
9 | variants: {
10 | labelPlacement: {
11 | left: ["flex-row"],
12 | right: ["flex-row-reverse"],
13 | top: ["flex-col"],
14 | bottom: ["flex-col-reverse"],
15 | },
16 | },
17 | },
18 | );
19 |
20 | export const label = cva(["text-on-surface"]);
21 |
22 | export const control = cva([
23 | "relative isolate inline-flex aspect-square h-[18px] rounded-xxs border-on-surface state-layer state-layer-10 ui-not-checked:border-2",
24 | "ui-disabled:border-on-surface/38 ui-disabled:ui-checked:bg-on-surface/38 ui-not-disabled:ui-checked:bg-primary",
25 | ]);
26 |
27 | export const indicator = cva([
28 | "inline-flex items-center justify-center text-icon-small text-on-primary reset-svg ui-not-checked:hidden",
29 | ]);
30 |
--------------------------------------------------------------------------------
/packages/core/src/components/dialog/index.tsx:
--------------------------------------------------------------------------------
1 | import type { As, PolymorphicProps } from "@kobalte/utils";
2 | import type { JSX } from "solid-js";
3 |
4 | import { Dialog as KDialog } from "@kobalte/core";
5 | import { Show, splitProps } from "solid-js";
6 |
7 | import { actions, content, description, icon, overlay, positioner, title } from "./styles";
8 |
9 | type DialogOwnProps = KDialog.DialogRootOptions & {
10 | class?: string;
11 | children?: JSX.Element;
12 | title: string;
13 | description: string;
14 | endAdornment?: JSX.Element;
15 | icon?: JSX.Element;
16 | };
17 |
18 | export type DialogContentProps = PolymorphicProps;
19 |
20 | function Content(props: DialogContentProps) {
21 | const [local, rest] = splitProps(props, ["class", "title", "children", "description", "endAdornment", "icon"]);
22 |
23 | return (
24 |
25 | {local.children}
26 |
27 |
28 |
29 |
30 |
31 | {local.icon}
32 |
33 | {local.title}
34 | {local.description}
35 |
36 | {local.endAdornment}
37 |
38 |
39 |
40 |
41 |
42 | );
43 | }
44 |
45 | export const Dialog = { Content, Trigger: KDialog.Trigger };
46 |
--------------------------------------------------------------------------------
/packages/core/src/components/dialog/styles.ts:
--------------------------------------------------------------------------------
1 | import { cva } from "class-variance-authority";
2 |
3 | export const overlay = cva(["fixed inset-0 bg-scrim/30"]);
4 |
5 | export const positioner = cva(["fixed inset-0 flex items-center justify-center"]);
6 |
7 | export const content = cva([
8 | "flex min-w-[280px] max-w-[560px] flex-col items-center gap-4 rounded-xl bg-surface p-6 outline-none",
9 | "relative overflow-hidden surface surface-3",
10 | ]);
11 |
12 | export const icon = cva(["text-icon-medium text-secondary reset-svg"]);
13 |
14 | export const title = cva(["text-headline-small text-on-surface"]);
15 |
16 | export const description = cva(["text-body-medium text-on-surface-variant"]);
17 |
18 | export const actions = cva(["flex w-full items-center justify-end gap-x-2 pt-2"]);
19 |
--------------------------------------------------------------------------------
/packages/core/src/components/divider/index.tsx:
--------------------------------------------------------------------------------
1 | import type { As, PolymorphicProps } from "@kobalte/utils";
2 |
3 | import { Separator as KSeparator } from "@kobalte/core";
4 | import { splitProps } from "solid-js";
5 |
6 | import { root } from "./styles";
7 |
8 | type DividerOwnProps = KSeparator.SeparatorRootOptions & {
9 | class?: string;
10 | };
11 |
12 | export type DividerProps = PolymorphicProps;
13 |
14 | export function Divider(props: DividerProps) {
15 | const [local, rest] = splitProps(props, ["class"]);
16 |
17 | return ;
18 | }
19 |
--------------------------------------------------------------------------------
/packages/core/src/components/divider/styles.ts:
--------------------------------------------------------------------------------
1 | import { cva } from "class-variance-authority";
2 |
3 | export const root = cva([
4 | "border-none bg-outline ui-horizontal:h-px ui-horizontal:w-full ui-vertical:h-full ui-vertical:w-px",
5 | ]);
6 |
--------------------------------------------------------------------------------
/packages/core/src/components/fab/index.tsx:
--------------------------------------------------------------------------------
1 | import type { PolymorphicProps, As } from "@kobalte/utils";
2 | import type { VariantProps } from "class-variance-authority";
3 | import type { JSX } from "solid-js";
4 |
5 | import { Button as KButton } from "@kobalte/core";
6 | import { createMemo, Show, splitProps } from "solid-js";
7 |
8 | import { icon, root } from "./styles";
9 | import { createRipples } from "~/primitives";
10 | import { mergeWithRefs } from "~/utils/refs";
11 |
12 | type FABOwnProps = KButton.ButtonRootOptions &
13 | VariantProps & {
14 | class?: string;
15 | icon?: JSX.Element;
16 | children?: JSX.Element;
17 | };
18 |
19 | export type FABProps = PolymorphicProps;
20 |
21 | export function FAB(props: FABProps): JSX.Element {
22 | const [trigger] = createRipples({ disabled: props.isDisabled });
23 | const [local, rest] = splitProps(props, ["class", "children", "variant", "icon", "size"]);
24 |
25 | const isExtended = createMemo(() => !!local.children);
26 |
27 | return (
28 |
37 |
38 | {local.icon}
39 |
40 | {local.children}
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/packages/core/src/components/fab/styles.ts:
--------------------------------------------------------------------------------
1 | import { cva } from "class-variance-authority";
2 |
3 | export const root = cva(
4 | [
5 | "inline-flex items-center justify-center gap-x-2 text-label-large outline-none state-layer ripple",
6 | "focus:focus-state-layer focus-visible:hover-state-layer ui-not-disabled:hover:hover-state-layer ui-not-disabled:active:press-state-layer",
7 | ],
8 | {
9 | variants: {
10 | isExtended: {
11 | true: ["min-w-[80px] px-4"],
12 | false: ["aspect-square"],
13 | },
14 | size: {
15 | small: ["h-10 rounded-md"],
16 | medium: ["h-14 rounded-lg"],
17 | large: ["h-24 rounded-xl"],
18 | },
19 | variant: {
20 | surface: ["bg-surface text-primary"],
21 | secondary: ["bg-secondary-container text-on-secondary-container"],
22 | tertiary: ["bg-tertiary-container text-on-tertiary-container"],
23 | },
24 | },
25 | },
26 | );
27 |
28 | export const icon = cva(["inline-flex reset-svg"], {
29 | variants: {
30 | size: {
31 | small: ["text-icon-medium"],
32 | medium: ["text-icon-medium"],
33 | large: ["text-icon-large"],
34 | },
35 | },
36 | });
37 |
--------------------------------------------------------------------------------
/packages/core/src/components/icon-button/index.tsx:
--------------------------------------------------------------------------------
1 | import type { As, PolymorphicProps } from "@kobalte/utils";
2 | import type { VariantProps } from "class-variance-authority";
3 | import type { JSX } from "solid-js";
4 |
5 | import { Button as KButton } from "@kobalte/core";
6 | import { splitProps } from "solid-js";
7 |
8 | import { root } from "./styles";
9 | import { createRipples } from "~/primitives";
10 | import { mergeWithRefs } from "~/utils/refs";
11 |
12 | type IconButtonOwnProps = KButton.ButtonRootOptions &
13 | VariantProps & {
14 | class?: string;
15 | children: JSX.Element;
16 | };
17 |
18 | export type IconButtonProps = PolymorphicProps;
19 |
20 | export function IconButton(props: IconButtonProps) {
21 | const [trigger] = createRipples({ center: true, disabled: props.isDisabled });
22 | const [local, rest] = splitProps(props, ["children", "class", "variant"]);
23 |
24 | return (
25 |
29 | {local.children}
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/packages/core/src/components/icon-button/styles.ts:
--------------------------------------------------------------------------------
1 | import { cva } from "class-variance-authority";
2 |
3 | export const root = cva(
4 | [
5 | "inline-flex aspect-square h-10 items-center justify-center rounded-full text-icon-medium",
6 | "outline-none state-layer ripple reset-svg",
7 | "focus:focus-state-layer focus-visible:hover-state-layer ui-not-disabled:hover:hover-state-layer ui-not-disabled:active:press-state-layer",
8 | ],
9 | {
10 | variants: {
11 | variant: {
12 | filled: ["bg-primary text-on-primary ui-disabled:bg-on-surface/12 ui-disabled:text-on-surface/38"],
13 | tonal: [
14 | "bg-secondary-container text-on-secondary-container ui-disabled:bg-on-surface/12 ui-disabled:text-on-surface/38",
15 | ],
16 | outlined: [
17 | "border border-outline bg-surface text-primary focus:border-primary ui-disabled:border-on-surface/12 ui-disabled:text-on-surface/38",
18 | ],
19 | standard: ["bg-transparent text-on-surface-variant ui-disabled:text-on-surface/38"],
20 | },
21 | },
22 | },
23 | );
24 |
--------------------------------------------------------------------------------
/packages/core/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./button";
2 | export * from "./card";
3 | export * from "./checkbox";
4 | export * from "./dialog";
5 | export * from "./divider";
6 | export * from "./fab";
7 | export * from "./icon-button";
8 | export * from "./navigation-drawer";
9 | export * from "./navigation-drawer-item";
10 | export * from "./navigation-item";
11 | export * from "./navigation-rail";
12 | export * from "./switch";
13 | export * from "./tabs";
14 | export * from "./text-field";
15 | export * from "./top-bar";
16 |
--------------------------------------------------------------------------------
/packages/core/src/components/navigation-drawer-item/index.tsx:
--------------------------------------------------------------------------------
1 | import type { As, PolymorphicProps } from "@kobalte/utils";
2 | import type { JSX } from "solid-js";
3 |
4 | import { Link as KLink } from "@kobalte/core";
5 | import { Show, splitProps } from "solid-js";
6 |
7 | import { content, icon, root } from "./styles";
8 | import { createRipples } from "~/primitives";
9 | import { mergeWithRefs } from "~/utils/refs";
10 |
11 | type NavigationDrawerItemOwnProps = KLink.LinkRootOptions & {
12 | class?: string;
13 | active?: boolean;
14 | icon?: JSX.Element;
15 | children: JSX.Element;
16 | };
17 |
18 | export type NavigationDrawerItemProps = PolymorphicProps;
19 |
20 | export function NavigationDrawerItem(props: NavigationDrawerItemProps) {
21 | const [trigger] = createRipples({ disabled: props.isDisabled });
22 | const [local, rest] = splitProps(props, ["class", "children", "icon", "active"]);
23 |
24 | return (
25 |
26 |
27 | {local.icon}
28 |
29 | {local.children}
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/packages/core/src/components/navigation-drawer-item/styles.ts:
--------------------------------------------------------------------------------
1 | import { cva } from "class-variance-authority";
2 |
3 | // eslint-disable-next-line tailwindcss/no-custom-classname
4 | export const root = cva(
5 | [
6 | "inline-flex h-14 w-full select-none items-center gap-x-3 rounded-full pl-3 pr-6 outline-none state-layer ripple",
7 | "focus:focus-state-layer focus-visible:hover-state-layer ui-not-disabled:hover:hover-state-layer ui-not-disabled:active:press-state-layer",
8 | "ui-in-route:bg-secondary-container ui-in-route:text-on-secondary-container ui-not-in-route:text-on-surface-variant",
9 | ],
10 | {
11 | variants: {
12 | active: {
13 | true: "active",
14 | },
15 | },
16 | },
17 | );
18 |
19 | export const content = cva(["flex-1 text-label-large"]);
20 |
21 | export const icon = cva(["text-icon-medium reset-svg"]);
22 |
--------------------------------------------------------------------------------
/packages/core/src/components/navigation-drawer/index.tsx:
--------------------------------------------------------------------------------
1 | import type { As, PolymorphicProps } from "@kobalte/utils";
2 | import type { JSX } from "solid-js";
3 |
4 | import { Show, splitProps } from "solid-js";
5 | import { Dynamic } from "solid-js/web";
6 |
7 | import { adornment, root } from "./styles";
8 |
9 | type NavigationDrawerOwnProps = {
10 | class?: string;
11 | children?: JSX.Element;
12 | startAdornment?: JSX.Element;
13 | };
14 |
15 | export type NavigationDrawerProps = PolymorphicProps;
16 |
17 | export function NavigationDrawer(props: NavigationDrawerProps) {
18 | const [local, rest] = splitProps(props, ["as", "class", "children", "startAdornment"]);
19 |
20 | return (
21 |
22 |
23 | {local.startAdornment}
24 |
25 | {local.children}
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/packages/core/src/components/navigation-drawer/styles.ts:
--------------------------------------------------------------------------------
1 | import { cva } from "class-variance-authority";
2 |
3 | export const root = cva(["flex w-full flex-col bg-surface px-3"]);
4 |
5 | export const adornment = cva(["mb-6 flex flex-col items-start gap-3"]);
6 |
--------------------------------------------------------------------------------
/packages/core/src/components/navigation-item/index.tsx:
--------------------------------------------------------------------------------
1 | import type { As, PolymorphicProps } from "@kobalte/utils";
2 | import type { JSX } from "solid-js";
3 |
4 | import { Link as KLink } from "@kobalte/core";
5 | import { splitProps } from "solid-js";
6 |
7 | import { content, icon, root } from "./styles";
8 | import { createRipples } from "~/primitives";
9 | import { mergeWithRefs } from "~/utils/refs";
10 |
11 | type NavigationItemOwnProps = KLink.LinkRootOptions & {
12 | class?: string;
13 | active?: boolean;
14 | icon: JSX.Element;
15 | children: JSX.Element;
16 | };
17 |
18 | export type NavigationItemProps = PolymorphicProps;
19 |
20 | export function NavigationItem(props: NavigationItemProps) {
21 | const [trigger, positioner] = createRipples({ center: true });
22 | const [local, rest] = splitProps(props, ["children", "class", "icon", "active"]);
23 |
24 | return (
25 |
26 |
27 | {local.icon}
28 |
29 | {local.children}
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/packages/core/src/components/navigation-item/styles.ts:
--------------------------------------------------------------------------------
1 | import { cva } from "class-variance-authority";
2 |
3 | // eslint-disable-next-line tailwindcss/no-custom-classname
4 | export const root = cva(
5 | [
6 | "group flex select-none flex-col items-center gap-1 px-3 pb-1 outline-none",
7 | "focus:group-focus-state-layer ui-not-disabled:hover:group-hover-state-layer ui-not-disabled:active:group-press-state-layer",
8 | ],
9 | {
10 | variants: {
11 | active: {
12 | true: ["active"],
13 | },
14 | },
15 | },
16 | );
17 |
18 | export const content = cva([
19 | "text-label-medium",
20 | "ui-group-not-in-route/link:text-on-surface-variant ui-group-in-route:text-on-surface",
21 | ]);
22 |
23 | export const icon = cva([
24 | "inline-flex aspect-[7/4] h-8 items-center justify-center rounded-full text-icon-medium state-layer ripple reset-svg",
25 | "ui-group-not-in-route:text-on-surface-variant ui-group-in-route:bg-secondary-container ui-group-in-route:text-on-secondary-container",
26 | ]);
27 |
--------------------------------------------------------------------------------
/packages/core/src/components/navigation-rail/index.tsx:
--------------------------------------------------------------------------------
1 | import type { As, PolymorphicProps } from "@kobalte/utils";
2 | import type { JSX } from "solid-js";
3 |
4 | import { Show, splitProps } from "solid-js";
5 | import { Dynamic } from "solid-js/web";
6 |
7 | import { adornment, root } from "./styles";
8 |
9 | type NavigationRailOwnProps = {
10 | class?: string;
11 | children?: JSX.Element;
12 | startAdornment?: JSX.Element;
13 | };
14 |
15 | export type NavigationRailProps = PolymorphicProps;
16 |
17 | export function NavigationRail(props: NavigationRailProps) {
18 | const [local, rest] = splitProps(props, ["as", "children", "class", "startAdornment"]);
19 |
20 | return (
21 |
22 |
23 | {local.startAdornment}
24 |
25 | {local.children}
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/packages/core/src/components/navigation-rail/styles.ts:
--------------------------------------------------------------------------------
1 | import { cva } from "class-variance-authority";
2 |
3 | export const root = cva(["flex max-w-[80px] flex-col items-center gap-3"]);
4 |
5 | export const adornment = cva(["mb-3 flex flex-col items-center gap-3"]);
6 |
--------------------------------------------------------------------------------
/packages/core/src/components/switch/index.tsx:
--------------------------------------------------------------------------------
1 | import type { As, PolymorphicProps } from "@kobalte/utils";
2 | import type { VariantProps } from "class-variance-authority";
3 |
4 | import { Switch as KSwitch } from "@kobalte/core";
5 | import { splitProps } from "solid-js";
6 |
7 | import { control, label, root, thumb } from "./styles";
8 | import { createRipples } from "~/primitives";
9 | import { mergeWithRefs } from "~/utils/refs";
10 |
11 | type SwitchOwnProps = KSwitch.SwitchRootOptions &
12 | VariantProps & {
13 | label: string;
14 | class?: string;
15 | };
16 |
17 | export type SwitchProps = PolymorphicProps;
18 |
19 | export function Switch(props: SwitchProps) {
20 | const [trigger, positioner] = createRipples({ disabled: props.isDisabled, center: true, size: 40 });
21 | const [local, rest] = splitProps(props, ["class", "label", "labelPlacement"]);
22 |
23 | return (
24 |
28 |
29 | {local.label}
30 |
31 |
32 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/packages/core/src/components/switch/styles.ts:
--------------------------------------------------------------------------------
1 | import { cva } from "class-variance-authority";
2 |
3 | export const root = cva(
4 | [
5 | "group flex items-center gap-4",
6 | "focus:group-focus-state-layer ui-not-disabled:hover:group-hover-state-layer ui-not-disabled:active:group-press-state-layer",
7 | ],
8 | {
9 | variants: {
10 | labelPlacement: {
11 | left: ["flex-row"],
12 | right: ["flex-row-reverse"],
13 | top: ["flex-col"],
14 | bottom: ["flex-col-reverse"],
15 | },
16 | },
17 | },
18 | );
19 |
20 | export const label = cva(["text-on-surface"]);
21 |
22 | export const control = cva([
23 | "inline-flex aspect-[13/8] h-8 items-center rounded-full border-2 border-outline transition duration-short4 ease-emphasized",
24 | "ui-checked:border-primary ui-checked:bg-primary ui-not-checked:bg-surface-variant",
25 | "ui-disabled:ui-checked:bg-on-surface/12 ui-disabled:ui-not-checked:border-on-surface/12 ui-disabled:ui-not-checked:bg-surface-variant/12",
26 | "ui-disabled:ui-checked:border-0",
27 | ]);
28 |
29 | export const thumb = cva([
30 | "aspect-square rounded-full transition-[transform,background-color,height] duration-short4 ease-emphasized state-layer state-layer-10",
31 | "ui-not-disabled:group-active:h-7 ui-checked:h-6 ui-checked:bg-on-primary ui-not-checked:h-4 ui-not-checked:bg-outline",
32 | "ui-checked:translate-x-[22px] ui-not-disabled:group-active:ui-checked:translate-x-5 ui-not-checked:translate-x-[6px] ui-not-disabled:group-active:ui-not-checked:translate-x-0",
33 | "ui-not-disabled:group-hover:ui-checked:bg-primary-container ui-not-disabled:group-hover:ui-not-checked:bg-on-surface-variant",
34 | "ui-not-disabled:group-focus:ui-checked:bg-primary-container ui-not-disabled:group-focus:ui-not-checked:bg-on-surface-variant",
35 | "ui-disabled:ui-checked:bg-surface ui-disabled:ui-not-checked:bg-on-surface/38",
36 | "ui-disabled:ui-checked:translate-x-6",
37 | ]);
38 |
--------------------------------------------------------------------------------
/packages/core/src/components/tabs/index.tsx:
--------------------------------------------------------------------------------
1 | import type { As, PolymorphicProps } from "@kobalte/utils";
2 | import type { VariantProps } from "class-variance-authority";
3 | import type { JSX } from "solid-js";
4 |
5 | import { Tabs as KTabs } from "@kobalte/core";
6 | import { Show, splitProps } from "solid-js";
7 |
8 | import { icon, indicator, item, line, list, root } from "./styles";
9 | import { createRipples } from "~/primitives";
10 |
11 | type TabsOwnProps = KTabs.TabsRootOptions &
12 | VariantProps & {
13 | class?: string;
14 | items: JSX.Element;
15 | children: JSX.Element;
16 | };
17 |
18 | export type TabsProps = PolymorphicProps;
19 |
20 | function Root(props: TabsProps) {
21 | const [local, rest] = splitProps(props, ["class", "items", "children", "variant", "hasIcons"]);
22 |
23 | return (
24 |
25 |
26 |
27 |
28 |
29 | {local.items}
30 |
31 | {local.children}
32 |
33 | );
34 | }
35 |
36 | type TabsItemOwnProps = KTabs.TabsTriggerOptions & {
37 | class?: JSX.Element;
38 | icon?: JSX.Element;
39 | children: JSX.Element;
40 | };
41 |
42 | export type TabsItemProps = PolymorphicProps;
43 |
44 | function Item(props: TabsItemProps) {
45 | const [local, rest] = splitProps(props, ["children", "icon", "class", "value"]);
46 | const [ref] = createRipples({});
47 |
48 | return (
49 |
50 |
51 | {local.icon}
52 |
53 | {local.children}
54 |
55 | );
56 | }
57 |
58 | export const Tabs = { Root, Content: KTabs.Content, Item };
59 |
--------------------------------------------------------------------------------
/packages/core/src/components/tabs/styles.ts:
--------------------------------------------------------------------------------
1 | import { cva } from "class-variance-authority";
2 |
3 | export const root = cva(["w-full"]);
4 |
5 | // eslint-disable-next-line tailwindcss/no-custom-classname
6 | export const list = cva(["relative flex w-full items-center border-b border-surface-variant"], {
7 | variants: {
8 | variant: {
9 | primary: ["h-12"],
10 | secondary: ["h-12"],
11 | },
12 | hasIcons: {
13 | true: [],
14 | },
15 | },
16 | compoundVariants: [
17 | {
18 | variant: "primary",
19 | hasIcons: true,
20 | class: ["!h-16"],
21 | },
22 | ],
23 | });
24 |
25 | export const indicator = cva(["absolute bottom-0 flex justify-center transition-all duration-medium4"]);
26 |
27 | export const line = cva(["w-full bg-primary"], {
28 | variants: {
29 | variant: {
30 | primary: ["h-[3px] max-w-[50px] rounded-t-[3px]"],
31 | secondary: ["h-0.5"],
32 | },
33 | },
34 | });
35 |
36 | export const item = cva([
37 | "flex h-full flex-1 flex-col items-center justify-center gap-1 text-title-small outline-none transition-colors state-layer ripple",
38 | "ui-selected:text-primary ui-not-selected:text-on-surface-variant",
39 | "focus-visible:hover-state-layer ui-not-disabled:hover:hover-state-layer ui-not-disabled:active:press-state-layer",
40 | ]);
41 |
42 | export const icon = cva(["text-icon-medium text-inherit"]);
43 |
--------------------------------------------------------------------------------
/packages/core/src/components/text-field/index.tsx:
--------------------------------------------------------------------------------
1 | import type { As, PolymorphicProps } from "@kobalte/utils";
2 | import type { VariantProps } from "class-variance-authority";
3 | import type { JSX } from "solid-js";
4 |
5 | import { TextField as KTextField } from "@kobalte/core";
6 | import { createMemo, createSignal, onCleanup, onMount, Show, splitProps } from "solid-js";
7 |
8 | import { adornment, description, field, input, label, root, wrapper } from "./styles";
9 | import { mergeWithRefs } from "~/utils/refs";
10 |
11 | type TextFieldOwnProps = KTextField.TextFieldRootOptions &
12 | VariantProps & {
13 | label: string;
14 | startAdornment?: JSX.Element;
15 | endAdornment?: JSX.Element;
16 | description?: string;
17 | class?: string;
18 | };
19 |
20 | export type TextFieldProps = PolymorphicProps;
21 |
22 | export function TextField(props: TextFieldProps) {
23 | const [ref, getRef] = createSignal();
24 | const [keepFloating, setKeepFloating] = createSignal(false);
25 |
26 | const [local, parent, rest] = splitProps(
27 | props,
28 | ["class", "label", "variant", "description", "startAdornment", "endAdornment"],
29 | [
30 | "value",
31 | "defaultValue",
32 | "onValueChange",
33 | "id",
34 | "name",
35 | "validationState",
36 | "isRequired",
37 | "isDisabled",
38 | "isReadOnly",
39 | ],
40 | );
41 |
42 | function shouldFloat(event: Event) {
43 | setKeepFloating(!!(event.target as HTMLInputElement)?.value);
44 | }
45 |
46 | onMount(() => {
47 | const el = ref();
48 |
49 | setKeepFloating(!!el?.value);
50 | el?.addEventListener("change", shouldFloat);
51 | });
52 |
53 | onCleanup(() => {
54 | const el = ref();
55 |
56 | el?.removeEventListener("change", shouldFloat);
57 | });
58 |
59 | const hasError = createMemo(() => props.validationState === "invalid");
60 |
61 | return (
62 |
63 |
64 |
65 | {local.startAdornment}
66 |
67 |
68 | {local.label}
69 |
70 |
71 |
72 | {local.endAdornment}
73 |
74 |
75 |
76 | {local.description}
77 |
78 |
79 | {local.description}
80 |
81 |
82 | );
83 | }
84 |
--------------------------------------------------------------------------------
/packages/core/src/components/text-field/styles.ts:
--------------------------------------------------------------------------------
1 | import { cva } from "class-variance-authority";
2 |
3 | // Disable this rule until https://github.com/francoismassart/eslint-plugin-tailwindcss/issues/183
4 | // eslint-disable-next-line tailwindcss/no-custom-classname
5 | export const root = cva([
6 | "group flex w-full flex-col gap-y-1",
7 | "focus-within:ui-invalid:text-error ui-not-invalid:text-on-surface-variant focus-within:ui-not-invalid:text-primary ui-not-disabled:ui-invalid:text-error ui-not-disabled:hover:ui-invalid:text-on-error-container",
8 | ]);
9 |
10 | export const wrapper = cva(["relative inline-flex w-full items-stretch overflow-hidden"], {
11 | variants: {
12 | variant: {
13 | filled: [
14 | "rounded-xs bg-surface-variant state-layer",
15 | "after:absolute after:bottom-0 after:left-1/2 after:h-0.5 after:w-0 after:bg-primary after:transition-[width,left] group-focus-within:after:left-0 group-focus-within:after:w-full",
16 | "group-focus-within:!reset-state-layer ui-group-not-disabled:group-hover:hover-state-layer",
17 | "ui-group-invalid:after:bg-error ui-group-disabled:bg-on-surface/4",
18 | ],
19 | },
20 | },
21 | });
22 |
23 | export const label = cva(
24 | [
25 | "pointer-events-none absolute left-4 select-none text-body-large text-inherit transition-[transform,top,font-size,letter-spacing,line-height,color]",
26 | "top-1/2 -translate-y-1/2 group-focus-within:top-0 group-focus-within:translate-y-1/2 group-focus-within:text-body-small",
27 | "ui-disabled:text-on-surface/38",
28 | ],
29 | {
30 | variants: {
31 | keepFloating: {
32 | true: ["!top-0 !translate-y-1/2 text-body-small text-primary"],
33 | },
34 | },
35 | },
36 | );
37 |
38 | export const field = cva(["relative w-full"]);
39 |
40 | export const input = cva([
41 | "h-14 w-full bg-transparent px-4 pt-5 pb-2 text-body-large outline-none ui-disabled:text-on-surface/38 ui-not-disabled:text-on-surface",
42 | ]);
43 |
44 | export const adornment = cva(["inline-flex items-center"], {
45 | variants: {
46 | variant: {
47 | start: ["pl-3 text-on-surface-variant"],
48 | end: ["pr-3 text-inherit"],
49 | },
50 | },
51 | });
52 |
53 | export const description = cva(["px-4 text-body-small ui-invalid:text-error ui-not-invalid:text-on-surface-variant"]);
54 |
--------------------------------------------------------------------------------
/packages/core/src/components/top-bar/index.tsx:
--------------------------------------------------------------------------------
1 | import type { As, PolymorphicProps } from "@kobalte/utils";
2 | import type { VariantProps } from "class-variance-authority";
3 | import type { JSX } from "solid-js";
4 |
5 | import { createSignal, onCleanup, onMount, Show, splitProps } from "solid-js";
6 | import { Dynamic } from "solid-js/web";
7 |
8 | import { adornment, root, title } from "./styles";
9 |
10 | type TopBarOwnProps = Omit, "elevated"> & {
11 | class?: string;
12 | children: JSX.Element;
13 | endAdornment?: JSX.Element;
14 | };
15 |
16 | export type TopBarProps = PolymorphicProps;
17 |
18 | export function TopBar(props: TopBarProps) {
19 | const [elevated, setElevated] = createSignal(false);
20 | const [local, rest] = splitProps(props, ["as", "class", "disableOnDesktop", "endAdornment", "children"]);
21 |
22 | function onScroll() {
23 | if (window.scrollY > 10) {
24 | setElevated(true);
25 | return;
26 | }
27 |
28 | setElevated(false);
29 | }
30 |
31 | onMount(() => window.addEventListener("scroll", onScroll, { passive: true }));
32 | onCleanup(() => window.removeEventListener("scroll", onScroll));
33 |
34 | return (
35 |
40 | {local.children}
41 |
42 | {local.endAdornment}
43 |
44 |
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/packages/core/src/components/top-bar/styles.ts:
--------------------------------------------------------------------------------
1 | import { cva } from "class-variance-authority";
2 |
3 | export const root = cva(["flex h-16 items-center gap-x-3 bg-surface px-4 surface"], {
4 | variants: {
5 | elevated: {
6 | true: ["surface-2"],
7 | },
8 | disableOnDesktop: {
9 | true: ["lg:!surface-0"],
10 | },
11 | },
12 | });
13 |
14 | export const title = cva(["flex flex-1 items-center text-title-large"]);
15 |
16 | export const adornment = cva(["flex items-center gap-x-3"]);
17 |
--------------------------------------------------------------------------------
/packages/core/src/index.tsx:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line import/export
2 | export * from "./components";
3 | export * from "./primitives";
4 |
--------------------------------------------------------------------------------
/packages/core/src/primitives/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./ripple";
2 |
--------------------------------------------------------------------------------
/packages/core/src/primitives/ripple/index.ts:
--------------------------------------------------------------------------------
1 | import { createSignal, onCleanup, onMount } from "solid-js";
2 |
3 | import { cancelRippleAnimation, centerElementToPointer, completedFactor, createRipple, duration } from "./utils";
4 | import { applyClasses, filterKeyboardEvent, filterMouseEvent, isKeyboardEvent, isTouchDevice } from "~/utils/dom";
5 |
6 | export type RippleOptions = {
7 | size?: number;
8 | center?: boolean;
9 | disabled?: boolean;
10 | };
11 |
12 | const self = () => document;
13 |
14 | const startEvents = ["pointerdown", "keydown"] as const;
15 | const endEvents = ["pointerout", "pointerup", "keyup"] as const;
16 |
17 | const className = "solid-material--ripple";
18 |
19 | /**
20 | * Add ripples to positioner element.
21 | *
22 | * Heavily inspired in https://github.com/asplunds/use-ripple
23 | *
24 | * @param options Ripple Options
25 | * @returns void
26 | */
27 | export function createRipples({
28 | disabled,
29 | size,
30 | center,
31 | }: RippleOptions) {
32 | const [trigger, getTrigger] = createSignal();
33 | const [positioner, getPositioner] = createSignal();
34 |
35 | const [pressing, setPressing] = createSignal(false);
36 |
37 | function onPointerDown(event: KeyboardEvent | PointerEvent) {
38 | const target = positioner() ?? trigger();
39 | const fromKeyboard = isKeyboardEvent(event);
40 |
41 | if (!target || disabled || pressing() || !filterMouseEvent(event) || !filterKeyboardEvent(event)) return;
42 | if (fromKeyboard) setPressing(true);
43 |
44 | if (window.getComputedStyle(target).position === "static") applyClasses("relative", target);
45 |
46 | requestAnimationFrame(() => {
47 | const begun = Date.now();
48 | const ripple = centerElementToPointer({
49 | event,
50 | target,
51 | center: fromKeyboard || center,
52 | element: createRipple({ name: className, size, fromKeyboard }, target, event),
53 | });
54 |
55 | const cancelRipple = () => {
56 | setPressing(false);
57 |
58 | const now = Date.now();
59 | const diff = now - begun;
60 |
61 | setTimeout(() => cancelRippleAnimation(ripple), diff > 0.4 * duration ? 0 : completedFactor * duration - diff);
62 | endEvents.forEach((item) => self().removeEventListener(item, cancelRipple));
63 | };
64 |
65 | if (!isTouchDevice()) endEvents.forEach((item) => self().addEventListener(item, cancelRipple));
66 | else setTimeout(() => cancelRippleAnimation(ripple), duration * completedFactor);
67 |
68 | target.appendChild(ripple);
69 | });
70 | }
71 |
72 | onMount(() => startEvents.forEach((event) => trigger()?.addEventListener(event, onPointerDown)));
73 | onCleanup(() => startEvents.forEach((event) => trigger()?.removeEventListener(event, onPointerDown)));
74 |
75 | return [getTrigger, getPositioner] as const;
76 | }
77 |
--------------------------------------------------------------------------------
/packages/core/src/primitives/ripple/styles.ts:
--------------------------------------------------------------------------------
1 | import { cva } from "class-variance-authority";
2 |
3 | export const ripple = cva([
4 | "pointer-events-none absolute -translate-x-1/2 -translate-y-1/2 scale-0 rounded-full bg-current opacity-30",
5 | ]);
6 |
--------------------------------------------------------------------------------
/packages/core/src/primitives/ripple/utils.ts:
--------------------------------------------------------------------------------
1 | import { ripple } from "./styles";
2 | import { applyClasses, applyStyles, getClientPosition } from "~/utils/dom";
3 |
4 | const self = () => document;
5 |
6 | export const completedFactor = 0.4;
7 | export const duration = 800;
8 | export const timingFunction = "cubic-bezier(0.2, 0, 0, 1)";
9 |
10 | type CreateRippleOptions = {
11 | name: string;
12 | size?: number;
13 | fromKeyboard?: boolean;
14 | };
15 |
16 | export function createRipple(
17 | { name, size, fromKeyboard }: CreateRippleOptions,
18 | ref: T,
19 | event: KeyboardEvent | PointerEvent,
20 | ): HTMLDivElement {
21 | const element = self().createElement("div");
22 |
23 | const { clientX, clientY } = getClientPosition(event);
24 | const { height, width, top, left } = ref.getBoundingClientRect();
25 |
26 | const maxHeight = fromKeyboard ? height / 2 : Math.max(clientY - top, height - clientY + top);
27 | const maxWidth = fromKeyboard ? width / 2 : Math.max(clientX - left, width - clientX + left);
28 |
29 | const currentSize = size ? `${size}px` : `${Math.hypot(maxHeight, maxWidth) * 2}px`;
30 |
31 | const classes = ripple({ class: name });
32 |
33 | const styles = {
34 | height: currentSize,
35 | width: currentSize,
36 | transition: `transform ${duration * 0.6}ms ${timingFunction}, opacity ${Math.max(duration * 0.05, 140)}ms ease-out`,
37 | };
38 |
39 | requestAnimationFrame(() => {
40 | applyClasses("scale-100", element);
41 | });
42 |
43 | applyStyles(styles, element);
44 | return applyClasses(classes, element);
45 | }
46 |
47 | export function cancelRippleAnimation(element: T) {
48 | const styles = {
49 | transition: `transform ${duration * 0.6}ms ${timingFunction}, opacity ${duration * 0.65}ms ease-in-out ${
50 | duration * 0.13
51 | }ms`,
52 | };
53 |
54 | applyClasses("opacity-0", element);
55 | applyStyles(styles, element);
56 |
57 | requestAnimationFrame(() => {
58 | element.addEventListener("transitionend", (e) => {
59 | if (e.propertyName === "opacity") element.remove();
60 | });
61 | });
62 | }
63 |
64 | type CenterElementToPointerOptions = {
65 | event: KeyboardEvent | PointerEvent;
66 | target: HTMLElement;
67 | element: T;
68 | center?: boolean;
69 | };
70 |
71 | export function centerElementToPointer({
72 | target,
73 | event,
74 | element,
75 | center = false,
76 | }: CenterElementToPointerOptions): T {
77 | if (center) {
78 | element.style.setProperty("top", "50%");
79 | element.style.setProperty("left", "50%");
80 |
81 | return element;
82 | }
83 |
84 | const { top, left } = target.getBoundingClientRect();
85 |
86 | element.style.setProperty("top", `${(event as PointerEvent).clientY - top}px`);
87 | element.style.setProperty("left", `${(event as PointerEvent).clientX - left}px`);
88 |
89 | return element;
90 | }
91 |
--------------------------------------------------------------------------------
/packages/core/src/utils/dom.ts:
--------------------------------------------------------------------------------
1 | export function isPointerEvent(event: any): event is PointerEvent {
2 | return "pointerType" in event;
3 | }
4 |
5 | export function isKeyboardEvent(event: any): event is KeyboardEvent {
6 | return "key" in event;
7 | }
8 |
9 | export function getClientPosition(event: KeyboardEvent | PointerEvent) {
10 | if (isPointerEvent(event)) {
11 | return { clientX: event.clientX, clientY: event.clientY };
12 | }
13 |
14 | return { clientX: 0, clientY: 0 };
15 | }
16 |
17 | export function filterKeyboardEvent(event: any, keys = ["Enter", " "]) {
18 | if (isKeyboardEvent(event) && !keys.includes(event.key)) {
19 | return null;
20 | }
21 |
22 | return event;
23 | }
24 |
25 | export function filterMouseEvent(event: any) {
26 | if (isPointerEvent(event) && event.button !== 0) {
27 | return null;
28 | }
29 |
30 | return event;
31 | }
32 |
33 | export function applyClasses(classes: string, target: T): T {
34 | if (!target) return target;
35 |
36 | classes
37 | .split(" ")
38 | .filter((item) => !target.classList.contains(item))
39 | .forEach((item) => target.classList.add(item));
40 |
41 | return target;
42 | }
43 |
44 | export function applyStyles(styles: Record, target: T): T {
45 | if (!target) return target;
46 |
47 | Object.entries(styles).forEach(([property, value]) => {
48 | target.style.setProperty(property, value);
49 | });
50 |
51 | return target;
52 | }
53 |
54 | export function isTouchDevice(): boolean {
55 | return "ontouchstart" in window || navigator.maxTouchPoints > 0 || ((navigator as any)?.msMaxTouchPoints ?? false);
56 | }
57 |
--------------------------------------------------------------------------------
/packages/core/src/utils/refs.ts:
--------------------------------------------------------------------------------
1 | type Ref = T | ((el: T) => void) | undefined;
2 |
3 | export function mergeRefs(...refs: Ref[]) {
4 | return (el: T) => {
5 | for (let ref of refs) {
6 | if (!ref) continue;
7 |
8 | if (typeof ref === "function") {
9 | ref(el);
10 | continue;
11 | }
12 |
13 | ref = el;
14 | }
15 | };
16 | }
17 |
18 | export function mergeWithRefs(ref: Ref, props: P) {
19 | return { ...props, ref: mergeRefs(ref, props?.ref) };
20 | }
21 |
--------------------------------------------------------------------------------
/packages/core/test/components/__snapshots__/button.spec.tsx.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1
2 |
3 | exports[`Button > should render 1`] = `"Save "`;
4 |
5 | exports[`Button > should render as custom element 1`] = `"Save "`;
6 |
7 | exports[`Button > should render disabled 1`] = `"Save "`;
8 |
9 | exports[`Button > should render in outlined variant 1`] = `"Save "`;
10 |
11 | exports[`Button > should render in text variant 1`] = `"Save "`;
12 |
13 | exports[`Button > should render in tonal variant 1`] = `"Save "`;
14 |
15 | exports[`Button > should render with icons 1`] = `" Save "`;
16 |
--------------------------------------------------------------------------------
/packages/core/test/components/__snapshots__/card.spec.tsx.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1
2 |
3 | exports[`Card > should render 1`] = `"Test
"`;
4 |
5 | exports[`Card > should render as custom element 1`] = `"Test "`;
6 |
7 | exports[`Card > should render with outlined variant 1`] = `"Test
"`;
8 |
--------------------------------------------------------------------------------
/packages/core/test/components/__snapshots__/divider.spec.tsx.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1
2 |
3 | exports[`Divider > should render 1`] = `" "`;
4 |
5 | exports[`Divider > should render in vertical orientation 1`] = `" "`;
6 |
--------------------------------------------------------------------------------
/packages/core/test/components/__snapshots__/fab.spec.tsx.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1
2 |
3 | exports[`FAB > should always render in medium size when it is in extended mode 1`] = `" Heart "`;
4 |
5 | exports[`FAB > should render 1`] = `" "`;
6 |
7 | exports[`FAB > should render as custom element 1`] = `"Heart "`;
8 |
9 | exports[`FAB > should render in extended mode 1`] = `" Heart "`;
10 |
11 | exports[`FAB > should render in large size 1`] = `" "`;
12 |
13 | exports[`FAB > should render in secondary variant 1`] = `" "`;
14 |
15 | exports[`FAB > should render in small size 1`] = `" "`;
16 |
17 | exports[`FAB > should render in tertiary variant 1`] = `" "`;
18 |
--------------------------------------------------------------------------------
/packages/core/test/components/__snapshots__/icon-button.spec.tsx.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1
2 |
3 | exports[`IconButton > should render 1`] = `" "`;
4 |
5 | exports[`IconButton > should render as custom element 1`] = `" "`;
6 |
7 | exports[`IconButton > should render disabled 1`] = `" "`;
8 |
9 | exports[`IconButton > should render in outlined variant 1`] = `" "`;
10 |
11 | exports[`IconButton > should render in standard variant 1`] = `" "`;
12 |
13 | exports[`IconButton > should render in tonal variant 1`] = `" "`;
14 |
--------------------------------------------------------------------------------
/packages/core/test/components/button.spec.tsx:
--------------------------------------------------------------------------------
1 | import { render } from "@solidjs/testing-library";
2 | import { AiOutlineHeart } from "solid-icons/ai";
3 | import { describe, it, expect } from "vitest";
4 |
5 | import { Button } from "~/components";
6 |
7 | describe("Button", () => {
8 | it("should render", () => {
9 | const { asFragment } = render(() => Save );
10 | expect(asFragment()).toMatchSnapshot();
11 | });
12 |
13 | it("should render in tonal variant", () => {
14 | const { asFragment } = render(() => Save );
15 | expect(asFragment()).toMatchSnapshot();
16 | });
17 |
18 | it("should render in outlined variant", () => {
19 | const { asFragment } = render(() => Save );
20 | expect(asFragment()).toMatchSnapshot();
21 | });
22 |
23 | it("should render in text variant", () => {
24 | const { asFragment } = render(() => Save );
25 | expect(asFragment()).toMatchSnapshot();
26 | });
27 |
28 | it("should render disabled", () => {
29 | const { asFragment } = render(() => Save );
30 | expect(asFragment()).toMatchSnapshot();
31 | });
32 |
33 | it("should render with icons", () => {
34 | const { asFragment } = render(() => (
35 | } endIcon={ }>
36 | Save
37 |
38 | ));
39 |
40 | expect(asFragment()).toMatchSnapshot();
41 | });
42 |
43 | it("should render as custom element", () => {
44 | const { asFragment, getByText } = render(() => Save );
45 |
46 | expect(asFragment()).toMatchSnapshot();
47 | expect(getByText(/Save/i)).toBeInstanceOf(HTMLAnchorElement);
48 | });
49 | });
50 |
--------------------------------------------------------------------------------
/packages/core/test/components/card.spec.tsx:
--------------------------------------------------------------------------------
1 | import { render } from "@solidjs/testing-library";
2 | import { describe, expect, it } from "vitest";
3 |
4 | import { Card } from "~/components";
5 |
6 | describe("Card", () => {
7 | it("should render", () => {
8 | const { asFragment } = render(() => Test );
9 | expect(asFragment()).toMatchSnapshot();
10 | });
11 |
12 | it("should render with outlined variant", () => {
13 | const { asFragment } = render(() => Test );
14 | expect(asFragment()).toMatchSnapshot();
15 | });
16 |
17 | it("should render as custom element", () => {
18 | const { asFragment, getByText } = render(() => Test );
19 |
20 | expect(asFragment()).toMatchSnapshot();
21 | expect(getByText(/Test/i)).toBeInstanceOf(HTMLButtonElement);
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/packages/core/test/components/divider.spec.tsx:
--------------------------------------------------------------------------------
1 | import { render } from "@solidjs/testing-library";
2 | import { describe, expect, it } from "vitest";
3 |
4 | import { Divider } from "~/components";
5 |
6 | describe("Divider", () => {
7 | it("should render", () => {
8 | const { asFragment } = render(() => );
9 | expect(asFragment()).toMatchSnapshot();
10 | });
11 |
12 | it("should render in vertical orientation", () => {
13 | const { asFragment } = render(() => );
14 | expect(asFragment()).toMatchSnapshot();
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/packages/core/test/components/fab.spec.tsx:
--------------------------------------------------------------------------------
1 | import { render } from "@solidjs/testing-library";
2 | import { AiOutlineHeart } from "solid-icons/ai";
3 | import { describe, expect, it } from "vitest";
4 |
5 | import { FAB } from "~/components";
6 |
7 | describe("FAB", () => {
8 | it("should render", () => {
9 | const { asFragment } = render(() => } />);
10 | expect(asFragment()).toMatchSnapshot();
11 | });
12 |
13 | it("should render in secondary variant", () => {
14 | const { asFragment } = render(() => } />);
15 | expect(asFragment()).toMatchSnapshot();
16 | });
17 |
18 | it("should render in tertiary variant", () => {
19 | const { asFragment } = render(() => } />);
20 | expect(asFragment()).toMatchSnapshot();
21 | });
22 |
23 | it("should render in large size", () => {
24 | const { asFragment } = render(() => } />);
25 | expect(asFragment()).toMatchSnapshot();
26 | });
27 |
28 | it("should render in small size", () => {
29 | const { asFragment } = render(() => } />);
30 | expect(asFragment()).toMatchSnapshot();
31 | });
32 |
33 | it("should render in extended mode", () => {
34 | const { asFragment } = render(() => }>Heart);
35 | expect(asFragment()).toMatchSnapshot();
36 | });
37 |
38 | it("should always render in medium size when it is in extended mode", () => {
39 | const { asFragment } = render(() => (
40 | }>
41 | Heart
42 |
43 | ));
44 |
45 | expect(asFragment()).toMatchSnapshot();
46 | });
47 |
48 | it("should render as custom element", () => {
49 | const { asFragment, getByText } = render(() => Heart );
50 |
51 | expect(asFragment()).toMatchSnapshot();
52 | expect(getByText(/Heart/i)).toBeInstanceOf(HTMLAnchorElement);
53 | });
54 | });
55 |
--------------------------------------------------------------------------------
/packages/core/test/components/icon-button.spec.tsx:
--------------------------------------------------------------------------------
1 | import { render } from "@solidjs/testing-library";
2 | import { AiOutlineHeart } from "solid-icons/ai";
3 | import { describe, it, expect } from "vitest";
4 |
5 | import { IconButton } from "~/components";
6 |
7 | describe("IconButton", () => {
8 | it("should render", () => {
9 | const { asFragment } = render(() => (
10 |
11 |
12 |
13 | ));
14 |
15 | expect(asFragment()).toMatchSnapshot();
16 | });
17 |
18 | it("should render in tonal variant", () => {
19 | const { asFragment } = render(() => (
20 |
21 |
22 |
23 | ));
24 |
25 | expect(asFragment()).toMatchSnapshot();
26 | });
27 |
28 | it("should render in outlined variant", () => {
29 | const { asFragment } = render(() => (
30 |
31 |
32 |
33 | ));
34 |
35 | expect(asFragment()).toMatchSnapshot();
36 | });
37 |
38 | it("should render in standard variant", () => {
39 | const { asFragment } = render(() => (
40 |
41 |
42 |
43 | ));
44 |
45 | expect(asFragment()).toMatchSnapshot();
46 | });
47 |
48 | it("should render disabled", () => {
49 | const { asFragment } = render(() => (
50 |
51 |
52 |
53 | ));
54 |
55 | expect(asFragment()).toMatchSnapshot();
56 | });
57 |
58 | it("should render as custom element", () => {
59 | const { asFragment, getByRole } = render(() => (
60 |
61 |
62 |
63 | ));
64 |
65 | expect(asFragment()).toMatchSnapshot();
66 | expect(getByRole(/button/i)).toBeInstanceOf(HTMLAnchorElement);
67 | });
68 | });
69 |
--------------------------------------------------------------------------------
/packages/core/test/setup.ts:
--------------------------------------------------------------------------------
1 | import type { TestingLibraryMatchers } from "@testing-library/jest-dom/matchers";
2 |
3 | import { cleanup } from "@solidjs/testing-library";
4 | import matchers from "@testing-library/jest-dom/matchers";
5 | import { afterEach, expect } from "vitest";
6 |
7 | declare global {
8 | namespace Vi {
9 | interface JestAssertion extends jest.Matchers, TestingLibraryMatchers {}
10 | }
11 | }
12 |
13 | expect.extend(matchers);
14 |
15 | afterEach(() => {
16 | cleanup();
17 | });
18 |
--------------------------------------------------------------------------------
/packages/core/test/utils/dom.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from "vitest";
2 |
3 | import { filterMouseEvent, getClientPosition, isKeyboardEvent, isPointerEvent } from "~/utils/dom";
4 |
5 | describe("isPointerEvent", () => {
6 | it("should return true when object has pointerType property", () => {
7 | expect(isPointerEvent({})).toBeFalsy();
8 | expect(isPointerEvent({ pointerType: "mouse" })).toBeTruthy();
9 | });
10 | });
11 |
12 | describe("isKeyEvent", () => {
13 | it("should return true when object has pointerType property", () => {
14 | expect(isKeyboardEvent({})).toBeFalsy();
15 | expect(isKeyboardEvent({ key: "Enter" })).toBeTruthy();
16 | });
17 | });
18 |
19 | describe.concurrent("getClientPosition", () => {
20 | it("should return 0 when is not pointer event", () => {
21 | expect(getClientPosition({} as any)).toStrictEqual({ clientX: 0, clientY: 0 });
22 | });
23 |
24 | it("should return clientX and clientY when is pointer event", () => {
25 | const event = { pointerType: "mouse", clientX: 10, clientY: 20 };
26 | expect(getClientPosition(event as any)).toStrictEqual({ clientX: 10, clientY: 20 });
27 | });
28 | });
29 |
30 | describe.concurrent("filterMouseEvent", () => {
31 | it("should return event when is not pointer event", () => {
32 | const event = {};
33 | expect(filterMouseEvent(event)).toStrictEqual(event);
34 | });
35 |
36 | it("should return event when is pointer event and button is equal to 0", () => {
37 | const event = { button: 0, pointerType: "mouse" };
38 | expect(filterMouseEvent(event)).toStrictEqual(event);
39 | });
40 |
41 | it("should return null when is pointer event and button is differente from 0", () => {
42 | const event = { button: 1, pointerType: "mouse" };
43 | expect(filterMouseEvent(event)).toBeNull();
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/packages/core/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@solidjs-material/configuration/tsconfig.base.json",
3 | "compilerOptions": {
4 | "lib": ["ESNext", "DOM", "DOM.Iterable"],
5 | "baseUrl": "src",
6 | "jsx": "preserve",
7 | "jsxImportSource": "solid-js",
8 | "paths": {
9 | "~/*": ["./*"]
10 | }
11 | },
12 | "include": ["src", "test", ".eslintrc.cjs", "tsup.config.ts", "vite.config.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/packages/core/tsup.config.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-extraneous-dependencies */
2 | import { defineConfig } from "tsup-preset-solid";
3 |
4 | export default defineConfig({ entry: "src/index.tsx" }, { cjs: true, writePackageJson: false });
5 |
--------------------------------------------------------------------------------
/packages/core/vite.config.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-extraneous-dependencies */
2 | import solid from "vite-plugin-solid";
3 | import { defineConfig } from "vitest/config";
4 |
5 | export default defineConfig({
6 | plugins: [solid()],
7 | resolve: {
8 | alias: {
9 | "~": "./src",
10 | },
11 | },
12 | test: {
13 | environment: "happy-dom",
14 | },
15 | });
16 |
--------------------------------------------------------------------------------
/packages/tailwind/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-extraneous-dependencies */
2 | const { resolve } = require("path");
3 |
4 | require("@rushstack/eslint-patch/modern-module-resolution");
5 |
6 | module.exports = {
7 | extends: [require.resolve("@solidjs-material/configuration/eslint.base.cjs")],
8 | parserOptions: { project: resolve(__dirname, "tsconfig.json") },
9 | };
10 |
--------------------------------------------------------------------------------
/packages/tailwind/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @solidjs-material/tailwind
2 |
3 | ## 0.0.8
4 |
5 | ### Patch Changes
6 |
7 | - 423f693: Fix CI
8 |
9 | ## 0.0.7
10 |
11 | ### Patch Changes
12 |
13 | - 986c180: Fix lint script
14 |
15 | ## 0.0.6
16 |
17 | ### Patch Changes
18 |
19 | - Add tabs component
20 | Add tests
21 | Bump kobalte version
22 | Fix components interactions
23 |
24 | ## 0.0.5
25 |
26 | ### Patch Changes
27 |
28 | - a99edda: Add checkbox component
29 |
30 | ## 0.0.4
31 |
32 | ### Patch Changes
33 |
34 | - c00fd89: Bump depedencies
35 |
36 | ## 0.0.3
37 |
38 | ### Patch Changes
39 |
40 | - fix text field styles
41 |
42 | ## 0.0.2
43 |
44 | ### Patch Changes
45 |
46 | - fix changeset configurations
47 |
48 | ## 0.0.1
49 |
50 | ### Patch Changes
51 |
52 | - 029b1d1: Base release
53 |
--------------------------------------------------------------------------------
/packages/tailwind/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@solidjs-material/tailwind",
3 | "version": "0.0.8",
4 | "description": "Solid Material tailwind preset",
5 | "keywords": [
6 | "material you",
7 | "material design",
8 | "solid-js",
9 | "tailwindcss"
10 | ],
11 | "license": "MIT",
12 | "author": {
13 | "name": "Carlos Eduardo de Oliveira Paludetto",
14 | "email": "ceo.paludetto@gmail.com"
15 | },
16 | "type": "module",
17 | "exports": {
18 | "import": {
19 | "types": "./dist/index.d.ts",
20 | "default": "./dist/index.js"
21 | },
22 | "require": "./dist/index.cjs"
23 | },
24 | "main": "./dist/index.cjs",
25 | "module": "./dist/index.js",
26 | "types": "./dist/index.d.ts",
27 | "files": [
28 | "./dist"
29 | ],
30 | "scripts": {
31 | "build": "tsup",
32 | "dev": "tsup --watch",
33 | "lint": "eslint \"src/**/*.ts\"",
34 | "lint:fix": "eslint \"src/**/*.ts\" --fix"
35 | },
36 | "dependencies": {
37 | "@kobalte/tailwindcss": "^0.4.2",
38 | "@material/material-color-utilities": "^0.2.0",
39 | "kebab-case": "^1.0.2",
40 | "tailwindcss": "^3.2.7"
41 | },
42 | "devDependencies": {
43 | "@rushstack/eslint-patch": "^1.2.0",
44 | "@solidjs-material/configuration": "workspace:^0.0.7",
45 | "eslint": "^8.34.0",
46 | "tsup": "^6.6.3",
47 | "type-fest": "^3.6.0"
48 | },
49 | "peerDependencies": {
50 | "tailwindcss": "^3.2.0"
51 | },
52 | "publishConfig": {
53 | "access": "public"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/packages/tailwind/src/index.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | import kobateTailwind from "@kobalte/tailwindcss";
4 | import plugin from "tailwindcss/plugin";
5 |
6 | import {
7 | createCSSVariablesFromScheme,
8 | createDurationTokens,
9 | createPaletteFromColor,
10 | createShapeTokens,
11 | createTailwindColorsFromScheme,
12 | createTransitionTokens,
13 | createTypographyTokens,
14 | } from "./tokens";
15 | import { addDefault } from "~/utils";
16 |
17 | export type DesignSystemOptions = {
18 | baseColor: string;
19 | addSelectionStyles?: boolean;
20 | };
21 |
22 | export function designSystem({ baseColor, addSelectionStyles = true }: DesignSystemOptions): Partial {
23 | const schemes = createPaletteFromColor(baseColor);
24 |
25 | return {
26 | theme: {
27 | surface: {
28 | 0: 0,
29 | 1: "0.05",
30 | 2: "0.08",
31 | 3: "0.11",
32 | 4: "0.12",
33 | 5: "0.14",
34 | },
35 | borderRadius: addDefault(createShapeTokens(), "xs"),
36 | fontSize: createTypographyTokens(),
37 | extend: {
38 | colors: createTailwindColorsFromScheme(schemes.light),
39 | transitionTimingFunction: addDefault(createTransitionTokens(), "standard"),
40 | transitionDuration: addDefault(createDurationTokens(), "short3"),
41 | opacity: {
42 | 4: ".04",
43 | 8: ".08",
44 | 11: ".11",
45 | 12: ".12",
46 | 14: ".14",
47 | 38: ".38",
48 | },
49 | scale: {
50 | 200: "2",
51 | },
52 | },
53 | },
54 | plugins: [
55 | kobateTailwind,
56 | plugin(({ addBase, addUtilities, addVariant, matchUtilities, theme }) => {
57 | addBase({
58 | ":root": createCSSVariablesFromScheme(schemes.light),
59 | "@media(prefers-color-scheme: dark)": {
60 | ":root": createCSSVariablesFromScheme(schemes.dark),
61 | },
62 | html: {
63 | "-webkit-tap-highlight-color": "transparent",
64 | },
65 | body: {
66 | backgroundColor: "rgb(var(--surface) / 1)",
67 | color: "rgb(var(--on-surface) / 1)",
68 | },
69 | ...(addSelectionStyles
70 | ? {
71 | "*::selection": {
72 | backgroundColor: "rgb(var(--tertiary) / 1)",
73 | color: "rgb(var(--on-tertiary) / 1)",
74 | },
75 | }
76 | : {}),
77 | });
78 |
79 | addUtilities({
80 | ".surface": {
81 | "&::before": {
82 | content: "''",
83 | display: "block",
84 | position: "absolute",
85 | left: "0",
86 | top: "0",
87 | height: "100%",
88 | width: "100%",
89 | pointerEvents: "none",
90 | background: `rgb(var(--primary) / var(--tw-surface-opacity, 0))`,
91 | transitionProperty: "background-color",
92 | transitionDuration: theme("transitionDuration.short3"),
93 | transitionTimingFunction: theme("transitionTimingFunction.standard"),
94 | },
95 | },
96 | ".state-layer": {
97 | "&::before": {
98 | content: '""',
99 | display: "block",
100 | position: "absolute",
101 | left: "50%",
102 | top: "50%",
103 | backgroundColor: theme("colors.current"),
104 | borderRadius: "var(--tw-state-layer-radius, 9999px)",
105 | height: "var(--tw-state-layer-size, 150%)",
106 | width: "var(--tw-state-layer-size, 150%)",
107 | opacity: "var(--tw-state-layer-opacity, 0)",
108 | pointerEvents: "none",
109 | transform: "translateX(-50%) translateY(-50%)",
110 | transitionProperty: "opacity, background-color",
111 | transitionDuration: theme("transitionDuration.short3"),
112 | transitionTimingFunction: theme("transitionTimingFunction.standard"),
113 | },
114 | },
115 | ".hover-state-layer": {
116 | "--tw-state-layer-opacity": theme("opacity.8"),
117 | },
118 | ".focus-state-layer": {
119 | "--tw-state-layer-opacity": theme("opacity.10"),
120 | },
121 | ".press-state-layer": {
122 | "--tw-state-layer-opacity": theme("opacity.12"),
123 | },
124 | ".reset-state-layer": {
125 | "--tw-state-layer-opacity": theme("opacity.0"),
126 | },
127 | ".group-hover-state-layer": {
128 | ".state-layer": {
129 | "--tw-state-layer-opacity": theme("opacity.8"),
130 | },
131 | },
132 | ".group-focus-state-layer": {
133 | ".state-layer": {
134 | "--tw-state-layer-opacity": theme("opacity.10"),
135 | },
136 | },
137 | ".group-press-state-layer": {
138 | ".state-layer": {
139 | "--tw-state-layer-opacity": theme("opacity.12"),
140 | },
141 | },
142 | ".reset-svg": {
143 | "> svg": {
144 | width: "1em",
145 | height: "1em",
146 | },
147 | },
148 | ".ripple": {
149 | overflow: "hidden",
150 | position: "relative",
151 | isolation: "isolate",
152 | },
153 | });
154 |
155 | matchUtilities(
156 | { surface: (value) => ({ "--tw-surface-opacity": value }) },
157 | { type: "length", values: theme("surface") },
158 | );
159 |
160 | matchUtilities(
161 | { "state-layer": (value) => ({ "--tw-state-layer-size": value }) },
162 | { type: ["absolute-size", "relative-size", "length", "percentage"], values: theme("spacing") },
163 | );
164 |
165 | const variants = [
166 | ["in-route", ".active"],
167 | ["disabled", "[data-disabled]"],
168 | ];
169 |
170 | variants.forEach(([name, variant]) => {
171 | addVariant(`ui-${name}`, `&${variant}`);
172 | addVariant(`ui-not-${name}`, `&:not(${variant})`);
173 |
174 | addVariant(`ui-group-${name}`, `:merge(.group)${variant} &`);
175 | addVariant(`ui-group-not-${name}`, `:merge(.group):not(${variant}) &`);
176 |
177 | addVariant(`ui-peer-${name}`, `:merge(.peer)${variant} ~ &`);
178 | addVariant(`ui-peer-not-${name}`, `:merge(.peer):not(${variant}) ~ &`);
179 | });
180 | }),
181 | ],
182 | };
183 | }
184 |
185 | export * from "./tokens";
186 |
--------------------------------------------------------------------------------
/packages/tailwind/src/tokens/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./palette";
2 | export * from "./shape";
3 | export * from "./transition";
4 | export * from "./typography";
5 |
--------------------------------------------------------------------------------
/packages/tailwind/src/tokens/palette.ts:
--------------------------------------------------------------------------------
1 | import type { Scheme } from "@material/material-color-utilities";
2 | import type { KebabCase } from "type-fest";
3 |
4 | import {
5 | argbFromHex,
6 | blueFromArgb,
7 | greenFromArgb,
8 | redFromArgb,
9 | themeFromSourceColor,
10 | } from "@material/material-color-utilities";
11 |
12 | import { getTypedEntries, getTypedKeys, kebabCase } from "~/utils";
13 |
14 | export type ColorVariants = KebabCase>;
15 |
16 | function toRGB(value: number) {
17 | return [redFromArgb(value), greenFromArgb(value), blueFromArgb(value)].join(" ");
18 | }
19 |
20 | export function createPaletteFromColor(color: string) {
21 | const { schemes } = themeFromSourceColor(argbFromHex(color));
22 | return schemes;
23 | }
24 |
25 | export function createTailwindColorsFromScheme(scheme: Scheme) {
26 | return getTypedKeys(scheme.toJSON()).reduce((acc, color) => {
27 | const name = kebabCase(color);
28 | acc[name] = `rgb(var(--${name}) / )`;
29 |
30 | return acc;
31 | }, {} as Record);
32 | }
33 |
34 | export function createCSSVariablesFromScheme(scheme: Scheme) {
35 | return getTypedEntries(scheme.toJSON()).reduce((acc, [color, value]) => {
36 | const name = kebabCase(color);
37 | acc[`--${name}`] = toRGB(value);
38 |
39 | return acc;
40 | }, {} as Record<`--${ColorVariants}`, string>);
41 | }
42 |
--------------------------------------------------------------------------------
/packages/tailwind/src/tokens/shape.ts:
--------------------------------------------------------------------------------
1 | import { getTypedEntries } from "~/utils";
2 |
3 | export const shapeTokens = {
4 | full: 9999,
5 | lg: 16,
6 | md: 12,
7 | none: 0,
8 | sm: 8,
9 | xl: 28,
10 | xs: 4,
11 | xxs: 2,
12 | };
13 |
14 | export type ShapeVariants = keyof typeof shapeTokens;
15 |
16 | type ShapeTokens = Record;
17 |
18 | export function createShapeTokens() {
19 | return getTypedEntries(shapeTokens).reduce((acc, [key, value]) => {
20 | acc[key] = `${value}px`;
21 | return acc;
22 | }, {} as ShapeTokens);
23 | }
24 |
--------------------------------------------------------------------------------
/packages/tailwind/src/tokens/transition.ts:
--------------------------------------------------------------------------------
1 | import { getTypedEntries } from "~/utils";
2 |
3 | export const transitionTokens = {
4 | emphasized: [0.2, 0, 0, 1],
5 | "emphasized-accelerate": [0.3, 0, 0.8, 0.15],
6 | "emphasized-decelerate": [0.05, 0.7, 0.1, 1],
7 | linear: [0, 0, 1, 1],
8 | standard: [0.2, 0, 0, 1],
9 | "standard-accelerate": [0.3, 0, 1, 1],
10 | "standard-decelerate": [0, 0, 0, 1],
11 | };
12 |
13 | export type TransitionVariants = keyof typeof transitionTokens;
14 |
15 | type TransitionTokens = Record;
16 |
17 | export function createTransitionTokens() {
18 | return getTypedEntries(transitionTokens).reduce((acc, [name, value]) => {
19 | acc[name] = `cubic-bezier(${value.join(",")})`;
20 | return acc;
21 | }, {} as TransitionTokens);
22 | }
23 |
24 | export const durationTokens = {
25 | "extra-long1": 700,
26 | "extra-long2": 800,
27 | "extra-long3": 900,
28 | "extra-long4": 1000,
29 | long1: 450,
30 | long2: 500,
31 | long3: 550,
32 | long4: 600,
33 | medium1: 250,
34 | medium2: 300,
35 | medium3: 350,
36 | medium4: 400,
37 | short1: 50,
38 | short2: 100,
39 | short3: 150,
40 | short4: 200,
41 | };
42 |
43 | export type DurationVariants = keyof typeof durationTokens;
44 |
45 | type DurationTokens = Record;
46 |
47 | export function createDurationTokens() {
48 | return getTypedEntries(durationTokens).reduce((acc, [name, value]) => {
49 | acc[name] = `${value}ms`;
50 | return acc;
51 | }, {} as DurationTokens);
52 | }
53 |
--------------------------------------------------------------------------------
/packages/tailwind/src/tokens/typography.ts:
--------------------------------------------------------------------------------
1 | import { getTypedEntries } from "~/utils";
2 |
3 | export const typographyTokens = {
4 | "body-large": { lineHeight: 24, size: 16, tracking: 0.5, weight: 400 },
5 | "body-medium": { lineHeight: 20, size: 14, tracking: 0.25, weight: 400 },
6 | "body-small": { lineHeight: 16, size: 12, tracking: 0.4, weight: 400 },
7 | "display-large": { lineHeight: 64, size: 57, tracking: 0, weight: 400 },
8 | "display-medium": { lineHeight: 52, size: 45, tracking: 0, weight: 400 },
9 | "display-small": { lineHeight: 44, size: 36, tracking: 0, weight: 400 },
10 | "headline-large": { lineHeight: 40, size: 32, tracking: 0, weight: 400 },
11 | "headline-medium": { lineHeight: 36, size: 28, tracking: 0, weight: 400 },
12 | "headline-small": { lineHeight: 32, size: 24, tracking: 0, weight: 400 },
13 | "label-large": { lineHeight: 20, size: 14, tracking: 0.1, weight: 500 },
14 | "label-medium": { lineHeight: 16, size: 12, tracking: 0.5, weight: 500 },
15 | "label-small": { lineHeight: 16, size: 11, tracking: 0.5, weight: 500 },
16 | "title-large": { lineHeight: 28, size: 22, tracking: 0, weight: 400 },
17 | "title-medium": { lineHeight: 24, size: 16, tracking: 0.15, weight: 500 },
18 | "title-small": { lineHeight: 20, size: 14, tracking: 0.1, weight: 500 },
19 |
20 | "icon-small": { lineHeight: 16, size: 18, tracking: 0, weight: 400 },
21 | "icon-medium": { lineHeight: 16, size: 24, tracking: 0, weight: 400 },
22 | "icon-large": { lineHeight: 16, size: 36, tracking: 0, weight: 400 },
23 | };
24 |
25 | export type TypographyVariants = keyof typeof typographyTokens;
26 |
27 | type TailwindTypographyToken = [string, { fontWeight: string; letterSpacing: string; lineHeight: string }];
28 |
29 | function toRem(value: number) {
30 | return `${value / 16}rem`;
31 | }
32 |
33 | export function createTypographyTokens() {
34 | return getTypedEntries(typographyTokens).reduce((acc, [name, value]) => {
35 | const { size, weight, lineHeight, tracking } = value;
36 | const definitions = {
37 | fontWeight: weight.toString(),
38 | letterSpacing: toRem(tracking),
39 | lineHeight: toRem(lineHeight),
40 | };
41 |
42 | acc[name] = [toRem(size), definitions];
43 | return acc;
44 | }, {} as Record);
45 | }
46 |
--------------------------------------------------------------------------------
/packages/tailwind/src/typescript/kobalte.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/consistent-type-imports */
2 |
3 | declare module "@kobalte/tailwindcss" {
4 | const plugin: { handler: import("tailwindcss/types/config").PluginCreator };
5 | export default plugin;
6 | }
7 |
--------------------------------------------------------------------------------
/packages/tailwind/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./object";
2 | export * from "./string";
3 |
--------------------------------------------------------------------------------
/packages/tailwind/src/utils/object.ts:
--------------------------------------------------------------------------------
1 | import type { Entries } from "type-fest";
2 |
3 | export function getTypedEntries(value: T) {
4 | return Object.entries(value) as Entries;
5 | }
6 |
7 | export function getTypedKeys(value: T) {
8 | return Object.keys(value) as (keyof T)[];
9 | }
10 |
11 | export function addDefault(value: T, item: keyof T) {
12 | return { ...value, DEFAULT: value[item] };
13 | }
14 |
--------------------------------------------------------------------------------
/packages/tailwind/src/utils/string.ts:
--------------------------------------------------------------------------------
1 | import type { KebabCase } from "type-fest";
2 |
3 | import kebab from "kebab-case";
4 |
5 | export function kebabCase(value: T) {
6 | return kebab(value) as KebabCase;
7 | }
8 |
--------------------------------------------------------------------------------
/packages/tailwind/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@solidjs-material/configuration/tsconfig.base.json",
3 | "compilerOptions": {
4 | "lib": ["ESNext", "DOM"],
5 | "baseUrl": "src",
6 | "paths": {
7 | "~/*": ["./*"]
8 | }
9 | },
10 | "include": ["src", ".eslintrc.cjs", "tsup.config.ts"]
11 | }
12 |
--------------------------------------------------------------------------------
/packages/tailwind/tsup.config.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-extraneous-dependencies */
2 | import { defineConfig } from "tsup";
3 |
4 | export default defineConfig({
5 | entry: ["src/index.ts"],
6 | format: ["esm", "cjs"],
7 | noExternal: ["@material/material-color-utilities"],
8 | dts: true,
9 | sourcemap: true,
10 | clean: true,
11 | treeshake: true,
12 | });
13 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - "packages/*"
3 | - "apps/*"
4 |
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turbo.build/schema.json",
3 | "pipeline": {
4 | "dev": {
5 | "dependsOn": ["^dev"]
6 | },
7 | "build": {
8 | "dependsOn": ["^build"],
9 | "outputs": ["dist/**"]
10 | },
11 | "test": {},
12 | "lint": {},
13 | "lint:fix": {}
14 | }
15 | }
16 |
--------------------------------------------------------------------------------