basecoat-cli 0.1.1 → 0.2.0-beta.1

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.
@@ -1,75 +1,196 @@
1
- window.basecoat = window.basecoat || {};
2
- window.basecoat.registerToast = function(Alpine) {
3
- if (Alpine.components && Alpine.components.toast) return;
4
-
5
- Alpine.store('toaster', { isPaused: false });
6
- Alpine.data('toast', (config={}) => ({
7
- config: config,
8
- open: false,
9
- timeoutDuration: null,
10
- timeoutId: null,
11
-
12
- init() {
13
- if (config.duration !== -1) {
14
- this.timeoutDuration = config.duration || (config.category === 'error' ? 5000 : 3000);
15
- this.timeoutId = setTimeout(() => { this.close() }, this.timeoutDuration);
1
+ (() => {
2
+ let toaster;
3
+ const toasts = new WeakMap();
4
+ let isPaused = false;
5
+ const ICONS = {
6
+ success: '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>',
7
+ error: '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>',
8
+ info: '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>',
9
+ warning: '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>'
10
+ };
11
+
12
+ function initToaster(toasterElement) {
13
+ if (toasterElement.dataset.toasterInitialized) return;
14
+ toaster = toasterElement;
15
+
16
+ toaster.addEventListener('mouseenter', pauseAllTimeouts);
17
+ toaster.addEventListener('mouseleave', resumeAllTimeouts);
18
+ toaster.addEventListener('click', (e) => {
19
+ const actionLink = e.target.closest('.toast footer a');
20
+ const actionButton = e.target.closest('.toast footer button');
21
+ if (actionLink || actionButton) {
22
+ closeToast(e.target.closest('.toast'));
23
+ }
24
+ });
25
+
26
+ toaster.querySelectorAll('.toast:not([data-toast-initialized])').forEach(initToast);
27
+ toaster.dataset.toasterInitialized = 'true';
28
+ }
29
+
30
+ function initToast(element) {
31
+ if (element.dataset.toastInitialized) return;
32
+
33
+ const duration = parseInt(element.dataset.duration);
34
+ const timeoutDuration = duration !== -1
35
+ ? duration || (element.dataset.category === 'error' ? 5000 : 3000)
36
+ : -1;
37
+
38
+ const state = {
39
+ remainingTime: timeoutDuration,
40
+ timeoutId: null,
41
+ startTime: null,
42
+ };
43
+
44
+ if (timeoutDuration !== -1) {
45
+ if (isPaused) {
46
+ state.timeoutId = null;
47
+ } else {
48
+ state.startTime = Date.now();
49
+ state.timeoutId = setTimeout(() => closeToast(element), timeoutDuration);
50
+ }
51
+ }
52
+ toasts.set(element, state);
53
+
54
+ element.dataset.toastInitialized = 'true';
55
+ }
56
+
57
+ function pauseAllTimeouts() {
58
+ if (isPaused) return;
59
+
60
+ isPaused = true;
61
+
62
+ toaster.querySelectorAll('.toast:not([aria-hidden="true"])').forEach(element => {
63
+ if (!toasts.has(element)) return;
64
+
65
+ const state = toasts.get(element);
66
+ if (state.timeoutId) {
67
+ clearTimeout(state.timeoutId);
68
+ state.timeoutId = null;
69
+ state.remainingTime -= Date.now() - state.startTime;
16
70
  }
17
- this.open = true;
18
- this.$watch('$store.toaster.isPaused', (isPaused) => {
19
- if (!this.open) return;
20
- if (isPaused) {
21
- this.pauseTimeout();
71
+ });
72
+ }
73
+
74
+ function resumeAllTimeouts() {
75
+ if (!isPaused) return;
76
+
77
+ isPaused = false;
78
+
79
+ toaster.querySelectorAll('.toast:not([aria-hidden="true"])').forEach(element => {
80
+ if (!toasts.has(element)) return;
81
+
82
+ const state = toasts.get(element);
83
+ if (state.remainingTime !== -1 && !state.timeoutId) {
84
+ if (state.remainingTime > 0) {
85
+ state.startTime = Date.now();
86
+ state.timeoutId = setTimeout(() => closeToast(element), state.remainingTime);
22
87
  } else {
23
- this.resumeTimeout();
88
+ closeToast(element);
24
89
  }
25
- });
26
- },
27
- pauseTimeout() {
28
- clearTimeout(this.timeoutId);
29
- this.timeoutId = null;
30
- },
31
- resumeTimeout(index) {
32
- if (this.open && this.timeoutId === null) {
33
- this.timeoutId = setTimeout(() => { this.close() }, this.timeoutDuration);
34
- }
35
- },
36
- close() {
37
- this.pauseTimeout();
38
- this.open = false;
39
- this.$el.blur();
40
- },
41
- executeAction(actionString) {
42
- if (actionString) {
43
- Alpine.evaluate(this.$el, actionString);
44
90
  }
45
- },
91
+ });
92
+ }
46
93
 
47
- $toastBindings: {
48
- ['@mouseenter']() { this.$store.toaster.isPaused = true },
49
- ['@mouseleave']() { this.$store.toaster.isPaused = false },
50
- ['@keydown.escape.prevent']() { this.close() },
51
- [':aria-hidden']() { return !this.open }
52
- },
53
- }));
94
+ function closeToast(element) {
95
+ if (!toasts.has(element)) return;
54
96
 
55
- Alpine.magic('toast', (el) => (config, toasterId='toaster') => {
56
- const toaster = document.getElementById(toasterId);
57
- const template = document.getElementById('toast-template');
97
+ const state = toasts.get(element);
98
+ clearTimeout(state.timeoutId);
99
+ toasts.delete(element);
100
+
101
+ if (document.activeElement) document.activeElement.blur();
102
+ element.setAttribute('aria-hidden', 'true');
103
+ element.addEventListener('transitionend', () => element.remove(), { once: true });
104
+ }
58
105
 
59
- if (!toaster) {
60
- console.error(`Toaster container with id #${toasterId} not found.`);
61
- return;
106
+ function executeAction(button, toast) {
107
+ const actionString = button.dataset.toastAction;
108
+ if (!actionString) return;
109
+ try {
110
+ const func = new Function('close', actionString);
111
+ func(() => closeToast(toast));
112
+ } catch (e) {
113
+ console.error('Error executing toast action:', e);
62
114
  }
63
- if (!template) {
64
- console.error('Toast template with id #toast-template not found.');
115
+ }
116
+
117
+ function createToast(config) {
118
+ const {
119
+ category = 'info',
120
+ title,
121
+ description,
122
+ action,
123
+ cancel,
124
+ duration,
125
+ icon,
126
+ } = config;
127
+
128
+ const iconHtml = icon || (category && ICONS[category]) || '';
129
+ const titleHtml = title ? `<h2>${title}</h2>` : '';
130
+ const descriptionHtml = description ? `<p>${description}</p>` : '';
131
+ const actionHtml = action?.href
132
+ ? `<a href="${action.href}" class="btn" data-toast-action>${action.label}</a>`
133
+ : action?.onclick
134
+ ? `<button type="button" class="btn" data-toast-action onclick="${action.onclick}">${action.label}</button>`
135
+ : '';
136
+ const cancelHtml = cancel
137
+ ? `<button type="button" class="btn-outline h-6 text-xs px-2.5 rounded-sm" data-toast-cancel onclick="${cancel?.onclick}">${cancel.label}</button>`
138
+ : '';
139
+
140
+ const footerHtml = actionHtml || cancelHtml ? `<footer>${actionHtml}${cancelHtml}</footer>` : '';
141
+
142
+ const html = `
143
+ <div
144
+ class="toast"
145
+ role="${category === 'error' ? 'alert' : 'status'}"
146
+ aria-atomic="true"
147
+ ${category ? `data-category="${category}"` : ''}
148
+ ${duration !== undefined ? `data-duration="${duration}"` : ''}
149
+ >
150
+ <div class="toast-content">
151
+ ${iconHtml}
152
+ <section>
153
+ ${titleHtml}
154
+ ${descriptionHtml}
155
+ </section>
156
+ ${footerHtml}
157
+ </div>
158
+ </div>
159
+ </div>
160
+ `;
161
+ const template = document.createElement('template');
162
+ template.innerHTML = html.trim();
163
+ return template.content.firstChild;
164
+ }
165
+
166
+ const initialToaster = document.getElementById('toaster');
167
+ if (initialToaster) initToaster(initialToaster);
168
+
169
+ window.addEventListener('basecoat:toast', (e) => {
170
+ if (!toaster) {
171
+ console.error('Cannot create toast: toaster container not found on page.');
65
172
  return;
66
173
  }
174
+ const config = e.detail?.config || {};
175
+ const toastElement = createToast(config);
176
+ toaster.appendChild(toastElement);
177
+ });
67
178
 
68
- const clone = template.content.firstElementChild.cloneNode(true);
69
-
70
- clone.setAttribute('x-data', `toast(${JSON.stringify(config)})`);
71
- clone.removeAttribute('id');
179
+ const observer = new MutationObserver((mutations) => {
180
+ mutations.forEach((mutation) => {
181
+ mutation.addedNodes.forEach((node) => {
182
+ if (node.nodeType !== Node.ELEMENT_NODE) return;
183
+
184
+ if (node.matches('#toaster')) {
185
+ initToaster(node);
186
+ }
72
187
 
73
- toaster.appendChild(clone);
188
+ if (toaster && node.matches('.toast:not([data-toast-initialized])')) {
189
+ initToast(node);
190
+ }
191
+ });
192
+ });
74
193
  });
75
- };
194
+
195
+ observer.observe(document.body, { childList: true, subtree: true });
196
+ })();
@@ -0,0 +1 @@
1
+ (()=>{let t;const e=new WeakMap;let n=!1;const o={success:'<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>',error:'<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>',info:'<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>',warning:'<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>'};function i(e){e.dataset.toasterInitialized||(t=e,t.addEventListener("mouseenter",r),t.addEventListener("mouseleave",s),t.addEventListener("click",(t=>{const e=t.target.closest(".toast footer a"),n=t.target.closest(".toast footer button");(e||n)&&d(t.target.closest(".toast"))})),t.querySelectorAll(".toast:not([data-toast-initialized])").forEach(a),t.dataset.toasterInitialized="true")}function a(t){if(t.dataset.toastInitialized)return;const o=parseInt(t.dataset.duration),i=-1!==o?o||("error"===t.dataset.category?5e3:3e3):-1,a={remainingTime:i,timeoutId:null,startTime:null};-1!==i&&(n?a.timeoutId=null:(a.startTime=Date.now(),a.timeoutId=setTimeout((()=>d(t)),i))),e.set(t,a),t.dataset.toastInitialized="true"}function r(){n||(n=!0,t.querySelectorAll('.toast:not([aria-hidden="true"])').forEach((t=>{if(!e.has(t))return;const n=e.get(t);n.timeoutId&&(clearTimeout(n.timeoutId),n.timeoutId=null,n.remainingTime-=Date.now()-n.startTime)})))}function s(){n&&(n=!1,t.querySelectorAll('.toast:not([aria-hidden="true"])').forEach((t=>{if(!e.has(t))return;const n=e.get(t);-1===n.remainingTime||n.timeoutId||(n.remainingTime>0?(n.startTime=Date.now(),n.timeoutId=setTimeout((()=>d(t)),n.remainingTime)):d(t))})))}function d(t){if(!e.has(t))return;const n=e.get(t);clearTimeout(n.timeoutId),e.delete(t),document.activeElement&&document.activeElement.blur(),t.setAttribute("aria-hidden","true"),t.addEventListener("transitionend",(()=>t.remove()),{once:!0})}const c=document.getElementById("toaster");c&&i(c),window.addEventListener("basecoat:toast",(e=>{if(!t)return void console.error("Cannot create toast: toaster container not found on page.");const n=function(t){const{category:e="info",title:n,description:i,action:a,cancel:r,duration:s,icon:d}=t,c=d||e&&o[e]||"",l=n?`<h2>${n}</h2>`:"",u=i?`<p>${i}</p>`:"",h=a?.href?`<a href="${a.href}" class="btn" data-toast-action>${a.label}</a>`:a?.onclick?`<button type="button" class="btn" data-toast-action onclick="${a.onclick}">${a.label}</button>`:"",m=r?`<button type="button" class="btn-outline h-6 text-xs px-2.5 rounded-sm" data-toast-cancel onclick="${r?.onclick}">${r.label}</button>`:"",g=`\n <div\n class="toast"\n role="${"error"===e?"alert":"status"}"\n aria-atomic="true"\n ${e?`data-category="${e}"`:""}\n ${void 0!==s?`data-duration="${s}"`:""}\n >\n <div class="toast-content">\n ${c}\n <section>\n ${l}\n ${u}\n </section>\n ${h||m?`<footer>${h}${m}</footer>`:""}\n </div>\n </div>\n </div>\n `,v=document.createElement("template");return v.innerHTML=g.trim(),v.content.firstChild}(e.detail?.config||{});t.appendChild(n)}));new MutationObserver((e=>{e.forEach((e=>{e.addedNodes.forEach((e=>{e.nodeType===Node.ELEMENT_NODE&&(e.matches("#toaster")&&i(e),t&&e.matches(".toast:not([data-toast-initialized])")&&a(e))}))}))})).observe(document.body,{childList:!0,subtree:!0})})();
@@ -17,94 +17,78 @@
17
17
  @param close_on_overlay_click {boolean} [optional] [default=true] - Whether clicking the overlay closes the dialog.
18
18
  #}
19
19
  {% macro dialog(
20
- id,
20
+ id=None,
21
21
  trigger=None,
22
22
  title=None,
23
23
  description=None,
24
24
  footer=None,
25
- main_attrs={},
25
+ dialog_attrs={},
26
26
  trigger_attrs={},
27
- content_attrs={},
28
- content_header_attrs={},
29
- content_body_attrs={},
30
- content_footer_attrs={},
27
+ header_attrs={},
28
+ body_attrs={},
29
+ footer_attrs={},
31
30
  open=false,
32
31
  close_button=true,
33
32
  close_on_overlay_click=true
34
33
  ) %}
35
- <div
34
+ {% set id = id or ("dialog-" + (range(100000, 999999) | random | string)) %}
35
+ {% if trigger %}
36
+ <button
37
+ type="button"
38
+ onclick="document.getElementById('{{ id }}').showModal()"
39
+ {% for key, value in trigger_attrs %}
40
+ {{ key }}="{{ value }}"
41
+ {% endfor %}
42
+ >
43
+ {{ trigger }}
44
+ </button>
45
+ {% endif %}
46
+ <dialog
36
47
  id="{{ id }}"
37
- x-data="dialog({{ 'true' if open else 'false' }}, {{ 'true' if close_on_overlay_click else 'false' }})"
38
- x-bind="$main"
39
48
  class="dialog"
40
- {% for key, value in main_attrs %}
49
+ aria-labelledby="{{ id }}-title"
50
+ {% if description %}aria-describedby="{{ id }}-description"{% endif %}
51
+ {% if close_on_overlay_click %}onclick="this.close()"{% endif %}
52
+ {% for key, value in dialog_attrs %}
41
53
  {{ key }}="{{ value }}"
42
54
  {% endfor %}
43
55
  >
44
- {% if trigger %}
45
- <button
46
- type="button"
47
- aria-expanded="false"
48
- aria-controls="{{ id }}-dialog"
49
- x-bind="$trigger"
50
- {% for key, value in trigger_attrs %}
51
- {{ key }}="{{ value }}"
52
- {% endfor %}
53
- >
54
- {{ trigger }}
55
- </button>
56
- {% endif %}
57
- <div
58
- role="dialog"
59
- id="{{ id }}-dialog"
60
- tabindex="-1"
61
- aria-modal="true"
62
- aria-labelledby="{{ id }}-title"
63
- inert
64
- x-bind="$content"
65
- >
66
- <article
67
- {% for key, value in content_attrs %}
68
- {{ key }}="{{ value }}"
69
- {% endfor %}
70
- >
71
- {% if title or description %}
72
- <header
73
- {% for key, value in content_header_attrs %}
74
- {{ key }}="{{ value }}"
75
- {% endfor %}
76
- >
77
- <h2 id="{{ id }}-title">{{ title | safe }}</h2>
78
- <p>{{ description | safe }}</p>
79
- </header>
80
- {% endif %}
81
- {% if caller %}
82
- <section
83
- {% for key, value in content_body_attrs %}
84
- {{ key }}="{{ value }}"
85
- {% endfor %}
86
- >
87
- {{ caller() }}
88
- </section>
89
- {% endif %}
90
- {% if footer %}
91
- <footer
92
- {% for key, value in content_footer_attrs %}
93
- {{ key }}="{{ value }}"
94
- {% endfor %}
95
- >
96
- {{ footer | safe }}
97
- </footer>
98
- {% endif %}
99
- {% if close_button %}
100
- <button
101
- @click="hide()"
102
- aria-label="Close dialog"
103
- >
56
+ <article {% if close_on_overlay_click %}onclick="event.stopPropagation()"{% endif %}>
57
+ {% if title or description %}
58
+ <header
59
+ {% for key, value in header_attrs %}
60
+ {{ key }}="{{ value }}"
61
+ {% endfor %}
62
+ >
63
+ <h2 id="{{ id }}-title">{{ title | safe }}</h2>
64
+ {% if description %}<p id="{{ id }}-description">{{ description | safe }}</p>{% endif %}
65
+ </header>
66
+ {% endif %}
67
+ {% if caller %}
68
+ <section
69
+ {% for key, value in body_attrs %}
70
+ {{ key }}="{{ value }}"
71
+ {% endfor %}
72
+ >
73
+ {{ caller() }}
74
+ </section>
75
+ {% endif %}
76
+ {% if footer %}
77
+ <footer
78
+ {% for key, value in footer_attrs %}
79
+ {{ key }}="{{ value }}"
80
+ {% endfor %}
81
+ >
82
+ {{ footer | safe }}
83
+ </footer>
84
+ {% endif %}
85
+ {% if close_button %}
86
+ <form method="dialog">
87
+ <button aria-label="Close dialog">
104
88
  <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
105
- </button>
106
- {% endif %}
107
- </article>
108
- </div>
109
- </div>
89
+ </button>
90
+ </form>
91
+ {% endif %}
92
+ </article>
93
+ </dialog>
110
94
  {% endmacro %}
@@ -10,50 +10,46 @@
10
10
  #}
11
11
  {% macro dropdown_menu(
12
12
  id=None,
13
- trigger=None,
14
- menu=None,
15
- main_attrs={},
13
+ trigger,
14
+ items=None,
16
15
  trigger_attrs={},
17
- content_attrs={}
16
+ popover_attrs={},
17
+ menu_attrs={}
18
18
  ) %}
19
- <div
20
- class="popover {{ main_attrs.class }}"
21
- x-data="dropdownMenu"
22
- @click.away="open = false"
23
- {% if id %}id="{{ id }}"{% endif %}
24
- {% for key, value in main_attrs %}
25
- {% if key != "class" %}{{ key }}="{{ value }}"{% endif %}
26
- {% endfor %}
27
- >
28
- {% if trigger %}
19
+ {% set id = id or ("dropdown-menu-" + (range(100000, 999999) | random | string)) %}
20
+
21
+ <div class="dropdown-menu">
29
22
  <button
30
23
  type="button"
31
- aria-haspopup="menu"
32
- aria-expanded="false"
33
- x-bind="$trigger"
34
- {% if id %}
35
24
  id="{{ id }}-trigger"
25
+ popovertarget="{{ id }}"
26
+ aria-haspopup="menu"
36
27
  aria-controls="{{ id }}-menu"
37
- {% endif %}
28
+ aria-expanded="false"
38
29
  {% for key, value in trigger_attrs %}
39
30
  {{ key }}="{{ value }}"
40
31
  {% endfor %}
41
32
  >
42
33
  {{ trigger | safe }}
43
34
  </button>
44
- {% endif %}
45
35
  <div
46
- data-popover
47
- aria-hidden="true"
48
- x-bind="$content"
49
- {% if id %}id="{{ id }}-menu"{% endif %}
50
- {% for key, value in content_attrs %}
51
- {{ key }}="{{ value }}"
36
+ popover
37
+ class="popover p-1 {% if popover_attrs.class %} {{ popover_attrs.class }}{% endif %}"
38
+ id="{{ id }}"
39
+ {% for key, value in popover_attrs %}
40
+ {% if key != "class" %}{{ key }}="{{ value }}"{% endif %}
52
41
  {% endfor %}
53
42
  >
54
- <nav role="menu">
55
- {% if menu %}
56
- {{ render_dropdown_items(menu, id ~ "-items" if id else "items") }}
43
+ <nav
44
+ role="menu"
45
+ id="{{ id }}-menu"
46
+ aria-labelledby="{{ id }}-trigger"
47
+ {% for key, value in menu_attrs %}
48
+ {{ key }}="{{ value }}"
49
+ {% endfor %}
50
+ >
51
+ {% if items %}
52
+ {{ render_dropdown_items(items, id ~ "-items" if id else "items") }}
57
53
  {% else %}
58
54
  {{ caller() if caller }}
59
55
  {% endif %}
@@ -9,47 +9,31 @@
9
9
  #}
10
10
  {% macro popover(
11
11
  id=None,
12
- trigger=None,
13
- main_attrs={},
12
+ trigger,
14
13
  trigger_attrs={},
15
- content_attrs={}
14
+ popover_attrs={}
16
15
  ) %}
16
+ {% set id = id or ("popover-" + (range(100000, 999999) | random | string)) %}
17
+ <button
18
+ type="button"
19
+ popovertarget="{{ id }}"
20
+ id="{{ id }}-trigger"
21
+ onclick="this.setAttribute('aria-expanded', this.getAttribute('aria-expanded') === 'false' ? 'true' : 'false')"
22
+ aria-expanded="false"
23
+ {% for key, value in trigger_attrs %}
24
+ {{ key }}="{{ value }}"
25
+ {% endfor %}
26
+ >
27
+ {{ trigger | safe }}
28
+ </button>
17
29
  <div
18
- class="popover {{ main_attrs.class }}"
19
- x-data="popover"
20
- @click.away="open = false"
21
- {% if id %}id="{{ id }}"{% endif %}
22
- {% for key, value in main_attrs %}
30
+ popover
31
+ id="{{ id }}"
32
+ class="popover {{ popover_attrs.class }}"
33
+ {% for key, value in popover_attrs %}
23
34
  {% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
24
35
  {% endfor %}
25
- >
26
- {% if trigger %}
27
- <button
28
- type="button"
29
- aria-haspopup="menu"
30
- aria-expanded="false"
31
- x-bind="$trigger"
32
- {% if id %}
33
- id="{{ id }}-trigger"
34
- aria-controls="{{ id }}-menu"
35
- {% endif %}
36
- {% for key, value in trigger_attrs %}
37
- {{ key }}="{{ value }}"
38
- {% endfor %}
39
- >
40
- {{ trigger | safe }}
41
- </button>
42
- {% endif %}
43
- <div
44
- data-popover
45
- aria-hidden="true"
46
- x-bind="$content"
47
- {% if id %}id="{{ id }}-menu"{% endif %}
48
- {% for key, value in content_attrs %}
49
- {{ key }}="{{ value }}"
50
- {% endfor %}
51
- >
52
- {{ caller() if caller }}
53
- </div>
36
+ >
37
+ {{ caller() if caller }}
54
38
  </div>
55
- {% endmacro %}
39
+ {% endmacro %}