├── .github
├── FUNDING.yml
└── workflows
│ └── test.yml
├── .gitignore
├── .prettierrc
├── LICENCE
├── README.md
├── e2e
├── redraw_example
│ ├── .gitignore
│ ├── .prettierrc
│ ├── .yarnrc.yml
│ ├── README.md
│ ├── gleam.toml
│ ├── index.html
│ ├── manifest.toml
│ ├── package.json
│ ├── src
│ │ ├── components.gleam
│ │ ├── redraw_example.gleam
│ │ └── redraw_example.main.mjs
│ ├── vite.config.js
│ └── yarn.lock
├── server_components
│ ├── .gitignore
│ ├── README.md
│ ├── gleam.toml
│ ├── manifest.toml
│ ├── src
│ │ └── server_components.gleam
│ └── test
│ │ └── server_components_test.gleam
├── shared_styles
│ ├── .github
│ │ └── workflows
│ │ │ └── test.yml
│ ├── .gitignore
│ ├── README.md
│ ├── gleam.toml
│ ├── manifest.toml
│ ├── src
│ │ └── shared_styles.gleam
│ └── test
│ │ └── shared_styles_test.gleam
├── shared_view
│ ├── README.md
│ ├── gleam.toml
│ ├── manifest.toml
│ ├── src
│ │ ├── components.gleam
│ │ ├── shared_view.gleam
│ │ └── styles.gleam
│ └── test
│ │ └── shared_view_test.gleam
├── ssr
│ ├── .gitignore
│ ├── README.md
│ ├── gleam.toml
│ ├── manifest.toml
│ ├── src
│ │ └── ssr.gleam
│ └── test
│ │ └── ssr_test.gleam
└── web_application
│ ├── .gitignore
│ ├── README.md
│ ├── gleam.toml
│ ├── index.html
│ ├── manifest.toml
│ └── src
│ └── web_application.gleam
├── landing_page
├── .yarnrc.yml
├── README.md
├── gleam.toml
├── index.html
├── manifest.toml
├── package.json
├── public
│ ├── FiraCode.ttf
│ ├── Lexend.ttf
│ ├── atom.css
│ ├── base.css
│ ├── logo.png
│ └── reset.css
├── src
│ ├── components
│ │ ├── button.gleam
│ │ ├── copy_button.gleam
│ │ ├── footer.gleam
│ │ ├── navbar.gleam
│ │ └── windows.gleam
│ ├── ffi.gleam
│ ├── icons.gleam
│ ├── icons
│ │ ├── book_open.gleam
│ │ ├── check.gleam
│ │ ├── copy.gleam
│ │ ├── github.gleam
│ │ └── home.gleam
│ ├── landing_page.ffi.mjs
│ ├── landing_page.gleam
│ ├── landing_page.main.mjs
│ ├── layout.gleam
│ └── texts.gleam
├── test
│ └── landing_page_test.gleam
├── vite.config.js
└── yarn.lock
├── sketch
├── CHANGELOG.md
├── LICENCE
├── README.md
├── birdie_snapshots
│ ├── erlang_aliased_css.accepted
│ ├── erlang_css_test.accepted
│ ├── erlang_dimensions_css.accepted
│ ├── erlang_edges_css.accepted
│ ├── erlang_exposed_css.accepted
│ ├── erlang_exposed_property.accepted
│ ├── erlang_font_face_css.accepted
│ ├── erlang_function_css.accepted
│ ├── erlang_function_property.accepted
│ ├── erlang_important_css.accepted
│ ├── erlang_keyframe_class.accepted
│ ├── erlang_keyframe_css.accepted
│ ├── erlang_medias_and.accepted
│ ├── erlang_medias_and_or.accepted
│ ├── erlang_medias_or.accepted
│ ├── erlang_medias_pseudo_class.accepted
│ ├── erlang_medias_simple.accepted
│ ├── erlang_nestings_css.accepted
│ ├── erlang_nestings_example.accepted
│ ├── erlang_variable_css.accepted
│ ├── js_aliased_css.accepted
│ ├── js_css_test.accepted
│ ├── js_dimensions_css.accepted
│ ├── js_edges_css.accepted
│ ├── js_exposed_css.accepted
│ ├── js_exposed_property.accepted
│ ├── js_font_face_css.accepted
│ ├── js_function_css.accepted
│ ├── js_function_property.accepted
│ ├── js_important_css.accepted
│ ├── js_keyframe_class.accepted
│ ├── js_keyframe_css.accepted
│ ├── js_medias_and.accepted
│ ├── js_medias_and_or.accepted
│ ├── js_medias_or.accepted
│ ├── js_medias_pseudo_class.accepted
│ ├── js_medias_simple.accepted
│ ├── js_nestings_css.accepted
│ ├── js_nestings_example.accepted
│ ├── js_variable_css.accepted
│ ├── run_globals_blue.accepted
│ └── run_globals_red.accepted
├── gleam.toml
├── manifest.toml
├── src
│ ├── sketch.ffi.mjs
│ ├── sketch.gleam
│ └── sketch
│ │ ├── css.gleam
│ │ ├── css
│ │ ├── angle.gleam
│ │ ├── font_face.gleam
│ │ ├── keyframe.gleam
│ │ ├── length.gleam
│ │ ├── media.gleam
│ │ ├── svg.gleam
│ │ └── transform.gleam
│ │ ├── error.gleam
│ │ └── internals
│ │ ├── cache
│ │ ├── actor.gleam
│ │ └── cache.gleam
│ │ └── string.gleam
└── test
│ ├── classes
│ ├── dimensions_css.gleam
│ ├── edges_css.gleam
│ ├── font_face_css.gleam
│ ├── globals_css.gleam
│ ├── important_css.gleam
│ ├── keyframe_css.gleam
│ ├── medias_css.gleam
│ └── nestings_css.gleam
│ ├── sketch_test.gleam
│ └── sketch_test
│ ├── class.gleam
│ ├── dimensions
│ ├── angle.gleam
│ └── length.gleam
│ ├── helpers.gleam
│ ├── properties
│ └── transform.gleam
│ └── rules
│ ├── font_face.gleam
│ └── media.gleam
├── sketch_css
├── .gitignore
├── CHANGELOG.md
├── LICENCE
├── README.md
├── birdie_snapshots
│ ├── erlang_css_aliased_css_gleam.accepted
│ ├── erlang_css_dimensions_css_gleam.accepted
│ ├── erlang_css_edges_css_gleam.accepted
│ ├── erlang_css_exposed_css_gleam.accepted
│ ├── erlang_css_font_face_css_gleam.accepted
│ ├── erlang_css_function_css_gleam.accepted
│ ├── erlang_css_important_css_gleam.accepted
│ ├── erlang_css_keyframe_css_gleam.accepted
│ ├── erlang_css_medias_css_gleam.accepted
│ ├── erlang_css_nestings_css_gleam.accepted
│ ├── erlang_css_variable_css_gleam.accepted
│ ├── erlang_gleam_aliased_css_gleam.accepted
│ ├── erlang_gleam_dimensions_css_gleam.accepted
│ ├── erlang_gleam_edges_css_gleam.accepted
│ ├── erlang_gleam_exposed_css_gleam.accepted
│ ├── erlang_gleam_font_face_css_gleam.accepted
│ ├── erlang_gleam_function_css_gleam.accepted
│ ├── erlang_gleam_important_css_gleam.accepted
│ ├── erlang_gleam_keyframe_css_gleam.accepted
│ ├── erlang_gleam_medias_css_gleam.accepted
│ ├── erlang_gleam_nestings_css_gleam.accepted
│ ├── erlang_gleam_variable_css_gleam.accepted
│ ├── js_css_aliased_css_gleam.accepted
│ ├── js_css_dimensions_css_gleam.accepted
│ ├── js_css_edges_css_gleam.accepted
│ ├── js_css_exposed_css_gleam.accepted
│ ├── js_css_font_face_css_gleam.accepted
│ ├── js_css_function_css_gleam.accepted
│ ├── js_css_important_css_gleam.accepted
│ ├── js_css_keyframe_css_gleam.accepted
│ ├── js_css_medias_css_gleam.accepted
│ ├── js_css_nestings_css_gleam.accepted
│ ├── js_css_variable_css_gleam.accepted
│ ├── js_gleam_aliased_css_gleam.accepted
│ ├── js_gleam_dimensions_css_gleam.accepted
│ ├── js_gleam_edges_css_gleam.accepted
│ ├── js_gleam_exposed_css_gleam.accepted
│ ├── js_gleam_font_face_css_gleam.accepted
│ ├── js_gleam_function_css_gleam.accepted
│ ├── js_gleam_important_css_gleam.accepted
│ ├── js_gleam_keyframe_css_gleam.accepted
│ ├── js_gleam_medias_css_gleam.accepted
│ ├── js_gleam_nestings_css_gleam.accepted
│ └── js_gleam_variable_css_gleam.accepted
├── gleam.toml
├── manifest.toml
├── src
│ ├── sketch_css.gleam
│ └── sketch_css
│ │ ├── commands
│ │ └── generate.gleam
│ │ ├── constants.gleam
│ │ ├── fs.gleam
│ │ ├── generate.gleam
│ │ ├── module.gleam
│ │ ├── module
│ │ ├── dependencies.gleam
│ │ ├── exposings.gleam
│ │ ├── functions.gleam
│ │ ├── imports.gleam
│ │ ├── pipes.gleam
│ │ └── stylesheet.gleam
│ │ ├── path.gleam
│ │ ├── uniconfig.gleam
│ │ └── utils.gleam
└── test
│ ├── sketch_css_test.gleam
│ └── sketch_css_test
│ ├── classes.gleam
│ ├── classes
│ ├── aliased_css.gleam
│ ├── dimensions_css.gleam
│ ├── edges_css.gleam
│ ├── exposed_css.gleam
│ ├── font_face_css.gleam
│ ├── function_css.gleam
│ ├── important_css.gleam
│ ├── keyframe_css.gleam
│ ├── medias_css.gleam
│ ├── nestings_css.gleam
│ └── variable_css.gleam
│ ├── constants.gleam
│ └── helpers.gleam
├── sketch_lustre
├── .gitignore
├── CHANGELOG.md
├── README.md
├── gleam.toml
├── manifest.toml
├── src
│ └── sketch
│ │ ├── lustre.gleam
│ │ └── lustre
│ │ ├── element.gleam
│ │ ├── element
│ │ ├── html.gleam
│ │ └── keyed.gleam
│ │ └── internals
│ │ ├── css-stylesheet.ffi.mjs
│ │ ├── css_stylesheet.gleam
│ │ ├── global.ffi.mjs
│ │ ├── global.gleam
│ │ └── sketch_global_ffi.erl
└── test
│ └── sketch_lustre_test.gleam
└── sketch_redraw
├── CHANGELOG.md
├── README.md
├── gleam.toml
├── manifest.toml
├── src
├── mutable.ffi.mjs
├── redraw.ffi.mjs
└── sketch
│ ├── redraw.gleam
│ └── redraw
│ ├── dom
│ └── html.gleam
│ └── internals
│ ├── mutable.gleam
│ └── object.gleam
└── test
└── sketch_redraw_test.gleam
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [ghivert]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
12 | polar: # Replace with a single Polar username
13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
14 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
15 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: test
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | - main
8 | pull_request:
9 |
10 | jobs:
11 | test-sketch:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v3
15 | - uses: erlef/setup-beam@v1
16 | with:
17 | otp-version: '27.0.1'
18 | gleam-version: '1.10.0'
19 | rebar3-version: '3'
20 | # elixir-version: "1.15.4"
21 | - run: gleam deps download
22 | working-directory: sketch
23 | - run: gleam test --target=erlang
24 | working-directory: sketch
25 | - run: gleam test --target=javascript
26 | working-directory: sketch
27 | - run: gleam format --check src test
28 | working-directory: sketch
29 |
30 | test-sketch-css:
31 | runs-on: ubuntu-latest
32 | steps:
33 | - uses: actions/checkout@v3
34 | - uses: erlef/setup-beam@v1
35 | with:
36 | otp-version: '27.0.1'
37 | gleam-version: '1.10.0'
38 | rebar3-version: '3'
39 | # elixir-version: "1.15.4"
40 | - run: gleam deps download
41 | working-directory: sketch_css
42 | - run: gleam format --check src test
43 | working-directory: sketch_css
44 | - run: gleam test --target=erlang
45 | working-directory: sketch_css
46 | - run: gleam test --target=javascript
47 | working-directory: sketch_css
48 |
49 | test-sketch-lustre:
50 | runs-on: ubuntu-latest
51 | steps:
52 | - uses: actions/checkout@v3
53 | - uses: erlef/setup-beam@v1
54 | with:
55 | otp-version: '27.0.1'
56 | gleam-version: '1.10.0'
57 | rebar3-version: '3'
58 | # elixir-version: "1.15.4"
59 | - run: gleam deps download
60 | working-directory: sketch_lustre
61 | - run: gleam test --target=erlang
62 | working-directory: sketch_lustre
63 | - run: gleam test --target=javascript
64 | working-directory: sketch_lustre
65 | - run: gleam format --check src test
66 | working-directory: sketch_lustre
67 |
68 | test-sketch-redraw:
69 | runs-on: ubuntu-latest
70 | steps:
71 | - uses: actions/checkout@v3
72 | - uses: erlef/setup-beam@v1
73 | with:
74 | otp-version: '27.0.1'
75 | gleam-version: '1.10.0'
76 | rebar3-version: '3'
77 | # elixir-version: "1.15.4"
78 | - run: gleam deps download
79 | working-directory: sketch_redraw
80 | - run: gleam test --target=javascript
81 | working-directory: sketch_redraw
82 | - run: gleam format --check src test
83 | working-directory: sketch_redraw
84 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.beam
2 | *.ez
3 | build/
4 | erl_crash.dump
5 | .DS_Store
6 | Thumbs.db
7 | src/sketch/styles
8 | styles/
9 | node_modules/
10 | .yarn
11 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "printWidth": 80,
4 | "singleQuote": true,
5 | "proseWrap": "always"
6 | }
7 |
--------------------------------------------------------------------------------
/LICENCE:
--------------------------------------------------------------------------------
1 | Copyright 2024-2025 Guillaume Hivert
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Sketch
2 |
3 | Monorepo for the Sketch, CSS-in-Gleam project. Sketch is both a specification
4 | and multiple runtimes to get CSS in Gleam correctly executed.
5 |
6 | ## Core package
7 |
8 | - [`sketch`](https://hexdocs.pm/sketch)
9 |
10 | ## Current integrations
11 |
12 | - [`sketch_css`](https://hexdocs.pm/sketch_css)
13 | - [`sketch_lustre`](https://hexdocs.pm/sketch_lustre)
14 | - [`sketch_redraw`](https://hexdocs.pm/sketch_redraw)
15 |
16 | ## Future integrations
17 |
18 | - `sketch_nakai`
19 |
20 | ## Integrating the Sketch list
21 |
22 | To integrate Sketch, you'll write a package and publish it on Hex. Feel free to
23 | submit a PR to point to your package in this README! This would give visibility
24 | to your package, and help users to find your integration directly from here!
25 |
26 | ## Examples
27 |
28 | Some examples can be found in `e2e/` folder. Take a look if you want to see what
29 | Sketch looks like in its real shape!
30 |
31 | ## Contributing
32 |
33 | You love Sketch and you'd like to contribute! Open an issue or a PR directly!
34 |
--------------------------------------------------------------------------------
/e2e/redraw_example/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 | .yarn
26 | .pnp.cjs
27 | .pnp.loader.mjs
28 |
--------------------------------------------------------------------------------
/e2e/redraw_example/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 80,
3 | "semi": false,
4 | "singleQuote": true,
5 | "arrowParens": "avoid",
6 | "trailingComma": "es5",
7 | "importOrder": [
8 | "^\\@.*$",
9 | "^[^@\\.].*$",
10 | "^\\..*$"
11 | ],
12 | "importOrderSortSpecifiers": true,
13 | "plugins": [
14 | "@trivago/prettier-plugin-sort-imports"
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/e2e/redraw_example/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | nodeLinker: node-modules
2 |
--------------------------------------------------------------------------------
/e2e/redraw_example/README.md:
--------------------------------------------------------------------------------
1 | # React + Vite
2 |
3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4 |
5 | Currently, two official plugins are available:
6 |
7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9 |
--------------------------------------------------------------------------------
/e2e/redraw_example/gleam.toml:
--------------------------------------------------------------------------------
1 | name = "redraw_example"
2 | target = "javascript"
3 | version = "1.0.0"
4 |
5 | description = ""
6 | licences = ["MIT"]
7 | links = [{title = "Sponsor", href = "https://github.com/sponsors/ghivert"}]
8 | repository = {type = "github", user = "ghivert", repo = "sketch"}
9 |
10 | [dependencies]
11 | gleam_stdlib = ">= 0.34.0 and < 2.0.0"
12 | redraw = ">= 2.0.0 and < 3.0.0"
13 | redraw_dom = ">= 2.0.0 and < 3.0.0"
14 | sketch = {path = "../../sketch"}
15 | sketch_redraw = {path = "../../sketch_redraw"}
16 | shared_styles = {path = "../shared_styles"}
17 |
18 | [dev-dependencies]
19 | gleeunit = ">= 1.0.0 and < 2.0.0"
20 |
--------------------------------------------------------------------------------
/e2e/redraw_example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React
8 |
9 |
10 |
14 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/e2e/redraw_example/manifest.toml:
--------------------------------------------------------------------------------
1 | # This file was generated by Gleam
2 | # You typically do not need to edit this file
3 |
4 | packages = [
5 | { name = "gleam_erlang", version = "0.34.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "0C38F2A128BAA0CEF17C3000BD2097EB80634E239CE31A86400C4416A5D0FDCC" },
6 | { name = "gleam_javascript", version = "0.13.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "F98328FCF573DA6F3A35D7F6CB3F9FF19FD5224CCBA9151FCBEAA0B983AF2F58" },
7 | { name = "gleam_otp", version = "0.16.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "50DA1539FC8E8FA09924EB36A67A2BBB0AD6B27BCDED5A7EF627057CF69D035E" },
8 | { name = "gleam_stdlib", version = "0.59.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "F8FEE9B35797301994B81AF75508CF87C328FE1585558B0FFD188DC2B32EAA95" },
9 | { name = "gleeunit", version = "1.3.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "A7DD6C07B7DA49A6E28796058AA89E651D233B357D5607006D70619CD89DAAAB" },
10 | { name = "murmur3a", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "murmur3a", source = "hex", outer_checksum = "DAA714CEF379915D0F718BC410389245AA8ABFB6F48C73ADB9F011B009F28893" },
11 | { name = "redraw", version = "2.0.2", build_tools = ["gleam"], requirements = ["gleam_javascript", "gleam_stdlib"], otp_app = "redraw", source = "hex", outer_checksum = "F95A1F4691A3CC2FD1FD7638178D7912AC5FB0E521C59970304E9DE4E739E40D" },
12 | { name = "redraw_dom", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "redraw"], otp_app = "redraw_dom", source = "hex", outer_checksum = "8318DA1E428B349177C444DDC2FA9AE0D33E0DD0CC5A55B82F030811FFD69EA4" },
13 | { name = "shared_styles", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "sketch"], source = "local", path = "../shared_styles" },
14 | { name = "sketch", version = "4.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "murmur3a"], source = "local", path = "../../sketch" },
15 | { name = "sketch_redraw", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "redraw", "redraw_dom", "sketch"], source = "local", path = "../../sketch_redraw" },
16 | ]
17 |
18 | [requirements]
19 | gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" }
20 | gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
21 | redraw = { version = ">= 2.0.0 and < 3.0.0" }
22 | redraw_dom = { version = ">= 2.0.0 and < 3.0.0" }
23 | shared_styles = { path = "../shared_styles" }
24 | sketch = { path = "../../sketch" }
25 | sketch_redraw = { path = "../../sketch_redraw" }
26 |
--------------------------------------------------------------------------------
/e2e/redraw_example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "redraw_example",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "lint": "eslint .",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "dotenv": "^16.4.5",
14 | "react": "^18.3.1",
15 | "react-dom": "^18.3.1"
16 | },
17 | "devDependencies": {
18 | "@eslint/js": "^9.9.0",
19 | "@trivago/prettier-plugin-sort-imports": "^4.3.0",
20 | "@types/react": "^18.3.3",
21 | "@types/react-dom": "^18.3.0",
22 | "@vitejs/plugin-react-swc": "^3.5.0",
23 | "eslint": "^9.9.0",
24 | "eslint-plugin-react": "^7.35.0",
25 | "eslint-plugin-react-hooks": "^5.1.0-rc.0",
26 | "eslint-plugin-react-refresh": "^0.4.9",
27 | "globals": "^15.9.0",
28 | "prettier": "^3.3.3",
29 | "vite": "^5.4.1",
30 | "vite-gleam": "^0.4.3"
31 | },
32 | "packageManager": "yarn@4.4.0"
33 | }
34 |
--------------------------------------------------------------------------------
/e2e/redraw_example/src/components.gleam:
--------------------------------------------------------------------------------
1 | //// Defines the base components used in the shared view. Think copmonents as in
2 | //// functions that simply returns the HTML correctly formatted.
3 | //// Every component accepts two arrays, attributes and children, to follow the
4 | //// same convention as Lustre standard HTML. That way, you could leverage
5 | //// your knowledge of Lustre, and behaves exactly as expected.
6 |
7 | import redraw
8 | import redraw/dom/html as h
9 | import shared_styles as styles
10 | import sketch/redraw/dom/html
11 |
12 | pub fn body(attrs, children) {
13 | // demonstrate ability to merge fragment at root
14 | redraw.fragment([
15 | html.div(styles.body_style(), attrs, children),
16 | h.footer([], []),
17 | ])
18 | }
19 |
20 | pub fn topbar(attrs, children) {
21 | html.div(styles.topbar_style(), attrs, children)
22 | }
23 |
24 | pub fn headline(value, attrs, children) {
25 | html.main(styles.headline_style(value), attrs, children)
26 | }
27 |
28 | pub fn headline_subtitle(attrs, children) {
29 | html.div(styles.headline_subtitle_style(), attrs, children)
30 | }
31 |
32 | pub fn headline_emphasize(attrs, children) {
33 | html.div(styles.headline_emphasize_style(), attrs, children)
34 | }
35 |
36 | pub fn counter(attrs, children) {
37 | html.div(styles.counter_style(), attrs, children)
38 | }
39 |
40 | pub fn counter_title(attrs, children) {
41 | h.div(attrs, children)
42 | }
43 |
44 | pub fn counter_subtitle(attrs, children) {
45 | html.div(styles.counter_subtitle_style(), attrs, children)
46 | }
47 |
48 | pub fn button(attrs, children) {
49 | html.button(styles.button_style(), attrs, children)
50 | }
51 |
52 | pub fn value(attrs, children) {
53 | html.div(styles.value_style(), attrs, children)
54 | }
55 |
56 | pub fn value_content(attrs, children) {
57 | html.div(styles.value_content_style(), attrs, children)
58 | }
59 |
60 | pub fn showcase(attrs, children) {
61 | html.div(styles.showcase_style(), attrs, children)
62 | }
63 |
64 | pub fn counter_body(attrs, children) {
65 | html.div(styles.counter_body_style(), attrs, children)
66 | }
67 |
68 | pub fn counter_body_title(attrs, children) {
69 | html.div(styles.counter_body_title_style(), attrs, children)
70 | }
71 |
72 | pub fn counter_counter(attrs, children) {
73 | html.div(styles.counter_counter_style(), attrs, children)
74 | }
75 |
76 | pub fn showcase_body(attrs, children) {
77 | html.div(styles.showcase_body_style(), attrs, children)
78 | }
79 |
80 | pub fn card_title(attrs, children) {
81 | h.div(attrs, children)
82 | }
83 |
--------------------------------------------------------------------------------
/e2e/redraw_example/src/redraw_example.gleam:
--------------------------------------------------------------------------------
1 | import components
2 | import gleam/int
3 | import redraw
4 | import redraw/dom/attribute as a
5 | import redraw/dom/client
6 | import redraw/dom/events
7 | import redraw/dom/html as h
8 | import sketch/redraw as sr
9 |
10 | pub fn main() {
11 | let app = app()
12 | let assert Ok(root) = client.create_root("root")
13 | client.render(root, redraw.strict_mode([sr.provider([app()])]))
14 | }
15 |
16 | fn app() {
17 | let view_counter_description = view_counter_description()
18 | let view_counter = view_counter()
19 | let showcase = showcase()
20 | use <- redraw.component__("App")
21 | let #(count, set_count) = redraw.use_state_(0)
22 | let increment = events.on_click(fn(_) { set_count(fn(c) { c + 1 }) })
23 | let decrement = events.on_click(fn(_) { set_count(fn(c) { c - 1 }) })
24 | components.body([], [
25 | components.topbar([], [h.text("Sketch")]),
26 | components.headline(count, [], [
27 | components.headline_subtitle([], [h.text("CSS-in-Gleam")]),
28 | components.headline_emphasize([], [
29 | h.text("Improve your CSS"),
30 | h.br([]),
31 | h.text("with Sketch"),
32 | ]),
33 | ]),
34 | components.counter([], [
35 | components.counter_body([], [
36 | components.counter_body_title([], [
37 | view_counter_description(),
38 | view_counter(#(count, increment, decrement)),
39 | ]),
40 | ]),
41 | components.card_title([], [h.text("See it in action")]),
42 | ]),
43 | showcase(),
44 | ])
45 | }
46 |
47 | fn view_counter_description() {
48 | use <- redraw.component__("CounterDescription")
49 | let use_counter = "Use the counter, and see the site changing with the model!"
50 | let now_edit = "Now, try to edit the code to see the modifications live!"
51 | h.div([], [
52 | h.text("Counter"),
53 | components.counter_subtitle([], [h.text(use_counter)]),
54 | components.counter_subtitle([], [h.text(now_edit)]),
55 | ])
56 | }
57 |
58 | fn view_counter() {
59 | use #(count, increment, decrement) <- redraw.component_("Counter")
60 | let disabled = a.disabled(count <= 0)
61 | let model = int.to_string(count)
62 | components.counter_counter([], [
63 | components.button([decrement, disabled], [h.text("-")]),
64 | components.value([], [components.value_content([], [h.text(model)])]),
65 | components.button([increment], [h.text("+")]),
66 | ])
67 | }
68 |
69 | fn showcase() {
70 | use <- redraw.component__("Showcase")
71 | components.showcase([], [
72 | components.showcase_body([], [h.text("Coming soon...")]),
73 | components.card_title([], [h.text("Showcase")]),
74 | ])
75 | }
76 |
--------------------------------------------------------------------------------
/e2e/redraw_example/src/redraw_example.main.mjs:
--------------------------------------------------------------------------------
1 | import * as landing from './redraw_example.gleam'
2 |
3 | landing.main()
4 |
--------------------------------------------------------------------------------
/e2e/redraw_example/vite.config.js:
--------------------------------------------------------------------------------
1 | import react from '@vitejs/plugin-react-swc'
2 | import 'dotenv/config'
3 | import { createLogger, defineConfig } from 'vite'
4 | import gleam from 'vite-gleam'
5 |
6 | const customLogger = createLogger()
7 | const loggerWarn = customLogger.warn
8 | customLogger.warn = (msg, options) => {
9 | if (msg.includes('import_')) return
10 | loggerWarn(msg, options)
11 | }
12 |
13 | export default defineConfig({
14 | customLogger,
15 | plugins: [gleam(), react()],
16 | })
17 |
--------------------------------------------------------------------------------
/e2e/server_components/.gitignore:
--------------------------------------------------------------------------------
1 | *.beam
2 | *.ez
3 | /build
4 | erl_crash.dump
5 |
--------------------------------------------------------------------------------
/e2e/server_components/README.md:
--------------------------------------------------------------------------------
1 | # Server Components
2 |
3 | Server component rendering of the application. Because the server will act as
4 | the client, nothing is needed here to make it work. Launch the server, heads up
5 | to [`localhost:1234`](http://localhost:1234) and let the magic happen.
6 |
7 | ## Getting started
8 |
9 | ```sh
10 | gleam run
11 | ```
12 |
--------------------------------------------------------------------------------
/e2e/server_components/gleam.toml:
--------------------------------------------------------------------------------
1 | name = "server_components"
2 | version = "1.0.0"
3 |
4 | [dependencies]
5 | gleam_stdlib = "~> 0.34 or ~> 1.0"
6 | sketch = {path = "../../sketch"}
7 | sketch_lustre = {path = "../../sketch_lustre"}
8 | gleam_http = "~> 3.6"
9 | mist = ">= 4.0.0 and < 5.0.0"
10 | lustre = ">= 5.0.0 and < 6.0.0"
11 | shared_view = {path = "../shared_view"}
12 | gleam_otp = ">= 0.13.0 and < 1.0.0"
13 | gleam_json = ">= 2.0.0 and < 3.0.0"
14 | gleam_erlang = ">= 0.28.0 and < 1.0.0"
15 |
16 | [dev-dependencies]
17 | gleeunit = "~> 1.0"
18 |
--------------------------------------------------------------------------------
/e2e/server_components/test/server_components_test.gleam:
--------------------------------------------------------------------------------
1 | import gleeunit
2 | import gleeunit/should
3 |
4 | pub fn main() {
5 | gleeunit.main()
6 | }
7 |
8 | // gleeunit test functions end in `_test`
9 | pub fn hello_world_test() {
10 | 1
11 | |> should.equal(1)
12 | }
13 |
--------------------------------------------------------------------------------
/e2e/shared_styles/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: test
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | - main
8 | pull_request:
9 |
10 | jobs:
11 | test:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v4
15 | - uses: erlef/setup-beam@v1
16 | with:
17 | otp-version: "26.0.2"
18 | gleam-version: "1.4.1"
19 | rebar3-version: "3"
20 | # elixir-version: "1.15.4"
21 | - run: gleam deps download
22 | - run: gleam test
23 | - run: gleam format --check src test
24 |
--------------------------------------------------------------------------------
/e2e/shared_styles/.gitignore:
--------------------------------------------------------------------------------
1 | *.beam
2 | *.ez
3 | /build
4 | erl_crash.dump
5 |
--------------------------------------------------------------------------------
/e2e/shared_styles/README.md:
--------------------------------------------------------------------------------
1 | # shared_styles
2 |
3 | [](https://hex.pm/packages/shared_styles)
4 | [](https://hexdocs.pm/shared_styles/)
5 |
6 | ```sh
7 | gleam add shared_styles@1
8 | ```
9 | ```gleam
10 | import shared_styles
11 |
12 | pub fn main() {
13 | // TODO: An example of the project in use
14 | }
15 | ```
16 |
17 | Further documentation can be found at .
18 |
19 | ## Development
20 |
21 | ```sh
22 | gleam run # Run the project
23 | gleam test # Run the tests
24 | ```
25 |
--------------------------------------------------------------------------------
/e2e/shared_styles/gleam.toml:
--------------------------------------------------------------------------------
1 | name = "shared_styles"
2 | version = "1.0.0"
3 |
4 | # Fill out these fields if you intend to generate HTML documentation or publish
5 | # your project to the Hex package manager.
6 | #
7 | # description = ""
8 | # licences = ["Apache-2.0"]
9 | # repository = { type = "github", user = "", repo = "" }
10 | # links = [{ title = "Website", href = "" }]
11 | #
12 | # For a full reference of all the available options, you can have a look at
13 | # https://gleam.run/writing-gleam/gleam-toml/.
14 |
15 | [dependencies]
16 | gleam_stdlib = ">= 0.34.0 and < 2.0.0"
17 | sketch = {path = "../../sketch"}
18 |
19 | [dev-dependencies]
20 | gleeunit = ">= 1.0.0 and < 2.0.0"
21 |
--------------------------------------------------------------------------------
/e2e/shared_styles/manifest.toml:
--------------------------------------------------------------------------------
1 | # This file was generated by Gleam
2 | # You typically do not need to edit this file
3 |
4 | packages = [
5 | { name = "gleam_erlang", version = "0.34.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "0C38F2A128BAA0CEF17C3000BD2097EB80634E239CE31A86400C4416A5D0FDCC" },
6 | { name = "gleam_otp", version = "0.16.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "50DA1539FC8E8FA09924EB36A67A2BBB0AD6B27BCDED5A7EF627057CF69D035E" },
7 | { name = "gleam_stdlib", version = "0.59.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "F8FEE9B35797301994B81AF75508CF87C328FE1585558B0FFD188DC2B32EAA95" },
8 | { name = "gleeunit", version = "1.3.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "A7DD6C07B7DA49A6E28796058AA89E651D233B357D5607006D70619CD89DAAAB" },
9 | { name = "murmur3a", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "murmur3a", source = "hex", outer_checksum = "DAA714CEF379915D0F718BC410389245AA8ABFB6F48C73ADB9F011B009F28893" },
10 | { name = "sketch", version = "4.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "murmur3a"], source = "local", path = "../../sketch" },
11 | ]
12 |
13 | [requirements]
14 | gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" }
15 | gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
16 | sketch = { path = "../../sketch" }
17 |
--------------------------------------------------------------------------------
/e2e/shared_styles/src/shared_styles.gleam:
--------------------------------------------------------------------------------
1 | import sketch/css
2 | import sketch/css/length.{ch, percent, px, rem}
3 |
4 | /// Standard class defining a card component. Can be used with `css.compose`
5 | /// everywhere a card is needed. Properties will be overriden if redefined in a
6 | /// composed class or element.
7 | fn card() {
8 | css.class([
9 | css.display("flex"),
10 | css.flex_direction("column"),
11 | css.border_radius(px(20)),
12 | css.padding(px(20)),
13 | css.font_size(rem(3.0)),
14 | css.font_weight("900"),
15 | ])
16 | }
17 |
18 | /// Standard class defining a card body component. Can be used with `css.compose`
19 | /// everywhere a card need a body. Properties will be overriden if redefined in a
20 | /// composed class or element.
21 | fn card_body() {
22 | css.class([
23 | css.flex("1"),
24 | css.font_family("Lexend"),
25 | css.font_weight("600"),
26 | css.font_size(rem(1.0)),
27 | css.line_height("normal"),
28 | ])
29 | }
30 |
31 | pub fn body_style() {
32 | css.class([
33 | css.line_height("0.75"),
34 | css.display("grid"),
35 | css.gap(px(10)),
36 | css.padding(px(10)),
37 | css.grid_template_columns("repeat(2, 1fr)"),
38 | css.grid_template_areas([
39 | "topbar topbar", "headline headline", "counter showcase",
40 | ]),
41 | css.max_width(px(1200)),
42 | css.margin_("auto"),
43 | ])
44 | }
45 |
46 | pub fn topbar_style() {
47 | css.class([
48 | css.display("flex"),
49 | css.grid_area("topbar"),
50 | css.font_size(rem(1.2)),
51 | css.padding_left(px(20)),
52 | css.padding_top(px(5)),
53 | ])
54 | }
55 |
56 | pub fn headline_style(value) {
57 | let background = case value % 2 == 1 {
58 | True -> "var(--atomic-tangerine)"
59 | False -> "var(--periwinkle)"
60 | }
61 | css.class([
62 | css.grid_area("headline"),
63 | css.background(background),
64 | css.text_align("center"),
65 | css.padding(px(120)) |> css.important,
66 | css.gap(px(20)),
67 | css.transition("all .2s"),
68 | css.compose(card()),
69 | ])
70 | }
71 |
72 | pub fn headline_subtitle_style() {
73 | css.class([css.font_size(rem(1.2)), css.font_weight("normal")])
74 | }
75 |
76 | pub fn headline_emphasize_style() {
77 | css.class([css.font_size(rem(3.0)), css.font_weight("900")])
78 | }
79 |
80 | pub fn counter_style() {
81 | css.class([
82 | css.grid_area("counter"),
83 | css.background("var(--aquamarine)"),
84 | css.compose(card()),
85 | ])
86 | }
87 |
88 | pub fn counter_subtitle_style() {
89 | css.class([
90 | css.font_weight("normal"),
91 | css.font_size(rem(0.9)),
92 | css.first_child([css.padding_top(px(5))]),
93 | ])
94 | }
95 |
96 | pub fn button_style() {
97 | css.class([
98 | css.appearance("none"),
99 | css.border("none"),
100 | css.font_family("Lexend"),
101 | css.background("black"),
102 | css.color("white"),
103 | css.border_radius(px(5)),
104 | css.padding_("5px 20px"),
105 | css.font_size(rem(1.2)),
106 | css.font_weight("600"),
107 | css.min_width(px(100)),
108 | css.cursor("pointer"),
109 | css.disabled([css.opacity(0.3), css.cursor("not-allowed")]),
110 | css.hover([css.background("#333")]),
111 | ])
112 | }
113 |
114 | pub fn value_style() {
115 | css.class([
116 | css.background("var(--turquoise)"),
117 | css.height(percent(100)),
118 | css.display("flex"),
119 | css.align_items("center"),
120 | css.justify_content("center"),
121 | css.border_radius(px(5)),
122 | ])
123 | }
124 |
125 | pub fn value_content_style() {
126 | css.class([css.width(ch(7.0)), css.text_align("center")])
127 | }
128 |
129 | pub fn showcase_style() {
130 | css.class([
131 | css.grid_area("showcase"),
132 | css.background("var(--turquoise)"),
133 | css.compose(card()),
134 | ])
135 | }
136 |
137 | pub fn counter_body_style() {
138 | css.class([css.compose(card_body()), css.padding_bottom(px(40))])
139 | }
140 |
141 | pub fn counter_body_title_style() {
142 | css.class([css.display("flex"), css.flex_direction("column"), css.gap(px(20))])
143 | }
144 |
145 | pub fn counter_counter_style() {
146 | css.class([
147 | css.display("grid"),
148 | css.align_items("center"),
149 | css.grid_template_columns("repeat(3, auto)"),
150 | css.justify_content("start"),
151 | css.font_size(rem(1.5)),
152 | css.gap(px(10)),
153 | ])
154 | }
155 |
156 | pub fn showcase_body_style() {
157 | css.class([css.compose(card_body()), css.opacity(0.5)])
158 | }
159 |
--------------------------------------------------------------------------------
/e2e/shared_styles/test/shared_styles_test.gleam:
--------------------------------------------------------------------------------
1 | import gleeunit
2 | import gleeunit/should
3 |
4 | pub fn main() {
5 | gleeunit.main()
6 | }
7 |
8 | // gleeunit test functions end in `_test`
9 | pub fn hello_world_test() {
10 | 1
11 | |> should.equal(1)
12 | }
13 |
--------------------------------------------------------------------------------
/e2e/shared_view/README.md:
--------------------------------------------------------------------------------
1 | # Shared View
2 |
3 | Base package used in server components, SSR & application rendering. Everything
4 | in here is generic, and can be used in the exact same way, whether it's on web,
5 | or anywhere else. Just import the package, and start to use it. Take a look in
6 | the siblings folder to get your head around that idea.
7 |
8 | ## Using it in siblings folder
9 |
10 | ```toml
11 | # In gleam.toml.
12 | # Leverage compiler path resolution.
13 | [dependencies]
14 | shared_view = { path = "../shared_view" }
15 | ```
16 |
--------------------------------------------------------------------------------
/e2e/shared_view/gleam.toml:
--------------------------------------------------------------------------------
1 | name = "shared_view"
2 | version = "1.0.0"
3 |
4 | [dependencies]
5 | gleam_stdlib = ">= 0.34.0 and < 2.0.0"
6 | sketch = {path = "../../sketch"}
7 | sketch_lustre = {path = "../../sketch_lustre"}
8 | lustre = ">= 5.0.0 and < 6.0.0"
9 | shared_styles = {path = "../shared_styles"}
10 |
11 | [dev-dependencies]
12 | gleeunit = ">= 1.0.0 and < 2.0.0"
13 |
--------------------------------------------------------------------------------
/e2e/shared_view/manifest.toml:
--------------------------------------------------------------------------------
1 | # This file was generated by Gleam
2 | # You typically do not need to edit this file
3 |
4 | packages = [
5 | { name = "gleam_erlang", version = "0.34.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "0C38F2A128BAA0CEF17C3000BD2097EB80634E239CE31A86400C4416A5D0FDCC" },
6 | { name = "gleam_json", version = "2.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "C55C5C2B318533A8072D221C5E06E5A75711C129E420DD1CE463342106012E5D" },
7 | { name = "gleam_otp", version = "0.16.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "50DA1539FC8E8FA09924EB36A67A2BBB0AD6B27BCDED5A7EF627057CF69D035E" },
8 | { name = "gleam_stdlib", version = "0.59.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "F8FEE9B35797301994B81AF75508CF87C328FE1585558B0FFD188DC2B32EAA95" },
9 | { name = "gleeunit", version = "1.3.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "A7DD6C07B7DA49A6E28796058AA89E651D233B357D5607006D70619CD89DAAAB" },
10 | { name = "houdini", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "houdini", source = "hex", outer_checksum = "5BA517E5179F132F0471CB314F27FE210A10407387DA1EA4F6FD084F74469FC2" },
11 | { name = "lustre", version = "5.0.3", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib", "houdini"], otp_app = "lustre", source = "hex", outer_checksum = "0BB69D69A9E75E675AA2C32A4A0E5086041D037829FC8AD385BA6A59E45A60A2" },
12 | { name = "murmur3a", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "murmur3a", source = "hex", outer_checksum = "DAA714CEF379915D0F718BC410389245AA8ABFB6F48C73ADB9F011B009F28893" },
13 | { name = "shared_styles", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "sketch"], source = "local", path = "../shared_styles" },
14 | { name = "sketch", version = "4.1.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "murmur3a"], source = "local", path = "../../sketch" },
15 | { name = "sketch_lustre", version = "3.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "lustre", "sketch"], source = "local", path = "../../sketch_lustre" },
16 | ]
17 |
18 | [requirements]
19 | gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" }
20 | gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
21 | lustre = { version = ">= 5.0.0 and < 6.0.0" }
22 | shared_styles = { path = "../shared_styles" }
23 | sketch = { path = "../../sketch" }
24 | sketch_lustre = { path = "../../sketch_lustre" }
25 |
--------------------------------------------------------------------------------
/e2e/shared_view/src/components.gleam:
--------------------------------------------------------------------------------
1 | //// Defines the base components used in the shared view. Think copmonents as in
2 | //// functions that simply returns the HTML correctly formatted.
3 | //// Every component accepts two arrays, attributes and children, to follow the
4 | //// same convention as Lustre standard HTML. That way, you could leverage
5 | //// your knowledge of Lustre, and behaves exactly as expected.
6 |
7 | import shared_styles as styles
8 | import sketch/lustre/element
9 | import sketch/lustre/element/html
10 | import sketch/lustre/element/keyed
11 |
12 | pub fn body(attrs, children) {
13 | // demonstrate ability to merge fragment at root
14 | element.fragment([
15 | html.div(styles.body_style(), attrs, children),
16 | html.footer_([], []),
17 | ])
18 | }
19 |
20 | pub fn topbar(attrs, children) {
21 | keyed.div(styles.topbar_style(), attrs, children)
22 | }
23 |
24 | pub fn headline(value, attrs, children) {
25 | html.main(styles.headline_style(value), attrs, children)
26 | }
27 |
28 | pub fn headline_subtitle(attrs, children) {
29 | html.div(styles.headline_subtitle_style(), attrs, children)
30 | }
31 |
32 | pub fn headline_emphasize(attrs, children) {
33 | html.div(styles.headline_emphasize_style(), attrs, children)
34 | }
35 |
36 | pub fn counter(attrs, children) {
37 | html.div(styles.counter_style(), attrs, children)
38 | }
39 |
40 | pub fn counter_title(attrs, children) {
41 | html.div_(attrs, children)
42 | }
43 |
44 | pub fn counter_subtitle(attrs, children) {
45 | html.div(styles.counter_subtitle_style(), attrs, children)
46 | }
47 |
48 | pub fn button(attrs, children) {
49 | html.button(styles.button_style(), attrs, children)
50 | }
51 |
52 | pub fn value(attrs, children) {
53 | html.div(styles.value_style(), attrs, children)
54 | }
55 |
56 | pub fn value_content(attrs, children) {
57 | html.div(styles.value_content_style(), attrs, children)
58 | }
59 |
60 | pub fn showcase(attrs, children) {
61 | html.div(styles.showcase_style(), attrs, children)
62 | }
63 |
64 | pub fn counter_body(attrs, children) {
65 | html.div(styles.counter_body_style(), attrs, children)
66 | }
67 |
68 | pub fn counter_body_title(attrs, children) {
69 | html.div(styles.counter_body_title_style(), attrs, children)
70 | }
71 |
72 | pub fn counter_counter(attrs, children) {
73 | html.div(styles.counter_counter_style(), attrs, children)
74 | }
75 |
76 | pub fn showcase_body(attrs, children) {
77 | html.div(styles.showcase_body_style(), attrs, children)
78 | }
79 |
80 | pub fn card_title(attrs, children) {
81 | html.div_(attrs, children)
82 | }
83 |
--------------------------------------------------------------------------------
/e2e/shared_view/src/shared_view.gleam:
--------------------------------------------------------------------------------
1 | import components
2 | import gleam/int
3 | import lustre
4 | import lustre/attribute as a
5 | import lustre/event as e
6 | import sketch
7 | import sketch/lustre as sketch_lustre
8 | import sketch/lustre/element/html as h
9 | import styles
10 |
11 | pub type Model =
12 | Int
13 |
14 | pub type Msg {
15 | Increment
16 | Decrement
17 | }
18 |
19 | /// Defines the standard app, used everywhere in Lustre applications.
20 | pub fn app(stylesheet: sketch.StyleSheet) {
21 | use model <- lustre.simple(init, update)
22 | use <- sketch_lustre.render(stylesheet:, in: [sketch_lustre.node()])
23 | view(model)
24 | }
25 |
26 | /// Function used specifically in SSR, in order to send the correct HTML
27 | /// before hydrating it. It can also be an example of HTML server-side
28 | /// generation, Sketch improved.
29 | pub fn ssr(model: Model, stylesheet: sketch.StyleSheet) {
30 | use <- sketch_lustre.render(stylesheet:, in: [sketch_lustre.node()])
31 | h.html([], [
32 | h.head([], [
33 | h.link([a.rel("stylesheet"), a.href(styles.fonts)]),
34 | h.style([], styles.default_style),
35 | ]),
36 | h.body_([], [view(model)]),
37 | ])
38 | }
39 |
40 | fn init(_flags) {
41 | 0
42 | }
43 |
44 | fn update(model: Model, msg: Msg) {
45 | case msg {
46 | Increment -> model + 1
47 | Decrement -> model - 1
48 | }
49 | }
50 |
51 | /// Main view function, used to render HTML, whether it is on server components,
52 | /// on web render, with SSR, or even simple server-side generation HTML, like a
53 | /// blog engine.
54 | fn view(model: Model) {
55 | components.body([], [
56 | components.topbar([a.id("topbar")], [#("topbar", h.text("Sketch"))]),
57 | components.headline(model, [], [
58 | components.headline_subtitle([], [h.text("CSS-in-Gleam")]),
59 | components.headline_emphasize([], [
60 | h.text("Improve your CSS"),
61 | h.br_([]),
62 | h.text("with Sketch"),
63 | ]),
64 | ]),
65 | components.counter([], [
66 | components.counter_body([], [
67 | components.counter_body_title([], [
68 | view_counter_description(),
69 | view_counter(model),
70 | ]),
71 | ]),
72 | components.card_title([], [h.text("See it in action")]),
73 | ]),
74 | components.showcase([], [
75 | components.showcase_body([], [h.text("Coming soon...")]),
76 | components.card_title([], [h.text("Showcase")]),
77 | ]),
78 | ])
79 | }
80 |
81 | fn view_counter_description() {
82 | let use_counter = "Use the counter, and see the site changing with the model!"
83 | let now_edit = "Now, try to edit the code to see the modifications live!"
84 | h.div_([], [
85 | h.text("Counter"),
86 | components.counter_subtitle([], [h.text(use_counter)]),
87 | components.counter_subtitle([], [h.text(now_edit)]),
88 | ])
89 | }
90 |
91 | fn view_counter(model: Model) {
92 | let disabled = a.disabled(model <= 0)
93 | let model = int.to_string(model)
94 | components.counter_counter([], [
95 | components.button([e.on_click(Decrement), disabled], [h.text("-")]),
96 | components.value([], [components.value_content([], [h.text(model)])]),
97 | components.button([e.on_click(Increment)], [h.text("+")]),
98 | ])
99 | }
100 |
--------------------------------------------------------------------------------
/e2e/shared_view/src/styles.gleam:
--------------------------------------------------------------------------------
1 | pub const fonts = "https://fonts.googleapis.com/css2?family=Lexend:wght@100..900&family=Zain:wght@200;300;400;700;800;900&display=swap"
2 |
3 | pub const default_style = "body {
4 | margin: 0;
5 | font-family: Zain;
6 | }
7 | :root {
8 | --atomic-tangerine: #f79256;
9 | --aquamarine: #acfcd9;
10 | --turquoise: #5dd9c1;
11 | --periwinkle: #a6b1e1;
12 | --purple: #dcd6f7;
13 | }"
14 |
--------------------------------------------------------------------------------
/e2e/shared_view/test/shared_view_test.gleam:
--------------------------------------------------------------------------------
1 | import gleeunit
2 | import gleeunit/should
3 |
4 | pub fn main() {
5 | gleeunit.main()
6 | }
7 |
8 | // gleeunit test functions end in `_test`
9 | pub fn hello_world_test() {
10 | 1
11 | |> should.equal(1)
12 | }
13 |
--------------------------------------------------------------------------------
/e2e/ssr/.gitignore:
--------------------------------------------------------------------------------
1 | *.beam
2 | *.ez
3 | /build
4 | erl_crash.dump
5 |
--------------------------------------------------------------------------------
/e2e/ssr/README.md:
--------------------------------------------------------------------------------
1 | # Server Side Rendering
2 |
3 | SSR rendering of the application. Because it's simply a server serving HTML in
4 | the classical way, nothing is needed here to make it work. Launch the server,
5 | heads up to [`localhost:1234`](http://localhost:1234) and let the magic happen.
6 |
7 | ## Getting started
8 |
9 | ```sh
10 | gleam run
11 | ```
12 |
--------------------------------------------------------------------------------
/e2e/ssr/gleam.toml:
--------------------------------------------------------------------------------
1 | name = "ssr"
2 | version = "1.0.0"
3 |
4 | [dependencies]
5 | gleam_erlang = ">= 0.25.0 and < 1.0.0"
6 | gleam_http = ">= 3.6.0 and < 4.0.0"
7 | gleam_stdlib = ">= 0.34.0 and < 2.0.0"
8 | lustre = ">= 5.0.0 and < 6.0.0"
9 | mist = "~> 4.0.0"
10 | sketch = {path = "../../sketch"}
11 | sketch_lustre = {path = "../../sketch_lustre"}
12 | shared_view = {path = "../shared_view"}
13 |
14 | [dev-dependencies]
15 | gleeunit = ">= 1.0.0 and < 2.0.0"
16 |
--------------------------------------------------------------------------------
/e2e/ssr/src/ssr.gleam:
--------------------------------------------------------------------------------
1 | import gleam/bytes_tree
2 | import gleam/erlang/process
3 | import gleam/http/response.{type Response}
4 | import lustre/element
5 | import mist.{type ResponseData}
6 | import shared_view
7 | import sketch
8 | import sketch/lustre as sketch_lustre
9 |
10 | pub fn main() {
11 | let assert Ok(stylesheet) = sketch_lustre.setup()
12 | let assert Ok(_) =
13 | fn(_) { greet(stylesheet) }
14 | |> mist.new()
15 | |> mist.port(1234)
16 | |> mist.start_http()
17 | process.sleep_forever()
18 | }
19 |
20 | fn greet(stylesheet: sketch.StyleSheet) -> Response(ResponseData) {
21 | shared_view.ssr(0, stylesheet)
22 | |> element.to_document_string()
23 | |> bytes_tree.from_string()
24 | |> mist.Bytes()
25 | |> response.set_body(response.new(200), _)
26 | }
27 |
--------------------------------------------------------------------------------
/e2e/ssr/test/ssr_test.gleam:
--------------------------------------------------------------------------------
1 | import gleeunit
2 | import gleeunit/should
3 |
4 | pub fn main() {
5 | gleeunit.main()
6 | }
7 |
8 | // gleeunit test functions end in `_test`
9 | pub fn hello_world_test() {
10 | 1
11 | |> should.equal(1)
12 | }
13 |
--------------------------------------------------------------------------------
/e2e/web_application/.gitignore:
--------------------------------------------------------------------------------
1 | *.beam
2 | *.ez
3 | /build
4 | erl_crash.dump
5 | priv/
6 |
--------------------------------------------------------------------------------
/e2e/web_application/README.md:
--------------------------------------------------------------------------------
1 | # Web Application
2 |
3 | Web Application rendering of the application, as an SPA. Because the application
4 | uses the Lustre devtools, nothing is needed here to make it work. Launch the
5 | server, heads up to [`localhost:1234`](http://localhost:1234) and let the magic
6 | happen.
7 |
8 | ## Getting started
9 |
10 | ```sh
11 | gleam run -m lustre/dev start
12 | ```
13 |
--------------------------------------------------------------------------------
/e2e/web_application/gleam.toml:
--------------------------------------------------------------------------------
1 | name = "web_application"
2 | target = "javascript"
3 | version = "1.0.0"
4 |
5 | [dependencies]
6 | gleam_stdlib = "~> 0.34 or ~> 1.0"
7 | lustre = ">= 5.0.0 and < 6.0.0"
8 | sketch = {path = "../../sketch"}
9 | sketch_lustre = {path = "../../sketch_lustre"}
10 | shared_view = {path = "../shared_view"}
11 |
12 | [dev-dependencies]
13 | gleeunit = "~> 1.0"
14 | lustre_dev_tools = ">= 1.8.0 and < 2.0.0"
15 |
--------------------------------------------------------------------------------
/e2e/web_application/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | 🚧 stylesheet_render
8 |
9 |
10 |
11 |
12 |
13 |
14 |
18 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/e2e/web_application/src/web_application.gleam:
--------------------------------------------------------------------------------
1 | import lustre
2 | import shared_view
3 | import sketch/lustre as sketch_lustre
4 |
5 | pub fn main() {
6 | let assert Ok(stylesheet) = sketch_lustre.setup()
7 | shared_view.app(stylesheet)
8 | |> lustre.start("#app", Nil)
9 | }
10 |
--------------------------------------------------------------------------------
/landing_page/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | nodeLinker: node-modules
2 |
--------------------------------------------------------------------------------
/landing_page/README.md:
--------------------------------------------------------------------------------
1 | # landing_page
2 |
3 | [](https://hex.pm/packages/landing_page)
4 | [](https://hexdocs.pm/landing_page/)
5 |
6 | ```sh
7 | gleam add landing_page@1
8 | ```
9 | ```gleam
10 | import landing_page
11 |
12 | pub fn main() {
13 | // TODO: An example of the project in use
14 | }
15 | ```
16 |
17 | Further documentation can be found at .
18 |
19 | ## Development
20 |
21 | ```sh
22 | gleam run # Run the project
23 | gleam test # Run the tests
24 | ```
25 |
--------------------------------------------------------------------------------
/landing_page/gleam.toml:
--------------------------------------------------------------------------------
1 | name = "landing_page"
2 | version = "1.0.0"
3 | target = "javascript"
4 |
5 | # Fill out these fields if you intend to generate HTML documentation or publish
6 | # your project to the Hex package manager.
7 | #
8 | # description = ""
9 | # licences = ["Apache-2.0"]
10 | # repository = { type = "github", user = "", repo = "" }
11 | # links = [{ title = "Website", href = "" }]
12 | #
13 | # For a full reference of all the available options, you can have a look at
14 | # https://gleam.run/writing-gleam/gleam-toml/.
15 |
16 | [dependencies]
17 | gleam_stdlib = ">= 0.34.0 and < 2.0.0"
18 | redraw = ">= 2.0.0 and < 3.0.0"
19 | redraw_dom = ">= 2.0.0 and < 3.0.0"
20 | sketch = {path = "../sketch"}
21 | sketch_redraw = {path = "../sketch_redraw"}
22 |
23 | [dev-dependencies]
24 | gleeunit = ">= 1.0.0 and < 2.0.0"
25 |
--------------------------------------------------------------------------------
/landing_page/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Sketch 🎨
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/landing_page/manifest.toml:
--------------------------------------------------------------------------------
1 | # This file was generated by Gleam
2 | # You typically do not need to edit this file
3 |
4 | packages = [
5 | { name = "gleam_erlang", version = "0.33.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "A1D26B80F01901B59AABEE3475DD4C18D27D58FA5C897D922FCB9B099749C064" },
6 | { name = "gleam_javascript", version = "0.13.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "F98328FCF573DA6F3A35D7F6CB3F9FF19FD5224CCBA9151FCBEAA0B983AF2F58" },
7 | { name = "gleam_otp", version = "0.16.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "FA0EB761339749B4E82D63016C6A18C4E6662DA05BAB6F1346F9AF2E679E301A" },
8 | { name = "gleam_stdlib", version = "0.52.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "50703862DF26453B277688FFCDBE9DD4AC45B3BD9742C0B370DB62BC1629A07D" },
9 | { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" },
10 | { name = "redraw", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_javascript", "gleam_stdlib"], otp_app = "redraw", source = "hex", outer_checksum = "FF52D8626E1E6DC92EB8BC9DC8C70BC6F0E25824524A7C0658222EA406B5BE23" },
11 | { name = "redraw_dom", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "redraw"], otp_app = "redraw_dom", source = "hex", outer_checksum = "8318DA1E428B349177C444DDC2FA9AE0D33E0DD0CC5A55B82F030811FFD69EA4" },
12 | { name = "sketch", version = "4.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], source = "local", path = "../sketch" },
13 | { name = "sketch_redraw", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "redraw", "redraw_dom", "sketch"], source = "local", path = "../sketch_redraw" },
14 | ]
15 |
16 | [requirements]
17 | gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" }
18 | gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
19 | redraw = { version = ">= 2.0.0 and < 3.0.0" }
20 | redraw_dom = { version = ">= 2.0.0 and < 3.0.0" }
21 | sketch = { path = "../sketch" }
22 | sketch_redraw = { path = "../sketch_redraw" }
23 |
--------------------------------------------------------------------------------
/landing_page/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "landing_page",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "preview": "vite preview"
10 | },
11 | "devDependencies": {
12 | "@chouqueth/gleam": "^1.6.3",
13 | "@vitejs/plugin-react-swc": "^3.7.1",
14 | "vite": "^5.4.8",
15 | "vite-gleam": "^0.4.3"
16 | },
17 | "packageManager": "yarn@4.5.0",
18 | "dependencies": {
19 | "@gleam-lang/highlight.js-gleam": "^1.5.0",
20 | "highlight.js": "^11.10.0",
21 | "react": "^18.3.1",
22 | "react-dom": "^18.3.1"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/landing_page/public/FiraCode.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ghivert/sketch/539f3110f85d813cf93652e6d3fba96f6ffd0cae/landing_page/public/FiraCode.ttf
--------------------------------------------------------------------------------
/landing_page/public/Lexend.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ghivert/sketch/539f3110f85d813cf93652e6d3fba96f6ffd0cae/landing_page/public/Lexend.ttf
--------------------------------------------------------------------------------
/landing_page/public/atom.css:
--------------------------------------------------------------------------------
1 | .hljs {
2 | display: block;
3 | overflow-x: auto;
4 | padding: 0.5em;
5 |
6 | color: #24292e;
7 | background: #ffffff;
8 | }
9 |
10 | .hljs-comment,
11 | .hljs-punctuation {
12 | color: #6a737d;
13 | }
14 |
15 | .hljs-attr,
16 | .hljs-attribute,
17 | .hljs-meta,
18 | .hljs-selector-attr,
19 | .hljs-selector-class,
20 | .hljs-selector-id {
21 | color: #005cc5;
22 | }
23 |
24 | .hljs-variable,
25 | .hljs-literal,
26 | .hljs-number,
27 | .hljs-doctag {
28 | color: #e36209;
29 | }
30 |
31 | .hljs-params {
32 | }
33 |
34 | .hljs-function {
35 | color: #6f42c1;
36 | }
37 |
38 | .hljs-class,
39 | .hljs-tag,
40 | .hljs-title,
41 | .hljs-built_in {
42 | color: #22863a;
43 | color: #005cc5;
44 | }
45 |
46 | .hljs-keyword,
47 | .hljs-type,
48 | .hljs-builtin-name,
49 | .hljs-meta-keyword,
50 | .hljs-template-tag,
51 | .hljs-template-variable {
52 | color: #d73a49;
53 | }
54 |
55 | .hljs-string,
56 | .hljs-undefined {
57 | color: #032f62;
58 | color: #22863a;
59 | }
60 |
61 | .hljs-regexp {
62 | color: #032f62;
63 | }
64 |
65 | .hljs-symbol {
66 | color: #005cc5;
67 | }
68 |
69 | .hljs-bullet {
70 | color: #e36209;
71 | }
72 |
73 | .hljs-section {
74 | color: #005cc5;
75 | font-weight: bold;
76 | }
77 |
78 | .hljs-quote,
79 | .hljs-name,
80 | .hljs-selector-tag,
81 | .hljs-selector-pseudo {
82 | color: #22863a;
83 | }
84 |
85 | .hljs-emphasis {
86 | color: #e36209;
87 | font-style: italic;
88 | }
89 |
90 | .hljs-strong {
91 | color: #e36209;
92 | font-weight: bold;
93 | }
94 |
95 | .hljs-deletion {
96 | color: #b31d28;
97 | background-color: #ffeef0;
98 | }
99 |
100 | .hljs-addition {
101 | color: #22863a;
102 | background-color: #f0fff4;
103 | }
104 |
105 | .hljs-link {
106 | color: #032f62;
107 | font-style: underline;
108 | }
109 |
--------------------------------------------------------------------------------
/landing_page/public/base.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'Lexend';
3 | src: url('./Lexend.ttf') format('truetype');
4 | font-weight: 100 900;
5 | font-style: normal;
6 | }
7 |
8 | @font-face {
9 | font-family: 'Fira Code';
10 | src: url('FiraCode.ttf') format('truetype');
11 | font-weight: 300 700;
12 | font-style: normal;
13 | }
14 |
15 | code {
16 | font-family: 'Fira Code';
17 | font-feature-settings: 'calt' 1; /* Enable ligatures for IE 10+, Edge */
18 | text-rendering: optimizeLegibility; /* Force ligatures for Webkit, Blink, Gecko */
19 | white-space: pre;
20 | line-height: 1.5;
21 | font-size: 0.8rem;
22 | }
23 |
24 | :root {
25 | --background: #ffffff;
26 | --dark-background: #eeeeee;
27 | --border-color: #cccccc;
28 | --button-hover: #f5f5f5;
29 | --text-color: #000000;
30 | --text-grey: #888888;
31 | --icon-hover: #000000;
32 | --navbar-background: rgba(250, 250, 250, 0.9);
33 |
34 | --window-border: rgb(223, 234, 255);
35 | --window-bg: rgb(241, 246, 255);
36 | }
37 |
38 | @media (prefers-color-scheme: dark) {
39 | :root {
40 | --background: #222222;
41 | --dark-background: #000000;
42 | --border-color: #444444;
43 | --button-hover: #0a0a0a;
44 | --text-color: #ffffff;
45 | --text-grey: #888888;
46 | --icon-hover: #ffffff;
47 | --navbar-background: rgba(25, 25, 25, 0.9);
48 |
49 | --window-bg: rgb(45, 46, 50);
50 | --window-border: rgb(109, 110, 112);
51 | }
52 | }
53 |
54 | body {
55 | background: var(--background);
56 | color: var(--text-color);
57 | }
58 |
--------------------------------------------------------------------------------
/landing_page/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ghivert/sketch/539f3110f85d813cf93652e6d3fba96f6ffd0cae/landing_page/public/logo.png
--------------------------------------------------------------------------------
/landing_page/public/reset.css:
--------------------------------------------------------------------------------
1 | /* http://meyerweb.com/eric/tools/css/reset/
2 | v2.0 | 20110126
3 | License: none (public domain)
4 | */
5 |
6 | html,
7 | body,
8 | div,
9 | span,
10 | applet,
11 | object,
12 | iframe,
13 | h1,
14 | h2,
15 | h3,
16 | h4,
17 | h5,
18 | h6,
19 | p,
20 | blockquote,
21 | pre,
22 | a,
23 | abbr,
24 | acronym,
25 | address,
26 | big,
27 | cite,
28 | code,
29 | del,
30 | dfn,
31 | em,
32 | img,
33 | ins,
34 | kbd,
35 | q,
36 | s,
37 | samp,
38 | small,
39 | strike,
40 | strong,
41 | sub,
42 | sup,
43 | tt,
44 | var,
45 | b,
46 | u,
47 | i,
48 | center,
49 | dl,
50 | dt,
51 | dd,
52 | ol,
53 | ul,
54 | li,
55 | fieldset,
56 | form,
57 | label,
58 | legend,
59 | table,
60 | caption,
61 | tbody,
62 | tfoot,
63 | thead,
64 | tr,
65 | th,
66 | td,
67 | article,
68 | aside,
69 | canvas,
70 | details,
71 | embed,
72 | figure,
73 | figcaption,
74 | footer,
75 | header,
76 | hgroup,
77 | menu,
78 | nav,
79 | output,
80 | ruby,
81 | section,
82 | summary,
83 | time,
84 | mark,
85 | audio,
86 | video {
87 | margin: 0;
88 | padding: 0;
89 | border: 0;
90 | font-size: 100%;
91 | font: inherit;
92 | vertical-align: baseline;
93 | }
94 |
95 | /* HTML5 display-role reset for older browsers */
96 | article,
97 | aside,
98 | details,
99 | figcaption,
100 | figure,
101 | footer,
102 | header,
103 | hgroup,
104 | menu,
105 | nav,
106 | section {
107 | display: block;
108 | }
109 |
110 | body {
111 | line-height: 1;
112 | }
113 |
114 | ol,
115 | ul {
116 | list-style: none;
117 | }
118 |
119 | blockquote,
120 | q {
121 | quotes: none;
122 | }
123 |
124 | blockquote:before,
125 | blockquote:after,
126 | q:before,
127 | q:after {
128 | content: '';
129 | content: none;
130 | }
131 |
132 | table {
133 | border-collapse: collapse;
134 | border-spacing: 0;
135 | }
136 |
137 | svg {
138 | max-height: 100%;
139 | max-width: 100%;
140 | }
141 |
142 | hr {
143 | margin-block-start: 0;
144 | margin-block-end: 0;
145 | margin-inline-start: 0;
146 | margin-inline-end: 0;
147 | border: none;
148 | }
149 |
150 | button {
151 | flex-shrink: 0;
152 | font-family: inherit;
153 | font-size: inherit;
154 | padding-block: 0px;
155 | padding-inline: 0px;
156 | text-align: left;
157 | appearance: none;
158 | border: none;
159 | background: inherit;
160 | cursor: pointer;
161 | }
162 |
163 | * {
164 | box-sizing: border-box;
165 |
--------------------------------------------------------------------------------
/landing_page/src/components/button.gleam:
--------------------------------------------------------------------------------
1 | import redraw/dom/attribute as a
2 | import redraw/dom/html as h
3 | import sketch/css
4 | import sketch/css/length.{px, rem}
5 | import sketch/redraw/dom/html as sh
6 |
7 | fn primary_class() {
8 | css.class([
9 | css.display("block"),
10 | css.text_decoration("none"),
11 | css.background("var(--dark-background)"),
12 | css.border_radius(px(6)),
13 | css.padding_("9px 16px"),
14 | css.font_size(rem(1.0)),
15 | css.color("inherit"),
16 | ])
17 | }
18 |
19 | pub fn primary(attributes, content: String) {
20 | primary_class()
21 | |> sh.button(attributes, [h.text(content)])
22 | }
23 |
24 | pub type Color {
25 | Red
26 | Orange
27 | Green
28 | }
29 |
30 | pub fn example(color, text) {
31 | let background = case color {
32 | Red -> "rgb(255, 95, 87)"
33 | Orange -> "rgb(254, 188, 46)"
34 | Green -> "rgb(40, 202, 65)"
35 | }
36 | css.class([
37 | css.background(background),
38 | css.color("white"),
39 | css.border_radius(px(8)),
40 | css.transition("all .3s"),
41 | css.border("none"),
42 | css.appearance("none"),
43 | css.font_family("inherit"),
44 | css.font_size(px(16)),
45 | css.padding(px(12)),
46 | css.font_weight("bold"),
47 | css.cursor("pointer"),
48 | css.min_width(px(200)),
49 | css.text_align("center"),
50 | css.hover([css.opacity(0.7)]),
51 | ])
52 | |> sh.button([], [h.text(text)])
53 | }
54 |
55 | pub fn link(link, content) {
56 | css.class([
57 | css.compose(primary_class()),
58 | css.background("var(--background)"),
59 | css.border("1px solid var(--border-color)"),
60 | ])
61 | |> sh.a([a.href(link)], [h.text(content)])
62 | }
63 |
--------------------------------------------------------------------------------
/landing_page/src/components/copy_button.gleam:
--------------------------------------------------------------------------------
1 | import ffi
2 | import gleam/bool
3 | import icons
4 | import redraw
5 | import redraw/dom/events
6 | import redraw/dom/html as h
7 | import sketch/css
8 | import sketch/css/length.{px, rem}
9 | import sketch/redraw/dom/html as sh
10 |
11 | pub fn copy_button() {
12 | use #(text) <- redraw.component_("CopyButton")
13 | let #(copied, on_copy) = use_copy(text)
14 | sh.code(code_install(), [on_copy], [
15 | h.text(text),
16 | sh.button(sm_button_class(), [on_copy], [
17 | icons.tiny(case copied {
18 | True -> icons.check()
19 | False -> icons.copy()
20 | }),
21 | ]),
22 | ])
23 | }
24 |
25 | fn use_copy(text: String) {
26 | let #(copied, set_copied) = redraw.use_state(False)
27 | use_copied_timeout(copied, set_copied)
28 | #(copied, on_copy(text, set_copied))
29 | }
30 |
31 | fn use_copied_timeout(copied: Bool, set_copied: fn(Bool) -> Nil) -> Nil {
32 | use <- redraw.use_effect_(_, #(copied))
33 | use <- bool.guard(when: !copied, return: fn() { Nil })
34 | let timeout = ffi.set_timeout(fn() { set_copied(False) }, 2000)
35 | fn() { ffi.clear_timeout(timeout) }
36 | }
37 |
38 | fn on_copy(text, set_copied) {
39 | use _ <- events.on_click
40 | ffi.clipboard_copy(text)
41 | set_copied(True)
42 | }
43 |
44 | fn code_install() {
45 | css.class([
46 | css.border("1px solid var(--border-color)"),
47 | css.border_radius(px(8)),
48 | css.display("flex"),
49 | css.align_items("center"),
50 | css.padding(px(2)),
51 | css.padding_left(px(8)),
52 | css.gap(px(9)),
53 | css.font_size(rem(0.7)),
54 | css.font_weight("450"),
55 | css.background("var(--background)"),
56 | css.cursor("pointer"),
57 | css.color("var(--text-color)"),
58 | css.hover([css.background("var(--button-hover)")]),
59 | ])
60 | }
61 |
62 | fn sm_button_class() {
63 | css.class([
64 | css.background("var(--dark-background)"),
65 | css.border_radius(px(6)),
66 | css.padding(px(4)),
67 | css.color("inherit"),
68 | ])
69 | }
70 |
71 | pub fn title(text) {
72 | css.class([css.font_size(rem(1.8)), css.font_weight("600")])
73 | |> sh.h3([], [h.text(text)])
74 | }
75 |
--------------------------------------------------------------------------------
/landing_page/src/components/footer.gleam:
--------------------------------------------------------------------------------
1 | import redraw
2 | import redraw/dom/attribute as a
3 | import redraw/dom/html as h
4 | import sketch/css
5 | import sketch/css/length.{px, rem}
6 | import sketch/redraw/dom/html as sh
7 |
8 | pub fn footer() {
9 | use <- redraw.component__("Footer")
10 | footer_([], [
11 | column([
12 | footer_details([h.text("Made with 💜 at Chou Corp.")]),
13 | footer_details([
14 | h.text("With the help of "),
15 | link("https://gaspardbuffet.com", "Gaspard Buffet"),
16 | ]),
17 | ]),
18 | ])
19 | }
20 |
21 | fn footer_(attributes, children) {
22 | css.class([
23 | css.margin_top(px(120)),
24 | css.padding(px(36)),
25 | css.display("flex"),
26 | css.justify_content("center"),
27 | ])
28 | |> sh.div(attributes, children)
29 | }
30 |
31 | fn footer_details(children) {
32 | css.class([
33 | css.font_size(rem(0.8)),
34 | css.line_height("1.4"),
35 | css.font_weight("500"),
36 | css.color("var(--text-grey)"),
37 | ])
38 | |> sh.div([], children)
39 | }
40 |
41 | fn column(children) {
42 | css.class([
43 | css.display("flex"),
44 | css.flex_direction("column"),
45 | css.align_items("center"),
46 | ])
47 | |> sh.div([], children)
48 | }
49 |
50 | fn link(href, content) {
51 | css.class([css.color("#ffaff3")])
52 | |> sh.a([a.href(href)], [h.text(content)])
53 | }
54 |
--------------------------------------------------------------------------------
/landing_page/src/components/navbar.gleam:
--------------------------------------------------------------------------------
1 | import icons
2 | import redraw
3 | import redraw/dom/attribute as a
4 | import redraw/dom/html as h
5 | import sketch/css
6 | import sketch/css/length.{px}
7 | import sketch/redraw/dom/html as sh
8 |
9 | pub fn navbar() {
10 | use <- redraw.component__("Navbar")
11 | nav([a.id("navbar")], [
12 | // icons.small(icons.home()),
13 | h.div([], []),
14 | sh.div(css.class([css.display("flex"), css.gap(px(24))]), [], [
15 | external_icon("https://hexdocs.pm/sketch", icons.book_open()),
16 | external_icon("https://github.com/ghivert/sketch", icons.github()),
17 | ]),
18 | ])
19 | }
20 |
21 | pub fn nav(attributes, children) {
22 | css.class([
23 | css.display("flex"),
24 | css.justify_content("space-between"),
25 | css.padding(px(18)),
26 | css.margin(px(18)),
27 | css.gap(px(36)),
28 | css.background("var(--navbar-background)"),
29 | css.position("sticky"),
30 | css.border_radius(px(10)),
31 | css.top(px(18)),
32 | css.border("1px solid var(--dark-background)"),
33 | css.backdrop_filter("blur(8px)"),
34 | ])
35 | |> sh.nav(attributes, children)
36 | }
37 |
38 | fn external_icon(url, icon) {
39 | css.class([
40 | css.color("#aaa"),
41 | css.transition("all .3s"),
42 | css.hover([css.color("var(--text-color)")]),
43 | ])
44 | |> sh.a([a.href(url)], [icons.small(icon)])
45 | }
46 |
--------------------------------------------------------------------------------
/landing_page/src/components/windows.gleam:
--------------------------------------------------------------------------------
1 | import ffi
2 | import redraw/dom/attribute as a
3 | import redraw/dom/html as h
4 | import sketch/css
5 | import sketch/css/length.{percent, px}
6 | import sketch/redraw/dom/html as sh
7 |
8 | pub fn scaffold(children) {
9 | css.class([
10 | css.background("var(--window-bg)"),
11 | css.border_radius(px(10)),
12 | css.border("1px solid var(--window-border)"),
13 | css.overflow("hidden"),
14 | css.flex("1"),
15 | css.display("flex"),
16 | css.flex_direction("column"),
17 | css.max_height(px(400)),
18 | css.max_width(percent(100)),
19 | ])
20 | |> sh.div([], children)
21 | }
22 |
23 | pub fn render(children) {
24 | css.class([
25 | css.background("var(--window-bg)"),
26 | css.border_radius(px(10)),
27 | css.border("1px solid var(--window-border)"),
28 | css.overflow("hidden"),
29 | css.flex("1"),
30 | css.margin(px(24)),
31 | ])
32 | |> sh.div([], children)
33 | }
34 |
35 | pub fn menu_bar(children) {
36 | css.class([])
37 | |> sh.div([], children)
38 | }
39 |
40 | pub fn traffic_lights() {
41 | css.class([
42 | css.display("flex"),
43 | css.gap(px(5)),
44 | css.padding(px(10)),
45 | css.border_bottom("1px solid var(--window-border)"),
46 | ])
47 | |> sh.div([], [
48 | traffic_light(Red),
49 | traffic_light(Orange),
50 | traffic_light(Green),
51 | ])
52 | }
53 |
54 | type Traffic {
55 | Red
56 | Orange
57 | Green
58 | }
59 |
60 | fn traffic_light(color) {
61 | let color = case color {
62 | Red -> "rgb(255, 95, 87)"
63 | Orange -> "rgb(254, 188, 46)"
64 | Green -> "rgb(40, 202, 65)"
65 | }
66 | css.class([
67 | css.width(px(10)),
68 | css.height(px(10)),
69 | css.background(color),
70 | css.border_radius(px(5)),
71 | ])
72 | |> sh.div([], [])
73 | }
74 |
75 | pub fn editor(content) {
76 | let content = a.inner_html(ffi.highlight(content))
77 | css.class([
78 | css.background("var(--background)"),
79 | css.padding(px(10)),
80 | css.display("flex"),
81 | css.overflow("auto"),
82 | css.flex_grow(1),
83 | ])
84 | |> sh.div([], [h.code([a.dangerously_set_inner_html(content)], [])])
85 | }
86 |
87 | pub fn css(content) {
88 | let content = a.inner_html(ffi.highlight_css(content))
89 | css.class([
90 | css.background("var(--background)"),
91 | css.padding(px(10)),
92 | css.display("flex"),
93 | css.max_height(px(400)),
94 | css.overflow("auto"),
95 | css.flex_grow(1),
96 | ])
97 | |> sh.div([], [h.code([a.dangerously_set_inner_html(content)], [])])
98 | }
99 |
100 | pub fn html(content) {
101 | let content = a.inner_html(ffi.highlight_xml(content))
102 | css.class([
103 | css.background("var(--background)"),
104 | css.padding(px(10)),
105 | css.display("flex"),
106 | css.max_height(px(400)),
107 | css.overflow("auto"),
108 | css.flex_grow(1),
109 | ])
110 | |> sh.div([], [h.code([a.dangerously_set_inner_html(content)], [])])
111 | }
112 |
--------------------------------------------------------------------------------
/landing_page/src/ffi.gleam:
--------------------------------------------------------------------------------
1 | pub type Timeout
2 |
3 | @external(javascript, "./landing_page.ffi.mjs", "setTimeout")
4 | pub fn set_timeout(callback: fn() -> Nil, timeout: Int) -> Timeout
5 |
6 | @external(javascript, "./landing_page.ffi.mjs", "clearTimeout")
7 | pub fn clear_timeout(timeout: Timeout) -> Nil
8 |
9 | @external(javascript, "./landing_page.ffi.mjs", "clipboardCopy")
10 | pub fn clipboard_copy(text: String) -> Nil
11 |
12 | @external(javascript, "./landing_page.ffi.mjs", "highlight")
13 | pub fn highlight(code: String) -> String
14 |
15 | @external(javascript, "./landing_page.ffi.mjs", "highlightCss")
16 | pub fn highlight_css(code: String) -> String
17 |
18 | @external(javascript, "./landing_page.ffi.mjs", "highlightXml")
19 | pub fn highlight_xml(code: String) -> String
20 |
21 | @external(javascript, "./landing_page.ffi.mjs", "scrollTo")
22 | pub fn scroll_to(id: String) -> Nil
23 |
--------------------------------------------------------------------------------
/landing_page/src/icons.gleam:
--------------------------------------------------------------------------------
1 | import icons/book_open
2 | import icons/check
3 | import icons/copy
4 | import icons/github
5 | import icons/home
6 | import sketch/css
7 | import sketch/css/length.{px}
8 | import sketch/redraw/dom/html as h
9 |
10 | pub fn small(icon) {
11 | css.class([css.width(px(24)), css.height(px(24))])
12 | |> h.div([], [icon])
13 | }
14 |
15 | pub fn tiny(icon) {
16 | css.class([css.width(px(12)), css.height(px(12))])
17 | |> h.div([], [icon])
18 | }
19 |
20 | pub const book_open = book_open.icon
21 |
22 | pub const check = check.icon
23 |
24 | pub const copy = copy.icon
25 |
26 | pub const github = github.icon
27 |
28 | pub const home = home.icon
29 |
--------------------------------------------------------------------------------
/landing_page/src/icons/book_open.gleam:
--------------------------------------------------------------------------------
1 | import redraw/dom/attribute as a
2 | import redraw/dom/svg
3 |
4 | const content = ""
5 |
6 | pub fn icon() {
7 | svg.svg(
8 | [
9 | a.style([#("max-width", "100%"), #("max-height", "100%")]),
10 | a.attribute("xmlns", "http://www.w3.org/2000/svg"),
11 | a.attribute("viewBox", "0 0 256 256"),
12 | a.attribute("fill", "currentColor"),
13 | a.dangerously_set_inner_html(a.inner_html(content)),
14 | ],
15 | [],
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/landing_page/src/icons/check.gleam:
--------------------------------------------------------------------------------
1 | import redraw/dom/attribute as a
2 | import redraw/dom/svg
3 |
4 | const content = ""
5 |
6 | pub fn icon() {
7 | svg.svg(
8 | [
9 | a.style([#("max-width", "100%"), #("max-height", "100%")]),
10 | a.attribute("xmlns", "http://www.w3.org/2000/svg"),
11 | a.attribute("viewBox", "0 0 256 256"),
12 | a.attribute("fill", "currentColor"),
13 | a.dangerously_set_inner_html(a.inner_html(content)),
14 | ],
15 | [],
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/landing_page/src/icons/copy.gleam:
--------------------------------------------------------------------------------
1 | import redraw/dom/attribute as a
2 | import redraw/dom/svg
3 |
4 | const content = ""
5 |
6 | pub fn icon() {
7 | svg.svg(
8 | [
9 | a.style([#("max-width", "100%"), #("max-height", "100%")]),
10 | a.attribute("xmlns", "http://www.w3.org/2000/svg"),
11 | a.attribute("viewBox", "0 0 256 256"),
12 | a.attribute("fill", "currentColor"),
13 | a.dangerously_set_inner_html(a.inner_html(content)),
14 | ],
15 | [],
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/landing_page/src/icons/github.gleam:
--------------------------------------------------------------------------------
1 | import redraw/dom/attribute as a
2 | import redraw/dom/svg
3 |
4 | const content = ""
5 |
6 | pub fn icon() {
7 | svg.svg(
8 | [
9 | a.style([#("max-width", "100%"), #("max-height", "100%")]),
10 | a.attribute("xmlns", "http://www.w3.org/2000/svg"),
11 | a.attribute("viewBox", "0 0 256 256"),
12 | a.attribute("fill", "currentColor"),
13 | a.dangerously_set_inner_html(a.inner_html(content)),
14 | ],
15 | [],
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/landing_page/src/icons/home.gleam:
--------------------------------------------------------------------------------
1 | import redraw/dom/attribute as a
2 | import redraw/dom/svg
3 |
4 | const content = ""
5 |
6 | pub fn icon() {
7 | svg.svg(
8 | [
9 | a.style([#("max-width", "100%"), #("max-height", "100%")]),
10 | a.attribute("xmlns", "http://www.w3.org/2000/svg"),
11 | a.attribute("viewBox", "0 0 256 256"),
12 | a.attribute("fill", "currentColor"),
13 | a.dangerously_set_inner_html(a.inner_html(content)),
14 | ],
15 | [],
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/landing_page/src/landing_page.ffi.mjs:
--------------------------------------------------------------------------------
1 | import hljs from 'highlight.js/lib/core'
2 |
3 | export function setTimeout(...args) {
4 | return window.setTimeout(...args)
5 | }
6 |
7 | export function clearTimeout(...args) {
8 | return window.clearTimeout(...args)
9 | }
10 |
11 | export function clipboardCopy(text) {
12 | return navigator.clipboard.writeText(text)
13 | }
14 |
15 | export function highlight(code) {
16 | return hljs.highlight(code, { language: 'gleam' }).value
17 | }
18 |
19 | export function highlightCss(code) {
20 | return hljs.highlight(code, { language: 'css' }).value
21 | }
22 |
23 | export function highlightXml(code) {
24 | return hljs.highlight(code, { language: 'xml' }).value
25 | }
26 |
27 | export function scrollTo(id) {
28 | const node = document.getElementById(id)
29 | const nav = document.getElementById('navbar')
30 | if (!node || !nav) return
31 | const nodeRect = node.getBoundingClientRect()
32 | const navRect = nav.getBoundingClientRect()
33 | const top = nodeRect.top - navRect.bottom - 18
34 | window.scrollTo({ top, behavior: 'smooth' })
35 | }
36 |
--------------------------------------------------------------------------------
/landing_page/src/landing_page.main.mjs:
--------------------------------------------------------------------------------
1 | import gleamHljs from '@gleam-lang/highlight.js-gleam'
2 | import hljs from 'highlight.js/lib/core'
3 | import plaintext from 'highlight.js/lib/languages/plaintext'
4 | import css from 'highlight.js/lib/languages/css'
5 | import xml from 'highlight.js/lib/languages/xml'
6 | import * as landing from './landing_page.gleam'
7 |
8 | hljs.registerLanguage('gleam', gleamHljs)
9 | hljs.registerLanguage('plaintext', plaintext)
10 | hljs.registerLanguage('css', css)
11 | hljs.registerLanguage('xml', xml)
12 | landing.main()
13 |
--------------------------------------------------------------------------------
/landing_page/src/layout.gleam:
--------------------------------------------------------------------------------
1 | import redraw/dom/attribute as a
2 | import redraw/dom/html as h
3 | import sketch/css
4 | import sketch/css/length.{percent, px, rem}
5 | import sketch/css/media
6 | import sketch/redraw/dom/html as sh
7 |
8 | fn max_width() {
9 | css.class([
10 | css.max_width(px(1500)),
11 | css.margin_("auto"),
12 | css.width(percent(100)),
13 | css.padding(px(36)),
14 | css.display("inherit"),
15 | css.flex_direction("inherit"),
16 | css.gap_("inherit"),
17 | ])
18 | }
19 |
20 | pub fn width_container(children) {
21 | sh.div(max_width(), [], children)
22 | }
23 |
24 | pub fn title_container(children) {
25 | css.class([css.compose(max_width()), css.gap(px(36)), css.display("flex")])
26 | |> sh.h1([], children)
27 | }
28 |
29 | pub fn title_container_inside(children) {
30 | css.class([css.gap(px(36))])
31 | |> sh.div([], children)
32 | }
33 |
34 | pub fn title(text) {
35 | css.class([
36 | css.line_height("1.6"),
37 | css.font_size(rem(2.0)),
38 | css.font_weight("600"),
39 | ])
40 | |> sh.h2([], [h.text(text)])
41 | }
42 |
43 | pub fn main_title(text) {
44 | css.class([
45 | css.line_height("1.6"),
46 | css.font_size(rem(2.0)),
47 | css.font_weight("600"),
48 | ])
49 | |> sh.div([], [h.text(text)])
50 | }
51 |
52 | pub fn row(attributes, children) {
53 | css.class([
54 | css.display("flex"),
55 | css.gap(px(12)),
56 | css.align_items("end"),
57 | css.flex_wrap("wrap"),
58 | ])
59 | |> sh.div(attributes, children)
60 | }
61 |
62 | pub fn column(attributes, children) {
63 | css.class([
64 | css.display("flex"),
65 | css.gap(px(12)),
66 | css.align_items("start"),
67 | css.flex_direction("column"),
68 | ])
69 | |> sh.div(attributes, children)
70 | }
71 |
72 | pub fn body_container(attributes, children) {
73 | css.class([
74 | css.line_height("1.4"),
75 | css.max_width(px(700)),
76 | css.display("flex"),
77 | css.flex_direction("column"),
78 | css.gap(px(9)),
79 | ])
80 | |> sh.div(attributes, children)
81 | }
82 |
83 | pub fn section(id, background, children) {
84 | css.class([
85 | css.background(background),
86 | css.display("flex"),
87 | css.flex_direction("column"),
88 | css.gap(px(36)),
89 | ])
90 | |> sh.section([a.id(id)], children)
91 | }
92 |
93 | pub fn section_explanation(attributes, children) {
94 | css.class([css.max_width(px(400)), css.line_height("1.4")])
95 | |> sh.div(attributes, children)
96 | }
97 |
98 | pub fn windows_row(children) {
99 | css.class([
100 | css.display("flex"),
101 | css.gap(px(36)),
102 | css.max_width(px(1000)),
103 | css.flex("1"),
104 | css.media(media.max_width(px(800)), [
105 | css.flex_direction("column"),
106 | css.align_items("center"),
107 | ]),
108 | ])
109 | |> sh.div([], children)
110 | }
111 |
112 | pub fn windows_wrapper(breakpoint, children) {
113 | css.class([
114 | css.display("flex"),
115 | css.flex_direction("row"),
116 | css.gap(px(36)),
117 | css.media(media.max_width(breakpoint), [css.flex_direction("column")]),
118 | ])
119 | |> sh.div([], children)
120 | }
121 |
122 | pub fn buttons_row(children) {
123 | css.class([
124 | css.display("flex"),
125 | css.gap(px(12)),
126 | css.height(percent(100)),
127 | css.flex_direction("column"),
128 | css.align_items("center"),
129 | css.justify_content("center"),
130 | css.media(media.max_width(px(800)), [css.padding(px(36))]),
131 | ])
132 | |> sh.div([], children)
133 | }
134 |
--------------------------------------------------------------------------------
/landing_page/test/landing_page_test.gleam:
--------------------------------------------------------------------------------
1 | import gleeunit
2 | import gleeunit/should
3 |
4 | pub fn main() {
5 | gleeunit.main()
6 | }
7 |
8 | // gleeunit test functions end in `_test`
9 | pub fn hello_world_test() {
10 | 1
11 | |> should.equal(1)
12 | }
13 |
--------------------------------------------------------------------------------
/landing_page/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import gleam from 'vite-gleam'
3 | import react from '@vitejs/plugin-react-swc'
4 |
5 | export default defineConfig(({}) => ({
6 | plugins: [gleam(), react()],
7 | build: {
8 | sourcemap: true,
9 | rollupOptions: {
10 | output: {
11 | interop: 'auto',
12 | },
13 | },
14 | },
15 | }))
16 |
--------------------------------------------------------------------------------
/sketch/LICENCE:
--------------------------------------------------------------------------------
1 | Copyright 2024-2025 Guillaume Hivert
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/sketch/birdie_snapshots/erlang_aliased_css.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: erlang_aliased_css
4 | ---
5 | .css-79177302 {
6 | background: red;
7 | }
--------------------------------------------------------------------------------
/sketch/birdie_snapshots/erlang_css_test.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: erlang_css_test
4 | file: ./test/sketch_test.gleam
5 | test_name: erlang_css_test
6 | ---
7 | .css-8772105 {
8 | appearance: none;
9 | background: none;
10 | border: 1px solid black;
11 | font-family: inherit;
12 | font-size: inherit;
13 | }
14 |
15 | .css-6675211 {
16 | background: red;
17 | }
18 |
19 | .css-6675211:hover {
20 | background: blue;
21 | }
22 |
23 | .css-6675211:hover > .css-8772105 {
24 | background: red;
25 | font-family: Verdana;
26 | }
27 |
28 | .css-6675211 > .css-8772105 {
29 | background: blue !important;
30 | font-size: 2.0rem;
31 | }
32 |
33 | @media (max-width: 700.0px) {
34 | .css-6675211 {
35 | background: yellow;
36 | }
37 | }
--------------------------------------------------------------------------------
/sketch/birdie_snapshots/erlang_dimensions_css.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: erlang_dimensions_css
4 | ---
5 | .css-25186BDA {
6 | padding: 12.0px;
7 | margin: 12.0px;
8 | transform: rotate(1.0rad) skew(1.0deg, 2.0deg);
9 | }
--------------------------------------------------------------------------------
/sketch/birdie_snapshots/erlang_edges_css.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: erlang_edges_css
4 | ---
5 | .css-95E3D239 {
6 | grid-template-areas: "header"
7 | "main";
8 | --example-property: example-value;
9 | color: blue;
10 | }
--------------------------------------------------------------------------------
/sketch/birdie_snapshots/erlang_exposed_css.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: erlang_exposed_css
4 | ---
5 | .css-132390646 {
6 | background: red;
7 | color: red;
8 | }
--------------------------------------------------------------------------------
/sketch/birdie_snapshots/erlang_exposed_property.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: erlang_exposed_property
4 | ---
5 | .css-132390646 {
6 | background: red;
7 | color: red;
8 | }
--------------------------------------------------------------------------------
/sketch/birdie_snapshots/erlang_font_face_css.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: erlang_font_face_css
4 | ---
5 | @font-face {
6 | src: file;
7 | font-family: Example;
8 | font-style: bold;
9 | }
--------------------------------------------------------------------------------
/sketch/birdie_snapshots/erlang_function_css.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: erlang_function_css
4 | ---
5 | .css-89700632 {
6 | background: #ddd;
7 | background: red;
8 | display: block;
9 | color: red;
10 | background: green;
11 | }
--------------------------------------------------------------------------------
/sketch/birdie_snapshots/erlang_function_property.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: erlang_function_property
4 | ---
5 | .css-19844900 {
6 | width: 12px;
7 | color: blue;
8 | color: red;
9 | }
--------------------------------------------------------------------------------
/sketch/birdie_snapshots/erlang_important_css.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: erlang_important_css
4 | ---
5 | .css-830510F3 {
6 | background: red;
7 | color: red;
8 | padding: 12.0px;
9 | color: blue;
10 | background: #ccc !important;
11 | }
--------------------------------------------------------------------------------
/sketch/birdie_snapshots/erlang_keyframe_class.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: erlang_keyframe_class
4 | ---
5 | @keyframes fade-out {
6 | from {
7 | opacity: 1.0;
8 | }
9 |
10 | 50% {
11 | opacity: 0.5;
12 | }
13 |
14 | to {
15 | opacity: 0.0;
16 | }
17 | }
18 |
19 | .css-4B5C1AF5 {
20 | opacity: 1.0;
21 | animation: fade-out;
22 | background: blue;
23 | }
--------------------------------------------------------------------------------
/sketch/birdie_snapshots/erlang_keyframe_css.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: erlang_keyframe_css
4 | ---
5 | @keyframes fade-out {
6 | from {
7 | opacity: 1.0;
8 | }
9 |
10 | 50% {
11 | opacity: 0.5;
12 | }
13 |
14 | to {
15 | opacity: 0.0;
16 | }
17 | }
--------------------------------------------------------------------------------
/sketch/birdie_snapshots/erlang_medias_and.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: erlang_medias_and
4 | ---
5 | .css-6ABB5AEF {
6 | background: blue;
7 | }
8 |
9 | @media (max-width: 700.0px) and (min-width: 600.0px) {
10 | .css-6ABB5AEF {
11 | background: red;
12 | background: blue;
13 | }
14 | }
--------------------------------------------------------------------------------
/sketch/birdie_snapshots/erlang_medias_and_or.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: erlang_medias_and_or
4 | ---
5 | .css-6B949287 {
6 | background: blue;
7 | }
8 |
9 | @media (max-width: 700.0px) and (min-width: 700.0px) or (min-width: 600.0px) {
10 | .css-6B949287 {
11 | background: red;
12 | background: blue;
13 | }
14 | }
--------------------------------------------------------------------------------
/sketch/birdie_snapshots/erlang_medias_or.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: erlang_medias_or
4 | ---
5 | .css-52978292 {
6 | background: blue;
7 | }
8 |
9 | @media (max-width: 700.0px) or (min-width: 600.0px) {
10 | .css-52978292 {
11 | background: red;
12 | background: blue;
13 | }
14 | }
--------------------------------------------------------------------------------
/sketch/birdie_snapshots/erlang_medias_pseudo_class.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: erlang_medias_pseudo_class
4 | ---
5 | .css-178B08CF {
6 | background: blue;
7 | }
8 |
9 | @media (max-width: 700.0px) {
10 | .css-178B08CF {
11 | background: red;
12 | background: blue;
13 | }
14 | .css-178B08CF:hover {
15 | background: green;
16 | }
17 | }
--------------------------------------------------------------------------------
/sketch/birdie_snapshots/erlang_medias_simple.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: erlang_medias_simple
4 | ---
5 | .css-10DAB2C4 {
6 | background: blue;
7 | }
8 |
9 | @media (max-width: 700.0px) {
10 | .css-10DAB2C4 {
11 | background: red;
12 | background: blue;
13 | }
14 | }
--------------------------------------------------------------------------------
/sketch/birdie_snapshots/erlang_nestings_css.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: erlang_nestings_css
4 | ---
5 | .css-786A9099 {
6 | color: blue;
7 | background: green;
8 | padding: 12.0px;
9 | }
--------------------------------------------------------------------------------
/sketch/birdie_snapshots/erlang_nestings_example.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: erlang_nestings_example
4 | ---
5 | .css-01AC2C66 {
6 | }
7 |
8 | .css-01AC2C66 > .css-786A9099 {
9 | background: red;
10 | }
11 |
12 | .css-01AC2C66 > .css-786A9099:hover {
13 | background: blue;
14 | }
15 |
16 | @media (max-width: 700.0px) or (min-width: 400.0px) {
17 | .css-01AC2C66 {
18 | background: blue;
19 | }
20 | .css-01AC2C66:hover {
21 | background: red;
22 | }
23 | .css-01AC2C66:hover {
24 | }
25 | .css-01AC2C66:hover > .css-786A9099 {
26 | background: blue;
27 | }
28 | .css-01AC2C66:hover > .css-786A9099:hover {
29 | background: red;
30 | }
31 | }
32 |
33 | .css-786A9099 {
34 | color: blue;
35 | background: green;
36 | padding: 12.0px;
37 | }
--------------------------------------------------------------------------------
/sketch/birdie_snapshots/erlang_variable_css.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: erlang_variable_css
4 | ---
5 | .css-79177302 {
6 | background: red;
7 | }
--------------------------------------------------------------------------------
/sketch/birdie_snapshots/js_aliased_css.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: js_aliased_css
4 | ---
5 | .css-2610672850 {
6 | background: red;
7 | }
--------------------------------------------------------------------------------
/sketch/birdie_snapshots/js_css_test.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: js_css_test
4 | file: ./test/sketch_test.gleam
5 | test_name: js_css_test
6 | ---
7 | .css-187874422 {
8 | background: red;
9 | }
10 |
11 | .css-187874422:hover {
12 | background: blue;
13 | }
14 |
15 | .css-187874422:hover > .css-902835160 {
16 | background: red;
17 | font-family: Verdana;
18 | }
19 |
20 | .css-187874422 > .css-902835160 {
21 | background: blue !important;
22 | font-size: 2.0rem;
23 | }
24 |
25 | @media (max-width: 700.0px) {
26 | .css-187874422 {
27 | background: yellow;
28 | }
29 | }
30 |
31 | .css-902835160 {
32 | appearance: none;
33 | background: none;
34 | border: 1px solid black;
35 | font-family: inherit;
36 | font-size: inherit;
37 | }
--------------------------------------------------------------------------------
/sketch/birdie_snapshots/js_dimensions_css.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: js_dimensions_css
4 | ---
5 | .css-4D527D56 {
6 | padding: 12.0px;
7 | margin: 12.0px;
8 | transform: rotate(1.0rad) skew(1.0deg, 2.0deg);
9 | }
--------------------------------------------------------------------------------
/sketch/birdie_snapshots/js_edges_css.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: js_edges_css
4 | ---
5 | .css-00143995 {
6 | grid-template-areas: "header"
7 | "main";
8 | --example-property: example-value;
9 | color: blue;
10 | }
--------------------------------------------------------------------------------
/sketch/birdie_snapshots/js_exposed_css.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: js_exposed_css
4 | ---
5 | .css-63449814 {
6 | background: red;
7 | color: red;
8 | }
--------------------------------------------------------------------------------
/sketch/birdie_snapshots/js_exposed_property.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: js_exposed_property
4 | ---
5 | .css-63449814 {
6 | background: red;
7 | color: red;
8 | }
--------------------------------------------------------------------------------
/sketch/birdie_snapshots/js_font_face_css.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: js_font_face_css
4 | ---
5 | @font-face {
6 | src: file;
7 | font-family: Example;
8 | font-style: bold;
9 | }
--------------------------------------------------------------------------------
/sketch/birdie_snapshots/js_function_css.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: js_function_css
4 | ---
5 | .css-3776639199 {
6 | background: #ddd;
7 | background: red;
8 | display: block;
9 | color: red;
10 | background: green;
11 | }
--------------------------------------------------------------------------------
/sketch/birdie_snapshots/js_function_property.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: js_function_property
4 | ---
5 | .css-2428701155 {
6 | width: 12px;
7 | color: blue;
8 | color: red;
9 | }
--------------------------------------------------------------------------------
/sketch/birdie_snapshots/js_important_css.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: js_important_css
4 | ---
5 | .css-CE56851B {
6 | background: red;
7 | color: red;
8 | padding: 12.0px;
9 | color: blue;
10 | background: #ccc !important;
11 | }
--------------------------------------------------------------------------------
/sketch/birdie_snapshots/js_keyframe_class.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: js_keyframe_class
4 | ---
5 | @keyframes fade-out {
6 | from {
7 | opacity: 1.0;
8 | }
9 |
10 | 50% {
11 | opacity: 0.5;
12 | }
13 |
14 | to {
15 | opacity: 0.0;
16 | }
17 | }
18 |
19 | .css-9264C90E {
20 | opacity: 1.0;
21 | animation: fade-out;
22 | background: blue;
23 | }
--------------------------------------------------------------------------------
/sketch/birdie_snapshots/js_keyframe_css.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: js_keyframe_css
4 | ---
5 | @keyframes fade-out {
6 | from {
7 | opacity: 1.0;
8 | }
9 |
10 | 50% {
11 | opacity: 0.5;
12 | }
13 |
14 | to {
15 | opacity: 0.0;
16 | }
17 | }
--------------------------------------------------------------------------------
/sketch/birdie_snapshots/js_medias_and.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: js_medias_and
4 | ---
5 | .css-A607E3E9 {
6 | background: blue;
7 | }
8 |
9 | @media (max-width: 700.0px) and (min-width: 600.0px) {
10 | .css-A607E3E9 {
11 | background: red;
12 | background: blue;
13 | }
14 | }
--------------------------------------------------------------------------------
/sketch/birdie_snapshots/js_medias_and_or.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: js_medias_and_or
4 | ---
5 | .css-120C23D0 {
6 | background: blue;
7 | }
8 |
9 | @media (max-width: 700.0px) and (min-width: 700.0px) or (min-width: 600.0px) {
10 | .css-120C23D0 {
11 | background: red;
12 | background: blue;
13 | }
14 | }
--------------------------------------------------------------------------------
/sketch/birdie_snapshots/js_medias_or.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: js_medias_or
4 | ---
5 | .css-A4452C7C {
6 | background: blue;
7 | }
8 |
9 | @media (max-width: 700.0px) or (min-width: 600.0px) {
10 | .css-A4452C7C {
11 | background: red;
12 | background: blue;
13 | }
14 | }
--------------------------------------------------------------------------------
/sketch/birdie_snapshots/js_medias_pseudo_class.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: js_medias_pseudo_class
4 | ---
5 | .css-2B46C00D {
6 | background: blue;
7 | }
8 |
9 | @media (max-width: 700.0px) {
10 | .css-2B46C00D {
11 | background: red;
12 | background: blue;
13 | }
14 | .css-2B46C00D:hover {
15 | background: green;
16 | }
17 | }
--------------------------------------------------------------------------------
/sketch/birdie_snapshots/js_medias_simple.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: js_medias_simple
4 | ---
5 | .css-FE6057D1 {
6 | background: blue;
7 | }
8 |
9 | @media (max-width: 700.0px) {
10 | .css-FE6057D1 {
11 | background: red;
12 | background: blue;
13 | }
14 | }
--------------------------------------------------------------------------------
/sketch/birdie_snapshots/js_nestings_css.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: js_nestings_css
4 | ---
5 | .css-3A1178AC {
6 | color: blue;
7 | background: green;
8 | padding: 12.0px;
9 | }
--------------------------------------------------------------------------------
/sketch/birdie_snapshots/js_nestings_example.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: js_nestings_example
4 | ---
5 | .css-C2B72822 {
6 | }
7 |
8 | .css-C2B72822 > .css-3A1178AC {
9 | background: red;
10 | }
11 |
12 | .css-C2B72822 > .css-3A1178AC:hover {
13 | background: blue;
14 | }
15 |
16 | @media (max-width: 700.0px) or (min-width: 400.0px) {
17 | .css-C2B72822 {
18 | background: blue;
19 | }
20 | .css-C2B72822:hover {
21 | background: red;
22 | }
23 | .css-C2B72822:hover {
24 | }
25 | .css-C2B72822:hover > .css-3A1178AC {
26 | background: blue;
27 | }
28 | .css-C2B72822:hover > .css-3A1178AC:hover {
29 | background: red;
30 | }
31 | }
32 |
33 | .css-3A1178AC {
34 | color: blue;
35 | background: green;
36 | padding: 12.0px;
37 | }
--------------------------------------------------------------------------------
/sketch/birdie_snapshots/js_variable_css.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: js_variable_css
4 | ---
5 | .css-2610672850 {
6 | background: red;
7 | }
--------------------------------------------------------------------------------
/sketch/birdie_snapshots/run_globals_blue.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: run_globals_blue
4 | ---
5 | :root {
6 | color: blue;
7 | background: blue;
8 | margin: 0.0px;
9 | }
--------------------------------------------------------------------------------
/sketch/birdie_snapshots/run_globals_red.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: run_globals_red
4 | ---
5 | :root {
6 | color: red;
7 | background: blue;
8 | margin: 0.0px;
9 | }
--------------------------------------------------------------------------------
/sketch/gleam.toml:
--------------------------------------------------------------------------------
1 | name = "sketch"
2 | version = "4.1.0"
3 |
4 | description = "A CSS-in-Gleam package, foundation for CSS packages!"
5 | internal_modules = ["sketch/internals", "sketch/internals/*"]
6 | licences = ["MIT"]
7 |
8 | [[links]]
9 | title = "Sponsor"
10 | href = "https://github.com/sponsors/ghivert"
11 |
12 | [repository]
13 | type = "github"
14 | user = "ghivert"
15 | repo = "sketch"
16 | path = "sketch"
17 |
18 | [dependencies]
19 | gleam_erlang = ">= 0.25.0 and < 1.0.0"
20 | gleam_otp = ">= 0.10.0 and < 1.0.0"
21 | gleam_stdlib = ">= 0.42.0 and < 1.0.0"
22 | murmur3a = ">= 1.0.0 and < 2.0.0"
23 |
24 | [dev-dependencies]
25 | birdie = ">= 1.2.5 and < 2.0.0"
26 | startest = ">= 0.6.0 and < 1.0.0"
27 |
--------------------------------------------------------------------------------
/sketch/src/sketch.ffi.mjs:
--------------------------------------------------------------------------------
1 | let id = 0
2 | export function uniqueId() {
3 | return id++
4 | }
5 |
--------------------------------------------------------------------------------
/sketch/src/sketch/css/angle.gleam:
--------------------------------------------------------------------------------
1 | //// `Angle` defines a [CSS Unit](https://developer.mozilla.org/docs/Web/CSS/CSS_Values_and_Units).
2 | //// It represents an angle value expressed in degrees, gradians, radians, or turns.
3 | //// It is used, for example, in ``s and in some `transform` functions.
4 | ////
5 | //// ---
6 | ////
7 | //// [MDN Reference](https://developer.mozilla.org/docs/Web/CSS/angle)
8 |
9 | import gleam/float
10 |
11 | /// `Angle` defines a [CSS Unit](https://developer.mozilla.org/docs/Web/CSS/CSS_Values_and_Units).
12 | /// It represents an angle value expressed in degrees, gradians, radians, or turns.
13 | /// It is used, for example, in ``s and in some `transform` functions.
14 | ///
15 | /// ---
16 | ///
17 | /// [MDN Reference](https://developer.mozilla.org/docs/Web/CSS/angle)
18 | pub opaque type Angle {
19 | Deg(Float)
20 | Rad(Float)
21 | Grad(Float)
22 | Turn(Float)
23 | }
24 |
25 | /// Represents an angle in [degrees](https://en.wikipedia.org/wiki/Degree_%28angle%29).
26 | /// One full circle is `360deg`. Examples: `0deg`, `90deg`, `14.23deg`.
27 | ///
28 | /// ---
29 | ///
30 | /// [MDN Reference](https://developer.mozilla.org/docs/Web/CSS/angle#deg)
31 | pub fn deg(value: Float) -> Angle {
32 | Deg(value)
33 | }
34 |
35 | /// Represents an angle in [radians](https://en.wikipedia.org/wiki/Radian).
36 | /// One full circle is 2π radians which approximates to `6.2832rad`. `1rad` is
37 | /// 180/π degrees. Examples: `0rad`, 1`.0708rad`, `6.2832rad`.
38 | ///
39 | /// ---
40 | ///
41 | /// [MDN Reference](https://developer.mozilla.org/docs/Web/CSS/angle#rad)
42 | pub fn rad(value: Float) -> Angle {
43 | Rad(value)
44 | }
45 |
46 | /// Represents an angle in [gradians](https://en.wikipedia.org/wiki/Gradian).
47 | /// One full circle is `400grad`. Examples: `0grad`, `100grad`, `38.8grad`.
48 | ///
49 | /// ---
50 | ///
51 | /// [MDN Reference](https://developer.mozilla.org/docs/Web/CSS/angle#grad)
52 | pub fn grad(value: Float) -> Angle {
53 | Grad(value)
54 | }
55 |
56 | /// Represents an angle in a number of turns. One full circle is `1turn`.
57 | /// Examples: `0turn`, `0.25turn`, `1.2turn`.
58 | ///
59 | /// ---
60 | ///
61 | /// [MDN Reference](https://developer.mozilla.org/docs/Web/CSS/angle#turn)
62 | pub fn turn(value: Float) -> Angle {
63 | Turn(value)
64 | }
65 |
66 | @internal
67 | pub fn to_string(angle: Angle) -> String {
68 | case angle {
69 | Deg(value) -> float.to_string(value) <> "deg"
70 | Rad(value) -> float.to_string(value) <> "rad"
71 | Grad(value) -> float.to_string(value) <> "grad"
72 | Turn(value) -> float.to_string(value) <> "turn"
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/sketch/src/sketch/css/keyframe.gleam:
--------------------------------------------------------------------------------
1 | //// The `@keyframes` CSS [at-rule](https://developer.mozilla.org/en-US/docs/Web/CSS/At-rule)
2 | //// controls the intermediate steps in a CSS animation sequence by defining
3 | //// styles for keyframes (or waypoints) along the animation sequence. This
4 | //// gives more control over the intermediate steps of the animation sequence
5 | //// than [transitions](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_transitions).
6 | ////
7 | //// ---
8 | ////
9 | //// [MDN Reference](https://developer.mozilla.org/docs/Web/CSS/@keyframes)
10 |
11 | import gleam/int
12 | import sketch/internals/cache/cache as style
13 |
14 | /// A keyframe is a part of an `@keyframes` rule.
15 | ///
16 | /// ---
17 | ///
18 | /// [MDN Reference](https://developer.mozilla.org/docs/Web/CSS/@keyframes)
19 | pub opaque type Keyframe {
20 | Keyframe(class: style.Class)
21 | }
22 |
23 | /// A starting offset of `0%`.
24 | ///
25 | /// ---
26 | ///
27 | /// [MDN Reference](https://developer.mozilla.org/docs/Web/CSS/@keyframes#from)
28 | pub fn from(styles: List(style.Style)) {
29 | Keyframe(style.named("from", styles))
30 | }
31 |
32 | /// An ending offset of `100%`.
33 | ///
34 | /// ---
35 | ///
36 | /// [MDN Reference](https://developer.mozilla.org/docs/Web/CSS/@keyframes#to)
37 | pub fn to(styles: List(style.Style)) {
38 | Keyframe(style.named("to", styles))
39 | }
40 |
41 | /// A percentage of the time through the animation sequence at which the
42 | /// specified keyframe should occur.
43 | ///
44 | /// ---
45 | ///
46 | /// [MDN Reference](https://developer.mozilla.org/docs/Web/CSS/@keyframes#percentage)
47 | pub fn at(percentage: Int, styles: List(style.Style)) {
48 | Keyframe(style.named(int.to_string(percentage) <> "%", styles))
49 | }
50 |
51 | /// Internal function, can be used if you need to go from a keyframe to a String
52 | /// in case you're building on top of sketch.
53 | @internal
54 | pub fn class(keyframe: Keyframe) -> style.Class {
55 | keyframe.class
56 | }
57 |
--------------------------------------------------------------------------------
/sketch/src/sketch/error.gleam:
--------------------------------------------------------------------------------
1 | //// Defines standard error returned by sketch.
2 |
3 | import gleam/otp/actor
4 |
5 | /// Standard Sketch Error. Used on BEAM target. JS target never fails.
6 | pub type SketchError {
7 | OtpError(actor.StartError)
8 | }
9 |
--------------------------------------------------------------------------------
/sketch/src/sketch/internals/cache/actor.gleam:
--------------------------------------------------------------------------------
1 | //// BEAM only.
2 |
3 | @target(erlang)
4 | import gleam/bool
5 | @target(erlang)
6 | import gleam/erlang/process.{type Subject}
7 | @target(erlang)
8 | import gleam/list
9 | @target(erlang)
10 | import gleam/otp/actor
11 | @target(erlang)
12 | import gleam/pair
13 | @target(erlang)
14 | import gleam/result
15 | @target(erlang)
16 | import sketch/error.{type SketchError}
17 | @target(erlang)
18 | import sketch/internals/cache/cache
19 |
20 | @target(erlang)
21 | /// Manages the styles. Can be instanciated with [`create_cache`](#create_cache).
22 | pub opaque type Cache {
23 | Persistent(proc: Subject(Request))
24 | Ephemeral(cache: cache.Cache)
25 | }
26 |
27 | @target(erlang)
28 | pub fn ephemeral() {
29 | Ephemeral(cache: cache.new())
30 | }
31 |
32 | @target(erlang)
33 | pub fn persistent() -> Result(Cache, SketchError) {
34 | cache.new()
35 | |> actor.start(loop)
36 | |> result.map(Persistent)
37 | |> result.map_error(error.OtpError)
38 | }
39 |
40 | @target(erlang)
41 | pub fn render(cache: Cache) -> String {
42 | case cache {
43 | Ephemeral(cache:) -> cache.render_sheet(cache)
44 | Persistent(proc:) ->
45 | process.try_call(proc, Render, 1000)
46 | |> result.replace_error(Nil)
47 | |> result.unwrap("")
48 | }
49 | }
50 |
51 | @target(erlang)
52 | pub fn class_name(class: cache.Class, cache: Cache) -> #(Cache, String) {
53 | case cache {
54 | Ephemeral(cache:) ->
55 | cache.class_name(class, cache)
56 | |> pair.map_first(Ephemeral)
57 | Persistent(proc:) -> {
58 | use <- bool.guard(when: list.is_empty(class.content), return: #(cache, ""))
59 | process.try_call(proc, Fetch(class, _), within: 100)
60 | |> result.unwrap("")
61 | |> pair.new(cache, _)
62 | }
63 | }
64 | }
65 |
66 | @target(erlang)
67 | pub fn at_rule(rule: cache.AtRule, cache: Cache) -> Cache {
68 | case cache {
69 | Ephemeral(cache:) -> Ephemeral(cache.at_rule(rule, cache))
70 | Persistent(proc:) -> {
71 | let _ = process.try_call(proc, Push(rule, _), within: 100)
72 | cache
73 | }
74 | }
75 | }
76 |
77 | @target(erlang)
78 | pub type Request {
79 | Render(response: Subject(String))
80 | Fetch(class: cache.Class, response: Subject(String))
81 | Push(rule: cache.AtRule, response: Subject(Nil))
82 | }
83 |
84 | @target(erlang)
85 | pub fn loop(msg: Request, cache: cache.Cache) -> actor.Next(a, cache.Cache) {
86 | case msg {
87 | Render(response:) -> {
88 | process.send(response, cache.render_sheet(cache))
89 | actor.continue(cache)
90 | }
91 | Fetch(class:, response:) -> {
92 | let #(cache, class_name) = cache.class_name(class, cache)
93 | process.send(response, class_name)
94 | actor.continue(cache)
95 | }
96 | Push(rule:, response:) -> {
97 | let cache = cache.at_rule(rule, cache)
98 | process.send(response, Nil)
99 | actor.continue(cache)
100 | }
101 | }
102 | }
103 |
104 | @target(erlang)
105 | pub fn dispose(cache: Cache) {
106 | case cache {
107 | Ephemeral(_) -> Nil
108 | Persistent(proc:) -> {
109 | process.subject_owner(proc)
110 | |> process.send_exit
111 | }
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/sketch/src/sketch/internals/string.gleam:
--------------------------------------------------------------------------------
1 | import gleam/option.{type Option}
2 | import gleam/string
3 |
4 | pub fn indent(indent: Int) -> String {
5 | string.repeat(" ", indent)
6 | }
7 |
8 | pub fn wrap_class(
9 | id: String,
10 | properties: List(String),
11 | indentation: Int,
12 | pseudo: Option(String),
13 | ) -> String {
14 | let base_indent = indent(indentation)
15 | let pseudo_ = option.unwrap(pseudo, "")
16 | [base_indent <> id <> pseudo_ <> " {", ..properties]
17 | |> string.join("\n")
18 | |> string.append("\n" <> base_indent <> "}")
19 | }
20 |
--------------------------------------------------------------------------------
/sketch/test/classes/dimensions_css.gleam:
--------------------------------------------------------------------------------
1 | import sketch/css
2 | import sketch/css/angle.{deg}
3 | import sketch/css/length.{px}
4 | import sketch/css/transform
5 |
6 | /// Multiple dimensions, with exposings or not.
7 | pub fn dimensions_variables() {
8 | css.class([
9 | css.padding(px(12)),
10 | css.margin(length.px(12)),
11 | css.transform([
12 | transform.rotate(angle.rad(1.0)),
13 | transform.skew(deg(1.0), deg(2.0)),
14 | ]),
15 | ])
16 | }
17 |
--------------------------------------------------------------------------------
/sketch/test/classes/edges_css.gleam:
--------------------------------------------------------------------------------
1 | import sketch/css
2 |
3 | pub fn edge_cases(custom) {
4 | css.class([
5 | css.grid_template_areas(["header", "main"]),
6 | css.property("--example-property", "example-value"),
7 | css.color(custom),
8 | ])
9 | }
10 |
--------------------------------------------------------------------------------
/sketch/test/classes/font_face_css.gleam:
--------------------------------------------------------------------------------
1 | import sketch/css
2 | import sketch/css/font_face
3 |
4 | pub fn font_face() {
5 | css.font_face([
6 | font_face.src("file"),
7 | font_face.font_family("Example"),
8 | font_face.font_style("bold"),
9 | ])
10 | }
11 |
--------------------------------------------------------------------------------
/sketch/test/classes/globals_css.gleam:
--------------------------------------------------------------------------------
1 | import sketch/css
2 | import sketch/css/length.{px}
3 |
4 | pub fn root(color: String) -> css.Global {
5 | css.global(":root", [
6 | css.color(color),
7 | css.background("blue"),
8 | css.margin(px(0)),
9 | ])
10 | }
11 |
--------------------------------------------------------------------------------
/sketch/test/classes/important_css.gleam:
--------------------------------------------------------------------------------
1 | import sketch/css
2 | import sketch/css/length
3 |
4 | pub fn important() {
5 | css.class([
6 | css.compose(content()),
7 | css.color("blue"),
8 | css.background("#ccc") |> css.important,
9 | ])
10 | }
11 |
12 | fn content() {
13 | css.class([
14 | css.background("red"),
15 | css.color("red"),
16 | css.padding(length.px(12)),
17 | ])
18 | }
19 |
--------------------------------------------------------------------------------
/sketch/test/classes/keyframe_css.gleam:
--------------------------------------------------------------------------------
1 | import sketch/css
2 | import sketch/css/keyframe
3 |
4 | pub fn keyframe() {
5 | css.keyframes("fade-out", [
6 | keyframe.from([css.opacity(1.0)]),
7 | keyframe.at(50, [css.opacity(0.5)]),
8 | keyframe.to([css.opacity(0.0)]),
9 | ])
10 | }
11 |
12 | pub fn example() {
13 | css.class([
14 | css.opacity(1.0),
15 | css.animation("fade-out"),
16 | css.background("blue"),
17 | ])
18 | }
19 |
--------------------------------------------------------------------------------
/sketch/test/classes/medias_css.gleam:
--------------------------------------------------------------------------------
1 | import sketch/css
2 | import sketch/css/length.{px}
3 | import sketch/css/media
4 |
5 | pub fn simple() {
6 | css.class([
7 | css.background("blue"),
8 | css.media(media.max_width(px(700)), [
9 | css.background("red"),
10 | css.background("blue"),
11 | ]),
12 | ])
13 | }
14 |
15 | pub fn and() {
16 | css.class([
17 | css.background("blue"),
18 | css.media(media.max_width(px(700)) |> media.and(media.min_width(px(600))), [
19 | css.background("red"),
20 | css.background("blue"),
21 | ]),
22 | ])
23 | }
24 |
25 | pub fn or() {
26 | css.class([
27 | css.background("blue"),
28 | css.media(media.max_width(px(700)) |> media.or(media.min_width(px(600))), [
29 | css.background("red"),
30 | css.background("blue"),
31 | ]),
32 | ])
33 | }
34 |
35 | pub fn and_or() {
36 | css.class([
37 | css.background("blue"),
38 | css.media(
39 | media.max_width(px(700))
40 | |> media.and(media.min_width(px(700)))
41 | |> media.or(media.min_width(px(600))),
42 | [css.background("red"), css.background("blue")],
43 | ),
44 | ])
45 | }
46 |
47 | pub fn pseudo_class() {
48 | css.class([
49 | css.background("blue"),
50 | css.media(media.max_width(px(700)), [
51 | css.background("red"),
52 | css.background("blue"),
53 | css.hover([css.background("green")]),
54 | ]),
55 | ])
56 | }
57 |
--------------------------------------------------------------------------------
/sketch/test/classes/nestings_css.gleam:
--------------------------------------------------------------------------------
1 | import sketch/css
2 | import sketch/css/length.{px}
3 | import sketch/css/media
4 |
5 | pub fn example(custom) {
6 | css.class([
7 | css.media(media.or(media.max_width(px(700)), media.min_width(px(400))), [
8 | css.background("blue"),
9 | css.hover([css.background("red")]),
10 | css.hover([
11 | css.child(content(custom), [
12 | css.background("blue"),
13 | css.hover([css.background("red")]),
14 | ]),
15 | ]),
16 | ]),
17 | css.child(content(custom), [
18 | css.background("red"),
19 | css.hover([css.background("blue")]),
20 | ]),
21 | ])
22 | }
23 |
24 | pub fn content(custom) {
25 | css.class([
26 | css.color(custom),
27 | css.background("green"),
28 | css.padding(length.px(12)),
29 | ])
30 | }
31 |
--------------------------------------------------------------------------------
/sketch/test/sketch_test.gleam:
--------------------------------------------------------------------------------
1 | import startest
2 |
3 | pub fn main() {
4 | startest.default_config()
5 | |> startest.run
6 | }
7 |
--------------------------------------------------------------------------------
/sketch/test/sketch_test/class.gleam:
--------------------------------------------------------------------------------
1 | import birdie
2 | import classes/globals_css
3 | import gleam/string
4 | import sketch
5 | import sketch_test/helpers
6 | import startest.{describe, it}
7 | import startest/expect
8 |
9 | import classes/dimensions_css
10 | import classes/edges_css
11 | import classes/font_face_css
12 | import classes/important_css
13 | import classes/keyframe_css
14 | import classes/medias_css
15 | import classes/nestings_css
16 |
17 | pub fn sketch_tests() {
18 | describe("Sketch", [
19 | describe("Class generation", [
20 | it("should handle dimensions", fn() {
21 | dimensions_css.dimensions_variables()
22 | |> helpers.compute_class("dimensions_css")
23 | }),
24 | it("should handle edges cases", fn() {
25 | edges_css.edge_cases("blue")
26 | |> helpers.compute_class("edges_css")
27 | }),
28 | it("should handle font-face rules", fn() {
29 | font_face_css.font_face()
30 | |> helpers.compute_at_rule("font_face_css")
31 | }),
32 | it("should handle important properties", fn() {
33 | important_css.important()
34 | |> helpers.compute_class("important_css")
35 | }),
36 | it("should handle keyframe rules", run_keyframes),
37 | it("should handle media queries", run_medias),
38 | it("should handle nestings properties", run_nestings),
39 | it("should handle global classes", run_globals),
40 | ]),
41 | ])
42 | }
43 |
44 | fn run_keyframes() {
45 | keyframe_css.keyframe() |> helpers.compute_at_rule("keyframe_css")
46 | let assert Ok(stylesheet) = sketch.stylesheet(strategy: sketch.Ephemeral)
47 | let stylesheet = sketch.at_rule(keyframe_css.keyframe(), stylesheet)
48 | let #(stylesheet, _) = sketch.class_name(keyframe_css.example(), stylesheet)
49 | let content = sketch.render(stylesheet)
50 | birdie.snap(title: helpers.multitarget_title("keyframe_class"), content:)
51 | Nil
52 | }
53 |
54 | fn run_medias() {
55 | medias_css.and() |> helpers.compute_class("medias_and")
56 | medias_css.and_or() |> helpers.compute_class("medias_and_or")
57 | medias_css.or() |> helpers.compute_class("medias_or")
58 | medias_css.pseudo_class() |> helpers.compute_class("medias_pseudo_class")
59 | medias_css.simple() |> helpers.compute_class("medias_simple")
60 | }
61 |
62 | fn run_nestings() {
63 | nestings_css.content("blue") |> helpers.compute_class("nestings_css")
64 | nestings_css.example("blue") |> helpers.compute_class("nestings_example")
65 | }
66 |
67 | fn run_globals() {
68 | let assert Ok(stylesheet) = sketch.stylesheet(strategy: sketch.Ephemeral)
69 | let stylesheet = sketch.global(stylesheet, globals_css.root("red"))
70 | let content = sketch.render(stylesheet)
71 | content |> string.contains(":root") |> expect.to_be_true
72 | birdie.snap(title: "run_globals_red", content:)
73 |
74 | let stylesheet = sketch.global(stylesheet, globals_css.root("blue"))
75 | let content = sketch.render(stylesheet)
76 | content |> string.contains(":root") |> expect.to_be_true
77 | birdie.snap(title: "run_globals_blue", content:)
78 |
79 | Nil
80 | }
81 |
--------------------------------------------------------------------------------
/sketch/test/sketch_test/dimensions/angle.gleam:
--------------------------------------------------------------------------------
1 | import sketch/css/angle
2 | import startest.{describe, it}
3 | import startest/expect
4 |
5 | pub fn sketch_tests() {
6 | describe("Sketch", [
7 | describe("CSS dimensions", [
8 | describe("angle", [
9 | it("should handle deg", fn() {
10 | angle.deg(10.0)
11 | |> expect_angle("10.0deg")
12 | }),
13 | it("should handle rad", fn() {
14 | angle.rad(10.0)
15 | |> expect_angle("10.0rad")
16 | }),
17 | it("should handle grad", fn() {
18 | angle.grad(10.0)
19 | |> expect_angle("10.0grad")
20 | }),
21 | it("should handle turn", fn() {
22 | angle.turn(10.0)
23 | |> expect_angle("10.0turn")
24 | }),
25 | ]),
26 | ]),
27 | ])
28 | }
29 |
30 | fn expect_angle(angle: angle.Angle, result: String) {
31 | angle
32 | |> angle.to_string
33 | |> expect.to_equal(result)
34 | }
35 |
--------------------------------------------------------------------------------
/sketch/test/sketch_test/helpers.gleam:
--------------------------------------------------------------------------------
1 | import birdie
2 | import gleam/string
3 | import sketch
4 | import sketch/css.{type Class}
5 | import startest/expect
6 |
7 | pub fn card_body(custom: String) -> Class {
8 | css.class([
9 | css.background("#ddd"),
10 | css.background("red"),
11 | css.display("block"),
12 | custom_color(custom),
13 | ])
14 | }
15 |
16 | pub fn custom_color(custom: String) -> css.Style {
17 | css.color(custom)
18 | }
19 |
20 | @target(erlang)
21 | pub fn multitarget_title(title: String) {
22 | "erlang_" <> title
23 | }
24 |
25 | @target(javascript)
26 | pub fn multitarget_title(title: String) {
27 | "js_" <> title
28 | }
29 |
30 | pub fn compute_class(class: css.Class, title: String) {
31 | let assert Ok(stylesheet) = sketch.stylesheet(strategy: sketch.Ephemeral)
32 | let #(stylesheet, class_name) = sketch.class_name(class, stylesheet)
33 | let content = sketch.render(stylesheet)
34 | content |> string.contains(class_name) |> expect.to_be_true
35 | birdie.snap(title: multitarget_title(title), content:)
36 | Nil
37 | }
38 |
39 | pub fn compute_at_rule(rule: css.AtRule, title: String) {
40 | let assert Ok(stylesheet) = sketch.stylesheet(strategy: sketch.Ephemeral)
41 | let stylesheet = sketch.at_rule(rule, stylesheet)
42 | let content = sketch.render(stylesheet)
43 | birdie.snap(title: multitarget_title(title), content:)
44 | Nil
45 | }
46 |
--------------------------------------------------------------------------------
/sketch/test/sketch_test/rules/font_face.gleam:
--------------------------------------------------------------------------------
1 | import sketch/css/font_face
2 | import startest.{describe, it}
3 | import startest/expect
4 |
5 | pub fn sketch_tests() {
6 | describe("Sketch", [
7 | describe("CSS at-rules", [
8 | describe("font-face", [
9 | it("should handle ascent-override", fn() {
10 | font_face.ascent_override(1.0)
11 | |> expect_font_face("ascent-override: 1.0%")
12 | }),
13 | it("should handle descent-override", fn() {
14 | font_face.descent_override(1.0)
15 | |> expect_font_face("descent-override: 1.0%")
16 | }),
17 | it("should handle font-display", fn() {
18 | font_face.font_display("example")
19 | |> expect_font_face("font-display: example")
20 | }),
21 | it("should handle font-family", fn() {
22 | font_face.font_family("example")
23 | |> expect_font_face("font-family: example")
24 | }),
25 | it("should handle font-stretch", fn() {
26 | font_face.font_stretch("example")
27 | |> expect_font_face("font-stretch: example")
28 | }),
29 | it("should handle font-style", fn() {
30 | font_face.font_style("example")
31 | |> expect_font_face("font-style: example")
32 | }),
33 | it("should handle font-weight", fn() {
34 | font_face.font_weight("example")
35 | |> expect_font_face("font-weight: example")
36 | }),
37 | it("should handle font-feature-settings", fn() {
38 | font_face.font_feature_settings("example")
39 | |> expect_font_face("font-feature-settings: example")
40 | }),
41 | it("should handle font-variation-settings", fn() {
42 | font_face.font_variation_settings("example")
43 | |> expect_font_face("font-variation-settings: example")
44 | }),
45 | it("should handle line-gap-override", fn() {
46 | font_face.line_gap_override(1.0)
47 | |> expect_font_face("line-gap-override: 1.0%")
48 | }),
49 | it("should handle size-adjust", fn() {
50 | font_face.size_adjust(1.0)
51 | |> expect_font_face("size-adjust: 1.0%")
52 | }),
53 | it("should handle src", fn() {
54 | font_face.src("example")
55 | |> expect_font_face("src: example")
56 | }),
57 | it("should handle unicode-range", fn() {
58 | font_face.unicode_range("example")
59 | |> expect_font_face("unicode-range: example")
60 | }),
61 | ]),
62 | ]),
63 | ])
64 | }
65 |
66 | fn expect_font_face(font_face: font_face.FontFace, result: String) {
67 | font_face
68 | |> font_face.to_string
69 | |> expect.to_equal(result)
70 | }
71 |
--------------------------------------------------------------------------------
/sketch/test/sketch_test/rules/media.gleam:
--------------------------------------------------------------------------------
1 | import sketch/css/length
2 | import sketch/css/media
3 | import startest.{describe, it}
4 | import startest/expect
5 |
6 | pub fn sketch_tests() {
7 | describe("Sketch", [
8 | describe("CSS at-rules", [
9 | describe("media", [
10 | it("it should handle max-width", fn() {
11 | media.max_width(length.px(10))
12 | |> expect_media("@media (max-width: 10.0px)")
13 | }),
14 | it("it should handle min-width", fn() {
15 | media.min_width(length.px(10))
16 | |> expect_media("@media (min-width: 10.0px)")
17 | }),
18 | it("it should handle max-height", fn() {
19 | media.max_height(length.px(10))
20 | |> expect_media("@media (max-height: 10.0px)")
21 | }),
22 | it("it should handle min-height", fn() {
23 | media.min_height(length.px(10))
24 | |> expect_media("@media (min-height: 10.0px)")
25 | }),
26 | it("it should handle dark_theme", fn() {
27 | media.dark_theme()
28 | |> expect_media("@media (prefers-color-scheme: dark)")
29 | }),
30 | it("it should handle light_theme", fn() {
31 | media.light_theme()
32 | |> expect_media("@media (prefers-color-scheme: light)")
33 | }),
34 | it("it should handle and", fn() {
35 | media.min_width(length.px(1))
36 | |> media.and(media.min_width(length.px(1)))
37 | |> expect_media("@media (min-width: 1.0px) and (min-width: 1.0px)")
38 | }),
39 | it("it should handle or", fn() {
40 | media.min_width(length.px(1))
41 | |> media.or(media.min_width(length.px(1)))
42 | |> expect_media("@media (min-width: 1.0px) or (min-width: 1.0px)")
43 | }),
44 | it("it should handle not", fn() {
45 | media.not(media.min_width(length.px(1)))
46 | |> expect_media("@media not (min-width: 1.0px)")
47 | }),
48 | it("it should handle landscape", fn() {
49 | media.landscape()
50 | |> expect_media("@media (orientation: landscape)")
51 | }),
52 | it("it should handle portrait", fn() {
53 | media.portrait()
54 | |> expect_media("@media (orientation: portrait)")
55 | }),
56 | it("it should handle screen", fn() {
57 | media.screen()
58 | |> expect_media("@media screen")
59 | }),
60 | it("it should handle print", fn() {
61 | media.print()
62 | |> expect_media("@media print")
63 | }),
64 | it("it should handle all", fn() {
65 | media.all()
66 | |> expect_media("@media all")
67 | }),
68 | it("it should handle only", fn() {
69 | media.only(media.all())
70 | |> expect_media("@media only all")
71 | }),
72 | ]),
73 | ]),
74 | ])
75 | }
76 |
77 | fn expect_media(media: media.Query, result: String) {
78 | media
79 | |> media.to_string
80 | |> expect.to_equal(result)
81 | }
82 |
--------------------------------------------------------------------------------
/sketch_css/.gitignore:
--------------------------------------------------------------------------------
1 | *.beam
2 | *.ez
3 | /build
4 | erl_crash.dump
5 | src/sketch/styles
6 | styles/
7 |
--------------------------------------------------------------------------------
/sketch_css/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## v2.0.0 - 2025-01-12
2 |
3 | Sketch CSS underwent a major rewrite. From a quick proof of concept drafted in a
4 | day to illustrate the abilities of Sketch, Sketch CSS is now completely
5 | reimplemented from scratch, with support for modern, up to date Gleam, with
6 | partial supports for "code evaluation".
7 |
8 | Sketch CSS now somewhat acts as an "interpreter". Instead of reading the code,
9 | and generating the corresponding CSS, Sketch CSS now traverse the sources, and
10 | execute some parts of the code, like an interpreter would do. This allows to
11 | have an whole vision of the Gleam AST, but also to perform variable
12 | interpretation, light computations, dependency detection, cyclic detection, and
13 | more!
14 |
15 | Sketch CSS marks the first real step of Sketch as a tool to generate CSS. New
16 | adventures await, with support for Lustre devtools, Vite, and such!
17 |
18 | ### Features
19 |
20 | - Sketch CSS reads `*_styles.gleam`, `*_css.gleam` & `*_sketch.gleam` files to
21 | generate CSS source files.
22 | - Sketch CSS configuration can be configured in CLI, with `sketch_css.toml`, or
23 | directly in `gleam.toml`.
24 |
--------------------------------------------------------------------------------
/sketch_css/LICENCE:
--------------------------------------------------------------------------------
1 | Copyright 2024-2025 Guillaume Hivert
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/sketch_css/README.md:
--------------------------------------------------------------------------------
1 | # Sketch CSS
2 |
3 | > In case you're here and don't know Sketch, take a look at
4 | > [`sketch` package](https://hexdocs.pm/sketch)!
5 |
6 | > This readme is a carbon-copy of the Sketch CSS section in `sketch` readme.
7 |
8 | Sketch CSS is a tool to generate CSS from Sketch Class definitions. Because pure
9 | CSS generation is straightforward, `sketch_css` does not need a cache to
10 | generate correct CSS files. Instead, `sketch_css` ships with a CLI tool, able to
11 | read your Gleam styles files, and output corresponding CSS automagically, while
12 | providing an abstraction layer written in Gleam, to make sure you're using the
13 | right classes! It's an other way to leverage Sketch core and enjoy the styling
14 | in Gleam, while taking advantage of all the static CSS power!
15 |
16 | To run the generator, you have to use the command
17 | `gleam run -m sketch/css generate` at the root of your project. By default,
18 | `sketch_css` will try to read all files named `*_styles.gleam`, `*_css.gleam`
19 | and `*_sketch.gleam` in your `src` folder, no matter where they are. You can put
20 | them at root, nested, or in a folder called `css`, `sketch_css` does not care!
21 | After fetching the styles files, `sketch_css` will output your generated CSS
22 | files in a `styles` folder, at the root of the project. They can then be served
23 | in the way you want. In the same time, `sketch_css` will output Gleam interfaces
24 | in `src/sketch/styles`, matching your styles files, to use in your project!
25 |
26 | ### Options
27 |
28 | Sketch CSS generation has strong defaults, but everything can be customised. To
29 | pass options to Sketch CSS, you have three ways:
30 |
31 | - Pass them directly on the CLI. Every option has its equivalent exposed in the
32 | CLI.
33 | - Write them in a `sketch_css.toml` file, at root of your project, next
34 | `gleam.toml`.
35 | - Write them directly in `gleam.toml`, under `[sketch_css]` section.
36 |
37 | Sketch CSS has 3 distinct options:
38 |
39 | - `--dest`, accepting a folder, relative to current directory. It defaults to
40 | `styles`.
41 | - `--src`, accepting a folder, relative to current directory. It defaults to
42 | `src`.
43 | - `--interface`, accepting a folder, relative to current directory. It defaults
44 | to `src/sketch/styles`.
45 |
46 | Write directly the folder, path resolution is done with current working
47 | directory as root.
48 |
49 | #### Examples
50 |
51 | ```sh
52 | gleam run -m sketch_css generate --src="src" --dest="styles" --interface="src/sketch/styles"
53 | ```
54 |
55 | ```toml
56 | # sketch_css.toml
57 | src = "src"
58 | dest = "styles"
59 | interface = "src/sketch/styles"
60 | ```
61 |
62 | ```toml
63 | # gleam.toml
64 | name = "name"
65 | version = "1.0.0"
66 |
67 | [sketch_css]
68 | src = "src"
69 | dst = "styles"
70 | interface = "src/sketch/styles"
71 |
72 | [dependencies]
73 | gleam_stdlib = ">= 0.34.0 and < 2.0.0"
74 | sketch = ">= 4.0.0 and < 5.0.0"
75 | sketch_css = ">= 2.0.0 and < 3.0.0"
76 |
77 | [dev-dependencies]
78 | gleeunit = ">= 1.0.0 and < 2.0.0"
79 | ```
80 |
81 | ### A note on generation algorithm
82 |
83 | Because a Sketch `Class` can be generated in multiple ways, and with variable,
84 | Sketch CSS takes that into account. Every simple Sketch `Class` will be iso
85 | generated in CSS, but every Sketch `Class` that contains variable will be
86 | generated with the variable taken into account! Sketch CSS being opinionated, it
87 | generates the class, with a CSS variable, letting you update it, override it,
88 | etc.
89 |
90 | Sketch CSS also acts as a basic interpreter. It means you can write basic
91 | constants or variables, and they will be taking into account. Be sure to write
92 | classes like you would do in CSS yet: Sketch CSS does not execute your
93 | functions!
94 |
95 | ### Example
96 |
97 | ```gleam
98 | // src/main_styles.gleam
99 | import sketch/css
100 |
101 | pub fn flexer() {
102 | let display = "flex"
103 | css.class([css.display(display)])
104 | }
105 |
106 | fn direction(flex_direction: String) {
107 | css.flex_direction(flex_direction)
108 | }
109 |
110 | pub fn flexer_direction(flex_direction: String) {
111 | css.class([
112 | css.compose(flexer()),
113 | direction(flex_direction),
114 | ])
115 | }
116 | ```
117 |
118 | ```css
119 | /* styles/main_styles.css */
120 | .main_styles-flexer {
121 | display: flex;
122 | }
123 |
124 | .main_styles-flexer_direction {
125 | display: flex;
126 | flex-direction: var(--flex-direction);
127 | }
128 | ```
129 |
130 | ```gleam
131 | // src/sketch/styles/main_styles.gleam
132 | pub const flexer = "main_styles-flexer"
133 |
134 | pub const flexer_direction = "main_styles-flexer_direction"
135 | ```
136 |
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/erlang_css_aliased_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: erlang_css_aliased_css.gleam
4 | ---
5 | .sketch_css_test-classes-aliased_css_aliased_module {
6 | background: red;
7 | }
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/erlang_css_dimensions_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: erlang_css_dimensions_css.gleam
4 | ---
5 | .sketch_css_test-classes-dimensions_css_dimensions_variables {
6 | padding: 12.0px;
7 | margin: 12.0px;
8 | transform: rotate(1.0rad) skew(1.0deg, 2.0deg);
9 | }
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/erlang_css_edges_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: erlang_css_edges_css.gleam
4 | ---
5 | .sketch_css_test-classes-edges_css_edge_cases {
6 | grid-template-areas: "header" "main";
7 | --example-property: example-value;
8 | color: var(--custom);
9 | }
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/erlang_css_exposed_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: erlang_css_exposed_css.gleam
4 | ---
5 | .sketch_css_test-classes-exposed_css_exposed_class {
6 | background: red;
7 | color: red;
8 | }
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/erlang_css_font_face_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: erlang_css_font_face_css.gleam
4 | ---
5 | @font-face {
6 | src: file;
7 | font-family: Example;
8 | font-style: bold;
9 | }
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/erlang_css_function_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: erlang_css_function_css.gleam
4 | ---
5 | .sketch_css_test-classes-function_css_class {
6 | background: #ddd;
7 | background: red;
8 | display: block;
9 | color: var(--custom);
10 | background: green;
11 | }
12 |
13 | .sketch_css_test-classes-function_css_property {
14 | width: 12px;
15 | color: blue;
16 | color: var(--custom);
17 | }
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/erlang_css_important_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: erlang_css_important_css.gleam
4 | ---
5 | .sketch_css_test-classes-important_css_important {
6 | color: blue;
7 | background: #ccc !important;
8 | }
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/erlang_css_keyframe_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: erlang_css_keyframe_css.gleam
4 | ---
5 | @keyframes fade-out {
6 | from {
7 | opacity: 1.0;
8 | }
9 |
10 | 50% {
11 | opacity: 0.5;
12 | }
13 |
14 | to {
15 | opacity: 0.0;
16 | }
17 | }
18 |
19 | .sketch_css_test-classes-keyframe_css_example {
20 | opacity: 1.0;
21 | animation: fade-out;
22 | background: blue;
23 | }
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/erlang_css_medias_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: erlang_css_medias_css.gleam
4 | ---
5 | .sketch_css_test-classes-medias_css_and {
6 | background: blue;
7 | }
8 |
9 | @media (max-width: 700.0px) and (min-width: 600.0px) {
10 | .sketch_css_test-classes-medias_css_and {
11 | background: red;
12 | background: blue;
13 | }
14 | }
15 |
16 | .sketch_css_test-classes-medias_css_and_or {
17 | background: blue;
18 | }
19 |
20 | @media (max-width: 700.0px) and (min-width: 700.0px) or (min-width: 600.0px) {
21 | .sketch_css_test-classes-medias_css_and_or {
22 | background: red;
23 | background: blue;
24 | }
25 | }
26 |
27 | .sketch_css_test-classes-medias_css_or {
28 | background: blue;
29 | }
30 |
31 | @media (max-width: 700.0px) or (min-width: 600.0px) {
32 | .sketch_css_test-classes-medias_css_or {
33 | background: red;
34 | background: blue;
35 | }
36 | }
37 |
38 | .sketch_css_test-classes-medias_css_pseudo_class {
39 | background: blue;
40 | }
41 |
42 | @media (max-width: 700.0px) {
43 | .sketch_css_test-classes-medias_css_pseudo_class {
44 | background: red;
45 | background: blue;
46 | }
47 | .sketch_css_test-classes-medias_css_pseudo_class:hover {
48 | background: green;
49 | }
50 | }
51 |
52 | .sketch_css_test-classes-medias_css_simple {
53 | background: blue;
54 | }
55 |
56 | @media (max-width: 700.0px) {
57 | .sketch_css_test-classes-medias_css_simple {
58 | background: red;
59 | background: blue;
60 | }
61 | }
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/erlang_css_nestings_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: erlang_css_nestings_css.gleam
4 | ---
5 | .sketch_css_test-classes-nestings_css_example {
6 | }
7 |
8 | .sketch_css_test-classes-nestings_css_example > .sketch_css_test-classes-nestings_css_content {
9 | background: red;
10 | }
11 |
12 | .sketch_css_test-classes-nestings_css_example > .sketch_css_test-classes-nestings_css_content:hover {
13 | background: blue;
14 | }
15 |
16 | @media (max-width: 700.0px) or (min-width: 400.0px) {
17 | .sketch_css_test-classes-nestings_css_example {
18 | background: blue;
19 | }
20 | .sketch_css_test-classes-nestings_css_example:hover {
21 | background: red;
22 | }
23 | .sketch_css_test-classes-nestings_css_example:hover {
24 | }
25 | .sketch_css_test-classes-nestings_css_example:hover > .sketch_css_test-classes-nestings_css_content {
26 | background: blue;
27 | }
28 | .sketch_css_test-classes-nestings_css_example:hover > .sketch_css_test-classes-nestings_css_content:hover {
29 | background: red;
30 | }
31 | }
32 |
33 | .sketch_css_test-classes-nestings_css_content {
34 | color: var(--custom);
35 | background: green;
36 | padding: 12.0px;
37 | }
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/erlang_css_variable_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: erlang_css_variable_css.gleam
4 | ---
5 | .sketch_css_test-classes-variable_css_variable_property {
6 | background: red;
7 | }
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/erlang_gleam_aliased_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: erlang_gleam_aliased_css.gleam
4 | ---
5 | pub const aliased_module = "sketch_css_test-classes-aliased_css_aliased_module"
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/erlang_gleam_dimensions_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: erlang_gleam_dimensions_css.gleam
4 | ---
5 | pub const dimensions_variables = "sketch_css_test-classes-dimensions_css_dimensions_variables"
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/erlang_gleam_edges_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: erlang_gleam_edges_css.gleam
4 | ---
5 | pub const edge_cases = "sketch_css_test-classes-edges_css_edge_cases"
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/erlang_gleam_exposed_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: erlang_gleam_exposed_css.gleam
4 | ---
5 | pub const exposed_property = "sketch_css_test-classes-exposed_css_exposed_property"
6 |
7 | pub const exposed_class = "sketch_css_test-classes-exposed_css_exposed_class"
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/erlang_gleam_font_face_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: erlang_gleam_font_face_css.gleam
4 | ---
5 |
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/erlang_gleam_function_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: erlang_gleam_function_css.gleam
4 | ---
5 | pub const property = "sketch_css_test-classes-function_css_property"
6 |
7 | pub const class = "sketch_css_test-classes-function_css_class"
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/erlang_gleam_important_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: erlang_gleam_important_css.gleam
4 | ---
5 | pub const important = "sketch_css_test-classes-important_css_important"
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/erlang_gleam_keyframe_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: erlang_gleam_keyframe_css.gleam
4 | ---
5 | pub const example = "sketch_css_test-classes-keyframe_css_example"
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/erlang_gleam_medias_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: erlang_gleam_medias_css.gleam
4 | ---
5 | pub const simple = "sketch_css_test-classes-medias_css_simple"
6 |
7 | pub const and = "sketch_css_test-classes-medias_css_and"
8 |
9 | pub const or = "sketch_css_test-classes-medias_css_or"
10 |
11 | pub const and_or = "sketch_css_test-classes-medias_css_and_or"
12 |
13 | pub const pseudo_class = "sketch_css_test-classes-medias_css_pseudo_class"
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/erlang_gleam_nestings_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: erlang_gleam_nestings_css.gleam
4 | ---
5 | pub const content = "sketch_css_test-classes-nestings_css_content"
6 |
7 | pub const example = "sketch_css_test-classes-nestings_css_example"
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/erlang_gleam_variable_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: erlang_gleam_variable_css.gleam
4 | ---
5 | pub const variable_property = "sketch_css_test-classes-variable_css_variable_property"
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/js_css_aliased_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: js_css_aliased_css.gleam
4 | ---
5 | .sketch_css_test-classes-aliased_css_aliased_module {
6 | background: red;
7 | }
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/js_css_dimensions_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: js_css_dimensions_css.gleam
4 | ---
5 | .sketch_css_test-classes-dimensions_css_dimensions_variables {
6 | padding: 12.0px;
7 | margin: 12.0px;
8 | transform: rotate(1.0rad) skew(1.0deg, 2.0deg);
9 | }
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/js_css_edges_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: js_css_edges_css.gleam
4 | ---
5 | .sketch_css_test-classes-edges_css_edge_cases {
6 | grid-template-areas: "header" "main";
7 | --example-property: example-value;
8 | color: var(--custom);
9 | }
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/js_css_exposed_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: js_css_exposed_css.gleam
4 | ---
5 | .sketch_css_test-classes-exposed_css_exposed_class {
6 | background: red;
7 | color: red;
8 | }
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/js_css_font_face_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: js_css_font_face_css.gleam
4 | ---
5 | @font-face {
6 | src: file;
7 | font-family: Example;
8 | font-style: bold;
9 | }
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/js_css_function_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: js_css_function_css.gleam
4 | ---
5 | .sketch_css_test-classes-function_css_property {
6 | width: 12px;
7 | color: blue;
8 | color: var(--custom);
9 | }
10 |
11 | .sketch_css_test-classes-function_css_class {
12 | background: #ddd;
13 | background: red;
14 | display: block;
15 | color: var(--custom);
16 | background: green;
17 | }
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/js_css_important_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: js_css_important_css.gleam
4 | ---
5 | .sketch_css_test-classes-important_css_important {
6 | color: blue;
7 | background: #ccc !important;
8 | }
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/js_css_keyframe_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: js_css_keyframe_css.gleam
4 | ---
5 | @keyframes fade-out {
6 | from {
7 | opacity: 1.0;
8 | }
9 |
10 | 50% {
11 | opacity: 0.5;
12 | }
13 |
14 | to {
15 | opacity: 0.0;
16 | }
17 | }
18 |
19 | .sketch_css_test-classes-keyframe_css_example {
20 | opacity: 1.0;
21 | animation: fade-out;
22 | background: blue;
23 | }
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/js_css_medias_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: js_css_medias_css.gleam
4 | ---
5 | .sketch_css_test-classes-medias_css_simple {
6 | background: blue;
7 | }
8 |
9 | @media (max-width: 700.0px) {
10 | .sketch_css_test-classes-medias_css_simple {
11 | background: red;
12 | background: blue;
13 | }
14 | }
15 |
16 | .sketch_css_test-classes-medias_css_pseudo_class {
17 | background: blue;
18 | }
19 |
20 | @media (max-width: 700.0px) {
21 | .sketch_css_test-classes-medias_css_pseudo_class {
22 | background: red;
23 | background: blue;
24 | }
25 | .sketch_css_test-classes-medias_css_pseudo_class:hover {
26 | background: green;
27 | }
28 | }
29 |
30 | .sketch_css_test-classes-medias_css_and_or {
31 | background: blue;
32 | }
33 |
34 | @media (max-width: 700.0px) and (min-width: 700.0px) or (min-width: 600.0px) {
35 | .sketch_css_test-classes-medias_css_and_or {
36 | background: red;
37 | background: blue;
38 | }
39 | }
40 |
41 | .sketch_css_test-classes-medias_css_or {
42 | background: blue;
43 | }
44 |
45 | @media (max-width: 700.0px) or (min-width: 600.0px) {
46 | .sketch_css_test-classes-medias_css_or {
47 | background: red;
48 | background: blue;
49 | }
50 | }
51 |
52 | .sketch_css_test-classes-medias_css_and {
53 | background: blue;
54 | }
55 |
56 | @media (max-width: 700.0px) and (min-width: 600.0px) {
57 | .sketch_css_test-classes-medias_css_and {
58 | background: red;
59 | background: blue;
60 | }
61 | }
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/js_css_nestings_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: js_css_nestings_css.gleam
4 | ---
5 | .sketch_css_test-classes-nestings_css_content {
6 | color: var(--custom);
7 | background: green;
8 | padding: 12.0px;
9 | }
10 |
11 | .sketch_css_test-classes-nestings_css_example {
12 | }
13 |
14 | .sketch_css_test-classes-nestings_css_example > .sketch_css_test-classes-nestings_css_content {
15 | background: red;
16 | }
17 |
18 | .sketch_css_test-classes-nestings_css_example > .sketch_css_test-classes-nestings_css_content:hover {
19 | background: blue;
20 | }
21 |
22 | @media (max-width: 700.0px) or (min-width: 400.0px) {
23 | .sketch_css_test-classes-nestings_css_example {
24 | background: blue;
25 | }
26 | .sketch_css_test-classes-nestings_css_example:hover {
27 | background: red;
28 | }
29 | .sketch_css_test-classes-nestings_css_example:hover {
30 | }
31 | .sketch_css_test-classes-nestings_css_example:hover > .sketch_css_test-classes-nestings_css_content {
32 | background: blue;
33 | }
34 | .sketch_css_test-classes-nestings_css_example:hover > .sketch_css_test-classes-nestings_css_content:hover {
35 | background: red;
36 | }
37 | }
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/js_css_variable_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: js_css_variable_css.gleam
4 | ---
5 | .sketch_css_test-classes-variable_css_variable_property {
6 | background: red;
7 | }
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/js_gleam_aliased_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: js_gleam_aliased_css.gleam
4 | ---
5 | pub const aliased_module = "sketch_css_test-classes-aliased_css_aliased_module"
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/js_gleam_dimensions_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: js_gleam_dimensions_css.gleam
4 | ---
5 | pub const dimensions_variables = "sketch_css_test-classes-dimensions_css_dimensions_variables"
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/js_gleam_edges_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: js_gleam_edges_css.gleam
4 | ---
5 | pub const edge_cases = "sketch_css_test-classes-edges_css_edge_cases"
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/js_gleam_exposed_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: js_gleam_exposed_css.gleam
4 | ---
5 | pub const exposed_property = "sketch_css_test-classes-exposed_css_exposed_property"
6 |
7 | pub const exposed_class = "sketch_css_test-classes-exposed_css_exposed_class"
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/js_gleam_font_face_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: js_gleam_font_face_css.gleam
4 | ---
5 |
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/js_gleam_function_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: js_gleam_function_css.gleam
4 | ---
5 | pub const property = "sketch_css_test-classes-function_css_property"
6 |
7 | pub const class = "sketch_css_test-classes-function_css_class"
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/js_gleam_important_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: js_gleam_important_css.gleam
4 | ---
5 | pub const important = "sketch_css_test-classes-important_css_important"
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/js_gleam_keyframe_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: js_gleam_keyframe_css.gleam
4 | ---
5 | pub const example = "sketch_css_test-classes-keyframe_css_example"
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/js_gleam_medias_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: js_gleam_medias_css.gleam
4 | ---
5 | pub const simple = "sketch_css_test-classes-medias_css_simple"
6 |
7 | pub const and = "sketch_css_test-classes-medias_css_and"
8 |
9 | pub const or = "sketch_css_test-classes-medias_css_or"
10 |
11 | pub const and_or = "sketch_css_test-classes-medias_css_and_or"
12 |
13 | pub const pseudo_class = "sketch_css_test-classes-medias_css_pseudo_class"
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/js_gleam_nestings_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: js_gleam_nestings_css.gleam
4 | ---
5 | pub const content = "sketch_css_test-classes-nestings_css_content"
6 |
7 | pub const example = "sketch_css_test-classes-nestings_css_example"
--------------------------------------------------------------------------------
/sketch_css/birdie_snapshots/js_gleam_variable_css_gleam.accepted:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.2.5
3 | title: js_gleam_variable_css.gleam
4 | ---
5 | pub const variable_property = "sketch_css_test-classes-variable_css_variable_property"
--------------------------------------------------------------------------------
/sketch_css/gleam.toml:
--------------------------------------------------------------------------------
1 | name = "sketch_css"
2 | version = "2.0.0"
3 |
4 | description = "A Sketch runtime package, made to generate CSS!"
5 | internal_modules = ["sketch_css/*"]
6 | licences = ["MIT"]
7 | links = [{title = "Sponsor", href = "https://github.com/sponsors/ghivert"}]
8 |
9 | [sketch_css]
10 | src = "test"
11 | dst = "styles"
12 | interface = "src/sketch/styles"
13 |
14 | [repository]
15 | type = "github"
16 | user = "ghivert"
17 | repo = "sketch"
18 | path = "sketch_css"
19 |
20 | [dependencies]
21 | argv = ">= 1.0.2 and < 2.0.0"
22 | glance = ">= 2.0.0 and < 3.0.0"
23 | gleam_erlang = ">= 0.25.0 and < 1.0.0"
24 | gleam_stdlib = ">= 0.42.0 and < 2.0.0"
25 | glint = ">= 1.2.0 and < 2.0.0"
26 | simplifile = ">= 2.0.1 and < 3.0.0"
27 | sketch = ">= 4.0.0 and < 5.0.0"
28 | snag = ">= 1.1.0 and < 2.0.0"
29 | tom = ">= 1.1.1 and < 2.0.0"
30 |
31 | [dev-dependencies]
32 | pprint = ">= 1.0.4 and < 2.0.0"
33 | birdie = ">= 1.2.5 and < 2.0.0"
34 | startest = ">= 0.6.0 and < 1.0.0"
35 |
--------------------------------------------------------------------------------
/sketch_css/src/sketch_css.gleam:
--------------------------------------------------------------------------------
1 | import argv
2 | import glint
3 | import sketch_css/commands/generate
4 |
5 | /// The `main` function is used as an entrypoint for Sketch CSS. That function
6 | /// is not meant to be used in your code, but is called when you use `gleam run`
7 | /// from the command line.
8 | ///
9 | /// ```
10 | /// gleam run -m sketch_css
11 | /// ```
12 | pub fn main() {
13 | glint.new()
14 | |> glint.with_name("Sketch CSS")
15 | |> glint.pretty_help(glint.default_pretty_help())
16 | |> glint.add(at: ["generate"], do: generate.css())
17 | |> glint.run(argv.load().arguments)
18 | }
19 |
--------------------------------------------------------------------------------
/sketch_css/src/sketch_css/commands/generate.gleam:
--------------------------------------------------------------------------------
1 | import gleam/io
2 | import gleam/option.{type Option}
3 | import gleam/result
4 | import glint
5 | import sketch_css/generate
6 | import sketch_css/utils
7 | import snag
8 |
9 | type Flag =
10 | Option(String)
11 |
12 | pub fn css() -> glint.Command(Nil) {
13 | use src, dst, interface <- run_glint()
14 | let assert Ok(directories) = utils.directories(src, dst, interface)
15 | io.println("Compiling Gleam styles files in " <> directories.src)
16 | io.println("Writing CSS files to " <> directories.dst)
17 | io.println("Writing interfaces files to " <> directories.interface)
18 | generate.stylesheets(directories:)
19 | |> result.map_error(snag.line_print)
20 | |> result.map_error(io.println)
21 | |> result.map(fn(_) {
22 | io.println("=========")
23 | io.println("Done!")
24 | })
25 | |> result.unwrap_both
26 | }
27 |
28 | fn run_glint(continuation: fn(Flag, Flag, Flag) -> Nil) {
29 | use <- glint.command_help("Generate CSS for your gleam_styles.gleam files!")
30 | use dst <- glint.flag(dst_flag())
31 | use src <- glint.flag(src_flag())
32 | use interface <- glint.flag(interface_flag())
33 | use _, _, flags <- glint.command
34 | let dst = dst(flags) |> option.from_result
35 | let src = src(flags) |> option.from_result
36 | let interface = interface(flags) |> option.from_result
37 | continuation(src, dst, interface)
38 | }
39 |
40 | fn dst_flag() {
41 | let msg = "Define the directory in which styles should be output."
42 | glint.string_flag("dest")
43 | |> glint.flag_help(msg)
44 | }
45 |
46 | fn src_flag() {
47 | let msg = "Define the directory in which styles should be read."
48 | glint.string_flag("src")
49 | |> glint.flag_help(msg)
50 | }
51 |
52 | fn interface_flag() {
53 | let msg = "Define the directory in which interfaces should be output."
54 | glint.string_flag("interface")
55 | |> glint.flag_help(msg)
56 | }
57 |
--------------------------------------------------------------------------------
/sketch_css/src/sketch_css/constants.gleam:
--------------------------------------------------------------------------------
1 | import sketch/css
2 |
3 | pub const combinators = [
4 | #("child", css.child),
5 | #("descendant", css.descendant),
6 | #("next_sibling", css.next_sibling),
7 | #("subsequent_sibling", css.subsequent_sibling),
8 | ]
9 |
10 | pub const pseudo_classes = [
11 | #("placeholder", "::placeholder"),
12 | #("selection", "::selection"),
13 | #("before", "::before"),
14 | #("after", "::after"),
15 | #("backdrop", "::backdrop"),
16 | #("cue", "::cue"),
17 | #("first_line", "::first-line"),
18 | #("grammar_error", "::grammar-error"),
19 | #("spelling_error", "::spelling-error"),
20 | #("marker", "::marker"),
21 | #("first_letter", "::first-letter"),
22 | #("file_selector_button", "::file-selector-button"),
23 | #("hover", ":hover"),
24 | #("any_link", ":any-link"),
25 | #("active", ":active"),
26 | #("focus", ":focus"),
27 | #("autofill", ":autofill"),
28 | #("buffering", ":buffering"),
29 | #("default", ":default"),
30 | #("defined", ":defined"),
31 | #("empty", ":empty"),
32 | #("fullscreen", ":fullscreen"),
33 | #("in_range", ":in-range"),
34 | #("indeterminate", ":indeterminate"),
35 | #("muted", ":muted"),
36 | #("paused", ":paused"),
37 | #("playing", ":playing"),
38 | #("seeking", ":seeking"),
39 | #("stalled", ":stalled"),
40 | #("state", ":state"),
41 | #("user_invalid", ":user-invalid"),
42 | #("user_valid", ":user-valid"),
43 | #("volume_locked", ":volume-locked"),
44 | #("placeholder_shown", ":placeholder-shown"),
45 | #("out_of_range", ":out-of-range"),
46 | #("dir", ":dir"),
47 | #("focus_visible", ":focus-visible"),
48 | #("focus_within", ":focus-within"),
49 | #("enabled", ":enabled"),
50 | #("disabled", ":disabled"),
51 | #("read_only", ":read-only"),
52 | #("read_write", ":read-write"),
53 | #("checked", ":checked"),
54 | #("valid", ":valid"),
55 | #("invalid", ":invalid"),
56 | #("required", ":required"),
57 | #("optional", ":optional"),
58 | #("link", ":link"),
59 | #("visited", ":visited"),
60 | #("target", ":target"),
61 | #("nth_child", ":nth-child"),
62 | #("nth_last_child", ":nth-last-child"),
63 | #("nth_of_type", ":nth-of-type"),
64 | #("nth_last_of_type", ":nth-last-of-type"),
65 | #("first_child", ":first-child"),
66 | #("last_child", ":last-child"),
67 | #("only_child", ":only-child"),
68 | #("first_of_type", ":first-of-type"),
69 | #("last_of_type", ":last-of-type"),
70 | #("only_of_type", ":only-of-type"),
71 | ]
72 |
--------------------------------------------------------------------------------
/sketch_css/src/sketch_css/fs.gleam:
--------------------------------------------------------------------------------
1 | import gleam/bool
2 | import gleam/list
3 | import gleam/result
4 | import gleam/string
5 | import simplifile
6 | import snag
7 |
8 | pub fn cwd() {
9 | simplifile.current_directory()
10 | |> snag.map_error(string.inspect)
11 | |> snag.context("Impossible to get current directory")
12 | }
13 |
14 | /// Assumes dir is a directory. Should be checked before calling the function.
15 | /// Returns a list of file paths.
16 | pub fn readdir(
17 | dir: String,
18 | recursive recursive: Bool,
19 | ) -> snag.Result(List(String)) {
20 | use content <- result.map(read_directory(dir))
21 | let dir = dir <> "/"
22 | let content = list.map(content, string.append(dir, _))
23 | use <- bool.guard(when: !recursive, return: content)
24 | list.flatten({
25 | use path <- list.filter_map(content)
26 | use is_dir <- result.try(is_directory(path))
27 | use <- bool.guard(when: is_dir, return: readdir(path, recursive:))
28 | Ok([path])
29 | })
30 | }
31 |
32 | pub fn read_file(path: String) -> snag.Result(String) {
33 | simplifile.read(path)
34 | |> snag.map_error(string.inspect)
35 | |> snag.context("Impossible to read file")
36 | |> snag.context("file: " <> path)
37 | }
38 |
39 | pub fn write_file(path: String, content: String) -> snag.Result(Nil) {
40 | simplifile.write(path, content)
41 | |> snag.map_error(string.inspect)
42 | |> snag.context("Impossible to write file")
43 | |> snag.context("file: " <> path)
44 | }
45 |
46 | pub fn mkdir(dir: String, recursive recursive: Bool) -> snag.Result(Nil) {
47 | case recursive {
48 | True -> simplifile.create_directory_all(dir)
49 | False -> simplifile.create_directory(dir)
50 | }
51 | |> snag.map_error(string.inspect)
52 | |> snag.context("Impossible to create directory")
53 | |> snag.context("dir: " <> dir)
54 | }
55 |
56 | fn read_directory(dir: String) {
57 | simplifile.read_directory(dir)
58 | |> snag.map_error(string.inspect)
59 | |> snag.context("Impossible to read the directory")
60 | |> snag.context("dir: " <> dir)
61 | }
62 |
63 | pub fn is_directory(dir: String) {
64 | simplifile.is_directory(dir)
65 | |> snag.map_error(string.inspect)
66 | |> snag.context("Impossible to test the directory")
67 | |> snag.context("dir: " <> dir)
68 | }
69 |
70 | pub fn current_directory() {
71 | simplifile.current_directory()
72 | |> snag.map_error(string.inspect)
73 | |> snag.context("Impossible to read current directory")
74 | }
75 |
--------------------------------------------------------------------------------
/sketch_css/src/sketch_css/generate.gleam:
--------------------------------------------------------------------------------
1 | import gleam/bool
2 | import gleam/io
3 | import gleam/list
4 | import gleam/pair
5 | import gleam/result
6 | import gleam/string
7 | import sketch
8 | import sketch_css/fs
9 | import sketch_css/module.{type Module}
10 | import sketch_css/module/stylesheet.{type StyleSheet}
11 | import sketch_css/utils.{type Directories}
12 | import snag
13 |
14 | /// Generate stylesheets from Gleam style definitions files. Recursively extract
15 | /// all files ending with `_styles.gleam`, `_css.gleam` or `_sketch.gleam` to
16 | /// proper stylesheets, and output some files interfaces to interact with them.
17 | pub fn stylesheets(
18 | directories directories: Directories,
19 | ) -> Result(Nil, snag.Snag) {
20 | use is_dir <- result.try(fs.is_directory(directories.src))
21 | use <- bool.guard(when: !is_dir, return: snag.error("Not a directory"))
22 | use source_files <- result.try(fs.readdir(directories.src, recursive: True))
23 | use modules <- result.try(convert(source_files, directories))
24 | write_css_files(modules, directories)
25 | }
26 |
27 | fn convert(source_files: List(String), directories: Directories) {
28 | source_files
29 | |> list.filter_map(module.from_path(_, directories.src))
30 | |> list.map(module.remove_pipes)
31 | |> list.map(module.rewrite_imports)
32 | |> list.map(module.rewrite_exposings)
33 | |> module.reject_cycles
34 | |> result.map(convert_modules)
35 | }
36 |
37 | fn convert_modules(modules: List(Module)) -> List(#(Module, StyleSheet)) {
38 | list.fold(modules, [], module.convert_style(modules))
39 | |> list.filter_map(fn(module) {
40 | list.find(modules, fn(m) { m.name == module.0 })
41 | |> result.map(pair.new(_, module.1))
42 | })
43 | }
44 |
45 | fn write_css_files(
46 | modules: List(#(Module, StyleSheet)),
47 | directories: Directories,
48 | ) -> snag.Result(Nil) {
49 | let assert Ok(stylesheet) = sketch.stylesheet(strategy: sketch.Ephemeral)
50 | let modules = list.filter(modules, is_css_file)
51 | let write_css_file = write_css_file(_, stylesheet, directories)
52 | list.map(modules, write_css_file)
53 | |> result.all
54 | |> result.replace(Nil)
55 | }
56 |
57 | fn write_css_file(
58 | module: #(Module, StyleSheet),
59 | stylesheet: sketch.StyleSheet,
60 | directories: Directories,
61 | ) -> snag.Result(Nil) {
62 | let #(stylesheet, names) = module.build_stylesheet(module, stylesheet)
63 | let content = sketch.render(stylesheet)
64 | let #(content, interfaces) =
65 | module.build_interface({ module.0 }.name, content, names)
66 | let outputs = utils.outputs(directories, { module.0 }.name)
67 | use _ <- result.try(fs.mkdir(outputs.dst, recursive: True))
68 | use _ <- result.try(fs.mkdir(outputs.interface, recursive: True))
69 | use _ <- result.try(fs.write_file(outputs.dst_file, content))
70 | use _ <- result.map(fs.write_file(outputs.interface_file, interfaces))
71 | io.println("=========")
72 | io.println("Gleam styles " <> { module.0 }.name <> " converted.")
73 | io.println("CSS file " <> outputs.dst_file <> " generated.")
74 | io.println("Interface file " <> outputs.interface_file <> " generated.")
75 | Nil
76 | }
77 |
78 | fn is_css_file(module: #(Module, a)) -> Bool {
79 | let path = { module.0 }.path
80 | string.ends_with(path, "_styles.gleam")
81 | || string.ends_with(path, "_css.gleam")
82 | || string.ends_with(path, "_sketch.gleam")
83 | }
84 |
--------------------------------------------------------------------------------
/sketch_css/src/sketch_css/module/dependencies.gleam:
--------------------------------------------------------------------------------
1 | import glance as g
2 | import gleam/list
3 | import gleam/result
4 | import gleam/string
5 | import snag
6 |
7 | pub fn reject_cycles(
8 | module: #(String, g.Module),
9 | visited: List(String),
10 | modules: List(#(String, g.Module)),
11 | ) -> snag.Result(Nil) {
12 | let modules_ = list.map({ module.1 }.imports, fn(i) { i.definition.module })
13 | case list.any(modules_, fn(module) { list.contains(visited, module) }) {
14 | True -> snag.error("cycle detected in module: " <> module.0)
15 | False -> {
16 | let by_name = fn(name) { list.find(modules, fn(m) { m.0 == name }) }
17 | list.filter_map(modules_, by_name)
18 | |> list.try_map(reject_cycles(_, [module.0, ..visited], modules))
19 | |> result.replace(Nil)
20 | }
21 | }
22 | }
23 |
24 | pub fn all_imports(
25 | module: #(String, g.Module),
26 | modules: List(#(String, g.Module)),
27 | visited: List(String),
28 | ) -> List(String) {
29 | let imports = list.map({ module.1 }.imports, fn(i) { i.definition.module })
30 | let visited = [module.0, ..visited]
31 | let modules_ =
32 | list.filter_map(imports, fn(i) { list.find(modules, fn(m) { m.0 == i }) })
33 | list.flat_map(modules_, all_imports(_, modules, visited))
34 | |> list.append(imports)
35 | |> list.unique
36 | |> list.sort(string.compare)
37 | }
38 |
--------------------------------------------------------------------------------
/sketch_css/src/sketch_css/module/functions.gleam:
--------------------------------------------------------------------------------
1 | import glance as g
2 | import gleam/bool
3 | import gleam/list
4 | import gleam/option
5 | import gleam/order
6 |
7 | /// True if the first is dependent of the second.
8 | pub fn is_dependent(a: g.Function, b: g.Function) -> order.Order {
9 | case in_statements(b.body, a.name) {
10 | True -> order.Lt
11 | False -> order.Gt
12 | }
13 | }
14 |
15 | fn in_statements(statements: List(g.Statement), other: String) {
16 | use found, statement <- list.fold(statements, False)
17 | use <- bool.guard(when: found, return: found)
18 | in_statement(statement, other)
19 | }
20 |
21 | fn in_statement(statement: g.Statement, other: String) {
22 | case statement {
23 | g.Expression(expression) -> in_expression(expression, other)
24 | g.Assignment(value:, ..) -> in_expression(value, other)
25 | g.Use(function:, ..) -> in_expression(function, other)
26 | }
27 | }
28 |
29 | fn in_expressions(expressions: List(g.Expression), other: String) {
30 | use found, expression <- list.fold(expressions, False)
31 | use <- bool.guard(when: found, return: found)
32 | in_expression(expression, other)
33 | }
34 |
35 | fn in_expression(expression: g.Expression, other: String) -> Bool {
36 | case expression {
37 | g.Variable(var) -> var == other
38 | g.NegateInt(expr) -> in_expression(expr, other)
39 | g.NegateBool(expr) -> in_expression(expr, other)
40 | g.FieldAccess(container:, ..) -> in_expression(container, other)
41 | g.TupleIndex(tuple:, ..) -> in_expression(tuple, other)
42 | g.Block(statements) -> in_statements(statements, other)
43 | g.Tuple(expressions) -> in_expressions(expressions, other)
44 | g.Fn(body:, ..) -> in_statements(body, other)
45 |
46 | g.List(elements:, rest:) -> {
47 | rest
48 | |> option.map(list.prepend(elements, _))
49 | |> option.unwrap(elements)
50 | |> in_expressions(other)
51 | }
52 |
53 | g.Call(function:, arguments:) ->
54 | arguments
55 | |> list.filter_map(extract_field)
56 | |> list.prepend(function)
57 | |> in_expressions(other)
58 |
59 | g.FnCapture(function:, arguments_before:, arguments_after:, ..) ->
60 | list.append(arguments_before, arguments_after)
61 | |> list.filter_map(extract_field)
62 | |> list.prepend(function)
63 | |> in_expressions(other)
64 |
65 | g.Case(subjects:, clauses:) -> {
66 | clauses
67 | |> list.map(fn(clause) { clause.body })
68 | |> list.append(subjects, _)
69 | |> in_expressions(other)
70 | }
71 |
72 | g.BinaryOperator(left:, right:, ..) -> {
73 | in_expression(left, other) || in_expression(right, other)
74 | }
75 |
76 | _ -> False
77 | }
78 | }
79 |
80 | fn extract_field(field: g.Field(a)) {
81 | case field {
82 | g.UnlabelledField(expression) -> Ok(expression)
83 | g.LabelledField(_, expression) -> Ok(expression)
84 | g.ShorthandField(..) -> Error(Nil)
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/sketch_css/src/sketch_css/module/pipes.gleam:
--------------------------------------------------------------------------------
1 | import glance as g
2 | import gleam/list
3 | import gleam/option
4 |
5 | /// Rewrites every pipe (`|>`) to the proper function call. Because pipe is an
6 | /// operator that disappears at runtime, it's useless to keep it in the AST.
7 | pub fn remove(module: g.Module) -> g.Module {
8 | g.Module(..module, functions: {
9 | use g.Definition(attributes:, definition:) <- list.map(module.functions)
10 | let attributes = list.map(attributes, remove_attribute)
11 | g.Definition(attributes:, definition: {
12 | g.Function(..definition, body: {
13 | use statement <- list.map(definition.body)
14 | remove_statement(statement)
15 | })
16 | })
17 | })
18 | }
19 |
20 | fn remove_attribute(attribute: g.Attribute) -> g.Attribute {
21 | let arguments = list.map(attribute.arguments, remove_expr)
22 | g.Attribute(..attribute, arguments:)
23 | }
24 |
25 | fn remove_statement(stat: g.Statement) -> g.Statement {
26 | case stat {
27 | g.Use(..) -> g.Use(..stat, function: remove_expr(stat.function))
28 | g.Expression(e) -> g.Expression(remove_expr(e))
29 | g.Assignment(..) -> g.Assignment(..stat, value: remove_expr(stat.value))
30 | }
31 | }
32 |
33 | fn remove_expr(expr: g.Expression) -> g.Expression {
34 | case expr {
35 | // Deeply rewrites.
36 | g.NegateInt(expr) -> g.NegateInt(remove_expr(expr))
37 | g.NegateBool(expr) -> g.NegateBool(remove_expr(expr))
38 | g.Block(stat) -> g.Block(list.map(stat, remove_statement))
39 | g.Todo(stat) -> g.Todo(option.map(stat, remove_expr))
40 | g.Panic(stat) -> g.Panic(option.map(stat, remove_expr))
41 | g.Tuple(stat) -> g.Tuple(list.map(stat, remove_expr))
42 | g.Fn(..) -> g.Fn(..expr, body: list.map(expr.body, remove_statement))
43 | g.TupleIndex(..) -> g.TupleIndex(..expr, tuple: remove_expr(expr.tuple))
44 |
45 | g.List(elements, rest) -> {
46 | let elements = list.map(elements, remove_expr)
47 | let rest = option.map(rest, remove_expr)
48 | g.List(elements:, rest:)
49 | }
50 |
51 | g.RecordUpdate(..) -> {
52 | let record = remove_expr(expr.record)
53 | g.RecordUpdate(..expr, record:, fields: {
54 | use field <- list.map(expr.fields)
55 | let item = option.map(field.item, remove_expr)
56 | g.RecordUpdateField(..field, item:)
57 | })
58 | }
59 |
60 | g.FieldAccess(..) -> {
61 | let container = remove_expr(expr.container)
62 | g.FieldAccess(..expr, container:)
63 | }
64 |
65 | g.Call(..) -> {
66 | let function = remove_expr(expr.function)
67 | g.Call(function:, arguments: {
68 | use argument <- list.map(expr.arguments)
69 | remove_expr_field(argument)
70 | })
71 | }
72 |
73 | g.Case(..) -> {
74 | let subjects = list.map(expr.subjects, remove_expr)
75 | g.Case(subjects:, clauses: {
76 | use clause <- list.map(expr.clauses)
77 | let guard = option.map(clause.guard, remove_expr)
78 | let body = remove_expr(clause.body)
79 | g.Clause(..clause, guard:, body:)
80 | })
81 | }
82 |
83 | // Rewrites pipe.
84 | g.BinaryOperator(g.Pipe, left:, right: g.Call(function:, arguments:)) -> {
85 | let left = g.UnlabelledField(remove_expr(left))
86 | let arguments = list.map(arguments, remove_expr_field)
87 | let arguments = [left, ..arguments]
88 | g.Call(function:, arguments:)
89 | }
90 |
91 | g.BinaryOperator(g.Pipe, left:, right: g.Variable(variable)) -> {
92 | let left = g.UnlabelledField(remove_expr(left))
93 | let arguments = [left]
94 | let function = g.Variable(variable)
95 | g.Call(function:, arguments:)
96 | }
97 |
98 | g.BinaryOperator(g.Pipe, left:, right:) -> {
99 | let left = remove_expr(left)
100 | let right = remove_expr(right)
101 | g.Call(function: right, arguments: [g.UnlabelledField(left)])
102 | }
103 |
104 | // Nothing to handle.
105 | _ -> expr
106 | }
107 | }
108 |
109 | fn remove_expr_field(argument: g.Field(g.Expression)) -> g.Field(g.Expression) {
110 | case argument {
111 | g.UnlabelledField(e) -> g.UnlabelledField(remove_expr(e))
112 | g.ShorthandField(label:) -> g.ShorthandField(label:)
113 | g.LabelledField(label:, item:) ->
114 | g.LabelledField(label:, item: remove_expr(item))
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/sketch_css/src/sketch_css/path.gleam:
--------------------------------------------------------------------------------
1 | import gleam/bool
2 | import gleam/list
3 | import gleam/result
4 | import gleam/string
5 | import simplifile
6 | import snag
7 |
8 | /// Keep the dirname of a filepath. Fails if path does not exist.
9 | /// Returns the original path if path is not a filepath.
10 | ///
11 | /// ```gleam
12 | /// let filepath = "/directory/file.txt"
13 | /// dirname(filepath) == "/directory"
14 | /// ```
15 | pub fn dirname(path: String) -> snag.Result(String) {
16 | use is_file <- result.map({
17 | simplifile.is_file(path)
18 | |> snag.map_error(string.inspect)
19 | |> snag.context("Impossible to detect file")
20 | })
21 | use <- bool.guard(when: !is_file, return: path)
22 | path
23 | |> string.split("/")
24 | |> list.reverse
25 | |> list.drop(1)
26 | |> list.reverse
27 | |> string.join("/")
28 | }
29 |
30 | /// Join path segments. Remove extraneous `/`.
31 | pub fn join(path: String, segment: String) -> String {
32 | [path, segment]
33 | |> string.join(with: "/")
34 | |> string.replace(each: "//", with: "/")
35 | }
36 |
--------------------------------------------------------------------------------
/sketch_css/src/sketch_css/uniconfig.gleam:
--------------------------------------------------------------------------------
1 | import gleam/bool
2 | import gleam/dict
3 | import gleam/list
4 | import gleam/pair
5 | import gleam/result
6 | import gleam/string
7 | import sketch_css/fs
8 | import snag
9 | import tom
10 |
11 | pub type Config {
12 | Config(directory: String, file: String, config: dict.Dict(String, tom.Toml))
13 | }
14 |
15 | pub fn read(package_name: String) -> snag.Result(Config) {
16 | use cwd <- result.try(fs.current_directory())
17 | do_read(cwd, package_name)
18 | }
19 |
20 | fn do_read(dir: String, package_name: String) -> snag.Result(Config) {
21 | use _ <- result.try_recover(package_config(dir, package_name))
22 | let error = "Impossible to find config"
23 | use <- bool.guard(when: string.is_empty(dir), return: snag.error(error))
24 | let segments = string.split(dir, on: "/")
25 | let segments = list.take(segments, list.length(segments) - 1)
26 | string.join(segments, with: "/")
27 | |> do_read(package_name)
28 | }
29 |
30 | fn package_config(dir: String, package_name: String) -> snag.Result(Config) {
31 | let filename = string.join([package_name, "toml"], with: ".")
32 | let filename = string.join([dir, filename], with: "/")
33 | let gleam_toml = string.join([dir, "gleam.toml"], with: "/")
34 | fs.read_file(filename)
35 | |> result.try(parse_toml)
36 | |> result.map(pair.new(filename, _))
37 | |> result.try_recover(fn(_) {
38 | fs.read_file(gleam_toml)
39 | |> result.try(parse_toml)
40 | |> result.then(fn(config) {
41 | tom.get_table(config, [package_name])
42 | |> snag.map_error(string.inspect)
43 | |> result.map(pair.new(gleam_toml, _))
44 | })
45 | })
46 | |> result.map(fn(config) {
47 | let #(file, config) = config
48 | let directory = add_leading_slash(dir)
49 | Config(config:, directory:, file:)
50 | })
51 | }
52 |
53 | fn parse_toml(content) {
54 | tom.parse(content)
55 | |> snag.map_error(string.inspect)
56 | |> snag.context("Unable to parse content")
57 | |> snag.context("content: " <> content)
58 | }
59 |
60 | fn add_leading_slash(dir: String) {
61 | use <- bool.guard(when: string.is_empty(dir), return: "/")
62 | dir
63 | }
64 |
--------------------------------------------------------------------------------
/sketch_css/src/sketch_css/utils.gleam:
--------------------------------------------------------------------------------
1 | import gleam/bool
2 | import gleam/list
3 | import gleam/option.{type Option}
4 | import gleam/pair
5 | import gleam/result
6 | import gleam/string
7 | import sketch_css/fs
8 | import sketch_css/uniconfig
9 | import snag
10 | import tom
11 |
12 | /// Directories used in scripts.
13 | /// - `src` indicates where to search for styles files.
14 | /// - `dst` indicates where to output the stylesheets.
15 | /// - `interface` indicates where to output the Gleam interface files.
16 | pub type Directories {
17 | Directories(src: String, dst: String, interface: String)
18 | }
19 |
20 | /// Outputs used in scripts.
21 | /// - `dst` indicates the directory to output the stylesheet.
22 | /// - `dst_file` indicates the filename to output the stylesheet.
23 | /// - `interface` indicates the directory to output the interface.
24 | /// - `interface_file` indicates the filename to output the interface.
25 | pub type Outputs {
26 | Outputs(
27 | dst: String,
28 | dst_file: String,
29 | interface: String,
30 | interface_file: String,
31 | )
32 | }
33 |
34 | pub fn directories(
35 | src: Option(String),
36 | dst: Option(String),
37 | interface: Option(String),
38 | ) -> snag.Result(Directories) {
39 | let config = uniconfig.read("sketch_css") |> option.from_result
40 | use cwd <- result.map(fs.cwd())
41 | let src = read_directory("src", src, cwd, config, "src")
42 | let dst = read_directory("dst", dst, cwd, config, "public/styles")
43 | Directories(src:, dst:, interface: {
44 | read_directory("interface", interface, cwd, config, "src/sketch/styles")
45 | })
46 | }
47 |
48 | pub fn remove_last_segment(path: String) {
49 | let segments = string.split(path, on: "/")
50 | let segments = list.take(segments, list.length(segments) - 1)
51 | string.join(segments, with: "/")
52 | }
53 |
54 | pub fn at(list: List(a), index: Int) {
55 | use <- bool.guard(when: index < 0, return: Error(Nil))
56 | case list {
57 | [] -> Error(Nil)
58 | [elem, ..] if index == 0 -> Ok(elem)
59 | [_, ..rest] -> at(rest, index - 1)
60 | }
61 | }
62 |
63 | /// Computes the output directories & files.
64 | pub fn outputs(directories: Directories, name: String) -> Outputs {
65 | let dst_file = string.join([directories.dst, name], with: "/")
66 | let dst_file = string.join([dst_file, "css"], with: ".")
67 | let interface_file = string.join([directories.interface, name], with: "/")
68 | let interface_file = string.join([interface_file, "gleam"], with: ".")
69 | let dst = remove_last_segment(dst_file)
70 | let interface = remove_last_segment(interface_file)
71 | Outputs(dst:, interface:, dst_file:, interface_file:)
72 | }
73 |
74 | pub fn remove_trailing_underscore(label: String) {
75 | case string.ends_with(label, "_") {
76 | False -> label
77 | True ->
78 | string.drop_end(label, 1)
79 | |> remove_trailing_underscore
80 | }
81 | }
82 |
83 | fn read_directory(
84 | key: String,
85 | flag: Option(String),
86 | cwd: String,
87 | config: Option(uniconfig.Config),
88 | default: String,
89 | ) -> String {
90 | flag
91 | |> option.map(pair.new(cwd, _))
92 | |> option.lazy_unwrap(fn() {
93 | option.then(config, read_config_directory(_, key))
94 | |> option.unwrap(#(cwd, default))
95 | })
96 | |> fn(res) { string.join([res.0, res.1], with: "/") }
97 | }
98 |
99 | fn read_config_directory(
100 | config: uniconfig.Config,
101 | key: String,
102 | ) -> Option(#(String, String)) {
103 | config.config
104 | |> tom.get_string([key])
105 | |> option.from_result
106 | |> option.map(pair.new(config.directory, _))
107 | }
108 |
--------------------------------------------------------------------------------
/sketch_css/test/sketch_css_test.gleam:
--------------------------------------------------------------------------------
1 | import startest
2 |
3 | pub fn main() {
4 | startest.default_config()
5 | |> startest.run
6 | }
7 |
--------------------------------------------------------------------------------
/sketch_css/test/sketch_css_test/classes.gleam:
--------------------------------------------------------------------------------
1 | import birdie
2 | import gleam/list
3 | import gleam/result
4 | import gleam/string
5 | import sketch_css/fs
6 | import sketch_css/generate
7 | import sketch_css/path
8 | import sketch_css/utils
9 | import sketch_css_test/helpers
10 | import startest.{describe, it}
11 | import startest/expect
12 |
13 | pub fn read_tests() {
14 | let assert Ok(css_files) = read_test_files()
15 | let assert Ok(cwd) = fs.cwd()
16 | let src = path.join(cwd, "test")
17 | let dst = path.join(cwd, "styles")
18 | let interface = path.join(cwd, "src/sketch/styles")
19 |
20 | utils.Directories(src:, dst:, interface:)
21 | |> generate.stylesheets
22 | |> expect.to_be_ok
23 |
24 | describe("Sketch CSS", [
25 | describe("generation", {
26 | use css_file <- list.map(css_files)
27 | it("should handle " <> css_file, fn() {
28 | read_snapshot_file(css_file, src, dst, extension: "css")
29 | read_snapshot_file(css_file, src, interface, extension: "gleam")
30 | })
31 | }),
32 | ])
33 | }
34 |
35 | fn read_snapshot_file(
36 | item: String,
37 | root: String,
38 | dst: String,
39 | extension extension: String,
40 | ) -> Nil {
41 | let assert Ok(name) = string.split(item, on: "/") |> list.last
42 | item
43 | |> string.replace(each: root, with: dst)
44 | |> string.replace(each: "gleam", with: extension)
45 | |> fs.read_file
46 | |> expect.to_be_ok
47 | |> birdie.snap(helpers.multitarget_title(extension <> "_" <> name))
48 | }
49 |
50 | fn read_test_files() {
51 | use cwd <- result.try(fs.cwd())
52 | let classes =
53 | string.join([cwd, "test", "sketch_css_test", "classes"], with: "/")
54 | fs.readdir(classes, recursive: True)
55 | }
56 |
--------------------------------------------------------------------------------
/sketch_css/test/sketch_css_test/classes/aliased_css.gleam:
--------------------------------------------------------------------------------
1 | import sketch/css as s
2 |
3 | pub fn aliased_module() {
4 | // Aliased property, should be rewrote.
5 | s.class([s.background("red")])
6 | }
7 |
--------------------------------------------------------------------------------
/sketch_css/test/sketch_css_test/classes/dimensions_css.gleam:
--------------------------------------------------------------------------------
1 | import sketch/css
2 | import sketch/css/angle.{deg}
3 | import sketch/css/length.{px}
4 | import sketch/css/transform
5 |
6 | /// Multiple dimensions, with exposings or not.
7 | pub fn dimensions_variables() {
8 | css.class([
9 | css.padding(px(12)),
10 | css.margin(length.px(12)),
11 | css.transform([
12 | transform.rotate(angle.rad(1.0)),
13 | transform.skew(deg(1.0), deg(2.0)),
14 | ]),
15 | ])
16 | }
17 |
--------------------------------------------------------------------------------
/sketch_css/test/sketch_css_test/classes/edges_css.gleam:
--------------------------------------------------------------------------------
1 | import sketch/css
2 |
3 | pub fn edge_cases(custom) {
4 | css.class([
5 | css.grid_template_areas(["header", "main"]),
6 | css.property("--example-property", "example-value"),
7 | css.color(custom),
8 | ])
9 | }
10 |
--------------------------------------------------------------------------------
/sketch_css/test/sketch_css_test/classes/exposed_css.gleam:
--------------------------------------------------------------------------------
1 | import sketch/css.{background, class as class_}
2 |
3 | pub fn exposed_property() {
4 | css.class([
5 | // Exposed property, should be rewritten correctly.
6 | background("red"),
7 | css.color("red"),
8 | ])
9 | }
10 |
11 | pub fn exposed_class() {
12 | // Exposed class, should be rewritten correctly.
13 | class_([background("red"), css.color("red")])
14 | }
15 |
--------------------------------------------------------------------------------
/sketch_css/test/sketch_css_test/classes/font_face_css.gleam:
--------------------------------------------------------------------------------
1 | import sketch/css
2 | import sketch/css/font_face
3 |
4 | pub fn font_face() {
5 | css.font_face([
6 | font_face.src("file"),
7 | font_face.font_family("Example"),
8 | font_face.font_style("bold"),
9 | ])
10 | }
11 |
--------------------------------------------------------------------------------
/sketch_css/test/sketch_css_test/classes/function_css.gleam:
--------------------------------------------------------------------------------
1 | import sketch/css
2 | import sketch_css_test/constants
3 | import sketch_css_test/helpers
4 |
5 | /// Constants in another file.
6 | pub fn property() {
7 | css.class([
8 | css.width_(constants.md),
9 | css.color(constants.blue),
10 | helpers.custom_color(constants.red),
11 | ])
12 | }
13 |
14 | /// Class in another file.
15 | pub fn class() {
16 | css.class([
17 | css.compose(helpers.card_body(constants.red)),
18 | css.background("green"),
19 | ])
20 | }
21 |
--------------------------------------------------------------------------------
/sketch_css/test/sketch_css_test/classes/important_css.gleam:
--------------------------------------------------------------------------------
1 | import sketch/css
2 | import sketch/css/length
3 |
4 | pub fn important() {
5 | css.class([
6 | css.compose(content()),
7 | css.color("blue"),
8 | css.background("#ccc") |> css.important,
9 | ])
10 | }
11 |
12 | fn content() {
13 | css.class([
14 | css.background("red"),
15 | css.color("red"),
16 | css.padding(length.px(12)),
17 | ])
18 | }
19 |
--------------------------------------------------------------------------------
/sketch_css/test/sketch_css_test/classes/keyframe_css.gleam:
--------------------------------------------------------------------------------
1 | import sketch/css
2 | import sketch/css/keyframe
3 |
4 | pub fn keyframe() {
5 | css.keyframes("fade-out", [
6 | keyframe.from([css.opacity(1.0)]),
7 | keyframe.at(50, [css.opacity(0.5)]),
8 | keyframe.to([css.opacity(0.0)]),
9 | ])
10 | }
11 |
12 | pub fn example() {
13 | css.class([
14 | css.opacity(1.0),
15 | css.animation("fade-out"),
16 | css.background("blue"),
17 | ])
18 | }
19 |
--------------------------------------------------------------------------------
/sketch_css/test/sketch_css_test/classes/medias_css.gleam:
--------------------------------------------------------------------------------
1 | import sketch/css
2 | import sketch/css/length.{px}
3 | import sketch/css/media
4 |
5 | pub fn simple() {
6 | css.class([
7 | css.background("blue"),
8 | css.media(media.max_width(px(700)), [
9 | css.background("red"),
10 | css.background("blue"),
11 | ]),
12 | ])
13 | }
14 |
15 | pub fn and() {
16 | css.class([
17 | css.background("blue"),
18 | css.media(media.max_width(px(700)) |> media.and(media.min_width(px(600))), [
19 | css.background("red"),
20 | css.background("blue"),
21 | ]),
22 | ])
23 | }
24 |
25 | pub fn or() {
26 | css.class([
27 | css.background("blue"),
28 | css.media(media.max_width(px(700)) |> media.or(media.min_width(px(600))), [
29 | css.background("red"),
30 | css.background("blue"),
31 | ]),
32 | ])
33 | }
34 |
35 | pub fn and_or() {
36 | css.class([
37 | css.background("blue"),
38 | css.media(
39 | media.max_width(px(700))
40 | |> media.and(media.min_width(px(700)))
41 | |> media.or(media.min_width(px(600))),
42 | [css.background("red"), css.background("blue")],
43 | ),
44 | ])
45 | }
46 |
47 | pub fn pseudo_class() {
48 | css.class([
49 | css.background("blue"),
50 | css.media(media.max_width(px(700)), [
51 | css.background("red"),
52 | css.background("blue"),
53 | css.hover([css.background("green")]),
54 | ]),
55 | ])
56 | }
57 |
--------------------------------------------------------------------------------
/sketch_css/test/sketch_css_test/classes/nestings_css.gleam:
--------------------------------------------------------------------------------
1 | import sketch/css
2 | import sketch/css/length.{px}
3 | import sketch/css/media
4 |
5 | pub fn example(custom) {
6 | css.class([
7 | css.media(media.or(media.max_width(px(700)), media.min_width(px(400))), [
8 | css.background("blue"),
9 | css.hover([css.background("red")]),
10 | css.hover([
11 | css.child(content(custom), [
12 | css.background("blue"),
13 | css.hover([css.background("red")]),
14 | ]),
15 | ]),
16 | ]),
17 | css.child(content(custom), [
18 | css.background("red"),
19 | css.hover([css.background("blue")]),
20 | ]),
21 | ])
22 | }
23 |
24 | pub fn content(custom) {
25 | css.class([
26 | css.color(custom),
27 | css.background("green"),
28 | css.padding(length.px(12)),
29 | ])
30 | }
31 |
--------------------------------------------------------------------------------
/sketch_css/test/sketch_css_test/classes/variable_css.gleam:
--------------------------------------------------------------------------------
1 | import sketch/css
2 |
3 | pub fn variable_property() {
4 | let red = "red"
5 | css.class([
6 | // Variable property, should be replaced.
7 | css.background(red),
8 | ])
9 | }
10 |
--------------------------------------------------------------------------------
/sketch_css/test/sketch_css_test/constants.gleam:
--------------------------------------------------------------------------------
1 | pub const red = "red"
2 |
3 | pub const blue = "blue"
4 |
5 | pub const md = "12px"
6 |
--------------------------------------------------------------------------------
/sketch_css/test/sketch_css_test/helpers.gleam:
--------------------------------------------------------------------------------
1 | import sketch/css.{type Class}
2 | import sketch_css_test/constants
3 |
4 | pub fn card_body(custom: String) -> Class {
5 | css.class([
6 | css.background("#ddd"),
7 | css.background(constants.red),
8 | css.display("block"),
9 | custom_color(custom),
10 | ])
11 | }
12 |
13 | pub fn custom_color(custom: String) -> css.Style {
14 | css.color(custom)
15 | }
16 |
17 | @target(erlang)
18 | pub fn multitarget_title(title: String) {
19 | "erlang_" <> title
20 | }
21 |
22 | @target(javascript)
23 | pub fn multitarget_title(title: String) {
24 | "js_" <> title
25 | }
26 |
--------------------------------------------------------------------------------
/sketch_lustre/.gitignore:
--------------------------------------------------------------------------------
1 | *.beam
2 | *.ez
3 | /build
4 | erl_crash.dump
5 |
--------------------------------------------------------------------------------
/sketch_lustre/gleam.toml:
--------------------------------------------------------------------------------
1 | name = "sketch_lustre"
2 | version = "3.1.0"
3 |
4 | description = "A Sketch runtime package, made to work with Lustre!"
5 | internal_modules = ["sketch/lustre/internals", "sketch/lustre/internals/*"]
6 | licences = ["MIT"]
7 | links = [{title = "Sponsor", href = "https://github.com/sponsors/ghivert"}]
8 | gleam = ">= 1.6.0"
9 |
10 | [repository]
11 | type = "github"
12 | user = "ghivert"
13 | repo = "sketch"
14 | path = "sketch_lustre"
15 |
16 | [dependencies]
17 | gleam_stdlib = ">= 0.56.0 and < 2.0.0"
18 | lustre = ">= 5.0.0 and < 6.0.0"
19 | sketch = ">= 4.1.0 and < 5.0.0"
20 |
21 | [dev-dependencies]
22 | gleeunit = ">= 1.0.0 and < 2.0.0"
23 |
--------------------------------------------------------------------------------
/sketch_lustre/manifest.toml:
--------------------------------------------------------------------------------
1 | # This file was generated by Gleam
2 | # You typically do not need to edit this file
3 |
4 | packages = [
5 | { name = "gleam_erlang", version = "0.34.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "0C38F2A128BAA0CEF17C3000BD2097EB80634E239CE31A86400C4416A5D0FDCC" },
6 | { name = "gleam_json", version = "2.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "C55C5C2B318533A8072D221C5E06E5A75711C129E420DD1CE463342106012E5D" },
7 | { name = "gleam_otp", version = "0.16.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "50DA1539FC8E8FA09924EB36A67A2BBB0AD6B27BCDED5A7EF627057CF69D035E" },
8 | { name = "gleam_stdlib", version = "0.59.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "F8FEE9B35797301994B81AF75508CF87C328FE1585558B0FFD188DC2B32EAA95" },
9 | { name = "gleeunit", version = "1.3.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "A7DD6C07B7DA49A6E28796058AA89E651D233B357D5607006D70619CD89DAAAB" },
10 | { name = "houdini", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "houdini", source = "hex", outer_checksum = "5BA517E5179F132F0471CB314F27FE210A10407387DA1EA4F6FD084F74469FC2" },
11 | { name = "lustre", version = "5.0.3", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib", "houdini"], otp_app = "lustre", source = "hex", outer_checksum = "0BB69D69A9E75E675AA2C32A4A0E5086041D037829FC8AD385BA6A59E45A60A2" },
12 | { name = "murmur3a", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "murmur3a", source = "hex", outer_checksum = "DAA714CEF379915D0F718BC410389245AA8ABFB6F48C73ADB9F011B009F28893" },
13 | { name = "sketch", version = "4.1.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "murmur3a"], otp_app = "sketch", source = "hex", outer_checksum = "66F32269C8C37B66BAD232413153250322364CA71D096F4FD0829178ABE61476" },
14 | ]
15 |
16 | [requirements]
17 | gleam_stdlib = { version = ">= 0.56.0 and < 2.0.0" }
18 | gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
19 | lustre = { version = ">= 5.0.0 and < 6.0.0" }
20 | sketch = { version = ">= 4.1.0 and < 5.0.0" }
21 |
--------------------------------------------------------------------------------
/sketch_lustre/src/sketch/lustre/element.gleam:
--------------------------------------------------------------------------------
1 | //// This module is a drop-in replacement for `lustre/element`. Just
2 | //// use the new functions, and everything will automagically be styled.
3 |
4 | import lustre/attribute.{type Attribute}
5 | import lustre/element as el
6 | import sketch
7 | import sketch/css
8 | import sketch/lustre/internals/global
9 |
10 | /// Alias for `lustre/element.Element`. \
11 | /// [Lustre Documentation](https://hexdocs.pm/lustre/lustre/element.html#Element)
12 | pub type Element(msg) =
13 | el.Element(msg)
14 |
15 | /// [Lustre Documentation](https://hexdocs.pm/lustre/lustre/element.html#fragment)
16 | pub const fragment = el.fragment
17 |
18 | /// [Lustre Documentation](https://hexdocs.pm/lustre/lustre/element.html#none)
19 | pub const none = el.none
20 |
21 | /// [Lustre Documentation](https://hexdocs.pm/lustre/lustre/element.html#text)
22 | pub const text = el.text
23 |
24 | /// [Lustre Documentation](https://hexdocs.pm/lustre/lustre/element.html#map)
25 | pub const map = el.map
26 |
27 | /// [Lustre Documentation](https://hexdocs.pm/lustre/lustre/element.html#element)
28 | pub fn element(
29 | tag tag: String,
30 | class class: css.Class,
31 | attributes attributes: List(Attribute(msg)),
32 | children children: List(el.Element(msg)),
33 | ) {
34 | let class_name = class_name(class)
35 | let attributes = [attribute.class(class_name), ..attributes]
36 | el.element(tag, attributes, children)
37 | }
38 |
39 | /// [Lustre Documentation](https://hexdocs.pm/lustre/lustre/element.html#element)
40 | pub fn element_(
41 | tag tag: String,
42 | attributes attributes: List(Attribute(msg)),
43 | children children: List(el.Element(msg)),
44 | ) {
45 | el.element(tag, attributes, children)
46 | }
47 |
48 | /// [Lustre Documentation](https://hexdocs.pm/lustre/lustre/element.html#namespaced)
49 | pub fn namespaced(
50 | tag tag: String,
51 | namespace namespace: String,
52 | class class: css.Class,
53 | attributes attributes: List(Attribute(msg)),
54 | children children: List(el.Element(msg)),
55 | ) {
56 | let class_name = class_name(class)
57 | let attributes = [attribute.class(class_name), ..attributes]
58 | el.namespaced(tag, namespace, attributes, children)
59 | }
60 |
61 | /// [Lustre Documentation](https://hexdocs.pm/lustre/lustre/element.html#namespaced)
62 | pub fn namespaced_(
63 | tag tag: String,
64 | namespace namespace: String,
65 | attributes attributes: List(Attribute(msg)),
66 | children children: List(el.Element(msg)),
67 | ) {
68 | el.namespaced(tag, namespace, attributes, children)
69 | }
70 |
71 | const error_msg = "Stylesheet is not initialized in your application. Please, initialize a stylesheet before rendering some nodes."
72 |
73 | /// Generate a class name from a `Class`, using the `StyleSheet` injected
74 | /// in the environment.
75 | pub fn class_name(class: css.Class) -> String {
76 | case global.get_stylesheet() {
77 | Error(_) -> panic as error_msg
78 | Ok(stylesheet) -> {
79 | let #(stylesheet, class_name) = sketch.class_name(class, stylesheet)
80 | let _ = global.set_stylesheet(stylesheet)
81 | class_name
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/sketch_lustre/src/sketch/lustre/internals/css-stylesheet.ffi.mjs:
--------------------------------------------------------------------------------
1 | export function createDocument() {
2 | const stylesheet = new CSSStyleSheet()
3 | document.adoptedStyleSheets.push(stylesheet)
4 | return stylesheet
5 | }
6 |
7 | export function createRoot(root) {
8 | const stylesheet = new CSSStyleSheet()
9 | if (root && root.adoptedStyleSheets) root.adoptedStyleSheets.push(stylesheet)
10 | return stylesheet
11 | }
12 |
13 | export function replaceSync(content, stylesheet) {
14 | stylesheet.replaceSync(content)
15 | }
16 |
--------------------------------------------------------------------------------
/sketch_lustre/src/sketch/lustre/internals/css_stylesheet.gleam:
--------------------------------------------------------------------------------
1 | import gleam/dynamic.{type Dynamic}
2 |
3 | pub type Kind {
4 | Document
5 | ShadowRoot(root: Dynamic)
6 | }
7 |
8 | pub fn create(kind: Kind) {
9 | case kind {
10 | Document -> create_document_stylesheet()
11 | ShadowRoot(root) -> create_shadow_root_stylesheet(root)
12 | }
13 | }
14 |
15 | @external(javascript, "./css-stylesheet.ffi.mjs", "createDocument")
16 | fn create_document_stylesheet() -> Dynamic {
17 | dynamic.from(0)
18 | }
19 |
20 | @external(javascript, "./css-stylesheet.ffi.mjs", "createRoot")
21 | fn create_shadow_root_stylesheet(_root: Dynamic) -> Dynamic {
22 | dynamic.from(0)
23 | }
24 |
25 | @external(javascript, "./css-stylesheet.ffi.mjs", "replaceSync")
26 | pub fn replace(_content: String, _stylesheet: Dynamic) -> Nil {
27 | Nil
28 | }
29 |
--------------------------------------------------------------------------------
/sketch_lustre/src/sketch/lustre/internals/global.ffi.mjs:
--------------------------------------------------------------------------------
1 | import * as gleam from '../../../gleam.mjs'
2 |
3 | let currentStylesheet = null
4 | const stylesheets = {}
5 |
6 | export function setStyleSheet(stylesheet) {
7 | stylesheets[stylesheet.id] = stylesheet
8 | return new gleam.Ok(stylesheet)
9 | }
10 |
11 | export function teardownStyleSheet(stylesheet) {
12 | delete stylesheets[stylesheet.id]
13 | return new gleam.Ok(undefined)
14 | }
15 |
16 | export function setCurrentStylesheet(stylesheet) {
17 | currentStylesheet = stylesheet.id
18 | return new gleam.Ok(stylesheet)
19 | }
20 |
21 | export function getStyleSheet() {
22 | const stylesheet = stylesheets[currentStylesheet]
23 | if (!stylesheet) return new gleam.Error()
24 | return new gleam.Ok(stylesheet)
25 | }
26 |
27 | export function dismissCurrentStylesheet() {
28 | currentStylesheet = null
29 | return new gleam.Ok(undefined)
30 | }
31 |
--------------------------------------------------------------------------------
/sketch_lustre/src/sketch/lustre/internals/global.gleam:
--------------------------------------------------------------------------------
1 | import sketch
2 |
3 | @external(erlang, "sketch_global_ffi", "set_stylesheet")
4 | @external(javascript, "./global.ffi.mjs", "setStyleSheet")
5 | pub fn set_stylesheet(
6 | stylesheet: sketch.StyleSheet,
7 | ) -> Result(sketch.StyleSheet, Nil)
8 |
9 | @external(erlang, "sketch_global_ffi", "teardown_stylesheet")
10 | @external(javascript, "./global.ffi.mjs", "teardownStyleSheet")
11 | pub fn teardown_stylesheet(stylesheet: sketch.StyleSheet) -> Result(Nil, Nil)
12 |
13 | @external(erlang, "sketch_global_ffi", "get_stylesheet")
14 | @external(javascript, "./global.ffi.mjs", "getStyleSheet")
15 | pub fn get_stylesheet() -> Result(sketch.StyleSheet, Nil)
16 |
17 | @external(erlang, "sketch_global_ffi", "set_current_stylesheet")
18 | @external(javascript, "./global.ffi.mjs", "setCurrentStylesheet")
19 | pub fn set_current_stylesheet(
20 | stylesheet: sketch.StyleSheet,
21 | ) -> Result(sketch.StyleSheet, Nil)
22 |
23 | @external(erlang, "sketch_global_ffi", "dismiss_current_stylesheet")
24 | @external(javascript, "./global.ffi.mjs", "dismissCurrentStylesheet")
25 | pub fn dismiss_current_stylesheet() -> Result(Nil, Nil)
26 |
--------------------------------------------------------------------------------
/sketch_lustre/src/sketch/lustre/internals/sketch_global_ffi.erl:
--------------------------------------------------------------------------------
1 | -module(sketch_global_ffi).
2 |
3 | -export([set_stylesheet/1, set_current_stylesheet/1, get_stylesheet/0,
4 | dismiss_current_stylesheet/0, teardown_stylesheet/1]).
5 |
6 | create_cache(Name) ->
7 | Exists = ets:whereis(Name),
8 | case Exists of
9 | undefined ->
10 | ets:new(Name, [set, public, named_table]);
11 | _ ->
12 | ok
13 | end.
14 |
15 | set_stylesheet(Stylesheet) ->
16 | create_cache(cache_manager),
17 | Id = element(3, Stylesheet),
18 | ets:insert(cache_manager, {Id, Stylesheet}),
19 | {ok, Stylesheet}.
20 |
21 | teardown_stylesheet(Stylesheet) ->
22 | create_cache(cache_manager),
23 | Id = element(3, Stylesheet),
24 | ets:delete(cache_manager, Id),
25 | {ok, nil}.
26 |
27 | set_current_stylesheet(Stylesheet) ->
28 | create_cache(view_manager),
29 | Id = element(3, Stylesheet),
30 | ets:insert(view_manager, {self(), Id}),
31 | {ok, Stylesheet}.
32 |
33 | dismiss_current_stylesheet() ->
34 | create_cache(view_manager),
35 | ets:delete(view_manager, self()),
36 | {ok, nil}.
37 |
38 | get_stylesheet() ->
39 | case ets:lookup(view_manager, self()) of
40 | [{_, Id}] ->
41 | case ets:lookup(cache_manager, Id) of
42 | [{_, Stylesheet}] ->
43 | {ok, Stylesheet};
44 | _ ->
45 | {error, nil}
46 | end;
47 | _ ->
48 | {error, nil}
49 | end.
50 |
--------------------------------------------------------------------------------
/sketch_lustre/test/sketch_lustre_test.gleam:
--------------------------------------------------------------------------------
1 | import gleeunit
2 | import gleeunit/should
3 |
4 | pub fn main() {
5 | gleeunit.main()
6 | }
7 |
8 | // gleeunit test functions end in `_test`
9 | pub fn hello_world_test() {
10 | 1
11 | |> should.equal(1)
12 | }
13 |
--------------------------------------------------------------------------------
/sketch_redraw/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## v2.0.0 - 2025-01-12
2 |
3 | v2.0.0 marks a breaking change with the new Sketch release (i.e. v4.0.0). Sketch
4 | Redraw should now be used in conjuction with `redraw_dom` exclusively.
5 |
6 | ## Improvements
7 |
8 | - Every HMTL element now has MDN Reference links & fragment of description to
9 | explain how to use them.
10 |
--------------------------------------------------------------------------------
/sketch_redraw/README.md:
--------------------------------------------------------------------------------
1 | # Sketch Redraw
2 |
3 | > In case you're here and don't know Sketch, take a look at
4 | > [`sketch` package](https://hexdocs.pm/sketch)!
5 |
6 | > This readme is a carbon-copy of the Sketch Redraw section in `sketch` readme.
7 |
8 | ## Setup
9 |
10 | When you're using Redraw, `sketch_redraw` covers you. `sketch_redraw` exposes
11 | one entrypoint, `sketch/redraw`, containing everything needed to get started.
12 |
13 | ```gleam
14 | // main.gleam
15 | import redraw
16 | import sketch/redraw as sketch_redraw
17 |
18 | pub fn main() {
19 | let root = client.create_root("root")
20 | client.render(root,
21 | redraw.strict_mode([
22 | // Initialise the cache. Sketch Redraw handles the details for you.
23 | sketch_redraw.provider([
24 | // Here comes your components!
25 | ])
26 | ])
27 | )
28 | }
29 | ```
30 |
31 | ## Usage
32 |
33 | `sketch_redraw` exposes one module to help you build your site, similarly to
34 | redraw: `sketch/redraw/dom/html`. `html` is simply a supercharged component,
35 | accepting a `sketch.Class` as first argument, and applies that style to the
36 | node. Because it's a simple component, `sketch/redraw/dom/html` and
37 | `redraw/html` can be mixed in the same code without issue! Because of that
38 | property, `sketch_redraw` _does not_ expose `text` and `none` function at that
39 | time.
40 |
41 | ```gleam
42 | import redraw/html as h
43 | import sketch/css
44 | import sketch/css/length.{px}
45 | import sketch/redraw/html
46 |
47 | fn main_style() {
48 | css.class([
49 | css.background("red"),
50 | css.font_size(px(16)),
51 | ])
52 | }
53 |
54 | fn view(model: Int) {
55 | html.div(main_style(), [], [
56 | h.div([], [
57 | h.text(int.to_string(model))
58 | ]),
59 | ])
60 | }
61 | ```
62 |
63 | And you're done! Enjoy your Redraw app, Sketch-enhanced!
64 |
65 | ## Final notes
66 |
67 | Sketch Redraw tries to integrate nicely with React Devtools! In case you're
68 | seeing something weird, signal the bug!
69 |
--------------------------------------------------------------------------------
/sketch_redraw/gleam.toml:
--------------------------------------------------------------------------------
1 | name = "sketch_redraw"
2 | target = "javascript"
3 | version = "2.0.0"
4 |
5 | description = "A Sketch runtime package, made to work with Redraw!"
6 | internal_modules = ["sketch/redraw/internals", "sketch/redraw/internals/*"]
7 | licences = ["MIT"]
8 | links = [{title = "Sponsor", href = "https://github.com/sponsors/ghivert"}]
9 |
10 | [repository]
11 | type = "github"
12 | user = "ghivert"
13 | repo = "sketch"
14 | path = "sketch_redraw"
15 |
16 | [dependencies]
17 | gleam_stdlib = ">= 0.42.0 and < 2.0.0"
18 | redraw = ">= 2.0.0 and < 3.0.0"
19 | redraw_dom = ">= 2.0.0 and < 3.0.0"
20 | sketch = ">= 4.0.0 and < 5.0.0"
21 |
22 | [dev-dependencies]
23 | gleeunit = ">= 1.0.0 and < 2.0.0"
24 |
--------------------------------------------------------------------------------
/sketch_redraw/manifest.toml:
--------------------------------------------------------------------------------
1 | # This file was generated by Gleam
2 | # You typically do not need to edit this file
3 |
4 | packages = [
5 | { name = "gleam_erlang", version = "0.33.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "A1D26B80F01901B59AABEE3475DD4C18D27D58FA5C897D922FCB9B099749C064" },
6 | { name = "gleam_javascript", version = "0.13.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "F98328FCF573DA6F3A35D7F6CB3F9FF19FD5224CCBA9151FCBEAA0B983AF2F58" },
7 | { name = "gleam_otp", version = "0.16.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "FA0EB761339749B4E82D63016C6A18C4E6662DA05BAB6F1346F9AF2E679E301A" },
8 | { name = "gleam_stdlib", version = "0.52.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "50703862DF26453B277688FFCDBE9DD4AC45B3BD9742C0B370DB62BC1629A07D" },
9 | { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" },
10 | { name = "redraw", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_javascript", "gleam_stdlib"], otp_app = "redraw", source = "hex", outer_checksum = "FF52D8626E1E6DC92EB8BC9DC8C70BC6F0E25824524A7C0658222EA406B5BE23" },
11 | { name = "redraw_dom", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "redraw"], otp_app = "redraw_dom", source = "hex", outer_checksum = "8318DA1E428B349177C444DDC2FA9AE0D33E0DD0CC5A55B82F030811FFD69EA4" },
12 | { name = "sketch", version = "4.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "sketch", source = "hex", outer_checksum = "AF090E77F6FD02467DABD8F0EDC1C329482853698FE6DB33B86D7D05C1BE32F4" },
13 | ]
14 |
15 | [requirements]
16 | gleam_stdlib = { version = ">= 0.42.0 and < 2.0.0" }
17 | gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
18 | redraw = { version = ">= 2.0.0 and < 3.0.0" }
19 | redraw_dom = { version = ">= 2.0.0 and < 3.0.0" }
20 | sketch = { version = ">= 4.0.0 and < 5.0.0" }
21 |
--------------------------------------------------------------------------------
/sketch_redraw/src/mutable.ffi.mjs:
--------------------------------------------------------------------------------
1 | export function wrap(current) {
2 | return { current }
3 | }
4 |
5 | export function set(variable, newValue) {
6 | variable.current = newValue
7 | return variable
8 | }
9 |
10 | export function get(variable) {
11 | return variable.current
12 | }
13 |
--------------------------------------------------------------------------------
/sketch_redraw/src/redraw.ffi.mjs:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | const styledFns = {}
4 |
5 | export function styledFn(tag, defaultValue) {
6 | if (styledFns[tag]) return styledFns[tag]
7 | let value = defaultValue.bind({})
8 | value.displayName = `Sketch.Styled(${tag})`
9 | styledFns[tag] ??= value
10 | return value
11 | }
12 |
13 | export function extract(props) {
14 | const { styles, as, ...props_ } = props
15 | return [as, styles, props_]
16 | }
17 |
18 | export function assign(props, fieldName, value) {
19 | return { ...props, [fieldName]: value }
20 | }
21 |
22 | export function dumpStyles(style, content) {
23 | if (style.innerHTML === content) return
24 | style.innerHTML = content
25 | }
26 |
27 | export function useInsertionEffect(setup) {
28 | return React.useInsertionEffect(() => {
29 | setup()
30 | })
31 | }
32 |
33 | export function createStyleTag() {
34 | const style = document.createElement('style')
35 | document.head.appendChild(style)
36 | return style
37 | }
38 |
39 | export function toProps(attributes) {
40 | const props = {}
41 | for (const item of attributes) {
42 | props[item.key] = item.content
43 | }
44 | return props
45 | }
46 |
--------------------------------------------------------------------------------
/sketch_redraw/src/sketch/redraw.gleam:
--------------------------------------------------------------------------------
1 | import redraw.{type Component} as react
2 | import redraw/dom/attribute.{type Attribute} as a
3 | import sketch
4 | import sketch/css.{type Class}
5 | import sketch/redraw/internals/mutable as mut
6 | import sketch/redraw/internals/object
7 |
8 | type StyleSheet {
9 | StyleSheet(cache: mut.Mutable(sketch.StyleSheet), render: fn() -> Nil)
10 | }
11 |
12 | /// Unique name for Sketch Context. Only used across the module.
13 | const context_name = "SketchRedrawContext"
14 |
15 | /// Error message used when querying context. Should be used to indicate to the
16 | /// user what should be done before using `sketch_redraw`.
17 | const error_msg = "Sketch Redraw Provider not set. Please, add the provider in your render tree."
18 |
19 | /// Create the Sketch provider used to manage the `StyleSheet`. \
20 | /// This makes sure identical styles will never be computed twice. \
21 | /// Use it at root of your render function.
22 | ///
23 | /// ```gleam
24 | /// import redraw
25 | /// import redraw/dom/client
26 | /// import sketch/redraw as sketch_redraw
27 | ///
28 | /// pub fn main() {
29 | /// let app = app()
30 | /// let root = client.create_root("root")
31 | /// client.render(root, {
32 | /// redraw.strict_mode([
33 | /// sketch_redraw.provider([
34 | /// app(),
35 | /// ]),
36 | /// ])
37 | /// })
38 | /// }
39 | /// ```
40 | pub fn provider(children) {
41 | let assert Ok(cache) = sketch.stylesheet(strategy: sketch.Persistent)
42 | let cache = mut.wrap(cache)
43 | let stylesheet = StyleSheet(cache:, render: fn() { Nil })
44 | let assert Ok(context) = react.create_context_(context_name, stylesheet)
45 | let style = create_style_tag()
46 | let render = fn() { dump_styles(style, sketch.render(mut.get(cache))) }
47 | let stylesheet = StyleSheet(cache:, render:)
48 | react.provider(context, stylesheet, children)
49 | }
50 |
51 | fn get_context() {
52 | case react.get_context(context_name) {
53 | Ok(context) -> context
54 | Error(_) -> panic as error_msg
55 | }
56 | }
57 |
58 | fn generate_class_name(cache, styles) {
59 | let #(cache_, class_name) = sketch.class_name(styles, mut.get(cache))
60 | mut.set(cache, cache_)
61 | class_name
62 | }
63 |
64 | fn do_styled(props) {
65 | let context = get_context()
66 | let StyleSheet(cache:, render:) = react.use_context(context)
67 | let #(tag, styles, props) = extract(props)
68 | let str = styles.as_string
69 | let class_name =
70 | react.use_memo(fn() { generate_class_name(cache, styles) }, #(cache, str))
71 | use_insertion_effect(fn() { render() }, #(class_name))
72 | react.jsx(tag, object.add(props, "className", class_name), Nil)
73 | }
74 |
75 | /// Style a native DOM node. Can probably be used for custom elements, but props
76 | /// will be different, so I don't know yet how to do it properly.
77 | @internal
78 | pub fn styled(
79 | tag: String,
80 | styles: Class,
81 | props: List(Attribute),
82 | children: List(Component),
83 | ) {
84 | let as_ = a.attribute("as", tag)
85 | let styles = a.attribute("styles", styles)
86 | let fun = styled_fn(tag, do_styled)
87 | to_props([as_, styles, ..props])
88 | |> react.jsx(fun, _, children)
89 | }
90 |
91 | // FFI
92 |
93 | /// Extract the props generated from `styled` function.
94 | /// Extract `as` and `styles`, and the new props without them.
95 | @external(javascript, "../redraw.ffi.mjs", "extract")
96 | fn extract(props: a) -> #(String, Class, a)
97 |
98 | @external(javascript, "../redraw.ffi.mjs", "useInsertionEffect")
99 | fn use_insertion_effect(setup: fn() -> Nil, deps: a) -> Nil
100 |
101 | @external(javascript, "../redraw.ffi.mjs", "createStyleTag")
102 | fn create_style_tag() -> a
103 |
104 | @external(javascript, "../redraw.ffi.mjs", "dumpStyles")
105 | fn dump_styles(style: a, content: String) -> Nil
106 |
107 | /// Creates the styled function from `do_styled` if it does not exists.
108 | /// Otherwise, returns the existing `do_styled` function specialized for the tag.
109 | @external(javascript, "../redraw.ffi.mjs", "styledFn")
110 | fn styled_fn(tag: String, value: a) -> a
111 |
112 | @external(javascript, "../redraw.ffi.mjs", "toProps")
113 | fn to_props(props: List(a.Attribute)) -> b
114 |
--------------------------------------------------------------------------------
/sketch_redraw/src/sketch/redraw/internals/mutable.gleam:
--------------------------------------------------------------------------------
1 | pub type Mutable(a)
2 |
3 | @external(javascript, "../../../mutable.ffi.mjs", "wrap")
4 | pub fn wrap(mut: a) -> Mutable(a)
5 |
6 | @external(javascript, "../../../mutable.ffi.mjs", "set")
7 | pub fn set(mut: Mutable(a), value: a) -> Mutable(a)
8 |
9 | @external(javascript, "../../../mutable.ffi.mjs", "get")
10 | pub fn get(mut: Mutable(a)) -> a
11 |
--------------------------------------------------------------------------------
/sketch_redraw/src/sketch/redraw/internals/object.gleam:
--------------------------------------------------------------------------------
1 | /// Add `field_name` to `value` of the object `object`.
2 | /// It will generate a new Object, and will not perform side-effect.
3 | @external(javascript, "../../../redraw.ffi.mjs", "assign")
4 | pub fn add(object: a, field_name: String, value: b) -> a
5 |
--------------------------------------------------------------------------------
/sketch_redraw/test/sketch_redraw_test.gleam:
--------------------------------------------------------------------------------
1 | import gleeunit
2 | import gleeunit/should
3 |
4 | pub fn main() {
5 | gleeunit.main()
6 | }
7 |
8 | // gleeunit test functions end in `_test`
9 | pub fn hello_world_test() {
10 | 1
11 | |> should.equal(1)
12 | }
13 |
--------------------------------------------------------------------------------