@keenmate/pure-admin-core 2.4.0 → 2.5.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.
Files changed (44) hide show
  1. package/README.md +11 -6
  2. package/dist/css/main.css +47 -130
  3. package/package.json +1 -1
  4. package/snippets/AUDIT.md +94 -0
  5. package/snippets/alerts.html +264 -89
  6. package/snippets/badges.html +193 -61
  7. package/snippets/buttons.html +178 -0
  8. package/snippets/callouts.html +210 -129
  9. package/snippets/cards.html +383 -200
  10. package/snippets/checkbox-lists.html +199 -65
  11. package/snippets/code.html +55 -11
  12. package/snippets/command-palette.html +401 -111
  13. package/snippets/comparison.html +144 -93
  14. package/snippets/customization.html +311 -104
  15. package/snippets/data-display.html +584 -0
  16. package/snippets/detail-panel.html +470 -138
  17. package/snippets/filter-card.html +246 -0
  18. package/snippets/forms.html +408 -308
  19. package/snippets/grid.html +253 -141
  20. package/snippets/layout.html +379 -480
  21. package/snippets/lists.html +144 -47
  22. package/snippets/loaders.html +64 -39
  23. package/snippets/manifest.json +330 -280
  24. package/snippets/modal-dialogs.html +137 -64
  25. package/snippets/modals.html +221 -151
  26. package/snippets/notifications.html +285 -0
  27. package/snippets/popconfirm.html +213 -19
  28. package/snippets/profile.html +290 -330
  29. package/snippets/statistics.html +247 -0
  30. package/snippets/tables.html +359 -150
  31. package/snippets/tabs.html +129 -45
  32. package/snippets/timeline.html +123 -56
  33. package/snippets/toasts.html +179 -31
  34. package/snippets/tooltips.html +199 -81
  35. package/snippets/typography.html +183 -58
  36. package/snippets/utilities.html +511 -415
  37. package/snippets/virtual-scroll.html +201 -75
  38. package/snippets/web-daterangepicker.html +369 -189
  39. package/snippets/web-multiselect.html +360 -124
  40. package/src/scss/core-components/_alerts.scss +51 -12
  41. package/src/scss/core-components/_pagers.scss +1 -1
  42. package/src/scss/core-components/_popconfirm.scss +35 -13
  43. package/src/scss/core-components/_tables.scss +2 -134
  44. package/src/scss/variables/_components.scss +17 -2
@@ -1,48 +1,23 @@
1
1
  <!-- ================================
2
2
  MODAL SNIPPETS
3
3
  Pure Admin Visual Framework
4
+
5
+ Classic overlay-dialog component. Toggle .pa-modal--show to open;
6
+ class off to close. The default body already scrolls when content
7
+ exceeds the container's max-height — use --scrollable only when
8
+ you want a shorter explicit ceiling.
4
9
  ================================ -->
5
10
 
6
- <!--
7
- MODAL WRAPPER CLASSES:
8
- - pa-modal: Base modal wrapper
9
- - pa-modal--show: Show the modal (add/remove via JavaScript)
10
- - pa-modal--top: Position modal near top instead of center (useful for search/command interfaces)
11
- - pa-modal--static: Prevent closing via ESC key or backdrop click (must use explicit button)
12
- - pa-modal--primary: Primary themed modal (blue header)
13
- - pa-modal--success: Success themed modal (green header)
14
- - pa-modal--warning: Warning themed modal (orange header)
15
- - pa-modal--danger: Danger themed modal (red header)
16
- - pa-modal--info: Info themed modal (cyan header)
17
-
18
- CONTAINER SIZE CLASSES:
19
- - pa-modal__container--sm: Small (20rem / 320px)
20
- - pa-modal__container: Medium/default (30rem / 480px)
21
- - pa-modal__container--lg: Large (50rem / 800px)
22
- - pa-modal__container--xl: Extra large (70rem / 1120px)
23
- - pa-modal__container--xxl: Extra extra large (90rem / 1440px)
24
- - pa-modal__container--fw: Full width (constrained by viewport margins)
25
-
26
- BODY CLASSES:
27
- - pa-modal__body: Base body class
28
- - pa-modal__body--scrollable: Scrollable body with max-height (header/footer fixed)
29
-
30
- HEADER THEME CLASSES (alternative to modal-level themes):
31
- - pa-modal__header--primary: Blue themed header
32
- - pa-modal__header--success: Green themed header
33
- - pa-modal__header--warning: Orange themed header
34
- - pa-modal__header--danger: Red themed header
35
- -->
36
11
 
37
- <!-- BASIC MODAL (Centered) -->
12
+ <!-- BASIC MODAL (centered) -->
38
13
 
39
- <!-- Modal Structure -->
40
14
  <div class="pa-modal pa-modal--show" id="basicModal">
41
15
  <div class="pa-modal__backdrop"></div>
42
16
  <div class="pa-modal__container">
43
17
  <div class="pa-modal__header">
44
18
  <h3 class="pa-modal__title">Modal Title</h3>
45
- <button class="pa-btn pa-btn--primary pa-btn--icon-only pa-btn--sm" onclick="closeModal('basicModal')">✕</button>
19
+ <button class="pa-btn pa-btn--sm pa-btn--icon-only pa-btn--secondary"
20
+ onclick="closeModal('basicModal')" aria-label="Close">✕</button>
46
21
  </div>
47
22
  <div class="pa-modal__body">
48
23
  <p>Modal body content goes here.</p>
@@ -54,19 +29,21 @@ HEADER THEME CLASSES (alternative to modal-level themes):
54
29
  </div>
55
30
  </div>
56
31
 
57
- <!-- TOP-ALIGNED MODAL -->
58
32
 
59
- <!-- Modal positioned near top (5vh from top) -->
33
+ <!-- TOP-ALIGNED MODAL (5vh from top instead of centered) -->
34
+
60
35
  <div class="pa-modal pa-modal--top pa-modal--show" id="topModal">
61
36
  <div class="pa-modal__backdrop"></div>
62
37
  <div class="pa-modal__container">
63
38
  <div class="pa-modal__header">
64
39
  <h3 class="pa-modal__title">Top-Aligned Modal</h3>
65
- <button class="pa-btn pa-btn--primary pa-btn--icon-only pa-btn--sm" onclick="closeModal('topModal')">✕</button>
40
+ <button class="pa-btn pa-btn--sm pa-btn--icon-only pa-btn--secondary"
41
+ onclick="closeModal('topModal')" aria-label="Close">✕</button>
66
42
  </div>
67
43
  <div class="pa-modal__body">
68
- <p>This modal appears near the top of the viewport.</p>
69
- <p>Useful for search interfaces, quick actions, or command palettes.</p>
44
+ <p>Appears near the top of the viewport. Useful for search
45
+ interfaces, quick-action panels, command palette-style
46
+ overlays.</p>
70
47
  </div>
71
48
  <div class="pa-modal__footer">
72
49
  <button class="pa-btn pa-btn--secondary" onclick="closeModal('topModal')">Close</button>
@@ -76,175 +53,172 @@ HEADER THEME CLASSES (alternative to modal-level themes):
76
53
  </div>
77
54
 
78
55
 
79
- <!-- STATIC MODAL (No ESC, No Backdrop Click) -->
56
+ <!-- STATIC MODAL (JS-contract, no CSS) -->
80
57
 
81
58
  <!--
82
- Static modals cannot be closed by:
83
- - Pressing the Escape key
84
- - Clicking the backdrop
85
-
86
- User MUST click an explicit button to close.
87
- Use for: license agreements, critical confirmations, required actions.
88
-
89
- Implementation:
90
- 1. Add pa-modal--static class to the modal wrapper
91
- 2. Do NOT add onclick to the backdrop
92
- 3. Optionally remove the X close button from header
93
- 4. JavaScript must check for --static class before closing on ESC
59
+ `pa-modal--static` is a JS hook, not a CSS rule. The SCSS has no
60
+ .pa-modal--static selector; the framework's example JS below checks
61
+ classList.contains('pa-modal--static') before closing, so backdrop
62
+ clicks and ESC become no-ops.
63
+
64
+ Use for: license acceptances, destructive confirmations that require
65
+ an explicit yes/no, onboarding steps the user shouldn't dismiss by
66
+ accident.
67
+
68
+ - Add .pa-modal--static on the root
69
+ - Don't add an onclick to __backdrop (or your JS will handle it)
70
+ - Usually omit the header close button force the footer choice
94
71
  -->
95
72
 
96
73
  <div class="pa-modal pa-modal--static" id="staticModal">
97
- <div class="pa-modal__backdrop"></div><!-- No onclick here! -->
74
+ <div class="pa-modal__backdrop"></div>
98
75
  <div class="pa-modal__container pa-modal__container--sm">
99
- <div class="pa-modal__header pa-modal__header--warning">
76
+ <!-- Theming lives on the root .pa-modal--{variant}; the header
77
+ picks up colour via descendant selector. No --warning modifier
78
+ on the header itself. -->
79
+ <div class="pa-modal__header">
100
80
  <h3 class="pa-modal__title">License Agreement</h3>
101
- <!-- No X close button - user must use footer buttons -->
102
81
  </div>
103
82
  <div class="pa-modal__body">
104
83
  <p>You must accept the terms to continue.</p>
105
- <p>This modal cannot be dismissed with ESC or by clicking outside.</p>
84
+ <p>This modal cannot be dismissed with Esc or by clicking outside.</p>
106
85
  </div>
107
86
  <div class="pa-modal__footer">
108
87
  <button class="pa-btn pa-btn--secondary" onclick="closeModal('staticModal')">Decline</button>
109
- <button class="pa-btn pa-btn--warning" onclick="closeModal('staticModal')">Accept</button>
88
+ <button class="pa-btn pa-btn--warning" onclick="closeModal('staticModal')">Accept</button>
110
89
  </div>
111
90
  </div>
112
91
  </div>
113
92
 
114
93
 
115
- <!-- MODAL SIZES -->
94
+ <!-- MODAL SIZES (on __container) -->
116
95
 
117
- <!-- Small Modal -->
118
96
  <div class="pa-modal" id="smallModal">
119
97
  <div class="pa-modal__backdrop"></div>
120
98
  <div class="pa-modal__container pa-modal__container--sm">
121
- <!-- content -->
99
+ <!-- 32rem / 320px -->
122
100
  </div>
123
101
  </div>
124
102
 
125
- <!-- Medium Modal (Default) -->
126
103
  <div class="pa-modal" id="mediumModal">
127
104
  <div class="pa-modal__backdrop"></div>
128
105
  <div class="pa-modal__container">
129
- <!-- content -->
106
+ <!-- Default: 48rem / 480px -->
130
107
  </div>
131
108
  </div>
132
109
 
133
- <!-- Large Modal -->
134
110
  <div class="pa-modal" id="largeModal">
135
111
  <div class="pa-modal__backdrop"></div>
136
112
  <div class="pa-modal__container pa-modal__container--lg">
137
- <!-- content -->
113
+ <!-- 80rem / 800px -->
138
114
  </div>
139
115
  </div>
140
116
 
141
- <!-- Extra Large Modal -->
142
117
  <div class="pa-modal" id="xlModal">
143
118
  <div class="pa-modal__backdrop"></div>
144
119
  <div class="pa-modal__container pa-modal__container--xl">
145
- <!-- content -->
120
+ <!-- 112rem / 1120px -->
146
121
  </div>
147
122
  </div>
148
123
 
149
- <!-- 2XL Modal -->
150
124
  <div class="pa-modal" id="xxlModal">
151
125
  <div class="pa-modal__backdrop"></div>
152
126
  <div class="pa-modal__container pa-modal__container--xxl">
153
- <!-- content -->
127
+ <!-- 144rem / 1440px -->
154
128
  </div>
155
129
  </div>
156
130
 
157
- <!-- Full Width Modal -->
131
+ <!-- Full-width: viewport minus $modal-fw-margin (1.6rem default) on every side -->
158
132
  <div class="pa-modal" id="fwModal">
159
133
  <div class="pa-modal__backdrop"></div>
160
134
  <div class="pa-modal__container pa-modal__container--fw">
161
- <!-- content -->
135
+ <!-- Fills viewport with a small gutter -->
162
136
  </div>
163
137
  </div>
164
138
 
165
139
 
166
- <!-- THEMED MODALS -->
140
+ <!-- THEMED MODALS (colored header) -->
141
+
142
+ <!--
143
+ Theming is applied at the ROOT level (.pa-modal--{variant}) and
144
+ cascades into the header via descendant selector. There's no
145
+ pa-modal__header--{variant} class in SCSS.
146
+ Available variants: primary, success, warning, danger.
147
+ (No --info, --secondary, --light, --dark on modals.)
148
+ -->
167
149
 
168
- <!-- Primary Modal -->
169
150
  <div class="pa-modal pa-modal--primary" id="primaryModal">
170
151
  <div class="pa-modal__backdrop"></div>
171
152
  <div class="pa-modal__container">
172
153
  <div class="pa-modal__header">
173
154
  <h3 class="pa-modal__title">Primary Modal</h3>
174
- <button class="pa-btn pa-btn--primary pa-btn--icon-only pa-btn--sm">✕</button>
175
- </div>
176
- <div class="pa-modal__body">
177
- Content here.
155
+ <button class="pa-btn pa-btn--sm pa-btn--icon-only pa-btn--light" aria-label="Close">✕</button>
178
156
  </div>
157
+ <div class="pa-modal__body">Body content.</div>
179
158
  </div>
180
159
  </div>
181
160
 
182
- <!-- Success Modal -->
183
161
  <div class="pa-modal pa-modal--success" id="successModal">
184
162
  <div class="pa-modal__backdrop"></div>
185
163
  <div class="pa-modal__container">
186
164
  <div class="pa-modal__header">
187
165
  <h3 class="pa-modal__title">Success Modal</h3>
188
- <button class="pa-btn pa-btn--primary pa-btn--icon-only pa-btn--sm">✕</button>
189
- </div>
190
- <div class="pa-modal__body">
191
- Content here.
166
+ <button class="pa-btn pa-btn--sm pa-btn--icon-only pa-btn--light" aria-label="Close">✕</button>
192
167
  </div>
168
+ <div class="pa-modal__body">Body content.</div>
193
169
  </div>
194
170
  </div>
195
171
 
196
- <!-- Warning Modal -->
197
172
  <div class="pa-modal pa-modal--warning" id="warningModal">
198
173
  <div class="pa-modal__backdrop"></div>
199
174
  <div class="pa-modal__container">
200
175
  <div class="pa-modal__header">
201
176
  <h3 class="pa-modal__title">Warning Modal</h3>
202
- <button class="pa-btn pa-btn--primary pa-btn--icon-only pa-btn--sm">✕</button>
203
- </div>
204
- <div class="pa-modal__body">
205
- Content here.
177
+ <button class="pa-btn pa-btn--sm pa-btn--icon-only pa-btn--light" aria-label="Close">✕</button>
206
178
  </div>
179
+ <div class="pa-modal__body">Body content.</div>
207
180
  </div>
208
181
  </div>
209
182
 
210
- <!-- Danger Modal -->
211
183
  <div class="pa-modal pa-modal--danger" id="dangerModal">
212
184
  <div class="pa-modal__backdrop"></div>
213
185
  <div class="pa-modal__container">
214
186
  <div class="pa-modal__header">
215
187
  <h3 class="pa-modal__title">Danger Modal</h3>
216
- <button class="pa-btn pa-btn--primary pa-btn--icon-only pa-btn--sm">✕</button>
217
- </div>
218
- <div class="pa-modal__body">
219
- Content here.
188
+ <button class="pa-btn pa-btn--sm pa-btn--icon-only pa-btn--light" aria-label="Close">✕</button>
220
189
  </div>
190
+ <div class="pa-modal__body">Body content.</div>
221
191
  </div>
222
192
  </div>
223
193
 
224
194
 
225
- <!-- SCROLLABLE MODAL BODY -->
195
+ <!-- SCROLLABLE BODY -->
196
+
197
+ <!--
198
+ The default __body already scrolls — it has overflow-y: auto, and the
199
+ container caps at max-height: 90vh. For most cases you don't need
200
+ --scrollable.
201
+
202
+ Use --scrollable when you want an EXPLICIT shorter max-height on the
203
+ body (default is 60vh). This keeps the footer visible even when the
204
+ viewport is very tall, so Accept / Decline stay on-screen.
205
+ -->
226
206
 
227
- <!-- Modal with Scrollable Content -->
228
207
  <div class="pa-modal" id="scrollableModal">
229
208
  <div class="pa-modal__backdrop"></div>
230
209
  <div class="pa-modal__container pa-modal__container--lg">
231
210
  <div class="pa-modal__header">
232
211
  <h3 class="pa-modal__title">Terms and Conditions</h3>
233
- <button class="pa-btn pa-btn--primary pa-btn--icon-only pa-btn--sm">✕</button>
212
+ <button class="pa-btn pa-btn--sm pa-btn--icon-only pa-btn--secondary" aria-label="Close">✕</button>
234
213
  </div>
235
214
  <div class="pa-modal__body pa-modal__body--scrollable">
236
- <!-- Long content that requires scrolling -->
237
215
  <h4>1. Introduction</h4>
238
- <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
216
+ <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
239
217
  <h4>2. Terms of Service</h4>
240
- <p>Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
218
+ <p>Ut enim ad minim veniam, quis nostrud exercitation.</p>
241
219
  <h4>3. Privacy Policy</h4>
242
- <p>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.</p>
243
- <h4>4. User Responsibilities</h4>
244
- <p>Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
245
- <h4>5. Disclaimer</h4>
246
- <p>Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium.</p>
247
- <!-- More content... -->
220
+ <p>Duis aute irure dolor in reprehenderit in voluptate.</p>
221
+ <!-- more content … -->
248
222
  </div>
249
223
  <div class="pa-modal__footer">
250
224
  <button class="pa-btn pa-btn--secondary">Decline</button>
@@ -253,34 +227,26 @@ Implementation:
253
227
  </div>
254
228
  </div>
255
229
 
256
- <!--
257
- SCROLLABLE BODY NOTES:
258
- - Use pa-modal__body--scrollable for content that may exceed viewport height
259
- - Body gets max-height and overflow-y: auto
260
- - Header and footer remain fixed, only body scrolls
261
- - Recommended for: Terms of service, long forms, data tables, file lists
262
- -->
263
230
 
231
+ <!-- MODAL WITH A FORM -->
264
232
 
265
- <!-- MODAL WITH FORM -->
266
-
267
- <!-- Form Modal -->
268
233
  <div class="pa-modal" id="formModal">
269
234
  <div class="pa-modal__backdrop"></div>
270
235
  <div class="pa-modal__container">
271
236
  <div class="pa-modal__header">
272
237
  <h3 class="pa-modal__title">User Information</h3>
273
- <button class="pa-btn pa-btn--primary pa-btn--icon-only pa-btn--sm">✕</button>
238
+ <button class="pa-btn pa-btn--sm pa-btn--icon-only pa-btn--secondary" aria-label="Close">✕</button>
274
239
  </div>
275
240
  <div class="pa-modal__body">
276
- <form>
241
+ <!-- Labels auto-style inside .pa-form .pa-form-group; no .pa-form-label class needed. -->
242
+ <form class="pa-form">
277
243
  <div class="pa-form-group">
278
- <label class="pa-form-label">Name</label>
279
- <input type="text" class="pa-input">
244
+ <label for="name">Name</label>
245
+ <input type="text" id="name" class="pa-input">
280
246
  </div>
281
247
  <div class="pa-form-group">
282
- <label class="pa-form-label">Email</label>
283
- <input type="email" class="pa-input">
248
+ <label for="email">Email</label>
249
+ <input type="email" id="email" class="pa-input">
284
250
  </div>
285
251
  </form>
286
252
  </div>
@@ -292,58 +258,162 @@ SCROLLABLE BODY NOTES:
292
258
  </div>
293
259
 
294
260
 
295
- <!-- JAVASCRIPT -->
261
+ <!-- ================================
262
+ JAVASCRIPT — open/close with scrollbar-gutter compensation
263
+ ================================ -->
264
+
296
265
  <script>
297
266
  function openModal(modalId) {
298
267
  const modal = document.getElementById(modalId);
299
- if (modal) {
300
- // Calculate scrollbar width to prevent layout shift
301
- const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
302
-
303
- modal.classList.add('pa-modal--show');
304
- document.body.style.overflow = 'hidden'; // Prevent background scrolling
305
- document.body.style.paddingRight = scrollbarWidth + 'px'; // Compensate for scrollbar
306
- }
268
+ if (!modal) return;
269
+ // Prevent layout shift when the vertical scrollbar disappears.
270
+ const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
271
+ modal.classList.add('pa-modal--show');
272
+ document.body.style.overflow = 'hidden';
273
+ document.body.style.paddingRight = scrollbarWidth + 'px';
307
274
  }
308
275
 
309
276
  function closeModal(modalId) {
310
277
  const modal = document.getElementById(modalId);
311
- if (modal) {
312
- modal.classList.remove('pa-modal--show');
313
- document.body.style.overflow = ''; // Restore scrolling
314
- document.body.style.paddingRight = ''; // Remove compensation
315
- }
278
+ if (!modal) return;
279
+ modal.classList.remove('pa-modal--show');
280
+ document.body.style.overflow = '';
281
+ document.body.style.paddingRight = '';
316
282
  }
317
283
 
318
- // Close modal with Escape key (skip static modals)
319
- document.addEventListener('keydown', function(e) {
284
+ // Esc closes the topmost open modal, unless it's --static.
285
+ document.addEventListener('keydown', (e) => {
320
286
  if (e.key === 'Escape') {
321
- const openModal = document.querySelector('.pa-modal--show');
322
- // Don't close static modals with ESC
323
- if (openModal && !openModal.classList.contains('pa-modal--static')) {
324
- closeModal(openModal.id);
287
+ const open = document.querySelector('.pa-modal--show');
288
+ if (open && !open.classList.contains('pa-modal--static')) {
289
+ closeModal(open.id);
325
290
  }
326
291
  }
327
292
  });
328
293
 
329
- // Close modal when clicking backdrop (skip static modals)
294
+ // Backdrop click closes the modal, unless it's --static.
330
295
  document.querySelectorAll('.pa-modal__backdrop').forEach(backdrop => {
331
- backdrop.addEventListener('click', function() {
296
+ backdrop.addEventListener('click', function () {
332
297
  const modal = this.closest('.pa-modal');
333
- // Don't close static modals on backdrop click
334
298
  if (modal && !modal.classList.contains('pa-modal--static')) {
335
299
  closeModal(modal.id);
336
300
  }
337
301
  });
338
302
  });
339
-
340
- // Close modal when clicking close button
341
- document.querySelectorAll('.pa-modal .pa-btn--icon-only').forEach(btn => {
342
- btn.addEventListener('click', function() {
343
- const modal = this.closest('.pa-modal');
344
- if (modal) {
345
- closeModal(modal.id);
346
- }
347
- });
348
- });
349
303
  </script>
304
+
305
+
306
+ <!-- ================================
307
+ COMPONENT REFERENCE
308
+ ================================ -->
309
+
310
+ <!--
311
+ WRAPPER:
312
+ - .pa-modal Fixed-inset root; display: none by default.
313
+ - .pa-modal--show Visible state (display: flex, centred by default).
314
+ - .pa-modal--top Align container near the top (5vh padding-top);
315
+ use for search/command-palette style modals.
316
+ - .pa-modal--static JS contract only — no CSS rule. The example
317
+ JS skips Esc + backdrop dismissal when this
318
+ class is present. Add it on the root.
319
+
320
+ Colour variants (cascade into __header via descendant selector):
321
+ - .pa-modal--primary
322
+ - .pa-modal--success
323
+ - .pa-modal--warning
324
+ - .pa-modal--danger
325
+ (No --info / --secondary / --light / --dark. No pa-modal__header--*
326
+ modifiers — theme lives on the root.)
327
+
328
+ STRUCTURE:
329
+ - .pa-modal__backdrop Full-viewport overlay behind the dialog.
330
+ - .pa-modal__container The dialog card itself.
331
+ - .pa-modal__header Flex row: __title (start) + close button (end).
332
+ - .pa-modal__title <h3>-weight title text.
333
+ - .pa-modal__body Scrollable content region (flex: 1;
334
+ overflow-y: auto — default scrolls when
335
+ content exceeds container max-height).
336
+ - .pa-modal__footer Right-aligned action buttons row
337
+ (justify-content: flex-end; gap).
338
+
339
+ CONTAINER SIZES (on __container):
340
+ - .pa-modal__container--sm 32rem / 320px
341
+ - (default) 48rem / 480px
342
+ - .pa-modal__container--lg 80rem / 800px
343
+ - .pa-modal__container--xl 112rem / 1120px
344
+ - .pa-modal__container--xxl 144rem / 1440px
345
+ - .pa-modal__container--fw Viewport minus $modal-fw-margin on every side
346
+
347
+ BODY MODIFIER:
348
+ - .pa-modal__body--scrollable Explicit max-height: $modal-body-
349
+ scrollable-max-height (60vh default).
350
+ Use when you want a shorter ceiling than
351
+ the container's own max-height (90vh),
352
+ so long content scrolls earlier and
353
+ __footer stays in view.
354
+
355
+ RESPONSIVE:
356
+ - Below $mobile-breakpoint (768px):
357
+ - __container margin drops to $modal-mobile-margin
358
+ - max-height becomes $modal-mobile-max-height
359
+ - --lg / --xl / --xxl all collapse to
360
+ calc(100vw - $spacing-base)
361
+ - --fw uses the same fw-margin with viewport-derived width/height
362
+
363
+ JAVASCRIPT CONTRACT:
364
+ - openModal(id) / closeModal(id) — toggle pa-modal--show on the root
365
+ - Esc handler skips modals with pa-modal--static
366
+ - Backdrop click handler skips modals with pa-modal--static
367
+ - Scrollbar-gutter compensation on open/close prevents layout shift
368
+ (body gets overflow: hidden + padding-right matching scrollbar width).
369
+
370
+ STRUCTURE PATTERNS:
371
+
372
+ Basic modal:
373
+ <div class="pa-modal" id="x">
374
+ <div class="pa-modal__backdrop"></div>
375
+ <div class="pa-modal__container">
376
+ <div class="pa-modal__header">
377
+ <h3 class="pa-modal__title">Title</h3>
378
+ <button class="pa-btn pa-btn--sm pa-btn--icon-only pa-btn--secondary" aria-label="Close">✕</button>
379
+ </div>
380
+ <div class="pa-modal__body">…</div>
381
+ <div class="pa-modal__footer">
382
+ <button class="pa-btn pa-btn--secondary">Cancel</button>
383
+ <button class="pa-btn pa-btn--primary">Save</button>
384
+ </div>
385
+ </div>
386
+ </div>
387
+
388
+ Themed modal (colour on the root, not the header):
389
+ <div class="pa-modal pa-modal--danger">
390
+ <div class="pa-modal__backdrop"></div>
391
+ <div class="pa-modal__container">
392
+ <div class="pa-modal__header">
393
+ <h3 class="pa-modal__title">Delete account?</h3>
394
+ </div>
395
+
396
+ </div>
397
+ </div>
398
+
399
+ Static modal (no Esc/backdrop dismiss; user must choose):
400
+ <div class="pa-modal pa-modal--static">
401
+ <div class="pa-modal__backdrop"></div>
402
+ <div class="pa-modal__container pa-modal__container--sm">
403
+ <div class="pa-modal__header"><h3 class="pa-modal__title">Confirm</h3></div>
404
+ <div class="pa-modal__body">…</div>
405
+ <div class="pa-modal__footer">
406
+ <button class="pa-btn pa-btn--secondary">Decline</button>
407
+ <button class="pa-btn pa-btn--danger">Accept</button>
408
+ </div>
409
+ </div>
410
+ </div>
411
+
412
+ Modal with a form (labels auto-style inside .pa-form .pa-form-group):
413
+ <form class="pa-form">
414
+ <div class="pa-form-group">
415
+ <label for="x">Field</label>
416
+ <input id="x" class="pa-input">
417
+ </div>
418
+ </form>
419
+ -->