@excom/content-tabs 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/.rush/temp/chunked-rush-logs/content-tabs.apply-exports.chunks.jsonl +1 -0
  2. package/.rush/temp/chunked-rush-logs/content-tabs.build_docs.chunks.jsonl +1 -0
  3. package/.rush/temp/chunked-rush-logs/content-tabs.build_package-metas.chunks.jsonl +1 -0
  4. package/.rush/temp/operation/apply-exports/all.log +1 -0
  5. package/.rush/temp/operation/apply-exports/log-chunks.jsonl +1 -0
  6. package/.rush/temp/operation/apply-exports/state.json +3 -0
  7. package/.rush/temp/operation/build_docs/all.log +1 -0
  8. package/.rush/temp/operation/build_docs/log-chunks.jsonl +1 -0
  9. package/.rush/temp/operation/build_docs/state.json +3 -0
  10. package/.rush/temp/operation/build_package-metas/all.log +1 -0
  11. package/.rush/temp/operation/build_package-metas/log-chunks.jsonl +1 -0
  12. package/.rush/temp/operation/build_package-metas/state.json +3 -0
  13. package/.rush/temp/shrinkwrap-deps.json +3 -0
  14. package/config/rig.json +5 -0
  15. package/content-tabs-body.ts +26 -0
  16. package/content-tabs-header.ts +64 -0
  17. package/content-tabs.ts +174 -0
  18. package/index.css +5 -0
  19. package/index.ts +33 -0
  20. package/package.json +43 -0
  21. package/rush-logs/content-tabs.apply-exports.cache.log +1 -0
  22. package/rush-logs/content-tabs.apply-exports.log +1 -0
  23. package/rush-logs/content-tabs.build_docs.cache.log +1 -0
  24. package/rush-logs/content-tabs.build_docs.log +1 -0
  25. package/rush-logs/content-tabs.build_package-metas.cache.log +1 -0
  26. package/rush-logs/content-tabs.build_package-metas.log +1 -0
  27. package/src/content-tabs.css +137 -0
  28. package/support/custom-elements.json +376 -0
  29. package/support/demos/file-tabs.html +6 -0
  30. package/support/demos/multi.html +6 -0
  31. package/support/demos/named.html +6 -0
  32. package/support/demos/simple.html +8 -0
  33. package/support/dist-docs/content-tabs-body.md +20 -0
  34. package/support/dist-docs/content-tabs-header.md +26 -0
  35. package/support/dist-docs/content-tabs.md +183 -0
  36. package/support/docs/README.md +74 -0
  37. package/support/package-meta.json +252 -0
  38. package/support/tests/content-tabs.test.ts +426 -0
  39. package/support/tests/file-tabs.view.test.ts +26 -0
  40. package/support/tests/multi.view.test.ts +29 -0
  41. package/support/tests/named.view.test.ts +27 -0
  42. package/support/tests/simple.view.test.ts +29 -0
  43. package/tsconfig.json +5 -0
@@ -0,0 +1 @@
1
+ {"kind":"O","text":"Invoking: cd \"$RUSH_PROJECT_FOLDER\" && node ../heft-rig/scripts/apply-exports.mjs \n"}
@@ -0,0 +1 @@
1
+ {"kind":"O","text":"Invoking: node node_modules/@excom/heft-rig/scripts/build-docs.mjs \n"}
@@ -0,0 +1 @@
1
+ {"kind":"O","text":"Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs \n"}
@@ -0,0 +1 @@
1
+ Invoking: cd "$RUSH_PROJECT_FOLDER" && node ../heft-rig/scripts/apply-exports.mjs
@@ -0,0 +1 @@
1
+ {"kind":"O","text":"Invoking: cd \"$RUSH_PROJECT_FOLDER\" && node ../heft-rig/scripts/apply-exports.mjs \n"}
@@ -0,0 +1,3 @@
1
+ {
2
+ "nonCachedDurationMs": 38.81611700000008
3
+ }
@@ -0,0 +1 @@
1
+ Invoking: node node_modules/@excom/heft-rig/scripts/build-docs.mjs
@@ -0,0 +1 @@
1
+ {"kind":"O","text":"Invoking: node node_modules/@excom/heft-rig/scripts/build-docs.mjs \n"}
@@ -0,0 +1,3 @@
1
+ {
2
+ "nonCachedDurationMs": 61.23910599999999
3
+ }
@@ -0,0 +1 @@
1
+ Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs
@@ -0,0 +1 @@
1
+ {"kind":"O","text":"Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs \n"}
@@ -0,0 +1,3 @@
1
+ {
2
+ "nonCachedDurationMs": 111.23345999999992
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "../../packages/content-tabs": "../../packages/content-tabs:9IWZ2jw5bqvMUIZ/3hS/tDuKtEoqJKQ6897NQAr/L48=:"
3
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
3
+ "rigPackageName": "@excom/heft-rig",
4
+ "rigProfile": "default"
5
+ }
@@ -0,0 +1,26 @@
1
+ import { Neutron } from "@excom/neutron";
2
+
3
+ /**
4
+ * Panel paired with a `<content-tabs-header>` — shown when that header is
5
+ * open. Lives inside a `<content-tabs>`.
6
+ *
7
+ * @summary Tab panel — visible when its paired header is open.
8
+ */
9
+ export const ContentTabsBody = Neutron({
10
+ tag: "content-tabs-body",
11
+ props: {
12
+ /**
13
+ * @option
14
+ * @state
15
+ * Visibility, kept in sync with the paired header's `is-open`. Set
16
+ * directly only if this body is not paired with a header.
17
+ */
18
+ isOpen: Boolean,
19
+ /**
20
+ * @option
21
+ * Pairs this body with the `<content-tabs-header>` sharing the same
22
+ * `tab-name`. Unset headers / bodies pair by position instead.
23
+ */
24
+ tabName: String,
25
+ },
26
+ });
@@ -0,0 +1,64 @@
1
+ import { ConstructorType, Neutron, TEvent } from "@excom/neutron";
2
+
3
+ export type ContentTabsHeaderOpenedEvent = TEvent & {
4
+ type: "content-tabs-header-opened";
5
+ detail: void;
6
+ };
7
+
8
+ /**
9
+ * Clickable tab header — lives inside a `<content-tabs>`. Click opens
10
+ * (`single` / `toggle`) or toggles (`multi`) `is-open`, which keeps the
11
+ * paired `<content-tabs-body>` in sync.
12
+ *
13
+ * @summary Tab header — opens its paired body on click.
14
+ *
15
+ * @fires content-tabs-header-opened - Whenever `is-open` is set. The
16
+ * parent `<content-tabs>` listens for this to close sibling headers.
17
+ * @type ContentTabsHeaderOpenedEvent
18
+ */
19
+ export const ContentTabsHeader = Neutron({
20
+ tag: "content-tabs-header",
21
+ props: {
22
+ /**
23
+ * @option
24
+ * Pairs this header with the `<content-tabs-body>` sharing the same
25
+ * `tab-name`. Unset headers / bodies pair by position instead.
26
+ */
27
+ tabName: String,
28
+ /**
29
+ * @option
30
+ * @state
31
+ * Open state. Click sets or toggles this depending on the parent's
32
+ * `tab-type`; set it directly to drive the tab programmatically.
33
+ */
34
+ isOpen: Boolean,
35
+ // private state
36
+ _parentTabs:
37
+ HTMLElement as unknown as ConstructorType<HTMLContentTabsElement>,
38
+ },
39
+ })
40
+ .defineMethods({
41
+ _getParentTabs: (element) => {
42
+ const tabs = element._parentTabs || element.closest("content-tabs");
43
+ return [tabs && { _parentTabs: tabs }, { returns: tabs }];
44
+ },
45
+ _handleClick: (element) => ({
46
+ isOpen: ["toggle", "multi"].includes(
47
+ // @ts-expect-error - TODO: method typing
48
+ element._getParentTabs()?.tabType as string
49
+ )
50
+ ? !element.isOpen
51
+ : true,
52
+ }),
53
+ _syncCorrespondingBody: (element) => {
54
+ // @ts-expect-error - TODO: method typing
55
+ element._getParentTabs()?._queueSyncBody(element);
56
+ },
57
+ })
58
+ .onPropUnset("isOpen", () => [{ _syncCorrespondingBody: [] }])
59
+ .onPropSet("isOpen", () => [
60
+ { _syncCorrespondingBody: [] },
61
+ { emit: ["content-tabs-header-opened"] },
62
+ ])
63
+ .onEvent("click", () => ({ _handleClick: [] }))
64
+ .onDisconnected(() => ({ _parentTabs: null }));
@@ -0,0 +1,174 @@
1
+ import type { ContentTabsBody } from "./content-tabs-body";
2
+ import type {
3
+ ContentTabsHeader,
4
+ ContentTabsHeaderOpenedEvent,
5
+ } from "./content-tabs-header";
6
+ import { ConstructorType, Neutron } from "@excom/neutron";
7
+
8
+ export type { ContentTabsHeaderOpenedEvent };
9
+
10
+ type TContentTabsHeader = typeof ContentTabsHeader.CustomElement;
11
+ type TContentTabsBody = typeof ContentTabsBody.CustomElement;
12
+
13
+ /**
14
+ * Open state of a `<content-tabs>` group. A tab is its header's
15
+ * `tab-name`, or its index among the group's own headers when unnamed.
16
+ * Nested groups are excluded.
17
+ */
18
+ export interface ContentTabsProvision {
19
+ /** The group's `tab-type` (`null` when unset, behaves as `single`). */
20
+ tabType: string | null;
21
+ /** Every open tab, in header order. */
22
+ openTabs: Array<string | number>;
23
+ /** The first open tab; `null` when none is open. */
24
+ activeTab: string | number | null;
25
+ }
26
+
27
+ /** A header's provision id: its `tab-name`, or its index among `headers`. */
28
+ const tabIdOf = (header: TContentTabsHeader, headers: TContentTabsHeader[]) =>
29
+ header.tabName || headers.indexOf(header);
30
+
31
+ /** Read `is-open` even if the header hasn't upgraded yet. */
32
+ const isHeaderOpen = (header: TContentTabsHeader) =>
33
+ header.isOpen ?? header.hasAttribute("is-open");
34
+
35
+ const isSameProvision = (
36
+ a: ContentTabsProvision | null | undefined,
37
+ b: ContentTabsProvision
38
+ ) =>
39
+ !!a &&
40
+ a.tabType === b.tabType &&
41
+ a.activeTab === b.activeTab &&
42
+ a.openTabs.length === b.openTabs.length &&
43
+ a.openTabs.every((tab, i) => tab === b.openTabs[i]);
44
+
45
+ /**
46
+ * Tabs / accordion primitive. Click a `<content-tabs-header>` to show its
47
+ * paired `<content-tabs-body>` — pair by matching `tab-name`, or by
48
+ * position when both are unnamed. Nested `<content-tabs>` groups are
49
+ * isolated from their ancestors.
50
+ *
51
+ * `.provision` holds the open state (`{ tabType, openTabs, activeTab }`)
52
+ * so Quark can read which tab is active with `prop("provision")`.
53
+ *
54
+ * @summary Tabs / accordion — single, multi, or toggle selection.
55
+ *
56
+ * @listens content-tabs-header-opened - Closes sibling headers when
57
+ * `tab-type` is `single` (default) or `toggle`.
58
+ * @type ContentTabsHeaderOpenedEvent
59
+ *
60
+ * @child content-tabs-header - Clickable tab headers.
61
+ * @child content-tabs-body - Panels shown / hidden to match their header.
62
+ */
63
+ export const ContentTabs = Neutron({
64
+ tag: "content-tabs",
65
+ props: {
66
+ /**
67
+ * @option
68
+ * Selection mode. `single` opens one header at a time (radio-like);
69
+ * `multi` allows any number open at once (accordion); `toggle` is
70
+ * like `single`, but re-clicking the open header closes it.
71
+ * @values single | multi | toggle
72
+ * @default single
73
+ */
74
+ tabType: String,
75
+ /**
76
+ * @provision
77
+ * Open state: `{ tabType, openTabs, activeTab }`. A tab is its
78
+ * header's `tab-name`, or its index among this group's own headers
79
+ * when unnamed; `activeTab` is the first open one (`null` when none).
80
+ * Set after mount and after every header change (batched with the
81
+ * body sync). Not reflected as an attribute.
82
+ * @type ContentTabsProvision
83
+ */
84
+ provision: Object as unknown as ConstructorType<ContentTabsProvision>,
85
+
86
+ // private state
87
+ _headersToSync: Array as unknown as ConstructorType<TContentTabsHeader[]>,
88
+ _mutationQueued: Boolean,
89
+ },
90
+ })
91
+ .defineMethods({
92
+ /* Own headers / bodies only. A nested `<content-tabs>` keeps its own
93
+ (nearest-group filter, not `:not(:scope …)`, which some selector
94
+ engines scope wrong). */
95
+ getHeaders: (element) => ({
96
+ returns: [...element.querySelectorAll("content-tabs-header")].filter(
97
+ (header) => header.closest("content-tabs") === element
98
+ ) as TContentTabsHeader[],
99
+ }),
100
+ getBodies: (element) => ({
101
+ returns: [...element.querySelectorAll("content-tabs-body")].filter(
102
+ (body) => body.closest("content-tabs") === element
103
+ ) as TContentTabsBody[],
104
+ }),
105
+ // @ts-expect-error - TODO: method typing
106
+ _syncBodies: ({ _headersToSync, getHeaders, getBodies }) => {
107
+ const allHeaders = getHeaders();
108
+ const allBodies = getBodies();
109
+ [...new Set(_headersToSync || [])].forEach((header) => {
110
+ const body = allBodies.find((body, bodyIndex) => {
111
+ if (header.tabName) {
112
+ return body.tabName === header.tabName;
113
+ }
114
+ return bodyIndex === allHeaders.indexOf(header);
115
+ });
116
+ if (body) {
117
+ body.isOpen = header.isOpen;
118
+ }
119
+ });
120
+ return {
121
+ _headersToSync: [],
122
+ _mutationQueued: false,
123
+ _syncProvision: [],
124
+ };
125
+ },
126
+ /**
127
+ * Rebuild `provision` from every own header (group-wide, once per
128
+ * batch). New object only when something changed, so Quark /
129
+ * `neutron-provision` listeners skip no-op batches.
130
+ */
131
+ // @ts-expect-error - TODO: method typing
132
+ _syncProvision: ({ getHeaders, tabType, provision }) => {
133
+ const headers = getHeaders();
134
+ const openTabs = headers
135
+ .filter(isHeaderOpen)
136
+ .map((header) => tabIdOf(header, headers));
137
+ const next: ContentTabsProvision = {
138
+ tabType: tabType || null,
139
+ openTabs,
140
+ activeTab: openTabs.length ? openTabs[0] : null,
141
+ };
142
+ return isSameProvision(provision, next) ? {} : { provision: next };
143
+ },
144
+ // @ts-expect-error - TODO: method typing
145
+ _queueSyncBodies: ({ _mutationQueued, _syncBodies }) => {
146
+ if (!_mutationQueued) {
147
+ // Heavy DOM queries: cache and batch.
148
+ queueMicrotask(_syncBodies);
149
+ return { _mutationQueued: true };
150
+ }
151
+ },
152
+ _queueSyncBody: ({ _headersToSync }, header) => [
153
+ { _headersToSync: [...(_headersToSync || []), header] },
154
+ { _queueSyncBodies: [] },
155
+ ],
156
+ })
157
+ .onEvent("content-tabs-header-opened", (element, e) => {
158
+ // Close sibling headers
159
+ e.stopPropagation();
160
+ if ([null, "single", "toggle"].includes(element.tabType)) {
161
+ element
162
+ .getHeaders()
163
+ // `e.target` is the header that opened
164
+ .filter((header) => header !== e.target)
165
+ .forEach((header) => (header.isOpen = false));
166
+ }
167
+ })
168
+ // Initial `is-open` headers may upgrade after this element, so the
169
+ // first provision read shares the same microtask queue as a change
170
+ .onConnected(() => ({ _queueSyncBodies: [] }))
171
+ .onDisconnected(
172
+ ({ isMoving }) =>
173
+ !isMoving && { _headersToSync: null, _mutationQueued: false }
174
+ );
package/index.css ADDED
@@ -0,0 +1,5 @@
1
+ @import "./src/content-tabs.css";
2
+
3
+ @layer {
4
+ @mixin module-content-tabs;
5
+ }
package/index.ts ADDED
@@ -0,0 +1,33 @@
1
+ import { ContentTabs } from "./content-tabs";
2
+ import { ContentTabsBody } from "./content-tabs-body";
3
+ import { ContentTabsHeader } from "./content-tabs-header";
4
+
5
+ ContentTabs.define();
6
+ ContentTabsBody.define();
7
+ ContentTabsHeader.define();
8
+
9
+ export { ContentTabs, ContentTabsBody, ContentTabsHeader };
10
+
11
+ type T_HTMLContentTabsElement = typeof ContentTabs.CustomElement;
12
+ type T_HTMLContentTabsBodyElement = typeof ContentTabsBody.CustomElement;
13
+ type T_HTMLContentTabsHeaderElement = typeof ContentTabsHeader.CustomElement;
14
+ declare global {
15
+ interface HTMLContentTabsElement extends T_HTMLContentTabsElement {}
16
+ interface HTMLContentTabsBodyElement extends T_HTMLContentTabsBodyElement {}
17
+ interface HTMLContentTabsHeaderElement extends T_HTMLContentTabsHeaderElement {}
18
+ interface Window {
19
+ HTMLContentTabsElement: HTMLContentTabsElement;
20
+ HTMLContentTabsBodyElement: HTMLContentTabsBodyElement;
21
+ HTMLContentTabsHeaderElement: HTMLContentTabsHeaderElement;
22
+ }
23
+ interface HTMLElementTagNameMap {
24
+ "content-tabs": HTMLContentTabsElement;
25
+ "content-tabs-body": HTMLContentTabsBodyElement;
26
+ "content-tabs-header": HTMLContentTabsHeaderElement;
27
+ }
28
+ }
29
+ export type {
30
+ HTMLContentTabsBodyElement,
31
+ HTMLContentTabsElement,
32
+ HTMLContentTabsHeaderElement,
33
+ };
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@excom/content-tabs",
3
+ "version": "0.1.0",
4
+ "description": "<content-tabs> custom element",
5
+ "license": "MIT",
6
+ "engines": {
7
+ "node": ">=24.13.0"
8
+ },
9
+ "type": "module",
10
+ "dependencies": {
11
+ "@excom/valence": "^0.1.0",
12
+ "@excom/neutron": "^0.1.0"
13
+ },
14
+ "peerDependencies": {},
15
+ "devDependencies": {
16
+ "@excom/heft-rig": "^0.1.0"
17
+ },
18
+ "repository": {
19
+ "url": "excom-dev/nucleus",
20
+ "directory": "packages/content-tabs"
21
+ },
22
+ "homepage": "https://github.com/excom-dev/nucleus/tree/main/packages/content-tabs/support/docs/README.md",
23
+ "bugs": "https://github.com/excom-dev/nucleus/issues",
24
+ "keywords": [
25
+ "content-tabs",
26
+ "neutron",
27
+ "custom-elements"
28
+ ],
29
+ "excom": {
30
+ "packageType": "kit-element"
31
+ },
32
+ "scripts": {
33
+ "build": "node node_modules/@excom/heft-rig/scripts/vite-build.mjs",
34
+ "build:watch": "node node_modules/@excom/heft-rig/scripts/vite-build-watch.mjs",
35
+ "format": "node node_modules/@excom/heft-rig/scripts/format.mjs",
36
+ "test": "node node_modules/@excom/heft-rig/scripts/vitest.mjs",
37
+ "coverage": "node node_modules/@excom/heft-rig/scripts/coverage.mjs",
38
+ "dev": "node node_modules/@excom/heft-rig/scripts/vite-dev.mjs",
39
+ "preview": "node node_modules/@excom/heft-rig/scripts/vite-preview.mjs",
40
+ "build:package-metas": "node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs",
41
+ "build:docs": "node node_modules/@excom/heft-rig/scripts/build-docs.mjs"
42
+ }
43
+ }
@@ -0,0 +1 @@
1
+ Caching has been disabled for this project's "apply-exports" command.
@@ -0,0 +1 @@
1
+ Invoking: cd "$RUSH_PROJECT_FOLDER" && node ../heft-rig/scripts/apply-exports.mjs
@@ -0,0 +1 @@
1
+ This project does not define the caching behavior of the "build:docs" command, so caching has been disabled.
@@ -0,0 +1 @@
1
+ Invoking: node node_modules/@excom/heft-rig/scripts/build-docs.mjs
@@ -0,0 +1 @@
1
+ This project does not define the caching behavior of the "build:package-metas" command, so caching has been disabled.
@@ -0,0 +1 @@
1
+ Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs
@@ -0,0 +1,137 @@
1
+ @import "@excom/valence/src/util.default.css";
2
+
3
+ /**
4
+ * Styles for `<content-tabs>`, `<content-tabs-header>`, and
5
+ * `<content-tabs-body>`. Primary layout lives on the `content-tabs`
6
+ * mixin; headers / bodies are styled via nested selectors.
7
+ * @element content-tabs
8
+ */
9
+
10
+ /* Aliases */
11
+ @custom-selector :--content-tabs content-tabs, .tag-content-tabs;
12
+ @custom-selector :--content-tabs-header content-tabs-header, .tag-content-tabs-header;
13
+ @custom-selector :--content-tabs-body content-tabs-body, .tag-content-tabs-body;
14
+ @custom-selector :--content-tab--is-open [is-open], [aria-selected="true"];
15
+
16
+ @define-mixin module-content-tabs {
17
+ :root {
18
+ /**
19
+ * Border color for `.file-tabs` headers / bodies.
20
+ * @cssproperty
21
+ * @syntax <color>
22
+ */
23
+ --content-tabs-border-color: var(--v-muted-border-color, currentColor);
24
+ /**
25
+ * Border width for `.file-tabs` headers / bodies.
26
+ * @cssproperty
27
+ * @syntax <length>
28
+ */
29
+ --content-tabs-border-width: var(--v-border-width, 2px);
30
+ /**
31
+ * Text color for the active header in `.underline`.
32
+ * @cssproperty
33
+ * @syntax <color>
34
+ */
35
+ --content-tabs-color-active: var(--v-color-primary, inherit);
36
+ /**
37
+ * Padding for each header.
38
+ * @cssproperty
39
+ * @syntax <length>{1,4}
40
+ */
41
+ --content-tabs-header-padding: 10px 15px;
42
+ /**
43
+ * Padding for each body in `.file-tabs`.
44
+ * @cssproperty
45
+ * @syntax <length>{1,4}
46
+ */
47
+ --content-tabs-body-padding: var(--v-spacing, 15px);
48
+ /**
49
+ * Height of the `.file-tabs` active-tab accent bar.
50
+ * @cssproperty
51
+ * @syntax <length>
52
+ */
53
+ --content-tabs-active-indicator-height: 2px;
54
+ }
55
+
56
+ :--content-tabs {
57
+ :--content-tabs-header {
58
+ cursor: pointer;
59
+ position: relative;
60
+ user-select: none;
61
+ }
62
+ :--content-tabs-body {
63
+ display: none;
64
+ }
65
+ :--content-tabs-body:--content-tab--is-open {
66
+ display: block;
67
+ }
68
+
69
+ /**
70
+ * File-explorer look — headers render as bordered folder tabs joined
71
+ * to the active body.
72
+ * @cssclass
73
+ */
74
+ &.file-tabs {
75
+ > :--content-tabs-header {
76
+ /* compensate for lack of flexbox */
77
+ margin-right: -5px;
78
+ }
79
+ :--content-tabs-header {
80
+ border: var(--content-tabs-border-width) solid
81
+ var(--content-tabs-border-color);
82
+ border-bottom: 0;
83
+ padding: var(--content-tabs-header-padding);
84
+ display: inline-block;
85
+
86
+ &:not(:last-of-type) {
87
+ border-right: 0;
88
+ }
89
+ :--content-tabs-body {
90
+ margin-top: -1px;
91
+ }
92
+ }
93
+ &:not(:has(:--content-tabs-header:--content-tab--is-open))
94
+ :--content-tabs-header:first-of-type,
95
+ :--content-tabs-header:--content-tab--is-open {
96
+ @mixin util-nested-card;
97
+ color: var(--v-primary);
98
+
99
+ &::after {
100
+ content: "";
101
+ position: absolute;
102
+ bottom: calc(var(--content-tabs-border-width) * -1);
103
+ left: 0;
104
+ width: 100%;
105
+ height: var(--content-tabs-active-indicator-height);
106
+ @mixin util-nested-card;
107
+ }
108
+ }
109
+ &:not(:has(:--content-tabs-body:--content-tab--is-open))
110
+ :--content-tabs-body:first-of-type,
111
+ :--content-tabs-body:--content-tab--is-open {
112
+ border: var(--content-tabs-border-width) solid
113
+ var(--content-tabs-border-color);
114
+ padding: var(--content-tabs-body-padding);
115
+ @mixin util-nested-card;
116
+ }
117
+ }
118
+ /**
119
+ * Paper / underline look — the active header gets a bottom accent
120
+ * border instead of a bordered card.
121
+ * @cssclass
122
+ */
123
+ &.underline {
124
+ :--content-tabs-header {
125
+ border-bottom: 2px solid transparent;
126
+ padding: var(--content-tabs-header-padding);
127
+ &:--content-tab--is-open {
128
+ color: var(--v-primary);
129
+ border-color: var(--v-primary-border);
130
+ }
131
+ }
132
+ :--content-tabs-body:--content-tab--is-open {
133
+ margin-top: 8px;
134
+ }
135
+ }
136
+ }
137
+ }