@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 +331 -0
- package/all.htm +44 -0
- package/package.json +33 -0
- package/parts/accordion.htm +81 -0
- package/parts/alert.htm +48 -0
- package/parts/badge.htm +33 -0
- package/parts/base.htm +71 -0
- package/parts/breadcrumb.htm +44 -0
- package/parts/button.htm +162 -0
- package/parts/card.htm +103 -0
- package/parts/carousel.htm +87 -0
- package/parts/check.htm +164 -0
- package/parts/collapse.htm +31 -0
- package/parts/dropdown.htm +86 -0
- package/parts/image.htm +46 -0
- package/parts/input.htm +178 -0
- package/parts/list-group.htm +58 -0
- package/parts/modal.htm +119 -0
- package/parts/nav.htm +89 -0
- package/parts/navbar.htm +82 -0
- package/parts/offcanvas.htm +90 -0
- package/parts/pagination.htm +73 -0
- package/parts/placeholder.htm +34 -0
- package/parts/popover.htm +43 -0
- package/parts/progress.htm +54 -0
- package/parts/scrollspy.htm +33 -0
- package/parts/select.htm +72 -0
- package/parts/spinner.htm +32 -0
- package/parts/table.htm +70 -0
- package/parts/theme.htm +70 -0
- package/parts/toast.htm +115 -0
- package/parts/tooltip.htm +57 -0
package/parts/image.htm
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<lib>
|
|
2
|
+
<:import src="base.htm" />
|
|
3
|
+
|
|
4
|
+
<!---
|
|
5
|
+
An image that behaves: responsive by default, and with `alt` a parameter
|
|
6
|
+
that is always written rather than an attribute that is often forgotten.
|
|
7
|
+
-->
|
|
8
|
+
<:define tag="bs-image:img"
|
|
9
|
+
|
|
10
|
+
// parameters
|
|
11
|
+
:extra=${null} // extra classes for this instance
|
|
12
|
+
:src=${null}
|
|
13
|
+
:alt=${''}
|
|
14
|
+
:fluid=${true}
|
|
15
|
+
:thumbnail=${false}
|
|
16
|
+
:rounded=${false}
|
|
17
|
+
|
|
18
|
+
class=${extra}
|
|
19
|
+
src=${src}
|
|
20
|
+
alt=${alt}
|
|
21
|
+
:class-img-fluid=${fluid}
|
|
22
|
+
:class-img-thumbnail=${thumbnail}
|
|
23
|
+
:class-rounded=${rounded}
|
|
24
|
+
/>
|
|
25
|
+
|
|
26
|
+
<!--- An image with its caption, as one thing. -->
|
|
27
|
+
<:define tag="bs-figure:figure"
|
|
28
|
+
class="figure ${extra ?? ''}"
|
|
29
|
+
|
|
30
|
+
// parameters
|
|
31
|
+
:extra=${null} // extra classes for this instance
|
|
32
|
+
:src=${null}
|
|
33
|
+
:alt=${''}
|
|
34
|
+
:caption=${null}
|
|
35
|
+
:align=${null} // 'center' | 'end'
|
|
36
|
+
>
|
|
37
|
+
<img class="figure-img img-fluid rounded"
|
|
38
|
+
src=${src}
|
|
39
|
+
alt=${alt}>
|
|
40
|
+
<figcaption class="figure-caption"
|
|
41
|
+
:class-text-center=${align === 'center'}
|
|
42
|
+
:class-text-end=${align === 'end'}
|
|
43
|
+
:if=${caption}>${caption}</figcaption>
|
|
44
|
+
</:define>
|
|
45
|
+
|
|
46
|
+
</lib>
|
package/parts/input.htm
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
<lib>
|
|
2
|
+
<:import src="base.htm" />
|
|
3
|
+
|
|
4
|
+
<!---
|
|
5
|
+
A form control with its label, help text and validation state wired up
|
|
6
|
+
rather than left to the caller.
|
|
7
|
+
|
|
8
|
+
`:value` is read AND written: the component keeps it in step with what is
|
|
9
|
+
typed, so a page can name the instance and read it from anywhere else on
|
|
10
|
+
the page —
|
|
11
|
+
|
|
12
|
+
<bs-input :aka="email" :label="Email" :type="email" />
|
|
13
|
+
<bs-button :disabled=${!email.value}>Send</bs-button>
|
|
14
|
+
|
|
15
|
+
— which is `:aka` doing what it does everywhere else, not a mechanism
|
|
16
|
+
special to forms.
|
|
17
|
+
|
|
18
|
+
The validity policy is this component's own, not Bootstrap's: a control is
|
|
19
|
+
invalid once it is non-empty and fails `:check`. Empty is neutral, so a
|
|
20
|
+
form doesn't open covered in errors. `:required` extends that to empty.
|
|
21
|
+
|
|
22
|
+
Floating labels are the same control with the label after the input
|
|
23
|
+
instead of before it — Bootstrap's CSS keys off that order — so the two
|
|
24
|
+
layouts are two regions rather than one with a class toggled on it.
|
|
25
|
+
-->
|
|
26
|
+
<:define tag="bs-input:div"
|
|
27
|
+
class="mb-3 ${extra ?? ''}"
|
|
28
|
+
|
|
29
|
+
// parameters
|
|
30
|
+
:extra=${null} // extra classes for this instance
|
|
31
|
+
:label=${null}
|
|
32
|
+
:type=${'text'}
|
|
33
|
+
:name=${null}
|
|
34
|
+
:value=${''}
|
|
35
|
+
:placeholder=${''}
|
|
36
|
+
:help=${null}
|
|
37
|
+
:size=${null} // 'sm' | 'lg'
|
|
38
|
+
:disabled=${false}
|
|
39
|
+
:readonly=${false}
|
|
40
|
+
:required=${false}
|
|
41
|
+
:floating=${false}
|
|
42
|
+
:check=${(v) => true}
|
|
43
|
+
:message=${'Please check this value.'}
|
|
44
|
+
|
|
45
|
+
// private
|
|
46
|
+
:_id=${`bs-input-${$id}`}
|
|
47
|
+
:_helpId=${`bs-input-help-${$id}`}
|
|
48
|
+
:_empty=${value === '' || value == null}
|
|
49
|
+
:_invalid=${_empty ? required : !check(value)}
|
|
50
|
+
:_class=${['form-control', size ? `form-control-${size}` : '']
|
|
51
|
+
.filter(s => s).join(' ')}
|
|
52
|
+
>
|
|
53
|
+
<!--- plain layout: label, control, feedback -->
|
|
54
|
+
<div :if=${!floating}>
|
|
55
|
+
<label class="form-label"
|
|
56
|
+
for=${_id}
|
|
57
|
+
:if=${label}>${label}</label>
|
|
58
|
+
|
|
59
|
+
<input class=${_class}
|
|
60
|
+
id=${_id}
|
|
61
|
+
type=${type}
|
|
62
|
+
name=${name}
|
|
63
|
+
value=${value}
|
|
64
|
+
placeholder=${placeholder}
|
|
65
|
+
aria-describedby=${help ? _helpId : null}
|
|
66
|
+
:class-is-invalid=${_invalid}
|
|
67
|
+
:attr-disabled=${disabled}
|
|
68
|
+
:attr-readonly=${readonly}
|
|
69
|
+
:attr-required=${required}
|
|
70
|
+
:on-input=${ev => value = ev.target.value}>
|
|
71
|
+
|
|
72
|
+
<div class="form-text"
|
|
73
|
+
id=${_helpId}
|
|
74
|
+
:if=${help}>${help}</div>
|
|
75
|
+
|
|
76
|
+
<div class="invalid-feedback">${message}</div>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<!--- floating layout: control, then label -->
|
|
80
|
+
<div class="form-floating" :else>
|
|
81
|
+
<input class=${_class}
|
|
82
|
+
id=${_id}
|
|
83
|
+
type=${type}
|
|
84
|
+
name=${name}
|
|
85
|
+
value=${value}
|
|
86
|
+
placeholder=${placeholder || label || ' '}
|
|
87
|
+
:class-is-invalid=${_invalid}
|
|
88
|
+
:attr-disabled=${disabled}
|
|
89
|
+
:attr-readonly=${readonly}
|
|
90
|
+
:attr-required=${required}
|
|
91
|
+
:on-input=${ev => value = ev.target.value}>
|
|
92
|
+
|
|
93
|
+
<label for=${_id}>${label}</label>
|
|
94
|
+
|
|
95
|
+
<div class="invalid-feedback">${message}</div>
|
|
96
|
+
</div>
|
|
97
|
+
</:define>
|
|
98
|
+
|
|
99
|
+
<!---
|
|
100
|
+
The same shape for multi-line text. Kept separate rather than folded into
|
|
101
|
+
`bs-input` as `type="textarea"`: it is a different element with its own
|
|
102
|
+
`rows`, and a type that isn't one of HTML's would be a small lie.
|
|
103
|
+
-->
|
|
104
|
+
<:define tag="bs-textarea:div"
|
|
105
|
+
class="mb-3 ${extra ?? ''}"
|
|
106
|
+
|
|
107
|
+
// parameters
|
|
108
|
+
:extra=${null} // extra classes for this instance
|
|
109
|
+
:label=${null}
|
|
110
|
+
:name=${null}
|
|
111
|
+
:value=${''}
|
|
112
|
+
:placeholder=${''}
|
|
113
|
+
:rows=${3}
|
|
114
|
+
:help=${null}
|
|
115
|
+
:disabled=${false}
|
|
116
|
+
:readonly=${false}
|
|
117
|
+
:required=${false}
|
|
118
|
+
:check=${(v) => true}
|
|
119
|
+
:message=${'Please check this value.'}
|
|
120
|
+
|
|
121
|
+
// private
|
|
122
|
+
:_id=${`bs-textarea-${$id}`}
|
|
123
|
+
:_helpId=${`bs-textarea-help-${$id}`}
|
|
124
|
+
:_empty=${value === '' || value == null}
|
|
125
|
+
:_invalid=${_empty ? required : !check(value)}
|
|
126
|
+
>
|
|
127
|
+
<label class="form-label"
|
|
128
|
+
for=${_id}
|
|
129
|
+
:if=${label}>${label}</label>
|
|
130
|
+
|
|
131
|
+
<textarea class="form-control"
|
|
132
|
+
id=${_id}
|
|
133
|
+
name=${name}
|
|
134
|
+
rows=${rows}
|
|
135
|
+
placeholder=${placeholder}
|
|
136
|
+
aria-describedby=${help ? _helpId : null}
|
|
137
|
+
:class-is-invalid=${_invalid}
|
|
138
|
+
:attr-disabled=${disabled}
|
|
139
|
+
:attr-readonly=${readonly}
|
|
140
|
+
:attr-required=${required}
|
|
141
|
+
:on-input=${ev => value = ev.target.value}>${value}</textarea>
|
|
142
|
+
|
|
143
|
+
<div class="form-text"
|
|
144
|
+
id=${_helpId}
|
|
145
|
+
:if=${help}>${help}</div>
|
|
146
|
+
|
|
147
|
+
<div class="invalid-feedback">${message}</div>
|
|
148
|
+
</:define>
|
|
149
|
+
|
|
150
|
+
<!---
|
|
151
|
+
An input group: text or markup either side of a control. `:prefix` and
|
|
152
|
+
`:suffix` cover the common case; anything richer goes in the slot as
|
|
153
|
+
ordinary `.input-group-text` markup.
|
|
154
|
+
-->
|
|
155
|
+
<:define tag="bs-input-group:div"
|
|
156
|
+
|
|
157
|
+
// parameters
|
|
158
|
+
:extra=${null} // extra classes for this instance
|
|
159
|
+
:prefix=${null}
|
|
160
|
+
:suffix=${null}
|
|
161
|
+
:size=${null}
|
|
162
|
+
|
|
163
|
+
// private
|
|
164
|
+
:_class=${['input-group', size ? `input-group-${size}` : '', extra]
|
|
165
|
+
.filter(s => s).join(' ')}
|
|
166
|
+
|
|
167
|
+
class=${_class}
|
|
168
|
+
>
|
|
169
|
+
<span class="input-group-text"
|
|
170
|
+
:if=${prefix}>${prefix}</span>
|
|
171
|
+
|
|
172
|
+
<:slot />
|
|
173
|
+
|
|
174
|
+
<span class="input-group-text"
|
|
175
|
+
:if=${suffix}>${suffix}</span>
|
|
176
|
+
</:define>
|
|
177
|
+
|
|
178
|
+
</lib>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<lib>
|
|
2
|
+
<:import src="base.htm" />
|
|
3
|
+
<:import src="badge.htm" />
|
|
4
|
+
|
|
5
|
+
<!---
|
|
6
|
+
A list group, from `{ name, link, active, disabled, variant, badge, text }`
|
|
7
|
+
entries.
|
|
8
|
+
|
|
9
|
+
Every entry is an `<a>`. Bootstrap only wants `.list-group-item-action`
|
|
10
|
+
(the hover and focus treatment) on the ones that actually go somewhere, so
|
|
11
|
+
that class follows `link` rather than being always on — and an anchor with
|
|
12
|
+
no `href` is inert, which is exactly what a non-link entry should be.
|
|
13
|
+
-->
|
|
14
|
+
<:define tag="bs-list-group:div"
|
|
15
|
+
|
|
16
|
+
// parameters
|
|
17
|
+
:extra=${null} // extra classes for this instance
|
|
18
|
+
:items=${[
|
|
19
|
+
{ name: 'An item', active: true },
|
|
20
|
+
{ name: 'A second item' },
|
|
21
|
+
{ name: 'A disabled item', disabled: true },
|
|
22
|
+
]}
|
|
23
|
+
:flush=${false}
|
|
24
|
+
:horizontal=${false}
|
|
25
|
+
|
|
26
|
+
// private
|
|
27
|
+
:_class=${[
|
|
28
|
+
'list-group',
|
|
29
|
+
flush ? 'list-group-flush' : '',
|
|
30
|
+
horizontal ? 'list-group-horizontal' : '',
|
|
31
|
+
extra,
|
|
32
|
+
].filter(s => s).join(' ')}
|
|
33
|
+
|
|
34
|
+
class=${_class}
|
|
35
|
+
>
|
|
36
|
+
<a :for-each=${items}
|
|
37
|
+
:for-as="item"
|
|
38
|
+
class="list-group-item d-flex justify-content-between align-items-center"
|
|
39
|
+
:class-list-group-item-action=${!!item.link}
|
|
40
|
+
:class-active=${item.active}
|
|
41
|
+
:class-disabled=${item.disabled}
|
|
42
|
+
href=${item.link ?? null}
|
|
43
|
+
aria-current=${item.active ? 'true' : null}
|
|
44
|
+
aria-disabled=${item.disabled ? 'true' : null}>
|
|
45
|
+
|
|
46
|
+
<span>
|
|
47
|
+
<span :class-fw-bold=${!!item.text}>${item.name}</span>
|
|
48
|
+
<span class="d-block small opacity-75"
|
|
49
|
+
:if=${item.text}>${item.text}</span>
|
|
50
|
+
</span>
|
|
51
|
+
|
|
52
|
+
<bs-badge :variant=${item.badge?.variant ?? 'secondary'}
|
|
53
|
+
:pill
|
|
54
|
+
:if=${item.badge}>${item.badge?.name ?? item.badge}</bs-badge>
|
|
55
|
+
</a>
|
|
56
|
+
</:define>
|
|
57
|
+
|
|
58
|
+
</lib>
|
package/parts/modal.htm
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
<lib>
|
|
2
|
+
<:import src="base.htm" />
|
|
3
|
+
<:import src="button.htm" />
|
|
4
|
+
|
|
5
|
+
<!---
|
|
6
|
+
A modal. Opened the Bootstrap way, by a trigger naming it:
|
|
7
|
+
|
|
8
|
+
<bs-button :toggle="modal" :target="#signup">Sign up</bs-button>
|
|
9
|
+
<bs-modal :name="signup" :title="Sign up">…</bs-modal>
|
|
10
|
+
|
|
11
|
+
The footer is a named slot with a working default, so a modal that only
|
|
12
|
+
needs a Close button doesn't have to say so. That is also why it isn't an
|
|
13
|
+
optional region: a `<:slot>` can't live inside a `:for-each`, and a
|
|
14
|
+
fallback is the better answer here anyway.
|
|
15
|
+
|
|
16
|
+
`:open` is the other way in — a value the page owns, which lets the modal
|
|
17
|
+
be driven by state rather than by a click. `:handle-open` is the door to
|
|
18
|
+
the imperative half of the DOM: `show()` and `hide()` are verbs, so no
|
|
19
|
+
amount of markup expresses them, and a handler is what runs them.
|
|
20
|
+
-->
|
|
21
|
+
<:define tag="bs-modal:div"
|
|
22
|
+
class="modal fade ${extra ?? ''}"
|
|
23
|
+
tabindex="-1"
|
|
24
|
+
|
|
25
|
+
// parameters
|
|
26
|
+
:extra=${null} // extra classes for this instance
|
|
27
|
+
:name=${null}
|
|
28
|
+
:title=${''}
|
|
29
|
+
:size=${null} // 'sm' | 'lg' | 'xl'
|
|
30
|
+
:centered=${false}
|
|
31
|
+
:scrollable=${false}
|
|
32
|
+
:fullscreen=${null} // true | 'sm-down' | 'md-down' | ...
|
|
33
|
+
:staticBackdrop=${false}
|
|
34
|
+
:open=${false}
|
|
35
|
+
|
|
36
|
+
// private
|
|
37
|
+
:_labelId=${`bs-modal-title-${$id}`}
|
|
38
|
+
:_dialogClass=${[
|
|
39
|
+
'modal-dialog',
|
|
40
|
+
size ? `modal-${size}` : '',
|
|
41
|
+
centered ? 'modal-dialog-centered' : '',
|
|
42
|
+
scrollable ? 'modal-dialog-scrollable' : '',
|
|
43
|
+
fullscreen === true ? 'modal-fullscreen' : '',
|
|
44
|
+
typeof fullscreen === 'string' ? `modal-fullscreen-${fullscreen}` : '',
|
|
45
|
+
].filter(s => s).join(' ')}
|
|
46
|
+
|
|
47
|
+
id=${name}
|
|
48
|
+
aria-labelledby=${_labelId}
|
|
49
|
+
aria-hidden="true"
|
|
50
|
+
data-bs-backdrop=${staticBackdrop ? 'static' : null}
|
|
51
|
+
data-bs-keyboard=${staticBackdrop ? 'false' : null}
|
|
52
|
+
|
|
53
|
+
/*
|
|
54
|
+
`globalThis` rather than a bare `bootstrap`: the standard
|
|
55
|
+
library is on the scope chain, a page's own libraries are not,
|
|
56
|
+
and going through `globalThis` is what keeps "this line needs a
|
|
57
|
+
browser" visible in the line
|
|
58
|
+
*/
|
|
59
|
+
:handle-open=${(v) => {
|
|
60
|
+
// only while this markup is in the page: an `:if`/`:for-data` region
|
|
61
|
+
// can take it out with `open` still true, and Bootstrap asked
|
|
62
|
+
// to show a detached element walks into its own internals
|
|
63
|
+
if (!$dom.isConnected) return;
|
|
64
|
+
const modal = globalThis.bootstrap.Modal.getOrCreateInstance($dom);
|
|
65
|
+
v ? modal.show() : modal.hide();
|
|
66
|
+
}}
|
|
67
|
+
/*
|
|
68
|
+
Bootstrap's plugins own DOM outside this element -- a backdrop,
|
|
69
|
+
a popper, a body class -- so an element removed without a
|
|
70
|
+
dispose() leaves it behind. `:will-detach` rather than
|
|
71
|
+
`:will-dispose`: an `:if`/`:for-data` region takes its markup out of
|
|
72
|
+
the page without its scope going anywhere, and the leak is the
|
|
73
|
+
same either way
|
|
74
|
+
Bootstrap's plugins own DOM and page state outside this
|
|
75
|
+
element -- a backdrop, a scroll lock on <body> -- and
|
|
76
|
+
`dispose()` releases the plugin without undoing any of it for
|
|
77
|
+
one that is still open. So the element is put back the way it
|
|
78
|
+
was found, which is what a page removing an open modal
|
|
79
|
+
would otherwise be left needing to do by hand.
|
|
80
|
+
|
|
81
|
+
`:will-detach` rather than `:will-dispose`: an `:if`/`:for-data`
|
|
82
|
+
region takes its markup out of the page without its scope going
|
|
83
|
+
anywhere, and the leak is identical either way
|
|
84
|
+
*/
|
|
85
|
+
:will-detach=${() => {
|
|
86
|
+
const doc = globalThis.document;
|
|
87
|
+
globalThis.bootstrap.Modal.getInstance($dom)?.dispose();
|
|
88
|
+
$dom.style.display = '';
|
|
89
|
+
$dom.classList.remove('show');
|
|
90
|
+
// only once nothing else is holding the page open
|
|
91
|
+
if (doc.querySelector('.modal.show')) return;
|
|
92
|
+
doc.body.classList.remove('modal-open');
|
|
93
|
+
doc.body.style.overflow = '';
|
|
94
|
+
doc.body.style.paddingRight = '';
|
|
95
|
+
}}
|
|
96
|
+
:on-hidden.bs.modal=${() => open = false}
|
|
97
|
+
:on-shown.bs.modal=${() => open = true}
|
|
98
|
+
>
|
|
99
|
+
<div class=${_dialogClass}>
|
|
100
|
+
<div class="modal-content">
|
|
101
|
+
<div class="modal-header">
|
|
102
|
+
<h1 class="modal-title fs-5" id=${_labelId}>${title}</h1>
|
|
103
|
+
<bs-close :dismiss="modal" />
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<div class="modal-body">
|
|
107
|
+
<:slot />
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<div class="modal-footer">
|
|
111
|
+
<:slot name="footer">
|
|
112
|
+
<bs-button :variant="secondary" :dismiss="modal">Close</bs-button>
|
|
113
|
+
</:slot>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</:define>
|
|
118
|
+
|
|
119
|
+
</lib>
|
package/parts/nav.htm
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
<lib>
|
|
2
|
+
<:import src="base.htm" />
|
|
3
|
+
|
|
4
|
+
<!---
|
|
5
|
+
Navs, pills, tabs and underline are one component in Bootstrap and one
|
|
6
|
+
here too: they differ by a class, not by structure.
|
|
7
|
+
|
|
8
|
+
Entries are `{ name, link, active, disabled, target }`. For a tabbed
|
|
9
|
+
interface set `:toggle="tab"` (or `"pill"`) and give each entry the
|
|
10
|
+
`target` of its pane; `bs-tab-pane` below is the other half.
|
|
11
|
+
-->
|
|
12
|
+
<:define tag="bs-nav:ul"
|
|
13
|
+
|
|
14
|
+
// parameters
|
|
15
|
+
:extra=${null} // extra classes for this instance
|
|
16
|
+
:items=${[
|
|
17
|
+
{ name: 'Active', link: '#', active: true },
|
|
18
|
+
{ name: 'Link', link: '#' },
|
|
19
|
+
{ name: 'Disabled', link: '#', disabled: true },
|
|
20
|
+
]}
|
|
21
|
+
:variant=${null} // 'tabs' | 'pills' | 'underline'
|
|
22
|
+
:fill=${false}
|
|
23
|
+
:justified=${false}
|
|
24
|
+
:vertical=${false}
|
|
25
|
+
:align=${'start'} // 'start' | 'center' | 'end'
|
|
26
|
+
:toggle=${null} // 'tab' | 'pill' — for tabbed panes
|
|
27
|
+
|
|
28
|
+
// private
|
|
29
|
+
:_class=${[
|
|
30
|
+
'nav',
|
|
31
|
+
variant ? `nav-${variant}` : '',
|
|
32
|
+
fill ? 'nav-fill' : '',
|
|
33
|
+
justified ? 'nav-justified' : '',
|
|
34
|
+
vertical ? 'flex-column' : '',
|
|
35
|
+
align === 'center' ? 'justify-content-center' : '',
|
|
36
|
+
align === 'end' ? 'justify-content-end' : '',
|
|
37
|
+
extra,
|
|
38
|
+
].filter(s => s).join(' ')}
|
|
39
|
+
|
|
40
|
+
class=${_class}
|
|
41
|
+
role=${toggle ? 'tablist' : null}
|
|
42
|
+
>
|
|
43
|
+
<li class="nav-item"
|
|
44
|
+
:for-each=${items}
|
|
45
|
+
:for-as="item"
|
|
46
|
+
role=${toggle ? 'presentation' : null}>
|
|
47
|
+
<button class="nav-link"
|
|
48
|
+
type="button"
|
|
49
|
+
:if=${toggle}
|
|
50
|
+
:class-active=${item.active}
|
|
51
|
+
:attr-disabled=${item.disabled}
|
|
52
|
+
data-bs-toggle=${toggle}
|
|
53
|
+
data-bs-target=${item.target}
|
|
54
|
+
role="tab"
|
|
55
|
+
aria-selected=${item.active ? 'true' : 'false'}>${item.name}</button>
|
|
56
|
+
|
|
57
|
+
<a class="nav-link"
|
|
58
|
+
:else
|
|
59
|
+
:class-active=${item.active}
|
|
60
|
+
:class-disabled=${item.disabled}
|
|
61
|
+
href=${item.link ?? null}
|
|
62
|
+
aria-current=${item.active ? 'page' : null}
|
|
63
|
+
aria-disabled=${item.disabled ? 'true' : null}>${item.name}</a>
|
|
64
|
+
</li>
|
|
65
|
+
</:define>
|
|
66
|
+
|
|
67
|
+
<!--- The panes a `:toggle`-ing `bs-nav` switches between. -->
|
|
68
|
+
<:define tag="bs-tab-content:div" class="tab-content">
|
|
69
|
+
<:slot />
|
|
70
|
+
</:define>
|
|
71
|
+
|
|
72
|
+
<:define tag="bs-tab-pane:div"
|
|
73
|
+
class="tab-pane fade ${extra ?? ''}"
|
|
74
|
+
role="tabpanel"
|
|
75
|
+
|
|
76
|
+
// parameters
|
|
77
|
+
:extra=${null} // extra classes for this instance
|
|
78
|
+
:name=${null}
|
|
79
|
+
:active=${false}
|
|
80
|
+
|
|
81
|
+
id=${name}
|
|
82
|
+
:class-show=${active}
|
|
83
|
+
:class-active=${active}
|
|
84
|
+
tabindex="0"
|
|
85
|
+
>
|
|
86
|
+
<:slot />
|
|
87
|
+
</:define>
|
|
88
|
+
|
|
89
|
+
</lib>
|
package/parts/navbar.htm
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<lib>
|
|
2
|
+
<:import src="base.htm" />
|
|
3
|
+
|
|
4
|
+
<!---
|
|
5
|
+
Bootstrap's navbar, driven by an array of entries rather than by writing
|
|
6
|
+
`<li><a class="nav-link">` per link. `:items` is this component's API, not
|
|
7
|
+
something from the Bootstrap docs — see the default below for its shape;
|
|
8
|
+
`button: true` turns an entry into a call to action.
|
|
9
|
+
|
|
10
|
+
The collapse target needs an id that no other navbar on the page shares,
|
|
11
|
+
and `$id` is exactly that: unique per instance, so two navbars can't
|
|
12
|
+
collide and the caller never has to invent a name.
|
|
13
|
+
|
|
14
|
+
Two named slots: the unnamed one is the brand, `end` is whatever should
|
|
15
|
+
sit past the links — a search box, a theme toggle, an avatar.
|
|
16
|
+
-->
|
|
17
|
+
<:define tag="bs-navbar:nav"
|
|
18
|
+
|
|
19
|
+
// parameters
|
|
20
|
+
:extra=${null} // extra classes for this instance
|
|
21
|
+
:items=${[
|
|
22
|
+
{ name: 'Home', link: '#', active: true },
|
|
23
|
+
{ name: 'Features', link: '#' },
|
|
24
|
+
{ name: 'Pricing', link: '#' },
|
|
25
|
+
]}
|
|
26
|
+
:brand=${'#'}
|
|
27
|
+
:expand=${'lg'}
|
|
28
|
+
:container=${'container'} // 'container' | 'container-fluid' | ...
|
|
29
|
+
:sticky=${false}
|
|
30
|
+
:fixed=${false}
|
|
31
|
+
:theme=${null} // 'light' | 'dark' — independent of the page
|
|
32
|
+
:bg=${'body-tertiary'}
|
|
33
|
+
|
|
34
|
+
// private
|
|
35
|
+
:_id=${`bs-navbar-${$id}`}
|
|
36
|
+
:_class=${[
|
|
37
|
+
'navbar',
|
|
38
|
+
expand ? `navbar-expand-${expand}` : '',
|
|
39
|
+
bg ? `bg-${bg}` : '',
|
|
40
|
+
extra,
|
|
41
|
+
].filter(s => s).join(' ')}
|
|
42
|
+
|
|
43
|
+
class=${_class}
|
|
44
|
+
:class-sticky-top=${sticky}
|
|
45
|
+
:class-fixed-top=${fixed}
|
|
46
|
+
data-bs-theme=${theme}
|
|
47
|
+
>
|
|
48
|
+
<div class=${container}>
|
|
49
|
+
<a class="navbar-brand" href=${brand}><:slot /></a>
|
|
50
|
+
|
|
51
|
+
<button class="navbar-toggler"
|
|
52
|
+
type="button"
|
|
53
|
+
data-bs-toggle="collapse"
|
|
54
|
+
data-bs-target="#${_id}"
|
|
55
|
+
aria-controls=${_id}
|
|
56
|
+
aria-expanded="false"
|
|
57
|
+
aria-label="Toggle navigation">
|
|
58
|
+
<span class="navbar-toggler-icon"></span>
|
|
59
|
+
</button>
|
|
60
|
+
|
|
61
|
+
<div class="collapse navbar-collapse" id=${_id}>
|
|
62
|
+
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
|
63
|
+
<li class="nav-item" :for-each=${items} :for-as="item">
|
|
64
|
+
<a class="nav-link"
|
|
65
|
+
:class-active=${item.active}
|
|
66
|
+
:class-disabled=${item.disabled}
|
|
67
|
+
:class-btn=${item.button}
|
|
68
|
+
:class-btn-primary=${item.button}
|
|
69
|
+
href=${item.link ?? null}
|
|
70
|
+
aria-current=${item.active ? 'page' : null}
|
|
71
|
+
aria-disabled=${item.disabled ? 'true' : null}>${item.name}</a>
|
|
72
|
+
</li>
|
|
73
|
+
</ul>
|
|
74
|
+
|
|
75
|
+
<div class="d-flex gap-2 align-items-center">
|
|
76
|
+
<:slot name="end" />
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</:define>
|
|
81
|
+
|
|
82
|
+
</lib>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<lib>
|
|
2
|
+
<:import src="base.htm" />
|
|
3
|
+
<:import src="button.htm" />
|
|
4
|
+
|
|
5
|
+
<!---
|
|
6
|
+
An offcanvas panel: the same shape as `bs-modal`, opened by a trigger
|
|
7
|
+
naming it or by the `:open` value.
|
|
8
|
+
|
|
9
|
+
<bs-button :toggle="offcanvas" :target="#menu">Menu</bs-button>
|
|
10
|
+
<bs-offcanvas :name="menu" :title="Menu">…</bs-offcanvas>
|
|
11
|
+
-->
|
|
12
|
+
<:define tag="bs-offcanvas:div"
|
|
13
|
+
class="offcanvas"
|
|
14
|
+
tabindex="-1"
|
|
15
|
+
|
|
16
|
+
// parameters
|
|
17
|
+
:extra=${null} // extra classes for this instance
|
|
18
|
+
:name=${null}
|
|
19
|
+
:title=${''}
|
|
20
|
+
:placement=${'start'} // 'start' | 'end' | 'top' | 'bottom'
|
|
21
|
+
:backdrop=${true}
|
|
22
|
+
:scroll=${false}
|
|
23
|
+
:responsive=${null} // 'lg' — an offcanvas below this breakpoint only
|
|
24
|
+
:open=${false}
|
|
25
|
+
|
|
26
|
+
// private
|
|
27
|
+
:_labelId=${`bs-offcanvas-title-${$id}`}
|
|
28
|
+
:_class=${[
|
|
29
|
+
responsive ? `offcanvas-${responsive}` : 'offcanvas',
|
|
30
|
+
`offcanvas-${placement}`,
|
|
31
|
+
extra,
|
|
32
|
+
].join(' ')}
|
|
33
|
+
|
|
34
|
+
class=${_class}
|
|
35
|
+
id=${name}
|
|
36
|
+
aria-labelledby=${_labelId}
|
|
37
|
+
data-bs-backdrop=${backdrop ? null : 'false'}
|
|
38
|
+
data-bs-scroll=${scroll ? 'true' : null}
|
|
39
|
+
|
|
40
|
+
:handle-open=${(v) => {
|
|
41
|
+
// only while this markup is in the page: an `:if`/`:for-data` region
|
|
42
|
+
// can take it out with `open` still true, and Bootstrap asked
|
|
43
|
+
// to show a detached element walks into its own internals
|
|
44
|
+
if (!$dom.isConnected) return;
|
|
45
|
+
const panel = globalThis.bootstrap.Offcanvas.getOrCreateInstance($dom);
|
|
46
|
+
v ? panel.show() : panel.hide();
|
|
47
|
+
}}
|
|
48
|
+
/*
|
|
49
|
+
Bootstrap's plugins own DOM outside this element -- a backdrop,
|
|
50
|
+
a popper, a body class -- so an element removed without a
|
|
51
|
+
dispose() leaves it behind. `:will-detach` rather than
|
|
52
|
+
`:will-dispose`: an `:if`/`:for-data` region takes its markup out of
|
|
53
|
+
the page without its scope going anywhere, and the leak is the
|
|
54
|
+
same either way
|
|
55
|
+
Bootstrap's plugins own DOM and page state outside this
|
|
56
|
+
element -- a backdrop, a scroll lock on <body> -- and
|
|
57
|
+
`dispose()` releases the plugin without undoing any of it for
|
|
58
|
+
one that is still open. So the element is put back the way it
|
|
59
|
+
was found, which is what a page removing an open offcanvas
|
|
60
|
+
would otherwise be left needing to do by hand.
|
|
61
|
+
|
|
62
|
+
`:will-detach` rather than `:will-dispose`: an `:if`/`:for-data`
|
|
63
|
+
region takes its markup out of the page without its scope going
|
|
64
|
+
anywhere, and the leak is identical either way
|
|
65
|
+
*/
|
|
66
|
+
:will-detach=${() => {
|
|
67
|
+
const doc = globalThis.document;
|
|
68
|
+
globalThis.bootstrap.Offcanvas.getInstance($dom)?.dispose();
|
|
69
|
+
$dom.style.display = '';
|
|
70
|
+
$dom.classList.remove('show');
|
|
71
|
+
// only once nothing else is holding the page open
|
|
72
|
+
if (doc.querySelector('.offcanvas.show')) return;
|
|
73
|
+
doc.body.classList.remove('offcanvas-open');
|
|
74
|
+
doc.body.style.overflow = '';
|
|
75
|
+
doc.body.style.paddingRight = '';
|
|
76
|
+
}}
|
|
77
|
+
:on-hidden.bs.offcanvas=${() => open = false}
|
|
78
|
+
:on-shown.bs.offcanvas=${() => open = true}
|
|
79
|
+
>
|
|
80
|
+
<div class="offcanvas-header">
|
|
81
|
+
<h5 class="offcanvas-title" id=${_labelId}>${title}</h5>
|
|
82
|
+
<bs-close :dismiss="offcanvas" />
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div class="offcanvas-body">
|
|
86
|
+
<:slot />
|
|
87
|
+
</div>
|
|
88
|
+
</:define>
|
|
89
|
+
|
|
90
|
+
</lib>
|