@markout-lang/bootstrap-kit 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,331 @@
1
+ # Bootstrap Kit
2
+
3
+ Bootstrap 5.3 as Markout components — one part per component, following
4
+ [Bootstrap's own cheatsheet](https://getbootstrap.com/docs/5.3/examples/cheatsheet/).
5
+
6
+ ```sh
7
+ npm install @markout-lang/bootstrap-kit # in a project
8
+ npm install -g @markout-lang/bootstrap-kit # for a bare docroot
9
+ ```
10
+
11
+ See [where kits are found](../../docs/reference/cli.md#where-kits-are-found):
12
+ a project's own kits are used whole, and a globally installed one is visible
13
+ only to a docroot that has none of its own.
14
+
15
+ ```html
16
+ <head>
17
+ <:import src="/npm/@markout-lang/bootstrap-kit/all.htm" />
18
+ </head>
19
+ ```
20
+
21
+ `/npm/` names the package the fragment came from, and is resolved at compile
22
+ time; everything the kit publishes is then addressed at `/bootstrap-kit`, the
23
+ logical root it declares. See [npm kits](../../docs/design/npm-kits.md) —
24
+ including the other case, a kit vendored into a docroot, which is imported by
25
+ its path instead.
26
+
27
+ `all.htm` pulls in everything. Each part imports `base.htm` itself and a file
28
+ is only imported once per page, so importing parts by hand never leaves
29
+ Bootstrap out:
30
+
31
+ ```html
32
+ <:import src="/npm/@markout-lang/bootstrap-kit/parts/button.htm" />
33
+ <:import src="/npm/@markout-lang/bootstrap-kit/parts/card.htm" />
34
+ ```
35
+
36
+ Run the showcase — every component below, live — with:
37
+
38
+ ```sh
39
+ npm run dev
40
+ ```
41
+
42
+ That serves [the site](../../sites/site/), which is where the pages built on
43
+ this kit live: `/demos/kitchen-sink.html`, every component one after another;
44
+ and `/demos/orbit.html`, an operations dashboard built out of them, which is
45
+ what they look like wired to one page's data.
46
+
47
+ The site is a plain Express app rather than markout's `Server`, because Orbit
48
+ is a whole application: it has an API of its own, served from a fake
49
+ in-memory database (`orbit-db.ts`), and markout is the middleware that
50
+ renders its pages. Orbit reads that API with `std-data` from the std kit,
51
+ which fetches while the page renders — so the served console is complete and
52
+ the browser asks for nothing.
53
+
54
+ Orbit is four files, which is the shape an application takes:
55
+ `demos/orbit.html` for its state, layout and logic; `demos/orbit/components.htm`
56
+ for the tags it defines on top of the kit's; `demos/orbit/sources.htm` for
57
+ where its data comes from; and `server.ts` for its API. The first is imported,
58
+ the second included — a definition wants to arrive once per page however often
59
+ it is named, and an instance wants to be spliced exactly where it is written,
60
+ because that is where its name resolves.
61
+
62
+ `orbit/sources.htm` also shows the token pattern working for an application's
63
+ own fragment: its root carries `:apiBase="/api"`, which lands on whatever
64
+ contains the `<:include>` unless that element declares it, so a page points
65
+ Orbit at another host without the fragment changing. That is the same
66
+ mechanism as `bsCssUrl` below — not something kits get and applications
67
+ don't.
68
+
69
+ ## Tests
70
+
71
+ `packages/cli/test/kits/bootstrap-kit.test.ts`, in two tiers:
72
+
73
+ - **compiled**, which is most of it: every part compiles on its own, the
74
+ showcase compiles and server-renders with nothing reported, and the id
75
+ wiring is checked mechanically — every `aria-controls`, `aria-labelledby`,
76
+ `for`, `data-bs-target` and `data-bs-parent` has to name an element that
77
+ exists. That last one is the kit's whole reason for existing, so it is
78
+ worth a test that can't be argued with.
79
+ - **live**, in Playwright: the value-driven components actually driven, and
80
+ a stubbed Bootstrap asserting the plugin calls. Skipped when no browser is
81
+ installed (`npx playwright install chromium`).
82
+
83
+ Nothing reaches the network: the tests set the URL tokens to local files.
84
+
85
+ ## What is and isn't a component
86
+
87
+ A component earns its place by removing something a person would otherwise
88
+ have to keep right by hand:
89
+
90
+ - **id wiring.** A modal, an accordion, a carousel, a navbar toggler, a
91
+ dropdown menu and a tab panel all connect two elements by `id`. Here the id
92
+ comes from `$id`, which is unique per instance, so a component can appear
93
+ twice on a page.
94
+ - **accessibility.** `role`, `aria-label`, `aria-current`, `aria-expanded`,
95
+ `visually-hidden` text: written once, not per use.
96
+ - **repetition.** A nav, a breadcrumb, a table, a dropdown menu, a carousel
97
+ and a list group are one shape repeated over a list, so their API is that
98
+ list.
99
+
100
+ Typography, spacing, the grid and the colour utilities are **not** components.
101
+ They are already one class each, and a tag that only forwards a class is a
102
+ name to learn for nothing.
103
+
104
+ ## Conventions
105
+
106
+ Every component follows the same rules, so knowing one is close to knowing
107
+ all of them.
108
+
109
+ **Parameters for the chrome, the slot for the content.** What a card *has* —
110
+ a title, a footer, an image — is a parameter. What it *contains* is slotted.
111
+
112
+ **`:extra` adds classes.** A `class` written at a usage site *replaces* the
113
+ one a definition sets, which is the language's rule and not something this
114
+ kit overrides. So every component takes `:extra` for the utility classes a
115
+ caller wants on top:
116
+
117
+ ```html
118
+ <bs-alert :variant="warning" :extra="mb-0">Careful</bs-alert>
119
+ ```
120
+
121
+ **Comments in a tag: `//` for one line, `/* … */` for more.** Both are
122
+ stripped at parse time. A run of `//` lines reads as a stack of fragments;
123
+ one block says it once.
124
+
125
+ **Lists are arrays.** `:items`, `:options`, `:columns`, `:rows`, `:slides`.
126
+ The default value of each is the shape it expects — read the definition to
127
+ see it.
128
+
129
+ **Callbacks are values, not events.** `:select`, `:check` and `:link` are
130
+ functions the component calls. `:on-click` and friends stay what they are in
131
+ the language: DOM events.
132
+
133
+ **A plugin is built on attach and released on detach.** The five components
134
+ that hand their element to Bootstrap's JS — `bs-modal`, `bs-offcanvas`,
135
+ `bs-toast`, `bs-tooltip`, `bs-popover` — do it from `:did-attach` and undo it
136
+ from `:will-detach`, not from `:did-init`/`:will-dispose`. A `:for-data`
137
+ region takes its markup out of the page without its scope going anywhere,
138
+ and a plugin left holding a removed element leaves its backdrop, its popper
139
+ and the page's scroll lock behind.
140
+
141
+ **Values are read and written.** `bs-input`, `bs-select`, `bs-check`,
142
+ `bs-range`, `bs-modal`, `bs-offcanvas` and `bs-toast` keep `:value` or
143
+ `:open` in step with what is on screen. Name the instance and read it from
144
+ anywhere:
145
+
146
+ ```html
147
+ <bs-input :aka="email" :label="Email" :type="email" />
148
+ <bs-button :disabled=${!email.value}>Send</bs-button>
149
+ ```
150
+
151
+ That holds however deeply the component sits: a `bs-toast :aka="saved"`
152
+ written inside a `bs-toast-container` is still named where you wrote it, so
153
+ `saved.open = true` reaches it from anywhere on the page.
154
+
155
+ **Optional regions are `:if`.** A region that exists only when a parameter
156
+ was given is written `:if=${header}`, and nothing inside it evaluates while
157
+ the condition is false — which is what makes `${user.name}` safe to write in
158
+ one. It asks the question JavaScript asks, so an unset parameter and an
159
+ empty string both count as absent:
160
+
161
+ ```html
162
+ <bs-close :if=${dismissible} />
163
+ <span :if=${!split}>${label}</span>
164
+ ```
165
+
166
+ `:else-if` and `:else` continue it, on the element immediately after. The
167
+ kit uses them where a component chooses between renderings rather than
168
+ merely omitting one — `bs-nav` between a tab button and a plain link:
169
+
170
+ ```html
171
+ <button class="nav-link" :if=${toggle} ...>${item.name}</button>
172
+ <a class="nav-link" :else ...>${item.name}</a>
173
+ ```
174
+
175
+ and `bs-dropdown` between the three things an item can be:
176
+
177
+ ```html
178
+ <hr class="dropdown-divider" :if=${item.divider}>
179
+ <h6 class="dropdown-header" :else-if=${item.header}>${item.header}</h6>
180
+ <a class="dropdown-item" :else ...>${item.name}</a>
181
+ ```
182
+
183
+ `:for-data` is for the other case — there is something to show, and the body
184
+ wants it. It is `!= null` rather than truthy, so `0` and `''` stay data, and
185
+ it binds the item as `data`. The kit has no use for it: every optional region
186
+ here renders a parameter it already has a name for.
187
+
188
+ Optional parts of a component are parameters, and a named slot where markup
189
+ belongs — `bs-card`'s header is both: `:header` sets the text, and a
190
+ `:slot="header"` replaces it with markup. A `<:slot>` may sit inside a
191
+ `:for-data` but not inside a `:for-each`, which is what makes that possible.
192
+
193
+ ## Where Bootstrap comes from
194
+
195
+ The CDN URLs and their hashes are tokens like any other, so a page points the
196
+ kit at its own copy without forking `base.htm`:
197
+
198
+ ```html
199
+ <head ::bsCssUrl="/vendor/bootstrap.min.css"
200
+ ::bsJsUrl="/vendor/bootstrap.bundle.min.js"
201
+ ::bsCssIntegrity=${null}
202
+ ::bsJsIntegrity=${null}>
203
+ <:import src="/bootstrap-kit/all.htm" />
204
+ </head>
205
+ ```
206
+
207
+ | Token | Default |
208
+ | --- | --- |
209
+ | `bsCssUrl` | jsDelivr, Bootstrap 5.3.8 |
210
+ | `bsJsUrl` | jsDelivr, Bootstrap 5.3.8 |
211
+ | `bsCssIntegrity` | the matching SRI hash |
212
+ | `bsJsIntegrity` | the matching SRI hash |
213
+
214
+ Drop the hashes when self-hosting: `crossorigin` follows the hash, and
215
+ neither means anything on a same-origin file.
216
+
217
+ Four reasons this is worth having rather than a convenience:
218
+
219
+ - **A content security policy** that allows no third-party origin, which is
220
+ most of them once an app is behind a login.
221
+ - **An offline or air-gapped build**, where a CDN isn't reachable at all.
222
+ - **A vendored, pinned copy**, so the page doesn't depend on a third party
223
+ staying up or staying honest.
224
+ - **Your own Bootstrap build.** The tokens below only reach what Bootstrap
225
+ exposes as CSS variables; a design system usually compiles Bootstrap from
226
+ Sass with its own variables. Pointing `bsCssUrl` at that build is how
227
+ the kit gets out of the way of it.
228
+
229
+ ## Theming
230
+
231
+ `base.htm` declares the kit's tokens and writes them into Bootstrap's own CSS
232
+ variables, so restyling everything is setting one value at the import site:
233
+
234
+ ```html
235
+ <head ::bsRadius="1rem" ::bsLinkDecoration="none">
236
+ <:import src="/bootstrap-kit/all.htm" />
237
+ </head>
238
+ ```
239
+
240
+ | Token | Default |
241
+ | --- | --- |
242
+ | `bsRadius` | `0.375rem` |
243
+ | `bsRadiusSm` | `0.25rem` |
244
+ | `bsRadiusLg` | `0.5rem` |
245
+ | `bsRadiusPill` | `50rem` |
246
+ | `bsFontSans` | Bootstrap's system stack |
247
+ | `bsLinkDecoration` | `underline` |
248
+
249
+ Colour modes are `theme.htm`: an inline pre-paint script so the page never
250
+ flashes the wrong mode, and `<bs-theme-toggle />` to switch it.
251
+
252
+ ## Components
253
+
254
+ Every tag also takes `:extra`. Defaults are in the definitions, which are
255
+ commented.
256
+
257
+ ### Content
258
+
259
+ | Tag | Parameters |
260
+ | --- | --- |
261
+ | `bs-image` | `src` `alt` `fluid` `thumbnail` `rounded` |
262
+ | `bs-figure` | `src` `alt` `caption` `align` |
263
+ | `bs-table` | `columns` `rows` `caption` `striped` `hover` `bordered` `borderless` `small` `variant` `headVariant` `align` `responsive` |
264
+
265
+ ### Forms
266
+
267
+ | Tag | Parameters |
268
+ | --- | --- |
269
+ | `bs-input` | `label` `type` `name` `value` `placeholder` `help` `size` `disabled` `readonly` `required` `floating` `check` `message` |
270
+ | `bs-textarea` | `label` `name` `value` `placeholder` `rows` `help` `disabled` `readonly` `required` `check` `message` |
271
+ | `bs-select` | `label` `name` `options` `value` `placeholder` `help` `size` `multiple` `disabled` `required` `floating` |
272
+ | `bs-check` | `label` `type` (`checkbox`/`radio`/`switch`) `name` `value` `checked` `inline` `reverse` `disabled` `help` |
273
+ | `bs-check-group` | `legend` `type` `options` `value` `inline` `disabled` |
274
+ | `bs-range` | `label` `name` `min` `max` `step` `value` `disabled` `showValue` |
275
+ | `bs-input-group` | `prefix` `suffix` `size` |
276
+
277
+ ### Components
278
+
279
+ | Tag | Parameters |
280
+ | --- | --- |
281
+ | `bs-accordion` | `exclusive` `flush` |
282
+ | `bs-accordion-item` | `title` `open` |
283
+ | `bs-alert` | `variant` `heading` `dismissible` |
284
+ | `bs-badge` | `variant` `pill` `position` |
285
+ | `bs-breadcrumb` | `items` `divider` |
286
+ | `bs-button` | `variant` `outline` `size` `active` `disabled` `type` `toggle` `target` `dismiss` |
287
+ | `bs-link` | `href` `variant` `outline` `size` `active` `disabled` `button` `toggle` `target` |
288
+ | `bs-button-group` | `label` `size` `vertical` |
289
+ | `bs-button-toolbar` | `label` `gap` |
290
+ | `bs-close` | `label` `dismiss` `disabled` |
291
+ | `bs-card` | `title` `subtitle` `header` `footer` `image` `imageAlt` `imageBottom` `variant` `border` `align` `bodyExtra` |
292
+ | `bs-card-group` | `cols` `gap` `attached` |
293
+ | `bs-carousel` | `slides` `controls` `indicators` `captions` `fade` `ride` `interval` `dark` |
294
+ | `bs-collapse` | `name` `open` `horizontal` |
295
+ | `bs-dropdown` | `label` `items` `variant` `outline` `size` `split` `direction` `align` `dark` |
296
+ | `bs-list-group` | `items` `flush` `horizontal` |
297
+ | `bs-modal` | `name` `title` `size` `centered` `scrollable` `fullscreen` `staticBackdrop` `open` |
298
+ | `bs-nav` | `items` `variant` `fill` `justified` `vertical` `align` `toggle` |
299
+ | `bs-tab-content`, `bs-tab-pane` | `name` `active` |
300
+ | `bs-navbar` | `items` `brand` `expand` `container` `sticky` `fixed` `theme` `bg`; slots: default (brand), `end` |
301
+ | `bs-offcanvas` | `name` `title` `placement` `backdrop` `scroll` `responsive` `open` |
302
+ | `bs-pagination` | `current` `pages` `size` `align` `prev` `next` `link` `select` `label` |
303
+ | `bs-placeholder` | `cols` `size` `variant` `animation` |
304
+ | `bs-popover` | `title` `content` `placement` `trigger` `html` |
305
+ | `bs-progress` | `value` `min` `max` `label` `variant` `striped` `animated` `height` `name` |
306
+ | `bs-progress-stacked` | — |
307
+ | `bs-scrollspy` | `target` `offset` `smooth` `height` |
308
+ | `bs-spinner` | `variant` `type` `small` `label` |
309
+ | `bs-theme-toggle` | `variant` `outline` `size` |
310
+ | `bs-toast` | `title` `time` `variant` `open` `autohide` `delay` |
311
+ | `bs-toast-container` | `placement` |
312
+ | `bs-tooltip` | `title` `placement` `html` `trigger` |
313
+
314
+ ## Notes on the awkward corners
315
+
316
+ A few components are shaped by something in the language rather than by
317
+ Bootstrap, and each says so in its own file:
318
+
319
+ - **`bs-accordion-item` reads `$host`.** An item needs the id of the
320
+ accordion it sits in, and slotted markup resolves at the call site, so it
321
+ cannot reach it by name. `$host` is the instance it was slotted *into* —
322
+ the one place the kit needs the structural relationship rather than the
323
+ lexical one. No id is written by anyone.
324
+ - **`bs-pagination` says `:current`, not `:page`.** `page` is already the
325
+ name of `<html>`'s own scope, so a parameter of that name would resolve to
326
+ the scope rather than to the number.
327
+ - **`bs-tooltip` and `bs-popover` construct their plugin from a
328
+ `:handle-`.** They are the only two components Bootstrap doesn't start by
329
+ itself, and the usual answer — a page-level loop over
330
+ `[data-bs-toggle="tooltip"]` — misses anything added later. A handler
331
+ builds one per instance instead, and rebuilds it when the text changes.
package/all.htm ADDED
@@ -0,0 +1,44 @@
1
+ <!---
2
+ Everything the kit defines. Import this for a page that uses a bit of
3
+ everything; import individual parts for one that doesn't.
4
+
5
+ Each part imports `base.htm` itself, and a file is only imported once per
6
+ page, so picking parts by hand never leaves Bootstrap out.
7
+ -->
8
+ <lib>
9
+ <:import src="parts/base.htm" />
10
+
11
+ <!--- content -->
12
+ <:import src="parts/image.htm" />
13
+ <:import src="parts/table.htm" />
14
+
15
+ <!--- forms -->
16
+ <:import src="parts/check.htm" />
17
+ <:import src="parts/input.htm" />
18
+ <:import src="parts/select.htm" />
19
+
20
+ <!--- components -->
21
+ <:import src="parts/accordion.htm" />
22
+ <:import src="parts/alert.htm" />
23
+ <:import src="parts/badge.htm" />
24
+ <:import src="parts/breadcrumb.htm" />
25
+ <:import src="parts/button.htm" />
26
+ <:import src="parts/card.htm" />
27
+ <:import src="parts/carousel.htm" />
28
+ <:import src="parts/collapse.htm" />
29
+ <:import src="parts/dropdown.htm" />
30
+ <:import src="parts/list-group.htm" />
31
+ <:import src="parts/modal.htm" />
32
+ <:import src="parts/nav.htm" />
33
+ <:import src="parts/navbar.htm" />
34
+ <:import src="parts/offcanvas.htm" />
35
+ <:import src="parts/pagination.htm" />
36
+ <:import src="parts/placeholder.htm" />
37
+ <:import src="parts/popover.htm" />
38
+ <:import src="parts/progress.htm" />
39
+ <:import src="parts/scrollspy.htm" />
40
+ <:import src="parts/spinner.htm" />
41
+ <:import src="parts/theme.htm" />
42
+ <:import src="parts/toast.htm" />
43
+ <:import src="parts/tooltip.htm" />
44
+ </lib>
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@markout-lang/bootstrap-kit",
3
+ "version": "0.2.0",
4
+ "description": "Bootstrap 5.3 as Markout components: one part per component, with the id wiring, the ARIA attributes and the repetition written once",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/fcapolini/markout.git",
8
+ "directory": "kits/bootstrap-kit"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/fcapolini/markout/issues"
12
+ },
13
+ "homepage": "https://markout.dev",
14
+ "markout": {
15
+ "root": "/bootstrap-kit"
16
+ },
17
+ "keywords": [
18
+ "markout",
19
+ "markout-kit",
20
+ "bootstrap",
21
+ "components",
22
+ "html"
23
+ ],
24
+ "author": "Fabrizio Capolini",
25
+ "license": "MIT",
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "files": [
30
+ "all.htm",
31
+ "parts"
32
+ ]
33
+ }
@@ -0,0 +1,81 @@
1
+ <lib>
2
+ <:import src="base.htm" />
3
+
4
+ <!---
5
+ An accordion, in two tags, because its panels hold arbitrary markup and
6
+ markup belongs in a slot rather than in an array.
7
+
8
+ <bs-accordion>
9
+ <bs-accordion-item :title="First" :open>…</bs-accordion-item>
10
+ <bs-accordion-item :title="Second">…</bs-accordion-item>
11
+ </bs-accordion>
12
+
13
+ The item needs one thing from the accordion — the id Bootstrap closes the
14
+ others by — and it asks for it with `$host`, which is the enclosing
15
+ instance rather than the scope the markup was written in. So neither tag
16
+ carries an id the caller had to invent, and the whole coupling is one
17
+ expression in the definition instead of a back-reference at every usage.
18
+
19
+ An item written outside an accordion has no `$host` and simply opens and
20
+ closes on its own, which is Bootstrap's "always open" behaviour reached
21
+ by itself rather than by remembering to leave an attribute off. Setting
22
+ `:exclusive=${false}` on the accordion asks for the same thing.
23
+
24
+ Every id here is derived from `$id`, so two accordions on a page cannot
25
+ collide and nothing has to be named.
26
+ -->
27
+ <:define tag="bs-accordion:div"
28
+ class="accordion ${extra ?? ''}"
29
+
30
+ // parameters
31
+ :extra=${null} // extra classes for this instance
32
+ :exclusive=${true} // one open panel at a time
33
+ :flush=${false}
34
+
35
+ /*
36
+ private: the id the items point at. Owned here rather than
37
+ rebuilt there, so the two can't drift apart
38
+ */
39
+ :_id=${`bs-accordion-${$id}`}
40
+
41
+ id=${_id}
42
+ :class-accordion-flush=${flush}
43
+ >
44
+ <:slot />
45
+ </:define>
46
+
47
+ <:define tag="bs-accordion-item:div"
48
+ class="accordion-item ${extra ?? ''}"
49
+
50
+ // parameters
51
+ :extra=${null} // extra classes for this instance
52
+ :title=${''}
53
+ :open=${false}
54
+
55
+ // private: the accordion this item was slotted into, if any
56
+ :_parent=${$host && $host.exclusive ? $host._id : null}
57
+ :_id=${`bs-accordion-item-${$id}`}
58
+ :_headingId=${`bs-accordion-heading-${$id}`}
59
+ >
60
+ <h2 class="accordion-header" id=${_headingId}>
61
+ <button class="accordion-button"
62
+ type="button"
63
+ :class-collapsed=${!open}
64
+ data-bs-toggle="collapse"
65
+ data-bs-target="#${_id}"
66
+ aria-expanded=${open ? 'true' : 'false'}
67
+ aria-controls=${_id}>${title}</button>
68
+ </h2>
69
+
70
+ <div class="accordion-collapse collapse"
71
+ id=${_id}
72
+ :class-show=${open}
73
+ aria-labelledby=${_headingId}
74
+ data-bs-parent=${_parent ? `#${_parent}` : null}>
75
+ <div class="accordion-body">
76
+ <:slot />
77
+ </div>
78
+ </div>
79
+ </:define>
80
+
81
+ </lib>
@@ -0,0 +1,48 @@
1
+ <lib>
2
+ <:import src="base.htm" />
3
+ <:import src="button.htm" />
4
+
5
+ <!---
6
+ An alert, with the two parts that are easy to get wrong done for you:
7
+ `role="alert"` so it is announced, and the `fade show` + close button
8
+ combination that a dismissible one needs to animate out.
9
+
10
+ `:heading` and the dismiss button are optional REGIONS, and both are
11
+ `:if`: there is something to decide, not something to show. Truthiness
12
+ is what is wanted of a heading — `:heading=""` is a heading nobody
13
+ asked for, and `:for-data`'s `!= null` would have rendered the empty
14
+ `<h4>` around it.
15
+
16
+ The body is a `<:slot>` and the heading a parameter because that is
17
+ what each of them is — markup in one case, text in the other. It used
18
+ to be forced as well: an optional region was a `:for-each` over a
19
+ one-or-zero list, and a `<:slot>` cannot sit inside one of those.
20
+ -->
21
+ <:define tag="bs-alert:div"
22
+ role="alert"
23
+
24
+ // parameters
25
+ :extra=${null} // extra classes for this instance
26
+ :variant=${'primary'}
27
+ :heading=${null}
28
+ :dismissible=${false}
29
+
30
+ // private
31
+ :_class=${['alert', `alert-${variant}`, extra].filter(s => s).join(' ')}
32
+
33
+ class=${_class}
34
+ :class-alert-dismissible=${dismissible}
35
+ :class-fade=${dismissible}
36
+ :class-show=${dismissible}
37
+ >
38
+ <h4 class="alert-heading"
39
+ :if=${heading}>${heading}</h4>
40
+
41
+ <:slot />
42
+
43
+ <bs-close :dismiss="alert"
44
+ :label="Close alert"
45
+ :if=${dismissible} />
46
+ </:define>
47
+
48
+ </lib>
@@ -0,0 +1,33 @@
1
+ <lib>
2
+ <:import src="base.htm" />
3
+
4
+ <!---
5
+ `text-bg-*` rather than `bg-*`: it sets the foreground colour along with
6
+ the background, which is the pairing that stays readable in both colour
7
+ modes.
8
+ -->
9
+ <:define tag="bs-badge:span"
10
+
11
+ // parameters
12
+ :extra=${null} // extra classes for this instance
13
+ :variant=${'primary'}
14
+ :pill=${false}
15
+ :position=${null} // 'top-end' | 'bottom-end' — for a badge on a button
16
+
17
+ // private
18
+ :_class=${[
19
+ 'badge',
20
+ `text-bg-${variant}`,
21
+ pill ? 'rounded-pill' : '',
22
+ position ? 'position-absolute translate-middle' : '',
23
+ position === 'top-end' ? 'top-0 start-100' : '',
24
+ position === 'bottom-end' ? 'top-100 start-100' : '',
25
+ extra,
26
+ ].filter(s => s).join(' ')}
27
+
28
+ class=${_class}
29
+ >
30
+ <:slot />
31
+ </:define>
32
+
33
+ </lib>
package/parts/base.htm ADDED
@@ -0,0 +1,71 @@
1
+ <!---
2
+ The one part every other part imports. It brings in Bootstrap itself and
3
+ declares the kit's design tokens.
4
+
5
+ A token is a value rather than a per-component CSS override: it is written
6
+ once into Bootstrap's own CSS variables below, so restyling the kit means
7
+ setting one value at the import site instead of patching each component:
8
+
9
+ <head ::bsRadius="1rem">
10
+ <:import src="/bootstrap-kit/all.htm" />
11
+ </head>
12
+
13
+ Where Bootstrap itself comes from is a token too. A page under a content
14
+ security policy that allows no third party, an offline build, or a vendored
15
+ copy pinned on purpose sets the two URLs and drops the hashes rather than
16
+ forking this file — and so does a page serving its own Sass build, which is
17
+ how a design system gets past what the tokens below can reach:
18
+
19
+ <head ::bsCssUrl="/vendor/bootstrap.min.css"
20
+ ::bsJsUrl="/vendor/bootstrap.bundle.min.js"
21
+ ::bsCssIntegrity=${null}
22
+ ::bsJsIntegrity=${null}>
23
+ -->
24
+ <lib
25
+ // where Bootstrap comes from
26
+ ::bsCssUrl=${'https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css'}
27
+ ::bsCssIntegrity=${'sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB'}
28
+ ::bsJsUrl=${'https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js'}
29
+ ::bsJsIntegrity=${'sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI'}
30
+
31
+ // tokens
32
+ ::bsRadius=${'0.375rem'}
33
+ ::bsRadiusSm=${'0.25rem'}
34
+ ::bsRadiusLg=${'0.5rem'}
35
+ ::bsRadiusPill=${'50rem'}
36
+ ::bsFontSans=${'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'}
37
+ ::bsLinkDecoration=${'underline'}
38
+ >
39
+ <meta charset="utf-8">
40
+ <meta name="viewport"
41
+ content="width=device-width, initial-scale=1">
42
+
43
+ <!--- `crossorigin` follows the hash: it is what makes one checkable, and
44
+ means nothing on a same-origin file -->
45
+ <link href=${bsCssUrl}
46
+ rel="stylesheet"
47
+ integrity=${bsCssIntegrity}
48
+ crossorigin=${bsCssIntegrity ? 'anonymous' : null}>
49
+
50
+ <!---
51
+ Deliberately not `async`. The components that need Bootstrap's JS
52
+ (tooltips, popovers, toasts) create their plugin instance from a
53
+ `:handle-`, which runs the moment Markout hydrates; loading the bundle
54
+ synchronously here is what guarantees `window.bootstrap` already exists
55
+ by then. Everything driven by `data-bs-*` alone would be fine either way.
56
+ -->
57
+ <script src=${bsJsUrl}
58
+ integrity=${bsJsIntegrity}
59
+ crossorigin=${bsJsIntegrity ? 'anonymous' : null}></script>
60
+
61
+ <style>
62
+ :root {
63
+ --bs-border-radius: ${bsRadius};
64
+ --bs-border-radius-sm: ${bsRadiusSm};
65
+ --bs-border-radius-lg: ${bsRadiusLg};
66
+ --bs-border-radius-pill: ${bsRadiusPill};
67
+ --bs-font-sans-serif: ${bsFontSans};
68
+ --bs-link-decoration: ${bsLinkDecoration};
69
+ }
70
+ </style>
71
+ </lib>