├── .browserslistrc ├── .editorconfig ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── spruce-logo-dark.svg ├── spruce-logo-light.svg ├── thumbnail-5.png └── workflows │ └── test.yml ├── .gitignore ├── .npmignore ├── .stylelintignore ├── .stylelintrc.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── css ├── spruce.css └── spruce.min.css ├── eleventy.config.js ├── package-lock.json ├── package.json ├── preview ├── dist │ ├── button │ │ └── index.html │ ├── color │ │ └── index.html │ ├── component │ │ └── index.html │ ├── css │ │ └── main.css │ ├── font │ │ ├── montserrat-v25-latin-ext_latin-700.woff2 │ │ └── montserrat-v25-latin-ext_latin-regular.woff2 │ ├── form │ │ └── index.html │ ├── function │ │ └── index.html │ ├── index copy │ │ └── index.html │ ├── index.html │ ├── js │ │ ├── reading-direction.js │ │ └── theme-mode.js │ ├── media │ │ └── index.html │ ├── mixin │ │ └── index.html │ ├── table │ │ └── index.html │ └── typography │ │ └── index.html └── src │ ├── _data │ ├── helper.js │ ├── navigation.json │ └── site.json │ ├── _includes │ ├── layout │ │ └── base.html │ └── partial │ │ └── meta.html │ ├── button.html │ ├── color.html │ ├── component.html │ ├── css │ └── main.css │ ├── font │ ├── montserrat-v25-latin-ext_latin-700.woff2 │ └── montserrat-v25-latin-ext_latin-regular.woff2 │ ├── form.html │ ├── function.html │ ├── index.html │ ├── js │ ├── reading-direction.js │ └── theme-mode.js │ ├── media.html │ ├── mixin.html │ ├── scss │ ├── component │ │ ├── _alert.scss │ │ ├── _color.scss │ │ ├── _index.scss │ │ ├── _navigation.scss │ │ └── _preview.scss │ ├── config │ │ ├── _config.scss │ │ ├── _dark-colors.scss │ │ ├── _dark-mode.scss │ │ ├── _font.scss │ │ ├── _index.scss │ │ └── _styles.scss │ ├── layout │ │ ├── _container.scss │ │ ├── _index.scss │ │ └── _main.scss │ └── main.scss │ ├── table.html │ └── typography.html ├── scss ├── config │ ├── _breakpoint.scss │ ├── _button.scss │ ├── _color.scss │ ├── _display.scss │ ├── _escaping-characters.scss │ ├── _generator.scss │ ├── _index.scss │ ├── _layout.scss │ ├── _print.scss │ ├── _setting.scss │ ├── _spacer.scss │ ├── _table.scss │ ├── _transition.scss │ ├── _typography.scss │ └── form │ │ ├── _check.scss │ │ ├── _control.scss │ │ ├── _description.scss │ │ ├── _fieldset.scss │ │ ├── _file.scss │ │ ├── _group.scss │ │ ├── _icon.scss │ │ ├── _index.scss │ │ ├── _label.scss │ │ ├── _range.scss │ │ ├── _row.scss │ │ ├── _select.scss │ │ └── _switch.scss ├── element │ ├── _accessibility.scss │ ├── _default.scss │ ├── _divider.scss │ ├── _index.scss │ ├── _media.scss │ ├── _root.scss │ ├── _table.scss │ ├── _typography.scss │ └── _utilities.scss ├── form │ ├── _button.scss │ ├── _check.scss │ ├── _control.scss │ ├── _description.scss │ ├── _fieldset.scss │ ├── _file.scss │ ├── _group-label.scss │ ├── _group.scss │ ├── _index.scss │ ├── _label.scss │ ├── _range.scss │ ├── _row.scss │ ├── _switch.scss │ └── _validation.scss ├── function │ ├── _color.scss │ ├── _config.scss │ ├── _css-variable.scss │ ├── _font-size.scss │ ├── _index.scss │ ├── _setting.scss │ ├── _spacer.scss │ └── _utilities.scss ├── mixin │ ├── _breakpoint.scss │ ├── _button.scss │ ├── _color.scss │ ├── _css-variable.scss │ ├── _font-face.scss │ ├── _form.scss │ ├── _generator.scss │ ├── _index.scss │ ├── _layout.scss │ ├── _selection.scss │ ├── _transition.scss │ ├── _utilities.scss │ └── _variables.scss ├── plugin │ ├── _index.scss │ └── _normalize.scss ├── print │ └── _index.scss ├── spruce-styles.scss └── spruce.scss └── test ├── function ├── _config.scss └── _index.scss ├── sass.test.js └── test.scss /.browserslistrc: -------------------------------------------------------------------------------- 1 | # Browsers that we support 2 | 3 | defaults 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 4 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | eclint_block_comment_start = /* 13 | eclint_block_comment = * 14 | eclint_block_comment_end = */ 15 | 16 | [*.{scss,css,js,json,yml,md,svg}] 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "airbnb-base" 8 | ], 9 | "parserOptions": { 10 | "ecmaVersion": "latest", 11 | "sourceType": "module" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/spruce-logo-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /.github/thumbnail-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/conedevelopment/sprucecss/0d57b4ae03be67e8b9db47b3c1292bcbf585548f/.github/thumbnail-5.png -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | workflow_dispatch: 10 | 11 | jobs: 12 | lint: 13 | name: Lint 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v3 19 | - name: Set up Node.js 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: 'lts/*' 23 | - name: Install dependencies 24 | run: npm ci 25 | - name: Run linter 26 | run: npm run sass:lint -- --formatter github 27 | - name: Check EditorConfig configuration 28 | run: test -f .editorconfig 29 | - name: Check adherence to EditorConfig 30 | uses: greut/eclint-action@v0 31 | with: 32 | eclint_args: | 33 | -exclude={css/*,preview/**} 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Node ### 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # TypeScript v1 declaration files 46 | typings/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Microbundle cache 58 | .rpt2_cache/ 59 | .rts2_cache_cjs/ 60 | .rts2_cache_es/ 61 | .rts2_cache_umd/ 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env 74 | .env.test 75 | .env*.local 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | .parcel-cache 80 | 81 | # Next.js build output 82 | .next 83 | 84 | # Nuxt.js build / generate output 85 | .nuxt 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and not Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | # Stores VSCode versions used for testing VSCode extensions 109 | .vscode-test 110 | 111 | ### Prepros ### 112 | # Config Prepros files to ignore 113 | prepros.cfg 114 | prepros-6.config 115 | prepros.config 116 | 117 | ### Windows ### 118 | # Windows thumbnail cache files 119 | Thumbs.db 120 | Thumbs.db:encryptable 121 | ehthumbs.db 122 | ehthumbs_vista.db 123 | 124 | # Dump file 125 | *.stackdump 126 | 127 | # Folder config file 128 | [Dd]esktop.ini 129 | 130 | # Recycle Bin used on file shares 131 | $RECYCLE.BIN/ 132 | 133 | # Windows Installer files 134 | *.cab 135 | *.msi 136 | *.msix 137 | *.msm 138 | *.msp 139 | 140 | # Windows shortcuts 141 | *.lnk 142 | 143 | # Preview's HTML and CSS 144 | preview/src/assets/css 145 | 146 | # .map files 147 | *.map 148 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /.browserslistrc 2 | /.editorconfig 3 | /.eslintrc.json 4 | /.github/ 5 | /.gitignore 6 | /.stylelintignore 7 | /.stylelintrc.json 8 | /.vscode/ 9 | /CODE_OF_CONDUCT.md 10 | /CONTRIBUTING.md 11 | /preview/ 12 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | scss/mixin/_layout.scss 2 | scss/plugin/_normalize.scss 3 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-sass-guidelines", 3 | "plugins": ["stylelint-order"], 4 | "rules": { 5 | "max-nesting-depth": 6, 6 | "selector-no-vendor-prefix": [ 7 | true, 8 | { 9 | "ignoreSelectors": ["/-moz-.*/", "/-ms-.*/", "/-webkit-.*/"] 10 | } 11 | ], 12 | "selector-no-qualifying-type": [ 13 | true, 14 | { 15 | "ignore": ["attribute"] 16 | } 17 | ], 18 | "value-no-vendor-prefix": [ 19 | true, 20 | { 21 | "ignoreValues": ["box"] 22 | } 23 | ], 24 | "color-function-notation": "modern", 25 | "selector-class-pattern": null, 26 | "scss/percent-placeholder-pattern": null, 27 | "order/properties-alphabetical-order": true, 28 | "@stylistic/function-parentheses-space-inside": null 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | hello@conedevelopment.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | [See the documentation](https://sprucecss.com/docs/getting-started/contribution). 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Cone Development 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |
4 | 5 | 6 | 7 | Spruce CSS 8 | 9 |
10 |
11 |

12 | 13 | **Spruce CSS is an open-source, lightweight and modernish CSS design system, framework built on Sass. Give your project a solid foundation.** 14 | 15 | [![Github release](https://img.shields.io/github/v/release/conedevelopment/sprucecss?color=2350f6&logo=github&logoColor=white&style=for-the-badge)](https://github.com/conedevelopment/sprucecss/releases/latest) 16 | [![npm version](https://img.shields.io/npm/v/sprucecss?color=2350f6&style=for-the-badge)](https://www.npmjs.com/package/sprucecss) 17 | [![Back-end](https://img.shields.io/github/actions/workflow/status/conedevelopment/sprucecss/test.yml?branch=main&logo=github&style=for-the-badge&label=Test)](https://github.com/conedevelopment/sprucecss/actions/workflows/test.yml) 18 | [![License](https://img.shields.io/badge/license-MIT-2350f6?style=for-the-badge)](https://github.com/conedevelopment/sprucecss/blob/main/LICENSE) 19 | 20 | The Spruce CSS logo, a minimalistic, low-level CSS framework caption and an abstract 3D illustration. 21 | 22 | ## What is Spruce CSS? 23 | 24 | - It is a Sass-based, small framework that operates with just a few utility classes. 25 | - It takes advantage of the Sass members: variables, mixins, and functions. 26 | - It embraces Sass modules, so it uses @use and namespacing for import. 27 | - Spruce is a good choice if you prefer writing CSS instead of HTML. It uses just a few classic utility classes. 28 | - It is a relatively small (~7kb gzipped) framework with a smaller learning curve. The codebase is small but can add more to any project with the available mixins and functions. 29 | - It is that bunch of code you keep manually carrying from project to project. 30 | - It is themeable. You can create different themes using CSS custom properties like a dark one. 31 | - The generated CSS code is separated from the framework. You can use only the tools (variables, mixins, functions) in your project [without the generated styles](https://sprucecss.com/docs/elements/generators). 32 | - Include just a few components. For UI, we have a separate project named [Spruce UI](/ui/getting-started/introduction), where you can find drop-in components. 33 | - [It comes with dark-mode](https://sprucecss.com/docs/customization/themes) (or any theme mode) support. It uses CSS custom properties, so it isn’t that hard to create a new color theme. 34 | - It doesn’t come with a classical grid system. 35 | 36 | ## How to start with Spruce? 37 | 38 | Firstly, we suggest checking out the documentation, precisely the [installation page](https://sprucecss.com/docs/getting-started/installation). 39 | 40 | There is nothing new if you previously used Sass unless you don’t know the newer [module system](https://sass-lang.com/blog/the-module-system-is-launched). 41 | 42 | We made a [Spruce CSS Eleventy Starter](https://github.com/conedevelopment/sprucecss-eleventy-starter), a boilerplate starter template based on the popular static site generator 11ty. It includes a basic compile setup and, of course, Spruce CSS. You can find more information about it on GitHub. 43 | 44 | ## Documentation 45 | 46 | For the complete documentation, please visit our site at [sprucecss.com](https://sprucecss.com). You can edit it at our [separate repository](https://github.com/conedevelopment/sprucecss-site-eleventy). 47 | 48 | ### Getting Started 49 | 50 | - [Introduction](https://sprucecss.com/docs/getting-started/introduction) 51 | - [Installation](https://sprucecss.com/docs/getting-started/installation) 52 | - [Structure and Code](https://sprucecss.com/docs/getting-started/structure-and-code) 53 | - [Sass](https://sprucecss.com/docs/getting-started/sass) 54 | - [Inclusivity and Accessibility](https://sprucecss.com/docs/getting-started/accessibility) 55 | - [Internationalization](https://sprucecss.com/docs/getting-started/internationalization) 56 | - [Print](https://sprucecss.com/docs/getting-started/print) 57 | - [JS](https://sprucecss.com/docs/getting-started/js) 58 | - [Contribution](https://sprucecss.com/docs/getting-started/contribution) 59 | - [Appendix](https://sprucecss.com/docs/getting-started/appendix) 60 | 61 | ### Customization 62 | - [Variables](https://sprucecss.com/docs/customization/variables/) 63 | - [Settings](https://sprucecss.com/docs/customization/settings) 64 | - [Themes](https://sprucecss.com/docs/customization/themes) 65 | 66 | ### Sass 67 | - [Colors](https://sprucecss.com/docs/sass/colors/) 68 | - [Variables](https://sprucecss.com/docs/sass/variables) 69 | - [Mixins](https://sprucecss.com/docs/sass/mixins) 70 | - [Functions](https://sprucecss.com/docs/sass/functions) 71 | 72 | ### Elements 73 | - [Generators](https://sprucecss.com/docs/elements/generators) 74 | - [Typography](https://sprucecss.com/docs/elements/typography) 75 | - [Tables](https://sprucecss.com/docs/elements/tables) 76 | - [Buttons](https://sprucecss.com/docs/elements/buttons) 77 | - [Forms](https://sprucecss.com/docs/elements/forms) 78 | - [Media](https://sprucecss.com/docs/elements/media) 79 | 80 | ## Components 81 | 82 | This collection of reusable user interfaces aims to help you create more coherently with Spruce CSS. 83 | 84 | ## Templates 85 | 86 | - **[Spruce Docs](https://github.com/conedevelopment/sprucecss-eleventy-documentation-template)** - A simple documentation template made with Eleventy. 87 | - **[Root](https://github.com/conedevelopment/sprucecss-root-admin-template)** - A straightforward administration template with standard views and lots of components. 88 | 89 | ## Contributing 90 | 91 | Thank you for considering contributing to Spruce CSS! The [contribution guide](https://sprucecss.com/docs/getting-started/contribution/) can be found in the documentation. 92 | -------------------------------------------------------------------------------- /eleventy.config.js: -------------------------------------------------------------------------------- 1 | export default function (config) { 2 | config.addPassthroughCopy('./preview/src/css/**'); 3 | config.addPassthroughCopy('./preview/src/font/**'); 4 | config.addPassthroughCopy('./preview/src/js/**'); 5 | 6 | return { 7 | markdownTemplateEngine: 'njk', 8 | dataTemplateEngine: 'njk', 9 | htmlTemplateEngine: 'njk', 10 | dir: { 11 | input: 'preview/src', 12 | output: 'preview/dist' 13 | } 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sprucecss", 3 | "version": "2.3.4", 4 | "description": "Spruce CSS - Another CSS Framework", 5 | "keywords": [ 6 | "css", 7 | "css-framework", 8 | "framework", 9 | "front-end", 10 | "responsive", 11 | "sass", 12 | "sass-framework", 13 | "web" 14 | ], 15 | "type": "module", 16 | "author": "Cone (https://conedevelopment.com)", 17 | "homepage": "https://sprucecss.com", 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/conedevelopment/sprucecss.git" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/conedevelopment/sprucecss/issues" 24 | }, 25 | "scripts": { 26 | "autoprefixer": "postcss css/*.css -u autoprefixer --replace --no-map", 27 | "delete:dev-folder": "del-cli --force css", 28 | "eleventy:serve": "npx eleventy --serve", 29 | "prod": "npm-run-all delete:dev-folder sass:prod:expanded sass:prod:compressed autoprefixer", 30 | "sass:preview": "sass --watch --update --style=expanded --no-source-map preview/src/scss:preview/src/css --load-path=scss", 31 | "sass:prod:expanded": "sass --no-source-map --style=expanded scss/spruce-styles.scss:css/spruce.css", 32 | "sass:prod:compressed": "sass --no-source-map --style=compressed scss/spruce-styles.scss:css/spruce.min.css", 33 | "sass:lint": "stylelint scss/**/*.scss", 34 | "sass:lint:fix": "stylelint scss/**/*.scss --fix", 35 | "start": "npm-run-all --parallel sass:preview eleventy:serve", 36 | "test": "jest" 37 | }, 38 | "license": "MIT", 39 | "devDependencies": { 40 | "@11ty/eleventy": "^3.0.0", 41 | "autoprefixer": "^10.4.21", 42 | "del-cli": "^6.0.0", 43 | "jest": "^29.7.0", 44 | "jest-environment-node-single-context": "^29.4.0", 45 | "npm-run-all": "^4.1.5", 46 | "postcss-cli": "^11.0.1", 47 | "sass": "^1.86.3", 48 | "sass-true": "^8.1.0", 49 | "stylelint": "^16.18.0", 50 | "stylelint-config-sass-guidelines": "^12.1.0", 51 | "stylelint-order": "^6.0.4" 52 | }, 53 | "jest": { 54 | "testEnvironment": "jest-environment-node-single-context" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /preview/dist/component/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | Component - Spruce CSS Preview 27 | 28 | 29 | 30 | 31 |
32 |
33 |
34 | 138 |
139 | 140 | 141 | 142 |
143 |
144 |
145 |
146 | 154 | 155 | 156 | 157 | 158 | -------------------------------------------------------------------------------- /preview/dist/font/montserrat-v25-latin-ext_latin-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/conedevelopment/sprucecss/0d57b4ae03be67e8b9db47b3c1292bcbf585548f/preview/dist/font/montserrat-v25-latin-ext_latin-700.woff2 -------------------------------------------------------------------------------- /preview/dist/font/montserrat-v25-latin-ext_latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/conedevelopment/sprucecss/0d57b4ae03be67e8b9db47b3c1292bcbf585548f/preview/dist/font/montserrat-v25-latin-ext_latin-regular.woff2 -------------------------------------------------------------------------------- /preview/dist/function/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | Function - Spruce CSS Preview 27 | 28 | 29 | 30 | 31 |
32 |
33 |
34 | 138 |
139 | 140 |

spacer

141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 | 153 |
154 |
155 |
156 |
157 | 165 | 166 | 167 | 168 | 169 | -------------------------------------------------------------------------------- /preview/dist/index copy/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | Home - Spruce CSS Preview 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 53 | 114 | 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /preview/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | Spruce CSS Preview - Minimal, modern CSS framework 27 | 28 | 29 | 30 | 31 |
32 |
33 |
34 | 138 |
139 | 140 | home 141 | 142 |
143 |
144 |
145 |
146 | 154 | 155 | 156 | 157 | 158 | -------------------------------------------------------------------------------- /preview/dist/js/reading-direction.js: -------------------------------------------------------------------------------- 1 | document.getElementById('reading-direction').addEventListener('input', (e) => { 2 | document.documentElement.setAttribute('dir', e.target.value); 3 | localStorage.setItem('spruce-reading-direction', e.target.value, 31556926, '/'); 4 | }, false); 5 | -------------------------------------------------------------------------------- /preview/dist/js/theme-mode.js: -------------------------------------------------------------------------------- 1 | document.getElementById('theme-mode').addEventListener('input', (e) => { 2 | document.documentElement.dataset.themeMode = e.target.value; 3 | localStorage.setItem('spruce-theme-mode', e.target.value, 31556926, '/'); 4 | }, false); 5 | -------------------------------------------------------------------------------- /preview/src/_data/helper.js: -------------------------------------------------------------------------------- 1 | export const getLinkActiveState = (itemUrl, pageUrl) => { 2 | let response = ''; 3 | 4 | if (itemUrl === pageUrl) { 5 | response = ' aria-current="page"'; 6 | } 7 | 8 | if (itemUrl.length > 1 && pageUrl.indexOf(itemUrl) === 0) { 9 | response += ' data-state="active"'; 10 | } 11 | 12 | return response; 13 | }; 14 | 15 | export const currentYear = () => { 16 | const today = new Date(); 17 | return today.getFullYear(); 18 | }; 19 | -------------------------------------------------------------------------------- /preview/src/_data/navigation.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Color", 4 | "url": "/color/" 5 | }, 6 | { 7 | "caption": "Typography", 8 | "url": "/typography/" 9 | }, 10 | { 11 | "caption": "Table", 12 | "url": "/table/" 13 | }, 14 | { 15 | "caption": "Media", 16 | "url": "/media/" 17 | }, 18 | { 19 | "caption": "Button", 20 | "url": "/button/" 21 | }, 22 | { 23 | "caption": "Form", 24 | "url": "/form/" 25 | }, 26 | { 27 | "caption": "Function", 28 | "url": "/function/" 29 | }, 30 | { 31 | "caption": "Mixin", 32 | "url": "/mixin/" 33 | }, 34 | { 35 | "caption": "Component", 36 | "url": "/component/" 37 | } 38 | ] 39 | -------------------------------------------------------------------------------- /preview/src/_data/site.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Spruce CSS Preview" 3 | } 4 | -------------------------------------------------------------------------------- /preview/src/_includes/layout/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 15 | {% include "partial/meta.html" %} 16 | 17 | 18 | 19 |
20 |
21 |
22 | 54 |
55 | {{ content | safe }} 56 |
57 |
58 |
59 |
60 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /preview/src/_includes/partial/meta.html: -------------------------------------------------------------------------------- 1 | {% if page.url.length > 1 %} 2 | {% set pageTitle = title + ' - ' + site.name %} 3 | {% else %} 4 | {% set pageTitle = site.name + ' - Minimal, modern CSS framework' %} 5 | {% endif %} 6 | 7 | {% if site.name === title %} 8 | {% set pageTitle = title %} 9 | {% endif %} 10 | 11 | {% set siteTitle = site.name %} 12 | {% set currentUrl = site.url + page.url %} 13 | 14 | {% if metaTitle %} 15 | {% set pageTitle = metaTitle %} 16 | {% endif %} 17 | 18 | {{ pageTitle }} 19 | -------------------------------------------------------------------------------- /preview/src/button.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Button" 3 | layout: "layout/base.html" 4 | --- 5 | 6 |

Variants

7 |
8 | 9 | 10 |
11 |

Outline

12 |
13 | 14 | 15 |
16 |

Sizes

17 |
18 | 19 | 20 | 21 |
22 |

Disabled

23 |
24 | 25 | 26 |
27 |

Shadow button

28 |
29 | 30 | 31 |
32 |

Custom button

33 |
34 | 35 | 36 |
37 |

Custom outline button

38 |
39 | 40 | 41 |
42 |

Clear Button

43 |
44 | 45 |
46 |

With Icons

47 |
48 | 53 | 58 | 63 | -------------------------------------------------------------------------------- /preview/src/component.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Component" 3 | layout: "layout/base.html" 4 | --- 5 | 6 | 7 | -------------------------------------------------------------------------------- /preview/src/font/montserrat-v25-latin-ext_latin-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/conedevelopment/sprucecss/0d57b4ae03be67e8b9db47b3c1292bcbf585548f/preview/src/font/montserrat-v25-latin-ext_latin-700.woff2 -------------------------------------------------------------------------------- /preview/src/font/montserrat-v25-latin-ext_latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/conedevelopment/sprucecss/0d57b4ae03be67e8b9db47b3c1292bcbf585548f/preview/src/font/montserrat-v25-latin-ext_latin-regular.woff2 -------------------------------------------------------------------------------- /preview/src/function.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Function" 3 | layout: "layout/base.html" 4 | --- 5 | 6 |

spacer

7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /preview/src/index.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Home" 3 | layout: "layout/base.html" 4 | --- 5 | -------------------------------------------------------------------------------- /preview/src/js/reading-direction.js: -------------------------------------------------------------------------------- 1 | document.getElementById('reading-direction').addEventListener('input', (e) => { 2 | document.documentElement.setAttribute('dir', e.target.value); 3 | localStorage.setItem('spruce-reading-direction', e.target.value, 31556926, '/'); 4 | }, false); 5 | -------------------------------------------------------------------------------- /preview/src/js/theme-mode.js: -------------------------------------------------------------------------------- 1 | document.getElementById('theme-mode').addEventListener('input', (e) => { 2 | document.documentElement.dataset.themeMode = e.target.value; 3 | localStorage.setItem('spruce-theme-mode', e.target.value, 31556926, '/'); 4 | }, false); 5 | -------------------------------------------------------------------------------- /preview/src/media.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Media" 3 | layout: "layout/base.html" 4 | --- 5 | 6 |
7 | 8 |
9 | 10 | ... 11 | 12 |
... 13 |
A caption for the above image.
14 |
15 | 16 |
17 | 18 |
19 | -------------------------------------------------------------------------------- /preview/src/mixin.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Mixin" 3 | layout: "layout/base.html" 4 | --- 5 | 6 |

short-ring

7 |
8 | 9 | 10 |
11 |

scrollbar

12 |
13 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ut diam lorem. Morbi porttitor venenatis dui, hendrerit volutpat dolor suscipit sed. Maecenas leo neque, pulvinar tempus ultricies a, eleifend eget sem. Fusce in egestas ex, nec ultricies eros. Pellentesque mollis egestas nulla, quis gravida urna facilisis sit amet. Nulla facilisi. Nam quis libero sem. Nulla eget justo eros. Nam rutrum a nunc et luctus. Nunc lobortis arcu metus, eget vehicula lacus malesuada quis. Cras quis elementum lacus. Quisque in ante dui. Curabitur vel rhoncus nisl.

14 |

Praesent molestie dui vitae lacinia commodo. Maecenas viverra ipsum accumsan venenatis lobortis. Nullam ligula nisl, hendrerit non consequat eu, aliquam ac turpis. Etiam convallis nibh non ante feugiat, eu posuere magna pharetra. Cras et odio varius, pellentesque ante in, ultrices dolor. Nulla id eleifend felis, nec porttitor eros. Nunc nec est posuere, vulputate tellus ut, commodo odio. Maecenas sagittis dolor vel rutrum lobortis. Mauris sit amet arcu id ipsum feugiat gravida aliquam vel tellus. Nam et aliquam diam. Nullam sit amet porttitor ligula, ut eleifend lacus. Sed tincidunt mollis sodales. Pellentesque feugiat sed dolor a faucibus. Donec eget eros nec mi dictum aliquam sit amet ut sapien.

15 |
16 |

a11y-card-link

17 | 18 |

text-ellipsis

19 |
20 |

Multiline ellipsis test Pellentesque quis sapien quis ex tincidunt ornare sit amet eu neque. Fusce ut tempor nisl, vitae semper dui. Fusce sagittis felis orci, ut ultrices justo congue a. Aenean a auctor nisi, eu fringilla erat. Cras id lacinia lorem. Nunc porttitor lorem tortor, sit amet faucibus urna vestibulum ac. Sed facilisis tristique consequat. Fusce condimentum sed enim eu varius. Vestibulum scelerisque sapien vulputate maximus commodo. Fusce dictum erat nec felis convallis elementum. Suspendisse blandit nibh varius, varius lacus quis, commodo orci. Mauris non mollis tellus. Maecenas id consectetur erat, at cursus ipsum.

21 |

Multiline ellipsis test Pellentesque quis sapien quis ex tincidunt ornare sit amet eu neque. Fusce ut tempor nisl, vitae semper dui. Fusce sagittis felis orci, ut ultrices justo congue a. Aenean a auctor nisi, eu fringilla erat. Cras id lacinia lorem. Nunc porttitor lorem tortor, sit amet faucibus urna vestibulum ac. Sed facilisis tristique consequat. Fusce condimentum sed enim eu varius. Vestibulum scelerisque sapien vulputate maximus commodo. Fusce dictum erat nec felis convallis elementum. Suspendisse blandit nibh varius, varius lacus quis, commodo orci. Mauris non mollis tellus. Maecenas id consectetur erat, at cursus ipsum.

22 | 23 |
24 |

selection

25 |
26 |

Sed facilisis tristique consequat. Fusce condimentum sed enim eu varius. Vestibulum scelerisque sapien vulputate maximus commodo. Fusce dictum erat nec felis convallis elementum. Suspendisse blandit nibh varius, varius lacus quis, commodo orci. Mauris non mollis tellus. Maecenas id consectetur erat, at cursus ipsum.

27 |

SELECTION Mauris non mollis tellus. Maecenas id consectetur erat, at cursus ipsum.

28 |
29 |

clear-list

30 | 36 |

spacer-clamp

37 |

Mauris non mollis tellus. Maecenas id consectetur erat, at cursus ipsum.

38 |

word-wrap

http://www.reallylong.link/rll/eHljLISQs4/VYnSQH4F8X3sNpWLpziElYnqUVCq8WmbR06lHV7vCttOFydbFBKzBovFUYDMl_SGFCRd3eWTUpIggT5TLgATzFQELX_YUMb4U6ah7UG/7sNSn6XjPWAke0FUy26kfo3ckKU0WtJbyeWNBoliQ4iI7ZIu0lSMD4E_AKRrWT_d27gw3/j/GA/xdi9_3xB3Lq2DcUndrHNPm0_iTVNbvxmodOkGqq3rsQwNU3G5mfWVAgKTTs64NlZ6NA11mQDTLLZQ_X6SCqZh3d8wnG2d2ZwrZ/3OU1hdOiWNKnry0Ml8_X/Oo3EZRHLGpv_mehdAfy4KOzDtsYYdeBfDTMj1QBwvVmh8r9sOqthOgtIE15muoY7dPw_SHWVSo8XoH68tdZr2nqm4NiOAjrchNnaS9LqpLD5Zd09_Tpj0hwgfQFAvlA0_0TQzI9xsV1bxB_zY9Y8a0DeP/_sijH9kCQV0rdKT8Fu6QcJ_MPELWSEmdvVqyBlLyjgWHcyAKzJNuU1CnLf425IOTLpjmAUVvCElISwCCdw1_4Klv4riPll1dfvZjU5QaBx3Vq/EZKK8Z247sL5vuap9pOW04CrkQzsLZF2RRLyZ98C5FWXJP9uD41SfTWR8f9ybpA1YaibXaQgTHgtHaFW/kbbmR3zl3LRhV0HZD9/vY4QyRoi4CHnZpF2wq/nDRLtY3Jy7AiBjUQGMi80UTmzucFY/2sV5VcvLObY7UrbVOlQax/dCjxP0NPAQMLJukneUmvf0C3uzoXm07DxynvusbvVejeexdxU9f5Hdqg1V8yKxYpD9NAOB5dPRBdSoT1uhO1IcVIziwdU0S5Ybmf7fqF06JjxIQQXetOf6w6d4P3BZER1FfD/WA7zj7p/KrqxK6ZMFu_npgHrWTI2qF7bn_nAzti9DyGCFaIyqKSmPMQVMyyF/VWjjF2bauMKliqzInbC9sE3pAf82Mgx570z3ThYHK/FRKT7YxrUWURUb1c2JF3vNs28t/G6c63XX0LTSloOpE1Ci1WNHdwzVKHVa1oz5MaW_1lW_Vb/km231/qu0rlV5hpQCH_0QqkJw7/p1uHBoWQq5TUxeH5RyfBwNI/8XYAa1ybPIGhtXQrnv7VYyG2P4pKkIXMjJkUHz4yAGSMT0C8ZShZHAG_xPmhhwpzXZaNsh4C44u5h/6sfjDD2a7YcfyxsGFd4bqfUqsJcwF7g1GbZeBeA68AsIGcA6ZOa_f1Rnzff858u/2X6yhCn53DSgHwQZIei1xo2OSI6Ia8UjgzW10hMr_fbkOuO64nqKTv9Rzk1cktR1lNnBCBZUaU2I0uUlsp4ELG1073iKab_kkPx0jfxNc6F9abeCp3zrTxu6Foo_bplsKdRUZe25wI_hVpc7PUzIxe/Agaf9/WQwO7azadwWDkJJfAHNruhwH6yVfS5JVR_4DT7CcjdowIV2_bNFmJjCljqn__vBn04FsT6nhCBpFCKV5vmGJtXI935staZS1eSp230RFl3hlCEkUxovrAvXKyPVv1OrndCeylLR8mGkUc/rTnl04YgMlqqflSOFdVIcqqH1JvgrhMFtW/myZ/R06O3dKMxppl2Az8NkU/OmYpcOZ4YrcCsFzpsVu2ybi6sjm_AYgC01zERrEoJ9ATX0BpxSdyjmF6XWZKP3iFcRsgv7otfGqzx1mMSVPBddS/jknevdQNezn3/7Qy/sUVKnS1sPEh0bNrxdJ4r1ovAM2Q1esGHxd7u67TA7iTJty3K5Zbh/poh7000k0ZjTLVhmP1PF2f0m3JjIsRoYJkUsjJNSBrnjflSJJbrjoKzFOdR2Og43phCRKDSRIBkZS6ZQULcYD2ptuQjppoTbcRuQhxsrhrEayABlU0KptmBX_tRrtj0zxzrpTxsiM0n_pzSFS0M4Os5EVkp9ZwqVKC6i7U8dafrpUAgk8VK_4pyQ3MFxk1eu2l_w/aS0yM96 39 | -------------------------------------------------------------------------------- /preview/src/scss/component/_alert.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:color'; 2 | @use 'sass:map'; 3 | @use 'spruce' as *; 4 | 5 | .alert { 6 | align-items: center; 7 | border: 1px solid; 8 | border-left: 0.4rem solid; 9 | border-radius: config('border-radius-sm', $display); 10 | display: flex; 11 | gap: spacer('m'); 12 | justify-content: space-between; 13 | line-height: config('line-height-md', $typography); 14 | padding: 0.65em 1em; 15 | 16 | @each $name, $value in map.get($colors, 'alert') { 17 | @at-root .alert--#{$name} { 18 | background-color: color.scale($value, $lightness: 95%); 19 | color: color.scale($value, $lightness: -30%); 20 | } 21 | 22 | @at-root .alert--#{$name} .alert__close { 23 | background-color: color.scale($value, $lightness: -30%); 24 | color: color.scale($value, $lightness: 90%); 25 | } 26 | } 27 | 28 | @each $name, $value in map.get($colors, 'alert') { 29 | @at-root [data-theme-mode='dark'] .alert--#{$name} { 30 | background-color: transparent; 31 | border-color: color.scale($value, $lightness: -30%); 32 | color: color('text'); 33 | } 34 | } 35 | 36 | &__caption { 37 | @include layout-stack('xxs'); 38 | } 39 | 40 | &__close { 41 | --dimension: 1.5rem; 42 | @include clear-btn; 43 | @include transition; 44 | align-items: center; 45 | block-size: var(--dimension); 46 | border-radius: config('border-radius-sm', $display); 47 | display: flex; 48 | flex-shrink: 0; 49 | inline-size: var(--dimension); 50 | justify-content: center; 51 | 52 | &:hover, 53 | &:focus { 54 | opacity: 0.75; 55 | } 56 | 57 | svg { 58 | --dimension: 0.5rem; 59 | block-size: var(--dimension); 60 | inline-size: var(--dimension); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /preview/src/scss/component/_color.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | @use 'spruce' as *; 3 | 4 | .colors { 5 | @include layout-grid('s:m', $minimum: 15rem); 6 | } 7 | 8 | .color-item { 9 | align-items: start; 10 | display: flex; 11 | gap: spacer(m); 12 | 13 | &--bordered &__color { 14 | box-shadow: 0 0 0 1px color('border') inset; 15 | } 16 | 17 | &__color { 18 | border-radius: config('border-radius-lg', $display); 19 | flex-shrink: 0; 20 | block-size: 3rem; 21 | inline-size: 3rem; 22 | } 23 | 24 | &__caption { 25 | display: flex; 26 | flex-direction: column; 27 | line-height: config('line-height-md', $typography); 28 | } 29 | 30 | &__name { 31 | color: color('heading'); 32 | font-weight: 700; 33 | } 34 | 35 | &__value { 36 | font-weight: 300; 37 | } 38 | } 39 | 40 | @each $name, $value in map.get($colors, 'base') { 41 | .color-item--base-#{$name} .color-item__color { 42 | background-color: color($name, 'base'); 43 | } 44 | } 45 | 46 | @each $name, $value in map.get($colors, 'selection') { 47 | .color-item--selection-#{$name} .color-item__color { 48 | background-color: color($name, 'selection'); 49 | } 50 | } 51 | 52 | @each $name, $value in map.get($colors, 'alert') { 53 | .color-item--alert-#{$name} .color-item__color { 54 | background-color: color($name, 'alert'); 55 | } 56 | } 57 | 58 | @each $name, $value in map.get($colors, 'btn') { 59 | .color-item--btn-#{$name} .color-item__color { 60 | background-color: color($name, 'btn'); 61 | } 62 | } 63 | 64 | @each $name, $value in map.get($colors, 'form') { 65 | .color-item--form-#{$name} .color-item__color { 66 | background-color: color($name, 'form'); 67 | } 68 | } 69 | 70 | @each $name, $value in map.get($colors, 'table') { 71 | .color-item--table-#{$name} .color-item__color { 72 | background-color: color($name, 'table'); 73 | } 74 | } 75 | 76 | @each $name, $value in map.get($colors, 'scrollbar') { 77 | .color-item--scrollbar-#{$name} .color-item__color { 78 | background-color: color($name, 'scrollbar'); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /preview/src/scss/component/_index.scss: -------------------------------------------------------------------------------- 1 | @forward 'color'; 2 | @forward 'navigation'; 3 | @forward 'alert'; 4 | @forward 'preview'; 5 | -------------------------------------------------------------------------------- /preview/src/scss/component/_navigation.scss: -------------------------------------------------------------------------------- 1 | @use 'spruce' as *; 2 | 3 | .navigation-menu { 4 | @include clear-list; 5 | @include layout-stack('xs'); 6 | 7 | a { 8 | color: color('text'); 9 | text-decoration: none; 10 | 11 | &[aria-current='page'] { 12 | color: color('primary'); 13 | font-weight: 700; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /preview/src/scss/component/_preview.scss: -------------------------------------------------------------------------------- 1 | @use 'spruce' as *; 2 | 3 | .preview-group { 4 | align-items: start; 5 | display: flex; 6 | flex-wrap: wrap; 7 | gap: spacer('s'); 8 | } 9 | -------------------------------------------------------------------------------- /preview/src/scss/config/_config.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:color'; 2 | @use 'dark-colors' as dark; 3 | 4 | $color-tertiary: hsl(337deg 55% 43%); 5 | $spacer: 1.25rem; 6 | 7 | @use 'spruce' with ( 8 | $colors: ( 9 | 'btn': ( 10 | 'custom-background': hsl(0deg 0% 100%), 11 | 'custom-background-hover': hsl(332deg 49% 29%), 12 | 'custom-foreground': hsl(0deg 0% 0%), 13 | 'custom-foreground-hover': hsl(0deg 0% 100%), 14 | 'dark-background': hsl(0deg 0% 0%), 15 | 'dark-background-hover': hsl(0deg 0% 20%), 16 | 'dark-foreground': hsl(0deg 0% 100%), 17 | 'dark-outline-border': hsl(260deg 4% 70%), 18 | 'dark-outline-foreground': hsl(0deg 0% 0%), 19 | 'dark-outline-foreground-hover': hsl(0deg 0% 100%), 20 | 'dark-outline-background-hover': hsl(0deg 0% 0%), 21 | 'dark-outline-focus-ring': hsl(295deg 100% 50%), 22 | 'tertiary-background': $color-tertiary, 23 | 'tertiary-foreground': hsl(0deg 0% 100%), 24 | 'tertiary-shadow-focus': color.adjust($color-tertiary, $alpha: -0.75), 25 | ), 26 | form: ( 27 | 'check-background': red, 28 | 'check-focus-ring': olive, 29 | 'check-foreground': aqua, 30 | 'switch-background': pink, 31 | 'switch-focus-ring': blue, 32 | 'switch-foreground': yellow, 33 | ), 34 | ), 35 | $dark-colors: dark.$colors, 36 | $form-select: ( 37 | 'padding-inline-end': 4rem, 38 | ), 39 | $settings: ( 40 | 'css-custom-properties': true, 41 | 'color-fallback': false, 42 | 'html-smooth-scrolling': true, 43 | 'hyphens': true, 44 | 'prefix': 'spruce', 45 | 'print': true, 46 | 'scaler': 25, 47 | 'utilities': ( 48 | 'typography': true, 49 | ), 50 | ), 51 | $btn-lg: ( 52 | 'text-transform': uppercase, 53 | ), 54 | $btn-sm: ( 55 | 'icon-size': 1.25rem, 56 | ), 57 | $form-fieldset: ( 58 | 'layout-gap': 1rem, 59 | 'legend-font-family': serif, 60 | ), 61 | $form-label: ( 62 | 'text-transform': uppercase, 63 | ), 64 | $form-range: ( 65 | 'thumb-block-size': 2rem, 66 | 'thumb-inline-size': 2rem, 67 | ), 68 | $generators: ( 69 | 'content': ( 70 | 'normalize': true, 71 | ), 72 | 'form': ( 73 | 'btn': true, 74 | ), 75 | ), 76 | $layout: ( 77 | 'container-inline-size': 70rem, 78 | ), 79 | $typography: ( 80 | 'letter-spacing-heading': 0.05em, 81 | ), 82 | ); 83 | -------------------------------------------------------------------------------- /preview/src/scss/config/_dark-colors.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:color'; 2 | 3 | $color-black: hsl(206deg 100% 7%); 4 | $color-danger: hsl(0deg 71% 51%); 5 | $color-gray: hsl(0deg 0% 97%); 6 | $color-gray-dark: hsl(0deg 0% 100% / 8%); 7 | $color-primary: hsl(261deg 54% 70%); 8 | $color-secondary: hsl(227deg 92% 55%); 9 | $color-success: hsl(150deg 100% 33%); 10 | $color-white: hsl(0deg 0% 95%); 11 | 12 | $colors: ( 13 | 'base': ( 14 | 'background': $color-black, 15 | 'blockquote-border': $color-primary, 16 | 'border': $color-gray-dark, 17 | 'code-background': hsl(207deg 64% 18%), 18 | 'code-foreground': $color-white, 19 | 'heading': $color-white, 20 | 'link': $color-primary, 21 | 'link-hover': color.scale($color-primary, $lightness: 20%), 22 | 'mark-background': hsl(50deg 100% 80%), 23 | 'mark-foreground': $color-black, 24 | 'marker': $color-primary, 25 | 'primary': $color-primary, 26 | 'secondary': $color-secondary, 27 | 'strong': $color-white, 28 | 'text': $color-gray, 29 | ), 30 | 'btn': ( 31 | 'primary-background': hsl(261deg 52% 59%), 32 | 'primary-background-hover': hsl(261deg 52% 65%), 33 | 'primary-foreground': $color-white, 34 | 'primary-shadow': color.adjust($color-primary, $lightness: -25%), 35 | 'secondary-background': $color-secondary, 36 | 'secondary-background-hover': color.adjust($color-secondary, $lightness: 5%), 37 | 'secondary-foreground': $color-white, 38 | 'secondary-shadow': color.adjust($color-secondary, $lightness: -20%), 39 | ), 40 | 'form': ( 41 | 'background': color.scale($color-black, $lightness: 5%), 42 | 'background-disabled': $color-black, 43 | 'border': $color-gray-dark, 44 | 'border-disabled': $color-gray-dark, 45 | 'border-focus': $color-primary, 46 | 'check-background': $color-primary, 47 | 'check-focus-ring': $color-primary, 48 | 'check-foreground': $color-black, 49 | 'group-label-background': color.scale($color-black, $lightness: 2.5%), 50 | 'group-label-foreground': $color-gray, 51 | 'invalid': $color-danger, 52 | 'invalid-focus-ring': color.adjust($color-danger, $alpha: -0.75), 53 | 'label': $color-white, 54 | 'legend': $color-white, 55 | 'placeholder': hsl(0deg 0% 60%), 56 | 'range-thumb-background': $color-primary, 57 | 'range-thumb-focus-ring': $color-primary, 58 | 'range-track-background': $color-gray-dark, 59 | 'ring-focus': color.adjust($color-primary, $alpha: -0.75), 60 | 'select-foreground': hsl(0deg 0% 100%), 61 | 'text': $color-gray, 62 | 'valid': $color-success, 63 | 'valid-focus-ring': color.adjust($color-success, $alpha: -0.75), 64 | ), 65 | 'selection': ( 66 | 'background': $color-primary, 67 | 'foreground': $color-white, 68 | ), 69 | 'scrollbar': ( 70 | 'thumb-background': hsl(0deg 0% 100% / 15%), 71 | 'thumb-background-hover': hsl(0deg 0% 100% / 25%), 72 | 'track-background': hsl(0deg 0% 100% / 5%), 73 | ), 74 | 'table': ( 75 | 'border': $color-gray-dark, 76 | 'caption': $color-gray, 77 | 'heading': $color-white, 78 | 'hover': hsl(0deg 0% 100% / 5%), 79 | 'text': $color-gray, 80 | 'stripe': hsl(0deg 0% 100% / 2.5%), 81 | ), 82 | ); 83 | -------------------------------------------------------------------------------- /preview/src/scss/config/_dark-mode.scss: -------------------------------------------------------------------------------- 1 | @use 'spruce' as *; 2 | 3 | @include generate-color-variables( 4 | $dark-colors, 5 | ':root[data-theme-mode="dark"]' 6 | ); 7 | 8 | [data-theme-mode='dark'] { 9 | color-scheme: dark; 10 | 11 | select.form-control:not([multiple]):not([size]) { 12 | @include field-icon( 13 | config('select', $form-icon, false), 14 | color('select-foreground', 'form', true, $dark-colors) 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /preview/src/scss/config/_font.scss: -------------------------------------------------------------------------------- 1 | @use 'spruce' as *; 2 | 3 | @include font-face( 4 | 'Montserrat', 5 | '../font/montserrat-v25-latin-ext_latin-regular.woff2' 6 | ); 7 | 8 | @include font-face( 9 | 'Montserrat', 10 | '../font/montserrat-v25-latin-ext_latin-700.woff2', 11 | 700 12 | ); 13 | -------------------------------------------------------------------------------- /preview/src/scss/config/_index.scss: -------------------------------------------------------------------------------- 1 | @forward 'config'; 2 | @forward 'font'; 3 | @forward 'styles'; 4 | @forward 'dark-mode'; 5 | -------------------------------------------------------------------------------- /preview/src/scss/config/_styles.scss: -------------------------------------------------------------------------------- 1 | @use 'spruce' as *; 2 | 3 | @include generate-styles; 4 | -------------------------------------------------------------------------------- /preview/src/scss/layout/_container.scss: -------------------------------------------------------------------------------- 1 | @use 'spruce' as *; 2 | 3 | .container { 4 | --inline-size: #{config('container-inline-size', $layout, false)}; 5 | --gap: #{spacer-clamp('m', 'l')}; 6 | 7 | @include layout-center( 8 | var(--gap), 9 | var(--inline-size) 10 | ); 11 | 12 | &--narrow { 13 | --inline-size: 50rem; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /preview/src/scss/layout/_index.scss: -------------------------------------------------------------------------------- 1 | @forward 'container'; 2 | @forward 'main'; 3 | -------------------------------------------------------------------------------- /preview/src/scss/layout/_main.scss: -------------------------------------------------------------------------------- 1 | @use 'spruce' as *; 2 | 3 | .l-main { 4 | padding-block: spacer-clamp('l', 'xl'); 5 | 6 | &__inner { 7 | @include layout-sidebar('l:xl', 12rem); 8 | } 9 | 10 | &__sidebar { 11 | &-inner { 12 | inset-block-start: spacer('m'); 13 | position: sticky; 14 | @include layout-stack('m'); 15 | } 16 | } 17 | 18 | &__body { 19 | @include layout-stack('s'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /preview/src/scss/main.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:color'; 2 | @use 'sass:math'; 3 | @use 'sass:map'; 4 | 5 | @forward 'config'; 6 | @forward 'layout'; 7 | @forward 'component'; 8 | 9 | @use 'spruce' as *; 10 | 11 | :root { 12 | @include set-css-variable(( 13 | --section-gap: spacer-clamp('xl', 'xxl') 14 | )); 15 | } 16 | 17 | @include generate-form-check( 18 | '.wpcf7-list-item label', 19 | '.wpcf7-list-item input', 20 | '.wpcf7-list-item .wpcf7-list-item-label' 21 | ); 22 | 23 | @include generate-table( 24 | 'table', 25 | false, 26 | false 27 | ); 28 | 29 | .btn--dark { 30 | @include btn-variant('dark'); 31 | } 32 | 33 | .btn--outline-dark { 34 | @include btn-variant-outline('dark'); 35 | } 36 | 37 | .btn--custom { 38 | @include btn-variant('custom'); 39 | box-shadow: -3px 5px color('custom-foreground', 'btn'); 40 | border: 3px solid color('custom-foreground', 'btn'); 41 | border-radius: 0; 42 | font-family: config('font-family-cursive', $typography); 43 | } 44 | 45 | .btn--tertiary { 46 | @include btn-variant('tertiary'); 47 | } 48 | 49 | .clear-btn { 50 | @include clear-btn; 51 | } 52 | 53 | .section-title { 54 | font-size: font-size('h4'); 55 | margin-block: 0 spacer('m'); 56 | 57 | * + & { 58 | margin-block-start: spacer('l'); 59 | } 60 | } 61 | 62 | .ellipsis-1 { 63 | @include text-ellipsis(1); 64 | } 65 | 66 | .ellipsis-2 { 67 | @include text-ellipsis(2); 68 | } 69 | 70 | .selection-1 { 71 | @include selection('secondary', $is-direct: true); 72 | @include transition; 73 | } 74 | 75 | .selection-2 { 76 | @include selection(#000, $is-direct: true); 77 | } 78 | 79 | .ellipsis-btn { 80 | @include text-ellipsis(1); 81 | max-inline-size: 10ch; 82 | } 83 | 84 | .scrollbar { 85 | max-block-size: 15rem; 86 | overflow: auto; 87 | padding-inline-end: spacer('m'); 88 | @include scrollbar; 89 | @include layout-stack; 90 | } 91 | 92 | .custom-heading-size { 93 | font-size: responsive-font-size(4rem, 30, 4vw); 94 | font-family: 'Montserrat', sans-serif; 95 | } 96 | 97 | .custom-link { 98 | @include transition(2s, background-color, linear); 99 | 100 | &:hover { 101 | background-color: aqua; 102 | } 103 | } 104 | 105 | .cleared-list { 106 | @include clear-list; 107 | } 108 | 109 | .card { 110 | @include a11y-card-link('.card__link', true); 111 | border: 1px solid color('border'); 112 | border-radius: config('border-radius-lg', $display); 113 | padding: spacer('m'); 114 | 115 | &__link { 116 | color: color-value('heading'); 117 | font-size: font-size('h3'); 118 | font-weight: 700; 119 | text-decoration: none; 120 | } 121 | } 122 | 123 | .break-long-url { 124 | @include word-wrap; 125 | } 126 | 127 | .btn-group { 128 | display: flex; 129 | flex-wrap: wrap; 130 | gap: spacer('s'); 131 | } 132 | 133 | .form-group { 134 | &--height-test { 135 | align-items: center; 136 | display: flex; 137 | gap: spacer('xs'); 138 | } 139 | } 140 | 141 | .form-group--stacked\:md { 142 | @include form-group-stacked('md'); 143 | } 144 | 145 | .ring-wrapper { 146 | align-items: center; 147 | display: flex; 148 | flex-wrap: wrap; 149 | gap: 1rem; 150 | } 151 | 152 | .ring-one, 153 | .ring-two { 154 | --size: 3rem; 155 | @include clear-btn; 156 | block-size: var(--size); 157 | inline-size: var(--size); 158 | } 159 | 160 | .ring-one { 161 | background-color: pink; 162 | 163 | &:focus-visible { 164 | @include short-ring('input'); 165 | } 166 | } 167 | 168 | .ring-two { 169 | background-color: rebeccapurple; 170 | 171 | &:focus-visible { 172 | @include short-ring('button', 'secondary'); 173 | } 174 | } 175 | 176 | .spacer-wrapper { 177 | align-items: center; 178 | display: flex; 179 | gap: spacer('m'); 180 | } 181 | 182 | .spacer { 183 | background-color: red; 184 | 185 | div { 186 | background-color: pink; 187 | block-size: 2rem; 188 | inline-size: 5rem; 189 | } 190 | 191 | &--one { 192 | div { 193 | margin: spacer('m'); 194 | } 195 | } 196 | 197 | &--two { 198 | div { 199 | margin: spacer('s'); 200 | } 201 | } 202 | 203 | &--three { 204 | div { 205 | margin: spacer('xxl:xl'); 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /preview/src/table.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Table" 3 | layout: "layout/base.html" 4 | --- 5 |

Regular

6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
PersonNumberThird Column
Someone Lastname900Nullam quis risus eget urna mollis ornare vel eu leo.
Person Name1200Vestibulum id ligula porta felis euismod semper. Donec ullamcorper nulla non metus auctor fringilla.
Another Person1500Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor. Nullam id dolor id nibh ultricies vehicula ut id elit.
Last One2800Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Cras mattis consectetur purus sit amet fermentum.
39 |
40 |

Rounded

41 |
42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 |
This is a regular sized, rounded, borderless table.
PersonNumberThird Column
Someone Lastname900Nullam quis risus eget urna mollis ornare vel eu leo.
Person Name1200Vestibulum id ligula porta felis euismod semper. Donec ullamcorper nulla non metus auctor fringilla.
Another Person1500Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor. Nullam id dolor id nibh ultricies vehicula ut id elit.
Last One2800Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Cras mattis consectetur purus sit amet fermentum.
74 |
75 |

Small

76 |
77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 |
This is a small sized table.
PersonNumberThird Column
Someone Lastname900Nullam quis risus eget urna mollis ornare vel eu leo.
Person Name1200Vestibulum id ligula porta felis euismod semper. Donec ullamcorper nulla non metus auctor fringilla.
Another Person1500Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor. Nullam id dolor id nibh ultricies vehicula ut id elit.
Last One2800Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Cras mattis consectetur purus sit amet fermentum.
109 |
110 |

In Line

111 |
112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 |
PersonNumberThird Column
Someone Lastname900Nullam quis risus eget urna mollis ornare vel eu leo.
Person Name1200Vestibulum id ligula porta felis euismod semper. Donec ullamcorper nulla non metus auctor fringilla.
Another Person1500Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor. Nullam id dolor id nibh ultricies vehicula ut id elit.
Last One2800Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Cras mattis consectetur purus sit amet fermentum.
143 |
144 |

Classless

145 |
146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 |
PersonNumberThird Column
Someone Lastname900Nullam quis risus eget urna mollis ornare vel eu leo.
Person Name1200Vestibulum id ligula porta felis euismod semper. Donec ullamcorper nulla non metus auctor fringilla.
Another Person1500Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor. Nullam id dolor id nibh ultricies vehicula ut id elit.
Last One2800Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Cras mattis consectetur purus sit amet fermentum.
177 |
178 | -------------------------------------------------------------------------------- /preview/src/typography.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Typography" 3 | layout: "layout/base.html" 4 | --- 5 |

Morbi dui augue, consequat non pulvinar ac, consequat nec massa. Nulla nec purus vitae enim eleifend laoreet quis vitae nunc. Fusce lacinia nunc eget arcu pulvinar finibus. Nulla et egestas augue. Nulla at nunc vel massa ullamcorper posuere. Donec cursus venenatis dui sed aliquam. Curabitur ultrices, odio ac aliquam mollis, urna felis gravida dolor, id mattis ante mauris eu dui.

This is an anchor link 6 |

Custom heading size

7 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit. In aliquam nibh in facilisis vestibulum. Pellentesque bibendum lorem risus, ut viverra lectus blandit sit amet. Ut rhoncus a dui ac euismod.

8 |

The quick brown fox jumps over the lazy dog

9 |

The quick brown fox jumps over the lazy dog

10 |

The quick brown fox jumps over the lazy dog

11 |

The quick brown fox jumps over the lazy dog

12 |
The quick brown fox jumps over the lazy dog
13 |
The quick brown fox jumps over the lazy dog
14 |

Curabitur posuere placerat odio, a suscipit velit consectetur nec. Proin tincidunt gravida risus eu commodo. Vivamus tempus enim ac metus finibus vestibulum. Donec ac sagittis quam, in ullamcorper dolor. Vestibulum rhoncus tempor lacus in commodo. Morbi finibus sapien sed tortor interdum, vitae ornare mi accumsan. Vestibulum rutrum facilisis tincidunt.

15 |
16 |

Phasellus vel tortor mi. Vivamus bibendum erat CSS lacus, quis tincidunt urna dictum non. Fusce vel ex feugiat, faucibus lectus sit amet, accumsan lacus. Quisque cursus leo nunc, ut maximus arcu suscipit ut. 17 | Nulla laoreet felis mauris, quis Ctrl + S urna aliquet ac. Ut ultricies eros pharetra, elementum urna non, mollis eros. Proin viverra, ipsum a laoreet laoreet, nunc erat pulvinar quam, vel vehicula enim nibh non velit. 18 |

19 |
20 | Details 21 |

Nulla et egestas augue. Nulla at nunc vel massa ullamcorper posuere. Donec cursus venenatis dui sed aliquam. Curabitur ultrices, odio ac aliquam mollis, urna felis gravida dolor, id mattis ante mauris eu dui.

22 |
23 |

Custom link, hey

24 |
1 Infinite Loop, CA 95014
United States
25 |
26 |
“Two things are infinite: the universe and human stupidity; and I'm not sure about the universe.”
27 |
— Albert Einstein, 28 | Quote Investigator 29 |
30 |
31 |
32 |

“Two things are infinite: the universe and human stupidity; and I'm not sure about the universe.”

33 |
34 |
35 |
Definition List Title
36 |
Definition list division.
37 |
Kitchen Sink
38 |
Used in expressions to describe work in which all conceivable (and some inconceivable) sources have been mined. In this case, a bunch of markup.
39 |
aside
40 |
Defines content aside from the page content
41 |
blockquote
42 |
Defines a section that is quoted from another source
43 |
44 | 64 |
65 |
    66 |
  1. List item one 67 |
      68 |
    1. List item one 69 |
        70 |
      1. List item one
      2. 71 |
      3. List item two
      4. 72 |
      5. List item three
      6. 73 |
      7. List item four
      8. 74 |
      75 |
    2. 76 |
    3. List item two
    4. 77 |
    5. List item three
    6. 78 |
    7. List item four
    8. 79 |
    80 |
  2. 81 |
  3. List item two
  4. 82 |
  5. List item three
  6. 83 |
  7. List item four
  8. 84 |
85 |

Heading level #1

86 | 87 | -------------------------------------------------------------------------------- /scss/config/_breakpoint.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | 3 | $breakpoints: () !default; 4 | $breakpoints: map.merge( 5 | ( 6 | 'xs': 32em, 7 | 'sm': 48em, 8 | 'md': 64em, 9 | 'lg': 80em, 10 | 'xl': 90em, 11 | 'xxl': 110em, 12 | ), 13 | $breakpoints 14 | ); 15 | -------------------------------------------------------------------------------- /scss/config/_button.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | @use 'display' as *; 3 | @use 'typography' as *; 4 | @use 'spacer' as *; 5 | 6 | $btn: () !default; 7 | $btn: map.merge( 8 | ( 9 | 'border-radius': map.get($display, 'border-radius-sm'), 10 | 'border-width': 1px, 11 | 'focus-ring-box-shadow-type': outside, 12 | 'focus-ring-offset': 2px, 13 | 'focus-ring-size': 2px, 14 | 'focus-ring-type': outline, 15 | 'font-family': null, 16 | 'font-size': map.get($typography, 'font-size-base'), 17 | 'font-style': null, 18 | 'font-weight': 500, 19 | 'gap': map.get($spacers, 'xs'), 20 | 'icon-padding': 0.75em, 21 | 'icon-size': 1em, 22 | 'padding': 0.75em 1em, 23 | 'shadow-size': 0.25rem, 24 | 'text-transform': null, 25 | ), 26 | $btn 27 | ); 28 | 29 | $btn-lg: () !default; 30 | $btn-lg: map.merge( 31 | ( 32 | 'font-size': 1.15rem, 33 | 'gap': map.get($spacers, 'xs'), 34 | 'icon-padding': 0.9em, 35 | 'padding': 0.9em 1.15em, 36 | ), 37 | $btn-lg 38 | ); 39 | 40 | $btn-sm: () !default; 41 | $btn-sm: map.merge( 42 | ( 43 | 'font-size': 0.8rem, 44 | 'gap': map.get($spacers, 'xxs'), 45 | 'icon-padding': 0.5em, 46 | 'icon-size': 0.8rem, 47 | 'padding': 0.5em 0.75em, 48 | ), 49 | $btn-sm 50 | ); 51 | -------------------------------------------------------------------------------- /scss/config/_color.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:color'; 2 | @use 'sass:map'; 3 | 4 | $color-black: hsl(205deg 100% 2%) !default; 5 | $color-danger: hsl(0deg 71% 51%) !default; 6 | $color-gray: hsl(208deg 9% 42%) !default; 7 | $color-gray-light: hsl(215deg 63% 93%) !default; 8 | $color-primary: hsl(262deg 71% 49%) !default; 9 | $color-secondary: hsl(227deg 92% 55%) !default; 10 | $color-success: hsl(150deg 100% 33%) !default; 11 | $color-white: hsl(0deg 0% 100%) !default; 12 | 13 | $colors: () !default; 14 | $colors: map.deep-merge( 15 | ( 16 | 'alert': ( 17 | 'danger': $color-danger, 18 | 'info': hsl(195deg 100% 42%), 19 | 'success': $color-success, 20 | 'warning': hsl(48deg 89% 55%), 21 | ), 22 | 'base': ( 23 | 'background': $color-white, 24 | 'blockquote-border': $color-primary, 25 | 'border': $color-gray-light, 26 | 'code-background': color.change($color-primary, $lightness: 97%), 27 | 'code-foreground': $color-black, 28 | 'heading': $color-black, 29 | 'link': $color-primary, 30 | 'link-hover': color.scale($color-primary, $lightness: -20%), 31 | 'mark-background': hsl(50deg 100% 80%), 32 | 'mark-foreground': $color-black, 33 | 'marker': $color-primary, 34 | 'primary': $color-primary, 35 | 'secondary': $color-secondary, 36 | 'strong': $color-black, 37 | 'text': $color-gray, 38 | ), 39 | 'btn': ( 40 | 'primary-background': $color-primary, 41 | 'primary-background-hover': color.adjust($color-primary, $lightness: -10%), 42 | 'primary-foreground': $color-white, 43 | 'primary-shadow': color.adjust($color-primary, $lightness: 35%), 44 | 'secondary-background': $color-secondary, 45 | 'secondary-background-hover': color.adjust($color-secondary, $lightness: -10%), 46 | 'secondary-foreground': $color-white, 47 | 'secondary-shadow': color.adjust($color-secondary, $lightness: 35%), 48 | ), 49 | 'form': ( 50 | 'background': $color-white, 51 | 'background-disabled': hsl(0deg 0% 95%), 52 | 'border': hsl(260deg 4% 70%), 53 | 'border-disabled': $color-gray-light, 54 | 'border-focus': $color-primary, 55 | 'check-background': $color-primary, 56 | 'check-focus-ring': $color-primary, 57 | 'check-foreground': $color-white, 58 | 'group-label-background': hsl(210deg 60% 98%), 59 | 'group-label-foreground': $color-gray, 60 | 'invalid': $color-danger, 61 | 'invalid-focus-ring': color.adjust($color-danger, $alpha: -0.75), 62 | 'label': $color-black, 63 | 'legend': $color-black, 64 | 'placeholder': hsl(208deg 7% 40%), 65 | 'range-thumb-background': $color-primary, 66 | 'range-thumb-focus-ring': $color-primary, 67 | 'range-track-background': $color-gray-light, 68 | 'ring-focus': color.adjust($color-primary, $alpha: -0.75), 69 | 'select-foreground': $color-black, 70 | 'switch-background': $color-primary, 71 | 'switch-focus-ring': $color-primary, 72 | 'switch-foreground': $color-white, 73 | 'text': $color-gray, 74 | 'valid': $color-success, 75 | 'valid-focus-ring': color.adjust($color-success, $alpha: -0.75), 76 | ), 77 | 'selection': ( 78 | 'foreground': $color-white, 79 | 'background': $color-primary, 80 | ), 81 | 'scrollbar': ( 82 | 'thumb-background': hsl(0deg 0% 0% / 15%), 83 | 'thumb-background-hover': hsl(0deg 0% 0% / 25%), 84 | 'track-background': hsl(0deg 0% 0% / 5%), 85 | ), 86 | 'table': ( 87 | 'border': $color-gray-light, 88 | 'caption': $color-gray, 89 | 'heading': $color-black, 90 | 'hover': hsl(0deg 0% 0% / 5%), 91 | 'stripe': hsl(0deg 0% 0% / 2.5%), 92 | 'text': $color-gray, 93 | ), 94 | ), 95 | $colors 96 | ); 97 | 98 | $dark-colors: () !default; 99 | $dark-colors: map.deep-merge( 100 | (), 101 | $dark-colors 102 | ); 103 | -------------------------------------------------------------------------------- /scss/config/_display.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | 3 | $display: () !default; 4 | $display: map.merge( 5 | ( 6 | 'border-radius-lg': 0.925rem, 7 | 'border-radius-sm': 0.425rem, 8 | ), 9 | $display 10 | ); 11 | -------------------------------------------------------------------------------- /scss/config/_escaping-characters.scss: -------------------------------------------------------------------------------- 1 | /// Characters to escape using SVG as data:image. 2 | $escaping-characters: ( 3 | ('<', '%3c'), 4 | ('>', '%3e'), 5 | ('#', '%23'), 6 | ('(', '%28'), 7 | (')', '%29'), 8 | ) !default; 9 | -------------------------------------------------------------------------------- /scss/config/_generator.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | 3 | $generators: () !default; 4 | $generators: map.deep-merge( 5 | ( 6 | 'content': ( 7 | 'accessibility': true, 8 | 'default': true, 9 | 'display': true, 10 | 'divider': true, 11 | 'layout': true, 12 | 'media': true, 13 | 'normalize': true, 14 | 'print': true, 15 | 'root': true, 16 | 'table': true, 17 | 'typography': true, 18 | 'utilities': true, 19 | ), 20 | 'form': ( 21 | 'btn': true, 22 | 'file-btn': true, 23 | 'form-check': true, 24 | 'form-control': true, 25 | 'form-description': true, 26 | 'form-feedback': true, 27 | 'form-fieldset': true, 28 | 'form-group-label': true, 29 | 'form-group': true, 30 | 'form-label': true, 31 | 'form-range': true, 32 | 'form-row': true, 33 | 'form-switch': true, 34 | ), 35 | ), 36 | $generators 37 | ); 38 | -------------------------------------------------------------------------------- /scss/config/_index.scss: -------------------------------------------------------------------------------- 1 | @forward 'color'; 2 | @forward 'setting'; 3 | @forward 'spacer'; 4 | @forward 'display'; 5 | @forward 'typography'; 6 | @forward 'button'; 7 | @forward 'form'; 8 | @forward 'table'; 9 | @forward 'layout'; 10 | @forward 'transition'; 11 | @forward 'breakpoint'; 12 | @forward 'print'; 13 | @forward 'escaping-characters'; 14 | @forward 'generator'; 15 | -------------------------------------------------------------------------------- /scss/config/_layout.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | 3 | $layout: () !default; 4 | $layout: map.merge( 5 | ( 6 | 'container-inline-size': 84rem, 7 | ), 8 | $layout 9 | ); 10 | -------------------------------------------------------------------------------- /scss/config/_print.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | 3 | $print: () !default; 4 | $print: map.merge( 5 | ( 6 | 'page-margin': 2cm, 7 | 'hidden-elements': 'header, footer, aside, nav, form, iframe, [class^="aspect-ratio"]', 8 | ), 9 | $print 10 | ); 11 | -------------------------------------------------------------------------------- /scss/config/_setting.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | 3 | $settings: () !default; 4 | $settings: map.deep-merge( 5 | ( 6 | 'color-fallback': false, 7 | 'css-custom-properties': false, 8 | 'html-smooth-scrolling': true, 9 | 'hyphens': true, 10 | 'optimal-font-size': '2vw + 1rem', 11 | 'optimal-spacer-size': '5vw', 12 | 'prefix': 'spruce', 13 | 'print': false, 14 | 'scaler': 15, 15 | 'utilities': ( 16 | 'display': true, 17 | 'typography': true, 18 | ), 19 | ), 20 | $settings 21 | ); 22 | 23 | // We use this value to prefix our CSS variables. The only difference to the default prefix value that we add the '-' suffix. 24 | $internal-prefix: if(map.get($settings, prefix), map.get($settings, prefix) + '-', ''); 25 | -------------------------------------------------------------------------------- /scss/config/_spacer.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | 3 | $spacer: 1rem !default; 4 | 5 | $spacers: () !default; 6 | $spacers: map.merge( 7 | ( 8 | 'xxs': $spacer * 0.25, 9 | 'xs': $spacer * 0.5, 10 | 's': $spacer, 11 | 'm': $spacer * 1.5, 12 | 'l': $spacer * 3, 13 | 'xl': $spacer * 4.5, 14 | 'xxl': $spacer * 7, 15 | 'xxxl': $spacer * 10, 16 | ), 17 | $spacers 18 | ); 19 | -------------------------------------------------------------------------------- /scss/config/_table.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | @use 'typography' as *; 3 | @use 'spacer' as *; 4 | 5 | $table: () !default; 6 | $table: map.merge( 7 | ( 8 | 'caption-font-size': null, 9 | 'caption-font-style': null, 10 | 'caption-font-weight': null, 11 | 'line-height': map.get($typography, 'line-height-md'), 12 | 'padding': map.get($spacers, 's'), 13 | 'responsive-inline-size': 40rem, 14 | 'stripe': odd, 15 | ), 16 | $table 17 | ); 18 | 19 | $table-sm: () !default; 20 | $table-sm: map.merge( 21 | ( 22 | 'padding': map.get($spacers, 'xs'), 23 | ), 24 | $table-sm 25 | ); 26 | -------------------------------------------------------------------------------- /scss/config/_transition.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | 3 | $transition: () !default; 4 | $transition: map.merge( 5 | ( 6 | 'duration': 0.15s, 7 | 'timing-function': ease-in-out, 8 | ), 9 | $transition 10 | ); 11 | -------------------------------------------------------------------------------- /scss/config/_typography.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | @use 'sass:math'; 3 | @use 'display' as *; 4 | 5 | $typography: () !default; 6 | $typography: map.merge( 7 | ( 8 | 'border-radius': map.get($display, 'border-radius-sm'), 9 | 'font-family-base': #{Seravek, 'Gill Sans Nova', Ubuntu, Calibri, 'DejaVu Sans', source-sans-pro, sans-serif}, 10 | 'font-family-cursive': #{ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace}, 11 | 'font-family-heading': #{Avenir, 'Avenir Next LT Pro', Montserrat, Corbel, 'URW Gothic', source-sans-pro, sans-serif}, 12 | 'font-size-base': 1rem, 13 | 'font-size-lead': clamp(1.15rem, 2vw, 1.35rem), 14 | 'font-size-lg': 1.125rem, 15 | 'font-size-ratio': 1.25, 16 | 'font-size-sm': 0.875rem, 17 | 'font-weight-base': null, 18 | 'font-weight-heading': 700, 19 | 'inline-padding': 0.1em 0.3em, 20 | 'letter-spacing-heading': null, 21 | 'line-height-base': 1.8, 22 | 'line-height-heading': calc(2px + 2ex + 2px), 23 | 'line-height-lg': 1.8, 24 | 'line-height-md': 1.5, 25 | 'line-height-sm': 1.2, 26 | ), 27 | $typography 28 | ); 29 | 30 | $font-sizes: () !default; 31 | $font-sizes: map.merge( 32 | ( 33 | 'h1': math.pow(map.get($typography, 'font-size-ratio'), 4) * map.get($typography, 'font-size-base'), 34 | 'h2': math.pow(map.get($typography, 'font-size-ratio'), 3) * map.get($typography, 'font-size-base'), 35 | 'h3': math.pow(map.get($typography, 'font-size-ratio'), 2) * map.get($typography, 'font-size-base'), 36 | 'h4': math.pow(map.get($typography, 'font-size-ratio'), 1) * map.get($typography, 'font-size-base'), 37 | 'h5': map.get($typography, 'font-size-base'), 38 | 'h6': map.get($typography, 'font-size-base'), 39 | ), 40 | $font-sizes 41 | ); 42 | -------------------------------------------------------------------------------- /scss/config/form/_check.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | @use 'label' as *; 3 | @use '../display' as *; 4 | @use '../typography' as *; 5 | 6 | $form-check: () !default; 7 | $form-check: map.merge( 8 | ( 9 | 'border-radius': map.get($display, border-radius-sm), 10 | 'border-width': 1px, 11 | 'focus-ring-box-shadow-type': outside, 12 | 'focus-ring-offset': 2px, 13 | 'focus-ring-size': 2px, 14 | 'focus-ring-type': outline, 15 | 'font-size': 1.125rem, 16 | 'font-weight': map.get($form-label, 'font-weight'), 17 | 'line-height': map.get($typography, 'line-height-md'), 18 | 'margin-block': 0.1em, 19 | 'vertical-alignment': center, 20 | ), 21 | $form-check 22 | ); 23 | 24 | $form-check-lg: () !default; 25 | $form-check-lg: map.merge( 26 | ( 27 | 'font-size': map.get($typography, 'size-lg'), 28 | ), 29 | $form-check-lg 30 | ); 31 | 32 | $form-check-sm: () !default; 33 | $form-check-sm: map.merge( 34 | ( 35 | 'font-size': map.get($typography, 'font-size-base'), 36 | ), 37 | $form-check-sm 38 | ); 39 | -------------------------------------------------------------------------------- /scss/config/form/_control.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | @use '../display' as *; 3 | @use '../typography' as *; 4 | 5 | $form-control: () !default; 6 | $form-control: map.merge( 7 | ( 8 | 'border-radius': map.get($display, 'border-radius-sm'), 9 | 'border-width': 1px, 10 | 'focus-ring-box-shadow-type': outside, 11 | 'focus-ring-offset': 2px, 12 | 'focus-ring-size': 0.25rem, 13 | 'focus-ring-type': box-shadow, 14 | 'font-family': null, 15 | 'font-size': map.get($typography, 'font-size-base'), 16 | 'font-weight': null, 17 | 'line-height': 1.5, 18 | 'padding': 0.5em 0.75em, 19 | 'textarea-block-size': 6rem, 20 | ), 21 | $form-control 22 | ); 23 | 24 | $form-control-lg: () !default; 25 | $form-control-lg: map.merge( 26 | ( 27 | 'font-size': map.get($typography, 'size-lg'), 28 | 'padding': 0.65em 1em, 29 | ), 30 | $form-control-lg 31 | ); 32 | 33 | $form-control-sm: () !default; 34 | $form-control-sm: map.merge( 35 | ( 36 | 'border-radius': 0.35em, 37 | 'font-size': map.get($typography, 'size-sm'), 38 | 'padding': 0.25em 0.75em, 39 | ), 40 | $form-control-sm 41 | ); 42 | 43 | $form-control-color: () !default; 44 | $form-control-color: map.merge( 45 | ( 46 | 'aspect-ratio': 1, 47 | 'block-size': 100%, 48 | 'inline-size': 2.625rem, 49 | 'padding': 0.5em, 50 | ), 51 | $form-control-color 52 | ); 53 | 54 | $form-control-color-lg: () !default; 55 | $form-control-color-lg: map.merge( 56 | ( 57 | 'aspect-ratio': 1, 58 | 'block-size': 100%, 59 | 'inline-size': 3.204rem, 60 | 'padding': 0.5em, 61 | ), 62 | $form-control-color-lg 63 | ); 64 | 65 | $form-control-color-sm: () !default; 66 | $form-control-color-sm: map.merge( 67 | ( 68 | 'aspect-ratio': 1, 69 | 'block-size': 100%, 70 | 'inline-size': 1.925rem, 71 | 'padding': 0.25em, 72 | ), 73 | $form-control-color-sm 74 | ); 75 | -------------------------------------------------------------------------------- /scss/config/form/_description.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | 3 | $form-description: () !default; 4 | $form-description: map.merge( 5 | ( 6 | 'font-size': 1em, 7 | 'font-style': null, 8 | 'font-weight': 400, 9 | ), 10 | $form-description 11 | ); 12 | -------------------------------------------------------------------------------- /scss/config/form/_fieldset.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | @use '../spacer' as *; 3 | @use '../typography' as *; 4 | 5 | $form-fieldset: () !default; 6 | $form-fieldset: map.merge( 7 | ( 8 | 'layout-gap': map.get($spacers, 's'), 9 | 'legend-font-family': null, 10 | 'legend-font-size': clamp(#{map.get($font-sizes, 'h5')}, 5vw, #{map.get($font-sizes, 'h4')}), 11 | 'legend-font-weight': 700, 12 | ), 13 | $form-fieldset 14 | ); 15 | -------------------------------------------------------------------------------- /scss/config/form/_file.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | 3 | $form-file: () !default; 4 | $form-file: map.merge( 5 | ( 6 | 'background': 'primary', 7 | ), 8 | $form-file 9 | ); 10 | -------------------------------------------------------------------------------- /scss/config/form/_group.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | @use '../spacer' as *; 3 | 4 | $form-group: () !default; 5 | $form-group: map.merge( 6 | ( 7 | 'gap': map.get($spacers, 'xs'), 8 | ), 9 | $form-group 10 | ); 11 | 12 | $form-group-check: () !default; 13 | $form-group-check: map.merge( 14 | ( 15 | 'gap': map.get($spacers, 's'), 16 | ), 17 | $form-group-check 18 | ); 19 | 20 | $form-group-row: () !default; 21 | $form-group-row: map.merge( 22 | ( 23 | 'container-inline-size': 38rem, 24 | 'gap': map.get($spacers, 'xxs') map.get($spacers, 's'), 25 | 'label-inline-size': 10rem, 26 | 'vertical-alignment': center, 27 | ), 28 | $form-group-row 29 | ); 30 | -------------------------------------------------------------------------------- /scss/config/form/_icon.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | 3 | $form-icon: () !default; 4 | $form-icon: map.merge( 5 | ( 6 | 'checkbox-indeterminate': '', 7 | 'checkbox': '', 8 | 'invalid': '', 9 | 'radio': '', 10 | 'select': '', 11 | 'switch': '', 12 | 'valid': '', 13 | ), 14 | $form-icon 15 | ); 16 | -------------------------------------------------------------------------------- /scss/config/form/_index.scss: -------------------------------------------------------------------------------- 1 | @forward 'label'; 2 | @forward 'control'; 3 | @forward 'description'; 4 | @forward 'fieldset'; 5 | @forward 'file'; 6 | @forward 'icon'; 7 | @forward 'range'; 8 | @forward 'group'; 9 | @forward 'row'; 10 | @forward 'select'; 11 | @forward 'switch'; 12 | @forward 'check'; 13 | -------------------------------------------------------------------------------- /scss/config/form/_label.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | 3 | $form-label: () !default; 4 | $form-label: map.merge( 5 | ( 6 | 'font-family': null, 7 | 'font-size': null, 8 | 'font-style': null, 9 | 'font-weight': null, 10 | 'text-align': start, 11 | 'text-transform': null, 12 | ), 13 | $form-label 14 | ); 15 | -------------------------------------------------------------------------------- /scss/config/form/_range.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | 3 | $form-range: () !default; 4 | $form-range: map.merge( 5 | ( 6 | 'focus-ring-box-shadow-type': outside, 7 | 'focus-ring-offset': 2px, 8 | 'focus-ring-size': 2px, 9 | 'focus-ring-type': outline, 10 | 'thumb-block-size': 1rem, 11 | 'thumb-border-radius': 0.5rem, 12 | 'thumb-inline-size': 1rem, 13 | 'track-block-size': 0.25rem, 14 | 'track-border-radius': 0.15rem, 15 | ), 16 | $form-range 17 | ); 18 | -------------------------------------------------------------------------------- /scss/config/form/_row.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | 3 | $form-row: () !default; 4 | $form-row: map.merge( 5 | ( 6 | 'inline-size': 20ch, 7 | ), 8 | $form-row 9 | ); 10 | -------------------------------------------------------------------------------- /scss/config/form/_select.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | 3 | $form-select: () !default; 4 | $form-select: map.merge( 5 | ( 6 | 'icon-inline-size': 1.25em, 7 | 'icon-right-offset': 0.5em, 8 | 'padding-inline-end': calc(0.75em + 1.25em), 9 | ), 10 | $form-select 11 | ); 12 | -------------------------------------------------------------------------------- /scss/config/form/_switch.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | @use '../typography' as *; 3 | @use 'label' as *; 4 | 5 | $form-switch: () !default; 6 | $form-switch: map.merge( 7 | ( 8 | 'border-width': 1px, 9 | 'font-size': 1.125rem, 10 | 'font-weight': map.get($form-label, 'font-weight'), 11 | 'line-height': map.get($typography, 'line-height-md'), 12 | 'margin-block': 0.15em, 13 | 'vertical-alignment': center, 14 | ), 15 | $form-switch 16 | ); 17 | 18 | $form-switch-lg: () !default; 19 | $form-switch-lg: map.merge( 20 | ( 21 | 'font-size': map.get($typography, 'font-size-lead'), 22 | ), 23 | $form-switch-lg 24 | ); 25 | 26 | 27 | $form-switch-sm: () !default; 28 | $form-switch-sm: map.merge( 29 | ( 30 | 'font-size': map.get($typography, 'font-size-base'), 31 | ), 32 | $form-switch-sm 33 | ); 34 | -------------------------------------------------------------------------------- /scss/element/_accessibility.scss: -------------------------------------------------------------------------------- 1 | @use '../mixin' as *; 2 | 3 | @mixin generate-accessibility { 4 | .sr-only { 5 | @include visually-hidden; 6 | } 7 | 8 | [tabindex='-1']:focus { 9 | outline: none !important; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /scss/element/_default.scss: -------------------------------------------------------------------------------- 1 | @use '../config' as *; 2 | @use '../function' as *; 3 | 4 | @mixin generate-default { 5 | ::selection { 6 | background-color: color('background', 'selection'); 7 | color: color('foreground', 'selection'); 8 | text-shadow: none; 9 | } 10 | 11 | html { 12 | box-sizing: border-box; 13 | 14 | @if setting('html-smooth-scrolling') { 15 | @media (prefers-reduced-motion: no-preference) { 16 | scroll-behavior: smooth; 17 | } 18 | } 19 | } 20 | 21 | *, 22 | ::before, 23 | ::after { 24 | box-sizing: inherit; 25 | } 26 | 27 | body { 28 | background: color('background'); 29 | color: color('text'); 30 | } 31 | 32 | a { 33 | color: color('link'); 34 | text-decoration: underline; 35 | transition-duration: config('duration', $transition); 36 | transition-property: color; 37 | transition-timing-function: config('timing-function', $transition); 38 | 39 | &:hover { 40 | color: color('link-hover'); 41 | } 42 | } 43 | 44 | button { 45 | color: inherit; 46 | } 47 | 48 | // Turn off double-tap on mobile to zoom 49 | a, 50 | button { 51 | touch-action: manipulation; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /scss/element/_divider.scss: -------------------------------------------------------------------------------- 1 | @use '../function' as *; 2 | 3 | @mixin generate-divider { 4 | hr { 5 | border: 0; 6 | border-block-start: 1px solid color('border'); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /scss/element/_index.scss: -------------------------------------------------------------------------------- 1 | @forward 'root'; 2 | @forward 'accessibility'; 3 | @forward 'default'; 4 | @forward 'divider'; 5 | @forward 'media'; 6 | @forward 'table'; 7 | @forward 'typography'; 8 | @forward 'utilities'; 9 | -------------------------------------------------------------------------------- /scss/element/_media.scss: -------------------------------------------------------------------------------- 1 | @use '../function' as *; 2 | @use '../mixin' as *; 3 | 4 | @mixin generate-media { 5 | img { 6 | block-size: auto; 7 | display: block; 8 | max-inline-size: 100%; 9 | user-select: none; 10 | } 11 | 12 | iframe { 13 | block-size: 100%; 14 | display: block; 15 | inline-size: 100%; 16 | } 17 | 18 | figure { 19 | margin-inline: 0; 20 | 21 | figcaption { 22 | margin-block-start: spacer('xs'); 23 | text-align: center; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /scss/element/_root.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | @use '../function' as *; 3 | @use '../mixin' as *; 4 | @use '../config' as *; 5 | 6 | @mixin generate-root { 7 | @include generate-color-variables; 8 | 9 | :root { 10 | @if map.get($generators, 'content', 'typography') { 11 | @include generate-variables($typography); 12 | } 13 | 14 | @if map.get($generators, 'content', 'display') { 15 | @include generate-variables($display); 16 | } 17 | 18 | @if map.get($generators, 'content', 'layout') { 19 | @include generate-variables($layout); 20 | } 21 | 22 | @if map.get($generators, 'content', 'print') { 23 | @include generate-variables($print); 24 | } 25 | 26 | @if setting('css-custom-properties') { 27 | @media (prefers-reduced-motion: reduce) { 28 | --#{$internal-prefix}duration: 0; 29 | } 30 | 31 | @media (prefers-reduced-motion: no-preference) { 32 | --#{$internal-prefix}duration: #{config('duration', $transition, false)}; 33 | --#{$internal-prefix}timing-function: #{config('timing-function', $transition, false)}; 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /scss/element/_table.scss: -------------------------------------------------------------------------------- 1 | @use '../config' as *; 2 | @use '../function' as *; 3 | @use '../mixin' as *; 4 | 5 | @mixin generate-table( 6 | $selector: '.table', 7 | $has-variations: true, 8 | $has-responsive-table: true 9 | ) { 10 | @if ($has-responsive-table) { 11 | .table-responsive { 12 | --inline-size: #{config('responsive-inline-size', $table, false)}; 13 | -webkit-overflow-scrolling: touch; 14 | overflow-x: auto; 15 | 16 | table { 17 | min-inline-size: var(--inline-size); 18 | } 19 | } 20 | } 21 | 22 | #{$selector} { 23 | @include generate-variables($table, ('stripe')); 24 | 25 | border-collapse: collapse; 26 | color: color('text', 'table'); 27 | inline-size: 100%; 28 | 29 | caption { 30 | color: color(caption, table); 31 | font-size: config('caption-font-size', $table); 32 | font-style: config('caption-font-style', $table); 33 | font-weight: config('caption-font-weight', $table); 34 | margin-block-end: spacer('s'); 35 | } 36 | 37 | th, 38 | td { 39 | border-block-end: 1px solid color('border', 'table'); 40 | line-height: config('line-height', $table); 41 | padding: config('padding', $table); 42 | } 43 | 44 | th { 45 | color: color('heading', 'table'); 46 | text-align: inherit; 47 | text-align: -webkit-match-parent; 48 | } 49 | 50 | @if ($has-variations) { 51 | &--striped { 52 | > tbody > tr:nth-child(#{config('stripe', $table, false)}) { 53 | background-color: color('stripe', 'table'); 54 | } 55 | } 56 | 57 | &--hover { 58 | > tbody > tr:hover { 59 | background: color('hover', 'table'); 60 | } 61 | } 62 | 63 | &--clear-border { 64 | th, 65 | td { 66 | border: 0; 67 | } 68 | } 69 | 70 | &--in-line { 71 | th:first-child, 72 | td:first-child { 73 | padding-inline-start: 0; 74 | } 75 | 76 | th:last-child, 77 | td:last-child { 78 | padding-inline-end: 0; 79 | } 80 | } 81 | 82 | &--sm { 83 | @include generate-variables($table-sm); 84 | 85 | th, 86 | td { 87 | padding: config('padding', $table-sm); 88 | } 89 | } 90 | 91 | &--rounded { 92 | th, 93 | td { 94 | &:first-child { 95 | border-end-start-radius: config('border-radius-sm', $display); 96 | border-start-start-radius: config('border-radius-sm', $display); 97 | } 98 | 99 | &:last-child { 100 | border-end-end-radius: config('border-radius-sm', $display); 101 | border-start-end-radius: config('border-radius-sm', $display); 102 | } 103 | } 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /scss/element/_typography.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | @use '../function' as *; 3 | @use '../mixin' as *; 4 | @use '../config' as *; 5 | 6 | @mixin generate-typography { 7 | html { 8 | -webkit-tap-highlight-color: hsl(0deg 0% 0% / 0%); 9 | } 10 | 11 | body { 12 | font-family: config('font-family-base', $typography); 13 | font-size: config('font-size-base', $typography); 14 | font-weight: config('font-weight-base', $typography); 15 | line-height: config('line-height-base', $typography); 16 | } 17 | 18 | @if setting('hyphens') { 19 | p, 20 | li, 21 | h1, 22 | h2, 23 | h3, 24 | h4, 25 | h5, 26 | h6 { 27 | hyphens: auto; 28 | overflow-wrap: break-word; 29 | } 30 | } 31 | 32 | h1, 33 | h2, 34 | h3, 35 | h4, 36 | h5, 37 | h6 { 38 | color: color('heading'); 39 | font-family: config('font-family-heading', $typography); 40 | font-weight: config('font-weight-heading', $typography); 41 | letter-spacing: config('letter-spacing-heading', $typography); 42 | line-height: config('line-height-heading', $typography); 43 | } 44 | 45 | h1 { 46 | font-size: font-size('h1'); 47 | } 48 | 49 | h2 { 50 | font-size: font-size('h2'); 51 | } 52 | 53 | h3 { 54 | font-size: font-size('h3'); 55 | } 56 | 57 | h4 { 58 | font-size: font-size('h4'); 59 | } 60 | 61 | h5 { 62 | font-size: font-size('h5'); 63 | } 64 | 65 | h6 { 66 | font-size: font-size('h6'); 67 | } 68 | 69 | ul, 70 | ol { 71 | list-style-position: inside; 72 | @include layout-stack('xxs'); 73 | 74 | li { 75 | list-style-position: outside; 76 | 77 | &::marker { 78 | color: color('marker'); 79 | } 80 | } 81 | } 82 | 83 | li > ul, 84 | li > ol { 85 | margin-block-start: spacer('xxs'); 86 | } 87 | 88 | dl { 89 | dt { 90 | color: color('heading'); 91 | font-weight: bold; 92 | } 93 | 94 | dd { 95 | margin: 0; 96 | } 97 | 98 | dd + dt { 99 | margin-block-start: spacer('s'); 100 | } 101 | } 102 | 103 | .quote { 104 | border-inline-start: 0.5rem solid color('blockquote-border'); 105 | padding-inline-start: spacer('m'); 106 | @include layout-stack('xs'); 107 | 108 | blockquote { 109 | border-inline-start: 0; 110 | padding-inline-start: 0; 111 | } 112 | 113 | figcaption { 114 | text-align: start; 115 | } 116 | } 117 | 118 | blockquote { 119 | border-inline-start: 0.5rem solid color('blockquote-border'); 120 | margin-inline-start: 0; 121 | padding-inline-start: spacer('m'); 122 | @include layout-stack('xs'); 123 | } 124 | 125 | abbr[title] { 126 | border-block-end: 1px dotted; 127 | cursor: help; 128 | text-decoration: none; 129 | } 130 | 131 | mark { 132 | background-color: color('mark-background'); 133 | border-radius: config('border-radius', $typography); 134 | color: color('mark-foreground'); 135 | padding: config('inline-padding', $typography); 136 | } 137 | 138 | code, 139 | kbd, 140 | samp { 141 | background-color: color('code-background'); 142 | border-radius: config('border-radius', $typography); 143 | color: color('code-foreground'); 144 | padding: config('inline-padding', $typography); 145 | } 146 | 147 | strong { 148 | color: color('strong'); 149 | } 150 | 151 | .lead { 152 | font-size: config('font-size-lead', $typography); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /scss/element/_utilities.scss: -------------------------------------------------------------------------------- 1 | @use '../function' as *; 2 | 3 | @mixin generate-utilities { 4 | @if setting('display', 'utilities') == true { 5 | .hidden, 6 | [hidden] { 7 | display: none !important; 8 | } 9 | } 10 | 11 | @if setting('typography', 'utilities') == true { 12 | .h1 { 13 | font-size: font-size('h1'); 14 | } 15 | 16 | .h2 { 17 | font-size: font-size('h2'); 18 | } 19 | 20 | .h3 { 21 | font-size: font-size('h3'); 22 | } 23 | 24 | .h4 { 25 | font-size: font-size('h4'); 26 | } 27 | 28 | .h5 { 29 | font-size: font-size('h5'); 30 | } 31 | 32 | .h6 { 33 | font-size: font-size('h6'); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /scss/form/_button.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | @use '../function' as *; 3 | @use '../mixin' as *; 4 | @use '../config' as *; 5 | 6 | @mixin generate-btn( 7 | $selector, 8 | $pseudo-selector: null, 9 | $has-icons: true, 10 | $has-sizes: true, 11 | ) { 12 | #{$selector}#{$pseudo-selector} { 13 | @include generate-variables($btn, ('focus-')); 14 | 15 | align-items: center; 16 | border-radius: config('border-radius', $btn); 17 | border-style: solid; 18 | border-width: config('border-width', $btn); 19 | cursor: pointer; 20 | display: inline-flex; 21 | font-family: config('font-family', $btn); 22 | font-size: config('font-size', $btn); 23 | font-style: config('font-style', $btn); 24 | font-weight: config('font-weight', $btn); 25 | gap: config('gap', $btn); 26 | justify-content: center; 27 | line-height: 1; 28 | padding: config('padding', $btn); 29 | text-align: start; 30 | text-decoration: none; 31 | text-transform: config('text-transform', $btn); 32 | transition-duration: config('duration', $transition); 33 | transition-property: background-color, border-color, box-shadow, color; 34 | transition-timing-function: config('timing-function', $transition); 35 | } 36 | 37 | #{$selector}:focus { 38 | outline-color: transparent; 39 | outline-style: solid; 40 | } 41 | 42 | #{$selector}:disabled { 43 | opacity: 0.5; 44 | pointer-events: none; 45 | } 46 | 47 | @if ($has-icons) { 48 | #{$selector}--icon { 49 | padding: config('icon-padding', $btn); 50 | 51 | &#{$selector}--sm { 52 | padding: config('icon-padding', $btn-sm); 53 | } 54 | 55 | &#{$selector}--lg { 56 | padding: config('icon-padding', $btn-lg); 57 | } 58 | } 59 | 60 | #{$selector}__icon { 61 | block-size: config('icon-size', $btn); 62 | flex-shrink: 0; 63 | inline-size: config('icon-size', $btn); 64 | pointer-events: none; 65 | 66 | &--sm { 67 | block-size: config('icon-size', $btn-sm); 68 | inline-size: config('icon-size', $btn-sm); 69 | } 70 | } 71 | } 72 | 73 | @if ($has-sizes) { 74 | // Sizes 75 | #{$selector}--sm#{$pseudo-selector} { 76 | @include generate-variables($btn-sm); 77 | 78 | font-size: config('font-size', $btn-sm); 79 | gap: config('gap', $btn-sm); 80 | padding: config('padding', $btn-sm); 81 | } 82 | 83 | #{$selector}--lg#{$pseudo-selector} { 84 | @include generate-variables($btn-lg); 85 | 86 | @if not map.get($settings, 'css-custom-properties') { 87 | gap: config('gap', $btn-lg); 88 | padding: config('padding', $btn-lg); 89 | 90 | @include breakpoint(md) { 91 | font-size: config('font-size', $btn-lg); 92 | } 93 | } 94 | } 95 | 96 | #{$selector}--block#{$pseudo-selector} { 97 | inline-size: 100%; 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /scss/form/_check.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | @use '../config' as *; 3 | @use '../function' as *; 4 | @use '../mixin' as *; 5 | 6 | // Create custom checkbox and radio 7 | @mixin generate-form-check( 8 | $parent, 9 | $input, 10 | $label, 11 | $has-sizes: false 12 | ) { 13 | #{$parent} { 14 | @include generate-variables($form-check, ('focus-')); 15 | 16 | align-items: config('vertical-alignment', $form-check); 17 | display: inline-flex; 18 | gap: spacer('xs'); 19 | } 20 | 21 | #{$parent}--vertical-center { 22 | align-items: center; 23 | } 24 | 25 | #{$parent}--vertical-start { 26 | align-items: flex-start; 27 | } 28 | 29 | @if ($has-sizes) { 30 | #{$parent}--sm { 31 | @include generate-variables($form-control-sm); 32 | 33 | #{$input} { 34 | font-size: config('font-size', $form-check-sm); 35 | } 36 | } 37 | 38 | #{$parent}--lg { 39 | @include generate-variables($form-control-lg); 40 | 41 | #{$input} { 42 | font-size: config('font-size', $form-check-lg); 43 | } 44 | } 45 | } 46 | 47 | @at-root { 48 | #{$input} { 49 | appearance: none; 50 | background-color: color('background', 'form'); 51 | background-position: center; 52 | background-repeat: no-repeat; 53 | background-size: contain; 54 | block-size: 1em; 55 | border: config('border-width', $form-check) solid color('border', 'form'); 56 | flex-shrink: 0; 57 | font-size: config('font-size', $form-check); 58 | font-weight: config('font-weight', $form-check); 59 | inline-size: 1em; 60 | line-height: 1; 61 | margin-block: config('margin-block', $form-check); 62 | transition-duration: config('duration', $transition); 63 | transition-property: border, box-shadow; 64 | transition-timing-function: config('timing-function', $transition); 65 | 66 | &[type='radio'] { 67 | border-radius: 50%; 68 | } 69 | 70 | &[type='checkbox'] { 71 | border-radius: config('border-radius', $form-check); 72 | } 73 | 74 | &:focus-visible { 75 | @include focus-ring( 76 | $type: config('focus-ring-type', $form-check, false), 77 | $border-color: color('check-background', 'form'), 78 | $ring-color: color('check-focus-ring', 'form'), 79 | $box-shadow-type: config('focus-ring-box-shadow-type', $form-check, false), 80 | $ring-size: config('focus-ring-size', $form-check, false), 81 | $ring-offset: config('focus-ring-offset', $form-check, false) 82 | ); 83 | } 84 | 85 | &:checked { 86 | background-color: color('check-background', 'form'); 87 | border-color: color('check-background', 'form'); 88 | 89 | &[type='radio'] { 90 | @include field-icon(config('radio', $form-icon, false), color('check-foreground', 'form', true)); 91 | } 92 | 93 | &[type='checkbox'] { 94 | @include field-icon(config('checkbox', $form-icon, false), color('check-foreground', 'form', true)); 95 | } 96 | } 97 | 98 | &:indeterminate { 99 | &[type='checkbox'] { 100 | @include field-icon(config('checkbox-indeterminate', $form-icon, false), color('check-foreground', 'form', true)); 101 | background-color: color('check-background', 'form'); 102 | border-color: color('check-background', 'form'); 103 | } 104 | } 105 | 106 | &:disabled, 107 | &.disabled { 108 | @include field-disabled( 109 | $background: color('background-disabled', 'form'), 110 | $border: color('border-disabled', 'form') 111 | ); 112 | 113 | + #{$label} { 114 | opacity: 0.5; 115 | } 116 | } 117 | } 118 | 119 | #{$label} { 120 | font-weight: config('font-weight', $form-check); 121 | line-height: config('line-height', $form-check); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /scss/form/_control.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | @use '../function' as *; 3 | @use '../mixin' as *; 4 | @use '../config' as *; 5 | 6 | @mixin generate-form-control( 7 | $selector, 8 | $has-states: false, 9 | $has-sizes: false, 10 | $has-select: true 11 | ) { 12 | #{$selector} { 13 | --webkit-date-line-height: 1.375; 14 | @include generate-variables($form-control, ('focus-')); 15 | 16 | appearance: none; 17 | background-color: color('background', 'form'); 18 | border: config('border-width', $form-control) solid color('border', 'form'); 19 | border-radius: config('border-radius', $form-control); 20 | box-sizing: border-box; 21 | color: color('text', 'form'); 22 | display: block; 23 | font-family: config('font-family', $form-control); 24 | font-size: config('font-size', $form-control); 25 | font-weight: config('font-weight', $form-control); 26 | inline-size: 100%; 27 | line-height: config('line-height', $form-control); 28 | padding: config('padding', $form-control); 29 | transition-duration: config('duration', $transition); 30 | transition-property: border, box-shadow; 31 | transition-timing-function: config('timing-function', $transition); 32 | 33 | &::placeholder { 34 | color: color('placeholder', 'form'); 35 | }; 36 | 37 | &::-webkit-datetime-edit { 38 | line-height: var(--webkit-date-line-height); 39 | } 40 | 41 | &:focus { 42 | @include focus-ring( 43 | $type: config('focus-ring-type', $form-control, false), 44 | $border-color: color('border-focus', 'form'), 45 | $ring-color: color('ring-focus', 'form'), 46 | $box-shadow-type: config('focus-ring-box-shadow-type', $form-control, false), 47 | $ring-size: config('focus-ring-size', $form-control, false), 48 | $ring-offset: config('focus-ring-offset', $form-control, false) 49 | ); 50 | } 51 | 52 | &[type='color'] { 53 | @include generate-variables($form-control-color); 54 | aspect-ratio: config('aspect-ratio', $form-control-color); 55 | block-size: config('block-size', $form-control-color); 56 | inline-size: config('inline-size', $form-control-color); 57 | padding: config('padding', $form-control-color); 58 | 59 | &::-webkit-color-swatch-wrapper { 60 | padding: 0; 61 | } 62 | 63 | &::-moz-color-swatch { 64 | border: 0; 65 | border-radius: config('border-radius', $form-control); 66 | } 67 | 68 | &::-webkit-color-swatch { 69 | border: 0; 70 | border-radius: config('border-radius', $form-control); 71 | } 72 | } 73 | 74 | &[disabled], 75 | &[disabled='true'] { 76 | @include field-disabled( 77 | $background: color('background-disabled', 'form'), 78 | $border: color('border-disabled', 'form') 79 | ); 80 | } 81 | 82 | @at-root { 83 | textarea#{$selector} { 84 | block-size: config('textarea-block-size', $form-control); 85 | min-block-size: config('textarea-block-size', $form-control); 86 | resize: vertical; 87 | } 88 | } 89 | 90 | @if ($has-states) { 91 | &--valid, 92 | &--invalid { 93 | background-position: center right config('icon-right-offset', $form-select, false); 94 | background-repeat: no-repeat; 95 | background-size: config('icon-inline-size', $form-select, false) auto; 96 | padding-inline-end: config('padding-inline-end', $form-select, false); 97 | 98 | html[dir='rtl'] & { 99 | background-position: center left config('icon-right-offset', $form-select, false); 100 | } 101 | } 102 | 103 | &--valid { 104 | @include field-icon(config('valid', $form-icon, false), color('success', 'alert', true)); 105 | border-color: color('success', 'alert'); 106 | 107 | &:focus { 108 | @include focus-ring( 109 | $type: config('focus-ring-type', $form-control, false), 110 | $border-color: color('valid', 'form'), 111 | $ring-color: color('valid-focus-ring', 'form', false), 112 | $box-shadow-type: config('focus-ring-box-shadow-type', $form-control), 113 | $ring-size: config('focus-ring-size', $form-control, false), 114 | $ring-offset: config('focus-ring-offset', $form-control, false) 115 | ); 116 | } 117 | } 118 | 119 | &--invalid { 120 | @include field-icon(config('invalid', $form-icon, false), color('danger', 'alert', true)); 121 | border-color: color('danger', 'alert'); 122 | 123 | &:focus { 124 | @include focus-ring( 125 | $type: config('focus-ring-type', $form-control, false), 126 | $border-color: color('invalid', 'form'), 127 | $ring-color: color('invalid-focus-ring', 'form'), 128 | $box-shadow-type: config('focus-ring-box-shadow-type', $form-control, false), 129 | $ring-size: config('focus-ring-size', $form-control, false), 130 | $ring-offset: config('focus-ring-offset', $form-control, false) 131 | ); 132 | } 133 | } 134 | } 135 | 136 | @if ($has-sizes) { 137 | &--sm { 138 | --webkit-date-line-height: 1.36; 139 | @include generate-variables($form-control-sm); 140 | 141 | &[type='color'] { 142 | @include generate-variables($form-control-color-sm); 143 | } 144 | 145 | @if not map.get($settings, 'css-custom-properties') { 146 | font-size: config('font-size', $form-control-sm); 147 | padding: config('padding', $form-control-sm); 148 | 149 | &[type='color'] { 150 | aspect-ratio: config('aspect-ratio', $form-control-color-sm); 151 | block-size: config('block-size', $form-control-color-sm); 152 | inline-size: config('inline-size', $form-control-color-sm); 153 | padding: config('padding', $form-control-color-sm); 154 | } 155 | } 156 | } 157 | 158 | &--lg { 159 | --webkit-date-line-height: 1.387; 160 | @include generate-variables($form-control-lg); 161 | 162 | &[type='color'] { 163 | @include generate-variables($form-control-color-lg); 164 | } 165 | 166 | @if not map.get($settings, 'css-custom-properties') { 167 | font-size: config('font-size', $form-control-lg); 168 | padding: config('padding', $form-control-lg); 169 | 170 | &[type='color'] { 171 | aspect-ratio: config('aspect-ratio', $form-control-color-lg); 172 | height: config('block-size', $form-control-color-lg); 173 | inline-size: config('inline-size', $form-control-color-lg); 174 | padding: config('padding', $form-control-color-lg); 175 | } 176 | } 177 | } 178 | } 179 | } 180 | 181 | @if ($has-select) { 182 | select#{$selector} { 183 | &:not([multiple]):not([size]) { 184 | @include field-icon(config('select', $form-icon, false), color('select-foreground', 'form', true)); 185 | background-position: center right config('icon-right-offset', $form-select, false); 186 | background-repeat: no-repeat; 187 | background-size: config('icon-inline-size', $form-select, false) auto; 188 | padding-inline-end: config('padding-inline-end', $form-select, false); 189 | 190 | html[dir='rtl'] & { 191 | background-position: center left config('icon-right-offset', $form-select, false); 192 | } 193 | } 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /scss/form/_description.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | @use '../function' as *; 3 | @use '../mixin' as *; 4 | @use '../config' as *; 5 | 6 | @mixin generate-form-description { 7 | .form-description { 8 | @include generate-variables($form-description); 9 | 10 | color: color('text', 'form'); 11 | display: block; 12 | font-size: config('font-size', $form-description); 13 | font-style: config('font-style', $form-description); 14 | font-weight: config('font-weight', $form-description); 15 | line-height: config('line-height-md', $typography); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /scss/form/_fieldset.scss: -------------------------------------------------------------------------------- 1 | @use '../function' as *; 2 | @use '../mixin' as *; 3 | @use '../config' as *; 4 | 5 | @mixin generate-form-fieldset { 6 | fieldset { 7 | border: 0; 8 | margin: 0; 9 | padding: 0; 10 | 11 | @include generate-variables($form-fieldset); 12 | @include layout-stack(config('layout-gap', $form-fieldset)); 13 | 14 | + fieldset { 15 | margin-block-start: spacer('l'); 16 | } 17 | } 18 | 19 | legend { 20 | color: color('legend', 'form'); 21 | font-family: config('legend-font-family', $form-fieldset); 22 | font-size: config('legend-font-size', $form-fieldset); 23 | font-weight: config('legend-font-weight', $form-fieldset); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /scss/form/_file.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | @use '../function' as *; 3 | @use '../mixin' as *; 4 | @use '../config' as *; 5 | @use 'button' as *; 6 | 7 | @mixin generate-file-btn( 8 | $selector, 9 | $pseudo-selector: null, 10 | $has-icons: true, 11 | $has-sizes: true, 12 | ) { 13 | @include generate-btn($selector, $pseudo-selector, $has-icons, $has-sizes); 14 | 15 | #{$selector} { 16 | display: block; 17 | 18 | &:focus { 19 | outline: revert; 20 | } 21 | 22 | &:focus-within#{$pseudo-selector} { 23 | background-color: color(config('background', $form-file, false) + '-background-hover', btn); 24 | } 25 | 26 | &#{$pseudo-selector} { 27 | margin-inline-end: spacer('s'); 28 | @include btn-variant(config('background', $form-file, false), false); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /scss/form/_group-label.scss: -------------------------------------------------------------------------------- 1 | @use '../function' as *; 2 | @use '../config' as *; 3 | @use '../mixin' as *; 4 | 5 | @mixin generate-form-group-label { 6 | .form-group-label { 7 | @include generate-variables($form-control, $include: ('border-width', 'border-radius')); 8 | align-items: center; 9 | background-color: color('group-label-background', 'form'); 10 | border: config('border-width', $form-control) solid color('border', 'form'); 11 | border-radius: config('border-radius', $form-control); 12 | color: color('group-label-foreground', 'form'); 13 | display: flex; 14 | padding-inline: spacer('s'); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /scss/form/_group.scss: -------------------------------------------------------------------------------- 1 | @use '../function' as *; 2 | @use '../mixin' as *; 3 | @use '../config' as *; 4 | 5 | @mixin generate-form-group { 6 | .form-group { 7 | @include generate-variables($form-group); 8 | 9 | display: flex; 10 | flex-direction: column; 11 | gap: config('gap', $form-group); 12 | 13 | &--horizontal-check { 14 | @include generate-variables($form-group-check); 15 | display: flex; 16 | flex-direction: row; 17 | flex-wrap: wrap; 18 | gap: config('gap', $form-group-check); 19 | } 20 | 21 | &--vertical-check { 22 | @include generate-variables($form-group-check); 23 | align-items: start; 24 | flex-direction: column; 25 | gap: config('gap', $form-group-check); 26 | } 27 | 28 | &--row { 29 | @include generate-variables($form-group-row); 30 | 31 | align-items: config('vertical-alignment', $form-group-row); 32 | display: grid; 33 | gap: config('gap', $form-group-row); 34 | grid-template-columns: minmax(0, 1fr); 35 | 36 | &\:vertical-center { 37 | align-items: center; 38 | } 39 | 40 | &\:vertical-start { 41 | align-items: flex-start; 42 | } 43 | 44 | @container form-group-container (inline-size > #{config('container-inline-size', $form-group-row, false)}) { 45 | grid-template-columns: minmax(0, #{config('label-inline-size', $form-group-row)}) minmax(0, 1fr); 46 | } 47 | 48 | .form-description, 49 | .field-feedback { 50 | @container form-group-container (inline-size > #{config('container-inline-size', $form-group-row, false)}) { 51 | grid-column-start: 2; 52 | } 53 | } 54 | } 55 | 56 | &--stacked { 57 | display: flex; 58 | 59 | > * { 60 | + * { 61 | border-radius: 0; 62 | margin-inline-start: -1px; 63 | } 64 | 65 | // stylelint-disable 66 | &:first-child { 67 | border-start-end-radius: 0; 68 | border-start-start-radius: config('border-radius', $form-control); 69 | border-end-end-radius: 0; 70 | border-end-start-radius: config('border-radius', $form-control); 71 | } 72 | 73 | &:last-child { 74 | border-start-end-radius: config('border-radius', $form-control); 75 | border-start-start-radius: 0; 76 | border-end-end-radius: config('border-radius', $form-control); 77 | border-end-start-radius: 0; 78 | } 79 | 80 | &:only-child { 81 | border-radius: config('border-radius', $form-control); 82 | } 83 | // stylelint-enable 84 | 85 | &:focus { 86 | z-index: 2; 87 | } 88 | } 89 | } 90 | 91 | &-container { 92 | container: form-group-container / inline-size; 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /scss/form/_index.scss: -------------------------------------------------------------------------------- 1 | @forward 'button'; 2 | @forward 'fieldset'; 3 | @forward 'label'; 4 | @forward 'control'; 5 | @forward 'description'; 6 | @forward 'group-label'; 7 | @forward 'group'; 8 | @forward 'row'; 9 | @forward 'check'; 10 | @forward 'switch'; 11 | @forward 'file'; 12 | @forward 'range'; 13 | @forward 'validation'; 14 | -------------------------------------------------------------------------------- /scss/form/_label.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | @use '../config' as *; 3 | @use '../function' as *; 4 | 5 | @mixin generate-form-label { 6 | .form-label { 7 | color: color('label', 'form'); 8 | font-family: map.get($form-label, 'font-family'); 9 | font-size: map.get($form-label, 'font-size'); 10 | font-style: map.get($form-label, 'font-style'); 11 | font-weight: map.get($form-label, 'font-weight'); 12 | line-height: map.get($typography, 'line-height-md'); 13 | text-align: map.get($form-label, 'text-align'); 14 | text-transform: map.get($form-label, 'text-transform'); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /scss/form/_range.scss: -------------------------------------------------------------------------------- 1 | @use '../config' as *; 2 | @use '../function' as *; 3 | @use '../mixin' as *; 4 | 5 | @mixin generate-form-range { 6 | .form-range { 7 | @include generate-variables($form-range); 8 | appearance: none; 9 | margin-block-start: calc(#{config('thumb-block-size', $form-range)} / 2 - #{config('track-block-size', $form-range)} / 2); 10 | 11 | &:focus-visible { 12 | outline: none; 13 | 14 | &::-webkit-slider-thumb { 15 | @include focus-ring( 16 | $type: config('focus-ring-type', $form-range, false), 17 | $border-color: null, 18 | $ring-color: color('range-thumb-focus-ring', 'form'), 19 | $box-shadow-type: config('focus-ring-box-shadow-type', $form-range, false), 20 | $ring-size: config('focus-ring-size', $form-range, false), 21 | $ring-offset: config('focus-ring-offset', $form-range, false) 22 | ); 23 | } 24 | 25 | &::-moz-range-thumb { 26 | @include focus-ring( 27 | $type: config('focus-ring-type', $form-range, false), 28 | $border-color: null, 29 | $ring-color: color('range-thumb-focus-ring', 'form'), 30 | $box-shadow-type: config('focus-ring-box-shadow-type', $form-range, false), 31 | $ring-size: config('focus-ring-size', $form-range, false), 32 | $ring-offset: config('focus-ring-offset', $form-range, false) 33 | ); 34 | } 35 | } 36 | 37 | &::-webkit-slider-runnable-track { 38 | background-color: color('range-track-background', 'form'); 39 | block-size: config('track-block-size', $form-range); 40 | border-radius: config('track-border-radius', $form-range); 41 | } 42 | 43 | &::-moz-range-track { 44 | background-color: color('range-track-background', 'form'); 45 | block-size: config('track-block-size', $form-range); 46 | border-radius: config('track-border-radius', $form-range); 47 | } 48 | 49 | &::-webkit-slider-thumb { 50 | appearance: none; 51 | background-color: color('range-thumb-background', 'form'); 52 | block-size: config('thumb-block-size', $form-range); 53 | border-radius: config('thumb-border-radius', $form-range); 54 | inline-size: config('thumb-inline-size', $form-range); 55 | margin-block-start: calc(#{config('track-block-size', $form-range)} / 2 - #{config('thumb-block-size', $form-range)} / 2); 56 | } 57 | 58 | &::-moz-range-thumb { 59 | background-color: color('range-thumb-background', 'form'); 60 | block-size: config('thumb-block-size', $form-range); 61 | border: 0; /*Removes extra border that FF applies*/ 62 | border-radius: config('thumb-border-radius', $form-range); 63 | inline-size: config('thumb-inline-size', $form-range); 64 | } 65 | 66 | &:disabled { 67 | cursor: not-allowed; 68 | opacity: 0.5; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /scss/form/_row.scss: -------------------------------------------------------------------------------- 1 | @use '../function' as *; 2 | @use '../mixin' as *; 3 | @use '../config' as *; 4 | 5 | @mixin generate-form-row { 6 | .form-row { 7 | &--mixed { 8 | --inline-size: #{config('inline-size', $form-row, false)}; 9 | @include layout-flex('s'); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /scss/form/_switch.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | @use '../config' as *; 3 | @use '../function' as *; 4 | @use '../mixin' as *; 5 | 6 | @mixin generate-form-switch( 7 | $parent, 8 | $input, 9 | $label, 10 | $has-sizes: false 11 | ) { 12 | #{$parent} { 13 | @include generate-variables($form-switch, ('focus-')); 14 | align-items: config('vertical-alignment', $form-switch); 15 | display: inline-flex; 16 | gap: spacer('xs'); 17 | 18 | &--block { 19 | inline-size: 100%; 20 | justify-content: space-between; 21 | } 22 | } 23 | 24 | #{$parent}--vertical-center { 25 | align-items: center; 26 | } 27 | 28 | #{$parent}--vertical-start { 29 | align-items: flex-start; 30 | } 31 | 32 | @if ($has-sizes) { 33 | #{$parent}--sm { 34 | @include generate-variables($form-switch-sm); 35 | 36 | @if not map.get($settings, 'css-custom-properties') { 37 | #{$input} { 38 | font-size: config('font-size', $form-switch-sm); 39 | } 40 | } 41 | } 42 | 43 | #{$parent}--lg { 44 | @include generate-variables($form-switch-lg); 45 | 46 | @if not map.get($settings, 'css-custom-properties') { 47 | #{$input} { 48 | font-size: config('font-size', $form-switch-lg); 49 | } 50 | } 51 | } 52 | } 53 | 54 | @at-root { 55 | #{$input} { 56 | @include field-icon(config('switch', $form-icon, false), color('border', 'form', 'true')); 57 | appearance: none; 58 | background-color: color('background', 'form'); 59 | background-position: left center; 60 | background-repeat: no-repeat; 61 | background-size: contain; 62 | block-size: 1em; 63 | border: config('border-width', $form-switch) solid color('border', 'form'); 64 | border-radius: 2em; 65 | flex-shrink: 0; 66 | font-size: config('font-size', $form-switch); 67 | inline-size: 2em; 68 | line-height: 1; 69 | margin-block: config('margin-block', $form-switch); 70 | transition-duration: config('duration', $transition); 71 | transition-property: background-position, border, box-shadow; 72 | transition-timing-function: config('timing-function', $transition); 73 | 74 | &:focus-visible { 75 | @include focus-ring( 76 | $type: config('focus-ring-type', $form-check, false), 77 | $border-color: color('switch-background', 'form'), 78 | $ring-color: color('switch-focus-ring', 'form'), 79 | $box-shadow-type: config('focus-ring-box-shadow-type', $form-check, false), 80 | $ring-size: config('focus-ring-size', $form-check, false), 81 | $ring-offset: config('focus-ring-offset', $form-check, false) 82 | ); 83 | } 84 | 85 | &:checked { 86 | @include field-icon(config('switch', $form-icon, false), color('switch-foreground', 'form', 'true')); 87 | background-color: color('switch-background', 'form'); 88 | background-position: right center; 89 | border-color: color('switch-background', 'form'); 90 | } 91 | 92 | &:disabled { 93 | @include field-disabled( 94 | $background: color('background-disabled', 'form'), 95 | $border: color('border-disabled', 'form') 96 | ); 97 | 98 | + #{$label} { 99 | opacity: 0.5; 100 | } 101 | } 102 | } 103 | 104 | [dir='rtl'] #{$input} { 105 | background-position: right center; 106 | 107 | &:checked { 108 | background-position: left center; 109 | } 110 | } 111 | 112 | #{$label} { 113 | font-weight: config('font-weight', $form-switch); 114 | line-height: config('line-height', $form-switch); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /scss/form/_validation.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | @use '../config' as *; 3 | @use '../function' as *; 4 | 5 | @mixin generate-form-feedback { 6 | .field-feedback { 7 | display: block; 8 | line-height: map.get($typography, 'line-height-md'); 9 | 10 | &--valid { 11 | color: color('success', 'alert'); 12 | } 13 | 14 | &--invalid { 15 | color: color('danger', 'alert'); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /scss/function/_color.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:color'; 2 | @use 'sass:map'; 3 | @use 'sass:math'; 4 | @use 'setting' as *; 5 | @use '../config' as *; 6 | 7 | /// Get any color value from $colors (or any) Sass map. 8 | /// @param {string} $key - The key name of the color. 9 | /// @param {string} $type - The type of the color group (base, dark, etc.). 10 | /// @param {boolean} $only-color - If true, return only the color value. 11 | /// @param {map} $map - The map to get the color from. 12 | /// @return {color} - The color value or the variable. 13 | @function color( 14 | $key, 15 | $type: 'base', 16 | $only-color: false, 17 | $map: $colors 18 | ) { 19 | @if not map.has-key($map, $type, $key) { 20 | @error 'The #{$key} key name doesn\'t exist under #{$type} at the specified map (default: $colors).'; 21 | } 22 | 23 | @if map.get($map, $type, $key) == null { 24 | @return null; 25 | } 26 | 27 | @if $only-color { 28 | @return map.get($map, $type, $key); 29 | } 30 | 31 | @if map.get($settings, color-fallback) { 32 | @return var(--#{$internal-prefix}#{$type}-color-#{$key}, #{map.get($map, $type, $key)}); 33 | } 34 | 35 | @return var(--#{$internal-prefix}#{$type}-color-#{$key}); 36 | } 37 | 38 | /// Get any - just - color value from $colors (or any) Sass map. 39 | /// @param {string} $key - The key name of the color. 40 | /// @param {string} $type - The type of the color group (base, dark, etc.). 41 | /// @param {map} $map - The map to get the color from. 42 | /// @return {color} - The color value. 43 | @function color-value( 44 | $key, 45 | $type: 'base', 46 | $map: $colors 47 | ) { 48 | @if not map.has-key($map, $type, $key) { 49 | @error 'The #{$key} key name doesn\'t exist under #{$type} at the specified map (default: $colors).'; 50 | } 51 | 52 | @if map.get($map, $type, $key) == null { 53 | @return null; 54 | } 55 | 56 | @return map.get($map, $type, $key); 57 | } 58 | 59 | /// Get a white or black contrast color for any color (on WCAG standards). 60 | /// Thanks for David Halford for this function: https://codepen.io/davidhalford/pen/ALrbEP 61 | /// @param {color} $color - The color to get the contrast color. 62 | /// @return {color} - The contrast color. 63 | @function color-contrast($color) { 64 | $color-brightness: math.round((color.channel($color, 'red', $space: rgb) * 299) + (color.channel($color, 'green', $space: rgb) * 587) + math.div(color.channel($color, 'blue', $space: rgb) * 114, 1000)); 65 | $light-color: math.round((color.channel(hsl(0deg 0% 100%), 'red', $space: rgb) * 299) + (color.channel(hsl(0deg 0% 100%), 'green', $space: rgb) * 587) + math.div(color.channel(hsl(0deg 0% 100%), 'blue', $space: rgb) * 114, 1000)); 66 | 67 | @if math.abs($color-brightness) < math.div($light-color, 2) { 68 | @return hsl(0 0% 100%); 69 | } @else { 70 | @return hsl(0 100% 0%); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /scss/function/_config.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | @use 'sass:string'; 3 | @use 'setting' as *; 4 | @use '../config' as *; 5 | 6 | /// Get the value of a key from a map. 7 | /// @param {string} $key - The key name. 8 | /// @param {map} $map - The map to get the value from. 9 | /// @param {boolean} $custom-property - Whether to return the value as a CSS custom property. 10 | /// @return {string} - The value of the key. 11 | /// @throws {error} - If the key doesn't exist. 12 | @function config( 13 | $key, 14 | $map, 15 | $custom-property: true 16 | ) { 17 | @if not map.has-key($map, $key) { 18 | @error $key; 19 | @error 'The #{$key} key name doesn\'t exist under #{$map} at the specified map.'; 20 | } 21 | 22 | @if map.get($map, $key) == null { 23 | @return null; 24 | } 25 | 26 | @if not $custom-property { 27 | @return map.get($map, $key); 28 | } 29 | 30 | @if map.get($settings, 'css-custom-properties') { 31 | @return var(--#{$internal-prefix}#{$key}); 32 | } 33 | 34 | @return map.get($map, $key); 35 | } 36 | -------------------------------------------------------------------------------- /scss/function/_css-variable.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:string'; 2 | @use 'sass:map'; 3 | @use '../config' as *; 4 | 5 | /// Add the prefix value to a CSS custom properties. 6 | /// @param {string} $var - The name of the CSS custom property. 7 | /// @return {string} - The CSS custom property with the prefix. 8 | /// @throws {error} - If the CSS custom property name is invalid. 9 | @function get-css-variable($var) { 10 | @if string.index($var, --) != 1 { 11 | @error 'It seems that this is not a valid CSS custom property name.'; 12 | } 13 | 14 | @return var(string.insert($var, '#{$internal-prefix}', 3)); 15 | } 16 | -------------------------------------------------------------------------------- /scss/function/_font-size.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | @use 'sass:math'; 3 | @use '../config' as *; 4 | 5 | /// Get the font size of a key from the $font-sizes map. 6 | /// @param {string} $key - The key name. 7 | /// @param {boolean} $fluid - Whether to return the fluid font size. 8 | /// @param {number} $scaler - The scaler value (15 = 15% smaller). 9 | /// @param {number} $optimal-size - The optimal font size. 10 | /// @return {string} - The font size of the key. 11 | /// @throws {error} - If the key doesn't exist. 12 | @function font-size( 13 | $key, 14 | $fluid: true, 15 | $scaler: map.get($settings, 'scaler'), 16 | $optimal-size: map.get($settings, 'optimal-font-size') 17 | ) { 18 | @if not map.has-key($font-sizes, $key) { 19 | @error 'The #{$key} key name doesn\'t exist at the $font-sizes map.'; 20 | } 21 | 22 | @if $scaler < 0 or $scaler > 100 { 23 | @error 'The $scaler value must be between 0 and 100.'; 24 | } 25 | 26 | @if $fluid { 27 | $scaled-size: map.get($font-sizes, $key) * math.div(100 - $scaler, 100); 28 | 29 | @if $scaled-size < map.get($typography, 'font-size-base') { 30 | @return map.get($font-sizes, $key); 31 | } 32 | 33 | @return clamp(#{$scaled-size}, #{$optimal-size}, #{map.get($font-sizes, $key)}); 34 | } 35 | 36 | @return map.get($font-sizes, $key); 37 | } 38 | 39 | /// Generate responsive font-size value using clamp(). 40 | /// @param {number} $size - The font size. 41 | /// @param {number} $scaler - The scaler value (15 = 15% smaller). 42 | /// @param {number} $optimal-size - The optimal font size. 43 | /// @return {string} - The responsive font-size value. 44 | @function responsive-font-size( 45 | $size, 46 | $scaler: map.get($settings, 'scaler'), 47 | $optimal-size: map.get($settings, 'optimal-font-size') 48 | ) { 49 | @if $scaler < 0 or $scaler > 100 { 50 | @error 'The $scaler value must be between 0 and 100.'; 51 | } 52 | 53 | @return clamp(#{$size * math.div(100 - $scaler, 100)}, #{$optimal-size}, #{$size}); 54 | } 55 | -------------------------------------------------------------------------------- /scss/function/_index.scss: -------------------------------------------------------------------------------- 1 | @forward 'color'; 2 | @forward 'config'; 3 | @forward 'css-variable'; 4 | @forward 'font-size'; 5 | @forward 'utilities'; 6 | @forward 'setting'; 7 | @forward 'spacer'; 8 | -------------------------------------------------------------------------------- /scss/function/_setting.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | @use '../config' as *; 3 | 4 | /// Get spacer value from $settings map. 5 | /// @param {string} $key - The key name. 6 | /// @return {string} - The value of the key. 7 | /// @throws {error} - If the key doesn't exist. 8 | @function setting( 9 | $key, 10 | $group: null 11 | ) { 12 | @if $group { 13 | @if not map.has-key($settings, $group) { 14 | @error 'The #{$group} key name doesn\'t exist at the $settings map.'; 15 | } 16 | 17 | @if not map.has-key(map.get($settings, $group), $key) { 18 | @error 'The #{$key} key name doesn\'t exist at the #{$group} map.'; 19 | } 20 | 21 | @return map.get(map.get($settings, $group), $key); 22 | } 23 | 24 | @if not map.has-key($settings, $key) { 25 | @error 'The #{$key} key name doesn\'t exist at the $settings map.'; 26 | } 27 | 28 | @return map.get($settings, $key); 29 | } 30 | -------------------------------------------------------------------------------- /scss/function/_spacer.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | @use 'sass:string'; 3 | @use 'sass:list'; 4 | @use '../config' as *; 5 | 6 | /// Split a string into a list of values. 7 | /// @param {string} $value - The string to split. 8 | /// @param {string} $separator - The separator to split by. 9 | /// @return {list} - The list of values. 10 | @function split-values($value, $separator: ':') { 11 | $colon-index: string.index($value, $separator); 12 | 13 | @if $colon-index { 14 | $first: string.slice($value, 1, $colon-index - 1); 15 | $second: string.slice($value, $colon-index + 1); 16 | 17 | @return ($first, $second); 18 | } 19 | 20 | @return null; 21 | } 22 | 23 | /// Get spacer value from $spacers map. 24 | /// @param {string} $key - The key name. 25 | /// @return {string} - The value of the key. 26 | /// @throws {error} - If the key doesn't exist. 27 | @function spacer($key) { 28 | @if string.index($key, ':') { 29 | $list: null; 30 | 31 | @each $key in split-values($key) { 32 | @if not map.has-key($spacers, $key) { 33 | @error 'The #{$key} key name doesn\'t exist at the $spacers map.'; 34 | } 35 | 36 | $list: list.append($list, map.get($spacers, $key)); 37 | } 38 | 39 | @return $list; 40 | } 41 | 42 | @if not map.has-key($spacers, $key) { 43 | @error 'The #{$key} key name doesn\'t exist at the $spacers map.'; 44 | } 45 | 46 | @return map.get($spacers, $key); 47 | } 48 | 49 | /// Get value returned in a clamp from $spacers maps. 50 | /// @param {string} $min - The minimum value. 51 | /// @param {string} $max - The maximum value. 52 | /// @param {string} $optimal - The optimal value. 53 | /// @return {string} - The value returned in a clamp. 54 | @function spacer-clamp( 55 | $min, 56 | $max, 57 | $optimal: map.get($settings, 'optimal-spacer-size') 58 | ) { 59 | @if map.has-key($spacers, $min) { 60 | $min: map.get($spacers, $min); 61 | } 62 | 63 | @if map.has-key($spacers, $max) { 64 | $max: map.get($spacers, $max); 65 | } 66 | 67 | @return clamp(#{$min}, #{$optimal}, #{$max}); 68 | } 69 | -------------------------------------------------------------------------------- /scss/function/_utilities.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:string'; 2 | @use '../config' as *; 3 | 4 | // Replace `$search` with `$replace` in `$string` 5 | // @author Kitty Giraudel 6 | // @param {String} $string - Initial string 7 | // @param {String} $search - Substring to replace 8 | // @param {String} $replace ('') - New value 9 | // @return {String} - Updated string 10 | @function str-replace($string, $search, $replace: '') { 11 | $index: string.index($string, $search); 12 | 13 | @if $index { 14 | @return string.slice($string, 1, $index - 1) + $replace + str-replace(string.slice($string, $index + string.length($search)), $search, $replace); 15 | } 16 | 17 | @return $string; 18 | } 19 | 20 | /// Escape a string to be used as a data URI. 21 | /// @param {String} $string - The string to escape. 22 | /// @return {String} - The escaped string. 23 | /// @author Kevin Weber - https://codepen.io/kevinweber/pen/dXWoRw 24 | @function svg-escape($svg) { 25 | @each $char, $encoded in $escaping-characters { 26 | $svg: str-replace($svg, $char, $encoded); 27 | } 28 | @return 'data:image/svg+xml,' + $svg; 29 | } 30 | -------------------------------------------------------------------------------- /scss/mixin/_breakpoint.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | @use '../config' as *; 3 | 4 | /// Return a media query for a breakpoint based on min-width. 5 | /// @param {string} $breakpoint - The breakpoint name. 6 | /// @param {string} $logic - The logic operator. 7 | /// @return {string} - The media query. 8 | /// @throws {error} - If the breakpoint doesn't exist. 9 | @mixin breakpoint( 10 | $breakpoint, 11 | $logic: false 12 | ) { 13 | @if map.has-key($breakpoints, $breakpoint) { 14 | $breakpoint: map.get($breakpoints, $breakpoint); 15 | 16 | @if $logic { 17 | @media #{$logic} and (min-width: $breakpoint) { 18 | @content; 19 | } 20 | } @else { 21 | @media (min-width: $breakpoint) { 22 | @content; 23 | } 24 | } 25 | } @else { 26 | @error 'Invalid breakpoint: #{$breakpoint}.'; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /scss/mixin/_button.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:color'; 2 | @use 'sass:map'; 3 | @use '../function' as *; 4 | @use '../config' as *; 5 | @use 'form' as *; 6 | 7 | /// Generate a button focus ring. 8 | /// @param {string} $type - The type of the button for the color value. 9 | /// @param {boolean} $focus - If the focus ring should be generated. 10 | /// @return {string} - The generated focus ring. 11 | @mixin btn-focus-helper( 12 | $type: 'primary', 13 | $focus: true 14 | ) { 15 | @if $focus { 16 | &:focus-visible { 17 | $ring-color: null; 18 | 19 | @if map.has-key($colors, 'btn', $type + '-focus-ring') { 20 | $ring-color: color($type + '-focus-ring', 'btn'); 21 | } @else { 22 | $ring-color: color($type + '-background', 'btn'); 23 | } 24 | 25 | @include focus-ring( 26 | $type: map.get($btn, 'focus-ring-type'), 27 | $ring-color: $ring-color, 28 | $box-shadow-type: map.get($btn, 'focus-ring-box-shadow-type'), 29 | $ring-size: map.get($btn, 'focus-ring-size'), 30 | $ring-offset: map.get($btn, 'focus-ring-offset') 31 | ); 32 | } 33 | } 34 | } 35 | 36 | /// Generate a button variant. 37 | /// @param {string} $type - The type of the button for the color value. 38 | /// @param {boolean} $focus - If the focus ring should be generated. 39 | /// @return {string} - The generated button variant. 40 | /// @throws {error} - If the color key doesn't exist. 41 | @mixin btn-variant( 42 | $type: 'primary', 43 | $focus: true 44 | ) { 45 | @if not map.has-key($colors, 'btn', $type + '-foreground') or not map.has-key($colors, 'btn', $type + '-background') { 46 | @error 'The #{$type + '-foreground'} or #{$type + '-background'} key name doesn\'t exist under btn at the $colors map.'; 47 | } 48 | 49 | @include btn-focus-helper($type, $focus); 50 | 51 | background-color: color($type + '-background', 'btn'); 52 | border-color: color($type + '-background', 'btn'); 53 | color: color($type + '-foreground', 'btn'); 54 | 55 | &:hover { 56 | @if map.has-key($colors, 'btn', $type + '-background-hover') { 57 | background-color: color($type + '-background-hover', 'btn'); 58 | border-color: color($type + '-background-hover', 'btn'); 59 | } @else { 60 | background-color: color.adjust(color($type + '-background', 'btn', true), $lightness: -10%); 61 | border-color: color.adjust(color($type + '-background', 'btn', true), $lightness: -10%); 62 | } 63 | 64 | @if map.has-key($colors, 'btn', $type + '-foreground-hover') { 65 | color: color($type + '-foreground-hover', 'btn'); 66 | } @else { 67 | color: color($type + '-foreground', 'btn'); 68 | } 69 | } 70 | 71 | @if map.has-key($colors, 'btn', $type + '-shadow') { 72 | &-shadow { 73 | box-shadow: 0 0.55em 1em -0.2em color($type + '-shadow', 'btn'), 0 0.15em 0.35em -0.185em color($type + '-shadow', 'btn'); 74 | } 75 | } 76 | } 77 | 78 | /// Generate a button variant with outline. 79 | /// @param {string} $type - The type of the button for the color value. 80 | /// @param {boolean} $focus - If the focus ring should be generated. 81 | /// @return {string} - The generated button variant with outline. 82 | /// @throws {error} - If the color key doesn't exist. 83 | @mixin btn-variant-outline( 84 | $type: primary, 85 | $focus: true 86 | ) { 87 | @if not map.has-key($colors, 'btn', $type + '-foreground') or not map.has-key($colors, 'btn', $type + '-background') { 88 | @error 'The #{$type + '-foreground'} or #{$type + '-background'} key name doesn\'t exist under btn at the $colors map.'; 89 | } 90 | 91 | @if map.has-key($colors, 'btn', $type + '-outline-focus-ring') { 92 | @include btn-focus-helper($type + '-outline', $focus); 93 | } @else { 94 | @include btn-focus-helper($type, $focus); 95 | } 96 | 97 | background-color: transparent; 98 | 99 | @if map.has-key($colors, 'btn', $type + '-outline-border') { 100 | border-color: color($type + '-outline-border', 'btn'); 101 | } @else { 102 | border-color: color($type + '-background', 'btn'); 103 | } 104 | 105 | @if map.has-key($colors, 'btn', $type + '-outline-foreground') { 106 | color: color($type + '-outline-foreground', 'btn'); 107 | } @else { 108 | color: color($type + '-background', 'btn'); 109 | } 110 | 111 | &:hover { 112 | @if map.has-key($colors, 'btn', $type + '-outline-background-hover') { 113 | background-color: color($type + '-outline-background-hover', 'btn'); 114 | border-color: color($type + '-outline-background-hover', 'btn'); 115 | } @else { 116 | background-color: color($type + '-background', 'btn'); 117 | border-color: color($type + '-background', 'btn'); 118 | } 119 | 120 | @if map.has-key($colors, 'btn', $type + '-outline-foreground-hover') { 121 | color: color($type + '-outline-foreground-hover', 'btn'); 122 | } @else { 123 | color: color($type + '-foreground', 'btn'); 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /scss/mixin/_color.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | @use '../function' as *; 3 | @use '../config' as *; 4 | 5 | /// Generate color variables. 6 | /// @param {map} $colors - The colors map. 7 | /// @param {string} $selector - The selector. 8 | /// @return {string} - The generated color variables. 9 | @mixin generate-color-variables( 10 | $colors: $colors, 11 | $selector: ':root' 12 | ) { 13 | @each $key, $value in $colors { 14 | #{$selector} { 15 | @each $name, $color in $value { 16 | @if $color { 17 | --#{$internal-prefix}#{$key}-color-#{$name}: #{$color}; 18 | } 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /scss/mixin/_css-variable.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:string'; 2 | @use 'sass:map'; 3 | @use '../config' as *; 4 | 5 | /// Declare CSS custom properties through Spruce to add the prefix. 6 | /// @param {map} $vars - The CSS custom properties. 7 | /// @return {null} 8 | /// @throws {error} - If the CSS custom property name is invalid. 9 | @mixin set-css-variable($vars) { 10 | @each $name, $value in $vars { 11 | @if string.index($name, --) != 1 { 12 | @error 'It seems that this is not a valid CSS custom property name.'; 13 | } 14 | 15 | #{string.insert($name, '#{$internal-prefix}', 3)}: #{$value}; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /scss/mixin/_font-face.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:string'; 2 | 3 | /// Generate font-face declaration. 4 | /// @param {string} $font-family - The font family name. 5 | /// @param {string} $src - The font source. 6 | /// @param {number} $font-weight - The font weight. 7 | /// @param {string} $font-style - The font style. 8 | /// @param {string} $font-display - The font display. 9 | /// @return {string} - The generated font-face declaration. 10 | /// @throws {error} - If the font format is not .woff2. 11 | @mixin font-face( 12 | $font-family: null, 13 | $src: null, 14 | $font-weight: 400, 15 | $font-style: normal, 16 | $font-display: swap 17 | ) { 18 | @if not string.index($src, '.woff2') { 19 | @error 'It seems that your font format is not .woff2, please use a that format.'; 20 | } 21 | 22 | @font-face { 23 | font-display: $font-display; 24 | font-family: $font-family; 25 | font-style: $font-style; 26 | font-weight: $font-weight; 27 | src: url('#{$src}') format('woff2'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /scss/mixin/_form.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | @use '../function' as *; 3 | @use '../config' as *; 4 | @use './variables' as *; 5 | @use './breakpoint' as *; 6 | 7 | /// Generate a from focus ring. 8 | /// @param {string} $type - The type of focus ring (box-shadow, outline). 9 | /// @param {string} $border-color - The border color. 10 | /// @param {string} $ring-color - The ring color. 11 | /// @param {string} $box-shadow-type - The box shadow type (outset, inset). 12 | /// @param {string} $ring-size - The ring width. 13 | /// @param {string} $ring-offset - The ring offset. 14 | /// @return {string} - The generated focus ring. 15 | @mixin focus-ring( 16 | $type: 'box-shadow', 17 | $border-color: null, 18 | $ring-color, 19 | $box-shadow-type: outset, 20 | $ring-size: 2px, 21 | $ring-offset: 2px 22 | ) { 23 | @if $type == 'box-shadow' { 24 | border-color: $border-color; 25 | @if $box-shadow-type == 'inset' { 26 | box-shadow: 0 0 0 $ring-size $ring-color inset; 27 | } @else { 28 | box-shadow: 0 0 0 $ring-size $ring-color; 29 | } 30 | outline: 2px solid transparent; 31 | } 32 | 33 | @if $type == 'outline' { 34 | outline: $ring-size solid $ring-color; 35 | outline-offset: $ring-offset; 36 | } 37 | } 38 | 39 | /// Style field disabled input states. 40 | /// @param {string} $background - The background color. 41 | /// @param {string} $border - The border color. 42 | /// @return {string} - The generated disabled input states. 43 | @mixin field-disabled( 44 | $background, 45 | $border 46 | ) { 47 | background-color: $background; 48 | border-color: $border; 49 | cursor: not-allowed; 50 | } 51 | 52 | /// Get custom icon background for input and select fields. 53 | /// @param {string} $icon - The icon (an SVG in string). 54 | /// @param {string} $color - The color. 55 | /// @return {string} - The generated icon background. 56 | @mixin field-icon( 57 | $icon, 58 | $color 59 | ) { 60 | background-image: url('#{svg-escape(str-replace($icon, "#COLOR#", $color))}'); 61 | } 62 | 63 | /// Create a form group stacked layout with custom breakpoint. 64 | /// @param {string} $breakpoint - The breakpoint. 65 | /// @return {string} - The generated form group stacked layout. 66 | @mixin form-group-stacked( 67 | $breakpoint: 'sm', 68 | ) { 69 | @if not map.has-key($breakpoints, $breakpoint) { 70 | @error 'The #{$breakpoint} not exists in the breakpoints map.'; 71 | } 72 | 73 | @include generate-variables($form-control, $include: ('border-radius')); 74 | display: flex; 75 | flex-direction: column; 76 | 77 | @include breakpoint($breakpoint) { flex-direction: row; } 78 | 79 | > * { 80 | + * { 81 | border-start-end-radius: 0; 82 | border-start-start-radius: 0; 83 | margin-block-start: -1px; 84 | 85 | @include breakpoint($breakpoint) { 86 | border-end-start-radius: 0; 87 | border-start-end-radius: config('border-radius', $form-control); 88 | margin-block-start: 0; 89 | margin-inline-start: -1px; 90 | } 91 | } 92 | 93 | &:not(:last-child) { 94 | border-end-end-radius: 0; 95 | border-end-start-radius: 0; 96 | 97 | @include breakpoint($breakpoint) { 98 | border-end-end-radius: 0; 99 | border-start-end-radius: 0; 100 | } 101 | } 102 | 103 | @include breakpoint($breakpoint) { 104 | &:first-child { 105 | border-end-start-radius: config('border-radius', $form-control); 106 | } 107 | } 108 | 109 | &:focus { 110 | z-index: 2; 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /scss/mixin/_generator.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | @use '../config' as *; 3 | @use '../element' as *; 4 | @use '../plugin' as *; 5 | @use '../form' as *; 6 | @use '../print' as *; 7 | @use 'button' as *; 8 | 9 | /// Generate all the styles. 10 | @mixin generate-styles { 11 | @if map.get($generators, 'content', 'normalize') { 12 | @include generate-normalize; 13 | } 14 | 15 | @if map.get($generators, 'content', 'root') { 16 | @include generate-root; 17 | } 18 | 19 | @if map.get($generators, 'content', 'accessibility') { 20 | @include generate-accessibility; 21 | } 22 | 23 | @if map.get($generators, 'content', 'default') { 24 | @include generate-default; 25 | } 26 | 27 | @if map.get($generators, 'content', 'divider') { 28 | @include generate-divider; 29 | } 30 | 31 | @if map.get($generators, 'content', 'media') { 32 | @include generate-media; 33 | } 34 | 35 | @if map.get($generators, 'content', 'table') { 36 | @include generate-table; 37 | } 38 | 39 | @if map.get($generators, 'content', 'typography') { 40 | @include generate-typography; 41 | } 42 | 43 | @if map.get($generators, 'content', 'utilities') { 44 | @include generate-utilities; 45 | } 46 | 47 | @if map.get($generators, 'content', 'print') { 48 | @include generate-print; 49 | } 50 | 51 | @if map.get($generators, 'form', 'btn') { 52 | @include generate-btn('.btn'); 53 | 54 | .btn--primary { @include btn-variant(primary); } 55 | .btn--secondary { @include btn-variant(secondary); } 56 | .btn--outline-primary { @include btn-variant-outline(primary); } 57 | .btn--outline-secondary { @include btn-variant-outline(secondary); } 58 | } 59 | 60 | @if map.get($generators, 'form', 'file-btn') { 61 | @include generate-file-btn( 62 | '.form-file', 63 | '::file-selector-button', 64 | false, 65 | true 66 | ); 67 | } 68 | 69 | @if map.get($generators, 'form', 'form-label') { 70 | @include generate-form-label; 71 | } 72 | 73 | @if map.get($generators, 'form', 'form-control') { 74 | @include generate-form-control( 75 | '.form-control', 76 | true, 77 | true, 78 | true 79 | ); 80 | } 81 | 82 | @if map.get($generators, 'form', 'form-check') { 83 | @include generate-form-check( 84 | '.form-check', 85 | '.form-check__control', 86 | '.form-check__label', 87 | true 88 | ); 89 | } 90 | 91 | @if map.get($generators, 'form', 'form-switch') { 92 | @include generate-form-switch( 93 | '.form-switch', 94 | '.form-switch__control', 95 | '.form-switch__label', 96 | true 97 | ); 98 | } 99 | 100 | @if map.get($generators, 'form', 'form-fieldset') { 101 | @include generate-form-fieldset; 102 | } 103 | 104 | @if map.get($generators, 'form', 'form-group-label') { 105 | @include generate-form-group-label; 106 | } 107 | 108 | @if map.get($generators, 'form', 'form-group') { 109 | @include generate-form-group; 110 | } 111 | 112 | @if map.get($generators, 'form', 'form-row') { 113 | @include generate-form-row; 114 | } 115 | 116 | @if map.get($generators, 'form', 'form-feedback') { 117 | @include generate-form-feedback; 118 | } 119 | 120 | @if map.get($generators, 'form', 'form-range') { 121 | @include generate-form-range; 122 | } 123 | 124 | @if map.get($generators, 'form', 'form-description') { 125 | @include generate-form-description; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /scss/mixin/_index.scss: -------------------------------------------------------------------------------- 1 | @forward 'breakpoint'; 2 | @forward 'button'; 3 | @forward 'color'; 4 | @forward 'css-variable'; 5 | @forward 'font-face'; 6 | @forward 'form'; 7 | @forward 'layout'; 8 | @forward 'transition'; 9 | @forward 'selection'; 10 | @forward 'utilities'; 11 | @forward 'variables'; 12 | -------------------------------------------------------------------------------- /scss/mixin/_layout.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | @use 'sass:meta'; 3 | @use 'sass:string'; 4 | @use 'sass:list'; 5 | @use '../function' as *; 6 | @use '../config' as *; 7 | 8 | /// Create center layout. 9 | /// @param {string} $gap - The gap between the container and the content. 10 | /// @param {string} $max-inline-size - The maximum width (inline-size) of the container. 11 | /// @return {mixin} - The centered layout. 12 | @mixin layout-center( 13 | $gap: m, 14 | $max-inline-size: config('container-inline-size', $layout) 15 | ) { 16 | @if map.has-key($spacers, $gap) { 17 | $gap: map.get($spacers, $gap); 18 | } 19 | 20 | inline-size: 100%; 21 | margin-inline: auto; 22 | max-inline-size: $max-inline-size; 23 | padding-inline: $gap; 24 | } 25 | 26 | /// Create stack layout. 27 | /// @param {string} $gap - The gap between the the elements. 28 | /// @param {boolean} $inline-size - Whether it has explicit width (inline-size). 29 | /// @param {string} $align - The horizontal alignment of the elements. 30 | /// @param {boolean} $important - Whether it should use the !important keyword. 31 | /// @return {mixin} - The stacked layout. 32 | @mixin layout-stack( 33 | $gap: 'm', 34 | $inline-size: false, 35 | $align: none, 36 | $important: false 37 | ) { 38 | @if map.has-key($spacers, $gap) { 39 | $gap: map.get($spacers, $gap); 40 | } 41 | 42 | @if $align == left or $align == right { 43 | display: flex; 44 | flex-direction: column; 45 | } 46 | 47 | @if $align == left { 48 | align-items: flex-start; 49 | } 50 | 51 | @if $align == right { 52 | align-items: flex-end; 53 | } 54 | 55 | > * { 56 | margin-block-end: 0; 57 | margin-block-start: 0; 58 | 59 | @if $inline-size and $align == none { 60 | inline-size: 100%; 61 | } 62 | } 63 | 64 | > * + * { 65 | @if $important == true { 66 | margin-block-start: $gap !important; 67 | } @else { 68 | margin-block-start: $gap; 69 | } 70 | } 71 | } 72 | 73 | /// Create grid layout. 74 | /// @param {string} $gap - The gap between the the elements. 75 | /// @param {string} $minimum - The minimum width (inline-size) of the elements. 76 | /// @return {mixin} - The grid layout. 77 | @mixin layout-grid( 78 | $gap: 'm', 79 | $minimum: 12.5rem 80 | ) { 81 | @if meta.type-of($gap) == string and string.index($gap, ':') { 82 | $gap: spacer($gap); 83 | } @else if map.has-key($spacers, $gap) { 84 | $gap: map.get($spacers, $gap); 85 | } 86 | 87 | display: grid; 88 | gap: $gap; 89 | 90 | @supports (inline-size: min(#{$minimum}, 100%)) { 91 | & { 92 | grid-template-columns: repeat(auto-fit, minmax(min(#{$minimum}, 100%), 1fr)); 93 | } 94 | } 95 | } 96 | 97 | /// Create sidebar layout. 98 | /// @param {string} $gap - The gap between the the elements. 99 | /// @param {string} $inline-size - The width (flex-basis) of the sidebar. 100 | /// @return {mixin} - The sidebar layout. 101 | @mixin layout-sidebar( 102 | $gap: 'm', 103 | $inline-size: 18.75rem 104 | ) { 105 | @if meta.type-of($gap) == string and string.index($gap, ':') { 106 | $gap: spacer($gap); 107 | } @else if map.has-key($spacers, $gap) { 108 | $gap: map.get($spacers, $gap); 109 | } 110 | 111 | display: flex; 112 | flex-wrap: wrap; 113 | gap: $gap; 114 | 115 | & > :first-child { 116 | flex-basis: $inline-size; 117 | flex-grow: 1; 118 | } 119 | 120 | & > :last-child { 121 | flex-basis: 0; 122 | flex-grow: 999; 123 | min-inline-size: 50%; 124 | } 125 | } 126 | 127 | /// Create instinctive flex layout. 128 | /// @param {string} $gap - The gap between the the elements. 129 | /// @param {string} $inline-size - The width (inline size) of the elements. 130 | /// @return {mixin} - The instinctive flex layout. 131 | @mixin layout-flex( 132 | $gap: 'm', 133 | $inline-size: var(--inline-size) 134 | ) { 135 | @if map.has-key($spacers, $gap) { 136 | $gap: map.get($spacers, $gap); 137 | } 138 | 139 | display: flex; 140 | flex-wrap: wrap; 141 | gap: $gap; 142 | 143 | > * { 144 | flex: 1 1 $inline-size; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /scss/mixin/_selection.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | @use '../function' as *; 3 | @use '../config' as *; 4 | 5 | /// Set the ::selection of an element with automatic foreground color. 6 | /// @param {color} $background - The background color. 7 | /// @param {color} $foreground - The foreground color. If null, the color will be automatically calculated. 8 | /// @param {boolean} $is-direct - If true, the selection will be applied to the current element if false it will be applied to its children. 9 | /// @return {mixin} - The selection mixin. 10 | @mixin selection( 11 | $background: 'primary', 12 | $foreground: null, 13 | $is-direct: false 14 | ) { 15 | $is-variable: false; 16 | $original-background: $background; 17 | 18 | @if map.has-key($colors, 'base', $background) { 19 | $background: color($background); 20 | $is-variable: true; 21 | } 22 | 23 | @if not $foreground and not $is-variable { 24 | $foreground: color-contrast($background); 25 | } @else if not $foreground and $is-variable { 26 | $foreground: color-contrast(color($original-background, $only-color: true)); 27 | } 28 | 29 | @if $is-direct { 30 | &::selection { 31 | background-color: $background; 32 | color: $foreground; 33 | } 34 | } @else { 35 | ::selection { 36 | background-color: $background; 37 | color: $foreground; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /scss/mixin/_transition.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:string'; 2 | @use '../function' as *; 3 | @use '../config' as *; 4 | 5 | /// Generates transition related declarations. 6 | /// @param {string} $duration - The duration of the transition. 7 | /// @param {string} $property - The property to which the transition is applied. 8 | /// @param {string} $timing-function - The speed curve of the transition. 9 | /// @return {string} - The generated transition declarations. 10 | /// @throws {error} - If the duration or timing-function is invalid. 11 | @mixin transition( 12 | $duration: config('duration', $transition), 13 | $property: all, 14 | $timing-function: config('timing-function', $transition), 15 | ) { 16 | transition-duration: $duration; 17 | transition-property: string.unquote($property); 18 | transition-timing-function: $timing-function; 19 | } 20 | -------------------------------------------------------------------------------- /scss/mixin/_utilities.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | @use '../function' as *; 3 | @use '../config' as *; 4 | @use 'form' as *; 5 | 6 | 7 | /// Hide something from the screen but keep it visible for assistive technology. 8 | /// @return {mixin} - The visually hidden mixin. 9 | @mixin visually-hidden { 10 | block-size: 1px !important; 11 | border: 0 !important; 12 | clip: rect(0, 0, 0, 0) !important; 13 | inline-size: 1px !important; 14 | margin: -1px !important; 15 | overflow: hidden !important; 16 | padding: 0 !important; 17 | position: absolute !important; 18 | white-space: nowrap !important; 19 | } 20 | 21 | /// Crop text and display an ellipsis with multiline. 22 | /// @param {number} $number-of-lines - The number of lines. 23 | /// @return {mixin} - The text ellipsis mixin. 24 | @mixin text-ellipsis( 25 | $number-of-lines: 1 26 | ) { 27 | overflow: hidden; 28 | text-overflow: ellipsis; 29 | 30 | @if $number-of-lines == 1 { 31 | white-space: nowrap; 32 | } @else { 33 | white-space: inherit; 34 | 35 | @supports (-webkit-line-clamp: $number-of-lines) { 36 | -webkit-box-orient: vertical; 37 | display: -webkit-box; 38 | -webkit-line-clamp: $number-of-lines; 39 | } 40 | } 41 | } 42 | 43 | /// Custom scrollbar. 44 | /// @param {string} $thumb-background-color - The background color of the thumb. 45 | /// @param {string} $thumb-background-color-hover - The background color of the thumb when hovered. 46 | /// @param {string} $track-background-color - The background color of the track. 47 | /// @param {string} $size - The size of the scrollbar. 48 | /// @param {string} $border-radius - The border radius of the scrollbar. 49 | /// @return {mixin} - The scrollbar mixin. 50 | @mixin scrollbar( 51 | $thumb-background-color: color('thumb-background', 'scrollbar'), 52 | $thumb-background-color-hover: color('thumb-background-hover', 'scrollbar'), 53 | $track-background-color: color('track-background', 'scrollbar'), 54 | $size: 0.5rem, 55 | $border-radius: config('border-radius-sm', $display) 56 | ) { 57 | &::-webkit-scrollbar { 58 | block-size: $size; 59 | inline-size: $size; 60 | } 61 | 62 | &::-webkit-scrollbar-thumb { 63 | background: $thumb-background-color; 64 | border-radius: $border-radius; 65 | 66 | &:hover { 67 | background: $thumb-background-color-hover; 68 | } 69 | } 70 | 71 | &::-webkit-scrollbar-track { 72 | background: $track-background-color; 73 | border-radius: $border-radius; 74 | } 75 | } 76 | 77 | /// Clear default button styles. 78 | /// @return {mixin} - The clear button mixin. 79 | @mixin clear-btn { 80 | background: none; 81 | border: 0; 82 | color: inherit; 83 | cursor: pointer; 84 | font: inherit; 85 | outline: inherit; 86 | padding: 0; 87 | } 88 | 89 | // Clear list styles 90 | @mixin clear-list { 91 | list-style: none; 92 | margin: 0; 93 | padding: 0; 94 | } 95 | 96 | /// More accessible card linking. 97 | /// @param {string} $link - The link element's selector. 98 | /// @param {boolean} $at-root - Whether to use @at-root. 99 | /// @return {mixin} - The a11y card link mixin. 100 | @mixin a11y-card-link( 101 | $link, 102 | $at-root: false 103 | ) { 104 | position: relative; 105 | 106 | @if $at-root == true { 107 | @at-root { 108 | #{$link}::before { 109 | content: ''; 110 | inset: 0; 111 | position: absolute; 112 | } 113 | } 114 | } @else { 115 | #{$link}::before { 116 | content: ''; 117 | inset: 0; 118 | position: absolute; 119 | } 120 | } 121 | } 122 | 123 | /// Break long string. 124 | /// @author Chris Coyier - https://css-tricks.com/snippets/css/prevent-long-urls-from-breaking-out-of-container/ 125 | /// @return {mixin} - The word-wrap mixin. 126 | @mixin word-wrap { 127 | hyphens: auto; 128 | overflow-wrap: break-word; 129 | word-break: break-all; 130 | word-break: break-word; 131 | word-wrap: break-word; 132 | } 133 | 134 | /// Generate a focus ring. 135 | /// @param {string} $type - The type of the focus ring. 136 | /// @param {string} $btn-type - The type - hence color - of the button. 137 | /// @return {mixin} - The focus ring mixin. 138 | @mixin short-ring( 139 | $type: 'input', 140 | $btn-type: 'primary' 141 | ) { 142 | @if $type == 'input' { 143 | @include focus-ring( 144 | $type: config('focus-ring-type', $form-control, false), 145 | $border-color: color('border-focus', 'form'), 146 | $ring-color: color('ring-focus', 'form'), 147 | $box-shadow-type: config('focus-ring-box-shadow-type', $form-control, false), 148 | $ring-size: config('focus-ring-size', $form-control, false), 149 | $ring-offset: config('focus-ring-offset', $form-control, false) 150 | ); 151 | } 152 | 153 | @if $type == 'button' { 154 | $ring-color: null; 155 | 156 | @if map.has-key($colors, 'btn', $btn-type + '-focus-ring') { 157 | $ring-color: color($btn-type + '-focus-ring', 'btn'); 158 | } @else { 159 | $ring-color: color($btn-type + '-background', 'btn'); 160 | } 161 | 162 | @include focus-ring( 163 | $type: config('focus-ring-type', $btn, false), 164 | $ring-color: $ring-color, 165 | $box-shadow-type: config('focus-ring-box-shadow-type', $btn, false), 166 | $ring-size: config('focus-ring-size', $btn, false), 167 | $ring-offset: config('focus-ring-offset', $btn, false) 168 | ); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /scss/mixin/_variables.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | @use 'sass:string'; 3 | @use '../config' as *; 4 | 5 | /// Generate CSS custom properties based on a map. 6 | /// @param {map} $map - The map to generate the CSS custom properties from. 7 | /// @param {list} $exclude - The list of keys (or a segment of it) to exclude. 8 | /// @param {list} $include - The list of keys (or a segment of it) to include. 9 | /// @return {string} - The generated CSS custom properties. 10 | /// @throws {error} - If you use both $exclude and $include arguments. 11 | @mixin generate-variables( 12 | $map, 13 | $exclude: null, 14 | $include: null 15 | ) { 16 | @if $exclude and $include { 17 | @error 'You can\'t use both $exclude and $include arguments.'; 18 | } 19 | 20 | @if map.get($settings, 'css-custom-properties') { 21 | $exclude-map: $map; 22 | $include-map: (); 23 | 24 | @if $exclude { 25 | @each $key, $value in $map { 26 | @if $value { 27 | @each $fraction in $exclude { 28 | @if string.index($key, $fraction) { 29 | $exclude-map: map.remove($exclude-map, $key); 30 | } 31 | } 32 | } 33 | } 34 | } 35 | 36 | @if $include { 37 | @each $key, $value in $map { 38 | @if $value { 39 | @each $fraction in $include { 40 | @if string.index($key, $fraction) { 41 | $include-map: map.set($include-map, $key, $value); 42 | } 43 | } 44 | } 45 | } 46 | } 47 | 48 | @if $exclude { 49 | @each $key, $value in $exclude-map { 50 | @if $value { 51 | --#{$internal-prefix}#{$key}: #{$value}; 52 | } 53 | } 54 | } 55 | 56 | @if $include { 57 | @each $key, $value in $include-map { 58 | @if $value { 59 | --#{$internal-prefix}#{$key}: #{$value}; 60 | } 61 | } 62 | } 63 | 64 | @if not $exclude and not $include { 65 | @each $key, $value in $map { 66 | @if $value { 67 | --#{$internal-prefix}#{$key}: #{$value}; 68 | } 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /scss/plugin/_index.scss: -------------------------------------------------------------------------------- 1 | @forward 'normalize'; 2 | -------------------------------------------------------------------------------- /scss/print/_index.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:map'; 2 | @use '../config' as *; 3 | @use '../function' as *; 4 | @use '../mixin' as *; 5 | 6 | @mixin generate-print { 7 | @if setting('print') == true { 8 | @media print { 9 | @page { 10 | margin: config('page-margin', $print); 11 | } 12 | 13 | #{config('hidden-elements', $print, false)} { 14 | display: none !important; 15 | } 16 | 17 | a[href^='http']::after { 18 | content: ' (' attr(href) ')'; 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /scss/spruce-styles.scss: -------------------------------------------------------------------------------- 1 | @use 'spruce' as * with ( 2 | $settings: ( 3 | 'css-custom-properties': true, 4 | ), 5 | ); 6 | 7 | @include generate-styles; 8 | -------------------------------------------------------------------------------- /scss/spruce.scss: -------------------------------------------------------------------------------- 1 | @forward 'config'; 2 | @forward 'function'; 3 | @forward 'mixin'; 4 | @forward 'mixin/generator'; 5 | @forward 'element'; 6 | @forward 'form'; 7 | @forward 'print'; 8 | @forward 'plugin'; 9 | -------------------------------------------------------------------------------- /test/function/_config.scss: -------------------------------------------------------------------------------- 1 | @use 'true' as *; 2 | @use '../../scss/config' as *; 3 | @use '../../scss/function/config' as *; 4 | 5 | @include describe('config()') { 6 | @include it('should return "0.75em 1em" as the base padding for the button elements') { 7 | @include assert-equal(config('padding', $btn), 0.75em 1em); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/function/_index.scss: -------------------------------------------------------------------------------- 1 | @forward 'config'; 2 | -------------------------------------------------------------------------------- /test/sass.test.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const sassTrue = require('sass-true'); 3 | 4 | const sassFile = path.join(__dirname, 'test.scss'); 5 | sassTrue.runSass({ describe, it }, sassFile); 6 | -------------------------------------------------------------------------------- /test/test.scss: -------------------------------------------------------------------------------- 1 | @forward 'function'; 2 | --------------------------------------------------------------------------------