@adia-ai/web-components 0.6.36 → 0.6.38
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 +48 -1
- package/components/accordion/accordion-item.a2ui.json +3 -0
- package/components/accordion/accordion-item.yaml +5 -0
- package/components/action-list/action-item.a2ui.json +5 -1
- package/components/action-list/action-item.yaml +7 -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/card/card.a2ui.json +17 -1
- package/components/card/card.yaml +24 -1
- 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/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 +2 -0
- 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.d.ts +2 -0
- package/components/datetime-picker/datetime-picker.yaml +14 -0
- package/components/empty-state/empty-state.a2ui.json +9 -0
- package/components/empty-state/empty-state.class.js +2 -0
- package/components/empty-state/empty-state.yaml +15 -0
- package/components/feed/feed-item.a2ui.json +5 -0
- package/components/feed/feed-item.yaml +10 -0
- package/components/feed/feed.class.js +13 -5
- package/components/feed/feed.css +14 -0
- package/components/field/field.a2ui.json +6 -0
- package/components/field/field.yaml +10 -0
- package/components/index.js +11 -0
- package/components/inline-edit/inline-edit.a2ui.json +159 -0
- package/components/inline-edit/inline-edit.class.js +184 -0
- package/components/inline-edit/inline-edit.css +62 -0
- package/components/inline-edit/inline-edit.d.ts +52 -0
- package/components/inline-edit/inline-edit.js +12 -0
- package/components/inline-edit/inline-edit.yaml +125 -0
- package/components/integration-card/integration-card.class.js +9 -0
- package/components/integration-card/integration-card.test.js +4 -3
- package/components/list/list-item.a2ui.json +8 -1
- package/components/list/list-item.yaml +12 -0
- package/components/list/list.css +36 -6
- package/components/mark/mark.a2ui.json +109 -0
- package/components/mark/mark.class.js +22 -0
- package/components/mark/mark.css +39 -0
- package/components/mark/mark.d.ts +27 -0
- package/components/mark/mark.js +12 -0
- package/components/mark/mark.yaml +87 -0
- package/components/modal/modal.a2ui.json +9 -0
- package/components/modal/modal.yaml +14 -0
- package/components/nav-group/nav-group.a2ui.json +3 -0
- package/components/nav-group/nav-group.css +7 -1
- package/components/nav-group/nav-group.yaml +5 -0
- package/components/nav-item/nav-item.a2ui.json +3 -0
- package/components/nav-item/nav-item.yaml +5 -0
- 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/segmented/segmented.class.js +15 -3
- package/components/select/select.a2ui.json +3 -0
- package/components/select/select.class.js +4 -0
- package/components/select/select.yaml +5 -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 +22 -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 +38 -6
- package/components/stat/stat.css +18 -14
- package/components/stepper/stepper-item.a2ui.json +3 -0
- package/components/stepper/stepper-item.yaml +5 -0
- package/components/table/table.class.js +29 -6
- package/components/table/table.css +31 -4
- package/components/table-toolbar/table-toolbar.class.js +3 -1
- package/components/tag/tag.a2ui.json +3 -2
- package/components/tag/tag.css +35 -11
- package/components/tag/tag.d.ts +14 -0
- package/components/tag/tag.test.js +35 -11
- package/components/tag/tag.yaml +13 -7
- package/components/timeline/timeline-item.a2ui.json +8 -1
- package/components/timeline/timeline-item.yaml +12 -0
- 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/tree/tree-item.a2ui.json +5 -1
- package/components/tree/tree-item.yaml +7 -0
- package/components/tree/tree.a2ui.json +3 -0
- package/components/tree/tree.yaml +5 -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/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +100 -89
- package/package.json +1 -1
- package/styles/colors/semantics.css +11 -2
- package/styles/components.css +11 -0
- package/styles/resets.css +10 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<toc-ui>` — auto-registers the tag on import.
|
|
3
|
+
*
|
|
4
|
+
* For non-side-effect class import (test isolation, tag override), use
|
|
5
|
+
* the `class` subpath:
|
|
6
|
+
*
|
|
7
|
+
* import { UITableOfContents } from '@adia-ai/web-components/components/toc/class';
|
|
8
|
+
*
|
|
9
|
+
* @see ../../USAGE.md#registration--auto-vs-explicit
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { defineIfFree } from '../../core/register.js';
|
|
13
|
+
import { UITableOfContents } from './toc.class.js';
|
|
14
|
+
|
|
15
|
+
defineIfFree('toc-ui', UITableOfContents);
|
|
16
|
+
|
|
17
|
+
export { UITableOfContents };
|