├── .nvmrc ├── tests ├── __init__.py ├── tests │ ├── __init__.py │ ├── utils.py │ ├── test_sections.py │ ├── test_context.py │ ├── test_tags.py │ ├── test_utils.py │ ├── test_context_modifiers.py │ └── test_commands.py ├── settings │ ├── __init__.py │ ├── dev.py │ ├── production.py │ └── base.py ├── templatetags │ ├── __init__.py │ ├── test_tags_invalid.py │ └── test_tags.py ├── templates │ ├── patterns │ │ ├── atoms │ │ │ ├── icons │ │ │ │ ├── icon.yaml │ │ │ │ └── icon.html │ │ │ ├── test_atom │ │ │ │ ├── test_atom.html │ │ │ │ ├── test_atom.yaml │ │ │ │ └── test_atom.md │ │ │ ├── test_atom_yml │ │ │ │ ├── test_atom_yml.html │ │ │ │ └── test_atom_yml.yml │ │ │ ├── test_extends │ │ │ │ ├── base.html │ │ │ │ ├── extended.yaml │ │ │ │ └── extended.html │ │ │ ├── test_includes │ │ │ │ ├── test_includes.html │ │ │ │ └── test_includes.yaml │ │ │ ├── tags_test_atom │ │ │ │ ├── invalid_tags_test_atom.html.fail │ │ │ │ ├── tags_test_atom.html │ │ │ │ └── tags_test_atom.yaml │ │ │ └── sprites │ │ │ │ └── sprites.html │ │ ├── molecules │ │ │ ├── test-molecule │ │ │ │ └── test-molecule.html │ │ │ ├── test_molecule │ │ │ │ ├── test_molecule_no_context.html │ │ │ │ ├── test_molecule.yaml │ │ │ │ └── test_molecule.html │ │ │ ├── accordion │ │ │ │ ├── accordion.md │ │ │ │ ├── accordion.yaml │ │ │ │ └── accordion.html │ │ │ ├── button │ │ │ │ ├── button.yaml │ │ │ │ └── button.html │ │ │ └── field │ │ │ │ └── field.html │ │ ├── pages │ │ │ ├── people │ │ │ │ ├── person_page.md │ │ │ │ ├── person_page.yaml │ │ │ │ └── person_page.html │ │ │ ├── search │ │ │ │ ├── search.yaml │ │ │ │ └── search.html │ │ │ ├── test_page │ │ │ │ ├── test_page.html │ │ │ │ └── test_page.yaml │ │ │ └── forms │ │ │ │ └── example_form.html │ │ ├── base_page.html │ │ └── base.html │ └── non-patterns │ │ ├── variable_include.html │ │ └── include.html ├── jinja │ ├── non-patterns │ │ ├── variable_include.html │ │ └── include.html │ └── patterns_jinja │ │ ├── components │ │ ├── test_atom │ │ │ ├── test_atom.html │ │ │ ├── test_atom.yaml │ │ │ └── test_atom.md │ │ ├── icons │ │ │ ├── icon.yaml │ │ │ └── icon.html │ │ ├── test_molecule │ │ │ ├── test_molecule_no_context.html │ │ │ ├── test_molecule.yaml │ │ │ └── test_molecule.html │ │ ├── test_extends │ │ │ ├── base.html │ │ │ ├── extended.yaml │ │ │ └── extended.html │ │ ├── button │ │ │ ├── button.yaml │ │ │ └── button.html │ │ ├── accordion │ │ │ ├── accordion.md │ │ │ ├── accordion.yaml │ │ │ └── accordion.html │ │ ├── test_includes │ │ │ ├── test_includes.html │ │ │ └── test_includes.yaml │ │ └── sprites │ │ │ └── sprites.html │ │ ├── base_page_jinja.html │ │ ├── pages │ │ ├── search │ │ │ ├── search.yaml │ │ │ └── search.html │ │ └── test_page │ │ │ ├── test_page.html │ │ │ └── test_page.yaml │ │ └── base_jinja.html ├── forms.py ├── urls.py ├── jinja2.py ├── static │ ├── main.js │ └── main.css └── pattern_contexts.py ├── pattern_library ├── py.typed ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── render_patterns.py ├── apps.py ├── static │ └── pattern_library │ │ └── src │ │ ├── scss │ │ ├── layout │ │ │ ├── _main.scss │ │ │ ├── _body.scss │ │ │ ├── _header.scss │ │ │ └── _sidebar.scss │ │ ├── components │ │ │ ├── _code.scss │ │ │ ├── _icon.scss │ │ │ ├── _wrapper.scss │ │ │ ├── _iframe.scss │ │ │ ├── _md.scss │ │ │ ├── _button.scss │ │ │ ├── _heading.scss │ │ │ ├── _tabbed-listing.scss │ │ │ └── _list.scss │ │ ├── _config.scss │ │ ├── main.scss │ │ ├── abstracts │ │ │ └── _base.scss │ │ └── hljs │ │ │ ├── _a11y-dark.scss │ │ │ └── _a11y-light.scss │ │ └── js │ │ ├── components │ │ ├── hide-menu-mobile.js │ │ ├── syntax-highlighting.js │ │ ├── navigation.js │ │ ├── persist-menu.js │ │ ├── tabbed-content.js │ │ ├── pattern-search.js │ │ └── iframe.js │ │ └── app.js ├── exceptions.py ├── urls.py ├── cm_utils.py ├── templates │ └── pattern_library │ │ ├── pattern_group.html │ │ ├── base.html │ │ └── index.html ├── __init__.py ├── context_modifiers.py └── views.py ├── .dockerignore ├── docs ├── images │ ├── favicon.png │ ├── pattern-library-screenshot.webp │ ├── pattern-library-talk-youtube.webp │ ├── getting-started │ │ ├── PatternLibraryEmpty.png │ │ └── getting-started-complete.png │ ├── logo.svg │ └── favicon.svg ├── recipes │ ├── inclusion-tags.md │ ├── image-include.md │ ├── image-lazyload.md │ ├── api-rendering.md │ ├── looping-for-tags.md │ ├── forms-and-fields.md │ └── pagination.md ├── community │ ├── security-policy.md │ ├── support.md │ ├── related-projects.md │ └── code-of-conduct.md ├── guides │ ├── reuse-across-projects.md │ ├── static-site-export.md │ ├── multiple-variants.md │ ├── customizing-template-rendering.md │ └── usage-tips.md ├── reference │ ├── concepts.md │ └── known-issues.md └── index.md ├── .github ├── pattern-library-screenshot.webp ├── SECURITY.md ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── BUG_REPORT.md │ └── FEATURE_REQUEST.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── codeql-analysis.yml │ └── ci.yml ├── tox_install.sh ├── setup.py ├── .babelrc.js ├── stylelint.config.js ├── .prettierrc.toml ├── .editorconfig ├── .git-blame-ignore-revs ├── Dockerfile ├── docker-compose.yml ├── .devcontainer └── devcontainer.json ├── tox.ini ├── runtests.py ├── .gitpod.yml ├── webpack.config.js ├── package.json ├── LICENSE ├── .gitignore ├── mkdocs.yml ├── pyproject.toml ├── README.md └── CONTRIBUTING.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 24 2 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pattern_library/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/settings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pattern_library/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pattern_library/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/settings/dev.py: -------------------------------------------------------------------------------- 1 | from .base import * # noqa 2 | -------------------------------------------------------------------------------- /tests/templates/patterns/atoms/icons/icon.yaml: -------------------------------------------------------------------------------- 1 | context: 2 | name: close 3 | -------------------------------------------------------------------------------- /tests/templates/patterns/atoms/test_atom/test_atom.html: -------------------------------------------------------------------------------- 1 | {{ atom_var }} 2 | -------------------------------------------------------------------------------- /tests/jinja/non-patterns/variable_include.html: -------------------------------------------------------------------------------- 1 | included content from variable 2 | -------------------------------------------------------------------------------- /tests/jinja/patterns_jinja/components/test_atom/test_atom.html: -------------------------------------------------------------------------------- 1 | {{ atom_var }} 2 | -------------------------------------------------------------------------------- /tests/templates/patterns/atoms/test_atom_yml/test_atom_yml.html: -------------------------------------------------------------------------------- 1 | {{ atom_var }} 2 | -------------------------------------------------------------------------------- /tests/jinja/patterns_jinja/components/icons/icon.yaml: -------------------------------------------------------------------------------- 1 | context: 2 | name: close 3 | -------------------------------------------------------------------------------- /tests/templates/non-patterns/variable_include.html: -------------------------------------------------------------------------------- 1 | included content from variable 2 | -------------------------------------------------------------------------------- /tests/templates/patterns/molecules/test-molecule/test-molecule.html: -------------------------------------------------------------------------------- 1 | template-with-dash 2 | -------------------------------------------------------------------------------- /tests/templates/patterns/molecules/test_molecule/test_molecule_no_context.html: -------------------------------------------------------------------------------- 1 | No context 2 | -------------------------------------------------------------------------------- /tests/jinja/non-patterns/include.html: -------------------------------------------------------------------------------- 1 | SHOWME 2 | {% if False %} 3 | HIDEME 4 | {% endif %} 5 | -------------------------------------------------------------------------------- /tests/jinja/patterns_jinja/components/test_molecule/test_molecule_no_context.html: -------------------------------------------------------------------------------- 1 | No context 2 | -------------------------------------------------------------------------------- /tests/templates/non-patterns/include.html: -------------------------------------------------------------------------------- 1 | SHOWME 2 | {% if False %} 3 | HIDEME 4 | {% endif %} 5 | -------------------------------------------------------------------------------- /tests/templates/patterns/atoms/test_extends/base.html: -------------------------------------------------------------------------------- 1 | {% block content %}base content{% endblock %} 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | /.git/ 2 | node_modules/ 3 | /build/ 4 | /dist/ 5 | /pattern_library/static/ 6 | site/ 7 | -------------------------------------------------------------------------------- /docs/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torchbox/django-pattern-library/HEAD/docs/images/favicon.png -------------------------------------------------------------------------------- /tests/jinja/patterns_jinja/components/test_extends/base.html: -------------------------------------------------------------------------------- 1 | {% block content %}base content{% endblock %} 2 | -------------------------------------------------------------------------------- /tests/templates/patterns/pages/people/person_page.md: -------------------------------------------------------------------------------- 1 | # person_page 2 | 3 | Testing page-level documentation 4 | -------------------------------------------------------------------------------- /tests/templates/patterns/atoms/test_atom/test_atom.yaml: -------------------------------------------------------------------------------- 1 | context: 2 | atom_var: 'atom_var value from test_atom.yaml' 3 | -------------------------------------------------------------------------------- /tests/templates/patterns/base_page.html: -------------------------------------------------------------------------------- 1 | {% extends 'patterns/base.html' %} 2 | 3 | {% block title %}Page{% endblock %} 4 | -------------------------------------------------------------------------------- /tests/jinja/patterns_jinja/components/test_atom/test_atom.yaml: -------------------------------------------------------------------------------- 1 | context: 2 | atom_var: 'atom_var value from test_atom.yaml' 3 | -------------------------------------------------------------------------------- /tests/templates/patterns/atoms/test_extends/extended.yaml: -------------------------------------------------------------------------------- 1 | context: 2 | parent_template_name: patterns/atoms/test_extends/base.html 3 | -------------------------------------------------------------------------------- /.github/pattern-library-screenshot.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torchbox/django-pattern-library/HEAD/.github/pattern-library-screenshot.webp -------------------------------------------------------------------------------- /tests/templates/patterns/molecules/accordion/accordion.md: -------------------------------------------------------------------------------- 1 | # Accordion 2 | 3 | The accordion is a good example of a pattern library component. 4 | -------------------------------------------------------------------------------- /docs/images/pattern-library-screenshot.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torchbox/django-pattern-library/HEAD/docs/images/pattern-library-screenshot.webp -------------------------------------------------------------------------------- /docs/images/pattern-library-talk-youtube.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torchbox/django-pattern-library/HEAD/docs/images/pattern-library-talk-youtube.webp -------------------------------------------------------------------------------- /tests/jinja/patterns_jinja/base_page_jinja.html: -------------------------------------------------------------------------------- 1 | {% extends 'patterns_jinja/base_jinja.html' %} 2 | 3 | {% block title %}{{ page.title }}{% endblock %} 4 | -------------------------------------------------------------------------------- /tests/jinja/patterns_jinja/components/test_extends/extended.yaml: -------------------------------------------------------------------------------- 1 | context: 2 | parent_template_name: patterns_jinja/components/test_extends/base.html 3 | -------------------------------------------------------------------------------- /tox_install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | poetry install 5 | 6 | if [ ! -z "$@" ] 7 | then 8 | poetry run python -m pip install $@ 9 | fi 10 | -------------------------------------------------------------------------------- /docs/images/getting-started/PatternLibraryEmpty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torchbox/django-pattern-library/HEAD/docs/images/getting-started/PatternLibraryEmpty.png -------------------------------------------------------------------------------- /tests/templates/patterns/atoms/test_atom_yml/test_atom_yml.yml: -------------------------------------------------------------------------------- 1 | context: 2 | atom_var: 'atom_var value from test_atom.yml (notice the missing "a" in the extension)' 3 | -------------------------------------------------------------------------------- /tests/jinja/patterns_jinja/components/test_extends/extended.html: -------------------------------------------------------------------------------- 1 | {% extends parent_template_name %} 2 | 3 | {% block content %}{{ super() }} - extended content{% endblock %} 4 | -------------------------------------------------------------------------------- /tests/jinja/patterns_jinja/pages/search/search.yaml: -------------------------------------------------------------------------------- 1 | context: 2 | search_query: test query 3 | search_results: 4 | - title: First result 5 | - title: Second result 6 | -------------------------------------------------------------------------------- /tests/templates/patterns/atoms/test_extends/extended.html: -------------------------------------------------------------------------------- 1 | {% extends parent_template_name %} 2 | 3 | {% block content %}{{ block.super }} - extended content{% endblock %} 4 | -------------------------------------------------------------------------------- /tests/templates/patterns/pages/search/search.yaml: -------------------------------------------------------------------------------- 1 | context: 2 | search_query: test query 3 | search_results: 4 | - title: First result 5 | - title: Second result 6 | -------------------------------------------------------------------------------- /docs/images/getting-started/getting-started-complete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torchbox/django-pattern-library/HEAD/docs/images/getting-started/getting-started-complete.png -------------------------------------------------------------------------------- /tests/templates/patterns/molecules/button/button.yaml: -------------------------------------------------------------------------------- 1 | context: 2 | target_page: 3 | title: Get started 4 | tags: 5 | pageurl: 6 | target_page: 7 | raw: /get-started 8 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | To report vulnerabilities, please refer to the project’s [security policy](https://torchbox.github.io/django-pattern-library/community/security-policy/) on our website. 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # We use Poetry to build this package, 2 | # this setup.py is only for GitHub’s "Used by" detection. 3 | from setuptools import setup 4 | 5 | setup(name="django-pattern-library") 6 | -------------------------------------------------------------------------------- /tests/jinja/patterns_jinja/components/button/button.yaml: -------------------------------------------------------------------------------- 1 | context: 2 | target_page: 3 | title: Get started 4 | tags: 5 | pageurl: 6 | target_page: 7 | raw: /get-started 8 | -------------------------------------------------------------------------------- /pattern_library/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PatternLibraryAppConfig(AppConfig): 5 | name = "pattern_library" 6 | default_auto_field = "django.db.models.AutoField" 7 | -------------------------------------------------------------------------------- /tests/templates/patterns/atoms/test_includes/test_includes.html: -------------------------------------------------------------------------------- 1 | {% load test_tags %} 2 | 3 | {% include 'non-patterns/include.html' %} 4 | {% include variable_include %} 5 | 6 | {% error_tag include %} 7 | -------------------------------------------------------------------------------- /tests/templates/patterns/molecules/test_molecule/test_molecule.yaml: -------------------------------------------------------------------------------- 1 | name: Pretty name for test molecule 2 | 3 | context: 4 | atom_var: 'atom_var value from test_molecule.yaml' 5 | utf8_test_var: '你好,世界' 6 | -------------------------------------------------------------------------------- /pattern_library/static/pattern_library/src/scss/layout/_main.scss: -------------------------------------------------------------------------------- 1 | @use '../config' as *; 2 | 3 | .main { 4 | display: block; 5 | padding: 20px; 6 | background: $white; 7 | overflow: scroll; 8 | } 9 | -------------------------------------------------------------------------------- /tests/jinja/patterns_jinja/components/accordion/accordion.md: -------------------------------------------------------------------------------- 1 | # Accordion (Jinja) 2 | 3 | The accordion is a good example of a pattern library component. This one uses [Jinja](https://jinja.palletsprojects.com/). 4 | -------------------------------------------------------------------------------- /tests/jinja/patterns_jinja/components/test_molecule/test_molecule.yaml: -------------------------------------------------------------------------------- 1 | name: Pretty name for test molecule 2 | 3 | context: 4 | atom_var: 'atom_var value from test_molecule.yaml' 5 | utf8_test_var: '你好,世界' 6 | -------------------------------------------------------------------------------- /.babelrc.js: -------------------------------------------------------------------------------- 1 | export default { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | modules: false, 7 | }, 8 | ], 9 | ], 10 | }; 11 | -------------------------------------------------------------------------------- /stylelint.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('stylelint').Config} */ 2 | export default { 3 | // See https://github.com/torchbox/stylelint-config-torchbox for rules. 4 | extends: 'stylelint-config-torchbox', 5 | }; 6 | -------------------------------------------------------------------------------- /tests/tests/utils.py: -------------------------------------------------------------------------------- 1 | from django import VERSION as DJANGO_VERSION 2 | 3 | if DJANGO_VERSION < (2,): 4 | from django.core.urlresolvers import reverse # noqa 5 | else: 6 | from django.urls import reverse # noqa 7 | -------------------------------------------------------------------------------- /pattern_library/static/pattern_library/src/scss/components/_code.scss: -------------------------------------------------------------------------------- 1 | .code { 2 | width: 600px; 3 | overflow: scroll; 4 | 5 | @media only screen and (min-width: 1200px) { 6 | width: 80vw; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/templates/patterns/atoms/icons/icon.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /tests/jinja/patterns_jinja/components/icons/icon.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /tests/jinja/patterns_jinja/components/test_includes/test_includes.html: -------------------------------------------------------------------------------- 1 | {% include 'non-patterns/include.html' %} 2 | {% include variable_include %} 3 | 4 | {# TODO: Unsupported override with Jinja #} 5 | {# {{ error_tag() }} #} 6 | -------------------------------------------------------------------------------- /tests/settings/production.py: -------------------------------------------------------------------------------- 1 | from .base import * # noqa 2 | 3 | # Serve the site as if it was hosted at the `/django-pattern-library/` sub-path 4 | STATIC_URL = "/django-pattern-library/demo/static/" 5 | GITHUB_PAGES_EXPORT = True 6 | -------------------------------------------------------------------------------- /tests/templates/patterns/atoms/test_includes/test_includes.yaml: -------------------------------------------------------------------------------- 1 | context: 2 | variable_include: non-patterns/variable_include.html 3 | 4 | tags: 5 | error_tag: 6 | include: 7 | template_name: 'non-patterns/include.html' 8 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | Please refer to the code of conduct on our website: [Contributor Covenant Code of Conduct](https://torchbox.github.io/django-pattern-library/community/code-of-conduct/). 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: '❓ Question' 4 | url: https://github.com/torchbox/django-pattern-library/discussions 5 | about: Use GitHub Discussions to get help with this project. 6 | -------------------------------------------------------------------------------- /pattern_library/exceptions.py: -------------------------------------------------------------------------------- 1 | class PatternLibraryException(Exception): 2 | pass 3 | 4 | 5 | class TemplateIsNotPattern(PatternLibraryException): 6 | pass 7 | 8 | 9 | class PatternLibraryEmpty(PatternLibraryException): 10 | pass 11 | -------------------------------------------------------------------------------- /tests/jinja/patterns_jinja/components/button/button.html: -------------------------------------------------------------------------------- 1 | 2 | {% if label %}{{ label }}{% else %}{{ target_page.title }}{% endif %} 3 | 4 | -------------------------------------------------------------------------------- /tests/templates/patterns/molecules/test_molecule/test_molecule.html: -------------------------------------------------------------------------------- 1 | {% include 'patterns/atoms/test_atom/test_atom.html' %} 2 | {% include 'patterns/atoms/test_atom/test_atom.html' with atom_var='atom_var value from test_molecule.html include tag' %} 3 | -------------------------------------------------------------------------------- /.prettierrc.toml: -------------------------------------------------------------------------------- 1 | # See https://prettier.io/docs/options.html. 2 | # Prettier also reads .editorconfig. 3 | printWidth = 80 4 | singleQuote = true 5 | quoteProps = 'consistent' 6 | trailingComma = 'all' 7 | arrowParens = 'always' 8 | proseWrap = 'preserve' 9 | -------------------------------------------------------------------------------- /tests/templates/patterns/atoms/test_atom/test_atom.md: -------------------------------------------------------------------------------- 1 | # Documentation for test atom - heading 1 2 | 3 | **bold text** 4 | 5 | _italic text_ 6 | 7 | ## Here's a heading 2 8 | 9 | ### Here's a heading 3 10 | 11 | - here's a list item 12 | - and another 13 | -------------------------------------------------------------------------------- /tests/jinja/patterns_jinja/components/test_atom/test_atom.md: -------------------------------------------------------------------------------- 1 | # Documentation for test atom - heading 1 2 | 3 | **bold text** 4 | 5 | _italic text_ 6 | 7 | ## Here's a heading 2 8 | 9 | ### Here's a heading 3 10 | 11 | - here's a list item 12 | - and another 13 | -------------------------------------------------------------------------------- /tests/templates/patterns/atoms/tags_test_atom/invalid_tags_test_atom.html.fail: -------------------------------------------------------------------------------- 1 | {% load test_tags_invalid %} 2 | 3 | MARMA{% default_html_tag_invalid empty_string %}LADE01 4 | MARMA{% default_html_tag_invalid none %}LADE02 5 | MARMA{% default_html_tag_invalid dict %}LADE03 6 | -------------------------------------------------------------------------------- /tests/templates/patterns/molecules/button/button.html: -------------------------------------------------------------------------------- 1 | {% load test_tags %} 2 | 3 | {% if label %}{{ label }}{% else %}{{ target_page.title }}{% endif %} 4 | 5 | -------------------------------------------------------------------------------- /tests/jinja/patterns_jinja/components/test_includes/test_includes.yaml: -------------------------------------------------------------------------------- 1 | context: 2 | variable_include: non-patterns/variable_include.html 3 | 4 | # TODO: Unsupported override with Jinja 5 | tags: 6 | error_tag: 7 | include: 8 | template_name: 'non-patterns/include.html' 9 | -------------------------------------------------------------------------------- /tests/jinja/patterns_jinja/components/test_molecule/test_molecule.html: -------------------------------------------------------------------------------- 1 | {% include 'patterns_jinja/components/test_atom/test_atom.html' %} 2 | {% set atom_var='atom_var value from test_molecule.html include tag' %} 3 | {% include 'patterns_jinja/components/test_atom/test_atom.html' %} 4 | -------------------------------------------------------------------------------- /docs/recipes/inclusion-tags.md: -------------------------------------------------------------------------------- 1 | ## Inclusion tags 2 | 3 | ```jinja2 4 | 7 | ``` 8 | 9 | ```yaml 10 | tags: 11 | footernav: 12 | '': 13 | template_name: 'patterns/molecules/navigation/footernav.html' 14 | ``` 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | 13 | [*.{yaml,yml,json,json5,md,toml}] 14 | indent_size = 2 15 | -------------------------------------------------------------------------------- /pattern_library/static/pattern_library/src/js/components/hide-menu-mobile.js: -------------------------------------------------------------------------------- 1 | export default function () { 2 | // hide the sidebar if coming from a mobile device 3 | if (window.matchMedia('(max-width: 600px)').matches) { 4 | document.querySelector('body').classList.add('nav-closed'); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tests/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | 4 | class ExampleForm(forms.Form): 5 | single_line_text = forms.CharField( 6 | max_length=255, help_text="This is some help text" 7 | ) 8 | choices = (("one", "One"), ("two", "Two"), ("three", "Three"), ("four", "Four")) 9 | select = forms.ChoiceField(choices=choices) 10 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # git-blame ignored revisions 2 | # To configure, run 3 | # git config blame.ignoreRevsFile .git-blame-ignore-revs 4 | # Requires Git > 2.23 5 | # See https://git-scm.com/docs/git-blame#Documentation/git-blame.txt---ignore-revs-fileltfilegt 6 | 7 | # Initial Prettier reformatting (#TBC) 8 | 849aaa98b7900ec5e93cfbed56379ddeb44c723d 9 | -------------------------------------------------------------------------------- /tests/templates/patterns/pages/test_page/test_page.html: -------------------------------------------------------------------------------- 1 | {% extends 'patterns/base_page.html' %} 2 | 3 | {% block content %} 4 |

{{ page.title }}

5 | {{ page.body }} 6 | 7 | {% include "patterns/molecules/accordion/accordion.html" %} 8 | 9 | {% if is_pattern_library %} 10 |

is_pattern_library = {{ is_pattern_library }}

11 | {% endif %} 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /tests/jinja/patterns_jinja/pages/test_page/test_page.html: -------------------------------------------------------------------------------- 1 | {% extends 'patterns_jinja/base_page_jinja.html' %} 2 | 3 | {% block content %} 4 |

{{ page.title }}

5 | {{ page.body }} 6 | 7 | {% include "patterns_jinja/components/accordion/accordion.html" %} 8 | 9 | {% if is_pattern_library %} 10 |

is_pattern_library = {{ is_pattern_library }}

11 | {% endif %} 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /pattern_library/static/pattern_library/src/js/components/syntax-highlighting.js: -------------------------------------------------------------------------------- 1 | import hljs from 'highlight.js/lib/core'; 2 | import django from 'highlight.js/lib/languages/django'; 3 | import yaml from 'highlight.js/lib/languages/yaml'; 4 | 5 | export default function () { 6 | hljs.registerLanguage('django', django); 7 | hljs.registerLanguage('yaml', yaml); 8 | hljs.highlightAll(); 9 | } 10 | -------------------------------------------------------------------------------- /tests/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.urls import include, path 3 | 4 | from pattern_library import urls as pattern_library_urls 5 | 6 | if settings.GITHUB_PAGES_EXPORT: 7 | urlpatterns = [ 8 | path("django-pattern-library/demo/", include(pattern_library_urls)), 9 | ] 10 | else: 11 | urlpatterns = [ 12 | path("", include(pattern_library_urls)), 13 | ] 14 | -------------------------------------------------------------------------------- /pattern_library/static/pattern_library/src/scss/layout/_body.scss: -------------------------------------------------------------------------------- 1 | @use '../config' as *; 2 | 3 | .body { 4 | display: grid; 5 | grid-template-columns: $sidebar-width 1fr; 6 | 7 | &.nav-closed { 8 | grid-template-columns: 0 1fr; 9 | } 10 | 11 | @media only screen and (min-width: 600px) { 12 | transition: grid-template-columns 0.25s ease; // works in firefox 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/templates/patterns/molecules/accordion/accordion.yaml: -------------------------------------------------------------------------------- 1 | context: 2 | id_prefix: test 3 | accordions: 4 | - title: Title A 5 | description: Description A. Ask for help. 6 | - title: Title B 7 | description: Description B. Ask for help. 8 | - title: Title C 9 | description: Description C. Ask for help. 10 | - title: Title D 11 | description: Description D. Ask for help. 12 | -------------------------------------------------------------------------------- /tests/jinja/patterns_jinja/components/accordion/accordion.yaml: -------------------------------------------------------------------------------- 1 | context: 2 | id_prefix: test 3 | accordions: 4 | - title: Title A (Jinja) 5 | description: Description A. Ask for help. 6 | - title: Title B 7 | description: Description B. Ask for help. 8 | - title: Title C 9 | description: Description C. Ask for help. 10 | - title: Title D 11 | description: Description D. Ask for help. 12 | -------------------------------------------------------------------------------- /tests/templates/patterns/atoms/tags_test_atom/tags_test_atom.html: -------------------------------------------------------------------------------- 1 | {% load test_tags %} 2 | 3 | SAND{% error_tag empty_string %}WICH 4 | SAND{% error_tag none %}WICH 5 | SAND{% error_tag zero %}WICH 6 | 7 | POTA{% default_html_tag page.url %}TO 8 | POTA{% default_html_tag page.child.url %}TO 9 | POTA{% default_html_tag None %}TO 10 | 11 | POTA{% default_html_tag_falsey empty_string %}TO1 12 | POTA{% default_html_tag_falsey none %}TO2 13 | POTA{% default_html_tag_falsey zero %}TO3 -------------------------------------------------------------------------------- /tests/templates/patterns/atoms/tags_test_atom/tags_test_atom.yaml: -------------------------------------------------------------------------------- 1 | tags: 2 | error_tag: 3 | empty_string: 4 | raw: '' 5 | none: 6 | raw: None 7 | zero: 8 | raw: 0 9 | default_html_tag: 10 | page.url: 11 | raw: 'example' 12 | page.child.url: 13 | raw: 'another_example' 14 | # None handled by default html value. 15 | default_html_tag_falsey: 16 | empty_string: 17 | raw: '' 18 | none: 19 | raw: None 20 | zero: 21 | raw: 0 22 | -------------------------------------------------------------------------------- /tests/templatetags/test_tags_invalid.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | from pattern_library.monkey_utils import override_tag 4 | 5 | register = template.Library() 6 | 7 | 8 | @register.simple_tag() 9 | def default_html_tag_invalid(arg=None): 10 | "Just raise an exception, never do anything" 11 | raise Exception("default_tag raised an exception") 12 | 13 | 14 | # Test overriding tag with a default_html that's not valid in Django >= 4.0 15 | override_tag(register, "default_html_tag_invalid", default_html=[1, 2, 3]) 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10 2 | 3 | WORKDIR /app 4 | 5 | RUN useradd --create-home dpl && \ 6 | mkdir -p /venv/ && \ 7 | chown -R dpl:dpl /venv/ /app/ 8 | 9 | ENV PATH=/venv/bin:/home/dpl/.local/bin:$PATH \ 10 | PYTHONPATH=/app/ \ 11 | VIRTUAL_ENV=/venv/ \ 12 | DJANGO_SETTINGS_MODULE=tests.settings.dev 13 | 14 | USER dpl 15 | 16 | RUN pip install --user "poetry>=2.1.2,<3" && \ 17 | python -m venv /venv/ 18 | 19 | COPY pyproject.toml ./ 20 | RUN poetry install --no-root 21 | 22 | CMD django-admin runserver 0.0.0.0:8000 23 | -------------------------------------------------------------------------------- /pattern_library/static/pattern_library/src/scss/components/_icon.scss: -------------------------------------------------------------------------------- 1 | @use '../config' as *; 2 | 3 | .icon { 4 | &--close-menu { 5 | width: 14px; 6 | height: 14px; 7 | fill: $white; 8 | transition: transform 0.25s ease; 9 | 10 | .nav-closed & { 11 | transform: rotate(-45deg); 12 | } 13 | } 14 | 15 | &--external { 16 | width: 18px; 17 | height: 20px; 18 | fill: $link-color; 19 | vertical-align: -4px; 20 | margin-left: 3px; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/jinja/patterns_jinja/base_jinja.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}Fragment{% endblock %} 6 | 7 | 8 | 9 | 10 | 11 |
12 | {% block content %}{% endblock %} 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /pattern_library/static/pattern_library/src/scss/_config.scss: -------------------------------------------------------------------------------- 1 | // Overwrite the title, main colour and font family here 2 | $site-title: 'Django Pattern Library'; 3 | $color-primary: #34b2b2; 4 | $family-primary: 5 | -apple-system, blinkmacsystemfont, 'Segoe UI', roboto, oxygen-sans, ubuntu, 6 | cantarell, 'Helvetica Neue', sans-serif; 7 | 8 | // $color--primary + $family--primary are in _config.scss 9 | $white: #fff; 10 | $off-white: #f6f6f6; 11 | $light-grey: #c4c4c4; 12 | $mid-grey: #606060; 13 | $off-black: #333; 14 | $black: #000; 15 | $link-color: #0001ee; 16 | $sidebar-width: 230px; 17 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.2' 2 | 3 | services: 4 | web: 5 | build: . 6 | environment: 7 | PYTHONDONTWRITEBYTECODE: 1 8 | ports: 9 | - '8000:8000' 10 | volumes: 11 | - type: bind 12 | source: . 13 | target: /app 14 | consistency: delegated 15 | command: tail -f /dev/null 16 | 17 | frontend: 18 | command: npm start 19 | image: node:24 20 | user: node 21 | volumes: 22 | - type: bind 23 | source: . 24 | target: /home/node/app 25 | consistency: delegated 26 | working_dir: /home/node/app 27 | -------------------------------------------------------------------------------- /pattern_library/static/pattern_library/src/scss/components/_wrapper.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | display: flex; 3 | height: 100%; 4 | 5 | &--pattern-header { 6 | display: block; 7 | align-items: center; 8 | justify-content: space-between; 9 | 10 | @media only screen and (min-width: 1010px) { 11 | display: flex; 12 | overflow: hidden; 13 | } 14 | } 15 | 16 | &--resize-buttons { 17 | margin-top: 20px; 18 | 19 | @media only screen and (min-width: 1010px) { 20 | margin-top: 0; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /pattern_library/static/pattern_library/src/scss/main.scss: -------------------------------------------------------------------------------- 1 | @charset 'UTF-8'; 2 | 3 | // Config 4 | @use 'config'; 5 | 6 | // Abstracts 7 | @use 'abstracts/base'; 8 | 9 | // Components 10 | @use 'components/code'; 11 | @use 'components/button'; 12 | @use 'components/heading'; 13 | @use 'components/iframe'; 14 | @use 'components/icon'; 15 | @use 'components/list'; 16 | @use 'components/md'; 17 | @use 'components/tabbed-listing'; 18 | @use 'components/wrapper'; 19 | 20 | // Layout 21 | @use 'layout/body'; 22 | @use 'layout/header'; 23 | @use 'layout/main'; 24 | @use 'layout/sidebar'; 25 | 26 | // hljs theme 27 | @use 'hljs/a11y-dark'; 28 | -------------------------------------------------------------------------------- /pattern_library/static/pattern_library/src/scss/components/_iframe.scss: -------------------------------------------------------------------------------- 1 | @use '../config' as *; 2 | 3 | .iframe { 4 | box-sizing: content-box; 5 | width: 100%; 6 | border: 1px solid $light-grey; 7 | margin: 20px 0; 8 | resize: both; 9 | overflow: auto; 10 | height: 415px; 11 | 12 | &.is-animatable { 13 | transition: width 0.25s ease; 14 | } 15 | 16 | .iframe-open & { 17 | position: absolute; 18 | top: 0; 19 | left: 0; 20 | height: 100%; 21 | background: $white; 22 | margin: 0; 23 | border: 0; 24 | resize: none; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/templates/patterns/pages/test_page/test_page.yaml: -------------------------------------------------------------------------------- 1 | context: 2 | page: 3 | body: > 4 |

5 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod 6 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 7 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 8 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 9 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat 10 | non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 11 |

12 | -------------------------------------------------------------------------------- /tests/jinja/patterns_jinja/pages/test_page/test_page.yaml: -------------------------------------------------------------------------------- 1 | context: 2 | page: 3 | title: Jinja test page 4 | body: > 5 |

6 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod 7 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 8 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 9 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 10 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat 11 | non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 12 |

13 | -------------------------------------------------------------------------------- /docs/recipes/image-include.md: -------------------------------------------------------------------------------- 1 | ## Image include 2 | 3 | To create test cases for images, we recommend using an image hotlinking service like [Unsplash](https://unsplash.com/) or [placeholder.com](https://placeholder.com/). 4 | 5 | ```jinja2 6 | {{ imageLarge.alt }} 7 | ``` 8 | 9 | YAML: 10 | 11 | ```yaml 12 | context: 13 | width: '720' 14 | height: '400' 15 | imageSmall: 16 | url: https://source.unsplash.com/pZ-XFIrJMtE/360x200 17 | imageLarge: 18 | url: https://source.unsplash.com/pZ-XFIrJMtE/720x400 19 | ``` 20 | -------------------------------------------------------------------------------- /tests/templates/patterns/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | {% block title %}Fragment{% endblock %} 7 | 8 | 9 | 10 | 11 | 12 | {% include "patterns/atoms/sprites/sprites.html" %} 13 | 14 |
15 | {% block content %}{{ pattern_library_rendered_pattern }}{% endblock %} 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /pattern_library/static/pattern_library/src/scss/components/_md.scss: -------------------------------------------------------------------------------- 1 | @use '../config' as *; 2 | 3 | .md { 4 | padding: 20px; 5 | background-color: $off-white; 6 | 7 | h1, 8 | h2, 9 | h3, 10 | h4, 11 | h5, 12 | h6 { 13 | margin-bottom: 0.67em; 14 | font-weight: 700; 15 | } 16 | 17 | h1 { 18 | font-size: 30px; 19 | } 20 | 21 | h2 { 22 | font-size: 25px; 23 | } 24 | 25 | h3 { 26 | font-size: 20px; 27 | } 28 | 29 | h4, 30 | h5, 31 | h6 { 32 | font-size: 16px; 33 | } 34 | 35 | p { 36 | margin-top: 1em; 37 | margin-bottom: 1em; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Please include a summary of the changes and which issue this relates to (if applicable). 4 | 5 | Fixes # (issue) 6 | 7 | ## Checklist 8 | 9 | - [ ] My code follows the style guidelines of this project 10 | - [ ] I have performed a self-review of my own code 11 | - [ ] I have commented my code, particularly in hard-to-understand areas 12 | - [ ] I have made corresponding changes to the documentation 13 | - [ ] My changes generate no new warnings 14 | - [ ] I have added tests that prove my fix is effective or that my feature works 15 | - [ ] New and existing unit tests pass locally with my changes 16 | - [ ] I have added an appropriate CHANGELOG entry 17 | -------------------------------------------------------------------------------- /pattern_library/static/pattern_library/src/scss/layout/_header.scss: -------------------------------------------------------------------------------- 1 | @use '../config' as *; 2 | 3 | .header { 4 | background-color: $color-primary; 5 | display: flex; 6 | align-items: center; 7 | height: 45px; 8 | grid-column: 1 / span 2; 9 | 10 | &__title { 11 | color: $white; 12 | font-weight: 200; 13 | text-transform: uppercase; 14 | letter-spacing: 1px; 15 | font-size: 16px; 16 | margin-left: 15px; 17 | 18 | &::after { 19 | content: $site-title; 20 | } 21 | 22 | @media only screen and (min-width: 600px) { 23 | font-size: 22px; 24 | letter-spacing: 5px; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pattern_library/static/pattern_library/src/scss/layout/_sidebar.scss: -------------------------------------------------------------------------------- 1 | @use '../config' as *; 2 | 3 | .sidebar { 4 | background-color: $off-white; 5 | height: 100vh; 6 | margin-left: 0; 7 | overflow: auto; 8 | 9 | &__inner { 10 | padding: 20px; 11 | } 12 | 13 | &__search { 14 | width: 100%; 15 | padding: 10px; 16 | font-size: 15px; 17 | margin: 0 0 15px; 18 | } 19 | 20 | &__search-results { 21 | > a { 22 | margin-bottom: 7px; 23 | display: block; 24 | font-size: 13px; 25 | } 26 | } 27 | 28 | &__nav { 29 | &--inactive { 30 | display: none; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /docs/recipes/image-lazyload.md: -------------------------------------------------------------------------------- 1 | ## Image lazy load 2 | 3 | ```jinja2 4 | {% image slide.image fill-100x71 as imageSmall %} 5 | {% image slide.image fill-829x585 as imageLarge %} 6 | 7 | {% include "patterns/atoms/image/image--lazyload.html" with imageSmall=imageSmall width=829 height=585 imageLarge=imageLarge classList='slide__image' %} 8 | ``` 9 | 10 | ```yaml 11 | tags: 12 | image: 13 | slide.image fill-100x71 as imageSmall: 14 | target_var: imageSmall 15 | raw: 16 | url: 'https://source.unsplash.com/100x71?ocean' 17 | slide.image fill-829x585 as imageLarge: 18 | target_var: imageLarge 19 | raw: 20 | url: 'https://source.unsplash.com/829x585?ocean' 21 | width: '829' 22 | height: '585' 23 | ``` 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '🐞 Bug Report' 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: 'type:Bug' 6 | assignees: '' 7 | --- 8 | 9 | Found a bug? Please fill out the sections below. 👍 10 | 11 | ### Issue Summary 12 | 13 | A summary of the issue. 14 | 15 | ### Steps to Reproduce 16 | 17 | 1. This 18 | 2. Then that… 19 | 3. And so on 20 | 21 | Any other relevant information. For example, why do you consider this a bug and what did you expect to happen instead? 22 | 23 | ### Technical details 24 | 25 | - Python version: Run `python --version`. 26 | - Django version: Look in your requirements.txt, or run `pip show django | grep Version`. 27 | - Browser version: You can use https://www.whatsmybrowser.org/ to find this out. 28 | -------------------------------------------------------------------------------- /pattern_library/static/pattern_library/src/js/app.js: -------------------------------------------------------------------------------- 1 | import '../scss/main.scss'; 2 | import persistMenu from './components/persist-menu.js'; 3 | import patternSearch from './components/pattern-search.js'; 4 | import tabbedContent from './components/tabbed-content.js'; 5 | import syntaxHighlighting from './components/syntax-highlighting.js'; 6 | import hideMenuMobile from './components/hide-menu-mobile.js'; 7 | import { setIframeSize, resizeIframe } from './components/iframe.js'; 8 | import { toggleNav, toggleNavItems } from './components/navigation.js'; 9 | 10 | document.addEventListener('DOMContentLoaded', () => { 11 | syntaxHighlighting(); 12 | toggleNavItems(); 13 | resizeIframe(); 14 | setIframeSize(); 15 | toggleNav(); 16 | tabbedContent(); 17 | persistMenu(); 18 | patternSearch(); 19 | hideMenuMobile(); 20 | }); 21 | -------------------------------------------------------------------------------- /pattern_library/static/pattern_library/src/js/components/navigation.js: -------------------------------------------------------------------------------- 1 | export function toggleNavItems() { 2 | const categoryButtons = document.querySelectorAll('.js-toggle-pattern'); 3 | 4 | categoryButtons.forEach((button) => { 5 | button.addEventListener('click', (e) => { 6 | e.target.classList.toggle('is-open'); 7 | for (const element of e.target.closest('.js-list-item') 8 | .childNodes) { 9 | if (element.nodeName === 'UL') { 10 | element.classList.toggle('is-open'); 11 | } 12 | } 13 | }); 14 | }); 15 | } 16 | 17 | export function toggleNav() { 18 | document.querySelector('.js-close-menu').addEventListener('click', (e) => { 19 | document.querySelector('body').classList.toggle('nav-closed'); 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /pattern_library/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, re_path 2 | 3 | from pattern_library import get_pattern_template_suffix, views 4 | 5 | app_name = "pattern_library" 6 | urlpatterns = [ 7 | # UI 8 | re_path(r"^$", views.IndexView.as_view(), name="index"), 9 | re_path( 10 | r"^pattern/(?P[\w./\-\\]+%s)$" 11 | % (get_pattern_template_suffix()), 12 | views.IndexView.as_view(), 13 | name="display_pattern", 14 | ), 15 | # iframe rendering 16 | re_path( 17 | r"^render-pattern/(?P[\w./\-\\]+%s)$" 18 | % (get_pattern_template_suffix()), 19 | views.RenderPatternView.as_view(), 20 | name="render_pattern", 21 | ), 22 | # API rendering 23 | path("api/v1/render-pattern", views.render_pattern_api, name="render_pattern_api"), 24 | ] 25 | -------------------------------------------------------------------------------- /docs/community/security-policy.md: -------------------------------------------------------------------------------- 1 | # Security policy 2 | 3 | We take security issues seriously. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions. 4 | 5 | ## Supported versions 6 | 7 | This project doesn’t have formal support targets for non-latest versions. Backporting security fixes to affected releases will be decided on a case-by-case basis, based on effort involved and known usage of affected versions. Please refer to our [compatibility documentation](https://torchbox.github.io/django-pattern-library/getting-started/#compatibility) when reporting issues specific to certain versions of Django or Python. 8 | 9 | ### Reporting a vulnerability 10 | 11 | To report a vulnerability, please contact any one of the [maintainers listed on PyPI](https://pypi.org/project/django-pattern-library/). 12 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.209.6/containers/docker-existing-docker-compose 3 | { 4 | "name": "django-pattern-library devcontainer", 5 | "dockerComposeFile": ["../docker-compose.yml"], 6 | "service": "web", 7 | "workspaceFolder": "/app", 8 | "settings": { 9 | "terminal.integrated.profiles.linux": { 10 | "bash": { 11 | "path": "bash", 12 | "icon": "terminal-bash" 13 | } 14 | }, 15 | "terminal.integrated.defaultProfile.linux": "bash" 16 | }, 17 | "extensions": [ 18 | "editorconfig.editorconfig", 19 | "github.vscode-pull-request-github", 20 | "ms-python.python", 21 | "ms-python.vscode-pylance", 22 | "syler.sass-indented" 23 | ], 24 | "remoteUser": "dpl" 25 | } 26 | -------------------------------------------------------------------------------- /tests/templates/patterns/molecules/accordion/accordion.html: -------------------------------------------------------------------------------- 1 |
2 | {% for accordion in accordions %} 3 |
4 |

5 | 9 |

10 |
11 |

{{ accordion.description }}

12 |
13 |
14 | {% endfor %} 15 |
16 | -------------------------------------------------------------------------------- /pattern_library/static/pattern_library/src/scss/components/_button.scss: -------------------------------------------------------------------------------- 1 | @use '../config' as *; 2 | 3 | .button { 4 | &--resize { 5 | border: 2px solid $light-grey; 6 | color: $mid-grey; 7 | padding: 5px 10px; 8 | margin: 0 5px; 9 | font-weight: bold; 10 | background-color: $off-white; 11 | 12 | &:hover { 13 | cursor: pointer; 14 | } 15 | 16 | &.is-active { 17 | border: 2px solid $mid-grey; 18 | } 19 | 20 | &:last-of-type { 21 | margin: 0 0 0 5px; 22 | } 23 | } 24 | 25 | &--close-menu { 26 | display: flex; 27 | background-color: $off-black; 28 | align-self: stretch; 29 | align-items: center; 30 | justify-content: center; 31 | width: 45px; 32 | } 33 | 34 | &__icon { 35 | width: 18px; 36 | height: 20px; 37 | fill: $light-grey; 38 | vertical-align: middle; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/templates/patterns/pages/people/person_page.yaml: -------------------------------------------------------------------------------- 1 | context: 2 | page: 3 | title: Dr John Doe 4 | first_name: John 5 | last_name: Doe 6 | photo: fake 7 | job_title: Stub person 8 | website: 'https://example.com' 9 | email: 'test@example.com' 10 | social_media_profile: 11 | all: 12 | - profile_url: 'https://example.com/' 13 | - profile_url: 'https://example.com/2' 14 | phone_numbers: 15 | all: 16 | - phone_number: 12345678 17 | - phone_number: 87654321 18 | introduction: Some intro text for this peroson 19 | person_types: 20 | all: 21 | - Staff 22 | - Director 23 | accordions: 24 | - title: Title A 25 | description: Description A. Ask for help. 26 | - title: Title B 27 | description: Description B. Ask for help. 28 | - title: Title C 29 | description: Description C. Ask for help. 30 | - title: Title D 31 | description: Description D. Ask for help. 32 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: 'CodeQL' 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths-ignore: 7 | - '**/*.md' 8 | - '**/*.yml' 9 | - '**/*.html' 10 | - '**/*.scss' 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [main] 14 | paths-ignore: 15 | - '**/*.md' 16 | - '**/*.yml' 17 | - '**/*.html' 18 | - '**/*.scss' 19 | schedule: 20 | - cron: '28 20 * * 5' 21 | 22 | jobs: 23 | analyze: 24 | name: Analyze 25 | runs-on: ubuntu-latest 26 | permissions: 27 | actions: read 28 | contents: read 29 | security-events: write 30 | strategy: 31 | fail-fast: false 32 | matrix: 33 | language: ['javascript', 'python'] 34 | 35 | steps: 36 | - uses: actions/checkout@v3 37 | - uses: github/codeql-action/init@v2 38 | with: 39 | languages: ${{ matrix.language }} 40 | - uses: github/codeql-action/analyze@v2 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '🚀 Feature request' 3 | about: Suggest an idea for improvement 4 | title: '' 5 | labels: 'type:Enhancement' 6 | assignees: '' 7 | --- 8 | 9 | ### Is your proposal related to a problem? 10 | 11 | 15 | 16 | (Write your answer here.) 17 | 18 | ### Describe the solution you'd like 19 | 20 | 23 | 24 | (Describe your proposed solution here.) 25 | 26 | ### Describe alternatives you've considered 27 | 28 | 31 | 32 | (Write your answer here.) 33 | 34 | ### Additional context 35 | 36 | 40 | 41 | (Write your answer here.) 42 | -------------------------------------------------------------------------------- /docs/community/support.md: -------------------------------------------------------------------------------- 1 | # Help and support 2 | 3 | If you need help with this project, here are things you should try. 4 | 5 | ## Documentation search 6 | 7 | We take good care to document common use cases of the library, with examples and code snippets. We also document use cases that are known to not be well supported, with links to issues. 8 | 9 | ## GitHub search 10 | 11 | GitHub’s [main search](https://github.com/torchbox/django-pattern-library/search?q=help) can find matches in existing issues, pull requests, and discussions. The [cs.github.com code search](https://cs.github.com/torchbox/django-pattern-library?q=help) is excellent to search within the repository. 12 | 13 | ## GitHub Discussions 14 | 15 | No luck with the existing documentation and other search results? Open a new discussion in [GitHub Discussions](https://github.com/torchbox/django-pattern-library/discussions) for conversations relating to the project. This can be used for support requests, or high-level feature ideas that don’t fit well with our issue template. 16 | -------------------------------------------------------------------------------- /tests/jinja/patterns_jinja/components/accordion/accordion.html: -------------------------------------------------------------------------------- 1 |
2 | {% for accordion in accordions %} 3 |
4 |

5 | 11 |

12 |
13 |

{{ accordion.description }}

14 |
15 |
16 | {% endfor %} 17 |
18 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py{310,311,312,313,314}-dj42 4 | py{310,311,312,313,314}-dj52 5 | py{312,313,314}-dj60 6 | py{314}-djmain 7 | lint 8 | skipsdist = true 9 | 10 | [testenv] 11 | allowlist_externals = 12 | poetry 13 | ./tox_install.sh 14 | install_command = 15 | ./tox_install.sh {packages} 16 | commands = 17 | poetry run python -X dev -W error runtests.py 18 | poetry run django-admin render_patterns --settings=tests.settings.dev --pythonpath=. --dry-run 19 | deps = 20 | dj42: Django>=4.2,<5.0 21 | dj52: Django>=5.2,<6.0 22 | dj60: Django>=6.0,<6.1 23 | djmain: https://github.com/django/django/archive/main.zip 24 | 25 | [testenv:lint] 26 | commands = 27 | poetry install -q 28 | poetry run flake8 29 | poetry run isort --check --diff . 30 | poetry run black --check --diff . 31 | 32 | [flake8] 33 | ignore = W503 34 | max-complexity = 13 35 | max-line-length = 120 36 | exclude = .tox,venv,migrations,node_modules 37 | 38 | [coverage:run] 39 | source = pattern_library 40 | -------------------------------------------------------------------------------- /pattern_library/static/pattern_library/src/scss/components/_heading.scss: -------------------------------------------------------------------------------- 1 | @use '../config' as *; 2 | 3 | .heading { 4 | &--iframe-size { 5 | color: $mid-grey; 6 | align-self: center; 7 | margin-right: 10px; 8 | } 9 | 10 | &--iframe-hint { 11 | position: fixed; 12 | top: 20%; 13 | left: 50%; 14 | transform: translate(-50%, -50%); 15 | background: rgb(0, 0, 0, 0.78); 16 | padding: 20px; 17 | border-radius: 10px; 18 | font-weight: bold; 19 | color: $white; 20 | opacity: 0; 21 | pointer-events: none; 22 | 23 | .iframe-open & { 24 | animation-delay: 0.2s; 25 | animation-name: fadeInOut; 26 | animation-iteration-count: 1; 27 | animation-timing-function: ease-in; 28 | animation-duration: 1.75s; 29 | } 30 | 31 | span { 32 | border: 1px solid $white; 33 | padding: 10px 5px; 34 | margin: 0 5px; 35 | border-radius: 5px; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /pattern_library/static/pattern_library/src/js/components/persist-menu.js: -------------------------------------------------------------------------------- 1 | export default function () { 2 | if (location.pathname.includes('/pattern/')) { 3 | // split url to match {{ template.origin.template_name }} 4 | const id = location.pathname.split('/pattern/')[1]; 5 | 6 | // find the matching pattern 7 | const currentPattern = document.getElementById(id); 8 | 9 | currentPattern.classList.add('is-active'); 10 | 11 | // grab the parent lists and headings 12 | const parentCategory = currentPattern.closest('ul'); 13 | const parentCategoryHeading = parentCategory.previousElementSibling; 14 | const grandParentCategory = parentCategoryHeading.closest('ul'); 15 | const grandParentCategoryHeading = 16 | grandParentCategory.previousElementSibling; 17 | 18 | parentCategory.classList.add('is-open'); 19 | parentCategoryHeading.classList.add('is-open'); 20 | grandParentCategory.classList.add('is-open'); 21 | grandParentCategoryHeading.classList.add('is-open'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/templates/patterns/pages/forms/example_form.html: -------------------------------------------------------------------------------- 1 | {% extends "patterns/base.html" %} 2 | 3 | {% block content %} 4 |
5 | {% csrf_token %} 6 |
7 | {% if form.errors %} 8 |
9 | There were some errors with your form. Please amend the fields highlighted below. 10 | 11 | {% if form.non_field_errors %} 12 |
    13 | {% for error in form.non_field_errors %} 14 |
  • {{ error }}
  • 15 | {% endfor %} 16 |
17 | {% endif %} 18 |
19 | {% endif %} 20 | 21 | {% for hidden_field in form.hidden_fields %} 22 | {{ hidden_field }} 23 | {% endfor %} 24 | 25 | {% for field in form.visible_fields %} 26 | {% include "patterns/molecules/field/field.html" with field=field %} 27 | {% endfor %} 28 | 29 | 30 |
31 |
32 | {% endblock %} 33 | -------------------------------------------------------------------------------- /docs/recipes/api-rendering.md: -------------------------------------------------------------------------------- 1 | # API rendering 2 | 3 | For additional flexibility, django-pattern-library supports rendering patterns via an API endpoint. 4 | This can be useful when implementing a custom UI while still using the pattern library’s Django rendering features. 5 | 6 | The API endpoint is available at `api/v1/render-pattern`. It accepts POST requests with a JSON payload containing the following fields: 7 | 8 | - `template_name` – the path of the template to render 9 | - `config` – the configuration for the template, with the same data structure as the configuration files (`context` and `tags`). 10 | 11 | Here is an example, with curl: 12 | 13 | ```bash 14 | echo '{"template_name": "patterns/molecules/button/button.html", "config": {"context": {"target_page": {"title": "API"}}, "tags": {"pageurl":{"target_page":{"raw": "/hello-api"}}}}}' | curl -d @- http://localhost:8000/api/v1/render-pattern 15 | ``` 16 | 17 | The response will be the pattern’s rendered HTML: 18 | 19 | ```html 20 | API 21 | ``` 22 | 23 | Note compared to iframe rendering, this API always renders the pattern’s HTML standalone, never within a base template. 24 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import argparse 3 | import os 4 | import sys 5 | 6 | import django 7 | from django.conf import settings 8 | from django.test.utils import get_runner 9 | 10 | import coverage 11 | 12 | parser = argparse.ArgumentParser() 13 | parser.add_argument( 14 | "-v", 15 | "--verbosity", 16 | action="store", 17 | dest="verbosity", 18 | default=1, 19 | type=int, 20 | choices=range(4), 21 | help="Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, 3=very verbose output", 22 | ) 23 | 24 | if __name__ == "__main__": 25 | # Coverage setup 26 | cov = coverage.Coverage() 27 | cov.start() 28 | 29 | # Django setup 30 | os.environ["DJANGO_SETTINGS_MODULE"] = "tests.settings.dev" 31 | django.setup() 32 | 33 | # Test runner setup 34 | TestRunner = get_runner(settings) 35 | TestRunner.add_arguments(parser) 36 | args = parser.parse_args() 37 | test_runner = TestRunner(**vars(args)) 38 | failures = test_runner.run_tests(["tests"]) 39 | 40 | # Generate coverage report 41 | cov.stop() 42 | cov.save() 43 | cov.html_report() 44 | 45 | sys.exit(bool(failures)) 46 | -------------------------------------------------------------------------------- /pattern_library/static/pattern_library/src/js/components/tabbed-content.js: -------------------------------------------------------------------------------- 1 | export default function () { 2 | let i; 3 | let tabItem = document.querySelectorAll('.tabbed-content__heading'); 4 | 5 | function tabs(tabClickEvent) { 6 | for (let i = 0; i < tabItem.length; i++) { 7 | tabItem[i].classList.remove('tabbed-content__heading--active'); 8 | } 9 | 10 | let clickedTab = tabClickEvent.currentTarget; 11 | 12 | clickedTab.classList.add('tabbed-content__heading--active'); 13 | tabClickEvent.preventDefault(); 14 | 15 | let contentPanes = document.querySelectorAll('.tabbed-content__item'); 16 | 17 | for (i = 0; i < contentPanes.length; i++) { 18 | contentPanes[i].classList.remove('tabbed-content__item--active'); 19 | } 20 | 21 | let anchorReference = tabClickEvent.target; 22 | let activePaneId = anchorReference.getAttribute('href'); 23 | let activePane = document.querySelector(activePaneId); 24 | 25 | activePane.classList.add('tabbed-content__item--active'); 26 | } 27 | 28 | for (i = 0; i < tabItem.length; i++) { 29 | tabItem[i].addEventListener('click', tabs); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pattern_library/cm_utils.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from importlib import import_module 3 | from typing import Callable 4 | 5 | from django.apps import apps 6 | from django.utils.module_loading import module_has_submodule 7 | 8 | 9 | def get_app_modules(): 10 | """ 11 | Generator function that yields a module object for each installed app 12 | yields tuples of (app_name, module) 13 | """ 14 | for app in apps.get_app_configs(): 15 | yield app.name, app.module 16 | 17 | 18 | def get_app_submodules(submodule_name): 19 | """ 20 | Searches each app module for the specified submodule 21 | yields tuples of (app_name, module) 22 | """ 23 | for name, module in get_app_modules(): 24 | if module_has_submodule(module, submodule_name): 25 | yield name, import_module("%s.%s" % (name, submodule_name)) 26 | 27 | 28 | def accepts_kwarg(func: Callable, kwarg: str) -> bool: 29 | """ 30 | Returns a boolean indicating whether the callable ``func`` has 31 | a signature that accepts the keyword argument ``kwarg``. 32 | """ 33 | signature = inspect.signature(func) 34 | try: 35 | signature.bind_partial(**{kwarg: None}) 36 | return True 37 | except TypeError: 38 | return False 39 | -------------------------------------------------------------------------------- /pattern_library/templates/pattern_library/pattern_group.html: -------------------------------------------------------------------------------- 1 | 25 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | # Commands to start on workspace startup 2 | 3 | ports: 4 | # when port 8000 becomes available open a browser preview 5 | # of the running django pattern library instance 6 | - port: 8000 7 | onOpen: open-preview 8 | 9 | tasks: 10 | # initialise and install the python part of the app by installing poetry 11 | # and the python dependencies 12 | - init: | 13 | pip install "poetry>=2.1.2,<3" 14 | poetry install 15 | gp sync-done python 16 | # Start the server for testing 17 | command: poetry run django-admin runserver --settings=tests.settings.dev --pythonpath=. 18 | name: Runserver 19 | 20 | # install the node part of the app by installing the required 21 | # node version and npm dependencies 22 | - init: | 23 | nvm install 24 | npm install 25 | # Build the assets 26 | command: npm run start 27 | name: Node 28 | openMode: split-right 29 | 30 | # when the python part of the app is ready... 31 | - init: | 32 | echo "Waiting for python..." 33 | gp sync-await python 34 | # run the tests 35 | command: | 36 | poetry run ./runtests.py 37 | poetry run django-admin render_patterns --settings=tests.settings.dev --pythonpath=. 38 | name: Tests 39 | openMode: split-right 40 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | const staticDir = path.resolve('pattern_library', 'static', 'pattern_library'); 4 | 5 | export default { 6 | entry: path.join(staticDir, 'src', 'js', 'app.js'), 7 | output: { 8 | path: path.join(staticDir, 'dist'), 9 | filename: 'bundle.js', 10 | publicPath: './dist', 11 | }, 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.js$/, 16 | use: { 17 | loader: 'babel-loader', 18 | }, 19 | }, 20 | { 21 | test: /\.scss$/, 22 | use: [ 23 | { 24 | loader: 'style-loader', // creates style nodes from JS strings 25 | }, 26 | { 27 | loader: 'css-loader', // translates CSS into CommonJS 28 | }, 29 | { 30 | loader: 'sass-loader', // compiles Sass to CSS 31 | options: { 32 | sassOptions: { 33 | outputStyle: 'compressed', 34 | }, 35 | }, 36 | }, 37 | ], 38 | }, 39 | ], 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /docs/recipes/looping-for-tags.md: -------------------------------------------------------------------------------- 1 | ## Looping over a template tag 2 | 3 | For a template such as: 4 | 5 | ```jinja2 6 | {% social_media_links as social_links %} 7 | 21 | ``` 22 | 23 | You can use the following syntax to mock the tag’s output: 24 | 25 | ```yaml 26 | tags: 27 | social_media_links: 28 | as social_links: 29 | raw: 30 | - url: '#' 31 | type: twitter 32 | label: Twitter 33 | - url: '#' 34 | type: facebook 35 | label: Facebook 36 | - url: '#' 37 | type: instagram 38 | label: Instagram 39 | - url: '#' 40 | type: youtube 41 | label: YouTube 42 | - url: '#' 43 | type: linkedin 44 | label: LinkedIn 45 | ``` 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "django-pattern-library", 3 | "version": "0.1.0", 4 | "private": true, 5 | "license": "BSD-3-Clause", 6 | "browserslist": [ 7 | "> 1% and last 2 versions", 8 | "Firefox ESR", 9 | "ios_saf 14", 10 | "safari 14", 11 | "not ie 11", 12 | "not ie_mob 11", 13 | "not android 4.4.3-4.4.4", 14 | "not OperaMini all" 15 | ], 16 | "type": "module", 17 | "scripts": { 18 | "start": "webpack --mode development --watch", 19 | "build": "webpack --mode production", 20 | "lint": "npm run lint:format && npm run lint:css", 21 | "lint:format": "prettier --check '**/?(.)*.{md,css,scss,js,json,yaml,yml}'", 22 | "lint:css": "stylelint --report-needless-disables --report-unscoped-disables 'pattern_library/static/pattern_library/src/scss/'", 23 | "format": "prettier --write '**/?(.)*.{md,css,scss,js,json,yaml,yml}'" 24 | }, 25 | "devDependencies": { 26 | "@babel/core": "^7.26.10", 27 | "@babel/preset-env": "^7.26.9", 28 | "babel-loader": "^10.0.0", 29 | "css-loader": "^7.1.2", 30 | "prettier": "^3.7.3", 31 | "sass": "^1.86.3", 32 | "sass-loader": "^16.0.5", 33 | "style-loader": "^4.0.0", 34 | "stylelint": "^16.26.1", 35 | "stylelint-config-torchbox": "^4.0.0", 36 | "webpack": "^5.99.2", 37 | "webpack-cli": "^6.0.1" 38 | }, 39 | "dependencies": { 40 | "highlight.js": "^11.11.1" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /pattern_library/static/pattern_library/src/scss/abstracts/_base.scss: -------------------------------------------------------------------------------- 1 | @use '../config' as *; 2 | 3 | *, 4 | *::before, 5 | *::after { 6 | box-sizing: border-box; 7 | } 8 | 9 | html, 10 | body { 11 | margin: 0; 12 | font-family: $family-primary; 13 | height: 100%; 14 | } 15 | 16 | p, 17 | h1, 18 | h2, 19 | h3, 20 | h4, 21 | h5, 22 | h6 { 23 | margin: 0; 24 | font-weight: 400; 25 | } 26 | 27 | a { 28 | overflow-wrap: break-word; 29 | color: $link-color; 30 | } 31 | 32 | .section-group-heading:not(:first-of-type) { 33 | margin-top: 100px; 34 | } 35 | 36 | .pattern-group-heading { 37 | font-weight: 500; 38 | margin: 30px 0 10px; 39 | } 40 | 41 | .border-group { 42 | box-sizing: border-box; 43 | padding: 20px; 44 | margin-bottom: 20px; 45 | border: 2px solid #eee; 46 | display: block; 47 | border-radius: 5px; 48 | transition: background-color 300ms ease; 49 | } 50 | 51 | .border-group:hover { 52 | background: #f1f1f1; 53 | } 54 | 55 | .sr-only { 56 | position: absolute; 57 | width: 1px; 58 | height: 1px; 59 | overflow: hidden; 60 | opacity: 0; 61 | clip-path: rect(1px, 1px, 1px, 1px); 62 | } 63 | 64 | @keyframes fadeInOut { 65 | 0% { 66 | opacity: 0; 67 | } 68 | 69 | 20% { 70 | opacity: 1; 71 | } 72 | 73 | 80% { 74 | opacity: 1; 75 | } 76 | 77 | 100% { 78 | opacity: 0; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /tests/jinja/patterns_jinja/pages/search/search.html: -------------------------------------------------------------------------------- 1 | {% extends "patterns_jinja/base_jinja.html" %} 2 | 3 | {% block content %} 4 |

{% if search_query %}Search results for “{{ search_query }}”{% else %}Search{% endif %}

5 | 6 | {% if search_results %} 7 | {% with count=paginator.count %} 8 | {{ count }} result{{ count|pluralize }} found. 9 | {% endwith %} 10 | 11 | {% for result in search_results %} 12 |

{{ result.title }}

13 | {% endfor %} 14 | 15 | {% if paginator.num_pages > 1 %} 16 | 29 | {% endif %} 30 | 31 | {% elif search_query %} 32 | No results found. 33 | {% endif %} 34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /tests/templates/patterns/pages/search/search.html: -------------------------------------------------------------------------------- 1 | {% extends "patterns/base.html" %} 2 | 3 | {% block content %} 4 |

{% if search_query %}Search results for “{{ search_query }}”{% else %}Search{% endif %}

5 | 6 | {% if search_results %} 7 | {% with count=search_results.paginator.count %} 8 | {{ count }} result{{ count|pluralize }} found. 9 | {% endwith %} 10 | 11 | {% for result in search_results %} 12 |

{{ result.title }}

13 | {% endfor %} 14 | 15 | {% if search_results.paginator.num_pages > 1 %} 16 | 29 | {% endif %} 30 | 31 | {% elif search_query %} 32 | No results found. 33 | {% endif %} 34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /tests/jinja2.py: -------------------------------------------------------------------------------- 1 | from django.apps import apps 2 | from django.contrib.staticfiles.storage import staticfiles_storage 3 | from django.template.context_processors import csrf 4 | from django.template.defaultfilters import ( 5 | cut, 6 | date, 7 | linebreaks, 8 | pluralize, 9 | slugify, 10 | truncatewords, 11 | urlencode, 12 | ) 13 | from django.urls import reverse 14 | 15 | from jinja2 import Environment 16 | 17 | from pattern_library.monkey_utils import override_jinja_tags 18 | 19 | if apps.is_installed("pattern_library"): 20 | override_jinja_tags() 21 | 22 | 23 | def error_tag(): 24 | "Just raise an exception, never do anything" 25 | raise Exception("error_tag raised an exception") 26 | 27 | 28 | def pageurl(page): 29 | """Approximation of wagtail built-in tag for realistic example.""" 30 | return "/page/url" 31 | 32 | 33 | def environment(**options): 34 | env = Environment(**options) 35 | env.globals.update( 36 | { 37 | "static": staticfiles_storage.url, 38 | "url": reverse, 39 | "csrf": csrf, 40 | "error_tag": error_tag, 41 | "pageurl": pageurl, 42 | } 43 | ) 44 | env.filters.update( 45 | { 46 | "cut": cut, 47 | "date": date, 48 | "linebreaks": linebreaks, 49 | "pluralize": pluralize, 50 | "slugify": slugify, 51 | "truncatewords": truncatewords, 52 | "urlencode": urlencode, 53 | } 54 | ) 55 | return env 56 | -------------------------------------------------------------------------------- /tests/templatetags/test_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.utils.text import camel_case_to_spaces, slugify 3 | 4 | from pattern_library.monkey_utils import override_tag 5 | 6 | register = template.Library() 7 | 8 | 9 | # Basic template tag 10 | @register.simple_tag 11 | def error_tag(arg=None): 12 | "Just raise an exception, never do anything" 13 | raise Exception("error_tag raised an exception") 14 | 15 | 16 | # Template tag to to test setting a default html value. 17 | @register.simple_tag() 18 | def default_html_tag(arg=None): 19 | "Just raise an exception, never do anything" 20 | raise Exception("default_tag raised an exception") 21 | 22 | 23 | # Template tag to to test setting a default html value that is falsey. 24 | @register.simple_tag() 25 | def default_html_tag_falsey(arg=None): 26 | "Just raise an exception, never do anything" 27 | raise Exception("default_tag raised an exception") 28 | 29 | 30 | @register.simple_tag() 31 | def pageurl(page): 32 | """Approximation of wagtail built-in tag for realistic example.""" 33 | return "/page/url" 34 | 35 | 36 | # Get widget type of a field 37 | @register.filter(name="widget_type") 38 | def widget_type(bound_field): 39 | return slugify(camel_case_to_spaces(bound_field.field.widget.__class__.__name__)) 40 | 41 | 42 | override_tag(register, "error_tag") 43 | override_tag(register, "default_html_tag", default_html="https://potato.com") 44 | override_tag(register, "default_html_tag_falsey", default_html=None) 45 | override_tag(register, "pageurl") 46 | -------------------------------------------------------------------------------- /docs/guides/reuse-across-projects.md: -------------------------------------------------------------------------------- 1 | # Reuse across projects 2 | 3 | django-pattern-library is designed to be useful for component reuse within a single project, but it can also be set up to create a component library reusable between multiple projects. Reusing pattern library components is a matter of packaging and publishing a Django app (that happens to contain a lot of templates, CSS, and JS). 4 | 5 | Here are the rough steps: 6 | 7 | - **Decide where to store the shared pattern library**. Whether it has its own repository, whether it’s published on PyPI, or in another way. 8 | - **Choose a versioning and release methodology**. With multiple projects reusing the code, it’s important for them to be able to pin specific versions, and have a clear sense of how to do updates. 9 | - **Provide a pattern library development environment**. Developers will need a way to iterate on pattern library components in isolation from the projects the UI components are reused in. 10 | 11 | ## Static files 12 | 13 | As part of your pattern library’s build process, make sure that the static files (CSS, JS, etc.) of each component can be reused individually of each-other. Different projects likely will reuse different components, and you don’t want to be paying the performance cost of loading components you don’t need. 14 | 15 | ## Useful resources 16 | 17 | - Django’s official [How to write reusable apps](https://docs.djangoproject.com/en/3.1/intro/reusable-apps/) 18 | - InVision’s [Guide to Design Systems](https://www.invisionapp.com/inside-design/guide-to-design-systems/) 19 | -------------------------------------------------------------------------------- /pattern_library/static/pattern_library/src/scss/components/_tabbed-listing.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:color'; 2 | @use '../config' as *; 3 | 4 | .tabbed-content { 5 | &__list { 6 | list-style: none; 7 | margin: 0; 8 | padding: 0; 9 | display: flex; 10 | justify-content: space-around; 11 | } 12 | 13 | &__heading { 14 | flex-grow: 1; 15 | text-align: center; 16 | 17 | > a { 18 | color: color.adjust($black, $lightness: 30%); 19 | padding: 12px 0; 20 | display: block; 21 | width: 100%; 22 | background-color: color.adjust($color-primary, $lightness: 30%); 23 | text-decoration: none; 24 | transition: 25 | color 0.25s ease, 26 | background-color 0.25s ease; 27 | 28 | &:hover { 29 | color: $black; 30 | background-color: $color-primary; 31 | } 32 | } 33 | 34 | &--active { 35 | > a { 36 | color: $black; 37 | background-color: $color-primary; 38 | } 39 | } 40 | 41 | &:not(:last-child) { 42 | border-right: 1px solid 43 | color.adjust($color-primary, $lightness: 20%); 44 | } 45 | } 46 | 47 | &__item { 48 | display: none; 49 | 50 | &--active { 51 | display: block; 52 | } 53 | } 54 | 55 | pre { 56 | margin-top: 0; 57 | } 58 | 59 | .hljs { 60 | width: 100%; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Torchbox Ltd and individual contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of Torchbox nor the names of its contributors may be used 15 | to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /tests/templates/patterns/atoms/sprites/sprites.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {# Show available icons, in pattern library view only #} 21 | {% if is_pattern_library %} 22 | 25 | 32 | {% endif %} 33 | -------------------------------------------------------------------------------- /tests/jinja/patterns_jinja/components/sprites/sprites.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {# Show available icons, in pattern library view only #} 21 | {% if is_pattern_library %} 22 | 26 | 33 | {% endif %} 34 | -------------------------------------------------------------------------------- /tests/static/main.js: -------------------------------------------------------------------------------- 1 | const initAccordions = () => { 2 | const accordions = [...document.querySelectorAll('[data-accordion]')]; 3 | accordions.forEach((node) => { 4 | const panels = [ 5 | ...document.querySelectorAll('[data-accordion-panel]', node), 6 | ]; 7 | 8 | panels.forEach((panel, i) => { 9 | const isFirst = i === 0; 10 | 11 | const toggle = panel.querySelector('[data-accordion-toggle]'); 12 | const content = panel.querySelector('[data-accordion-content]'); 13 | 14 | if (!toggle || !content) { 15 | return; 16 | } 17 | 18 | toggle.addEventListener('click', () => { 19 | const wasOpen = toggle.getAttribute('aria-pressed') === 'true'; 20 | const isOpen = !wasOpen; 21 | 22 | toggle.setAttribute('aria-pressed', isOpen); 23 | toggle.setAttribute('aria-expanded', isOpen); 24 | content.hidden = !isOpen; 25 | }); 26 | 27 | // All panels are open by default. When JS kicks in, the first panel stays open, 28 | // other panels are closed. 29 | if (isFirst) { 30 | toggle.setAttribute('aria-pressed', true); 31 | toggle.setAttribute('aria-expanded', true); 32 | content.hidden = false; 33 | } else { 34 | toggle.setAttribute('aria-pressed', false); 35 | toggle.setAttribute('aria-expanded', false); 36 | content.hidden = true; 37 | } 38 | }); 39 | }); 40 | }; 41 | 42 | document.addEventListener('DOMContentLoaded', () => { 43 | initAccordions(); 44 | }); 45 | -------------------------------------------------------------------------------- /docs/reference/concepts.md: -------------------------------------------------------------------------------- 1 | # Concepts 2 | 3 | To understand how `django-pattern-library` works, the following concepts are important. 4 | 5 | ## Patterns 6 | 7 | Any template that is displayed by the pattern library is referred to as a pattern. Patterns are divided into two categories: fragments and pages. 8 | 9 | ## Fragments 10 | 11 | A fragment is a pattern whose markup does not include all of the resources (typically CSS and Javascript) for it to be displayed correctly on its own. This is typical for reusable component templates which depend on global stylesheets or Javascript bundles to render and behave correctly. 12 | 13 | To enable them to be correctly displayed in the pattern library, `django-pattern-library` will inject the rendered markup of fragments into the **pattern base template** specified by `PATTERN_LIBRARY['PATTERN_BASE_TEMPLATE_NAME']`. 14 | 15 | This template should include references to any required static files. The rendered markup of fragments will be available in the `pattern_library_rendered_pattern` context variable (see the tests for [an example](https://github.com/torchbox/django-pattern-library/blob/main/tests/templates/patterns/base.html)). 16 | 17 | ## Pages 18 | 19 | In contrast to fragments, pages are patterns that include everything they need to be displayed correctly in their markup. Pages are defined by `PATTERN_LIBRARY['BASE_TEMPLATE_NAMES']`. 20 | 21 | Any template in that list — or that extends a template in that list — is considered a page and will be displayed as-is when rendered in the pattern library. 22 | 23 | It is common practice for page templates to extend the pattern base template to avoid duplicate references to stylesheets and Javascript bundles. Again, [an example](https://github.com/torchbox/django-pattern-library/blob/main/tests/templates/patterns/base_page.html) of this can be seen in the tests. 24 | -------------------------------------------------------------------------------- /pattern_library/static/pattern_library/src/scss/components/_list.scss: -------------------------------------------------------------------------------- 1 | @use '../config' as *; 2 | 3 | .list { 4 | list-style: none; 5 | padding: 0; 6 | margin: 0; 7 | 8 | &--child, 9 | &--grandchild { 10 | font-size: 13px; 11 | color: $mid-grey; 12 | display: none; 13 | padding-left: 10px; 14 | 15 | &.is-open { 16 | display: block; 17 | } 18 | } 19 | 20 | &--grandchild { 21 | padding-left: 15px; 22 | } 23 | 24 | &__button { 25 | display: flex; 26 | align-items: center; 27 | margin: 10px 0; 28 | user-select: none; 29 | font-size: 19px; 30 | appearance: none; 31 | background-color: transparent; 32 | border: 0; 33 | gap: 5px; 34 | padding: 0; 35 | 36 | &:hover { 37 | cursor: pointer; 38 | } 39 | 40 | &--child { 41 | color: $mid-grey; 42 | font-size: 13px; 43 | } 44 | } 45 | 46 | &__item-icon { 47 | width: 15px; 48 | height: 15px; 49 | pointer-events: none; 50 | transition: transform 0.15s ease-in-out; 51 | 52 | .is-open > & { 53 | transform: rotate(90deg); 54 | } 55 | } 56 | 57 | &__item-icon--small { 58 | height: 10px; 59 | } 60 | 61 | &__item-link { 62 | background: $off-white; 63 | display: block; 64 | font-size: inherit; 65 | border-left: 5px solid $off-white; 66 | transition: 67 | background, 68 | border, 69 | 0.15s ease-in-out; 70 | padding: 10px 20px 10px 30px; 71 | margin: 0 -20px 0 -35px; 72 | 73 | &:hover, 74 | &.is-active { 75 | background: $white; 76 | border-left: 5px solid $color-primary; 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /pattern_library/static/pattern_library/src/js/components/pattern-search.js: -------------------------------------------------------------------------------- 1 | export default function () { 2 | const searchBox = document.getElementById('js-pattern-search-input'); 3 | const patternList = [...document.querySelectorAll('.list__item-link')]; 4 | const patternListContainer = document.getElementById('sidebar-nav'); 5 | const searchResultsContainer = document.getElementById( 6 | 'js-pattern-search-results-container', 7 | ); 8 | 9 | searchBox.addEventListener('keyup', (e) => { 10 | let searchValue = e.target.value.toLowerCase(); 11 | 12 | // Clear if input value is empty 13 | if (searchValue === '') { 14 | searchResultsContainer.innerHTML = ''; 15 | patternListContainer.classList.remove('sidebar__nav--inactive'); 16 | } 17 | 18 | // On enter key 19 | if (e.keyCode == 13 && searchValue != '') { 20 | // Clear results list and hide pattern list 21 | searchResultsContainer.innerHTML = ''; 22 | patternListContainer.classList.add('sidebar__nav--inactive'); 23 | 24 | // Match search query 25 | let matchedValues = patternList.filter(function (item) { 26 | return item.textContent.toLowerCase().includes(searchValue); 27 | }); 28 | 29 | // Populate search results 30 | if (matchedValues.length) { 31 | matchedValues.forEach((item) => { 32 | searchResultsContainer.innerHTML += 33 | '' + 36 | item.textContent + 37 | ''; 38 | }); 39 | } else { 40 | searchResultsContainer.innerHTML = 'No results found.'; 41 | } 42 | } 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /pattern_library/static/pattern_library/src/js/components/iframe.js: -------------------------------------------------------------------------------- 1 | export function resizeIframe() { 2 | const body = document.querySelector('body'); 3 | const patternIframe = document.querySelector('.js-iframe'); 4 | const resizeButtons = document.querySelectorAll('.js-resize-iframe'); 5 | 6 | // remove animating class to prevent delay when dragging iframe 7 | patternIframe.addEventListener('mousedown', function () { 8 | this.classList.remove('is-animatable'); 9 | }); 10 | 11 | patternIframe.addEventListener('mouseup', function () { 12 | this.classList.add('is-animatable'); 13 | }); 14 | 15 | patternIframe.contentWindow.addEventListener('resize', (e) => { 16 | document.querySelector('.js-iframe-size').innerHTML = 17 | `${e.target.innerWidth} x ${e.target.innerHeight}`; 18 | }); 19 | 20 | // Close iframe with escape key 21 | document.addEventListener('keydown', (e) => { 22 | e = e || window.event; 23 | if (e.key === 'Escape') { 24 | body.classList.remove('iframe-open'); 25 | } 26 | }); 27 | 28 | // Resize iframe via buttons 29 | resizeButtons.forEach((button) => { 30 | button.addEventListener('click', (e) => { 31 | resizeButtons.forEach((button) => 32 | button.classList.remove('is-active'), 33 | ); 34 | e.target.classList.add('is-active'); 35 | patternIframe.style.width = 36 | e.target.dataset.resize == 100 37 | ? `${e.target.dataset.resize}%` 38 | : `${e.target.dataset.resize}px`; 39 | }); 40 | }); 41 | } 42 | 43 | export function setIframeSize() { 44 | const iframe = document.querySelector('.js-iframe').contentWindow; 45 | document.querySelector('.js-iframe-size').innerHTML = 46 | `${iframe.innerWidth} x ${iframe.innerHeight}`; 47 | } 48 | -------------------------------------------------------------------------------- /docs/recipes/forms-and-fields.md: -------------------------------------------------------------------------------- 1 | # Forms and fields 2 | 3 | We can define context for forms and fields in Python, with [context modifiers](../guides/defining-template-context.md#modifying-template-contexts-with-python). 4 | 5 | Basic Django form definition: 6 | 7 | ```python 8 | from django import forms 9 | 10 | 11 | class ExampleForm(forms.Form): 12 | single_line_text = forms.CharField( 13 | max_length=255, help_text="This is some help text" 14 | ) 15 | choices = (("one", "One"), ("two", "Two"), ("three", "Three"), ("four", "Four")) 16 | select = forms.ChoiceField(choices=choices) 17 | ``` 18 | 19 | Rendered as: 20 | 21 | ```jinja2 22 | {% extends "patterns/base.html" %} 23 | 24 | {% block content %} 25 |
26 | {% csrf_token %} 27 |
28 | {% for hidden_field in form.hidden_fields %} 29 | {{ hidden_field }} 30 | {% endfor %} 31 | 32 | {% for field in form.visible_fields %} 33 | {% include "patterns/molecules/field/field.html" with field=field %} 34 | {% endfor %} 35 | 36 | 37 |
38 |
39 | {% endblock %} 40 | ``` 41 | 42 | Context overrides when rendering the whole form: 43 | 44 | ```python 45 | from pattern_library import register_context_modifier 46 | from .forms import ExampleForm 47 | 48 | 49 | @register_context_modifier 50 | def add_common_forms(context, request): 51 | context['form'] = ExampleForm() 52 | ``` 53 | 54 | Context overrides for `field.html`: 55 | 56 | ```python 57 | from pattern_library import register_context_modifier 58 | from .forms import ExampleForm 59 | 60 | 61 | @register_context_modifier(template='patterns/molecules/field/field.html') 62 | def add_field(context, request): 63 | form = ExampleForm() 64 | context['field'] = form['single_line_text'] 65 | ``` 66 | -------------------------------------------------------------------------------- /docs/guides/static-site-export.md: -------------------------------------------------------------------------------- 1 | # Static site export 2 | 3 | It can be useful to publish your pattern library as a static site – for example to host it without a runtime dependency on the rest of your code, or to save past versions, or save the output as build artifacts. 4 | 5 | ## With `render_patterns` 6 | 7 | The [`render_patterns` command](../reference/api.md#render_patterns) command can be used to export all your templates, without exporting the pattern library UI. 8 | 9 | ```sh 10 | # Export all templates, wrapped in the base template like the pattern library UI does. 11 | ./manage.py render_patterns --wrap-fragments --output dpl-rendered-patterns 12 | # Or alternatively export all templates without extra wrapping. 13 | ./manage.py render_patterns --output dpl-rendered-patterns 14 | ``` 15 | 16 | This command will create a new folder, with a structure matching that of your templates: 17 | 18 | ```txt 19 | dpl-rendered-patterns 20 | ├── atoms 21 | │   ├── icons 22 | │   │   └── icon.html 23 | ├── molecules 24 | │   ├── accordion 25 | │   │   └── accordion.html 26 | └── pages 27 | └── people 28 |    └── person_page.html 29 | ``` 30 | 31 | Note this will export all templates but won’t export static files. If you need static files for your export, additionally run [`collectstatic`](https://docs.djangoproject.com/en/3.1/ref/contrib/staticfiles/#collectstatic), and move its output to be where [`STATIC_URL`](https://docs.djangoproject.com/en/3.1/ref/settings/#std:setting-STATIC_URL) expects it: 32 | 33 | ```sh 34 | ./manage.py collectstatic 35 | # Moving the collected files to match STATIC_URL 36 | mv static dpl-rendered-patterns/static 37 | ``` 38 | 39 | ## With a website scrapper 40 | 41 | It’s very straightforward to export the whole pattern library as a static site, including all templates, and the pattern library UI. Here is an example exporting the pattern library with [`wget`](https://en.wikipedia.org/wiki/Wget): 42 | 43 | ```sh 44 | wget --mirror --page-requisites --no-parent http://localhost:8000/pattern-library/ 45 | ``` 46 | -------------------------------------------------------------------------------- /tests/templates/patterns/pages/people/person_page.html: -------------------------------------------------------------------------------- 1 | {% extends 'patterns/base_page.html' %} 2 | 3 | {% block content %} 4 |
5 |
6 |
7 |

{{ page.first_name }} {{ page.last_name }}

8 | 9 | {% if page.job_title %} 10 |

{{ page.job_title }}

11 | {% endif %} 12 | 13 | {% if page.website %} 14 |

{{ page.website }}

15 | {% endif %} 16 | 17 | {% if page.email %} 18 |

{{ page.email }}

19 | {% endif %} 20 | 21 | {% for item in page.social_media_profile.all %} 22 |

{{ item.profile_url }}

23 | {% endfor %} 24 | 25 | {% with phone_numbers=page.phone_numbers.all %} 26 | {% if phone_numbers %} 27 | {% for related_phone_number in phone_numbers %} 28 |

{{ related_phone_number.phone_number }}

29 | {% endfor %} 30 | {% endif %} 31 | {% endwith %} 32 |
33 |
34 | 35 |
36 | {% if page.introduction %} 37 |

{{ page.introduction }}

38 | {% endif %} 39 |
40 | 41 |
42 | 43 | {% include "patterns/molecules/accordion/accordion.html" with accordions=page.accordions %} 44 | 45 |
46 | 47 | {% with person_type=page.person_types.all %} 48 | {% if person_type %} 49 |
50 |

Person types

51 | {% for person_type in person_type %} 52 |

{{ person_type }}

53 | {% endfor %} 54 |
55 | {% endif %} 56 | {% endwith %} 57 |
58 | {% endblock %} 59 | -------------------------------------------------------------------------------- /pattern_library/static/pattern_library/src/scss/hljs/_a11y-dark.scss: -------------------------------------------------------------------------------- 1 | // a11y-dark theme 2 | // Based on the Tomorrow Night Eighties theme: https://github.com/isagalaev/highlight.js/blob/master/src/styles/tomorrow-night-eighties.css 3 | // @author: ericwbailey 4 | 5 | // stylelint-disable selector-class-pattern, scale-unlimited/declaration-strict-value 6 | 7 | /* Comment */ 8 | .hljs-comment, 9 | .hljs-quote { 10 | color: #d4d0ab; 11 | } 12 | 13 | /* Red */ 14 | .hljs-variable, 15 | .hljs-template-variable, 16 | .hljs-tag, 17 | .hljs-name, 18 | .hljs-selector-id, 19 | .hljs-selector-class, 20 | .hljs-regexp, 21 | .hljs-deletion { 22 | color: #ffa07a; 23 | } 24 | 25 | /* Orange */ 26 | .hljs-number, 27 | .hljs-built_in, 28 | .hljs-builtin-name, 29 | .hljs-literal, 30 | .hljs-type, 31 | .hljs-params, 32 | .hljs-meta, 33 | .hljs-link { 34 | color: #f5ab35; 35 | } 36 | 37 | /* Yellow */ 38 | .hljs-attribute { 39 | color: #ffd700; 40 | } 41 | 42 | /* Green */ 43 | .hljs-string, 44 | .hljs-symbol, 45 | .hljs-bullet, 46 | .hljs-addition { 47 | color: #abe338; 48 | } 49 | 50 | /* Blue */ 51 | .hljs-title, 52 | .hljs-section { 53 | color: #00e0e0; 54 | } 55 | 56 | /* Purple */ 57 | .hljs-keyword, 58 | .hljs-selector-tag { 59 | color: #dcc6e0; 60 | } 61 | 62 | .hljs { 63 | display: block; 64 | overflow-x: auto; 65 | background: #2b2b2b; 66 | color: #f8f8f2; 67 | padding: 0.5em; 68 | } 69 | 70 | .hljs-emphasis { 71 | font-style: italic; 72 | } 73 | 74 | .hljs-strong { 75 | font-weight: bold; 76 | } 77 | 78 | @media screen and (-ms-high-contrast: active) { 79 | .hljs-addition, 80 | .hljs-attribute, 81 | .hljs-built_in, 82 | .hljs-builtin-name, 83 | .hljs-bullet, 84 | .hljs-comment, 85 | .hljs-link, 86 | .hljs-literal, 87 | .hljs-meta, 88 | .hljs-number, 89 | .hljs-params, 90 | .hljs-string, 91 | .hljs-symbol, 92 | .hljs-type, 93 | .hljs-quote { 94 | color: highlight; 95 | } 96 | 97 | .hljs-keyword, 98 | .hljs-selector-tag { 99 | font-weight: bold; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /pattern_library/static/pattern_library/src/scss/hljs/_a11y-light.scss: -------------------------------------------------------------------------------- 1 | // a11y-light theme 2 | // Based on the Tomorrow Night Eighties theme: https://github.com/isagalaev/highlight.js/blob/master/src/styles/tomorrow-night-eighties.css 3 | // @author: ericwbailey 4 | 5 | // stylelint-disable selector-class-pattern, scale-unlimited/declaration-strict-value 6 | 7 | /* Comment */ 8 | .hljs-comment, 9 | .hljs-quote { 10 | color: #696969; 11 | } 12 | 13 | /* Red */ 14 | .hljs-variable, 15 | .hljs-template-variable, 16 | .hljs-tag, 17 | .hljs-name, 18 | .hljs-selector-id, 19 | .hljs-selector-class, 20 | .hljs-regexp, 21 | .hljs-deletion { 22 | color: #d91e18; 23 | } 24 | 25 | /* Orange */ 26 | .hljs-number, 27 | .hljs-built_in, 28 | .hljs-builtin-name, 29 | .hljs-literal, 30 | .hljs-type, 31 | .hljs-params, 32 | .hljs-meta, 33 | .hljs-link { 34 | color: #aa5d00; 35 | } 36 | 37 | /* Yellow */ 38 | .hljs-attribute { 39 | color: #aa5d00; 40 | } 41 | 42 | /* Green */ 43 | .hljs-string, 44 | .hljs-symbol, 45 | .hljs-bullet, 46 | .hljs-addition { 47 | color: #008000; 48 | } 49 | 50 | /* Blue */ 51 | .hljs-title, 52 | .hljs-section { 53 | color: #007faa; 54 | } 55 | 56 | /* Purple */ 57 | .hljs-keyword, 58 | .hljs-selector-tag { 59 | color: #7928a1; 60 | } 61 | 62 | .hljs { 63 | display: block; 64 | overflow-x: auto; 65 | background: #fefefe; 66 | color: #545454; 67 | padding: 0.5em; 68 | } 69 | 70 | .hljs-emphasis { 71 | font-style: italic; 72 | } 73 | 74 | .hljs-strong { 75 | font-weight: bold; 76 | } 77 | 78 | @media screen and (-ms-high-contrast: active) { 79 | .hljs-addition, 80 | .hljs-attribute, 81 | .hljs-built_in, 82 | .hljs-builtin-name, 83 | .hljs-bullet, 84 | .hljs-comment, 85 | .hljs-link, 86 | .hljs-literal, 87 | .hljs-meta, 88 | .hljs-number, 89 | .hljs-params, 90 | .hljs-string, 91 | .hljs-symbol, 92 | .hljs-type, 93 | .hljs-quote { 94 | color: highlight; 95 | } 96 | 97 | .hljs-keyword, 98 | .hljs-selector-tag { 99 | font-weight: bold; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /docs/recipes/pagination.md: -------------------------------------------------------------------------------- 1 | # Pagination 2 | 3 | The Django paginator API is impossible to fully recreate in YAML. Instead, we can define context in Python, with [context modifiers](../guides/defining-template-context.md#modifying-template-contexts-with-python). Take a template like: 4 | 5 | ```jinja2 6 | {% with count=search_results.paginator.count %} 7 | {{ count }} result{{ count|pluralize }} found. 8 | {% endwith %} 9 | 10 | {% for result in search_results %} 11 |

{{ result.title }}

12 | {% endfor %} 13 | 14 | {% if search_results.paginator.num_pages > 1 %} 15 | 28 | {% endif %} 29 | ``` 30 | 31 | We can create the needed context by using Django’s [`Paginator` API](https://docs.djangoproject.com/en/3.2/topics/pagination/). 32 | 33 | ```python 34 | from django.core.paginator import Paginator 35 | 36 | from pattern_library import register_context_modifier 37 | 38 | 39 | @register_context_modifier(template='patterns/pages/search/search.html') 40 | def replicate_pagination(context, request): 41 | object_list = context.pop('search_results', None) 42 | if object_list is None: 43 | return 44 | 45 | original_length = len(object_list) 46 | 47 | # add dummy items to force pagination 48 | for i in range(50): 49 | object_list.append(None) 50 | 51 | paginator = Paginator(object_list, original_length) 52 | context.update( 53 | paginator=paginator, 54 | search_results=paginator.page(10), 55 | is_paginated=True, 56 | object_list=object_list 57 | ) 58 | ``` 59 | -------------------------------------------------------------------------------- /tests/templates/patterns/molecules/field/field.html: -------------------------------------------------------------------------------- 1 | {% load test_tags %} 2 | {% with widget_type=field|widget_type %} 3 |
4 | 5 | {% if field.errors %} 6 |
7 | {{ field.errors }} 8 |
9 | {% endif %} 10 | 11 | {% if widget_type == 'checkbox-select-multiple' or widget_type == 'radio-select' %} 12 |
13 | 14 | {{ field.label }} 15 | {% if field.field.required %}{% endif %} 16 | 17 | 18 |
    19 | {% for boundwidget in field %} 20 |
  • 21 | 22 | 23 |
  • 24 | {% endfor %} 25 |
26 |
27 | 28 | {% elif widget_type == 'checkbox-input' %} 29 | 30 |
31 | {{ field }} 32 | 36 |
37 | 38 | {% else %} 39 | 40 | 44 | {{ field }} 45 | 46 | {% endif %} 47 | 48 | {% if field.help_text %}
{{ field.help_text }}
{% endif %} 49 | 50 |
51 | {% endwith %} 52 | -------------------------------------------------------------------------------- /tests/tests/test_sections.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.test import SimpleTestCase, override_settings 3 | 4 | from bs4 import BeautifulSoup 5 | 6 | from .utils import reverse 7 | 8 | 9 | class SectionsTestCase(SimpleTestCase): 10 | def get_sections(self): 11 | response = self.client.get(reverse("pattern_library:index")) 12 | self.assertEqual(response.status_code, 200) 13 | 14 | soup = BeautifulSoup(response.content, features="html.parser") 15 | sidebar_nav = soup.select_one("#sidebar-nav") 16 | sections = [ 17 | h.text.strip() 18 | for h in sidebar_nav.find_all("button", {"class": "list__button--parent"}) 19 | ] 20 | 21 | return sections 22 | 23 | def test_just_atoms(self): 24 | pl_settings = settings.PATTERN_LIBRARY 25 | 26 | with override_settings( 27 | PATTERN_LIBRARY={ 28 | "TEMPLATE_DIR": pl_settings["TEMPLATE_DIR"], 29 | "SECTIONS": [ 30 | ("atoms", ["patterns/atoms"]), 31 | ], 32 | } 33 | ): 34 | # Check the section list in the sidebar only contains the Atoms sections 35 | sections = self.get_sections() 36 | self.assertListEqual(sections, ["Atoms"]) 37 | 38 | # Check a pattern from Atoms can be rendered 39 | test_atom_render_url = reverse( 40 | "pattern_library:render_pattern", 41 | kwargs={ 42 | "pattern_template_name": "patterns/atoms/test_atom/test_atom.html" 43 | }, 44 | ) 45 | 46 | response = self.client.get(test_atom_render_url) 47 | self.assertEqual(response.status_code, 200) 48 | 49 | # Check a pattern from Molecules cannot be rendered 50 | test_molecule_render_url = reverse( 51 | "pattern_library:render_pattern", 52 | kwargs={ 53 | "pattern_template_name": "patterns/molecules/test_molecule/test_molecule.html" 54 | }, 55 | ) 56 | 57 | response = self.client.get(test_molecule_render_url) 58 | self.assertEqual(response.status_code, 404) 59 | -------------------------------------------------------------------------------- /pattern_library/__init__.py: -------------------------------------------------------------------------------- 1 | import django 2 | 3 | from .context_modifiers import register_context_modifier 4 | 5 | if django.VERSION < (3, 2): 6 | default_app_config = "pattern_library.apps.PatternLibraryAppConfig" 7 | 8 | __all__ = [ 9 | "DEFAULT_SETTINGS", 10 | "get_setting", 11 | "get_pattern_template_suffix", 12 | "get_pattern_base_template_name", 13 | "get_base_template_names", 14 | "get_sections", 15 | "get_pattern_context_var_name", 16 | "register_context_modifier", 17 | ] 18 | 19 | DEFAULT_SETTINGS = { 20 | # PATTERN_BASE_TEMPLATE_NAME is the template that fragments will be wrapped with. 21 | # It should include any required CSS and JS and output 22 | # `pattern_library_rendered_pattern` from context. 23 | "PATTERN_BASE_TEMPLATE_NAME": "patterns/base.html", 24 | # Any template in BASE_TEMPLATE_NAMES or any template that extends a template in 25 | # BASE_TEMPLATE_NAMES is a "page" and will be rendered as-is without being wrapped. 26 | "BASE_TEMPLATE_NAMES": ["patterns/base_page.html"], 27 | "TEMPLATE_SUFFIX": ".html", 28 | # SECTIONS controls the groups of templates that appear in the navigation. The keys 29 | # are the group titles and the value are lists of template name prefixes that will 30 | # be searched to populate the groups. 31 | "SECTIONS": ( 32 | ("atoms", ["patterns/atoms"]), 33 | ("molecules", ["patterns/molecules"]), 34 | ("organisms", ["patterns/organisms"]), 35 | ("templates", ["patterns/templates"]), 36 | ("pages", ["patterns/pages"]), 37 | ), 38 | } 39 | 40 | 41 | def get_setting(attr): 42 | from django.conf import settings 43 | 44 | library_settings = DEFAULT_SETTINGS.copy() 45 | library_settings.update(getattr(settings, "PATTERN_LIBRARY", {})) 46 | return library_settings.get(attr) 47 | 48 | 49 | def get_pattern_template_suffix(): 50 | return get_setting("TEMPLATE_SUFFIX") 51 | 52 | 53 | def get_pattern_base_template_name(): 54 | return get_setting("PATTERN_BASE_TEMPLATE_NAME") 55 | 56 | 57 | def get_base_template_names(): 58 | return get_setting("BASE_TEMPLATE_NAMES") 59 | 60 | 61 | def get_sections(): 62 | return get_setting("SECTIONS") 63 | 64 | 65 | def get_pattern_context_var_name(): 66 | return "is_pattern_library" 67 | -------------------------------------------------------------------------------- /tests/pattern_contexts.py: -------------------------------------------------------------------------------- 1 | from django.core.paginator import Paginator 2 | 3 | from pattern_library import register_context_modifier 4 | 5 | from .forms import ExampleForm 6 | 7 | 8 | @register_context_modifier 9 | def add_common_forms(context, request): 10 | context["form"] = ExampleForm() 11 | 12 | 13 | @register_context_modifier(template="patterns/molecules/field/field.html") 14 | def add_field(context, request): 15 | form = ExampleForm() 16 | context["field"] = form["single_line_text"] 17 | 18 | 19 | @register_context_modifier(template="patterns/pages/search/search.html") 20 | def replicate_pagination(context, request): 21 | """ 22 | Replace lists of items using the 'page_obj.object_list' key 23 | with a real Paginator page, and add a few other pagination-related 24 | things to the context (like Django's `ListView` does). 25 | """ 26 | object_list = context.pop("search_results", None) 27 | if object_list is None: 28 | return 29 | 30 | original_length = len(object_list) 31 | 32 | # add dummy items to force pagination 33 | for i in range(50): 34 | object_list.append({"title": i}) 35 | 36 | # paginate and add ListView-like values 37 | paginator = Paginator(object_list, original_length) 38 | context.update( 39 | paginator=paginator, 40 | search_results=paginator.page(10), 41 | is_paginated=True, 42 | object_list=object_list, 43 | ) 44 | 45 | 46 | @register_context_modifier(template="patterns_jinja/pages/search/search.html") 47 | def replicate_pagination_jinja(context, request): 48 | """ 49 | Replace lists of items using the 'page_obj.object_list' key 50 | with a real Paginator page, and add a few other pagination-related 51 | things to the context (like Django's `ListView` does). 52 | """ 53 | object_list = context.pop("search_results", None) 54 | if object_list is None: 55 | return 56 | 57 | original_length = len(object_list) 58 | 59 | # add dummy items to force pagination 60 | for i in range(50): 61 | object_list.append({"title": i}) 62 | 63 | # paginate and add ListView-like values 64 | paginator = Paginator(object_list, original_length) 65 | context.update( 66 | paginator=paginator, 67 | search_results=paginator.page(10), 68 | is_paginated=True, 69 | object_list=object_list, 70 | ) 71 | -------------------------------------------------------------------------------- /pattern_library/context_modifiers.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | from operator import attrgetter 3 | from typing import Callable 4 | 5 | from django.core.exceptions import ImproperlyConfigured 6 | 7 | from .cm_utils import accepts_kwarg, get_app_submodules 8 | 9 | GENERIC_CM_KEY = "__generic__" 10 | ORDER_ATTR_NAME = "__cm_order" 11 | 12 | __all__ = ["ContextModifierRegistry", "register_context_modifier"] 13 | 14 | 15 | class ContextModifierRegistry(defaultdict): 16 | def __init__(self): 17 | super().__init__(list) 18 | self.searched_for_modifiers = False 19 | 20 | def search_for_modifiers(self) -> None: 21 | if not self.searched_for_modifiers: 22 | list(get_app_submodules("pattern_contexts")) 23 | self.searched_for_modifiers = True 24 | 25 | def register(self, func: Callable, template: str = None, order: int = 0) -> None: 26 | """ 27 | Adds a context modifier to the registry. 28 | """ 29 | if not callable(func): 30 | raise ImproperlyConfigured( 31 | f"Context modifiers must be callables. {func} is a {type(func).__name__}." 32 | ) 33 | if not accepts_kwarg(func, "context"): 34 | raise ImproperlyConfigured( 35 | f"Context modifiers must accept a 'context' keyword argument. {func} does not." 36 | ) 37 | if not accepts_kwarg(func, "request"): 38 | raise ImproperlyConfigured( 39 | f"Context modifiers must accept a 'request' keyword argument. {func} does not." 40 | ) 41 | 42 | key = template or GENERIC_CM_KEY 43 | if func not in self[key]: 44 | setattr(func, ORDER_ATTR_NAME, order) 45 | self[key].append(func) 46 | self[key].sort(key=attrgetter(ORDER_ATTR_NAME)) 47 | 48 | return func 49 | 50 | def register_decorator(self, func: Callable = None, **kwargs): 51 | if func is None: 52 | return lambda func: self.register(func, **kwargs) 53 | return self.register(func, **kwargs) 54 | 55 | def get_for_template(self, template: str): 56 | self.search_for_modifiers() 57 | modifiers = self[GENERIC_CM_KEY] + self[template] 58 | return sorted(modifiers, key=attrgetter(ORDER_ATTR_NAME)) 59 | 60 | 61 | registry = ContextModifierRegistry() 62 | register_context_modifier = registry.register_decorator 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # ------------------------------------------------- 4 | # OS files 5 | # ------------------------------------------------- 6 | .DS_Store 7 | .DS_Store? 8 | ._* 9 | .Spotlight-V100 10 | .Trashes 11 | ehthumbs.db 12 | Thumbs.db 13 | 14 | # Byte-compiled / optimized / DLL files 15 | __pycache__/ 16 | *.py[cod] 17 | *$py.class 18 | # C extensions 19 | *.so 20 | # Distribution / packaging 21 | .Python 22 | env/ 23 | build/ 24 | develop-eggs/ 25 | dist/ 26 | downloads/ 27 | eggs/ 28 | .eggs/ 29 | lib/ 30 | lib64/ 31 | parts/ 32 | sdist/ 33 | var/ 34 | *.egg-info/ 35 | .installed.cfg 36 | *.egg 37 | # PyInstaller 38 | # Usually these files are written by a python script from a template 39 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 40 | *.manifest 41 | *.spec 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # ------------------------------------------------- 47 | # Instrumentation and tooling 48 | # ------------------------------------------------- 49 | htmlcov/ 50 | .tox/ 51 | .coverage 52 | .coverage.* 53 | .cache 54 | nosetests.xml 55 | coverage.xml 56 | *,cover 57 | .hypothesis/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | # Django stuff: 63 | *.log 64 | # Flask instance folder 65 | instance/ 66 | # Scrapy stuff: 67 | .scrapy 68 | # Sphinx documentation 69 | docs/_build/ 70 | # mkdocs documentation 71 | site 72 | # PyBuilder 73 | target/ 74 | # IPython Notebook 75 | .ipynb_checkpoints 76 | # pyenv 77 | .python-version 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | # virtualenv 81 | venv/ 82 | ENV/ 83 | # Spyder project settings 84 | .spyderproject 85 | # Rope project settings 86 | .ropeproject 87 | 88 | # Node modules 89 | node_modules* 90 | 91 | # ------------------------------------------------- 92 | # Users Environment 93 | # ------------------------------------------------- 94 | .lock-wscript 95 | .idea 96 | .installed.cfg 97 | .vagrant 98 | .anaconda 99 | Vagrantfile.local 100 | .env 101 | /local 102 | local.py 103 | *.sublime-project 104 | *.sublime-workspace 105 | .vscode 106 | 107 | # ------------------------------------------------- 108 | # Your own project's ignores 109 | # ------------------------------------------------- 110 | dpl-rendered-patterns 111 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: django-pattern-library 2 | repo_url: https://github.com/torchbox/django-pattern-library 3 | site_url: 4 | edit_uri: edit/main/docs/ 5 | 6 | repo_name: django-pattern-library 7 | dev_addr: localhost:8001 8 | theme: 9 | name: material 10 | font: false 11 | palette: 12 | primary: indigo 13 | logo: images/logo.svg 14 | favicon: images/favicon.png 15 | markdown_extensions: 16 | - codehilite 17 | - admonition 18 | - sane_lists 19 | - toc: 20 | permalink: true 21 | # pymdown-extensions meant to bring the Markdown implementation closer to GFM. 22 | - pymdownx.details 23 | - pymdownx.magiclink 24 | - pymdownx.tasklist: 25 | custom_checkbox: true 26 | - pymdownx.tilde 27 | plugins: 28 | - search 29 | - git-revision-date 30 | - redirects: 31 | redirect_maps: 32 | 'reference/related-projects.md': 'community/related-projects.md' 33 | nav: 34 | - 'Home': 'index.md' 35 | - 'Getting started': 'getting-started.md' 36 | # The most important guides are shown in the top-level navigation. 37 | - 'Template context': 'guides/defining-template-context.md' 38 | - 'Overriding tags': 'guides/overriding-template-tags.md' 39 | - 'Usage tips': 'guides/usage-tips.md' 40 | - 'Guides': 41 | - 'Customizing rendering': 'guides/customizing-template-rendering.md' 42 | - 'Multiple variants': 'guides/multiple-variants.md' 43 | - 'Automated tests': 'guides/automated-tests.md' 44 | - 'Static site export': 'guides/static-site-export.md' 45 | - 'Reuse across projects': 'guides/reuse-across-projects.md' 46 | - 'Recipes': 47 | - 'Forms and fields': 'recipes/forms-and-fields.md' 48 | - 'Image include': 'recipes/image-include.md' 49 | - 'Image lazyload': 'recipes/image-lazyload.md' 50 | - 'Inclusion tags': 'recipes/inclusion-tags.md' 51 | - 'Looping for tags': 'recipes/looping-for-tags.md' 52 | - 'Pagination': 'recipes/pagination.md' 53 | - 'API rendering': 'recipes/api-rendering.md' 54 | - 'Reference': 55 | - 'API and settings': 'reference/api.md' 56 | - 'Concepts': 'reference/concepts.md' 57 | - 'Known issues': 'reference/known-issues.md' 58 | - 'Community': 59 | - 'Support': 'community/support.md' 60 | - 'Related projects': 'community/related-projects.md' 61 | - 'Code of conduct': 'community/code-of-conduct.md' 62 | - 'Security policy': 'community/security-policy.md' 63 | - 'Online demo': '/django-pattern-library/demo/' 64 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "django-pattern-library" 3 | version = "1.5.0" 4 | description = "A module for Django that allows to build pattern libraries for your projects." 5 | authors = ["Thibaud Colas "] 6 | license = "BSD-3-Clause" 7 | readme = "README.md" 8 | repository = "https://github.com/torchbox/django-pattern-library" 9 | documentation = "https://torchbox.github.io/django-pattern-library/" 10 | classifiers = [ 11 | "Development Status :: 5 - Production/Stable", 12 | "Environment :: Web Environment", 13 | "Intended Audience :: Developers", 14 | "License :: OSI Approved :: BSD License", 15 | "Operating System :: OS Independent", 16 | "Programming Language :: Python", 17 | "Programming Language :: Python :: 3.10", 18 | "Programming Language :: Python :: 3.11", 19 | "Programming Language :: Python :: 3.12", 20 | "Programming Language :: Python :: 3.13", 21 | "Programming Language :: Python :: 3.14", 22 | "Framework :: Django", 23 | "Framework :: Django :: 4.2", 24 | "Framework :: Django :: 5.2", 25 | "Framework :: Django :: 6.0", 26 | ] 27 | packages = [{ include = "pattern_library" }] 28 | include = [ 29 | { path = "pattern_library/static/pattern_library/dist/bundle.js", format = [ 30 | "sdist", 31 | "wheel", 32 | ] }, 33 | ] 34 | exclude = ['pattern_library/static/pattern_library/src/**/*'] 35 | 36 | [tool.poetry.urls] 37 | "Demo" = "https://torchbox.github.io/django-pattern-library/demo/" 38 | "Support" = "https://github.com/torchbox/django-pattern-library/discussions" 39 | "Issues" = "https://github.com/torchbox/django-pattern-library/issues" 40 | 41 | [tool.poetry.dependencies] 42 | python = "^3.10" 43 | Django = ">=4.2" 44 | PyYAML = ">=5.1,<7.0" 45 | Markdown = "^3.1" 46 | 47 | [tool.poetry.group.dev.dependencies] 48 | beautifulsoup4 = "^4.8" 49 | coverage = "^7.4" 50 | flake8 = { version = "^7.0", python = ">=3.8.1,<4.0" } 51 | isort = "^5.10.1" 52 | mkdocs = "^1.1.2" 53 | mkdocs-material = "^5.5.14" 54 | pymdown-extensions = "^10.0" 55 | mkdocs-git-revision-date-plugin = "^0.3.1" 56 | mkdocs-redirects = "^1.0.3" 57 | black = "^25.1.0" 58 | 59 | [tool.isort] 60 | known_first_party = "pattern_library" 61 | known_django = "django" 62 | skip = ".tox,venv,migrations,node_modules" 63 | skip_gitignore = true 64 | sections = "FUTURE,STDLIB,DJANGO,THIRDPARTY,FIRSTPARTY,LOCALFOLDER" 65 | default_section = "THIRDPARTY" 66 | profile = "black" 67 | 68 | [build-system] 69 | requires = ["poetry-core"] 70 | build-backend = "poetry.core.masonry.api" 71 | -------------------------------------------------------------------------------- /docs/guides/multiple-variants.md: -------------------------------------------------------------------------------- 1 | # Multiple template variants 2 | 3 | See [#87](https://github.com/torchbox/django-pattern-library/issues/87). There is currently no support for trying out a single component with different variations in context or tag overrides, but this can worked around by creating pattern-library-only templates. 4 | 5 | For example, for this `call_to_action` template: 6 | 7 | ```django 8 |
9 | 10 |

{{ call_to_action.title }}

11 | {% include "patterns/atoms/link/link.html" with type="primary" classes="call-to-action__link" href=call_to_action.get_link_url text=call_to_action.get_link_text %} 12 |
13 | ``` 14 | 15 | We can try it out once with the following YAML: 16 | 17 | ```yaml 18 | context: 19 | call_to_action: 20 | title: Will you help us protect these magnificant creatures in the UK waters? 21 | illustration: 22 | url: /static/images/illustrations/sharks.svg 23 | get_link_text: Sign up for our appeal 24 | get_link_url: '#' 25 | ``` 26 | 27 | If we want to try multiple variants, simply create a custom template for pattern library usage only, that renders `call_to_action` multiple times: 28 | 29 | ```django 30 |
31 |

Call to action

32 | {% for call_to_action in ctas %} 33 |
34 |

{{ call_to_action.type }}

35 | {% include "patterns/molecules/cta/call_to_action.html" with call_to_action=call_to_action %} 36 |
37 | {% endfor %} 38 |
39 | ``` 40 | 41 | ```yaml 42 | context: 43 | ctas: 44 | - type: Call to action 45 | title: Will you help us protect these magnificant creatures in the UK waters? 46 | illustration: 47 | url: /static/images/illustrations/sharks.svg 48 | get_link_text: Sign up for our appeal 49 | get_link_url: '#' 50 | - type: Call to action with short title 51 | title: Will you help us? 52 | illustration: 53 | url: /static/images/illustrations/sharks.svg 54 | get_link_text: Sign up for our appeal 55 | get_link_url: '#' 56 | - type: Call to action with long title 57 | title: Will you help us protect these magnificant and learn how to make environmentally responsible choices when buying seafood? 58 | illustration: 59 | url: /static/images/illustrations/sharks.svg 60 | get_link_text: Sign up for our appeal 61 | get_link_url: '#' 62 | ``` 63 | -------------------------------------------------------------------------------- /pattern_library/templates/pattern_library/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | Django pattern library 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | Close menu 18 | 19 | 20 |

21 | Pattern Library 22 | {# Set in _config.scss #} 23 |

24 |
25 | 47 |
48 | {% block content %}{% endblock %} 49 |
50 | 51 | 52 | -------------------------------------------------------------------------------- /pattern_library/templates/pattern_library/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'pattern_library/base.html' %} 2 | 3 | {% block content %} 4 |
5 |
6 |

7 | 8 | {{ pattern_name }} 9 | 10 | 11 |

12 |
13 |

14 | Window sizes 15 |

16 | 17 | 18 | 19 | 20 |
21 |
22 | 23 | 24 | 25 |
26 | 38 | 39 |
40 |
41 |
{{ pattern_source }}
42 |
43 | 44 |
45 |
{{ pattern_config }}
46 |
47 | 48 |
49 |
{{ pattern_markdown|safe }}
50 |
51 |
52 |
53 | 54 |
55 | {% endblock %} 56 | -------------------------------------------------------------------------------- /docs/guides/customizing-template-rendering.md: -------------------------------------------------------------------------------- 1 | # Customizing template rendering 2 | 3 | ## Customizing all patterns’ surroundings 4 | 5 | All patterns that are not pages are rendered within a base page template. The pattern library will render patterns inside the `content` block, which you can tweak to change how patterns are displayed. 6 | 7 | You can for example add a theme wrapper around the components: 8 | 9 | ```jinja2 10 | {% block content %} 11 | {% if pattern_library_rendered_pattern %} 12 |
13 | {{ pattern_library_rendered_pattern }} 14 |
15 | {% endif %} 16 | {% endblock %} 17 | ``` 18 | 19 | `pattern_library_rendered_pattern` can also be used to do other modifications on the page for the pattern library only, for example adding an extra class to ``: 20 | 21 | ```jinja2 22 | 23 | ``` 24 | 25 | ## Customizing a single pattern’s surroundings 26 | 27 | There is no API to customize a single pattern’s surroundings, but it can be done by using pattern-library-only templates. For example, with our `quote_block.html` component: 28 | 29 | ```django 30 |
31 |
32 |

{{ quote }}

33 | {% if attribution %} 34 |

{{ attribution }}

35 | {% endif %} 36 |
37 |
38 | ``` 39 | 40 | We could create another template next to it called `quote_block_example.html`, 41 | 42 | ```django 43 |
44 | {% include "patterns/components/quote_block/quote_block.html" with attribution=attribution quote=quote %} 45 |
46 | ``` 47 | 48 | This is a fair amount of boilerplate, but neatly solves the problem per pattern. 49 | 50 | ## Customizing a single pattern’s rendering 51 | 52 | Sometimes, it can help for a pattern to work differently in the pattern library. This can be done to make it easier to test, or to avoid rendering parts of a component that have intricate dependencies in the context of the pattern library. 53 | 54 | We can do this with the `is_pattern_library` context variable. Here is an example where we bypass loading the real menu data and would instead use the pattern library’s mock context: 55 | 56 | ```django 57 | {% load hub_tags %} 58 | 59 | {# Check if this is loading the pattern library or not. #} 60 | {% if not is_pattern_library %} 61 | {% get_hub_menu page as menu %} 62 | {% endif %} 63 | 64 | 74 | ``` 75 | -------------------------------------------------------------------------------- /tests/tests/test_context.py: -------------------------------------------------------------------------------- 1 | from django.test import SimpleTestCase 2 | from django.utils.safestring import SafeText 3 | 4 | from .utils import reverse 5 | 6 | 7 | class ContextTestCase(SimpleTestCase): 8 | def test_context_from_file(self): 9 | response = self.client.get( 10 | reverse( 11 | "pattern_library:display_pattern", 12 | kwargs={ 13 | "pattern_template_name": "patterns/atoms/test_atom/test_atom.html" 14 | }, 15 | ) 16 | ) 17 | self.assertContains(response, "atom_var value from test_atom.yaml") 18 | 19 | def test_including_context_overrides_included_context(self): 20 | response = self.client.get( 21 | reverse( 22 | "pattern_library:display_pattern", 23 | kwargs={ 24 | "pattern_template_name": "patterns/molecules/test_molecule/test_molecule.html" 25 | }, 26 | ) 27 | ) 28 | self.assertContains(response, "atom_var value from test_molecule.yaml") 29 | self.assertContains( 30 | response, "atom_var value from test_molecule.html include tag" 31 | ) 32 | self.assertNotContains(response, "atom_var value from test_atom.html") 33 | 34 | def test_marking_strings_safe_in_context(self): 35 | from pattern_library.utils import mark_context_strings_safe 36 | 37 | context = { 38 | "str": "str", 39 | "list": ["str", "str"], 40 | "dict": { 41 | "a": "str", 42 | "b": "str", 43 | "c": "str", 44 | }, 45 | "complex": { 46 | "nested_list": [["0_0", "0_1"], ["1_0", "1_1"]], 47 | "nested_dict": { 48 | "a": "b", 49 | "c": "d", 50 | }, 51 | }, 52 | 1: 2, # Just here to check they don't cause errors 53 | } 54 | 55 | mark_context_strings_safe(context) 56 | 57 | self.assertTrue(isinstance(context["str"], SafeText)) 58 | 59 | for value in context["list"]: 60 | self.assertTrue(isinstance(value, SafeText)) 61 | 62 | for key, value in context["dict"].items(): 63 | self.assertTrue(isinstance(key, str)) 64 | self.assertTrue(isinstance(value, SafeText)) 65 | 66 | complex_value = context["complex"] 67 | for key in complex_value.keys(): 68 | self.assertTrue(isinstance(key, str)) 69 | 70 | nested_list = complex_value["nested_list"] 71 | for sub_list in nested_list: 72 | for value in sub_list: 73 | self.assertTrue(isinstance(value, SafeText)) 74 | 75 | nested_dict = complex_value["nested_dict"] 76 | for key, value in nested_dict.items(): 77 | self.assertTrue(isinstance(key, str)) 78 | self.assertTrue(isinstance(value, SafeText)) 79 | -------------------------------------------------------------------------------- /docs/community/related-projects.md: -------------------------------------------------------------------------------- 1 | # Related projects 2 | 3 | Here are other projects that are related to django-pattern-library, and may be relevant if you’re looking to go further, or wanting to try out alternatives: 4 | 5 | ## Complementary packages 6 | 7 | To create and reuse components within Django templates: 8 | 9 | - [django-components](https://github.com/django-components/django-components) – Reusable UI components for Django, going further than template partials. 10 | - [slippers](https://github.com/mixxorz/slippers) – Reusable components for Django, without writing a single line of Python. 11 | - [JinjaX](https://jinjax.scaletti.dev/) – Write server-side components as single Jinja template files. Use them as HTML tags without doing any importing. 12 | - [django-viewcomponent](https://github.com/rails-inspire-django/django-viewcomponent) - Build reusable components in Django, inspired by Rails ViewComponent, the components built by django-viewcomponent can be used in both Django template or Python code. 13 | - [django-bird](https://github.com/joshuadavidthomas/django-bird) - reusable components in Django inspired by Flux from Laravel. 14 | - [dj-angles](https://github.com/adamghill/dj-angles) - HTML-like elements in Django templates in place of partials and tags. 15 | - [django-cotton](https://github.com/wrabit/django-cotton)- HTML-like syntax for Django template tags 16 | 17 | Tooling leveraging component-driven development: 18 | 19 | - [storybook-django](https://github.com/torchbox/storybook-django) – attempting to bridge the gap between React and Django, by bringing django-pattern-library patterns into Storybook stories. 20 | 21 | ## Alternatives 22 | 23 | - [Storybook](https://storybook.js.org/), and in particular [Storybook for Server](https://github.com/storybookjs/storybook/tree/master/app/server) – Storybook integration with server-rendered UI components. 24 | - [Pattern Lab](http://patternlab.io/) – PHP or Node pattern library, from which this project is heavily inspired. 25 | - [django-lookbook](https://github.com/rails-inspire-django/django-lookbook) - Empower your Django development with this pluggable app for creating a robust component library. Includes preview system, documentation engine, and parameter editor for building modular UI effortlessly. 26 | 27 | ## Pattern libraries based on Django 28 | 29 | Here are open-source projects that maintain pattern libraries for Django. 30 | 31 | With `django-pattern-library`: 32 | 33 | - [Wagtail](https://github.com/wagtail/wagtail) 34 | - [rca.ac.uk](https://github.com/torchbox/rca-wagtail-2019) 35 | - [torchbox.com](https://github.com/torchbox/wagtail-torchbox) 36 | - [buckinghamshire.gov.uk](https://github.com/Buckinghamshire-Digital-Service/buckinghamshire-council) 37 | - [Big Peach - PyATL meetup management system](https://github.com/pyatl/big-peach) 38 | 39 | Without `django-pattern-library`: 40 | 41 | - [Wagtail NHS.UK frontend](https://github.com/nhsuk/wagtail-nhsuk-frontend) 42 | - [consumerfinance.gov](https://github.com/cfpb/consumerfinance.gov) 43 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Django pattern library 2 | 3 | > UI pattern libraries for Django templates 4 | 5 | ## Features 6 | 7 | The [django-pattern-library](https://pypi.org/project/django-pattern-library/) package automates the maintenance of UI pattern libraries or styleguides for Django projects, and allows developers to experiment with Django templates without having to create Django views and models. 8 | 9 | - Create reusable patterns by creating Django templates files as usual. 10 | - All patterns automatically show up in the pattern library’s interface. 11 | - Define data as YAML files for the templates to render with the relevant Django context. 12 | - Override Django Templates tags as needed to mock the template’s dependencies. 13 | - Document your patterns with Markdown. 14 | - Experimental: support for Jinja templates. 15 | 16 | Here is a screenshot of the pattern library in action: 17 | 18 | [![Screenshot of the pattern library UI, with navigation, pattern rendering, and configuration](images/pattern-library-screenshot.webp)](images/pattern-library-screenshot.webp) 19 | 20 | ## Why you need this 21 | 22 | Pattern libraries will change your workflow for the better: 23 | 24 | - They help separate concerns, both in code, and between members of a development team. 25 | - If needed, they make it possible for UI development to happen before models and views are created. 26 | - They encourage code reuse – defining independent UI components, that can be reused across apps, or ported to other projects. 27 | - It makes it much simpler to test UI components – no need to figure out where they’re used across a site or app. 28 | 29 | ## Online demo 30 | 31 | The pattern library is dependent on Django for rendering – but also supports exporting as a static site if needed. Try out our online demo: 32 | 33 | - For a component, [accordion.html](https://torchbox.github.io/django-pattern-library/demo/pattern/patterns/molecules/accordion/accordion.html) 34 | - For a page-level template, [person_page.html](https://torchbox.github.io/django-pattern-library/demo/pattern/patterns/pages/people/person_page.html) 35 | 36 | ## Why this exists 37 | 38 | We want to make it possible for developers to maintain large pattern libraries with minimal fuss – no copy-pasting of templates between a static library and the “production” templates. 39 | 40 | There are a lot of alternative solutions for building pattern libraries, or to have [UI development playgrounds](https://www.componentdriven.org/). 41 | At [Torchbox](https://torchbox.com/) we mainly use Django and Wagtail, and we found it hard to maintain large libraries with those tools that have no awareness of Django Templates. 42 | This is our attempt to solve this issue – [Pattern Lab](http://patternlab.io/) goes Django! 43 | 44 | To learn more about how this package can be used, have a look at our talk: 45 | 46 | [Reusable UI components: A journey from React to Wagtail](https://www.youtube.com/watch?v=isrOufI7TKc) 47 | 48 | [![Reusable UI components: A journey from React to Wagtail](images/pattern-library-talk-youtube.webp)](https://www.youtube.com/watch?v=isrOufI7TKc) 49 | -------------------------------------------------------------------------------- /tests/static/main.css: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | } 4 | 5 | *, 6 | *::before, 7 | *::after { 8 | box-sizing: inherit; 9 | } 10 | 11 | .icon { 12 | fill: currentColor; 13 | } 14 | .icon--close { 15 | width: 24px; 16 | height: 24px; 17 | } 18 | .icon--cancel { 19 | width: 11px; 20 | height: 12px; 21 | } 22 | .icon--chevron-down { 23 | width: 23px; 24 | height: 14px; 25 | } 26 | .icon--checkmark { 27 | width: 12px; 28 | height: 10px; 29 | } 30 | 31 | .accordion__panel { 32 | padding: 20px; 33 | border-top: 1px solid #222222; 34 | } 35 | .accordion__panel:last-child { 36 | border-bottom: 1px solid #222222; 37 | } 38 | .accordion__toggle { 39 | display: flex; 40 | width: 100%; 41 | padding: 15px 0; 42 | align-items: baseline; 43 | justify-content: space-between; 44 | text-align: left; 45 | appearance: none; 46 | background-color: transparent; 47 | background-image: none; 48 | color: inherit; 49 | border: 0; 50 | cursor: pointer; 51 | } 52 | .accordion__title { 53 | margin-bottom: 0; 54 | line-height: 30px; 55 | } 56 | .accordion__title-inner { 57 | display: block; 58 | flex: 1; 59 | } 60 | .accordion__icon { 61 | color: navy; 62 | transform: rotate(360deg); 63 | transition: transform 0.25s ease-out; 64 | margin-left: 10px; 65 | } 66 | .accordion__toggle[aria-pressed='true'] .accordion__icon { 67 | transform: rotate(180deg); 68 | } 69 | .accordion__content { 70 | line-height: 1.25; 71 | max-width: 768px; 72 | padding-bottom: 5px; 73 | } 74 | 75 | .field { 76 | margin-bottom: 20px; 77 | } 78 | 79 | .field--errors { 80 | padding: 20px; 81 | margin-bottom: 20px; 82 | border: 1px dotted #f00; 83 | } 84 | 85 | .field__required { 86 | color: #f00; 87 | } 88 | .field__label, 89 | .field__label--multiple { 90 | display: block; 91 | margin-bottom: 5px; 92 | font-weight: 700; 93 | } 94 | .field__errors { 95 | margin-bottom: 10px; 96 | font-weight: 700; 97 | color: #f00; 98 | } 99 | .field__help { 100 | margin-top: 5px; 101 | } 102 | .field input:not([type='checkbox']):not([type='radio']):not([type='submit']), 103 | .field textarea, 104 | .field select { 105 | width: 100%; 106 | padding: 10px; 107 | border: 1px solid #141414; 108 | } 109 | 110 | .field select:not([multiple]) { 111 | background-color: #fff; 112 | padding: 0.5em 3.5em 0.5em 1em; 113 | appearance: none; 114 | background-image: 115 | linear-gradient(45deg, transparent 50%, #444 50%), 116 | linear-gradient(135deg, #444 50%, transparent 50%), 117 | linear-gradient(to right, rgba(20, 20, 20, 0.2), rgba(20, 20, 20, 0.2)); 118 | background-position: 119 | calc(100% - 20px) calc(1em + 2px), 120 | calc(100% - 15px) calc(1em + 2px), 121 | calc(100% - 2.5em) 0.5em; 122 | background-size: 123 | 5px 5px, 124 | 5px 5px, 125 | 1px 1.5em; 126 | background-repeat: no-repeat; 127 | } 128 | -------------------------------------------------------------------------------- /tests/settings/base.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 4 | 5 | PROJECT_DIR = os.path.dirname(os.path.abspath(__file__)) 6 | BASE_DIR = os.path.dirname(PROJECT_DIR) 7 | 8 | SECRET_KEY = "foobar" 9 | 10 | DEBUG = True 11 | 12 | ALLOWED_HOSTS = ["*"] 13 | 14 | INSTALLED_APPS = [ 15 | "django.contrib.auth", 16 | "django.contrib.contenttypes", 17 | "django.contrib.staticfiles", 18 | "pattern_library", 19 | "tests", 20 | ] 21 | 22 | STATIC_URL = "/static/" 23 | 24 | # This is where Django will put files collected from application directories 25 | # and custom direcotires set in "STATICFILES_DIRS" when 26 | # using "django-admin collectstatic" command. 27 | # https://docs.djangoproject.com/en/stable/ref/settings/#static-root 28 | STATIC_ROOT = os.path.join(BASE_DIR, "static") 29 | 30 | # This is where Django will look for static files outside the directories of 31 | # applications which are used by default. 32 | # https://docs.djangoproject.com/en/stable/ref/settings/#staticfiles-dirs 33 | STATICFILES_DIRS = [os.path.join(BASE_DIR, "tests", "static")] 34 | 35 | ROOT_URLCONF = "tests.urls" 36 | 37 | PATTERN_LIBRARY = { 38 | "TEMPLATE_DIR": os.path.join( 39 | os.path.dirname(os.path.abspath(__file__)), "templates" 40 | ), 41 | "SECTIONS": [ 42 | ("atoms", ["patterns/atoms"]), 43 | ("molecules", ["patterns/molecules"]), 44 | ("pages", ["patterns/pages"]), 45 | ("jinja", ["patterns_jinja/components"]), 46 | ("jinja pages", ["patterns_jinja/pages"]), 47 | ], 48 | } 49 | 50 | TEMPLATES = [ 51 | { 52 | "BACKEND": "django.template.backends.django.DjangoTemplates", 53 | "DIRS": [ 54 | "tests/templates", 55 | ], 56 | "APP_DIRS": True, 57 | "OPTIONS": { 58 | "context_processors": [ 59 | "django.contrib.auth.context_processors.auth", 60 | "django.template.context_processors.debug", 61 | "django.template.context_processors.i18n", 62 | "django.template.context_processors.media", 63 | "django.template.context_processors.static", 64 | "django.template.context_processors.tz", 65 | "django.contrib.messages.context_processors.messages", 66 | ], 67 | "builtins": ["pattern_library.loader_tags"], 68 | }, 69 | }, 70 | { 71 | "BACKEND": "django.template.backends.jinja2.Jinja2", 72 | "DIRS": [ 73 | "tests/jinja", 74 | ], 75 | "APP_DIRS": True, 76 | "OPTIONS": { 77 | "environment": "tests.jinja2.environment", 78 | "extensions": [ 79 | "jinja2.ext.do", 80 | "jinja2.ext.i18n", 81 | "jinja2.ext.loopcontrols", 82 | ], 83 | }, 84 | }, 85 | ] 86 | 87 | MIDDLEWARE = [ 88 | "django.middleware.security.SecurityMiddleware", 89 | "django.contrib.sessions.middleware.SessionMiddleware", 90 | "django.middleware.common.CommonMiddleware", 91 | "django.middleware.csrf.CsrfViewMiddleware", 92 | "django.contrib.auth.middleware.AuthenticationMiddleware", 93 | "django.contrib.messages.middleware.MessageMiddleware", 94 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 95 | ] 96 | 97 | # Preparation for Django 5.0 change. 98 | USE_TZ = False 99 | 100 | GITHUB_PAGES_EXPORT = False 101 | -------------------------------------------------------------------------------- /docs/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | u267B-blackuniversalrecyclingsymbol 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /docs/images/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | u267B-blackuniversalrecyclingsymbol 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /docs/guides/usage-tips.md: -------------------------------------------------------------------------------- 1 | # Usage tips 2 | 3 | The workflow of developing UI components in a pattern library can be quite different from one-off templates that are only rendered where they are used. Here are tips to make the most of it. 4 | 5 | ## Keep the pattern library in sync 6 | 7 | One of the upsides of having the pattern library built with Django is that the HTML templates can never go out of sync – but the data can! Make sure your template context and tag overrides keep in sync with your actual templates. This can for example be part of a code review checklist. 8 | 9 | You may also consider using the [`render_patterns` command](../reference/api.md#render_patterns) to have a baseline check the patterns don’t raise errors while rendering. 10 | 11 | ## Document your patterns 12 | 13 | Patterns support defining a custom `name` in YAML, as well as rendering fully-fledged documentation in Markdown. Create a file next to the template to document it, ensuring the filename has a `.md` extension (e.g., `call_to_action.md`): 14 | 15 | ```markdown 16 | This template can be used in different places. In streamfield block 17 | or directly in a page template. To use this template pass `call_to_action` into context. 18 | 19 | Example: 20 | 21 | {% include "patterns/molecules/cta/call_to_action.html" with call_to_action=call_to_action %} 22 | ``` 23 | 24 | ## Test faster 25 | 26 | One of the main points of maintaining a pattern library is to be able to work on UI components in isolation from where they are used, which is very useful when testing components – access them directly in the pattern library, rather than always having to figure out where they are used, and manually navigating to them. 27 | 28 | You can use the pattern library’s iframe view directly to test the component without the pattern library UI, but remember that the pattern will be displayed wrapped in the base template ([`PATTERN_BASE_TEMPLATE_NAME`](../reference/api.md#pattern_base_template_name)), which isn’t always truthful to how components are used in situ. 29 | 30 | A pattern library can also help with [automated tests](../guides/automated-tests.md). 31 | 32 | ## Iterate on components 33 | 34 | ### Back-end first 35 | 36 | Traditionally, Django development starts from models, and everything else is derived from the models’ structure. This is very natural from a back-end perspective – first define your data model, then the view(s) that reuse it, and finally templates. 37 | 38 | We generally recommend this approach, but keep in mind that: 39 | 40 | - With this workflow it’s natural to write templates that are heavily tied to the database structure, and as such not very reusable, and may be out of touch with visual design. 41 | - There can be work needed later on to reconcile the data structure as defined by the back-end, with what is mandated by the designs. 42 | 43 | To mitigate this risk, and overall make templates more reusable, take the time to massage data into basic structures that map better to visual representations. 44 | 45 | ### Front-end first 46 | 47 | Alternatively, the pattern library makes it possible to write templates without models and views. This can be very convenient if your project’s schedule requires this kind of progression. 48 | 49 | With this approach, keep in mind that: 50 | 51 | - When creating the template from UI principles, there will be assumptions made about the underlying data structures to be provided by Django models. Templates will be heavily tied to their visual design (which generally uses basic data structures like lists and mappings), and may be out of touch with the models once they are created. 52 | - There will be work to do to reconcile the data structure as defined in the UI components, with what is mandated by the models. 53 | 54 | Here as well, to mitigate this risk, and overall make templates more reusable, take the time to massage data into basic structures that map better to visual representations. 55 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v6 12 | - run: pipx install "poetry>=2.1.2,<3" 13 | - uses: actions/setup-python@v6 14 | with: 15 | python-version: '3.14' 16 | cache: 'poetry' 17 | - run: pip install tox 18 | - run: tox -e lint,py314-dj60 19 | test_compatibility: 20 | needs: test 21 | runs-on: ubuntu-latest 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | include: 26 | # Test with all supported Django versions, for all compatible Python versions. 27 | # See https://docs.djangoproject.com/en/dev/faq/install/#what-python-version-can-i-use-with-django for the official matrix. 28 | # Additionally test on Django’s main branch with the most recent Python version. 29 | - python: '3.10' 30 | toxenv: py310-dj42,py310-dj52 31 | - python: '3.11' 32 | toxenv: py311-dj42,py311-dj52 33 | - python: '3.12' 34 | toxenv: py312-dj42,py312-dj52,py312-dj60 35 | - python: '3.13' 36 | toxenv: py313-dj42,py313-dj52,py313-dj60 37 | - python: '3.14' 38 | toxenv: py314-dj42,py314-dj52,py314-djmain 39 | steps: 40 | - uses: actions/checkout@v6 41 | - run: pipx install "poetry>=2.1.2,<3" 42 | - uses: actions/setup-python@v6 43 | with: 44 | python-version: ${{ matrix.python }} 45 | allow-prereleases: true 46 | - run: pip install tox 47 | - run: tox -q 48 | env: 49 | TOXENV: ${{ matrix.toxenv }} 50 | build_site: 51 | needs: test 52 | runs-on: ubuntu-latest 53 | steps: 54 | - uses: actions/checkout@v6 55 | - uses: actions/setup-node@v6 56 | with: 57 | node-version-file: '.nvmrc' 58 | - run: npm ci --no-audit 59 | - run: npm run lint 60 | - run: npm run build 61 | - run: pipx install "poetry>=2.1.2,<3" 62 | - uses: actions/setup-python@v6 63 | with: 64 | python-version-file: pyproject.toml 65 | cache: 'poetry' 66 | - run: poetry install 67 | - run: poetry run django-admin runserver --settings=tests.settings.production --pythonpath=. & 68 | # Docs website build. 69 | - run: poetry run mkdocs build --strict 70 | # Demo website build. 71 | - run: wget --mirror --page-requisites --no-parent --no-verbose http://localhost:8000/django-pattern-library/demo/ 72 | - run: mv localhost:8000/django-pattern-library/demo site 73 | # Demo render_patterns. 74 | - run: poetry run django-admin render_patterns --settings=tests.settings.production --pythonpath=. --wrap-fragments --output=site/dpl-rendered-patterns 2>&1 >/dev/null | tee dpl-list.txt 75 | # Create an archive of render_patterns output so the build artifacts can be inspected easily. 76 | - run: mv dpl-list.txt site/dpl-rendered-patterns && tar -czvf site/dpl-rendered-patterns.tar.gz site/dpl-rendered-patterns 77 | # Package build, incl. publishing an experimental pre-release via GitHub Pages for builds on `main`. 78 | - run: cat pyproject.toml | awk '{sub(/^version = .+/,"version = \"0.0.0.dev\"")}1' > pyproject.toml.tmp && mv pyproject.toml.tmp pyproject.toml 79 | - run: poetry build 80 | - run: mv dist site 81 | - uses: actions/configure-pages@v4 82 | - uses: actions/upload-pages-artifact@v3 83 | with: 84 | path: site 85 | deploy_site: 86 | needs: build_site 87 | runs-on: ubuntu-latest 88 | permissions: 89 | pages: write 90 | id-token: write 91 | environment: 92 | name: github-pages 93 | url: ${{ steps.deployment.outputs.page_url }} 94 | if: github.event_name == 'push' && github.ref == 'refs/heads/main' 95 | steps: 96 | - uses: actions/deploy-pages@v4 97 | -------------------------------------------------------------------------------- /tests/tests/test_tags.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | from django.shortcuts import render 4 | from django.test import RequestFactory, SimpleTestCase 5 | 6 | from pattern_library import get_pattern_context_var_name 7 | 8 | from .utils import reverse 9 | 10 | 11 | class TagsTestCase(SimpleTestCase): 12 | def test_falsey_raw_values_for_tag_output(self): 13 | response = self.client.get( 14 | reverse( 15 | "pattern_library:render_pattern", 16 | kwargs={ 17 | "pattern_template_name": "patterns/atoms/tags_test_atom/tags_test_atom.html" 18 | }, 19 | ) 20 | ) 21 | self.assertContains(response, "SANDWICH") 22 | self.assertContains(response, "SANDNoneWICH") 23 | self.assertContains(response, "SAND0WICH") 24 | 25 | def test_default_html_override(self): 26 | response = self.client.get( 27 | reverse( 28 | "pattern_library:render_pattern", 29 | kwargs={ 30 | "pattern_template_name": "patterns/atoms/tags_test_atom/tags_test_atom.html" 31 | }, 32 | ) 33 | ) 34 | 35 | self.assertContains(response, "POTAexampleTO") 36 | self.assertContains(response, "POTAanother_exampleTO") 37 | self.assertContains(response, "POTAhttps://potato.comTO") 38 | 39 | def test_falsey_default_html_overide(self): 40 | response = self.client.get( 41 | reverse( 42 | "pattern_library:render_pattern", 43 | kwargs={ 44 | "pattern_template_name": "patterns/atoms/tags_test_atom/tags_test_atom.html" 45 | }, 46 | ) 47 | ) 48 | self.assertContains(response, "POTATO1") 49 | self.assertContains(response, "POTANoneTO2") 50 | self.assertContains(response, "POTA0TO3") 51 | 52 | 53 | class TagsTestFailCase(SimpleTestCase): 54 | def test_bad_default_html_warning(self): 55 | """ 56 | Test that the library raises a warning when passing a non-string `default_html` argument to `override_tag` 57 | in Django < 4.0 58 | """ 59 | with patch("django.VERSION", (3, 2, 0, "final", 0)): 60 | with self.assertWarns(Warning) as cm: 61 | template_name = ( 62 | "patterns/atoms/tags_test_atom/invalid_tags_test_atom.html.fail" 63 | ) 64 | request = RequestFactory().get("/") 65 | 66 | # Rendering the template with a non-string `default_html` argument will cause Django >= 4 to raise 67 | # a `TypeError`, which we need to catch and ignore in order to check that the warning is raised 68 | try: 69 | render( 70 | request, 71 | template_name, 72 | context={get_pattern_context_var_name(): True}, 73 | ) 74 | except TypeError: 75 | pass 76 | 77 | self.assertIn( 78 | "default_html argument to override_tag should be a string to ensure compatibility with Django", 79 | str(cm.warnings[0]), 80 | ) 81 | 82 | def test_bad_default_html_error(self): 83 | """ 84 | Test that the library raises a TypeError when passing a non-string `default_html` argument to `override_tag` 85 | in Django >= 4.0 86 | """ 87 | with patch("django.VERSION", (4, 2, 0, "final", 0)): 88 | with self.assertRaises(TypeError) as cm: 89 | template_name = ( 90 | "patterns/atoms/tags_test_atom/invalid_tags_test_atom.html.fail" 91 | ) 92 | request = RequestFactory().get("/") 93 | render( 94 | request, 95 | template_name, 96 | context={get_pattern_context_var_name(): True}, 97 | ) 98 | self.assertIn( 99 | "default_html argument to override_tag must be a string", 100 | str(cm.exception), 101 | ) 102 | -------------------------------------------------------------------------------- /tests/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.conf import settings 4 | from django.test import SimpleTestCase, override_settings 5 | 6 | from pattern_library.utils import ( 7 | get_pattern_config_str, 8 | get_renderer, 9 | get_template_dirs, 10 | ) 11 | 12 | 13 | class TestGetTemplateAncestors(SimpleTestCase): 14 | def setUp(self): 15 | self.renderer = get_renderer() 16 | 17 | def test_page(self): 18 | self.assertEqual( 19 | self.renderer.get_template_ancestors( 20 | "patterns/pages/test_page/test_page.html" 21 | ), 22 | [ 23 | "patterns/pages/test_page/test_page.html", 24 | "patterns/base_page.html", 25 | "patterns/base.html", 26 | ], 27 | ) 28 | 29 | def test_fragment(self): 30 | self.assertEqual( 31 | self.renderer.get_template_ancestors( 32 | "patterns/atoms/test_atom/test_atom.html" 33 | ), 34 | [ 35 | "patterns/atoms/test_atom/test_atom.html", 36 | ], 37 | ) 38 | 39 | def test_parent_template_from_variable(self): 40 | self.assertEqual( 41 | self.renderer.get_template_ancestors( 42 | "patterns/atoms/test_extends/extended.html", 43 | context={"parent_template_name": "patterns/base.html"}, 44 | ), 45 | [ 46 | "patterns/atoms/test_extends/extended.html", 47 | "patterns/base.html", 48 | ], 49 | ) 50 | 51 | 52 | class TestGetTemplateDirs(SimpleTestCase): 53 | def get_relative_template_dirs(self): 54 | """Make paths relative with a predefined root so we can use them in assertions.""" 55 | base = os.path.dirname(settings.BASE_DIR) 56 | dirs = get_template_dirs() 57 | return ["/".join(str(d).replace(base, "dpl").split("/")[-4:-1]) for d in dirs] 58 | 59 | @override_settings( 60 | TEMPLATES=[ 61 | { 62 | "BACKEND": "django.template.backends.django.DjangoTemplates", 63 | "APP_DIRS": True, 64 | }, 65 | ] 66 | ) 67 | def test_get_template_dirs_app_dirs(self): 68 | self.assertListEqual( 69 | self.get_relative_template_dirs(), 70 | [ 71 | "django/contrib/auth", 72 | "dpl/pattern_library", 73 | "dpl/tests", 74 | ], 75 | ) 76 | 77 | @override_settings( 78 | TEMPLATES=[ 79 | { 80 | "NAME": "one", 81 | "BACKEND": "django.template.backends.django.DjangoTemplates", 82 | "DIRS": [os.path.join(settings.BASE_DIR, "test_one", "templates")], 83 | "APP_DIRS": True, 84 | }, 85 | { 86 | "NAME": "two", 87 | "BACKEND": "django.template.backends.django.DjangoTemplates", 88 | "DIRS": [os.path.join(settings.BASE_DIR, "test_two", "templates")], 89 | }, 90 | ] 91 | ) 92 | def test_get_template_dirs_list_dirs(self): 93 | self.assertListEqual( 94 | self.get_relative_template_dirs(), 95 | [ 96 | "dpl/tests/test_one", 97 | "dpl/tests/test_two", 98 | "django/contrib/auth", 99 | "dpl/pattern_library", 100 | "dpl/tests", 101 | ], 102 | ) 103 | 104 | 105 | class TestGetPatternConfigStr(SimpleTestCase): 106 | def test_not_existing_template(self): 107 | result = get_pattern_config_str("doesnotexist") 108 | 109 | self.assertEqual(result, "") 110 | 111 | def test_atom_yaml(self): 112 | result = get_pattern_config_str("patterns/atoms/test_atom/test_atom.html") 113 | 114 | self.assertNotEqual(result, "") 115 | self.assertIn("atom_var value from test_atom.yaml", result) 116 | 117 | def test_atom_yml(self): 118 | result = get_pattern_config_str( 119 | "patterns/atoms/test_atom_yml/test_atom_yml.html" 120 | ) 121 | 122 | self.assertNotEqual(result, "") 123 | self.assertIn("atom_var value from test_atom.yml", result) 124 | -------------------------------------------------------------------------------- /tests/tests/test_context_modifiers.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from django.core.exceptions import ImproperlyConfigured 4 | from django.http import HttpRequest 5 | from django.test import SimpleTestCase 6 | 7 | from pattern_library import register_context_modifier 8 | from pattern_library.context_modifiers import registry 9 | from pattern_library.utils import render_pattern 10 | 11 | 12 | def accepts_context_only(context): 13 | pass 14 | 15 | 16 | def accepts_request_only(request): 17 | pass 18 | 19 | 20 | def modifier_1(context, request): 21 | context["foo"] = "foo" 22 | 23 | 24 | def modifier_2(context, request): 25 | context["foo"] = "bar" 26 | 27 | 28 | def modifier_3(context, request): 29 | context["beep"] = "boop" 30 | 31 | 32 | atom_template = "patterns/atoms/test_atom/test_atom.html" 33 | 34 | 35 | class ContextModifierTestCase(SimpleTestCase): 36 | maxDiff = None 37 | 38 | def setUp(self): 39 | registry.clear() 40 | 41 | def tearDown(self): 42 | registry.clear() 43 | 44 | def test_validation(self): 45 | with self.assertRaisesRegex(ImproperlyConfigured, "must be callables"): 46 | registry.register(0, template=atom_template) 47 | with self.assertRaisesRegex( 48 | ImproperlyConfigured, "must accept a 'request' keyword argument" 49 | ): 50 | registry.register(accepts_context_only, template=atom_template) 51 | with self.assertRaisesRegex( 52 | ImproperlyConfigured, "must accept a 'context' keyword argument" 53 | ): 54 | registry.register(accepts_request_only, template=atom_template) 55 | 56 | def test_registered_without_ordering(self): 57 | registry.register(modifier_1, template=atom_template) 58 | registry.register(modifier_2, template=atom_template) 59 | registry.register(modifier_3, template=atom_template) 60 | 61 | result = registry.get_for_template(atom_template) 62 | self.assertEqual( 63 | result, 64 | [ 65 | modifier_1, 66 | modifier_2, 67 | modifier_3, 68 | ], 69 | ) 70 | 71 | def test_registered_with_ordering(self): 72 | registry.register(modifier_1, template=atom_template, order=10) 73 | registry.register(modifier_2, template=atom_template, order=5) 74 | registry.register(modifier_3, template=atom_template, order=0) 75 | 76 | result = registry.get_for_template(atom_template) 77 | self.assertEqual( 78 | result, 79 | [ 80 | modifier_3, 81 | modifier_2, 82 | modifier_1, 83 | ], 84 | ) 85 | 86 | def test_registered_via_decorator(self): 87 | @register_context_modifier(order=100) 88 | def func_a(context, request): 89 | pass 90 | 91 | @register_context_modifier(order=50) 92 | def func_b(context, request): 93 | pass 94 | 95 | @register_context_modifier(template=atom_template) 96 | def func_c(context, request): 97 | pass 98 | 99 | @register_context_modifier(template="different_template.html", order=1) 100 | def func_x(context, request): # NOQA 101 | pass 102 | 103 | result = registry.get_for_template(atom_template) 104 | self.assertEqual( 105 | result, 106 | [ 107 | func_c, 108 | func_b, 109 | func_a, 110 | ], 111 | ) 112 | 113 | @mock.patch("pattern_library.utils.render_to_string") 114 | def test_applied_by_render_pattern(self, render_to_string): 115 | request = HttpRequest() 116 | registry.register(modifier_1) 117 | registry.register(modifier_2, template=atom_template) 118 | registry.register(modifier_3, template=atom_template) 119 | 120 | render_pattern(request, atom_template) 121 | render_to_string.assert_called_with( 122 | atom_template, 123 | request=request, 124 | context={ 125 | "atom_var": "atom_var value from test_atom.yaml", 126 | "is_pattern_library": True, 127 | "__pattern_library_tag_overrides": {}, 128 | "foo": "bar", 129 | "beep": "boop", 130 | }, 131 | ) 132 | -------------------------------------------------------------------------------- /pattern_library/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.http import Http404, HttpResponse 4 | from django.template.loader import get_template 5 | from django.utils.decorators import method_decorator 6 | from django.utils.html import escape 7 | from django.utils.safestring import mark_safe 8 | from django.views.decorators.clickjacking import xframe_options_sameorigin 9 | from django.views.decorators.csrf import csrf_exempt 10 | from django.views.generic.base import TemplateView 11 | 12 | from pattern_library import get_base_template_names, get_pattern_base_template_name 13 | from pattern_library.exceptions import PatternLibraryEmpty, TemplateIsNotPattern 14 | from pattern_library.utils import ( 15 | get_pattern_config, 16 | get_pattern_config_str, 17 | get_pattern_context, 18 | get_pattern_markdown, 19 | get_renderer, 20 | get_sections, 21 | is_pattern, 22 | render_pattern, 23 | ) 24 | 25 | 26 | class IndexView(TemplateView): 27 | http_method_names = ("get",) 28 | template_name = "pattern_library/index.html" 29 | 30 | def first_template_from_group(self, templates): 31 | try: 32 | return templates["templates_stored"][0] 33 | except IndexError: 34 | for template_group in templates["template_groups"].values(): 35 | return self.first_template_from_group(template_group) 36 | return None 37 | 38 | def get_first_template(self, templates): 39 | first_template = self.first_template_from_group(templates) 40 | if first_template: 41 | return first_template.origin.template_name 42 | 43 | sections = get_sections() 44 | if sections: 45 | raise PatternLibraryEmpty( 46 | "No templates found matching: '%s'" % str(sections) 47 | ) 48 | else: 49 | raise PatternLibraryEmpty( 50 | "No 'SECTIONS' found in the 'PATTERN_LIBRARY' setting" 51 | ) 52 | 53 | def get(self, request, pattern_template_name=None): 54 | # Get all pattern templates 55 | renderer = get_renderer() 56 | templates = renderer.get_pattern_templates() 57 | 58 | if pattern_template_name is None: 59 | # Just display the first pattern if a specific one isn't requested 60 | pattern_template_name = self.get_first_template(templates) 61 | 62 | if not is_pattern(pattern_template_name): 63 | raise Http404 64 | 65 | template = get_template(pattern_template_name) 66 | pattern_config = get_pattern_config(pattern_template_name) 67 | 68 | context = self.get_context_data() 69 | context["pattern_templates"] = templates 70 | context["pattern_template_name"] = pattern_template_name 71 | context["pattern_source"] = renderer.get_pattern_source(template) 72 | context["pattern_config"] = escape( 73 | get_pattern_config_str(pattern_template_name) 74 | ) 75 | context["pattern_name"] = pattern_config.get("name", pattern_template_name) 76 | context["pattern_markdown"] = get_pattern_markdown(pattern_template_name) 77 | 78 | return self.render_to_response(context) 79 | 80 | 81 | class RenderPatternView(TemplateView): 82 | http_method_names = ("get",) 83 | template_name = get_pattern_base_template_name() 84 | 85 | @method_decorator(xframe_options_sameorigin) 86 | def get(self, request, pattern_template_name=None): 87 | renderer = get_renderer() 88 | pattern_template_ancestors = renderer.get_template_ancestors( 89 | pattern_template_name, 90 | context=get_pattern_context(self.kwargs["pattern_template_name"]), 91 | ) 92 | pattern_is_fragment = set(pattern_template_ancestors).isdisjoint( 93 | set(get_base_template_names()) 94 | ) 95 | 96 | try: 97 | rendered_pattern = render_pattern(request, pattern_template_name) 98 | except TemplateIsNotPattern: 99 | raise Http404 100 | 101 | if pattern_is_fragment: 102 | context = self.get_context_data() 103 | context["pattern_library_rendered_pattern"] = mark_safe(rendered_pattern) 104 | return self.render_to_response(context) 105 | 106 | return HttpResponse(rendered_pattern) 107 | 108 | 109 | @csrf_exempt 110 | def render_pattern_api(request): 111 | data = json.loads(request.body.decode("utf-8")) 112 | template_name = data["template_name"] 113 | config = data["config"] 114 | 115 | try: 116 | rendered_pattern = render_pattern( 117 | request, template_name, allow_non_patterns=False, config=config 118 | ) 119 | except TemplateIsNotPattern: 120 | raise Http404 121 | 122 | return HttpResponse(rendered_pattern) 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [django-pattern-library](https://torchbox.github.io/django-pattern-library/) 2 | 3 | [![PyPI](https://img.shields.io/pypi/v/django-pattern-library.svg)](https://pypi.org/project/django-pattern-library/) [![PyPI downloads](https://img.shields.io/pypi/dm/django-pattern-library.svg)](https://pypi.org/project/django-pattern-library/) [![Build status](https://github.com/torchbox/django-pattern-library/workflows/CI/badge.svg)](https://github.com/torchbox/django-pattern-library/actions) 4 | 5 | > UI pattern libraries for Django templates. Try our [online demo](https://torchbox.github.io/django-pattern-library/demo/). 6 | 7 | ![Screenshot of the pattern library UI, with navigation, pattern rendering, and configuration](https://raw.githubusercontent.com/torchbox/django-pattern-library/main/.github/pattern-library-screenshot.webp) 8 | 9 | ## Features 10 | 11 | This package automates the maintenance of UI pattern libraries or styleguides for Django projects, and allows developers to experiment with Django templates without having to create Django views and models. 12 | 13 | - Create reusable patterns by creating Django templates files as usual. 14 | - All patterns automatically show up in the pattern library’s interface. 15 | - Define data as YAML files for the templates to render with the relevant Django context. 16 | - Override Django Templates tags as needed to mock the template’s dependencies. 17 | - Document your patterns with Markdown. 18 | - Experimental: support for Jinja templates. 19 | 20 | ## Why you need this 21 | 22 | Pattern libraries will change your workflow for the better: 23 | 24 | - They help separate concerns, both in code, and between members of a development team. 25 | - If needed, they make it possible for UI development to happen before models and views are created. 26 | - They encourage code reuse – defining independent UI components, that can be reused across apps, or ported to other projects. 27 | - It makes it much simpler to test UI components – no need to figure out where they’re used across a site or app. 28 | 29 | Learn more by watching our presentation – [Reusable UI components: A journey from React to Wagtail](https://www.youtube.com/watch?v=isrOufI7TKc). 30 | 31 | ## Online demo 32 | 33 | The pattern library is dependent on Django for rendering – but also supports exporting as a static site if needed. Try out our online demo: 34 | 35 | - For a component, [accordion.html](https://torchbox.github.io/django-pattern-library/demo/pattern/patterns/molecules/accordion/accordion.html) 36 | - For a page-level template, [person_page.html](https://torchbox.github.io/django-pattern-library/demo/pattern/patterns/pages/people/person_page.html) 37 | 38 | ## Documentation 39 | 40 | Documentation is available at [torchbox.github.io/django-pattern-library](https://torchbox.github.io/django-pattern-library/), with source files in the `docs` directory. 41 | 42 | - **[Getting started](https://torchbox.github.io/django-pattern-library/getting-started/)** 43 | - **Guides** 44 | - [Defining template context](https://torchbox.github.io/django-pattern-library/guides/defining-template-context/) 45 | - [Overriding template tags](https://torchbox.github.io/django-pattern-library/guides/overriding-template-tags/) 46 | - [Customizing template rendering](https://torchbox.github.io/django-pattern-library/guides/customizing-template-rendering/) 47 | - [Usage tips](https://torchbox.github.io/django-pattern-library/guides/usage-tips/) 48 | - **Reference** 49 | - [API & settings](https://torchbox.github.io/django-pattern-library/reference/api/) 50 | - [Known issues and limitations](https://torchbox.github.io/django-pattern-library/reference/known-issues/) 51 | 52 | ## Contributing 53 | 54 | See anything you like in here? Anything missing? We welcome all support, whether on bug reports, feature requests, code, design, reviews, tests, documentation, and more. Please have a look at our [contribution guidelines](https://github.com/torchbox/django-pattern-library/blob/main/CONTRIBUTING.md). 55 | 56 | If you want to set up the project on your own computer, the contribution guidelines also contain all of the setup commands. 57 | 58 | ### Nightly builds 59 | 60 | To try out the latest features before a release, we also create builds from every commit to `main`. Note we make no guarantee as to the quality of those pre-releases, and the pre-releases are overwritten on every build so shouldn’t be relied on for reproducible builds. [Download the latest `django_pattern_library-0.0.0.dev0-py3-none-any.whl`](http://torchbox.github.io/django-pattern-library/dist/django_pattern_library-0.0.0.dev0-py3-none-any.whl). 61 | 62 | ## Credits 63 | 64 | View the full list of [contributors](https://github.com/torchbox/django-pattern-library/graphs/contributors). [BSD](https://github.com/torchbox/django-pattern-library/blob/main/LICENSE) licensed. 65 | 66 | Project logo from [FxEmoji](https://github.com/mozilla/fxemoji). Documentation website built with [MkDocs](https://www.mkdocs.org/), and hosted in [GitHub Pages](https://pages.github.com/). 67 | -------------------------------------------------------------------------------- /pattern_library/management/commands/render_patterns.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from django.core.management.base import BaseCommand 4 | from django.template.loader import render_to_string 5 | from django.test.client import RequestFactory 6 | 7 | from pattern_library import get_base_template_names, get_pattern_base_template_name 8 | from pattern_library.utils import get_pattern_context, get_renderer, render_pattern 9 | 10 | 11 | class Command(BaseCommand): 12 | help = "Renders all django-pattern-library patterns to HTML files, in a directory structure." 13 | 14 | def add_arguments(self, parser): 15 | super().add_arguments(parser) 16 | parser.add_argument( 17 | "--output", 18 | "-o", 19 | action="store", 20 | dest="output_dir", 21 | default="dpl-rendered-patterns", 22 | help="Directory where to render your patterns", 23 | type=str, 24 | ) 25 | parser.add_argument( 26 | "--dry-run", 27 | action="store_true", 28 | help="Render the patterns without writing them to disk.", 29 | ) 30 | parser.add_argument( 31 | "--wrap-fragments", 32 | action="store_true", 33 | help="Render fragment patterns wrapped in the base template.", 34 | ) 35 | 36 | def handle(self, **options): 37 | self.verbosity = options["verbosity"] 38 | self.dry_run = options["dry_run"] 39 | self.wrap_fragments = options["wrap_fragments"] 40 | self.output_dir = options["output_dir"] 41 | 42 | renderer = get_renderer() 43 | templates = renderer.get_pattern_templates() 44 | 45 | factory = RequestFactory() 46 | request = factory.get("/") 47 | 48 | if self.verbosity >= 2: 49 | if self.dry_run: 50 | self.stderr.write( 51 | f"Target directory: {self.output_dir}. Dry run, not writing files to disk" 52 | ) 53 | else: 54 | self.stderr.write(f"Target directory: {self.output_dir}") 55 | 56 | if self.wrap_fragments: 57 | self.stderr.write("Writing fragment patterns wrapped in base template") 58 | 59 | # Resolve the output dir according to the directory the command is run from. 60 | parent_dir = Path.cwd().joinpath(self.output_dir) 61 | 62 | if not self.dry_run: 63 | parent_dir.mkdir(exist_ok=True) 64 | 65 | self.render_group(request, parent_dir, templates) 66 | 67 | def render_group(self, request, parent_dir: Path, pattern_templates): 68 | for template in pattern_templates["templates_stored"]: 69 | if self.verbosity >= 2: 70 | self.stderr.write(f"Pattern: {template.pattern_filename}") 71 | if self.verbosity >= 1: 72 | self.stderr.write(template.origin.template_name) 73 | 74 | render_path = parent_dir.joinpath(template.pattern_filename) 75 | rendered_pattern = self.render_pattern( 76 | request, template.origin.template_name 77 | ) 78 | 79 | if self.dry_run: 80 | if self.verbosity >= 2: 81 | self.stdout.write(rendered_pattern) 82 | else: 83 | render_path.write_text(rendered_pattern) 84 | 85 | if not pattern_templates["template_groups"]: 86 | return 87 | 88 | for pattern_type_group, pattern_templates in pattern_templates[ 89 | "template_groups" 90 | ].items(): 91 | if self.verbosity >= 2: 92 | self.stderr.write(f"Group: {pattern_type_group}") 93 | group_parent = parent_dir.joinpath(pattern_type_group) 94 | if not self.dry_run: 95 | group_parent.mkdir(exist_ok=True) 96 | self.render_group(request, group_parent, pattern_templates) 97 | 98 | def render_pattern(self, request, pattern_template_name): 99 | rendered_pattern = render_pattern(request, pattern_template_name) 100 | 101 | # If we don’t wrap fragments in the base template, we can simply render the pattern and return as-is. 102 | if not self.wrap_fragments: 103 | return rendered_pattern 104 | 105 | renderer = get_renderer() 106 | pattern_template_ancestors = renderer.get_template_ancestors( 107 | pattern_template_name, 108 | context=get_pattern_context(pattern_template_name), 109 | ) 110 | pattern_is_fragment = set(pattern_template_ancestors).isdisjoint( 111 | set(get_base_template_names()) 112 | ) 113 | 114 | if pattern_is_fragment: 115 | base_template = get_pattern_base_template_name() 116 | context = get_pattern_context(base_template) 117 | context["pattern_library_rendered_pattern"] = rendered_pattern 118 | return render_to_string(base_template, request=request, context=context) 119 | else: 120 | return rendered_pattern 121 | -------------------------------------------------------------------------------- /tests/tests/test_commands.py: -------------------------------------------------------------------------------- 1 | import io 2 | import shutil 3 | import tempfile 4 | from pathlib import Path 5 | 6 | from django.core.management import call_command 7 | from django.test import SimpleTestCase 8 | 9 | 10 | class RenderPatternsTests(SimpleTestCase): 11 | """Tests of the render_pattern command’s output, based on the test project’s templates""" 12 | 13 | def test_displays_patterns(self): 14 | stdout = io.StringIO() 15 | stderr = io.StringIO() 16 | call_command("render_patterns", dry_run=True, stdout=stdout, stderr=stderr) 17 | self.assertIn( 18 | """patterns/atoms/tags_test_atom/tags_test_atom.html 19 | patterns/atoms/test_atom/test_atom.html 20 | """, 21 | stderr.getvalue(), 22 | ) 23 | 24 | def test_verbose_output(self): 25 | stdout = io.StringIO() 26 | stderr = io.StringIO() 27 | call_command( 28 | "render_patterns", dry_run=True, stdout=stdout, stderr=stderr, verbosity=2 29 | ) 30 | self.assertIn( 31 | """Target directory: dpl-rendered-patterns. Dry run, not writing files to disk 32 | Group: atoms 33 | Group: icons 34 | Pattern: icon.html 35 | patterns/atoms/icons/icon.html 36 | """, 37 | stderr.getvalue(), 38 | ) 39 | self.assertIn( 40 | """""", 43 | stdout.getvalue(), 44 | ) 45 | 46 | def test_quiet_output(self): 47 | stdout = io.StringIO() 48 | stderr = io.StringIO() 49 | call_command( 50 | "render_patterns", dry_run=True, stdout=stdout, stderr=stderr, verbosity=0 51 | ) 52 | self.assertEqual(stdout.getvalue(), "") 53 | self.assertEqual(stderr.getvalue(), "") 54 | 55 | def test_shows_output_folder(self): 56 | stdout = io.StringIO() 57 | stderr = io.StringIO() 58 | temp = tempfile.gettempdir() 59 | call_command( 60 | "render_patterns", 61 | dry_run=True, 62 | stdout=stdout, 63 | stderr=stderr, 64 | output=temp, 65 | verbosity=2, 66 | ) 67 | self.assertIn(temp, stderr.getvalue()) 68 | 69 | def test_shows_wrap_fragment(self): 70 | stdout = io.StringIO() 71 | stderr = io.StringIO() 72 | call_command( 73 | "render_patterns", 74 | dry_run=True, 75 | wrap_fragments=True, 76 | stdout=stdout, 77 | stderr=stderr, 78 | verbosity=2, 79 | ) 80 | self.assertIn( 81 | "Writing fragment patterns wrapped in base template", stderr.getvalue() 82 | ) 83 | # Only testing a small subset of the output just to show patterns are wrapped. 84 | self.assertIn( 85 | """ 88 | 89 | 90 | 91 | 92 | """, 93 | stdout.getvalue(), 94 | ) 95 | 96 | def test_saves_with_template_filename(self): 97 | stdout = io.StringIO() 98 | stderr = io.StringIO() 99 | call_command( 100 | "render_patterns", dry_run=True, stdout=stdout, stderr=stderr, verbosity=2 101 | ) 102 | self.assertIn("Pattern: test_molecule.html", stderr.getvalue()) 103 | 104 | 105 | class RenderPatternsFileSystemTests(SimpleTestCase): 106 | """Tests of the render_pattern command’s file system changes, based on the test project’s templates""" 107 | 108 | def setUp(self): 109 | self.output = tempfile.mkdtemp() 110 | 111 | def tearDown(self): 112 | shutil.rmtree(self.output) 113 | 114 | def test_uses_output(self): 115 | stdout = io.StringIO() 116 | stderr = io.StringIO() 117 | modification_time_before = Path(self.output).stat().st_mtime 118 | call_command( 119 | "render_patterns", 120 | dry_run=False, 121 | stdout=stdout, 122 | stderr=stderr, 123 | output=self.output, 124 | ) 125 | self.assertNotEqual(Path(self.output).stat().st_mtime, modification_time_before) 126 | 127 | def test_uses_subfolders(self): 128 | stdout = io.StringIO() 129 | stderr = io.StringIO() 130 | call_command( 131 | "render_patterns", 132 | dry_run=False, 133 | stdout=stdout, 134 | stderr=stderr, 135 | output=self.output, 136 | ) 137 | subfolders = Path(self.output).iterdir() 138 | self.assertIn("atoms", [p.name for p in subfolders]) 139 | 140 | def test_outputs_html(self): 141 | stdout = io.StringIO() 142 | stderr = io.StringIO() 143 | call_command( 144 | "render_patterns", 145 | dry_run=False, 146 | stdout=stdout, 147 | stderr=stderr, 148 | output=self.output, 149 | ) 150 | html_files = Path(self.output).glob("**/*.html") 151 | self.assertIn("test_atom.html", [p.name for p in html_files]) 152 | -------------------------------------------------------------------------------- /docs/reference/known-issues.md: -------------------------------------------------------------------------------- 1 | # Known issues and limitations 2 | 3 | django-pattern-library has a few known limitations due to its design, which are worth knowing about when authoring templates or attempting to document them in the pattern library. 4 | 5 | ## Overriding filters is not supported 6 | 7 | See [#114](https://github.com/torchbox/django-pattern-library/issues/114) for Django Templates. PRs welcome! 8 | 9 | ## Can’t override context in a child template 10 | 11 | See [#8](https://github.com/torchbox/django-pattern-library/issues/8). 12 | 13 | If you have a `some_page.html`, `some_page.yaml`, and `include_me.html`, `include_me.html`, and `some_page.html` includes `include_me.html`. 14 | 15 | `some_page.yaml` with something like: 16 | 17 | ```yaml 18 | context: 19 | page: 20 | pk: 1 21 | title: 'my title' 22 | ``` 23 | 24 | and `include_me.yaml` with something like: 25 | 26 | ```yaml 27 | context: 28 | page: 29 | title: 'Title from include' 30 | ``` 31 | 32 | `Title from include` will appear on both patterns. It's impossible to override single key in `some_page.html` 33 | 34 | ## No support for pattern variations 35 | 36 | See [#87](https://github.com/torchbox/django-pattern-library/issues/87). There is currently no support for trying out a single component with different variations in context or tag overrides. 37 | 38 | This can be worked around by creating pattern-library-only templates, see [Multiple template variants](../guides/multiple-variants.md) 39 | 40 | ## Can’t mock each use of a template tag with different attributes 41 | 42 | See [#138](https://github.com/torchbox/django-pattern-library/issues/138). For example, with a template that uses the same tag many times like: 43 | 44 | ```django 45 | {% load wagtailcore_tags %} 46 | {% for link in primarynav %} 47 | {% with children=link.value.page.get_children.live.public.in_menu %} 48 |
49 | {% include_block link with has_children=children.exists nav_type="primary-nav" %} 50 | 58 |
59 | {% endwith %} 60 | {% endfor %} 61 | ``` 62 | 63 | This can’t be mocked for all usage of `include_block`. 64 | 65 | ## Jinja2 overrides 66 | 67 | There is experimental support, excluding overrides of arbitrary tags, functions, and filters. If you’re interested in this, please share your thoughts with us on [#180](https://github.com/torchbox/django-pattern-library/discussions/180). 68 | 69 | ## Past limitations 70 | 71 | ### Jinja2 support 72 | 73 | 🎉 This is now addressed as of v1.5.0, though with only experimental support, and no capability to override tags, functions, filters (see above). 74 | 75 | ### No way to specify objects that have attributes and support iteration 76 | 77 | 🎉 This is now addressed as of v0.5.0, with the [context modifiers in Python](../guides/defining-template-context.md#modifying-template-contexts-with-python) API. View our [pagination](../recipes/pagination.md) recipe. 78 | 79 | --- 80 | 81 | See [#10](https://github.com/torchbox/django-pattern-library/issues/10). It’s impossible to mock the context when a variable needs to support iteration _and_ attributes. Here is an example of this impossible case: 82 | 83 | ```django 84 | {% for result in search_results %} 85 | {# […] #} 86 | {% if search_results.paginator.count %} 87 | ``` 88 | 89 | ### Django form fields are not well supported 90 | 91 | 🎉 This is now addressed as of v0.5.0, with the [context modifiers in Python](../guides/defining-template-context.md#modifying-template-contexts-with-python) API. View our [forms and fields](../recipes/forms-and-fields.md) recipe. 92 | 93 | --- 94 | 95 | See [#113](https://github.com/torchbox/django-pattern-library/issues/113). If a template contains `{% for field in form %}` or even `{% if form %}`, then it's easy enough to render in django-pattern-library so long as we force the form to be null in the YAML context, and are happy not to have the form. 96 | 97 | If the form is rendered explicitly by field names, then it requires a lot more work, which can quickly become too much of a maintenance burden – for example creating deeply nested structures for form fields: 98 | 99 | ```yaml 100 | form: 101 | email: 102 | bound_field: 103 | field: 104 | widget: 105 | class: 106 | __name__: char_field 107 | ``` 108 | 109 | While this is in theory possible, it’s not a very desirable prospect. 110 | 111 | ### Can’t mock objects comparison by reference 112 | 113 | 🎉 This is now addressed as of v0.5.0, with the [context modifiers in Python](../guides/defining-template-context.md#modifying-template-contexts-with-python) API. 114 | 115 | --- 116 | 117 | With instances of models, the following works fine in vanilla Django, due to `item` and `page` being the same object: 118 | 119 | ```django 120 | {% if item == page %} 121 | ``` 122 | 123 | This can’t be mocked with the pattern library’s context mocking support. As a workaround, you can switch equality checks to using literals: 124 | 125 | ```django 126 | {% if item.id == page.id %} 127 | ``` 128 | -------------------------------------------------------------------------------- /docs/community/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 via GitHub’s [Report to repository admins](https://docs.github.com/en/communities/maintaining-your-safety-on-github/reporting-abuse-or-spam) feature, or to any of the named authors in the package’s [pyproject.toml](https://github.com/torchbox/django-pattern-library/blob/main/pyproject.toml). 63 | All complaints will be reviewed and investigated promptly and fairly. 64 | 65 | All community leaders are obligated to respect the privacy and security of the 66 | reporter of any incident. 67 | 68 | ## Enforcement Guidelines 69 | 70 | Community leaders will follow these Community Impact Guidelines in determining 71 | the consequences for any action they deem in violation of this Code of Conduct: 72 | 73 | ### 1. Correction 74 | 75 | **Community Impact**: Use of inappropriate language or other behavior deemed 76 | unprofessional or unwelcome in the community. 77 | 78 | **Consequence**: A private, written warning from community leaders, providing 79 | clarity around the nature of the violation and an explanation of why the 80 | behavior was inappropriate. A public apology may be requested. 81 | 82 | ### 2. Warning 83 | 84 | **Community Impact**: A violation through a single incident or series 85 | of actions. 86 | 87 | **Consequence**: A warning with consequences for continued behavior. No 88 | interaction with the people involved, including unsolicited interaction with 89 | those enforcing the Code of Conduct, for a specified period of time. This 90 | includes avoiding interactions in community spaces as well as external channels 91 | like social media. Violating these terms may lead to a temporary or 92 | permanent ban. 93 | 94 | ### 3. Temporary Ban 95 | 96 | **Community Impact**: A serious violation of community standards, including 97 | sustained inappropriate behavior. 98 | 99 | **Consequence**: A temporary ban from any sort of interaction or public 100 | communication with the community for a specified period of time. No public or 101 | private interaction with the people involved, including unsolicited interaction 102 | with those enforcing the Code of Conduct, is allowed during this period. 103 | Violating these terms may lead to a permanent ban. 104 | 105 | ### 4. Permanent Ban 106 | 107 | **Community Impact**: Demonstrating a pattern of violation of community 108 | standards, including sustained inappropriate behavior, harassment of an 109 | individual, or aggression toward or disparagement of classes of individuals. 110 | 111 | **Consequence**: A permanent ban from any sort of public interaction within 112 | the community. 113 | 114 | ## Attribution 115 | 116 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 117 | version 2.0, available at 118 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 119 | 120 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 121 | enforcement ladder](https://github.com/mozilla/diversity). 122 | 123 | [homepage]: https://www.contributor-covenant.org 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | https://www.contributor-covenant.org/faq. Translations are available at 127 | https://www.contributor-covenant.org/translations. 128 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing guidelines 2 | 3 | This document contains information for anyone wishing to contribute to the project. 4 | 5 | ## Installation 6 | 7 | The repo includes a simple test application that can be run to develop the pattern library itself. Give it a try by [opening django-pattern-library in Gitpod](https://gitpod.io/#https://github.com/torchbox/django-pattern-library), or follow the instructions below for a local setup. 8 | 9 | First, clone the repo: 10 | 11 | ```sh 12 | git clone git@github.com:torchbox/django-pattern-library.git 13 | cd django-pattern-library 14 | ``` 15 | 16 | Once you have the code, there are several ways of running the project: 17 | 18 | - [In a VS Code devcontainer](#vs-code-devcontainer) 19 | - [In Docker, via docker-compose](#docker-compose) 20 | - [Locally, with Poetry](#run-locally-with-poetry) 21 | 22 | ### VS Code devcontainer 23 | 24 | For users of Docker and VS Code, there is a [devcontainer](https://code.visualstudio.com/docs/remote/containers) setup included in the repository 25 | that will automatically install the Python dependencies and start the frontend tooling. 26 | 27 | Once the container is built, open a terminal with VS Code and run `django-admin runserver` and click the URL (normally http://127.0.0.1:8000/) to open the app in your browser. You'll see a 404 page, because there's nothing at `/`, add `pattern-libary/` to the end of the URL to view the demo app. 28 | 29 | ### `docker-compose` 30 | 31 | First [install Docker and docker-compose](https://docs.docker.com/compose/install/), and make sure Docker is started. Then: 32 | 33 | ```sh 34 | # Install the front-end tooling in the docker container: 35 | docker-compose run frontend npm ci 36 | # Bring up the web container and run the front-end tooling in watch mode: 37 | docker-compose up 38 | # Run the development server: 39 | docker-compose exec web django-admin runserver 0.0.0.0:8000 40 | ``` 41 | 42 | Once the server is started, the pattern library will be available at `http://localhost:8000/`. 43 | 44 | ### Run locally with Poetry 45 | 46 | We use [Poetry](https://python-poetry.org/docs/) to manage Python dependencies, so make sure you've got it installed. 47 | 48 | Then you can install the dependencies and run the test app: 49 | 50 | ```sh 51 | poetry install 52 | # Start the server for testing: 53 | poetry run django-admin runserver --settings=tests.settings.dev --pythonpath=. 54 | # Or to try out the render_patterns command: 55 | poetry run django-admin render_patterns --settings=tests.settings.dev --pythonpath=. --dry-run --verbosity 2 56 | ``` 57 | 58 | ## Front-end tooling 59 | 60 | If you want to make changes to the front-end assets (located in the `pattern_library/static/pattern_library/src` folder), you'll need to ensure the tooling is set up in order to build the assets. 61 | 62 | If you are using Docker, you will already have the tooling set up and running in watch mode. You can view the logs with `docker-compose logs frontend` from your host machine. 63 | 64 | Otherwise, we recommend using [`nvm`](https://github.com/nvm-sh/nvm): 65 | 66 | ```sh 67 | # Install the correct version of Node 68 | nvm install 69 | # Install Node dependencies locally 70 | npm install 71 | # Build the assets 72 | npm run build 73 | # Watch files and build as needed 74 | npm run start 75 | ``` 76 | 77 | ## Documentation 78 | 79 | The project’s documentation website is built with [MkDocs](https://www.mkdocs.org/). 80 | 81 | ```sh 82 | # One-off build. 83 | poetry run mkdocs build --strict 84 | # Rebuild the docs as you work on them 85 | poetry run mkdocs serve 86 | ``` 87 | 88 | ## Running the tests 89 | 90 | To run the python tests, use the script in the root of the repo: 91 | 92 | ```sh 93 | poetry run ./runtests.py 94 | ``` 95 | 96 | To run the tests using different Python versions, use [`tox`](https://tox.readthedocs.io/). Note your Python versions will need to be installed on your machine, for example with [pyenv](https://github.com/pyenv/pyenv). 97 | 98 | ## Writing tests 99 | 100 | There is a simple test pattern library app in the `tests/` folder. The tests modules themselves and are `tests/tests`. 101 | 102 | ## Code review 103 | 104 | Create a pull request with your changes so that it can be code reviewed by a maintainer. Ensure that you give a summary with the purpose of the change and any steps that the reviewer needs to take to test your work. Please make sure to provide unit tests for your work. 105 | 106 | ## Releasing a new version 107 | 108 | On the `main` branch: 109 | 110 | 1. Bump the release number in `pyproject.toml` 111 | 2. Update the CHANGELOG 112 | 3. Commit and tag the release: 113 | ```sh 114 | git commit -m "Updates for version 0.1.14" 115 | git tag -a v0.1.14 -m "Release version v0.1.14" 116 | git push --tags 117 | ``` 118 | 4. Check that your working copy is clean by running: 119 | ```sh 120 | git clean -dxn -e __pycache__ 121 | ``` 122 | Any files returned by this command should be removed before continuing to prevent them being included in the build. 123 | 5. Install the locked versions of the `node` dependencies and run the production build. 124 | 125 | You can either do this directly on your local machine: 126 | 127 | ```sh 128 | npm ci 129 | npm run build 130 | ``` 131 | 132 | Or, via the docker container: 133 | 134 | ```sh 135 | docker-compose run frontend npm ci 136 | docker-compose run frontend npm run-script build 137 | ``` 138 | 139 | 6. Package the new version using `poetry build` 140 | 141 | 7. Test the newly-built package: 142 | Find the file ending in `.whl` in the `dist` directory 143 | Copy it to a test local build. 144 | Run this to install it on the test build: 145 | 146 | ```sh 147 | pip install django_pattern_library-0.2.6-py3-none-any.whl 148 | ``` 149 | 150 | Verify that the pattern library is working as you expect it to on your local build. 151 | 152 | 8. Upload the latest version to PyPI (requires credentials, ask someone named in the [maintainers listed on PyPI](https://pypi.org/project/django-pattern-library/)): `poetry publish` 153 | --------------------------------------------------------------------------------