@adia-ai/web-components 0.6.35 → 0.6.37
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/CHANGELOG.md +56 -0
- package/components/badge/badge.a2ui.json +10 -0
- package/components/badge/badge.css +70 -0
- package/components/badge/badge.yaml +20 -0
- package/components/blockquote/blockquote.a2ui.json +121 -0
- package/components/blockquote/blockquote.class.js +68 -0
- package/components/blockquote/blockquote.css +46 -0
- package/components/blockquote/blockquote.d.ts +31 -0
- package/components/blockquote/blockquote.js +17 -0
- package/components/blockquote/blockquote.yaml +124 -0
- package/components/button/button.css +11 -3
- package/components/calendar-picker/calendar-picker.a2ui.json +15 -0
- package/components/calendar-picker/calendar-picker.class.js +7 -1
- package/components/calendar-picker/calendar-picker.yaml +14 -0
- package/components/color-input/color-input.a2ui.json +2 -2
- package/components/color-input/color-input.class.js +9 -2
- package/components/color-input/color-input.yaml +2 -2
- package/components/combobox/combobox.class.js +4 -0
- package/components/combobox/combobox.css +12 -0
- package/components/context-menu/context-menu.a2ui.json +159 -0
- package/components/context-menu/context-menu.class.js +275 -0
- package/components/context-menu/context-menu.css +56 -0
- package/components/context-menu/context-menu.d.ts +70 -0
- package/components/context-menu/context-menu.js +17 -0
- package/components/context-menu/context-menu.yaml +136 -0
- package/components/date-range-picker/date-range-picker.a2ui.json +15 -0
- package/components/date-range-picker/date-range-picker.class.js +3 -1
- package/components/date-range-picker/date-range-picker.css +4 -1
- package/components/date-range-picker/date-range-picker.yaml +14 -0
- package/components/datetime-picker/datetime-picker.a2ui.json +15 -0
- package/components/datetime-picker/datetime-picker.class.js +3 -1
- package/components/datetime-picker/datetime-picker.css +7 -1
- package/components/datetime-picker/datetime-picker.d.ts +2 -0
- package/components/datetime-picker/datetime-picker.yaml +14 -0
- package/components/empty-state/empty-state.class.js +2 -0
- package/components/feed/feed.class.js +13 -5
- package/components/feed/feed.css +14 -0
- package/components/index.js +9 -0
- package/components/input/input.css +15 -1
- package/components/input/input.test.js +40 -0
- package/components/integration-card/integration-card.class.js +9 -0
- package/components/integration-card/integration-card.test.js +4 -3
- package/components/nav-group/nav-group.css +7 -1
- package/components/number-format/number-format.a2ui.json +180 -0
- package/components/number-format/number-format.class.js +96 -0
- package/components/number-format/number-format.css +18 -0
- package/components/number-format/number-format.d.ts +68 -0
- package/components/number-format/number-format.js +17 -0
- package/components/number-format/number-format.yaml +204 -0
- package/components/pagination/pagination.a2ui.json +19 -2
- package/components/pagination/pagination.class.js +90 -37
- package/components/pagination/pagination.css +32 -127
- package/components/pagination/pagination.d.ts +8 -2
- package/components/pagination/pagination.test.js +195 -0
- package/components/pagination/pagination.yaml +22 -1
- package/components/password-strength/password-strength.a2ui.json +152 -0
- package/components/password-strength/password-strength.class.js +157 -0
- package/components/password-strength/password-strength.css +80 -0
- package/components/password-strength/password-strength.d.ts +59 -0
- package/components/password-strength/password-strength.js +17 -0
- package/components/password-strength/password-strength.yaml +153 -0
- package/components/popover/popover.css +43 -23
- package/components/popover/popover.yaml +8 -4
- package/components/qr-code/QR-TEST.svg +4 -0
- package/components/qr-code/qr-code.a2ui.json +154 -0
- package/components/qr-code/qr-code.class.js +129 -0
- package/components/qr-code/qr-code.css +41 -0
- package/components/qr-code/qr-code.d.ts +83 -0
- package/components/qr-code/qr-code.js +17 -0
- package/components/qr-code/qr-code.yaml +203 -0
- package/components/qr-code/qr-encoder.js +633 -0
- package/components/relative-time/relative-time.a2ui.json +120 -0
- package/components/relative-time/relative-time.class.js +136 -0
- package/components/relative-time/relative-time.css +22 -0
- package/components/relative-time/relative-time.d.ts +51 -0
- package/components/relative-time/relative-time.js +17 -0
- package/components/relative-time/relative-time.yaml +133 -0
- package/components/search/search.class.js +2 -0
- package/components/segmented/segmented.class.js +5 -1
- package/components/select/select.class.js +4 -0
- package/components/skip-nav/skip-nav.a2ui.json +92 -0
- package/components/skip-nav/skip-nav.class.js +45 -0
- package/components/skip-nav/skip-nav.css +54 -0
- package/components/skip-nav/skip-nav.d.ts +27 -0
- package/components/skip-nav/skip-nav.js +12 -0
- package/components/skip-nav/skip-nav.yaml +68 -0
- package/components/slider/slider.a2ui.json +16 -1
- package/components/slider/slider.class.js +264 -122
- package/components/slider/slider.css +82 -2
- package/components/slider/slider.d.ts +19 -3
- package/components/slider/slider.test.js +55 -0
- package/components/slider/slider.yaml +28 -6
- package/components/table/table.class.js +29 -6
- package/components/table/table.css +31 -4
- package/components/table-toolbar/table-toolbar.class.js +4 -1
- package/components/tag/tag.a2ui.json +10 -0
- package/components/tag/tag.class.js +8 -1
- package/components/tag/tag.css +108 -20
- package/components/tag/tag.d.ts +14 -0
- package/components/tag/tag.test.js +99 -1
- package/components/tag/tag.yaml +20 -0
- package/components/tags-input/tags-input.class.js +10 -3
- package/components/tags-input/tags-input.css +12 -3
- package/components/textarea/textarea.css +10 -1
- package/components/toast/toast.class.js +12 -4
- package/components/toc/toc.a2ui.json +159 -0
- package/components/toc/toc.class.js +222 -0
- package/components/toc/toc.css +92 -0
- package/components/toc/toc.d.ts +61 -0
- package/components/toc/toc.js +17 -0
- package/components/toc/toc.yaml +180 -0
- package/components/toolbar/toolbar.class.js +3 -0
- package/components/visually-hidden/visually-hidden.a2ui.json +71 -0
- package/components/visually-hidden/visually-hidden.class.js +14 -0
- package/components/visually-hidden/visually-hidden.css +25 -0
- package/components/visually-hidden/visually-hidden.d.ts +26 -0
- package/components/visually-hidden/visually-hidden.js +12 -0
- package/components/visually-hidden/visually-hidden.yaml +54 -0
- package/core/anchor.js +19 -3
- package/core/provider.js +19 -2
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +101 -89
- package/package.json +1 -1
- package/styles/colors/semantics.css +11 -2
- package/styles/components.css +9 -0
- package/styles/resets.css +10 -0
package/components/tag/tag.yaml
CHANGED
|
@@ -51,6 +51,26 @@ props:
|
|
|
51
51
|
- success
|
|
52
52
|
- warning
|
|
53
53
|
- danger
|
|
54
|
+
tone:
|
|
55
|
+
description: |
|
|
56
|
+
Fill style — orthogonal to [variant]. Three values:
|
|
57
|
+
- `solid` (default for family variants) — saturated bg + on-strong
|
|
58
|
+
(near-white) text. The chip IS the state.
|
|
59
|
+
- `muted` — tinted bg with scheme-paired text. Matches <badge-ui>'s
|
|
60
|
+
default look. Use on metadata chips in dense lists where the
|
|
61
|
+
saturated default would compete for attention.
|
|
62
|
+
- `outline` — transparent bg + family-colored border + family-colored
|
|
63
|
+
text. The lightest visual weight; good in dense data tables or
|
|
64
|
+
faceted filter rows where multiple chips would otherwise compete.
|
|
65
|
+
The `default` variant (no family) stays quiet chrome regardless of
|
|
66
|
+
tone unless `tone="solid"` is set explicitly (high-contrast neutral
|
|
67
|
+
inverse), or `tone="outline"` (fg-muted text + subtle border).
|
|
68
|
+
type: string
|
|
69
|
+
default: solid
|
|
70
|
+
enum:
|
|
71
|
+
- solid
|
|
72
|
+
- muted
|
|
73
|
+
- outline
|
|
54
74
|
events:
|
|
55
75
|
remove:
|
|
56
76
|
description: Fired when the dismiss button is activated.
|
|
@@ -518,12 +518,19 @@ export class UITagsInput extends UIFormElement {
|
|
|
518
518
|
#handleChipRemove(e) {
|
|
519
519
|
const chip = e.target.closest('tag-ui[data-index]');
|
|
520
520
|
if (!chip) return;
|
|
521
|
-
//
|
|
522
|
-
//
|
|
523
|
-
//
|
|
521
|
+
// tag-ui's `remove` CustomEvent bubbles SYNCHRONOUSLY before tag-ui's
|
|
522
|
+
// own `this.remove()` runs (see tag/tag.class.js:59-63 — dispatch then
|
|
523
|
+
// remove). If we react inside the bubble (call `#renderChips()` now),
|
|
524
|
+
// the DOM still contains the about-to-be-removed chip and the
|
|
525
|
+
// positional reconcile in `#renderChips()` mutates the WRONG chip's
|
|
526
|
+
// text — visually erasing one or more adjacent chips. Pre-emptively
|
|
527
|
+
// detach the source chip ourselves so the DOM matches `#value` before
|
|
528
|
+
// render. tag-ui's later `this.remove()` is a harmless no-op on an
|
|
529
|
+
// already-detached node.
|
|
524
530
|
e.stopPropagation();
|
|
525
531
|
const idx = Number(chip.dataset.index);
|
|
526
532
|
if (!Number.isInteger(idx)) return;
|
|
533
|
+
chip.remove();
|
|
527
534
|
this.#removeByIndex(idx, 'chip-click');
|
|
528
535
|
}
|
|
529
536
|
|
|
@@ -110,14 +110,23 @@
|
|
|
110
110
|
white-space: pre-wrap;
|
|
111
111
|
overflow-wrap: anywhere;
|
|
112
112
|
cursor: text;
|
|
113
|
+
/* Positioning context for the [data-empty]::before placeholder pseudo
|
|
114
|
+
(kept out of inline flow so the caret renders at content-start). */
|
|
115
|
+
position: relative;
|
|
113
116
|
}
|
|
114
|
-
/* Empty-state placeholder
|
|
115
|
-
|
|
116
|
-
|
|
117
|
+
/* Empty-state placeholder. The pseudo is absolutely positioned (NOT
|
|
118
|
+
inline) so an in-flow pseudo box doesn't push the caret-at-position-0
|
|
119
|
+
to the right of the placeholder text after type-then-delete. Same
|
|
120
|
+
pattern input-ui + textarea-ui + combobox-ui use. */
|
|
117
121
|
[data-inline-input][data-empty]::before {
|
|
118
122
|
content: attr(data-placeholder);
|
|
119
123
|
color: var(--tags-input-placeholder-fg, var(--tags-input-placeholder-fg-default));
|
|
120
124
|
pointer-events: none;
|
|
125
|
+
position: absolute;
|
|
126
|
+
inset: 0;
|
|
127
|
+
padding: inherit;
|
|
128
|
+
display: flex;
|
|
129
|
+
align-items: center;
|
|
121
130
|
}
|
|
122
131
|
|
|
123
132
|
/* ── Spinner (async validator) ── */
|
|
@@ -60,6 +60,10 @@ textarea-ui:not([disabled]) [slot="text"]:hover {
|
|
|
60
60
|
[slot="label"][label]::after { content: attr(label); }
|
|
61
61
|
|
|
62
62
|
[slot="text"] {
|
|
63
|
+
/* Positioning context for the [data-empty]::before placeholder pseudo,
|
|
64
|
+
which is absolutely positioned (out of inline flow) so the caret
|
|
65
|
+
renders at content-start instead of after the placeholder text. */
|
|
66
|
+
position: relative;
|
|
63
67
|
min-height: var(--textarea-min-height, var(--textarea-min-height-default));
|
|
64
68
|
padding: var(--textarea-py, var(--textarea-py-default)) var(--textarea-px, var(--textarea-px-default));
|
|
65
69
|
border: 1px solid var(--textarea-border, var(--textarea-border-default));
|
|
@@ -99,11 +103,16 @@ textarea-ui:not([disabled]) [slot="text"]:hover {
|
|
|
99
103
|
color: var(--textarea-label-fg-focus, var(--textarea-label-fg-focus-default));
|
|
100
104
|
}
|
|
101
105
|
|
|
102
|
-
/* Placeholder
|
|
106
|
+
/* Placeholder — see input.css companion comment. Out-of-flow positioning
|
|
107
|
+
prevents the inline pseudo box from pushing the caret position to the
|
|
108
|
+
right of the placeholder text after type-then-delete. */
|
|
103
109
|
[slot="text"][data-empty]::before {
|
|
104
110
|
content: attr(data-placeholder);
|
|
105
111
|
color: var(--textarea-placeholder-fg, var(--textarea-placeholder-fg-default));
|
|
106
112
|
pointer-events: none;
|
|
113
|
+
position: absolute;
|
|
114
|
+
inset: 0;
|
|
115
|
+
padding: inherit;
|
|
107
116
|
}
|
|
108
117
|
|
|
109
118
|
/* Disabled */
|
|
@@ -78,10 +78,14 @@ export class UIToast extends UIElement {
|
|
|
78
78
|
* @param {string} [opts.variant='info'] — `error` aliases to `danger`.
|
|
79
79
|
* @param {number} [opts.duration=4000]
|
|
80
80
|
* @param {string} [opts.position='bottom-right']
|
|
81
|
+
* @param {boolean} [opts.dismissible] Force-show the close button on auto-
|
|
82
|
+
* fade toasts (sticky toasts always show it).
|
|
83
|
+
* @param {string} [opts.action] Optional action button label (e.g. 'Undo').
|
|
84
|
+
* @param {function} [opts.onAction] Action button click handler.
|
|
81
85
|
* @returns {{id:string|null, dismiss:function, update:function}} FeedHandle.
|
|
82
86
|
*/
|
|
83
87
|
static show(opts = {}) {
|
|
84
|
-
const { text, variant = 'info',
|
|
88
|
+
const { text, variant = 'info', position = 'bottom-right', action, onAction } = opts;
|
|
85
89
|
// §224 (v0.5.9, FEEDBACK-10 §2): mark the spawned feed container so
|
|
86
90
|
// DOM-query consumers (Playwright, devtools, instrumentation) can
|
|
87
91
|
// distinguish toast-spawned <feed-ui> from user-authored ones. The marker
|
|
@@ -91,11 +95,15 @@ export class UIToast extends UIElement {
|
|
|
91
95
|
if (container && !container.hasAttribute('data-spawned-by')) {
|
|
92
96
|
container.setAttribute('data-spawned-by', 'toast');
|
|
93
97
|
}
|
|
94
|
-
|
|
98
|
+
const payload = {
|
|
95
99
|
text,
|
|
96
100
|
variant: variant === 'error' ? 'danger' : variant,
|
|
97
|
-
duration,
|
|
98
101
|
position,
|
|
99
|
-
}
|
|
102
|
+
};
|
|
103
|
+
if ('duration' in opts) payload.duration = opts.duration;
|
|
104
|
+
if ('dismissible' in opts) payload.dismissible = !!opts.dismissible;
|
|
105
|
+
if (action) payload.action = action;
|
|
106
|
+
if (typeof onAction === 'function') payload.onAction = onAction;
|
|
107
|
+
return UIFeed.post(payload);
|
|
100
108
|
}
|
|
101
109
|
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://adiaui.dev/a2ui/v0_9/components/TableOfContents.json",
|
|
4
|
+
"title": "TableOfContents",
|
|
5
|
+
"description": "Auto-generated in-page table of contents. Scans a target container\nfor headings (default `h2,h3`), ensures each has an `id` (slugifies\nthe text content if missing), and stamps a `<nav>` list of anchor\nlinks. An `IntersectionObserver` tracks the active heading and\napplies `[data-active]` to the matching link so consumers can style\nthe currently-visible section. Smooth-scroll on click is handled by\nthe global `scroll-behavior: smooth` set in resets.css.\n\nPair with a sticky container (`position: sticky; top: <offset>;`) in\nan aside / right rail for the classic docs-site outline pattern.\n",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"allOf": [
|
|
8
|
+
{
|
|
9
|
+
"$ref": "common_types.json#/$defs/ComponentCommon"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"$ref": "common_types.json#/$defs/CatalogComponentCommon"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"properties": {
|
|
16
|
+
"component": {
|
|
17
|
+
"const": "TableOfContents"
|
|
18
|
+
},
|
|
19
|
+
"headings": {
|
|
20
|
+
"description": "CSS selector listing the heading tags to include (e.g. `h2,h3`).\nThe selector is matched against the target container's\ndescendants. Default `h2,h3` produces the common two-level\noutline.\n",
|
|
21
|
+
"type": "string",
|
|
22
|
+
"default": "h2,h3"
|
|
23
|
+
},
|
|
24
|
+
"label": {
|
|
25
|
+
"description": "`aria-label` for the nav. Defaults to `Table of contents`.\n",
|
|
26
|
+
"type": "string",
|
|
27
|
+
"default": "Table of contents"
|
|
28
|
+
},
|
|
29
|
+
"offset": {
|
|
30
|
+
"description": "Top offset in pixels for active-state detection. The\nIntersectionObserver's `rootMargin` is set to\n`-<offset>px 0px -50% 0px` so the active item becomes the\nheading nearest the top of the viewport BELOW any sticky header\nchrome. Tune to match the height of your sticky topbar (default\n`80` is a reasonable docs-shell value).\n",
|
|
31
|
+
"type": "number",
|
|
32
|
+
"default": 80
|
|
33
|
+
},
|
|
34
|
+
"target": {
|
|
35
|
+
"description": "CSS selector pointing to the container to scan. Empty (default)\nscans the toc-ui's parent element. Set to `#article` /\n`[data-toc-target]` / `main` for a specific scope.\n",
|
|
36
|
+
"type": "string",
|
|
37
|
+
"default": ""
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"required": [
|
|
41
|
+
"component"
|
|
42
|
+
],
|
|
43
|
+
"unevaluatedProperties": false,
|
|
44
|
+
"x-adiaui": {
|
|
45
|
+
"anti_patterns": [
|
|
46
|
+
{
|
|
47
|
+
"fix": "<toc-ui target=\"#article\"></toc-ui>\n<article id=\"article\">\n <h2>...</h2><h3>...</h3>\n</article>\n",
|
|
48
|
+
"why": "toc-ui's default scans its PARENT for headings. If toc-ui is a\nsibling of the content (not inside it), the scan finds the\nheadings inside the toc-ui's parent which usually means the\nheadings + the toc itself — works only by coincidence. Set\n[target] explicitly for clarity.\n",
|
|
49
|
+
"wrong": "<toc-ui></toc-ui>\n<h2>...</h2><h3>...</h3>\n"
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
"category": "navigation",
|
|
53
|
+
"composes": [],
|
|
54
|
+
"events": {
|
|
55
|
+
"section-change": {
|
|
56
|
+
"description": "Fired when the active heading changes (the user scrolls into a new section). Detail carries the new active id.",
|
|
57
|
+
"detail": {
|
|
58
|
+
"activeId": "string"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"examples": [
|
|
63
|
+
{
|
|
64
|
+
"description": "TOC scanning the parent container.",
|
|
65
|
+
"a2ui": "[\n {\n \"id\": \"page\",\n \"component\": \"Section\",\n \"children\": [\"toc\", \"h1\", \"h2-1\", \"p-1\", \"h2-2\", \"p-2\"]\n },\n {\"id\": \"toc\", \"component\": \"TableOfContents\"},\n {\"id\": \"h1\", \"component\": \"Text\", \"variant\": \"title\", \"textContent\": \"Article title\"},\n {\"id\": \"h2-1\", \"component\": \"Text\", \"variant\": \"heading\", \"textContent\": \"Introduction\"},\n {\"id\": \"p-1\", \"component\": \"Text\", \"textContent\": \"...\"},\n {\"id\": \"h2-2\", \"component\": \"Text\", \"variant\": \"heading\", \"textContent\": \"Conclusion\"},\n {\"id\": \"p-2\", \"component\": \"Text\", \"textContent\": \"...\"}\n]\n",
|
|
66
|
+
"name": "default"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"description": "TOC scanning a specific article container.",
|
|
70
|
+
"a2ui": "[\n {\n \"id\": \"shell\",\n \"component\": \"Row\",\n \"children\": [\"toc\", \"article\"]\n },\n {\n \"id\": \"toc\",\n \"component\": \"TableOfContents\",\n \"target\": \"#main-article\",\n \"headings\": \"h2,h3,h4\"\n },\n {\n \"id\": \"article\",\n \"component\": \"Section\",\n \"attrs\": { \"id\": \"main-article\" },\n \"children\": []\n }\n]\n",
|
|
71
|
+
"name": "targeted"
|
|
72
|
+
}
|
|
73
|
+
],
|
|
74
|
+
"keywords": [
|
|
75
|
+
"toc",
|
|
76
|
+
"table-of-contents",
|
|
77
|
+
"outline",
|
|
78
|
+
"sub-nav",
|
|
79
|
+
"page-nav",
|
|
80
|
+
"in-page-nav",
|
|
81
|
+
"heading-nav"
|
|
82
|
+
],
|
|
83
|
+
"name": "UITableOfContents",
|
|
84
|
+
"related": [
|
|
85
|
+
"aside",
|
|
86
|
+
"nav",
|
|
87
|
+
"link",
|
|
88
|
+
"text"
|
|
89
|
+
],
|
|
90
|
+
"slots": {
|
|
91
|
+
"default": {
|
|
92
|
+
"description": "Stamped automatically — a `<nav>` containing a flat `<ul>` of `<a href=\"#id\">` links. Override by authoring your own content; auto-stamp is skipped when a `<nav>` is already present."
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"states": [
|
|
96
|
+
{
|
|
97
|
+
"description": "Default — no active section yet.",
|
|
98
|
+
"name": "idle"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"description": "A section is in view; the matching `<a>` carries `[data-active]`.",
|
|
102
|
+
"attribute": "data-active",
|
|
103
|
+
"name": "active"
|
|
104
|
+
}
|
|
105
|
+
],
|
|
106
|
+
"status": "stable",
|
|
107
|
+
"synonyms": {
|
|
108
|
+
"outline": [
|
|
109
|
+
"toc",
|
|
110
|
+
"table-of-contents"
|
|
111
|
+
],
|
|
112
|
+
"toc": [
|
|
113
|
+
"table-of-contents",
|
|
114
|
+
"outline"
|
|
115
|
+
]
|
|
116
|
+
},
|
|
117
|
+
"tag": "toc-ui",
|
|
118
|
+
"tokens": {
|
|
119
|
+
"--toc-fg": {
|
|
120
|
+
"description": "Default link color.",
|
|
121
|
+
"default": "var(--a-fg-muted)"
|
|
122
|
+
},
|
|
123
|
+
"--toc-fg-active": {
|
|
124
|
+
"description": "Active-link color.",
|
|
125
|
+
"default": "var(--a-fg)"
|
|
126
|
+
},
|
|
127
|
+
"--toc-fg-hover": {
|
|
128
|
+
"description": "Hover color.",
|
|
129
|
+
"default": "var(--a-fg)"
|
|
130
|
+
},
|
|
131
|
+
"--toc-gap": {
|
|
132
|
+
"description": "Vertical gap between links.",
|
|
133
|
+
"default": "var(--a-space-1)"
|
|
134
|
+
},
|
|
135
|
+
"--toc-indent": {
|
|
136
|
+
"description": "Per-depth-level indent.",
|
|
137
|
+
"default": "var(--a-space-3)"
|
|
138
|
+
},
|
|
139
|
+
"--toc-padding-block": {
|
|
140
|
+
"description": "Per-link block padding (top/bottom).",
|
|
141
|
+
"default": "var(--a-space-1)"
|
|
142
|
+
},
|
|
143
|
+
"--toc-padding-inline": {
|
|
144
|
+
"description": "Per-link inline padding.",
|
|
145
|
+
"default": "var(--a-space-2)"
|
|
146
|
+
},
|
|
147
|
+
"--toc-rule-active": {
|
|
148
|
+
"description": "Active-item indicator rule color (left border).",
|
|
149
|
+
"default": "var(--a-accent-bg)"
|
|
150
|
+
},
|
|
151
|
+
"--toc-size": {
|
|
152
|
+
"description": "Link font-size.",
|
|
153
|
+
"default": "var(--a-ui-sm)"
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
"traits": [],
|
|
157
|
+
"version": 1
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Non-side-effect class export for `<toc-ui>`.
|
|
3
|
+
*
|
|
4
|
+
* Importing this file gives you the class without auto-registering the
|
|
5
|
+
* tag. Useful for test isolation, subclassing with tag-name override,
|
|
6
|
+
* or selective composition.
|
|
7
|
+
*
|
|
8
|
+
* The auto-register path stays at `@adia-ai/web-components/components/toc`
|
|
9
|
+
* (which imports this file + calls `defineIfFree()`).
|
|
10
|
+
*
|
|
11
|
+
* @see ../../USAGE.md#registration--auto-vs-explicit
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* <toc-ui target="#article" headings="h2,h3"></toc-ui>
|
|
16
|
+
*
|
|
17
|
+
* Layout:
|
|
18
|
+
* <nav aria-label="Table of contents">
|
|
19
|
+
* <ul>
|
|
20
|
+
* <li><a href="#intro" data-depth="0">Introduction</a></li>
|
|
21
|
+
* <li><a href="#setup" data-depth="1">Setup</a></li>
|
|
22
|
+
* <li><a href="#conclude" data-depth="0">Conclusion</a></li>
|
|
23
|
+
* </ul>
|
|
24
|
+
* </nav>
|
|
25
|
+
*
|
|
26
|
+
* Depth is relative — the shallowest heading found becomes depth 0,
|
|
27
|
+
* each level deeper gets +1. CSS indents via `[data-depth=N]`.
|
|
28
|
+
*
|
|
29
|
+
* Active state: an IntersectionObserver picks the heading nearest the
|
|
30
|
+
* top of the viewport (below the configured offset). That heading's
|
|
31
|
+
* link gets `[data-active]`. The `section-change` event fires when
|
|
32
|
+
* the active id changes.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { UIElement } from '../../core/element.js';
|
|
36
|
+
|
|
37
|
+
function slugify(text) {
|
|
38
|
+
return (text || '')
|
|
39
|
+
.trim()
|
|
40
|
+
.toLowerCase()
|
|
41
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
42
|
+
.replace(/^-+|-+$/g, '')
|
|
43
|
+
|| 'section';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class UITableOfContents extends UIElement {
|
|
47
|
+
static properties = {
|
|
48
|
+
target: { type: String, default: '', reflect: true },
|
|
49
|
+
headings: { type: String, default: 'h2,h3', reflect: true },
|
|
50
|
+
offset: { type: Number, default: 80, reflect: true },
|
|
51
|
+
label: { type: String, default: 'Table of contents', reflect: true },
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
static template = () => null;
|
|
55
|
+
|
|
56
|
+
#observer = null;
|
|
57
|
+
#activeId = null;
|
|
58
|
+
#lastRoot = null;
|
|
59
|
+
#lastHeadingsKey = '';
|
|
60
|
+
#scheduledScan = false;
|
|
61
|
+
|
|
62
|
+
#resolveRoot() {
|
|
63
|
+
if (this.target) {
|
|
64
|
+
const found = this.ownerDocument?.querySelector(this.target) || document.querySelector(this.target);
|
|
65
|
+
if (!found) return null;
|
|
66
|
+
return found;
|
|
67
|
+
}
|
|
68
|
+
return this.parentElement;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
#scanHeadings() {
|
|
72
|
+
const root = this.#resolveRoot();
|
|
73
|
+
if (!root) return [];
|
|
74
|
+
const selector = (this.headings || 'h2,h3').trim();
|
|
75
|
+
const headings = Array.from(root.querySelectorAll(selector));
|
|
76
|
+
// Exclude any headings that are descendants of this toc-ui itself
|
|
77
|
+
// (avoid the toc reading its own stamped <a> labels when they
|
|
78
|
+
// contain header tags, and avoid recursion when toc-ui is inside
|
|
79
|
+
// the scan root).
|
|
80
|
+
return headings.filter((h) => !this.contains(h));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
#ensureIds(headings) {
|
|
84
|
+
const doc = this.ownerDocument || document;
|
|
85
|
+
for (const h of headings) {
|
|
86
|
+
if (h.id) continue;
|
|
87
|
+
const base = slugify(h.textContent);
|
|
88
|
+
let id = base;
|
|
89
|
+
let i = 1;
|
|
90
|
+
while (doc.getElementById(id) && doc.getElementById(id) !== h) {
|
|
91
|
+
id = `${base}-${i++}`;
|
|
92
|
+
}
|
|
93
|
+
h.id = id;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
#computeDepths(headings) {
|
|
98
|
+
if (headings.length === 0) return [];
|
|
99
|
+
// Heading levels (1..6) — find the shallowest as the depth-0 baseline.
|
|
100
|
+
const levels = headings.map((h) => {
|
|
101
|
+
const m = /^h([1-6])$/i.exec(h.tagName);
|
|
102
|
+
return m ? parseInt(m[1], 10) : 6;
|
|
103
|
+
});
|
|
104
|
+
const minLevel = Math.min(...levels);
|
|
105
|
+
return levels.map((l) => l - minLevel);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#render() {
|
|
109
|
+
const headings = this.#scanHeadings();
|
|
110
|
+
this.#ensureIds(headings);
|
|
111
|
+
const depths = this.#computeDepths(headings);
|
|
112
|
+
|
|
113
|
+
// Compute a key from id+depth to skip re-stamping when nothing
|
|
114
|
+
// structurally changed (cheap idempotence).
|
|
115
|
+
const key = headings.map((h, i) => `${h.id}:${depths[i]}`).join('|');
|
|
116
|
+
if (key === this.#lastHeadingsKey && this.querySelector('nav')) {
|
|
117
|
+
this.#setupObserver(headings);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
this.#lastHeadingsKey = key;
|
|
121
|
+
|
|
122
|
+
// Clear prior stamp (preserve any consumer-authored non-stamped
|
|
123
|
+
// content by checking the data-stamped marker).
|
|
124
|
+
const prior = this.querySelector('nav[data-stamped]');
|
|
125
|
+
if (prior) prior.remove();
|
|
126
|
+
|
|
127
|
+
if (headings.length === 0) {
|
|
128
|
+
this.removeAttribute('aria-label');
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const nav = document.createElement('nav');
|
|
133
|
+
nav.setAttribute('data-stamped', '');
|
|
134
|
+
nav.setAttribute('aria-label', this.label || 'Table of contents');
|
|
135
|
+
|
|
136
|
+
const ul = document.createElement('ul');
|
|
137
|
+
|
|
138
|
+
headings.forEach((h, i) => {
|
|
139
|
+
const li = document.createElement('li');
|
|
140
|
+
const a = document.createElement('a');
|
|
141
|
+
a.setAttribute('href', `#${h.id}`);
|
|
142
|
+
a.setAttribute('data-target-id', h.id);
|
|
143
|
+
a.setAttribute('data-depth', String(depths[i]));
|
|
144
|
+
a.textContent = (h.textContent || '').trim();
|
|
145
|
+
li.appendChild(a);
|
|
146
|
+
ul.appendChild(li);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
nav.appendChild(ul);
|
|
150
|
+
this.appendChild(nav);
|
|
151
|
+
|
|
152
|
+
this.#setupObserver(headings);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
#setupObserver(headings) {
|
|
156
|
+
this.#observer?.disconnect();
|
|
157
|
+
if (typeof IntersectionObserver === 'undefined') return;
|
|
158
|
+
if (headings.length === 0) return;
|
|
159
|
+
|
|
160
|
+
const offset = Number.isFinite(this.offset) ? this.offset : 80;
|
|
161
|
+
this.#observer = new IntersectionObserver((entries) => {
|
|
162
|
+
// Among entries currently intersecting, pick the one nearest the
|
|
163
|
+
// top of the viewport (smallest boundingClientRect.top ≥ 0).
|
|
164
|
+
const visible = entries
|
|
165
|
+
.filter((e) => e.isIntersecting)
|
|
166
|
+
.sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top);
|
|
167
|
+
if (visible.length === 0) return;
|
|
168
|
+
this.#setActive(visible[0].target.id);
|
|
169
|
+
}, {
|
|
170
|
+
rootMargin: `-${offset}px 0px -50% 0px`,
|
|
171
|
+
threshold: 0,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
headings.forEach((h) => this.#observer.observe(h));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
#setActive(id) {
|
|
178
|
+
if (id === this.#activeId) return;
|
|
179
|
+
this.#activeId = id;
|
|
180
|
+
this.querySelectorAll('a[data-target-id]').forEach((a) => {
|
|
181
|
+
if (a.dataset.targetId === id) a.setAttribute('data-active', '');
|
|
182
|
+
else a.removeAttribute('data-active');
|
|
183
|
+
});
|
|
184
|
+
this.dispatchEvent(new CustomEvent('section-change', {
|
|
185
|
+
bubbles: true,
|
|
186
|
+
detail: { activeId: id },
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
connected() {
|
|
191
|
+
super.connected();
|
|
192
|
+
this.setAttribute('role', 'navigation');
|
|
193
|
+
if (!this.hasAttribute('aria-label')) {
|
|
194
|
+
this.setAttribute('aria-label', this.label || 'Table of contents');
|
|
195
|
+
}
|
|
196
|
+
// Defer scan to the next microtask so any sibling content stamped
|
|
197
|
+
// by the same render pass has time to mount before we read its
|
|
198
|
+
// heading structure.
|
|
199
|
+
if (!this.#scheduledScan) {
|
|
200
|
+
this.#scheduledScan = true;
|
|
201
|
+
queueMicrotask(() => {
|
|
202
|
+
this.#scheduledScan = false;
|
|
203
|
+
this.#render();
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
render() {
|
|
209
|
+
// Re-scan when target / headings / offset / label change. The
|
|
210
|
+
// first render after connection is handled by the microtask in
|
|
211
|
+
// connected(); this catches dynamic prop updates.
|
|
212
|
+
if (this.#lastHeadingsKey || this.querySelector('nav[data-stamped]')) {
|
|
213
|
+
this.#render();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
disconnected() {
|
|
218
|
+
super.disconnected();
|
|
219
|
+
this.#observer?.disconnect();
|
|
220
|
+
this.#observer = null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
2
|
+
TOC-UI — Auto-generated table-of-contents navigation.
|
|
3
|
+
═══════════════════════════════════════════════════════════════ */
|
|
4
|
+
|
|
5
|
+
@scope (toc-ui) {
|
|
6
|
+
:where(:scope) {
|
|
7
|
+
/* ── Layout ── */
|
|
8
|
+
--toc-gap-default: var(--a-space-1);
|
|
9
|
+
--toc-indent-default: var(--a-space-3);
|
|
10
|
+
--toc-padding-block-default: var(--a-space-1);
|
|
11
|
+
--toc-padding-inline-default: var(--a-space-2);
|
|
12
|
+
|
|
13
|
+
/* ── Colors ── */
|
|
14
|
+
--toc-fg-default: var(--a-fg-muted);
|
|
15
|
+
--toc-fg-active-default: var(--a-fg);
|
|
16
|
+
--toc-fg-hover-default: var(--a-fg);
|
|
17
|
+
--toc-rule-active-default: var(--a-accent-bg);
|
|
18
|
+
--toc-rule-inactive-default: transparent;
|
|
19
|
+
|
|
20
|
+
/* ── Typography ── */
|
|
21
|
+
--toc-size-default: var(--a-ui-sm);
|
|
22
|
+
--toc-weight-default: var(--a-weight);
|
|
23
|
+
--toc-weight-active-default: var(--a-weight-medium);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* ── Host ── */
|
|
27
|
+
:scope {
|
|
28
|
+
box-sizing: border-box;
|
|
29
|
+
display: block;
|
|
30
|
+
color: var(--toc-fg, var(--toc-fg-default));
|
|
31
|
+
font-size: var(--toc-size, var(--toc-size-default));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* ── Nav scaffold ── */
|
|
35
|
+
nav {
|
|
36
|
+
display: block;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
ul {
|
|
40
|
+
list-style: none;
|
|
41
|
+
margin: 0;
|
|
42
|
+
padding: 0;
|
|
43
|
+
display: flex;
|
|
44
|
+
flex-direction: column;
|
|
45
|
+
gap: var(--toc-gap, var(--toc-gap-default));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
li {
|
|
49
|
+
/* Flush — the link element carries its own block padding so the
|
|
50
|
+
hit-target is the entire row. */
|
|
51
|
+
list-style: none;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* ── Link ── */
|
|
55
|
+
a {
|
|
56
|
+
display: block;
|
|
57
|
+
box-sizing: border-box;
|
|
58
|
+
padding-block: var(--toc-padding-block, var(--toc-padding-block-default));
|
|
59
|
+
padding-inline: var(--toc-padding-inline, var(--toc-padding-inline-default));
|
|
60
|
+
text-decoration: none;
|
|
61
|
+
color: inherit;
|
|
62
|
+
line-height: 1.4;
|
|
63
|
+
border-inline-start: 2px solid var(--toc-rule-inactive, var(--toc-rule-inactive-default));
|
|
64
|
+
transition:
|
|
65
|
+
color var(--a-duration-fast) var(--a-easing),
|
|
66
|
+
border-color var(--a-duration-fast) var(--a-easing);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
a:hover {
|
|
70
|
+
color: var(--toc-fg-hover, var(--toc-fg-hover-default));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
a[data-active] {
|
|
74
|
+
color: var(--toc-fg-active, var(--toc-fg-active-default));
|
|
75
|
+
border-inline-start-color: var(--toc-rule-active, var(--toc-rule-active-default));
|
|
76
|
+
font-weight: var(--toc-weight-active, var(--toc-weight-active-default));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
a:focus-visible {
|
|
80
|
+
outline: none;
|
|
81
|
+
box-shadow: var(--a-focus-ring);
|
|
82
|
+
border-radius: var(--a-radius-sm);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* ── Indentation by depth ── */
|
|
86
|
+
a[data-depth="0"] { padding-inline-start: var(--toc-padding-inline, var(--toc-padding-inline-default)); }
|
|
87
|
+
a[data-depth="1"] { padding-inline-start: calc(var(--toc-padding-inline, var(--toc-padding-inline-default)) + var(--toc-indent, var(--toc-indent-default)) * 1); }
|
|
88
|
+
a[data-depth="2"] { padding-inline-start: calc(var(--toc-padding-inline, var(--toc-padding-inline-default)) + var(--toc-indent, var(--toc-indent-default)) * 2); }
|
|
89
|
+
a[data-depth="3"] { padding-inline-start: calc(var(--toc-padding-inline, var(--toc-padding-inline-default)) + var(--toc-indent, var(--toc-indent-default)) * 3); }
|
|
90
|
+
a[data-depth="4"] { padding-inline-start: calc(var(--toc-padding-inline, var(--toc-padding-inline-default)) + var(--toc-indent, var(--toc-indent-default)) * 4); }
|
|
91
|
+
a[data-depth="5"] { padding-inline-start: calc(var(--toc-padding-inline, var(--toc-padding-inline-default)) + var(--toc-indent, var(--toc-indent-default)) * 5); }
|
|
92
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<toc-ui>` — Auto-generated in-page table of contents. Scans a target container
|
|
3
|
+
for headings (default `h2,h3`), ensures each has an `id` (slugifies
|
|
4
|
+
the text content if missing), and stamps a `<nav>` list of anchor
|
|
5
|
+
links. An `IntersectionObserver` tracks the active heading and
|
|
6
|
+
applies `[data-active]` to the matching link so consumers can style
|
|
7
|
+
the currently-visible section. Smooth-scroll on click is handled by
|
|
8
|
+
the global `scroll-behavior: smooth` set in resets.css.
|
|
9
|
+
|
|
10
|
+
Pair with a sticky container (`position: sticky; top: <offset>;`) in
|
|
11
|
+
an aside / right rail for the classic docs-site outline pattern.
|
|
12
|
+
|
|
13
|
+
*
|
|
14
|
+
* @see https://ui-kit.exe.xyz/site/components/toc
|
|
15
|
+
*
|
|
16
|
+
* Type declarations generated by scripts/build/dts-codegen.mjs from
|
|
17
|
+
* the component's `.a2ui.json` sidecar(s). Edit the source `.yaml`,
|
|
18
|
+
* run `npm run build:components`, then `npm run codegen:dts` to
|
|
19
|
+
* regenerate; or hand-author this file fully if rich event types are
|
|
20
|
+
* needed beyond what the yaml `events:` block can express.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { UIElement } from '../../core/element.js';
|
|
24
|
+
|
|
25
|
+
export interface TableOfContentsSectionChangeEventDetail {
|
|
26
|
+
activeId: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type TableOfContentsSectionChangeEvent = CustomEvent<TableOfContentsSectionChangeEventDetail>;
|
|
30
|
+
|
|
31
|
+
export class UITableOfContents extends UIElement {
|
|
32
|
+
/** CSS selector listing the heading tags to include (e.g. `h2,h3`).
|
|
33
|
+
The selector is matched against the target container's
|
|
34
|
+
descendants. Default `h2,h3` produces the common two-level
|
|
35
|
+
outline.
|
|
36
|
+
*/
|
|
37
|
+
headings: string;
|
|
38
|
+
/** `aria-label` for the nav. Defaults to `Table of contents`.
|
|
39
|
+
*/
|
|
40
|
+
label: string;
|
|
41
|
+
/** Top offset in pixels for active-state detection. The
|
|
42
|
+
IntersectionObserver's `rootMargin` is set to
|
|
43
|
+
`-<offset>px 0px -50% 0px` so the active item becomes the
|
|
44
|
+
heading nearest the top of the viewport BELOW any sticky header
|
|
45
|
+
chrome. Tune to match the height of your sticky topbar (default
|
|
46
|
+
`80` is a reasonable docs-shell value).
|
|
47
|
+
*/
|
|
48
|
+
offset: number;
|
|
49
|
+
/** CSS selector pointing to the container to scan. Empty (default)
|
|
50
|
+
scans the toc-ui's parent element. Set to `#article` /
|
|
51
|
+
`[data-toc-target]` / `main` for a specific scope.
|
|
52
|
+
*/
|
|
53
|
+
target: string;
|
|
54
|
+
|
|
55
|
+
addEventListener<K extends keyof HTMLElementEventMap>(
|
|
56
|
+
type: K,
|
|
57
|
+
listener: (this: UITableOfContents, ev: HTMLElementEventMap[K]) => unknown,
|
|
58
|
+
options?: boolean | AddEventListenerOptions,
|
|
59
|
+
): void;
|
|
60
|
+
addEventListener(type: 'section-change', listener: (ev: TableOfContentsSectionChangeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
61
|
+
}
|