├── .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 | [![Package Version](https://img.shields.io/hexpm/v/shared_styles)](https://hex.pm/packages/shared_styles) 4 | [![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](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 | [![Package Version](https://img.shields.io/hexpm/v/landing_page)](https://hex.pm/packages/landing_page) 4 | [![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](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 | --------------------------------------------------------------------------------