@aarongustafson/img-alt-overlay 0.0.0-stage → 0.0.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Aaron Gustafson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,444 @@
1
- # Temporary Holding Version
1
+ # img-alt-overlay
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm version](https://img.shields.io/npm/v/@aarongustafson/img-alt-overlay.svg)](https://www.npmjs.com/package/@aarongustafson/img-alt-overlay)
4
+ [![CI status](https://img.shields.io/github/actions/workflow/status/aarongustafson/img-alt-overlay/ci.yml?branch=main&label=CI)](https://github.com/aarongustafson/img-alt-overlay/actions/workflows/ci.yml)
5
+ [![Minified + gzipped size](https://img.shields.io/bundlejs/size/@aarongustafson/img-alt-overlay?format=minzip&label=min%2Bgzip)](https://bundlejs.com/?q=@aarongustafson/img-alt-overlay)
6
+ [![License](https://img.shields.io/npm/l/@aarongustafson/img-alt-overlay)](https://github.com/aarongustafson/img-alt-overlay/blob/main/LICENSE)
7
+ [![Type definitions](https://img.shields.io/npm/types/@aarongustafson/img-alt-overlay)](https://github.com/aarongustafson/img-alt-overlay/blob/main/img-alt-overlay.d.ts)
8
+
9
+ `<img-alt-overlay>` wraps an image and adds a button that reveals its alternative
10
+ text in a non-modal dialog over the image. It leaves the image, its `alt` attribute, responsive
11
+ sources, and links alone. Without JavaScript, the original image still works.
12
+
13
+ The interaction builds on the alt-text overlay on
14
+ [Aaron’s website](https://www.aaron-gustafson.com/notebook/there-can-be-only-one-redux/).
15
+ Like that panel, it stays inside the image’s block and leaves the rest of the
16
+ page interactive. It has no backdrop and doesn’t trap keyboard focus. Activate
17
+ the opener again to close the panel; its label changes to the dismissal wording
18
+ while open.
19
+
20
+ ## Try it
21
+
22
+ This package is in development and hasn’t been published. From a checkout:
23
+
24
+ ```sh
25
+ npm ci
26
+ node scripts/serve.js
27
+ ```
28
+
29
+ Open [the demo](http://127.0.0.1:4177/demo/). It covers default controls,
30
+ translation, right-to-left layout, linked responsive images, theming, decorative
31
+ images, and failed image loading.
32
+
33
+ ## Load and register
34
+
35
+ Once published, import the registration entry point:
36
+
37
+ ```js
38
+ import '@aarongustafson/img-alt-overlay/define.js';
39
+ ```
40
+
41
+ Load the optional stylesheet using your bundler, or serve it as a normal stylesheet:
42
+
43
+ ```css
44
+ @import '@aarongustafson/img-alt-overlay/img-alt-overlay.css';
45
+ ```
46
+
47
+ For a local checkout, put these in the document head:
48
+
49
+ ```html
50
+ <link rel="stylesheet" href="./img-alt-overlay/img-alt-overlay.css" />
51
+ <script type="module" src="./img-alt-overlay/define.js"></script>
52
+ ```
53
+
54
+ The package root and class file are **class-only imports**; neither registers
55
+ an element:
56
+
57
+ ```js
58
+ import { ImgAltOverlayElement } from '@aarongustafson/img-alt-overlay';
59
+
60
+ customElements.define('img-alt-overlay', ImgAltOverlayElement);
61
+ ```
62
+
63
+ Importing `define.js` registers `img-alt-overlay` when a browser registry exists.
64
+ It also exports `defineImgAltOverlay()`, which returns `false` without a registry
65
+ and `true` when the name is already registered or registration succeeds. It
66
+ doesn’t overwrite an existing registration. Class and registration imports are
67
+ safe without a browser DOM; constructing the class requires one.
68
+
69
+ ## Wrap an image
70
+
71
+ ```html
72
+ <img-alt-overlay>
73
+ <img
74
+ src="bicycle.jpg"
75
+ width="640"
76
+ height="360"
77
+ alt="A red bicycle beside a blue wall, with a basket on its handlebars."
78
+ />
79
+ </img-alt-overlay>
80
+ ```
81
+
82
+ Use exactly one image belonging to the wrapper. A `picture` element and an image
83
+ link are supported:
84
+
85
+ ```html
86
+ <figure>
87
+ <img-alt-overlay>
88
+ <a href="bicycle-large.jpg">
89
+ <picture>
90
+ <source media="(min-width: 40em)" srcset="bicycle-wide.jpg" />
91
+ <img
92
+ src="bicycle.jpg"
93
+ width="640"
94
+ height="360"
95
+ alt="View the red bicycle photograph."
96
+ />
97
+ </picture>
98
+ </a>
99
+ </img-alt-overlay>
100
+ <figcaption>A neighborhood bicycle.</figcaption>
101
+ </figure>
102
+ ```
103
+
104
+ Put links **inside** the wrapper, not around it. The generated button must not be
105
+ nested in a link, button, or label. Keep captions outside the wrapper so the
106
+ overlay is positioned over the image rather than the caption.
107
+
108
+ Empty, missing, or whitespace-only alt text produces no controls. Empty `alt=""`
109
+ is appropriate for decorative images; missing alt text isn’t a substitute for
110
+ writing an appropriate alternative. This component doesn’t generate descriptions,
111
+ infer them from filenames, or repair missing alt text.
112
+
113
+ ## Native commands, with a small fallback
114
+
115
+ The component adds light-DOM markup equivalent to:
116
+
117
+ ```html
118
+ <button
119
+ data-alt-trigger
120
+ type="button"
121
+ commandfor="image-alt-dialog"
122
+ command="--toggle-alt"
123
+ aria-controls="image-alt-dialog"
124
+ aria-haspopup="dialog"
125
+ aria-expanded="false"
126
+ >
127
+ View alt text
128
+ </button>
129
+ <div data-alt-ui>
130
+ <dialog
131
+ data-alt-dialog
132
+ id="image-alt-dialog"
133
+ aria-labelledby="image-alt-heading"
134
+ >
135
+ <h2 id="image-alt-heading">Image description</h2>
136
+ <p tabindex="-1" autofocus>The image’s alternative text.</p>
137
+ <button
138
+ type="button"
139
+ commandfor="image-alt-dialog"
140
+ command="close"
141
+ aria-label="Close"
142
+ >
143
+ <span aria-hidden="true">✕</span>
144
+ </button>
145
+ </dialog>
146
+ </div>
147
+ ```
148
+
149
+ IDs are generated per instance. Supporting browsers handle the
150
+ [`command` and `commandfor` attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button#command).
151
+ The opener uses the custom `--toggle-alt` command: its `command` event calls
152
+ `dialog.show()` when closed and `dialog.close()` when open. HTML has no built-in
153
+ command for toggling a non-modal dialog. The separate Close button uses the
154
+ built-in `close` command. The opener’s wording and `aria-expanded` stay in sync
155
+ when either control or Escape closes the panel.
156
+ Where the button’s `commandForElement` API is absent, a delegated click listener
157
+ calls the native dialog’s `show()` and `close()` methods instead. It respects
158
+ canceled clicks and never runs alongside the supported native command path.
159
+
160
+ Escape closes the dialog while focus is inside the wrapper, unless the key event
161
+ has already been canceled. The listener belongs to the wrapper, not the document;
162
+ Escape elsewhere on the page doesn’t dismiss an unrelated image’s description.
163
+
164
+ The fallback is for **commands**, not for dialog itself. If the native dialog
165
+ methods aren’t available, enhancement reports an error and adds no controls.
166
+ There’s no dialog polyfill, popover mode, image enlargement, or remote request.
167
+
168
+ ## Labels and configuration
169
+
170
+ | Attribute | Default | Purpose |
171
+ | -------------- | ------------------- | ---------------------------------------------------------------------------------------- |
172
+ | `button-label` | `View alt text` | Visible and accessible name of the opener |
173
+ | `dialog-label` | `Image description` | Dialog heading and accessible name; visually hidden by the default stylesheet |
174
+ | `close-label` | `Close` | Dismissal wording for the open-state opener and accessible name of the icon Close button |
175
+
176
+ Authored attributes override defaults. All labels must contain non-whitespace
177
+ text. Labels and alt text are inserted as plain text, never interpreted as HTML.
178
+ There are no separate ARIA-only labels that could diverge from visible wording.
179
+
180
+ ```html
181
+ <img-alt-overlay
182
+ lang="fr"
183
+ button-label="Voir le texte alternatif"
184
+ dialog-label="Description de l’image"
185
+ close-label="Fermer"
186
+ >
187
+ <img
188
+ src="bicycle.jpg"
189
+ alt="Un vélo rouge devant un mur bleu, avec un panier sur le guidon."
190
+ />
191
+ </img-alt-overlay>
192
+ ```
193
+
194
+ Supply translations for all three labels and the image alternative. `lang` and
195
+ `dir` inherit normally. Changing a label attribute schedules a refresh, closing
196
+ any open dialog and rebuilding the controls with the new wording.
197
+
198
+ ## Updates, lifecycle, and errors
199
+
200
+ Setup runs in a queued microtask after connection. Normal module scripts in the
201
+ document head defer until parsing finishes; don’t hide the image while waiting
202
+ for definition or successful setup.
203
+
204
+ The component observes only the controlled image’s `alt` attribute. Text changes
205
+ update the existing dialog without closing it. Emptying or removing `alt` closes
206
+ and removes the controls; adding nonempty text creates them again.
207
+
208
+ Call `refresh()` after replacing an image or changing the authored structure:
209
+
210
+ ```js
211
+ const wrapper = document.querySelector('img-alt-overlay');
212
+ wrapper.refresh();
213
+ ```
214
+
215
+ `refresh()` closes any open dialog and rereads the image and labels. It doesn’t
216
+ change authored nodes. Disconnecting closes and removes generated UI and
217
+ disconnects the observer and fallback listener. Reconnecting initializes again.
218
+
219
+ The read-only `state` property is:
220
+
221
+ | Value | Meaning |
222
+ | ---------- | -------------------------------------------- |
223
+ | `idle` | Not connected or not yet initialized |
224
+ | `inactive` | Valid image, but no nonempty alt text |
225
+ | `ready` | Dialog controls are available |
226
+ | `error` | Enhancement failed; authored content remains |
227
+
228
+ | Event | When | Detail |
229
+ | ----------------------- | --------------------------------------- | ----------- |
230
+ | `img-alt-overlay:ready` | A new set of usable controls is created | None |
231
+ | `img-alt-overlay:error` | Invalid configuration or setup failure | `{ error }` |
232
+
233
+ Both events bubble. Listen before inserting the element or loading the
234
+ registration module if you need initial events:
235
+
236
+ ```js
237
+ document.addEventListener('img-alt-overlay:error', (event) => {
238
+ console.error('Alt-text enhancement failed:', event.detail.error);
239
+ });
240
+ ```
241
+
242
+ Invalid image counts, wrappers inside interactive controls, empty labels, and
243
+ unsupported dialog APIs are errors. Fix the markup and call `refresh()` to retry.
244
+ An image load failure isn’t an enhancement failure: its authored alternative
245
+ text remains available in the dialog.
246
+
247
+ ## Styling
248
+
249
+ All generated UI is light DOM. Without the optional stylesheet, controls remain
250
+ in normal flow and the browser supplies the dialog presentation. With it, the
251
+ opener sits at the image’s block-start/inline-end corner and the non-modal dialog
252
+ is vertically centered in the space below the opener within the image wrapper.
253
+ Its width and maximum height are bounded by that block, not the viewport.
254
+ The default overlay area reserves 3.75rem above the panel for the opener.
255
+ If you move the opener or use larger, multiline labels, adjust the overlay
256
+ insets to leave enough room. Long descriptions
257
+ wrap and scroll inside the panel; it moves with the image as the page scrolls
258
+ or resizes. The layout layer lets pointer clicks pass through to the image or
259
+ its link outside the controls and open panel.
260
+
261
+ The description stays centered across and along the panel, with no paragraph
262
+ margin or visible focus ring. It still receives initial focus so the browser can
263
+ present the dialog and its text. Long content starts at the top and scrolls
264
+ rather than being clipped above the panel.
265
+
266
+ The separate Close button is a ✕ with its accessible name taken from
267
+ `close-label`. It stays visually hidden until focused and remains in the keyboard
268
+ tab order and accessibility tree. When revealed, it sits at the dialog’s block
269
+ start/inline end, has a 44px minimum target at the default font size, and keeps
270
+ its visible focus ring. Its space is reserved, so revealing it doesn’t enlarge
271
+ the panel or cover the text. It remains visible during pointer interaction once
272
+ revealed. Without the stylesheet, the icon button stays visible in normal flow.
273
+
274
+ Use `--img-alt-dialog-min-block-size` to change the panel’s minimum height.
275
+ The default is `30%` of the available image-overlay area, rather than the whole
276
+ image; the opener’s reserved space is outside that area. The minimum is capped
277
+ at the available area so small images and long descriptions remain bounded.
278
+ When space permits, a `4rem` floor keeps the Close button and its focus ring
279
+ inside the panel, even with a short description or a smaller configured minimum.
280
+ The percentage-based cap requires an overlay area with a definite block size,
281
+ even when the custom property contains a length. For an auto-sized,
282
+ edge-anchored area, set the dialog’s `min-block-size` directly instead, as in
283
+ the example below.
284
+
285
+ The [website-style demo](./demo/index.html#site-style-example) shows a full-image-width,
286
+ borderless black panel with white text. It sets the overlay’s inline insets to
287
+ zero, keeps spacing around the opener, and themes only that instance through
288
+ an authored class.
289
+
290
+ Stable hooks:
291
+
292
+ - `[data-alt-ui]`: positioned area containing the panel, separate from the opener.
293
+ - `[data-alt-trigger]`: opener button.
294
+ - `[data-alt-dialog]`: native non-modal dialog; there is no backdrop.
295
+ - `[data-alt-text]`: alternative-text paragraph.
296
+ - `[data-alt-close]`: close button.
297
+
298
+ | CSS custom property | Default |
299
+ | --------------------------------- | ------------ |
300
+ | `--img-alt-button-color` | `#fff` |
301
+ | `--img-alt-button-background` | `#111` |
302
+ | `--img-alt-dialog-color` | `CanvasText` |
303
+ | `--img-alt-dialog-background` | `Canvas` |
304
+ | `--img-alt-focus-color` | `Highlight` |
305
+ | `--img-alt-dialog-min-block-size` | `30%` |
306
+
307
+ ### Position the opener and overlay
308
+
309
+ These properties accept CSS inset values, such as lengths, percentages, and
310
+ `auto`. They position the opener and overlay area independently, relative to the
311
+ image wrapper. Logical sides follow the writing mode and direction:
312
+ `inline-start` is left in horizontal left-to-right text and right in
313
+ right-to-left text.
314
+
315
+ | CSS custom property | Default |
316
+ | -------------------------------------- | --------- |
317
+ | `--img-alt-button-inset-block-start` | `0.5rem` |
318
+ | `--img-alt-button-inset-block-end` | `auto` |
319
+ | `--img-alt-button-inset-inline-start` | `auto` |
320
+ | `--img-alt-button-inset-inline-end` | `0.5rem` |
321
+ | `--img-alt-overlay-inset-block-start` | `3.75rem` |
322
+ | `--img-alt-overlay-inset-block-end` | `0.5rem` |
323
+ | `--img-alt-overlay-inset-inline-start` | `0.5rem` |
324
+ | `--img-alt-overlay-inset-inline-end` | `0.5rem` |
325
+
326
+ When both block insets have values, the dialog centers in the bounded area
327
+ between them. Set the opposite inset to `auto` to anchor the natural-height
328
+ area to one block edge instead. The same CSS positioning rules apply to the
329
+ opener: when moving it to another corner, clear its opposing insets.
330
+ The [repositioning demo](./demo/index.html#inset-example) moves the opener to
331
+ block end/inline start and leaves room for it below the overlay.
332
+
333
+ For a panel anchored to block end and both inline edges:
334
+
335
+ ```css
336
+ img-alt-overlay.bottom-panel {
337
+ --img-alt-overlay-inset-block-start: auto;
338
+ --img-alt-overlay-inset-block-end: 0;
339
+ --img-alt-overlay-inset-inline-start: 0;
340
+ --img-alt-overlay-inset-inline-end: 0;
341
+ }
342
+
343
+ img-alt-overlay.bottom-panel [data-alt-ui] {
344
+ max-block-size: calc(100% - 3.75rem);
345
+ }
346
+
347
+ img-alt-overlay.bottom-panel [data-alt-dialog] {
348
+ min-block-size: 4rem;
349
+ }
350
+ ```
351
+
352
+ The height limit leaves room for the default opener when text needs to scroll.
353
+ The minimum height fits the Close button and its focus ring even when the
354
+ description is short.
355
+ The [slide-in demo](./demo/index.html#slide-up-example) adds 300ms CSS transitions
356
+ to this layout, sliding and fading the panel in and out. It clips motion within
357
+ the image and skips transitions when the reader requests reduced motion.
358
+ [`@starting-style`](https://developer.mozilla.org/en-US/docs/Web/CSS/@starting-style)
359
+ defines the entry state, and a
360
+ [`display` transition with `allow-discrete`](https://developer.mozilla.org/en-US/docs/Web/CSS/transition-behavior)
361
+ keeps the closing panel rendered until the exit finishes. Browsers without
362
+ discrete-transition support open and close it immediately. Some engines parse
363
+ `allow-discrete` but can’t transition a native dialog’s `display`; those still
364
+ animate entry but close immediately. This non-modal dialog
365
+ doesn’t enter the top layer or use a backdrop, so it needs no `overlay` or
366
+ `::backdrop` transition. The component’s default stylesheet adds no motion.
367
+
368
+ Keep visible focus indicators, legible contrast, and a reachable Close control.
369
+ Don’t override the native dialog’s closed-state visibility. If you manually
370
+ register the class under another tag name, adapt the stylesheet’s
371
+ `img-alt-overlay` selectors to that tag.
372
+
373
+ ## Accessibility behavior and limits
374
+
375
+ - The original image keeps its native alternative text. The dialog provides an
376
+ optional visible copy; it doesn’t replace the image’s accessible name.
377
+ - The opener is a real `type="button"` with `aria-haspopup="dialog"` and a
378
+ synchronized `aria-expanded` state. The dialog is named by its heading, which
379
+ the default stylesheet visually hides to keep the panel compact.
380
+ - Opening focuses the alternative-text paragraph, which is programmatically
381
+ focusable but not an extra Tab stop. This keeps a long description’s beginning
382
+ visible rather than scrolling immediately to Close.
383
+ - The dialog is non-modal: the rest of the document stays interactive, and Tab
384
+ can leave the panel. Close or Escape within the wrapper dismisses it; native
385
+ dialog behavior restores prior keyboard focus when focus is still inside.
386
+ Pointer focus follows browser conventions: Safari, for example, doesn’t always
387
+ focus clicked buttons. The component doesn’t force a different pointer-focus
388
+ policy.
389
+ - This is a named dialog with an interactive Close control, not `role="tooltip"`.
390
+ It opens by explicit activation rather than hover alone.
391
+ - No extra live region announces the same description, and no animation is added.
392
+ - Images, links, sources, dimensions, and loading hints remain unchanged. Without
393
+ JavaScript, or after setup failure, no dead controls are added.
394
+
395
+ An alt-text dialog isn’t a replacement for visible captions, nearby explanations,
396
+ or a proper long description. Authors remain responsible for the alternative’s
397
+ quality and context. Don’t include sensitive information in alt text expecting it
398
+ to remain hidden.
399
+
400
+ Chromium, Firefox, and WebKit have automated browser checks for the native
401
+ interaction, fallback, focus, layout, and failure states. Automated checks don’t
402
+ establish full accessibility conformance; a manual screen-reader review is still
403
+ needed before release.
404
+
405
+ ## Development
406
+
407
+ ```sh
408
+ npm ci
409
+ npm run test:run
410
+ npm run test:types
411
+ npx playwright install --with-deps chromium firefox webkit
412
+ npm run test:browser
413
+ npm run lint
414
+ npm run test:coverage
415
+ npm audit --omit=dev
416
+ ```
417
+
418
+ Use test-driven development. Tests cover image preservation, commands, labels,
419
+ text-only insertion, lifecycle cleanup, declaration imports, package contents,
420
+ and release automation. Browser checks cover native focus and keyboard behavior,
421
+ long content, narrow screens, translations, and no-JavaScript/module-failure
422
+ fallbacks. The CDN tests route unpublished URLs to the local source; they don’t
423
+ claim a future package is available from the CDN.
424
+
425
+ ## Publishing
426
+
427
+ The starter’s release hooks remain intact:
428
+
429
+ - `version`: `node scripts/update-demo-versions.js && git add demo/esm.html`
430
+ - `postversion`: `git push --follow-tags`
431
+
432
+ When explicitly releasing, `npm version minor` updates package metadata and the
433
+ versioned CDN demo, creates the version commit and annotated tag, and pushes the
434
+ branch and tag. Don’t run it merely to test readiness.
435
+
436
+ The tag-triggered `publish.yml` workflow uses npm trusted publishing. Reserve
437
+ the package and configure npm’s trusted publisher before the first automated
438
+ release: owner `aarongustafson`, repository `img-alt-overlay`, workflow filename
439
+ `publish.yml`, and no environment. The workflow’s manual dry-run option doesn’t
440
+ publish. No version bump or publication is part of this initial implementation.
441
+
442
+ ## License
443
+
444
+ [MIT](./LICENSE).
@@ -0,0 +1,196 @@
1
+ {
2
+ "schemaVersion": "1.0.0",
3
+ "readme": "README.md",
4
+ "modules": [
5
+ {
6
+ "kind": "javascript-module",
7
+ "path": "img-alt-overlay.js",
8
+ "declarations": [
9
+ {
10
+ "kind": "class",
11
+ "name": "ImgAltOverlayElement",
12
+ "description": "A light-DOM image wrapper with a non-modal, in-image alt-text dialog, custom toggle command, native closing command, and JavaScript command fallback. The opener reflects aria-expanded; the default stylesheet reveals the separate Close control on focus.",
13
+ "tagName": "img-alt-overlay",
14
+ "customElement": true,
15
+ "superclass": { "name": "HTMLElement" },
16
+ "attributes": [
17
+ {
18
+ "name": "button-label",
19
+ "description": "Visible and accessible opener label. Must be nonempty.",
20
+ "type": { "text": "string" },
21
+ "default": "\"View alt text\""
22
+ },
23
+ {
24
+ "name": "dialog-label",
25
+ "description": "Dialog heading and accessible dialog name. Visually hidden by the default stylesheet. Must be nonempty.",
26
+ "type": { "text": "string" },
27
+ "default": "\"Image description\""
28
+ },
29
+ {
30
+ "name": "close-label",
31
+ "description": "Dismissal wording for the opener while open and accessible name of the separate icon Close button. Must be nonempty.",
32
+ "type": { "text": "string" },
33
+ "default": "\"Close\""
34
+ }
35
+ ],
36
+ "members": [
37
+ {
38
+ "kind": "field",
39
+ "name": "state",
40
+ "readonly": true,
41
+ "type": {
42
+ "text": "'idle' | 'inactive' | 'ready' | 'error'"
43
+ },
44
+ "description": "Current enhancement state.",
45
+ "default": "\"idle\""
46
+ },
47
+ {
48
+ "kind": "method",
49
+ "name": "refresh",
50
+ "description": "Closes any open dialog, restores authored markup, and rereads image structure and labels.",
51
+ "return": { "type": { "text": "void" } }
52
+ }
53
+ ],
54
+ "events": [
55
+ {
56
+ "name": "img-alt-overlay:ready",
57
+ "type": { "text": "CustomEvent<void>" },
58
+ "description": "Bubbles when a new set of usable controls has been created."
59
+ },
60
+ {
61
+ "name": "img-alt-overlay:error",
62
+ "type": { "text": "CustomEvent<{ error: Error }>" },
63
+ "description": "Bubbles on configuration or setup failure. Authored content remains intact."
64
+ }
65
+ ],
66
+ "cssProperties": [
67
+ {
68
+ "name": "--img-alt-dialog-min-block-size",
69
+ "default": "30%",
70
+ "description": "Minimum dialog block size within the available overlay area, with a 4rem control-sized floor when space permits and capped at the area's size. Accepts CSS lengths or percentages and requires a definite overlay block size. For auto-sized, edge-anchored areas, set the dialog's min-block-size directly."
71
+ },
72
+ {
73
+ "name": "--img-alt-button-inset-block-start",
74
+ "default": "0.5rem",
75
+ "description": "Logical block-start inset of the independently positioned opener."
76
+ },
77
+ {
78
+ "name": "--img-alt-button-inset-block-end",
79
+ "default": "auto",
80
+ "description": "Logical block-end inset of the opener. Unset block-start to auto when anchoring here."
81
+ },
82
+ {
83
+ "name": "--img-alt-button-inset-inline-start",
84
+ "default": "auto",
85
+ "description": "Logical inline-start inset of the opener."
86
+ },
87
+ {
88
+ "name": "--img-alt-button-inset-inline-end",
89
+ "default": "0.5rem",
90
+ "description": "Logical inline-end inset of the opener."
91
+ },
92
+ {
93
+ "name": "--img-alt-overlay-inset-block-start",
94
+ "default": "3.75rem",
95
+ "description": "Logical block-start inset of the overlay area. Default reserves room below the opener."
96
+ },
97
+ {
98
+ "name": "--img-alt-overlay-inset-block-end",
99
+ "default": "0.5rem",
100
+ "description": "Logical block-end inset of the overlay area."
101
+ },
102
+ {
103
+ "name": "--img-alt-overlay-inset-inline-start",
104
+ "default": "0.5rem",
105
+ "description": "Logical inline-start inset of the overlay area."
106
+ },
107
+ {
108
+ "name": "--img-alt-overlay-inset-inline-end",
109
+ "default": "0.5rem",
110
+ "description": "Logical inline-end inset of the overlay area."
111
+ },
112
+ {
113
+ "name": "--img-alt-button-color",
114
+ "default": "#fff",
115
+ "description": "Opener text color in the optional stylesheet."
116
+ },
117
+ {
118
+ "name": "--img-alt-button-background",
119
+ "default": "#111",
120
+ "description": "Opener background."
121
+ },
122
+ {
123
+ "name": "--img-alt-dialog-color",
124
+ "default": "CanvasText",
125
+ "description": "Dialog text color."
126
+ },
127
+ {
128
+ "name": "--img-alt-dialog-background",
129
+ "default": "Canvas",
130
+ "description": "Dialog background."
131
+ },
132
+ {
133
+ "name": "--img-alt-focus-color",
134
+ "default": "Highlight",
135
+ "description": "Visible focus outline color."
136
+ }
137
+ ]
138
+ }
139
+ ],
140
+ "exports": [
141
+ {
142
+ "kind": "js",
143
+ "name": "ImgAltOverlayElement",
144
+ "declaration": {
145
+ "name": "ImgAltOverlayElement",
146
+ "module": "img-alt-overlay.js"
147
+ }
148
+ }
149
+ ]
150
+ },
151
+ {
152
+ "kind": "javascript-module",
153
+ "path": "index.js",
154
+ "exports": [
155
+ {
156
+ "kind": "js",
157
+ "name": "ImgAltOverlayElement",
158
+ "declaration": {
159
+ "name": "ImgAltOverlayElement",
160
+ "module": "img-alt-overlay.js"
161
+ }
162
+ }
163
+ ]
164
+ },
165
+ {
166
+ "kind": "javascript-module",
167
+ "path": "define.js",
168
+ "declarations": [
169
+ {
170
+ "kind": "function",
171
+ "name": "defineImgAltOverlay",
172
+ "description": "Registers img-alt-overlay when a browser registry exists; also called on import.",
173
+ "return": { "type": { "text": "boolean" } }
174
+ }
175
+ ],
176
+ "exports": [
177
+ {
178
+ "kind": "js",
179
+ "name": "defineImgAltOverlay",
180
+ "declaration": {
181
+ "name": "defineImgAltOverlay",
182
+ "module": "define.js"
183
+ }
184
+ },
185
+ {
186
+ "kind": "custom-element-definition",
187
+ "name": "img-alt-overlay",
188
+ "declaration": {
189
+ "name": "ImgAltOverlayElement",
190
+ "module": "img-alt-overlay.js"
191
+ }
192
+ }
193
+ ]
194
+ }
195
+ ]
196
+ }
package/define.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import './img-alt-overlay.js';
2
+
3
+ /** Registers the class when a browser registry exists; registration also runs on import. */
4
+ export function defineImgAltOverlay(): boolean;
package/define.js ADDED
@@ -0,0 +1,19 @@
1
+ import { ImgAltOverlayElement } from './img-alt-overlay.js';
2
+
3
+ export function defineImgAltOverlay() {
4
+ const tagName = 'img-alt-overlay';
5
+ const hasWindow = typeof window !== 'undefined';
6
+ const registry = hasWindow ? window.customElements : undefined;
7
+
8
+ if (!registry || typeof registry.define !== 'function') {
9
+ return false;
10
+ }
11
+
12
+ if (!registry.get(tagName)) {
13
+ registry.define(tagName, ImgAltOverlayElement);
14
+ }
15
+
16
+ return true;
17
+ }
18
+
19
+ defineImgAltOverlay();
@@ -0,0 +1,134 @@
1
+ img-alt-overlay {
2
+ display: inline-block;
3
+ position: relative;
4
+ max-inline-size: 100%;
5
+ vertical-align: top;
6
+ }
7
+
8
+ img-alt-overlay[hidden],
9
+ img-alt-overlay [hidden] {
10
+ display: none;
11
+ }
12
+
13
+ img-alt-overlay img {
14
+ display: block;
15
+ max-inline-size: 100%;
16
+ block-size: auto;
17
+ }
18
+
19
+ img-alt-overlay [data-alt-trigger],
20
+ img-alt-overlay [data-alt-close] {
21
+ font: inherit;
22
+ min-block-size: 2.75rem;
23
+ cursor: pointer;
24
+ }
25
+
26
+ img-alt-overlay [data-alt-trigger] {
27
+ color: var(--img-alt-button-color, #fff);
28
+ background: var(--img-alt-button-background, #111);
29
+ border: 2px solid currentColor;
30
+ border-radius: 0.25rem;
31
+ padding: 0.5rem 0.75rem;
32
+ }
33
+
34
+ img-alt-overlay [data-alt-trigger] {
35
+ position: absolute;
36
+ inset-block-start: var(--img-alt-button-inset-block-start, 0.5rem);
37
+ inset-block-end: var(--img-alt-button-inset-block-end, auto);
38
+ inset-inline-start: var(--img-alt-button-inset-inline-start, auto);
39
+ inset-inline-end: var(--img-alt-button-inset-inline-end, 0.5rem);
40
+ z-index: 1;
41
+ }
42
+
43
+ img-alt-overlay [data-alt-ui] {
44
+ position: absolute;
45
+ inset-block-start: var(--img-alt-overlay-inset-block-start, 3.75rem);
46
+ inset-block-end: var(--img-alt-overlay-inset-block-end, 0.5rem);
47
+ inset-inline-start: var(--img-alt-overlay-inset-inline-start, 0.5rem);
48
+ inset-inline-end: var(--img-alt-overlay-inset-inline-end, 0.5rem);
49
+ max-inline-size: 100%;
50
+ max-block-size: 100%;
51
+ display: flex;
52
+ flex-direction: column;
53
+ pointer-events: none;
54
+ }
55
+
56
+ img-alt-overlay [data-alt-ui] > * {
57
+ pointer-events: auto;
58
+ }
59
+
60
+ img-alt-overlay [data-alt-dialog] {
61
+ position: relative;
62
+ flex: 0 1 auto;
63
+ margin: auto 0;
64
+ box-sizing: border-box;
65
+ inline-size: 100%;
66
+ max-inline-size: 100%;
67
+ max-block-size: 100%;
68
+ min-block-size: min(
69
+ max(var(--img-alt-dialog-min-block-size, 30%), 4rem),
70
+ 100%
71
+ );
72
+ overflow: auto;
73
+ overscroll-behavior: contain;
74
+ color: var(--img-alt-dialog-color, CanvasText);
75
+ background: var(--img-alt-dialog-background, Canvas);
76
+ border: 2px solid currentColor;
77
+ border-radius: 0.5rem;
78
+ padding: 0.75rem 3.75rem;
79
+ overflow-wrap: anywhere;
80
+ text-align: start;
81
+ }
82
+
83
+ img-alt-overlay [data-alt-dialog][open] {
84
+ display: flex;
85
+ flex-direction: column;
86
+ align-items: center;
87
+ justify-content: safe center;
88
+ }
89
+
90
+ img-alt-overlay [data-alt-text] {
91
+ flex: 0 0 auto;
92
+ max-inline-size: 100%;
93
+ white-space: pre-wrap;
94
+ margin: 0;
95
+ text-align: center;
96
+ outline: none;
97
+ }
98
+
99
+ img-alt-overlay [data-alt-close] {
100
+ position: absolute;
101
+ inset-block-start: 0.5rem;
102
+ inset-inline-end: 0.5rem;
103
+ display: grid;
104
+ place-items: center;
105
+ inline-size: 2.75rem;
106
+ block-size: 2.75rem;
107
+ margin: 0;
108
+ padding: 0;
109
+ border: 0;
110
+ background: transparent;
111
+ color: inherit;
112
+ font-size: 1.25em;
113
+ line-height: 1;
114
+ }
115
+
116
+ /* Keep a focused control stable if Safari blurs it during pointer activation. */
117
+ img-alt-overlay [data-alt-dialog] h2,
118
+ img-alt-overlay [data-alt-close]:not(:focus, :active) {
119
+ position: absolute;
120
+ inline-size: 1px;
121
+ block-size: 1px;
122
+ min-block-size: 0;
123
+ margin: -1px;
124
+ padding: 0;
125
+ border: 0;
126
+ overflow: hidden;
127
+ clip-path: inset(50%);
128
+ white-space: nowrap;
129
+ }
130
+
131
+ img-alt-overlay button:focus-visible {
132
+ outline: 3px solid var(--img-alt-focus-color, Highlight);
133
+ outline-offset: 3px;
134
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * A light-DOM image wrapper with a non-modal, in-image alt-text dialog.
3
+ * @cssprop --img-alt-button-inset-block-start - Opener block-start inset (0.5rem).
4
+ * @cssprop --img-alt-button-inset-block-end - Opener block-end inset (auto).
5
+ * @cssprop --img-alt-button-inset-inline-start - Opener inline-start inset (auto).
6
+ * @cssprop --img-alt-button-inset-inline-end - Opener inline-end inset (0.5rem).
7
+ * @cssprop --img-alt-overlay-inset-block-start - Overlay area block-start inset (3.75rem).
8
+ * @cssprop --img-alt-overlay-inset-block-end - Overlay area block-end inset (0.5rem).
9
+ * @cssprop --img-alt-overlay-inset-inline-start - Overlay area inline-start inset (0.5rem).
10
+ * @cssprop --img-alt-overlay-inset-inline-end - Overlay area inline-end inset (0.5rem).
11
+ * @cssprop --img-alt-dialog-min-block-size - Dialog minimum block size within the overlay area (30%, with a 4rem control-sized floor when space permits).
12
+ */
13
+ export class ImgAltOverlayElement extends HTMLElement {
14
+ static readonly observedAttributes: string[];
15
+ readonly state: 'idle' | 'inactive' | 'ready' | 'error';
16
+ /** Reread image structure and labels. Closes any open dialog. */
17
+ refresh(): void;
18
+ }
19
+
20
+ declare global {
21
+ interface HTMLElementTagNameMap {
22
+ 'img-alt-overlay': ImgAltOverlayElement;
23
+ }
24
+ interface HTMLElementEventMap {
25
+ 'img-alt-overlay:ready': CustomEvent<void>;
26
+ 'img-alt-overlay:error': CustomEvent<{ error: Error }>;
27
+ }
28
+ }
@@ -0,0 +1,274 @@
1
+ const BaseElement = globalThis.HTMLElement ?? class {};
2
+ let nextId = 0;
3
+
4
+ /**
5
+ * Reveal an image's alternative text in a non-modal, in-image dialog.
6
+ * @element img-alt-overlay
7
+ * @cssprop --img-alt-button-inset-block-start - Opener block-start inset (0.5rem).
8
+ * @cssprop --img-alt-button-inset-block-end - Opener block-end inset (auto).
9
+ * @cssprop --img-alt-button-inset-inline-start - Opener inline-start inset (auto).
10
+ * @cssprop --img-alt-button-inset-inline-end - Opener inline-end inset (0.5rem).
11
+ * @cssprop --img-alt-overlay-inset-block-start - Overlay area block-start inset (3.75rem).
12
+ * @cssprop --img-alt-overlay-inset-block-end - Overlay area block-end inset (0.5rem).
13
+ * @cssprop --img-alt-overlay-inset-inline-start - Overlay area inline-start inset (0.5rem).
14
+ * @cssprop --img-alt-overlay-inset-inline-end - Overlay area inline-end inset (0.5rem).
15
+ * @cssprop --img-alt-dialog-min-block-size - Dialog minimum block size within the overlay area (30%, with a 4rem control-sized floor when space permits).
16
+ */
17
+ export class ImgAltOverlayElement extends BaseElement {
18
+ static observedAttributes = ['button-label', 'dialog-label', 'close-label'];
19
+
20
+ constructor() {
21
+ super();
22
+ if (!this.ownerDocument) {
23
+ throw new Error('ImgAltOverlayElement requires a browser DOM.');
24
+ }
25
+ this._state = 'idle';
26
+ this._pending = false;
27
+ this._image = null;
28
+ this._ui = null;
29
+ this._observer = null;
30
+ this._revision = 0;
31
+ this._onClick = (event) => {
32
+ if (event.defaultPrevented || !this._ui) return;
33
+ const button = event.target.closest?.('button');
34
+ const { trigger, close, dialog } = this._ui;
35
+ if (button === trigger) this._toggleDialog();
36
+ else if (button === close) dialog.close();
37
+ };
38
+ this._onCommand = (event) => {
39
+ const ui = this._ui;
40
+ if (
41
+ !event.defaultPrevented &&
42
+ ui &&
43
+ event.target === ui.dialog &&
44
+ event.source === ui.trigger &&
45
+ event.command === '--toggle-alt'
46
+ ) {
47
+ this._toggleDialog();
48
+ }
49
+ };
50
+ this._onKeyDown = (event) => {
51
+ if (
52
+ event.defaultPrevented ||
53
+ event.key !== 'Escape' ||
54
+ !this._ui?.dialog.open
55
+ )
56
+ return;
57
+ event.preventDefault();
58
+ this._toggleDialog();
59
+ };
60
+ this._onClose = (event) => {
61
+ if (event.target === this._ui?.dialog) this._updateTrigger();
62
+ };
63
+ }
64
+
65
+ _toggleDialog() {
66
+ const ui = this._ui;
67
+ if (!ui) return;
68
+ if (ui.dialog.open) ui.dialog.close();
69
+ else ui.dialog.show();
70
+ if (this._ui === ui) this._updateTrigger();
71
+ }
72
+
73
+ _updateTrigger() {
74
+ const { trigger, dialog } = this._ui;
75
+ trigger.textContent =
76
+ this._labels[dialog.open ? 'close-label' : 'button-label'];
77
+ trigger.setAttribute('aria-expanded', String(dialog.open));
78
+ }
79
+
80
+ get state() {
81
+ return this._state;
82
+ }
83
+
84
+ connectedCallback() {
85
+ this._schedule();
86
+ }
87
+
88
+ disconnectedCallback() {
89
+ this._teardown();
90
+ }
91
+
92
+ attributeChangedCallback(name, previous, value) {
93
+ if (previous !== value && this.isConnected) this._schedule();
94
+ }
95
+
96
+ _schedule() {
97
+ if (this._pending) return;
98
+ this._pending = true;
99
+ queueMicrotask(() => {
100
+ this._pending = false;
101
+ if (this.isConnected) this.refresh();
102
+ });
103
+ }
104
+
105
+ /** Reread image markup and labels, closing any open dialog. */
106
+ refresh() {
107
+ const revision = this._teardown();
108
+ if (!this.isConnected || revision !== this._revision) return;
109
+ try {
110
+ const images = [...this.querySelectorAll('img')].filter((image) => {
111
+ for (
112
+ let parent = image.parentElement;
113
+ parent;
114
+ parent = parent.parentElement
115
+ ) {
116
+ if (parent instanceof ImgAltOverlayElement)
117
+ return parent === this;
118
+ }
119
+ return false;
120
+ });
121
+ if (images.length !== 1) {
122
+ throw new Error('img-alt-overlay requires exactly one image.');
123
+ }
124
+ if (this.closest('a, button, label')) {
125
+ throw new Error(
126
+ 'Place the image link inside img-alt-overlay, not around it; controls cannot be nested in a link, button, or label.',
127
+ );
128
+ }
129
+ this._labels = {};
130
+ for (const [attribute, fallback] of [
131
+ ['button-label', 'View alt text'],
132
+ ['dialog-label', 'Image description'],
133
+ ['close-label', 'Close'],
134
+ ]) {
135
+ const value = this.getAttribute(attribute) ?? fallback;
136
+ if (!value.trim())
137
+ throw new Error(`${attribute} must not be empty.`);
138
+ this._labels[attribute] = value;
139
+ }
140
+ this._image = images[0];
141
+ const { MutationObserver } = this.ownerDocument.defaultView;
142
+ this._observer = new MutationObserver(() => this._sync());
143
+ this._observer.observe(this._image, {
144
+ attributes: true,
145
+ attributeFilter: ['alt'],
146
+ });
147
+ this._sync();
148
+ } catch (error) {
149
+ this._fail(error);
150
+ }
151
+ }
152
+
153
+ _sync() {
154
+ const revision = this._revision;
155
+ const text = this._image?.getAttribute('alt') ?? '';
156
+ if (!text.trim()) {
157
+ this._removeUI();
158
+ if (revision === this._revision) this._state = 'inactive';
159
+ return;
160
+ }
161
+ try {
162
+ const created = !this._ui;
163
+ if (created) this._createUI();
164
+ this._ui.text.textContent = text;
165
+ this._state = 'ready';
166
+ if (created) {
167
+ this.dispatchEvent(
168
+ new CustomEvent('img-alt-overlay:ready', {
169
+ bubbles: true,
170
+ }),
171
+ );
172
+ }
173
+ } catch (error) {
174
+ this._fail(error);
175
+ }
176
+ }
177
+
178
+ _createUI() {
179
+ const document = this.ownerDocument;
180
+ const dialog = document.createElement('dialog');
181
+ if (
182
+ typeof dialog.show !== 'function' ||
183
+ typeof dialog.close !== 'function'
184
+ ) {
185
+ throw new Error('img-alt-overlay requires the native dialog API.');
186
+ }
187
+ const root = this.getRootNode();
188
+ let id;
189
+ do {
190
+ id = `img-alt-overlay-${++nextId}`;
191
+ } while (
192
+ root.getElementById(`${id}-dialog`) ||
193
+ root.getElementById(`${id}-heading`)
194
+ );
195
+ dialog.id = `${id}-dialog`;
196
+ dialog.setAttribute('data-alt-dialog', '');
197
+ dialog.setAttribute('aria-labelledby', `${id}-heading`);
198
+
199
+ const heading = document.createElement('h2');
200
+ heading.id = `${id}-heading`;
201
+ heading.textContent = this._labels['dialog-label'];
202
+ const text = document.createElement('p');
203
+ text.tabIndex = -1;
204
+ text.setAttribute('autofocus', '');
205
+ text.setAttribute('data-alt-text', '');
206
+ const trigger = document.createElement('button');
207
+ trigger.type = 'button';
208
+ trigger.textContent = this._labels['button-label'];
209
+ trigger.setAttribute('data-alt-trigger', '');
210
+ trigger.setAttribute('commandfor', dialog.id);
211
+ trigger.setAttribute('command', '--toggle-alt');
212
+ trigger.setAttribute('aria-controls', dialog.id);
213
+ trigger.setAttribute('aria-haspopup', 'dialog');
214
+ trigger.setAttribute('aria-expanded', 'false');
215
+ const close = document.createElement('button');
216
+ close.type = 'button';
217
+ close.setAttribute('aria-label', this._labels['close-label']);
218
+ const icon = document.createElement('span');
219
+ icon.setAttribute('aria-hidden', 'true');
220
+ icon.textContent = '\u2715';
221
+ close.append(icon);
222
+ close.setAttribute('data-alt-close', '');
223
+ close.setAttribute('commandfor', dialog.id);
224
+ close.setAttribute('command', 'close');
225
+ dialog.append(heading, text, close);
226
+ const container = document.createElement('div');
227
+ container.setAttribute('data-alt-ui', '');
228
+ container.append(dialog);
229
+ this._ui = { dialog, trigger, close, text, container };
230
+ this.append(trigger, container);
231
+ dialog.addEventListener('close', this._onClose);
232
+ this.addEventListener('keydown', this._onKeyDown);
233
+ if ('commandForElement' in trigger) {
234
+ dialog.addEventListener('command', this._onCommand);
235
+ } else {
236
+ this.addEventListener('click', this._onClick);
237
+ }
238
+ }
239
+
240
+ _removeUI() {
241
+ this.removeEventListener('click', this._onClick);
242
+ this.removeEventListener('keydown', this._onKeyDown);
243
+ if (!this._ui) return;
244
+ const { dialog, trigger, container } = this._ui;
245
+ this._ui = null;
246
+ dialog.removeEventListener('command', this._onCommand);
247
+ dialog.removeEventListener('close', this._onClose);
248
+ if (dialog.open) dialog.close();
249
+ trigger.remove();
250
+ container.remove();
251
+ }
252
+
253
+ _teardown() {
254
+ const revision = ++this._revision;
255
+ this._observer?.disconnect();
256
+ this._observer = null;
257
+ this._image = null;
258
+ this._state = 'idle';
259
+ this._removeUI();
260
+ return revision;
261
+ }
262
+
263
+ _fail(error) {
264
+ const revision = this._teardown();
265
+ if (!this.isConnected || revision !== this._revision) return;
266
+ this._state = 'error';
267
+ this.dispatchEvent(
268
+ new CustomEvent('img-alt-overlay:error', {
269
+ detail: { error },
270
+ bubbles: true,
271
+ }),
272
+ );
273
+ }
274
+ }
package/index.js ADDED
@@ -0,0 +1 @@
1
+ export { ImgAltOverlayElement } from './img-alt-overlay.js';
package/package.json CHANGED
@@ -1,6 +1,90 @@
1
1
  {
2
- "name": "@aarongustafson/img-alt-overlay",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@aarongustafson/img-alt-overlay",
3
+ "version": "0.0.1",
4
+ "description": "Reveal an image's alternative text in a non-modal, in-image dialog",
5
+ "keywords": [
6
+ "html",
7
+ "custom element",
8
+ "web component"
9
+ ],
10
+ "author": "Aaron Gustafson <aaron@easy-designs.net> (https://www.aaron-gustafson.com/)",
11
+ "license": "MIT",
12
+ "homepage": "https://github.com/aarongustafson/img-alt-overlay#readme",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/aarongustafson/img-alt-overlay.git"
16
+ },
17
+ "bugs": {
18
+ "url": "https://github.com/aarongustafson/img-alt-overlay/issues"
19
+ },
20
+ "type": "module",
21
+ "main": "index.js",
22
+ "module": "img-alt-overlay.js",
23
+ "customElements": "custom-elements.json",
24
+ "types": "img-alt-overlay.d.ts",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./img-alt-overlay.d.ts",
28
+ "import": "./index.js"
29
+ },
30
+ "./img-alt-overlay.js": {
31
+ "types": "./img-alt-overlay.d.ts",
32
+ "import": "./img-alt-overlay.js"
33
+ },
34
+ "./define.js": {
35
+ "types": "./define.d.ts",
36
+ "import": "./define.js"
37
+ },
38
+ "./img-alt-overlay.css": "./img-alt-overlay.css",
39
+ "./custom-elements.json": "./custom-elements.json"
40
+ },
41
+ "files": [
42
+ "img-alt-overlay.js",
43
+ "img-alt-overlay.d.ts",
44
+ "define.js",
45
+ "define.d.ts",
46
+ "img-alt-overlay.css",
47
+ "index.js",
48
+ "custom-elements.json",
49
+ "README.md",
50
+ "LICENSE"
51
+ ],
52
+ "engines": {
53
+ "node": ">=22.0.0"
54
+ },
55
+ "packageManager": "npm@11.15.0",
56
+ "scripts": {
57
+ "test": "vitest",
58
+ "test:ui": "vitest --ui",
59
+ "test:run": "vitest run",
60
+ "test:types": "tsc --noEmit --strict --lib DOM,ES2022 --module NodeNext --moduleResolution NodeNext test/types.ts && tsc --noEmit --strict --lib DOM,ES2022 --module NodeNext --moduleResolution NodeNext test/registration-types.ts",
61
+ "test:browser": "playwright test",
62
+ "test:coverage": "vitest run --coverage",
63
+ "lint": "npm run lint:eslint && npm run lint:prettier",
64
+ "format": "npm run format:eslint && npm run format:prettier",
65
+ "lint:eslint": "eslint .",
66
+ "format:eslint": "eslint . --fix",
67
+ "lint:prettier": "prettier \"**/*.{js,ts,html,css,md,json,yml}\" --check --ignore-path .gitignore",
68
+ "format:prettier": "prettier \"**/*.{js,ts,html,css,md,json,yml}\" --write --ignore-path .gitignore",
69
+ "version": "node scripts/update-demo-versions.js && git add demo/esm.html",
70
+ "postversion": "git push --follow-tags"
71
+ },
72
+ "devDependencies": {
73
+ "@playwright/test": "^1.64.1",
74
+ "@open-wc/eslint-config": "^13.1.0",
75
+ "@testing-library/dom": "^10.4.2",
76
+ "@testing-library/user-event": "^14.6.7",
77
+ "@vitest/coverage-v8": "^5.0.5",
78
+ "@vitest/ui": "^5.0.5",
79
+ "eslint": "^10.12.0",
80
+ "eslint-config-prettier": "^10.1.8",
81
+ "happy-dom": "^21.0.4",
82
+ "marked": "18.1.0",
83
+ "prettier": "^3.9.9",
84
+ "vitest": "^5.0.5",
85
+ "typescript": "^7.0.2"
86
+ },
87
+ "publishConfig": {
88
+ "access": "public"
89
+ }
90
+ }