├── .gitignore
├── tests
├── README.md
├── visual
│ ├── badge_upstream.html
│ ├── badge.html
│ ├── button.html
│ ├── button_upstream.html
│ ├── progress.html
│ ├── alert.html
│ ├── alert_upstream.html
│ └── progress_upstream.html
└── visual.js
├── elements.js
├── helpers
└── helpers.js
├── .editorconfig
├── elements
├── button-group.js
├── badge.js
├── progress.js
├── alert.js
└── button.js
├── rollup.config.js
├── .circleci
└── config.yml
├── LICENSE
├── package.json
├── styles
├── type.js
└── colors.js
├── README.md
├── CONTRIBUTING.md
└── demo
└── index.html
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | test-results
3 | dist
4 |
--------------------------------------------------------------------------------
/tests/README.md:
--------------------------------------------------------------------------------
1 | # Test overview
2 |
3 | ## Visual
4 |
5 | * based on https://github.com/Polymer/pwa-starter-kit/blob/master/test/integration/visual.js
6 |
--------------------------------------------------------------------------------
/elements.js:
--------------------------------------------------------------------------------
1 | export * from './elements/alert.js';
2 | export * from './elements/badge.js';
3 | export * from './elements/button-group.js';
4 | export * from './elements/button.js';
5 | export * from './elements/progress.js';
6 |
--------------------------------------------------------------------------------
/helpers/helpers.js:
--------------------------------------------------------------------------------
1 | import {render} from 'lit-html/lit-html.js';
2 |
3 | export function renderToHead(style) {
4 | let fragment = document.createDocumentFragment();
5 | render(style, fragment);
6 | document.head.appendChild(fragment);
7 | }
8 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = LF
5 | indent_style = tab
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 |
10 | [*.{json,md,yml}]
11 | indent_style = space
12 | indent_size = 2
13 |
--------------------------------------------------------------------------------
/elements/button-group.js:
--------------------------------------------------------------------------------
1 | import { LitElement, html, css } from 'lit-element';
2 |
3 | export default class BsButtonGroup extends LitElement {
4 | static get styles() {
5 | return css`
6 | :host {
7 | display: inline-flex;
8 | vertical-align: middle;
9 | }
10 |
11 | ::slotted(bs-button:not(:last-child)) {
12 | --bs-border-top-right-radius: 0;
13 | --bs-border-bottom-right-radius: 0;
14 | }
15 |
16 | ::slotted(bs-button:not(:first-child)) {
17 | --bs-border-top-left-radius: 0;
18 | --bs-border-bottom-left-radius: 0;
19 | }
20 | `;
21 | }
22 |
23 | render() {
24 | return html`
25 |
26 | `;
27 | }
28 | }
29 |
30 | customElements.define('bs-button-group', BsButtonGroup);
31 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import rimraf from 'rimraf';
2 | import minify from 'rollup-plugin-babel-minify';
3 | import resolve from 'rollup-plugin-node-resolve';
4 | import filesize from 'rollup-plugin-filesize';
5 | const outFolder = 'dist';
6 |
7 | rimraf.sync(outFolder);
8 |
9 | export default [
10 | {
11 | input: 'elements.js',
12 | output: {
13 | file: `${outFolder}/elements.bundled.js`,
14 | format: 'iife',
15 | name: 'BootstrapElements'
16 | },
17 | plugins: [resolve()]
18 | },
19 | {
20 | input: 'elements.js',
21 | output: {
22 | file: `${outFolder}/elements.bundled.min.js`,
23 | format: 'iife',
24 | name: 'BootstrapElements'
25 | },
26 | plugins: [resolve(), minify({ comments: false }), filesize({showBrotliSize: true})]
27 | }
28 | ];
29 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2 # https://circleci.com/docs/2.0/language-javascript/
2 | jobs:
3 | build:
4 | docker:
5 | - image: circleci/node:8-browsers # https://hub.docker.com/r/circleci/node/
6 |
7 | steps:
8 | - checkout
9 |
10 | - restore_cache:
11 | keys:
12 | - dependencies-{{ checksum "package.json" }}
13 | # fallback to using the latest cache if no exact match is found
14 | - dependencies-
15 |
16 | - run: npm install
17 |
18 | - save_cache:
19 | paths:
20 | - node_modules
21 | key: dependencies-{{ checksum "package.json" }}
22 |
23 | - run:
24 | name: visual
25 | command: npm run test -- --reporter mocha-junit-reporter --reporter-options mochaFile=./test-results/visual/result.xml
26 |
27 | - store_artifacts:
28 | path: test-results
29 |
30 | - store_test_results:
31 | path: test-results
32 |
33 | - run:
34 | name: build
35 | command: npm run build
36 |
37 | - store_artifacts:
38 | path: dist
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 morbidick
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/tests/visual/badge_upstream.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | Primary
13 | Secondary
14 | Success
15 | Danger
16 | Warning
17 | Info
18 | Light
19 | Dark
20 |
21 | Primary
22 | Secondary
23 | Success
24 | Danger
25 | Warning
26 | Info
27 | Light
28 | Dark
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/tests/visual/badge.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
15 |
20 |
21 |
22 |
23 |
24 | Primary
25 | Secondary
26 | Success
27 | Danger
28 | Warning
29 | Info
30 | Light
31 | Dark
32 |
33 | Primary
34 | Secondary
35 | Success
36 | Danger
37 | Warning
38 | Info
39 | Light
40 | Dark
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/tests/visual/button.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
15 |
20 |
21 |
22 |
23 |
24 | Primary
25 | Secondary
26 | Success
27 | Danger
28 | Warning
29 | Info
30 | Light
31 | Dark
32 |
33 | Primary
34 | Secondary
35 | Success
36 | Danger
37 | Warning
38 | Info
39 | Light
40 | Dark
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@morbidick/bootstrap",
3 | "version": "0.4.2",
4 | "description": "Rewrite of bootstrap components as web components",
5 | "license": "MIT",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/morbidick/bootstrap-webcomponents"
9 | },
10 | "main": "dist/elements.bundled.js",
11 | "module": "elements.js",
12 | "dependencies": {
13 | "@morbidick/lit-element-notify": "^1.0.0",
14 | "lit-element": "^2.0.1",
15 | "lit-html": "^1.0.0",
16 | "rimraf": "^2.6.3"
17 | },
18 | "devDependencies": {
19 | "@polymer/iron-demo-helpers": "^3.0.2",
20 | "@webcomponents/webcomponentsjs": "^2.2.6",
21 | "chai": "^4.2.0",
22 | "fs-extra": "^7.0.1",
23 | "mocha": "^5.0.0",
24 | "mocha-junit-reporter": "^1.18.0",
25 | "pixelmatch": "^4.0.2",
26 | "pngjs": "^3.3.3",
27 | "polyserve": "^0.27.15",
28 | "puppeteer": "1.6.2",
29 | "rollup": "^0.59.4",
30 | "rollup-plugin-babel-minify": "^5.0.0",
31 | "rollup-plugin-filesize": "^4.0.1",
32 | "rollup-plugin-node-resolve": "^3.4.0"
33 | },
34 | "scripts": {
35 | "start": "npm run serve",
36 | "serve": "polyserve --npm --module-resolution=node",
37 | "test": "mocha tests/visual.js",
38 | "build": "rollup -c",
39 | "prepack": "npm test && npm run build"
40 | },
41 | "files": [
42 | "dist",
43 | "elements",
44 | "elements.js",
45 | "helpers",
46 | "styles"
47 | ]
48 | }
49 |
--------------------------------------------------------------------------------
/tests/visual/button_upstream.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | Primary
13 | Secondary
14 | Success
15 | Danger
16 | Warning
17 | Info
18 | Light
19 | Dark
20 |
21 | Primary
22 | Secondary
23 | Success
24 | Danger
25 | Warning
26 | Info
27 | Light
28 | Dark
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/styles/type.js:
--------------------------------------------------------------------------------
1 | // https://getbootstrap.com/docs/4.1/content/typography/
2 | import {css, unsafeCSS, html} from 'lit-element';
3 | import {primary} from './colors.js';
4 |
5 | export const fontFamilySansSerif = unsafeCSS`-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`;
6 | export const fontFamilyMonospace = unsafeCSS`SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace`;
7 | export const fontStyle = css`
8 | :root {
9 | font-family: var(--bs-body-font-family, ${fontFamilySansSerif});
10 | line-height: 1.5;
11 | color: var(--bs-body-font-color, #212529);
12 | }
13 |
14 | h1, h2, h3, h4, h5, h6 {
15 | margin-top: 1.5em;
16 | margin-bottom: 0.5em;
17 | font-weight: 500;
18 | line-height: 1.2;
19 | }
20 |
21 | h1 { font-size: 2.5rem; }
22 | h2 { font-size: 2rem; }
23 | h3 { font-size: 1.75rem; }
24 | h4 { font-size: 1.5rem; }
25 | h5 { font-size: 1.25rem; }
26 | h6 { font-size: 1rem; }
27 |
28 | p {
29 | margin-top: 0;
30 | margin-bottom: 1rem;
31 | }`
32 |
33 | export const linkStyle = css`
34 | a {
35 | color: var(--bs-link-color, ${primary.color});
36 | text-decoration: none;
37 | }
38 | a:hover, a:focus {
39 | text-decoration: underline;
40 | }`
41 |
42 | export const hrStyle = css`
43 | hr {
44 | margin-top: 1rem;
45 | margin-bottom: 1rem;
46 | border: 0;
47 | border-top: 1px solid currentColor;
48 | opacity: 0.1;
49 | }`
50 |
51 | export default html`
52 |
57 | `;
58 |
--------------------------------------------------------------------------------
/tests/visual/progress.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
15 |
24 |
25 |
26 |
27 |
28 | 25%
29 |
30 | 75%
31 |
32 |
33 | primary
34 | secondary
35 | success
36 | info
37 | warning
38 | danger
39 | light
40 | dark
41 |
42 | 10%
43 |
44 | 50%
45 |
46 | 100%
47 |
48 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Bootstrap Web Components
2 |
3 | [](https://circleci.com/gh/morbidick/bootstrap-webcomponents)
4 | [](https://www.npmjs.com/package/@morbidick/bootstrap)
5 | [](https://bundlephobia.com/result?p=@morbidick/bootstrap)
6 |
7 | > ## 🛠 Status: In Development
8 | > Components get implemented as needed. PRs welcome!
9 |
10 | Rewrite of bootstrap components as web components. See [the demo](demo/index.html) for all implemented features.
11 |
12 | ## Motivation
13 |
14 | This isn't just a webcomponents wrapper around the bootstrap css, this aims to be a clean and simple rewrite easily understandable to everyone new to the webcomponents standards.
15 |
16 | * No build chain / preprocessors
17 | * allow easy theming via css custom properties
18 | * provide a clear api to end users in the form of attributes
19 | * Use web standards over big libraries where possible (even if that means loosing compatibility eg. dialog element)
20 | * Provide support to projects to include polyfills and buildsteps for the above points
21 |
22 | ## Install
23 |
24 | ```bash
25 | npm install @morbidick/bootstrap
26 | ```
27 |
28 | ## Usage
29 |
30 | Import into your module script:
31 |
32 | ```javascript
33 | import { BsButton, BsBadge } from "@morbidick/bootstrap/elements.js"
34 | ```
35 |
36 | Alternatively, load a bundled version from CDN:
37 |
38 | ```html
39 |
40 | ```
41 |
42 | Use it in your web page:
43 |
44 | ```html
45 | Click Me
46 | warning
47 | ```
48 |
49 | ## Development
50 |
51 | View the [contributing notes](CONTRIBUTING.md) for instructions and further resources.
52 |
--------------------------------------------------------------------------------
/elements/badge.js:
--------------------------------------------------------------------------------
1 | import { LitElement, html, css } from 'lit-element';
2 | import colors from '../styles/colors.js';
3 |
4 |
5 | export default class BsBadge extends LitElement {
6 | static get properties() {
7 | return {
8 | href: {
9 | type: String,
10 | reflect: true,
11 | },
12 | theme: {
13 | type: String,
14 | reflect: true,
15 | },
16 | }
17 | }
18 |
19 | constructor() {
20 | super();
21 | this.theme = 'secondary';
22 | }
23 |
24 | static get styles() {
25 | return [
26 | css`
27 | :host {
28 | display: inline-block;
29 | }
30 | :host([hidden]) {
31 | display: hidden;
32 | }
33 | a, span {
34 | display: inline-block;
35 | padding: .25em .4em;
36 | border-top-left-radius: var(--bs-border-top-left-radius, .25em);
37 | border-top-right-radius: var(--bs-border-top-right-radius, .25em);
38 | border-bottom-left-radius: var(--bs-border-bottom-left-radius, .25em);
39 | border-bottom-right-radius: var(--bs-border-bottom-right-radius, .25em);
40 |
41 | transition: filter .15s ease-in-out;
42 | color: var(--bs-badge-color);
43 | background-color: var(--bs-badge-background-color);
44 |
45 | font-size: .75em;
46 | font-weight: 700;
47 | line-height: 1;
48 | text-align: center;
49 | white-space: nowrap;
50 | vertical-align: baseline;
51 | text-decoration: none;
52 | }
53 | a:hover, a:active, a:focus {
54 | text-decoration: none;
55 | background-color: var(--bs-badge-hover-background-color)
56 | }
57 | :host([theme~="pill"]) > * {
58 | padding-right: .6em;
59 | padding-left: .6em;
60 | border-radius: 10em;
61 | }`,
62 | ...colors.map(({selector, color, contrast, hoverbg}) => css`
63 | :host(${selector}) {
64 | --bs-badge-background-color: ${color};
65 | --bs-badge-color: ${contrast};
66 | --bs-badge-hover-background-color: ${hoverbg};
67 | }`
68 | )
69 | ];
70 | };
71 |
72 | render() {
73 | return html`
74 | ${this.href ? html` `: html` `}
75 | `;}
76 | }
77 |
78 | customElements.define('bs-badge', BsBadge);
79 |
--------------------------------------------------------------------------------
/tests/visual/alert.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
15 |
20 |
21 |
22 |
23 | This is a primary alert with an example link . Give it a click if you like.
24 | This is a secondary alert with an example link . Give it a click if you like.
25 | This is a success alert with an example link . Give it a click if you like.
26 | This is a danger alert with an example link . Give it a click if you like.
27 | This is a warning alert with an example link . Give it a click if you like.
28 | This is a info alert with an example link . Give it a click if you like.
29 | This is a light alert with an example link . Give it a click if you like.
30 | This is a dark alert with an example link . Give it a click if you like.
31 |
32 | This is a primary dismissable alert.
33 | This is a secondary dismissable alert.
34 | This is a success dismissable alert.
35 | This is a danger dismissable alert.
36 | This is a warning dismissable alert.
37 | This is a info dismissable alert.
38 | This is a light dismissable alert.
39 | This is a dark dismissable alert.
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/elements/progress.js:
--------------------------------------------------------------------------------
1 | import { LitElement, html, css } from 'lit-element';
2 | import colors, { primary } from '../styles/colors.js';
3 |
4 | export default class BsProgress extends LitElement {
5 | static get properties() {
6 | return {
7 | theme: {
8 | type: String,
9 | reflect: true,
10 | },
11 | value: {
12 | type: Number,
13 | reflect: true,
14 | },
15 | maxValue: {
16 | type: Number,
17 | reflect: true,
18 | },
19 | }
20 | }
21 |
22 | constructor() {
23 | super();
24 | this.value = 0;
25 | this.maxValue = 100;
26 | }
27 |
28 | static get styles() {
29 | return [
30 | css`
31 | :host {
32 | display: flex;
33 | height: 1rem;
34 | overflow: hidden;
35 | font-size: .75rem;
36 | background-color: var(--bs-progress-default-color, #e9ecef);
37 | border-radius: .25rem;
38 | }
39 | :host([hidden]) {
40 | display: hidden;
41 | }
42 | #bar {
43 | display: flex;
44 | flex-direction: column;
45 | justify-content: center;
46 | color: var(--bs-progress-color, ${primary.contrast});
47 | text-align: center;
48 | white-space: nowrap;
49 | background-color: var(--bs-progress-background-color, ${primary.color});
50 | transition: width .6s ease;
51 | }
52 | :host([theme~="striped"]) #bar {
53 | background-image: linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);
54 | background-size: 1rem 1rem;
55 | }
56 | @keyframes stripes {
57 | from { background-position: 1rem 0; }
58 | to { background-position: 0 0; }
59 | }
60 | :host([theme~="animated"]) #bar {
61 | animation: stripes 1s linear infinite;
62 | }
63 | @media (prefers-reduced-motion: reduce) {
64 | :host([theme~="animated"]) #bar {
65 | animation: none;
66 | }
67 | }
68 | `,
69 | ...colors.map(({selector, color, contrast}) => css`
70 | :host(${selector}) {
71 | --bs-progress-background-color: ${color};
72 | --bs-progress-color: ${contrast};
73 | }`
74 | )
75 | ];
76 | };
77 |
78 | get $bar() {
79 | return this.shadowRoot.querySelector('#bar');
80 | }
81 |
82 | updated(changed) {
83 | if (changed.has('value') || changed.has('maxValue')) {
84 | this.$bar.style.width = `${this.value/this.maxValue*100}%`;
85 | }
86 | }
87 |
88 | render() {
89 | return html`
90 |
96 |
97 |
98 | `;}
99 | }
100 |
101 | customElements.define('bs-progress', BsProgress);
102 |
--------------------------------------------------------------------------------
/elements/alert.js:
--------------------------------------------------------------------------------
1 | import { LitElement, html, css } from 'lit-element';
2 | import colors from '../styles/colors.js';
3 |
4 | export default class BsAlert extends LitElement {
5 | static get properties() {
6 | return {
7 | dismissable: {
8 | type: Boolean,
9 | reflect: true,
10 | },
11 | theme: {
12 | type: String,
13 | reflect: true,
14 | },
15 | }
16 | }
17 |
18 | constructor() {
19 | super();
20 | this.theme = 'secondary';
21 | }
22 |
23 | connectedCallback() {
24 | super.connectedCallback();
25 | if (!this.hasAttribute('role')) {
26 | this.setAttribute('role', 'alert');
27 | }
28 | }
29 |
30 | static get styles() {
31 | return [
32 | css`
33 | :host {
34 | display: block;
35 | }
36 | :host([hidden]) {
37 | display: hidden;
38 | }
39 | :host {
40 | position: relative;
41 | padding: .75rem 1.25rem;
42 | margin-bottom: 1rem;
43 | border: 1px solid transparent;
44 | border-radius: .25rem;
45 |
46 | background-color: var(--bs-alert-background-color);
47 | color: var(--bs-alert-color);
48 | border-color: var(--bs-alert-border-color);
49 | }
50 | :host([dismissable]) {
51 | padding-right: 4em;
52 | }
53 | :host(:not([dismissable])) button {
54 | display: none;
55 | }
56 | button {
57 | position: absolute;
58 | top: 0;
59 | right: 0;
60 | padding: .75rem 1.25rem;
61 | border: 0;
62 |
63 | cursor: pointer;
64 | color: inherit;
65 | background-color: transparent;
66 | font-size: 1.5rem;
67 | font-weight: 700;
68 | line-height: 1;
69 | text-shadow: 0 1px 0 #fff;
70 | opacity: .5;
71 | }
72 | button:hover{
73 | color: #000;
74 | text-decoration: none;
75 | opacity: .75;
76 | }
77 | ::slotted(a) {
78 | font-weight: 700;
79 | }
80 | /* important seems to be back to override parent scope */
81 | ::slotted(:first-child) {
82 | margin-top: 0 !important;
83 | }
84 | ::slotted(:last-child) {
85 | margin-bottom: 0 !important;
86 | }
87 | `,
88 | ...colors.map(({selector, alertcolor, alertbg, alertborder, alertlinkcolor}) => css`
89 | :host(${selector}) {
90 | --bs-alert-background-color: ${alertbg};
91 | --bs-alert-color: ${alertcolor};
92 | --bs-alert-border-color: ${alertborder};
93 | --bs-link-color: ${alertlinkcolor};
94 | }
95 | `)
96 | ]
97 | }
98 |
99 | render() {
100 | return html`
101 |
102 |
106 | ×
107 |
108 | `;
109 | }
110 |
111 | remove() {
112 | super.remove();
113 | this.dispatchEvent(new CustomEvent('dismissed', {
114 | bubbles: false,
115 | composed: true,
116 | }));
117 | }
118 | }
119 |
120 | customElements.define('bs-alert', BsAlert);
121 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to contribute
2 |
3 | ## Communicating
4 |
5 | * [polymer slack](https://bit.ly/polymerslack) `#bootstrap`
6 |
7 | ## Setup
8 |
9 | * Install dependencies `npm install`
10 | * Run dev Server with `npm start` and open your browser on [localhost:8081/demo](http://localhost:8081/demo)
11 | * Run the [tests](tests) with `npm test`
12 | * To produce a bundle run `npm run build`
13 |
14 | ## Implementing a new compoment
15 |
16 | * write down a list of all theme and functional variants in `tests/component_upstream.html`
17 | * evaluate native elements (e.g. dialog for modals or progress for the progress bar)
18 | * design a simple api surface (e.g. setting the `href` or `toggle` attribute to the button component changes the underlying element) in `tests/component.html`
19 | * open an issue (more eyes more ideas ;) )
20 | * implement the dom and functional parts
21 | * try to keep it simple
22 | * prefer `slots` over attributes/properties
23 | * provide sane defaults (a11y and user experience wise)
24 | * implement the styles
25 | * have a look at the [upstream scss](https://github.com/twbs/bootstrap/blob/v4-dev/scss/_buttons.scss)
26 | * have a look at the resulting css in [your browser](http://localhost:8081/tests/button_upstream.html) (`npm start` first)
27 | * write the base css first, prefix css custom proberties with `bs`
28 | * for performance reasons implement static css that selects on host attributes
29 | * for theme variants loop over `styles/colors.js`
30 | * run the screenshot tests `npm test`, see the results in [test-results](test-results)
31 | * push early and iterate often (most components so far took me about 3 implementations until it felt right)
32 | * extra goal: implement functional browser tests
33 |
34 | ## Resources
35 |
36 | ### Bootstrap
37 |
38 | * [components](https://getbootstrap.com/docs/4.1/components/)
39 | * [variables](https://github.com/twbs/bootstrap/blob/v4-dev/scss/_variables.scss)
40 |
41 | ### Web components
42 |
43 | #### Basics
44 |
45 | * [web components introduction](https://www.webcomponents.org/introduction)
46 | * [lit element docs](https://github.com/Polymer/lit-element#minimal-example)
47 | * [lit-html docs](https://polymer.github.io/lit-html/)
48 |
49 |
50 | #### Best Practices
51 |
52 | * [aria authoring practices](https://www.w3.org/TR/wai-aria-practices-1.1/)
53 | * [Google Dev Custom Element Best Practices](https://developers.google.com/web/fundamentals/web-components/best-practices)
54 | * [ChromeLabs howto components](https://github.com/GoogleChromeLabs/howto-components)
55 | * [Form Participation API](https://docs.google.com/document/d/1JO8puctCSpW-ZYGU8lF-h4FWRIDQNDVexzHoOQ2iQmY/edit?pli=1)
56 |
57 | #### Other libraries
58 |
59 | * [Elix](https://github.com/elix/elix)
60 | * [Vaadin Elements](https://github.com/search?q=topic%3Awebcomponents+org%3Avaadin&type=Repositories)
61 | * [Github Elements](https://github.com/search?q=topic%3Aweb-components+org%3Agithub&type=Repositories)
62 | * [predix ui](https://www.predix-ui.com/#/elements)
63 |
64 | #### Styling
65 |
66 | * [native css reset](https://caniuse.com/#search=appearance)
67 | * [vaadin themeable mixin](https://github.com/vaadin/vaadin-themable-mixin)
68 | * [::part and ::theme spec](https://drafts.csswg.org/css-shadow-parts-1/)
69 | * [CSS Modules](https://github.com/w3c/webcomponents/issues/759) and [constructible Stylesheets](https://github.com/WICG/construct-stylesheets/blob/gh-pages/explainer.md)
70 | * [color calculations in pure css](https://css-tricks.com/switch-font-color-for-different-backgrounds-with-css/)
71 |
--------------------------------------------------------------------------------
/tests/visual.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs-extra');
2 | const path = require('path');
3 | const puppeteer = require('puppeteer');
4 | const pixelmatch = require('pixelmatch');
5 | const expect = require('chai').expect;
6 | const {startServer} = require('polyserve');
7 | const PNG = require('pngjs').PNG;
8 |
9 | const testDir = `tests/visual`;
10 | const screenshotDir = `test-results/visual/screenshots`;
11 |
12 | describe('implementation visually matches original bootstrap', function() {
13 | this.timeout(5000);
14 | let polyserve, browser, page;
15 |
16 | // This is ran when the suite starts up.
17 | before(async function() {
18 | // This is where you would substitute your python or Express server or whatever.
19 | polyserve = await startServer({port:4000, root:path.join(__dirname, '..'), moduleResolution:'node'})
20 |
21 | // Create the test directory if needed.
22 | await fs.ensureDir(screenshotDir);
23 | });
24 |
25 | // This is ran when the suite is done. Stop your server here.
26 | after((done) => polyserve.close(done));
27 |
28 | // This is ran before every test. It's where you start a clean browser.
29 | beforeEach(async function() {
30 | browser = await puppeteer.launch();
31 | page = await browser.newPage();
32 | page.setViewport({width: 1024, height: 768});
33 | });
34 |
35 | // This is ran after every test; clean up after your browser.
36 | afterEach(() => browser.close());
37 |
38 | it('Buttons', async function() {
39 | return takeAndCompareScreenshot(page, 'button');
40 | });
41 |
42 | it('Badges', async function() {
43 | return takeAndCompareScreenshot(page, 'badge');
44 | });
45 |
46 | it('Alerts', async function() {
47 | return takeAndCompareScreenshot(page, 'alert');
48 | });
49 |
50 | it('Progress bar', async function() {
51 | return takeAndCompareScreenshot(page, 'progress');
52 | });
53 | });
54 |
55 | // - page is a reference to the Puppeteer page.
56 | // - route is the path you're loading
57 | async function takeAndCompareScreenshot(page, route) {
58 | const filePath = `${screenshotDir}/${route}.png`;
59 | const filePathUpstream = `${screenshotDir}/${route}_upstream.png`;
60 | const filePathDiff = `${screenshotDir}/${route}_diff.png`;
61 |
62 | // take rebuild screenshot
63 | await page.goto(`http://127.0.0.1:4000/${testDir}/${route}.html`, {waitUntil: 'networkidle2'});
64 | await page.screenshot({path: filePath, fullPage: true});
65 |
66 | // take upstream screenshot
67 | await page.goto(`http://127.0.0.1:4000/${testDir}/${route}_upstream.html`, {waitUntil: 'networkidle2'});
68 | await page.screenshot({path: filePathUpstream, fullPage: true});
69 |
70 | // Test to see if it's right.
71 | return compareScreenshots(filePath, filePathUpstream, filePathDiff);
72 | }
73 |
74 | function compareScreenshots(file1, file2, diffPath) {
75 | return new Promise((resolve, reject) => {
76 | const img1 = fs.createReadStream(file1).pipe(new PNG()).on('parsed', doneReading);
77 | const img2 = fs.createReadStream(file2).pipe(new PNG()).on('parsed', doneReading);
78 |
79 | let filesRead = 0;
80 | function doneReading() {
81 | // Wait until both files are read.
82 | if (++filesRead < 2) return;
83 |
84 | // The files should be the same size.
85 | expect(img1.width, 'image widths are the same').equal(img2.width);
86 | expect(img1.height, 'image heights are the same').equal(img2.height);
87 |
88 | // Do the visual diff.
89 | const diff = new PNG({width: img1.width, height: img1.height});
90 | const numDiffPixels = pixelmatch(
91 | img1.data, img2.data, diff.data, img1.width, img1.height,
92 | {threshold: 0.15});
93 |
94 | // write the diff
95 | diff.pack().pipe(fs.createWriteStream(diffPath));
96 |
97 | // The files should look the same.
98 | expect(numDiffPixels, 'number of different pixels').equal(0);
99 | resolve();
100 | }
101 | });
102 | }
103 |
--------------------------------------------------------------------------------
/tests/visual/alert_upstream.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | This is a primary alert with
an example link . Give it a click if you like.
13 |
14 |
15 | This is a secondary alert with
an example link . Give it a click if you like.
16 |
17 |
18 | This is a success alert with
an example link . Give it a click if you like.
19 |
20 |
21 | This is a danger alert with
an example link . Give it a click if you like.
22 |
23 |
24 | This is a warning alert with
an example link . Give it a click if you like.
25 |
26 |
27 | This is a info alert with
an example link . Give it a click if you like.
28 |
29 |
30 | This is a light alert with
an example link . Give it a click if you like.
31 |
32 |
33 | This is a dark alert with
an example link . Give it a click if you like.
34 |
35 |
36 |
37 | This is a primary dismissable alert.
38 |
39 | ×
40 |
41 |
42 |
43 | This is a secondary dismissable alert.
44 |
45 | ×
46 |
47 |
48 |
49 | This is a success dismissable alert.
50 |
51 | ×
52 |
53 |
54 |
55 | This is a danger dismissable alert.
56 |
57 | ×
58 |
59 |
60 |
61 | This is a warning dismissable alert.
62 |
63 | ×
64 |
65 |
66 |
67 | This is a info dismissable alert.
68 |
69 | ×
70 |
71 |
72 |
73 | This is a light dismissable alert.
74 |
75 | ×
76 |
77 |
78 |
79 | This is a dark dismissable alert.
80 |
81 | ×
82 |
83 |
84 |
85 |
86 |
87 |
--------------------------------------------------------------------------------
/tests/visual/progress_upstream.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
9 |
19 |
20 |
21 |
22 |
25 |
28 |
31 |
34 |
37 |
38 |
42 |
46 |
50 |
54 |
58 |
62 |
66 |
70 |
71 |
75 |
79 |
83 |
87 |
91 |
92 |
96 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/styles/colors.js:
--------------------------------------------------------------------------------
1 | import {unsafeCSS} from 'lit-element';
2 |
3 | export const primary = {
4 | selector: unsafeCSS`[theme~="primary"]`,
5 | color: unsafeCSS`var(--bs-primary-color, #007bff)`,
6 | focusring: unsafeCSS`var(--bs-primary-focusring-color, #007bff50)`,
7 | hoverbg: unsafeCSS`var(--bs-primary-hover-background-color, #0069d9)`,
8 | contrast: unsafeCSS`var(--bs-primary-contrast-color, #fff)`,
9 | alertcolor: unsafeCSS`var(--bs-primary-alert-color, #004085)`,
10 | alertbg: unsafeCSS`var(--bs-primary-alert-background-color, #cce5ff)`,
11 | alertborder: unsafeCSS`var(--bs-primary-alert-border-color, #b8daff)`,
12 | alertlinkcolor: unsafeCSS`var(--bs-primary-alert-link-color, #002752)`,
13 |
14 | }
15 | export const secondary = {
16 | selector: unsafeCSS`[theme~="secondary"]`,
17 | color: unsafeCSS`var(--bs-secondary-color, #6c757d)`,
18 | focusring: unsafeCSS`var(--bs-secondary-focusring-color, #6c757d50)`,
19 | hoverbg: unsafeCSS`var(--bs-secondary-hover-background-color, #5a6268)`,
20 | contrast: unsafeCSS`var(--bs-secondary-contrast-color, #fff)`,
21 | alertcolor: unsafeCSS`var(--bs-secondary-alert-color, #383d41)`,
22 | alertbg: unsafeCSS`var(--bs-secondary-alert-background-color, #e2e3e5)`,
23 | alertborder: unsafeCSS`var(--bs-secondary-alert-border-color, #d6d8db)`,
24 | alertlinkcolor: unsafeCSS`var(--bs-secondary-alert-link-color, #202326)`,
25 | }
26 | export const success = {
27 | selector: unsafeCSS`[theme~="success"]`,
28 | color: unsafeCSS`var(--bs-success-color, #28a745)`,
29 | focusring: unsafeCSS`var(--bs-success-focusring-color, #28a74550)`,
30 | hoverbg: unsafeCSS`var(--bs-success-hover-background-color, #218838)`,
31 | contrast: unsafeCSS`var(--bs-success-contrast-color, #fff)`,
32 | alertcolor: unsafeCSS`var(--bs-success-alert-color, #155724)`,
33 | alertbg: unsafeCSS`var(--bs-success-alert-background-color, #d4edda)`,
34 | alertborder: unsafeCSS`var(--bs-success-alert-border-color, #c3e6cb)`,
35 | alertlinkcolor: unsafeCSS`var(--bs-success-alert-link-color, #0b2e13)`,
36 | }
37 | export const danger = {
38 | selector: unsafeCSS`[theme~="danger"]`,
39 | color: unsafeCSS`var(--bs-danger-color, #dc3545)`,
40 | focusring: unsafeCSS`var(--bs-danger-focusring-color, #dc354550)`,
41 | hoverbg: unsafeCSS`var(--bs-danger-hover-background-color, #c82333)`,
42 | contrast: unsafeCSS`var(--bs-danger-contrast-color, #fff)`,
43 | alertcolor: unsafeCSS`var(--bs-danger-alert-color, #721c24)`,
44 | alertbg: unsafeCSS`var(--bs-danger-alert-background-color, #f8d7da)`,
45 | alertborder: unsafeCSS`var(--bs-danger-alert-border-color, #f5c6cb)`,
46 | alertlinkcolor: unsafeCSS`var(--bs-danger-alert-link-color, #491217)`,
47 | }
48 | export const warning = {
49 | selector: unsafeCSS`[theme~="warning"]`,
50 | color: unsafeCSS`var(--bs-warning-color, #ffc107)`,
51 | focusring: unsafeCSS`var(--bs-warning-focusring-color, #ffc10750)`,
52 | hoverbg: unsafeCSS`var(--bs-warning-hover-background-color, #e0a800)`,
53 | contrast: unsafeCSS`var(--bs-warning-contrast-color, #212529)`,
54 | alertcolor: unsafeCSS`var(--bs-warning-alert-color, #856404)`,
55 | alertbg: unsafeCSS`var(--bs-warning-alert-background-color, #fff3cd)`,
56 | alertborder: unsafeCSS`var(--bs-warning-alert-border-color, #ffeeba)`,
57 | alertlinkcolor: unsafeCSS`var(--bs-warning-alert-link-color, #533f03)`,
58 | }
59 | export const info = {
60 | selector: unsafeCSS`[theme~="info"]`,
61 | color: unsafeCSS`var(--bs-info-color, #17a2b8)`,
62 | focusring: unsafeCSS`var(--bs-info-focusring-color, #17a2b850)`,
63 | hoverbg: unsafeCSS`var(--bs-info-hover-background-color, #138496)`,
64 | contrast: unsafeCSS`var(--bs-info-contrast-color, #fff)`,
65 | alertcolor: unsafeCSS`var(--bs-info-alert-color, #0c5460)`,
66 | alertbg: unsafeCSS`var(--bs-info-alert-background-color, #d1ecf1)`,
67 | alertborder: unsafeCSS`var(--bs-info-alert-border-color, #bee5eb)`,
68 | alertlinkcolor: unsafeCSS`var(--bs-info-alert-link-color, #062c33)`,
69 | }
70 | export const light = {
71 | selector: unsafeCSS`[theme~="light"]`,
72 | color: unsafeCSS`var(--bs-light-color, #f8f9fa)`,
73 | focusring: unsafeCSS`var(--bs-light-focusring-color, #f8f9fa50)`,
74 | hoverbg: unsafeCSS`var(--bs-light-hover-background-color, #e2e6ea)`,
75 | contrast: unsafeCSS`var(--bs-light-contrast-color, #212529)`,
76 | alertcolor: unsafeCSS`var(--bs-light-alert-color, #818182)`,
77 | alertbg: unsafeCSS`var(--bs-light-alert-background-color, #fefefe)`,
78 | alertborder: unsafeCSS`var(--bs-light-alert-border-color, #fdfdfe)`,
79 | alertlinkcolor: unsafeCSS`var(--bs-light-alert-link-color, #686868)`,
80 | }
81 | export const dark = {
82 | selector: unsafeCSS`[theme~="dark"]`,
83 | color: unsafeCSS`var(--bs-dark-color, #343a40)`,
84 | focusring: unsafeCSS`var(--bs-dark-focusring-color, #343a4050)`,
85 | hoverbg: unsafeCSS`var(--bs-dark-hover-background-color, #23272b)`,
86 | contrast: unsafeCSS`var(--bs-dark-contrast-color, #fff)`,
87 | alertcolor: unsafeCSS`var(--bs-dark-alert-color, #1b1e21)`,
88 | alertbg: unsafeCSS`var(--bs-dark-alert-background-color, #d6d8d9)`,
89 | alertborder: unsafeCSS`var(--bs-dark-alert-border-color, #c6c8ca)`,
90 | alertlinkcolor: unsafeCSS`var(--bs-dark-alert-link-color, #040505)`,
91 | };
92 |
93 | export default [
94 | primary,
95 | secondary,
96 | success,
97 | info,
98 | warning,
99 | danger,
100 | light,
101 | dark,
102 | ];
103 |
--------------------------------------------------------------------------------
/elements/button.js:
--------------------------------------------------------------------------------
1 | import { LitElement, html, css } from 'lit-element';
2 | import { LitNotify } from '@morbidick/lit-element-notify';
3 | import colors from '../styles/colors.js';
4 |
5 | export default class BsButton extends LitNotify(LitElement) {
6 | static get properties() {
7 | return {
8 | // set button state to active
9 | active: {
10 | type: Boolean,
11 | reflect: true,
12 | notify: true,
13 | },
14 | // disable the button
15 | disabled: {
16 | type: Boolean,
17 | reflect: true,
18 | },
19 | // set to an url to get an anchor element with button styling
20 | href: {
21 | type: String,
22 | reflect: true,
23 | },
24 | theme: {
25 | type: String,
26 | reflect: true,
27 | },
28 | // set to true to act as a toggle button
29 | toggle: {
30 | type: Boolean,
31 | reflect: true,
32 | },
33 | // the button type (default: button)
34 | type: {
35 | type: String,
36 | reflect: true,
37 | },
38 | }
39 | }
40 |
41 | constructor() {
42 | super();
43 | this.active = false;
44 | this.disabled = false;
45 | this.theme = 'secondary';
46 | this.type = 'button';
47 | }
48 |
49 | static get styles() {
50 | return [
51 | css`
52 | :host(:not([hidden])) {
53 | display: inline-block;
54 | }
55 | :host > *:not([disabled]) {
56 | cursor: pointer;
57 | }
58 | :host > *:focus {
59 | outline: none;
60 | }
61 | a, button {
62 | box-sizing: border-box;
63 | display: inline-block;
64 | padding: .375em .75em;
65 | border: 1px solid transparent;
66 | border-top-left-radius: var(--bs-border-top-left-radius, .25em);
67 | border-top-right-radius: var(--bs-border-top-right-radius, .25em);
68 | border-bottom-left-radius: var(--bs-border-bottom-left-radius, .25em);
69 | border-bottom-right-radius: var(--bs-border-bottom-right-radius, .25em);
70 |
71 | font-size: 1rem;
72 | line-height: 1.5;
73 | text-align: center;
74 | user-select: none;
75 | vertical-align: middle;
76 | white-space: nowrap;
77 | text-decoration: none;
78 |
79 | background-color: transparent;
80 | transition:
81 | color .15s ease-in-out,
82 | background-color .15s ease-in-out,
83 | border-color .15s ease-in-out,
84 | box-shadow .15s ease-in-out,
85 | filter .15s ease-in-out;
86 | }
87 | /* reset firefox button focus style */
88 | button::-moz-focus-inner {
89 | border: 0;
90 | }
91 | :host > [active]:not([disabled]) {
92 | filter: brightness(0.85);
93 | }
94 | :host > [disabled] {
95 | opacity: 0.65;
96 | }
97 | :host([theme~="small"]) > * {
98 | font-size: .875rem;
99 | }
100 | :host([theme~="large"]) > * {
101 | font-size: 1.25rem;
102 | }
103 | *:focus:not([disabled]) {
104 | box-shadow: 0 0 0 .2rem var(--bs-button-focusring-color);
105 | }
106 | :host(:not([theme~="outline"])) > * {
107 | background-color: var(--bs-button-background-color);
108 | color: var(--bs-button-color);
109 | }
110 | :host(:not([theme~="outline"])) > *:hover:not([disabled]) {
111 | background-color: var(--bs-button-hover-background-color);
112 | }
113 | :host([theme~="outline"]) > * {
114 | color: var(--bs-button-background-color);
115 | border-color: var(--bs-button-background-color);
116 | }
117 | :host([theme~="outline"]) > *:hover:not([disabled]), :host([theme~="outline"]) > [active] {
118 | background-color: var(--bs-button-background-color);
119 | color: var(--bs-button-color);
120 | }
121 | :host([theme~="outline"]) > *:hover:not([active]) {
122 | filter: none;
123 | }
124 | `,
125 | ...colors.map(({selector, color, contrast, focusring, hoverbg}) => css`
126 | :host(${selector}) {
127 | --bs-button-background-color: ${color};
128 | --bs-button-color: ${contrast};
129 | --bs-button-focusring-color: ${focusring};
130 | --bs-button-hover-background-color: ${hoverbg};
131 | }`
132 | )
133 | ]
134 | }
135 |
136 | render() {
137 | return html`
138 | ${this.href ? html` `:
139 | !this.toggle ? html` `:
140 | html`
148 |
149 |
150 | `}
151 | `;
152 | }
153 |
154 | /**
155 | * Toggle the button state
156 | */
157 | _toggle() {
158 | if (this.disabled)
159 | return;
160 | this.active = !this.active;
161 | }
162 |
163 | _toggleClickHandler(event) {
164 | event.preventDefault();
165 | this._toggle();
166 | }
167 |
168 | _toggleKeypressHandler(event) {
169 | if (event.altKey)
170 | return;
171 |
172 | switch (event.code) {
173 | case "Enter":
174 | case "Space":
175 | event.preventDefault();
176 | this._toggle();
177 | break;
178 | default:
179 | return;
180 | }
181 | }
182 |
183 | /**
184 | * Pass external clicks to the internal button
185 | */
186 | click() {
187 | this.$button.click();
188 | }
189 |
190 | /**
191 | * Pass external focuses to the internal button
192 | */
193 | focus() {
194 | this.$button.focus();
195 | }
196 |
197 | /**
198 | * Expose internal button
199 | */
200 | get $button() {
201 | return this.shadowRoot.querySelector('#button');
202 | }
203 | }
204 |
205 | customElements.define('bs-button', BsButton);
206 |
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Bootstrap Webcomponents
9 |
10 |
11 |
12 |
24 |
33 |
34 |
35 |
36 |
37 |
Bootstrap Webcomponents
38 |
44 |
45 |
46 |
47 | Default
48 | Primary
49 | Secondary
50 | Success
51 | Danger
52 | Warning
53 | Info
54 | Light
55 | Dark
56 |
57 |
58 |
59 |
outline
60 |
61 |
62 | Primary
63 | Secondary
64 | Success
65 | Danger
66 | Warning
67 | Info
68 | Light
69 | Dark
70 |
71 |
72 |
73 |
sizes
74 |
75 |
76 | small
77 | regular
78 | large
79 |
80 |
81 |
82 |
states
83 |
84 |
85 | disabled
86 | active
87 | active outline
88 | toggle button
89 |
90 |
91 |
92 |
groups
93 |
94 |
95 |
96 | active
97 | button
98 | button
99 | button
100 |
101 |
102 |
103 |
104 |
links
105 |
106 |
107 | Default
108 | Info
109 |
110 | Group
111 | Group
112 |
113 |
114 |
115 |
116 |
custom theme
117 |
118 |
119 |
127 | pink custom
128 | pink custom
129 |
130 |
131 |
132 |
Badges
133 |
134 |
135 | Primary
136 | Secondary
137 | Success
138 | Danger
139 | Warning
140 | Info
141 | Light
142 | Dark
143 |
144 |
145 |
146 |
pill round
147 |
148 |
149 | Primary
150 | Secondary
151 | Success
152 | Danger
153 | Warning
154 | Info
155 | Light
156 | Dark
157 |
158 |
159 |
160 |
as links
161 |
162 |
163 | Secondary
164 | Success
165 |
166 |
167 |
168 |
Alerts
169 |
170 |
171 | This is a primary alert with an example link .
172 | This is a secondary alert with an example link .
173 | This is a success alert with an example link .
174 | This is a danger alert with an example link .
175 | This is a warning alert with an example link .
176 | This is a info alert with an example link .
177 | This is a light alert with an example link .
178 | This is a dark alert with an example link .
179 |
180 |
181 |
182 |
rich content
183 |
184 |
185 |
186 | Well done!
187 | Aww yeah, you successfully read this important alert message. This example text is going to run a bit longer so that you can see how spacing within an alert works with this kind of content.
188 |
189 | Whenever you need to, be sure to use margins to keep things nice and tidy.
190 |
191 |
192 |
193 |
194 |
dismissable
195 |
196 |
197 | This is a primary dismissable alert.
198 | This is a secondary dismissable alert.
199 | This is a success dismissable alert. This example text is going to run a bit longer so that you can see how spacing within an alert works with this kind of content
200 |
201 |
202 |
203 |
Progress
204 |
205 |
206 | primary
207 | secondary
208 | success
209 | info
210 | warning
211 | danger
212 | dark
213 |
214 |
215 |
216 |
217 |
218 |
219 |
--------------------------------------------------------------------------------