@primitiv-ui/web 0.0.0-stage → 0.1.42

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.
@@ -0,0 +1 @@
1
+ function g(i,t,e){return`${i}-${t}-${e}`}function K({orientation:i,dir:t,homeEnd:e=!1,activate:o=!1}){let r={};if(i==="horizontal"||i==="both"){let l=t==="rtl"?"ArrowLeft":"ArrowRight",d=t==="rtl"?"ArrowRight":"ArrowLeft";r[l]="next",r[d]="prev"}return(i==="vertical"||i==="both")&&(r.ArrowDown="next",r.ArrowUp="prev"),e&&(r.Home="first",r.End="last"),o&&(r.Enter="activate",r[" "]="activate"),r}function C({action:i,navigable:t,currentKey:e}){let o=t.length;if(o===0)return null;switch(i){case"activate":return{target:e};case"first":return{target:t[0]};case"last":return{target:t[o-1]};case void 0:return null;case"next":case"prev":{let r=t.indexOf(e);if(r===-1)return null;let l=i==="next"?1:o-1;return{target:t[(r+l)%o]}}}}function x(i){return i.active??i.values[0]}function p(i,t){return i.disabled.has(t)||i.active===t?null:t}function A(i,t,e){let o=K({orientation:i.orientation,dir:i.dir,homeEnd:!0,activate:!0})[e],r=C({action:o,navigable:i.values,currentKey:t});if(!r)return null;let l=r.target,d=i.activationMode==="automatic"||o==="activate";return{focus:l,activate:d?p(i,l):null}}var v=class{#r;#i;#t=new Map;#e=null;constructor(t,e){this.#r=t,this.#i=e}registerTrigger(t,e,o={}){this.#t.set(e,{value:t,disabled:o.disabled??!1})}setTriggers(t){this.#t.clear();for(let{value:e,element:o,disabled:r}of t)this.registerTrigger(e,o,{disabled:r})}unregisterTrigger(t){this.#t.delete(t)}connect(){if(this.#e)return;this.#e=new AbortController;let{signal:t}=this.#e;this.#r.addEventListener("click",e=>this.#l(e),{signal:t}),this.#r.addEventListener("keydown",e=>this.#s(e),{signal:t})}disconnect(){this.#e?.abort(),this.#e=null}#o(){return[...this.#t].sort(([t],[e])=>t.compareDocumentPosition(e)&Node.DOCUMENT_POSITION_FOLLOWING?-1:1)}#a(){let t=new Set;for(let e of this.#t.values())e.disabled&&t.add(e.value);return t}#l(t){let e=this.#n(t.target);if(!e)return;let{active:o}=this.#i.state(),r=p({disabled:this.#a(),active:o},e.value);if(r!==null){let l=this.#o().map(([,d])=>d.value);this.#i.activate(r,l.indexOf(r))}}#s(t){let e=this.#n(t.target);if(!e)return;let{active:o,orientation:r,dir:l,activationMode:d}=this.#i.state(),b=this.#o(),a=b.map(([,c])=>c.value),n=A({values:a,disabled:this.#a(),active:o,orientation:r,dir:l,activationMode:d},e.value,t.key);n&&(t.preventDefault(),n.activate!==null&&this.#i.activate(n.activate,a.indexOf(n.activate)),b[a.indexOf(n.focus)][0].focus())}#n(t){for(let[e,o]of this.#t)if(e.contains(t))return o}};var y=globalThis.HTMLElement??class{},E="primitiv-tabs",H="primitiv-tabs-list",M="primitiv-tabs-trigger",k="primitiv-tabs-content",S=0,L=new WeakSet;function I(i){let t=i.getAttribute("value");return i.childElementCount===1&&t!==null?{tab:i.firstElementChild,value:t}:(L.has(i)||(L.add(i),console.error(`<${M}> needs a value and exactly one element child (a <button>, an <a href> or a router link) to decorate.`,i)),null)}function s(i,t,e){i.getAttribute(t)!==e&&(e===null?i.removeAttribute(t):i.setAttribute(t,e))}var f=class extends y{#r;#i;#t=!1;#e=new v(this,this.#a());#o=new MutationObserver(()=>this.#c());connectedCallback(){this.#e.connect(),this.#o.observe(this,{subtree:!0,childList:!0,attributes:!0,attributeFilter:["value","disabled","label","orientation"]}),this.#c()}disconnectedCallback(){this.#e.disconnect(),this.#o.disconnect()}get value(){return this.getAttribute("value")??void 0}set value(t){this.setAttribute("value",t)}#a(){return{state:()=>({active:this.value,orientation:this.#s(),dir:this.matches(":dir(rtl)")?"rtl":"ltr",activationMode:this.#l()}),activate:(t,e)=>{let o=new CustomEvent("primitiv-tabs-change",{bubbles:!0,cancelable:!0,detail:{value:t,index:e}});this.dispatchEvent(o)&&(this.value=t)}}}#l(){let t=this.getAttribute("activation-mode");return t==="manual"?t:"automatic"}#s(){return this.getAttribute("orientation")==="vertical"?"vertical":"horizontal"}#n(t){return[...this.querySelectorAll(t)].filter(e=>e.closest(E)===this)}#d(){return this.id||(this.#r??=`${E}-${++S}`)}#c(){let t=this.#s(),e=this.value,o=this.#d();s(this,"data-orientation",t);for(let a of this.#n(H)){s(a,"role","tablist"),s(a,"aria-orientation",t),s(a,"data-orientation",t);let n=a.getAttribute("label");n!==null&&s(a,"aria-label",n)}let r=new Map;for(let a of this.#n(k)){let n=a.getAttribute("value");n!==null&&r.set(n,a)}let l=this.#n(M).flatMap(a=>{let n=I(a);return n?[{trigger:a,...n}]:[]}),d=x({values:l.map(a=>a.value),active:e});this.#e.setTriggers(l.map(({trigger:a,tab:n,value:c})=>({value:c,element:n,disabled:a.hasAttribute("disabled")})));for(let{trigger:a,tab:n,value:c}of l){let u=r.get(c),w=c===e,O=a.hasAttribute("disabled");n.id||(n.id=g(o,"trigger",c)),u&&!u.id&&(u.id=g(o,"panel",c)),s(n,"role","tab"),s(n,"aria-selected",String(w)),s(n,"aria-controls",u?u.id:null),s(n,"tabindex",c===d?"0":"-1"),s(n,"aria-disabled",O?"true":null),s(n,"data-disabled",O?"":null),s(n,"data-state",w?"active":"inactive"),s(n,"data-orientation",t),u&&s(u,"aria-labelledby",n.id)}for(let[a,n]of r){let c=a===e;s(n,"role","tabpanel"),s(n,"tabindex","0"),s(n,"hidden",c?null:""),s(n,"data-state",c?"active":"inactive"),s(n,"data-orientation",t)}let b=r.get(e);this.#t&&e!==this.#i&&b&&b.dispatchEvent(new CustomEvent("primitiv-tabs-show",{bubbles:!0})),this.#i=e,this.#t=!0}},h=class extends y{},m=class extends y{},T=class extends y{};function R(i=globalThis.customElements){if(!i)return;let t=[[E,f],[H,h],[M,m],[k,T]];for(let[e,o]of t)i.get(e)||i.define(e,o)}R();export{T as PrimitivTabsContentElement,f as PrimitivTabsElement,h as PrimitivTabsListElement,m as PrimitivTabsTriggerElement,v as TabsController,R as defineTabs};
@@ -0,0 +1,57 @@
1
+ import { type TabsActivationMode, type TabsOrientation, type TabsReadingDirection } from "@primitiv-ui/core";
2
+ /**
3
+ * What a {@link TabsController} asks of whoever hosts it. The controller owns
4
+ * behaviour and never owns state (RFC 0033 D28): the custom elements keep state
5
+ * in reflected attributes, the React adapter keeps it in React, and each answers
6
+ * these two calls from wherever it keeps it.
7
+ */
8
+ export type TabsControllerHost = {
9
+ /** The state the controller cannot see for itself. */
10
+ state(): {
11
+ active: string | undefined;
12
+ orientation: TabsOrientation;
13
+ dir: TabsReadingDirection;
14
+ activationMode: TabsActivationMode;
15
+ };
16
+ /** Apply an activation the user asked for. `index` is the tab's DOM position. */
17
+ activate(value: string, index: number): void;
18
+ };
19
+ /**
20
+ * The DOM behaviour of a tab set: listening, focus, and the ordered collection
21
+ * of triggers. It attaches to whatever elements register with it, so the same
22
+ * code runs under the custom elements and under the React adapter, which renders
23
+ * its own markup (RFC 0033 §3.4).
24
+ */
25
+ export declare class TabsController {
26
+ #private;
27
+ constructor(root: HTMLElement, host: TabsControllerHost);
28
+ /**
29
+ * Register `element` as the trigger for `value`. A disabled trigger is still
30
+ * part of the collection — arrow keys land on it — but is never activated.
31
+ * Registration order does not matter: the collection follows DOM order, so
32
+ * parts that arrive in any order (a fragment swap, a framework render) line up.
33
+ * Registering an element again updates it.
34
+ */
35
+ registerTrigger(value: string, element: HTMLElement, options?: {
36
+ disabled?: boolean;
37
+ }): void;
38
+ /**
39
+ * Replace the whole collection in one call. A host that re-reads its parts on
40
+ * every change (the custom elements, the React adapter) hands over what it
41
+ * has now; a trigger missing from the list is simply no longer registered.
42
+ */
43
+ setTriggers(triggers: ReadonlyArray<{
44
+ value: string;
45
+ element: HTMLElement;
46
+ disabled?: boolean;
47
+ }>): void;
48
+ /** Remove `element` from the collection. */
49
+ unregisterTrigger(element: HTMLElement): void;
50
+ /**
51
+ * Start listening on the root. Connecting again while connected does nothing,
52
+ * so a host whose lifecycle runs twice never stacks a second set of handlers.
53
+ */
54
+ connect(): void;
55
+ /** Stop listening. The collection is kept, so a later `connect` resumes. */
56
+ disconnect(): void;
57
+ }
@@ -0,0 +1,106 @@
1
+ import { tabsClick, tabsKeydown, } from "@primitiv-ui/core";
2
+ /**
3
+ * The DOM behaviour of a tab set: listening, focus, and the ordered collection
4
+ * of triggers. It attaches to whatever elements register with it, so the same
5
+ * code runs under the custom elements and under the React adapter, which renders
6
+ * its own markup (RFC 0033 §3.4).
7
+ */
8
+ export class TabsController {
9
+ #root;
10
+ #host;
11
+ #triggers = new Map();
12
+ #connection = null;
13
+ constructor(root, host) {
14
+ this.#root = root;
15
+ this.#host = host;
16
+ }
17
+ /**
18
+ * Register `element` as the trigger for `value`. A disabled trigger is still
19
+ * part of the collection — arrow keys land on it — but is never activated.
20
+ * Registration order does not matter: the collection follows DOM order, so
21
+ * parts that arrive in any order (a fragment swap, a framework render) line up.
22
+ * Registering an element again updates it.
23
+ */
24
+ registerTrigger(value, element, options = {}) {
25
+ this.#triggers.set(element, { value, disabled: options.disabled ?? false });
26
+ }
27
+ /**
28
+ * Replace the whole collection in one call. A host that re-reads its parts on
29
+ * every change (the custom elements, the React adapter) hands over what it
30
+ * has now; a trigger missing from the list is simply no longer registered.
31
+ */
32
+ setTriggers(triggers) {
33
+ this.#triggers.clear();
34
+ for (const { value, element, disabled } of triggers) {
35
+ this.registerTrigger(value, element, { disabled });
36
+ }
37
+ }
38
+ /** Remove `element` from the collection. */
39
+ unregisterTrigger(element) {
40
+ this.#triggers.delete(element);
41
+ }
42
+ /**
43
+ * Start listening on the root. Connecting again while connected does nothing,
44
+ * so a host whose lifecycle runs twice never stacks a second set of handlers.
45
+ */
46
+ connect() {
47
+ if (this.#connection)
48
+ return;
49
+ this.#connection = new AbortController();
50
+ const { signal } = this.#connection;
51
+ this.#root.addEventListener("click", (event) => this.#onClick(event), { signal });
52
+ this.#root.addEventListener("keydown", (event) => this.#onKeydown(event), { signal });
53
+ }
54
+ /** Stop listening. The collection is kept, so a later `connect` resumes. */
55
+ disconnect() {
56
+ this.#connection?.abort();
57
+ this.#connection = null;
58
+ }
59
+ /** Every registered trigger, in DOM order. */
60
+ #ordered() {
61
+ return [...this.#triggers].sort(([a], [b]) => a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1);
62
+ }
63
+ #disabled() {
64
+ const disabled = new Set();
65
+ for (const trigger of this.#triggers.values()) {
66
+ if (trigger.disabled)
67
+ disabled.add(trigger.value);
68
+ }
69
+ return disabled;
70
+ }
71
+ #onClick(event) {
72
+ const trigger = this.#triggerFor(event.target);
73
+ if (!trigger)
74
+ return;
75
+ const { active } = this.#host.state();
76
+ const target = tabsClick({ disabled: this.#disabled(), active }, trigger.value);
77
+ if (target !== null) {
78
+ const values = this.#ordered().map(([, t]) => t.value);
79
+ this.#host.activate(target, values.indexOf(target));
80
+ }
81
+ }
82
+ #onKeydown(event) {
83
+ const trigger = this.#triggerFor(event.target);
84
+ if (!trigger)
85
+ return;
86
+ const { active, orientation, dir, activationMode } = this.#host.state();
87
+ const ordered = this.#ordered();
88
+ const values = ordered.map(([, t]) => t.value);
89
+ const outcome = tabsKeydown({ values, disabled: this.#disabled(), active, orientation, dir, activationMode }, trigger.value, event.key);
90
+ if (!outcome)
91
+ return;
92
+ event.preventDefault();
93
+ if (outcome.activate !== null) {
94
+ this.#host.activate(outcome.activate, values.indexOf(outcome.activate));
95
+ }
96
+ // The focus target is always one of `values`, so its index is always valid.
97
+ ordered[values.indexOf(outcome.focus)][0].focus();
98
+ }
99
+ #triggerFor(target) {
100
+ for (const [element, trigger] of this.#triggers) {
101
+ if (element.contains(target))
102
+ return trigger;
103
+ }
104
+ return undefined;
105
+ }
106
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * The base class every element extends. It falls back to an empty class where
3
+ * the DOM does not exist, so a server renderer (Next.js, Nuxt, Angular SSR,
4
+ * Astro) can import this module without `HTMLElement` throwing at load time
5
+ * (RFC 0033 D21).
6
+ */
7
+ declare const ElementBase: typeof HTMLElement;
8
+ /**
9
+ * `<primitiv-tabs>` — the state owner of a WAI-ARIA tab set.
10
+ *
11
+ * The root finds its own parts, scoped with `closest()` so a tab set nested in a
12
+ * panel keeps its own, and reconciles their ARIA from its reflected state. It
13
+ * never caches the parts, so markup that arrives later is picked up on the next
14
+ * reconcile. One `MutationObserver` is the only trigger after connect: it sees
15
+ * parts arriving and leaving, and every attribute the root reads, including the
16
+ * root's own `value` and `orientation`, and it already delivers a task's changes
17
+ * as one callback, so no further batching is needed. `activation-mode` is read
18
+ * live at each key press, so a change to it needs no reconcile.
19
+ *
20
+ * @customElement primitiv-tabs
21
+ * @summary The state owner of a WAI-ARIA tab set.
22
+ * @attr {string} value - The active tab's value. Reflected: it moves as the user switches tabs, so a server re-render or a history snapshot sees the live selection.
23
+ * @attr {"horizontal" | "vertical"} orientation - Layout axis, which picks the arrow-key pair. Defaults to `horizontal`.
24
+ * @attr {"automatic" | "manual"} activation-mode - `automatic` activates a tab as focus arrives; `manual` waits for Enter or Space. Defaults to `automatic`.
25
+ * @fires {CustomEvent<{ value: string; index: number }>} primitiv-tabs-change - Bubbles, cancelable, fired before a user-driven change is applied. Call `preventDefault()` to veto it.
26
+ */
27
+ export declare class PrimitivTabsElement extends ElementBase {
28
+ #private;
29
+ connectedCallback(): void;
30
+ /**
31
+ * Leaving the document stops all listening. A move (an `hx-preserve`, a
32
+ * framework reparenting) is a disconnect followed by a connect, so handlers
33
+ * never stack.
34
+ */
35
+ disconnectedCallback(): void;
36
+ /** The active tab's value, reflected as the `value` attribute. */
37
+ get value(): string | undefined;
38
+ set value(next: string);
39
+ }
40
+ /**
41
+ * `<primitiv-tabs-list>` — the tablist. Structural: the root writes its ARIA.
42
+ *
43
+ * @customElement primitiv-tabs-list
44
+ * @attr {string} label - The tablist's accessible name. Give this or `aria-labelledby`.
45
+ */
46
+ export declare class PrimitivTabsListElement extends ElementBase {
47
+ }
48
+ /**
49
+ * `<primitiv-tabs-trigger>` — an interactive part. It decorates exactly one
50
+ * child the consumer writes (a `<button>`, an `<a href>`, a router link), which
51
+ * receives the tab role, so the rendered DOM matches React's `<button role="tab">`
52
+ * (RFC 0033 D24).
53
+ *
54
+ * @customElement primitiv-tabs-trigger
55
+ * @attr {string} value - The tab's value, pairing it with the panel of the same value.
56
+ * @attr {boolean} disabled - Marks the tab disabled: arrow keys still land on it, but it never activates.
57
+ */
58
+ export declare class PrimitivTabsTriggerElement extends ElementBase {
59
+ }
60
+ /**
61
+ * `<primitiv-tabs-content>` — a tab panel. Structural: the root writes its ARIA.
62
+ *
63
+ * @customElement primitiv-tabs-content
64
+ * @attr {string} value - The panel's value, pairing it with the tab of the same value.
65
+ * @fires {CustomEvent} primitiv-tabs-show - Bubbles from the panel as it becomes active (not on first render), so a lazy panel can fetch its content.
66
+ */
67
+ export declare class PrimitivTabsContentElement extends ElementBase {
68
+ }
69
+ /** The two calls `defineTabs` makes on a custom-element registry. */
70
+ export type ElementRegistry = Pick<CustomElementRegistry, "get" | "define">;
71
+ /**
72
+ * Define the four Tabs elements on `registry` (the global `customElements` by
73
+ * default, so a scoped registry can be passed when browsers support one). Safe
74
+ * to call more than once, and a no-op where there is no registry (a server
75
+ * render).
76
+ */
77
+ export declare function defineTabs(registry?: ElementRegistry | undefined): void;
78
+ export {};
@@ -0,0 +1,265 @@
1
+ import { deriveId, tabsTabStop, } from "@primitiv-ui/core";
2
+ import { TabsController } from "./TabsController.js";
3
+ /**
4
+ * The base class every element extends. It falls back to an empty class where
5
+ * the DOM does not exist, so a server renderer (Next.js, Nuxt, Angular SSR,
6
+ * Astro) can import this module without `HTMLElement` throwing at load time
7
+ * (RFC 0033 D21).
8
+ */
9
+ const ElementBase = globalThis.HTMLElement ?? class {
10
+ };
11
+ // Stryker disable StringLiteral: static — evaluated once at import, so runtime mutant switching never reaches them; the names are pinned by the defineTabs registry test instead.
12
+ const ROOT = "primitiv-tabs";
13
+ const LIST = "primitiv-tabs-list";
14
+ const TRIGGER = "primitiv-tabs-trigger";
15
+ const CONTENT = "primitiv-tabs-content";
16
+ // Stryker restore StringLiteral
17
+ let generatedIds = 0;
18
+ /** Triggers already reported as misused, so each is reported once, not per reconcile. */
19
+ const reported = new WeakSet();
20
+ /**
21
+ * An interactive part decorates exactly one element child and needs a `value`.
22
+ * Anything else is reported once and left undecorated rather than thrown: a
23
+ * server-rendered page has no error boundary to catch a throw (RFC 0033 D24).
24
+ */
25
+ function decoratable(trigger) {
26
+ const value = trigger.getAttribute("value");
27
+ if (trigger.childElementCount === 1 && value !== null) {
28
+ return { tab: trigger.firstElementChild, value };
29
+ }
30
+ if (!reported.has(trigger)) {
31
+ reported.add(trigger);
32
+ console.error(`<${TRIGGER}> needs a value and exactly one element child (a <button>, an <a href> or a router link) to decorate.`, trigger);
33
+ }
34
+ return null;
35
+ }
36
+ /**
37
+ * Write an attribute only when it would change: `null` removes it. The elements
38
+ * write only the attributes in their contract (RFC 0033 §5.3), and comparing
39
+ * first means a value a framework or the server already rendered is never
40
+ * rewritten (D20).
41
+ */
42
+ function write(element, name, value) {
43
+ if (element.getAttribute(name) === value)
44
+ return;
45
+ if (value === null)
46
+ element.removeAttribute(name);
47
+ else
48
+ element.setAttribute(name, value);
49
+ }
50
+ /**
51
+ * `<primitiv-tabs>` — the state owner of a WAI-ARIA tab set.
52
+ *
53
+ * The root finds its own parts, scoped with `closest()` so a tab set nested in a
54
+ * panel keeps its own, and reconciles their ARIA from its reflected state. It
55
+ * never caches the parts, so markup that arrives later is picked up on the next
56
+ * reconcile. One `MutationObserver` is the only trigger after connect: it sees
57
+ * parts arriving and leaving, and every attribute the root reads, including the
58
+ * root's own `value` and `orientation`, and it already delivers a task's changes
59
+ * as one callback, so no further batching is needed. `activation-mode` is read
60
+ * live at each key press, so a change to it needs no reconcile.
61
+ *
62
+ * @customElement primitiv-tabs
63
+ * @summary The state owner of a WAI-ARIA tab set.
64
+ * @attr {string} value - The active tab's value. Reflected: it moves as the user switches tabs, so a server re-render or a history snapshot sees the live selection.
65
+ * @attr {"horizontal" | "vertical"} orientation - Layout axis, which picks the arrow-key pair. Defaults to `horizontal`.
66
+ * @attr {"automatic" | "manual"} activation-mode - `automatic` activates a tab as focus arrives; `manual` waits for Enter or Space. Defaults to `automatic`.
67
+ * @fires {CustomEvent<{ value: string; index: number }>} primitiv-tabs-change - Bubbles, cancelable, fired before a user-driven change is applied. Call `preventDefault()` to veto it.
68
+ */
69
+ export class PrimitivTabsElement extends ElementBase {
70
+ #generatedId;
71
+ /** The value of the panel last shown, so a change fires primitiv-tabs-show. */
72
+ #shown;
73
+ #rendered = false;
74
+ #controller = new TabsController(this, this.#host());
75
+ /**
76
+ * Watches the subtree for parts arriving, leaving, or changing the attributes
77
+ * the root reads. The root's own writes (role, aria-*, ids, data-*) are not in
78
+ * the filter, so reconciling never re-triggers itself.
79
+ */
80
+ #observer = new MutationObserver(() => this.#reconcile());
81
+ connectedCallback() {
82
+ this.#controller.connect();
83
+ this.#observer.observe(this, {
84
+ subtree: true,
85
+ childList: true,
86
+ attributes: true,
87
+ attributeFilter: ["value", "disabled", "label", "orientation"],
88
+ });
89
+ this.#reconcile();
90
+ }
91
+ /**
92
+ * Leaving the document stops all listening. A move (an `hx-preserve`, a
93
+ * framework reparenting) is a disconnect followed by a connect, so handlers
94
+ * never stack.
95
+ */
96
+ disconnectedCallback() {
97
+ this.#controller.disconnect();
98
+ this.#observer.disconnect();
99
+ }
100
+ /** The active tab's value, reflected as the `value` attribute. */
101
+ get value() {
102
+ return this.getAttribute("value") ?? undefined;
103
+ }
104
+ set value(next) {
105
+ this.setAttribute("value", next);
106
+ }
107
+ /**
108
+ * The controller's view of this element. State lives in reflected attributes;
109
+ * an activation is announced first, as a cancelable event, so a listener can
110
+ * veto it — the elements' equivalent of React's controlled mode (D7).
111
+ */
112
+ #host() {
113
+ return {
114
+ state: () => ({
115
+ active: this.value,
116
+ orientation: this.#orientation(),
117
+ // Stryker disable next-line StringLiteral: equivalent — core tests only for "rtl", so any other string reads as left-to-right.
118
+ dir: this.matches(":dir(rtl)") ? "rtl" : "ltr",
119
+ activationMode: this.#activationMode(),
120
+ }),
121
+ activate: (value, index) => {
122
+ const change = new CustomEvent("primitiv-tabs-change", {
123
+ bubbles: true,
124
+ cancelable: true,
125
+ detail: { value, index },
126
+ });
127
+ if (this.dispatchEvent(change))
128
+ this.value = value;
129
+ },
130
+ };
131
+ }
132
+ #activationMode() {
133
+ const mode = this.getAttribute("activation-mode");
134
+ return mode === "manual" ? mode : "automatic";
135
+ }
136
+ #orientation() {
137
+ return this.getAttribute("orientation") === "vertical" ? "vertical" : "horizontal";
138
+ }
139
+ /** Parts of this tab set only, never those of a nested one. */
140
+ #parts(selector) {
141
+ return [...this.querySelectorAll(selector)].filter((part) => part.closest(ROOT) === this);
142
+ }
143
+ #rootId() {
144
+ // Stryker disable next-line UpdateOperator: equivalent — counting down yields ids just as unique.
145
+ return this.id || (this.#generatedId ??= `${ROOT}-${++generatedIds}`);
146
+ }
147
+ #reconcile() {
148
+ const orientation = this.#orientation();
149
+ const active = this.value;
150
+ const rootId = this.#rootId();
151
+ write(this, "data-orientation", orientation);
152
+ for (const list of this.#parts(LIST)) {
153
+ write(list, "role", "tablist");
154
+ write(list, "aria-orientation", orientation);
155
+ write(list, "data-orientation", orientation);
156
+ const label = list.getAttribute("label");
157
+ if (label !== null)
158
+ write(list, "aria-label", label);
159
+ }
160
+ const panels = new Map();
161
+ for (const panel of this.#parts(CONTENT)) {
162
+ const value = panel.getAttribute("value");
163
+ if (value !== null)
164
+ panels.set(value, panel);
165
+ }
166
+ const triggers = this.#parts(TRIGGER).flatMap((trigger) => {
167
+ const part = decoratable(trigger);
168
+ return part ? [{ trigger, ...part }] : [];
169
+ });
170
+ const stop = tabsTabStop({ values: triggers.map((t) => t.value), active });
171
+ // Replace the registration wholesale: a removed part, or a trigger whose child
172
+ // a framework re-rendered, simply is not registered again.
173
+ this.#controller.setTriggers(triggers.map(({ trigger, tab, value }) => ({
174
+ value,
175
+ element: tab,
176
+ disabled: trigger.hasAttribute("disabled"),
177
+ })));
178
+ for (const { trigger, tab, value } of triggers) {
179
+ const panel = panels.get(value);
180
+ const selected = value === active;
181
+ const disabled = trigger.hasAttribute("disabled");
182
+ if (!tab.id)
183
+ tab.id = deriveId(rootId, "trigger", value);
184
+ if (panel && !panel.id)
185
+ panel.id = deriveId(rootId, "panel", value);
186
+ write(tab, "role", "tab");
187
+ write(tab, "aria-selected", String(selected));
188
+ write(tab, "aria-controls", panel ? panel.id : null);
189
+ write(tab, "tabindex", value === stop ? "0" : "-1");
190
+ write(tab, "aria-disabled", disabled ? "true" : null);
191
+ write(tab, "data-disabled", disabled ? "" : null);
192
+ write(tab, "data-state", selected ? "active" : "inactive");
193
+ write(tab, "data-orientation", orientation);
194
+ if (panel)
195
+ write(panel, "aria-labelledby", tab.id);
196
+ }
197
+ for (const [value, panel] of panels) {
198
+ const selected = value === active;
199
+ write(panel, "role", "tabpanel");
200
+ write(panel, "tabindex", "0");
201
+ write(panel, "hidden", selected ? null : "");
202
+ write(panel, "data-state", selected ? "active" : "inactive");
203
+ write(panel, "data-orientation", orientation);
204
+ }
205
+ // A panel that becomes active announces it, so a lazy panel can fetch its
206
+ // content (`hx-trigger="primitiv-tabs-show once"`, RFC 0033 D9). The first
207
+ // render shows what the server already rendered, so it announces nothing.
208
+ // `Map.get(undefined)` is simply undefined, so no tab active means no panel.
209
+ const shownPanel = panels.get(active);
210
+ if (this.#rendered && active !== this.#shown && shownPanel) {
211
+ shownPanel.dispatchEvent(new CustomEvent("primitiv-tabs-show", { bubbles: true }));
212
+ }
213
+ this.#shown = active;
214
+ this.#rendered = true;
215
+ }
216
+ }
217
+ /**
218
+ * `<primitiv-tabs-list>` — the tablist. Structural: the root writes its ARIA.
219
+ *
220
+ * @customElement primitiv-tabs-list
221
+ * @attr {string} label - The tablist's accessible name. Give this or `aria-labelledby`.
222
+ */
223
+ export class PrimitivTabsListElement extends ElementBase {
224
+ }
225
+ /**
226
+ * `<primitiv-tabs-trigger>` — an interactive part. It decorates exactly one
227
+ * child the consumer writes (a `<button>`, an `<a href>`, a router link), which
228
+ * receives the tab role, so the rendered DOM matches React's `<button role="tab">`
229
+ * (RFC 0033 D24).
230
+ *
231
+ * @customElement primitiv-tabs-trigger
232
+ * @attr {string} value - The tab's value, pairing it with the panel of the same value.
233
+ * @attr {boolean} disabled - Marks the tab disabled: arrow keys still land on it, but it never activates.
234
+ */
235
+ export class PrimitivTabsTriggerElement extends ElementBase {
236
+ }
237
+ /**
238
+ * `<primitiv-tabs-content>` — a tab panel. Structural: the root writes its ARIA.
239
+ *
240
+ * @customElement primitiv-tabs-content
241
+ * @attr {string} value - The panel's value, pairing it with the tab of the same value.
242
+ * @fires {CustomEvent} primitiv-tabs-show - Bubbles from the panel as it becomes active (not on first render), so a lazy panel can fetch its content.
243
+ */
244
+ export class PrimitivTabsContentElement extends ElementBase {
245
+ }
246
+ /**
247
+ * Define the four Tabs elements on `registry` (the global `customElements` by
248
+ * default, so a scoped registry can be passed when browsers support one). Safe
249
+ * to call more than once, and a no-op where there is no registry (a server
250
+ * render).
251
+ */
252
+ export function defineTabs(registry = globalThis.customElements) {
253
+ if (!registry)
254
+ return;
255
+ const elements = [
256
+ [ROOT, PrimitivTabsElement],
257
+ [LIST, PrimitivTabsListElement],
258
+ [TRIGGER, PrimitivTabsTriggerElement],
259
+ [CONTENT, PrimitivTabsContentElement],
260
+ ];
261
+ for (const [name, element] of elements) {
262
+ if (!registry.get(name))
263
+ registry.define(name, element);
264
+ }
265
+ }
@@ -0,0 +1,2 @@
1
+ export { TabsController, type TabsControllerHost } from "./TabsController.js";
2
+ export { defineTabs, PrimitivTabsContentElement, PrimitivTabsElement, PrimitivTabsListElement, PrimitivTabsTriggerElement, type ElementRegistry, } from "./elements.js";
@@ -0,0 +1,2 @@
1
+ export { TabsController } from "./TabsController.js";
2
+ export { defineTabs, PrimitivTabsContentElement, PrimitivTabsElement, PrimitivTabsListElement, PrimitivTabsTriggerElement, } from "./elements.js";
package/dist/tags.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Opt-in TypeScript tag map: bring this module into scope and DOM APIs return
3
+ * the element classes for Primitiv's tags.
4
+ *
5
+ * ```ts
6
+ * import type {} from "@primitiv-ui/web/tags";
7
+ * document.querySelector("primitiv-tabs"); // PrimitivTabsElement | null
8
+ * ```
9
+ *
10
+ * or list `"@primitiv-ui/web/tags"` in `compilerOptions.types`.
11
+ *
12
+ * It is a separate, npm-only entry because it augments a global type, which
13
+ * JSR does not allow a published module to do (RFC 0033 §5.6.4).
14
+ */
15
+ import type { PrimitivTabsContentElement, PrimitivTabsElement, PrimitivTabsListElement, PrimitivTabsTriggerElement } from "./tabs/elements.js";
16
+ declare global {
17
+ interface HTMLElementTagNameMap {
18
+ "primitiv-tabs": PrimitivTabsElement;
19
+ "primitiv-tabs-list": PrimitivTabsListElement;
20
+ "primitiv-tabs-trigger": PrimitivTabsTriggerElement;
21
+ "primitiv-tabs-content": PrimitivTabsContentElement;
22
+ }
23
+ }
24
+ export {};
package/dist/tags.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,105 @@
1
1
  {
2
2
  "name": "@primitiv-ui/web",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.42",
4
+ "description": "Primitiv's headless components as standard custom elements: light DOM, no framework, no runtime dependencies. Works in plain HTML, server-rendered stacks, React, Vue, Angular and anything else that renders HTML.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ },
13
+ "./tabs": {
14
+ "types": "./dist/tabs/index.d.ts",
15
+ "default": "./dist/tabs/index.js"
16
+ },
17
+ "./define": {
18
+ "types": "./dist/define/index.d.ts",
19
+ "default": "./dist/define/index.js"
20
+ },
21
+ "./define/tabs": {
22
+ "types": "./dist/define/tabs.d.ts",
23
+ "default": "./dist/define/tabs.js"
24
+ },
25
+ "./tags": {
26
+ "types": "./dist/tags.d.ts",
27
+ "default": "./dist/tags.js"
28
+ },
29
+ "./react": {
30
+ "types": "./dist/frameworks/react.d.ts",
31
+ "default": "./dist/frameworks/react.js"
32
+ },
33
+ "./vue": {
34
+ "types": "./dist/frameworks/vue.d.ts",
35
+ "default": "./dist/frameworks/vue.js"
36
+ },
37
+ "./svelte": {
38
+ "types": "./dist/frameworks/svelte.d.ts",
39
+ "default": "./dist/frameworks/svelte.js"
40
+ }
41
+ },
42
+ "sideEffects": [
43
+ "./src/define/**",
44
+ "./dist/define/**"
45
+ ],
46
+ "files": [
47
+ "dist"
48
+ ],
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "https://github.com/primitiv-ui/primitiv.git",
52
+ "directory": "packages/web"
53
+ },
54
+ "dependencies": {
55
+ "@primitiv-ui/core": "0.1.42"
56
+ },
57
+ "devDependencies": {
58
+ "@custom-elements-manifest/analyzer": "0.11.0",
59
+ "@stryker-mutator/core": "9.6.1",
60
+ "@stryker-mutator/vitest-runner": "9.6.1",
61
+ "@testing-library/dom": "^10.4.1",
62
+ "@testing-library/user-event": "^14.6.1",
63
+ "@types/react": "^19.2.14",
64
+ "@vitest/coverage-v8": "^4.1.4",
65
+ "esbuild": "^0.28.0",
66
+ "jsdom": "^29.0.2",
67
+ "svelte": "^5.46.4",
68
+ "typescript": "^6.0.2",
69
+ "vitest": "^4.1.4",
70
+ "vue": "^3.5.0",
71
+ "@primitiv-ui/conformance": "0.0.0"
72
+ },
73
+ "publishConfig": {
74
+ "access": "public"
75
+ },
76
+ "customElements": "dist/custom-elements.json",
77
+ "peerDependencies": {
78
+ "@types/react": "^19.0.0",
79
+ "vue": "^3.3.0",
80
+ "svelte": "^5.0.0"
81
+ },
82
+ "peerDependenciesMeta": {
83
+ "@types/react": {
84
+ "optional": true
85
+ },
86
+ "vue": {
87
+ "optional": true
88
+ },
89
+ "svelte": {
90
+ "optional": true
91
+ }
92
+ },
93
+ "scripts": {
94
+ "build": "rm -rf dist && tsc -p tsconfig.build.json && node ../../scripts/finalize-dist.mjs dist && pnpm run build:bundle && pnpm run build:manifest",
95
+ "qa": "pnpm qa:units",
96
+ "qa:units": "vitest run --coverage",
97
+ "qa:units:watch": "vitest --coverage",
98
+ "mutate": "stryker run",
99
+ "typecheck": "tsc -p tsconfig.json --noEmit",
100
+ "build:bundle": "esbuild src/bundle.ts --bundle --format=esm --minify --target=es2022 --outfile=dist/primitiv-web.js",
101
+ "size": "node scripts/check-size.mjs",
102
+ "build:manifest": "cem analyze --globs \"src/**/elements.ts\" --outdir dist",
103
+ "manifest:check": "node scripts/check-manifest.mjs"
104
+ }
6
105
  }