@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.
@@ -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>
@@ -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>
@@ -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>
@@ -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>