@ptlm-azulejo/tabs 0.0.1-alpha.192

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 ADDED
@@ -0,0 +1,4 @@
1
+ # Change Log
2
+
3
+ All notable changes to this project will be documented in this file.
4
+ See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
package/README.md ADDED
@@ -0,0 +1,133 @@
1
+ # Tabs
2
+
3
+ The `AzTabs` component organizes related content into multiple sections, allowing users to switch between views without leaving the current page. Tabs can display labels, icons, and notification badges.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @ptlm-azulejo/tabs @ptlm-azulejo/themes
9
+ # or
10
+ yarn add @ptlm-azulejo/tabs @ptlm-azulejo/themes
11
+ ```
12
+
13
+ `@ptlm-azulejo/tabs` depends on `@ptlm-azulejo/divider` and `@ptlm-azulejo/number-badge` (installed automatically).
14
+ `@ptlm-azulejo/themes` is an optional peer — install it so brand tokens resolve at runtime.
15
+ ## Styles & theming
16
+
17
+ Import a brand preset and the component styles, then set the brand class on the root element:
18
+
19
+ ```js
20
+ import '@ptlm-azulejo/themes/presets/leroy-merlin.css' // or /presets/adeo.css
21
+ import '@ptlm-azulejo/tabs/style.css'
22
+ ```
23
+
24
+ ```html
25
+ <html class="preset-lm" data-theme="dark">
26
+ ```
27
+
28
+ > Without a preset **and** the `.preset-*` class, the component renders uncolored.
29
+ > See the [themes package](../themes/README.md) for brand switching, dark mode,
30
+ > and custom brands.
31
+
32
+ ## Props
33
+
34
+ | Name | Type | Default | Description |
35
+ | --- | --- | --- | --- |
36
+ | `tabs` | `TabItem[]` | — | Tab definitions. Each item supports `label`, `id`, `icon`, `badge`, and `disabled`. |
37
+ | `modelValue` | `string \| number` | `0` | Active tab (`v-model`). A string must match a tab `id`; a number is an index. |
38
+ | `description` | `string` | — | Accessible name for the tab list (`aria-label`). |
39
+ | `size` | `'s' \| 'm'` | `'m'` | Tab size. |
40
+ | `align` | `'left' \| 'center' \| 'right'` | `'left'` | Horizontal alignment of the tab list. |
41
+ | `rounded` | `'none' \| 's' \| 'm' \| 'l' \| 'full'` | `'m'` | Border radius of each tab button. |
42
+ | `divider` | `boolean` | `true` | Renders a divider below the tab list. |
43
+ | `ui` | `TabsUi` | `{}` | Tailwind class overrides per structural part. Merged via `cn()` (conflicting utilities win). |
44
+
45
+ ### TabItem
46
+
47
+ | Property | Type | Description |
48
+ | --- | --- | --- |
49
+ | `label` | `string` | Text displayed in the tab. |
50
+ | `id` | `string` | Unique id used when `modelValue` is a string. |
51
+ | `icon` | `Component \| string` | Optional icon before the label. Use a Vue component, or an Azulejo icon class string (`'az az-ui-home'`). |
52
+ | `badge` | `number` | Optional numeric badge after the label. |
53
+ | `disabled` | `boolean` | Disables the tab. |
54
+
55
+ ### TabsUi parts
56
+
57
+ `root`, `list`, `item`, `tab`, `tabSelected`, `icon`, `label`, `badge`, `divider`
58
+
59
+ ## Events
60
+
61
+ | Name | Payload | Description |
62
+ | --- | --- | --- |
63
+ | `update:modelValue` | `string \| number` | Emitted when the selected tab changes. |
64
+
65
+ ## Basic usage
66
+
67
+ ```vue
68
+ <script setup>
69
+ import { ref } from 'vue'
70
+ import { AzTabs } from '@ptlm-azulejo/tabs'
71
+
72
+ const selected = ref('overview')
73
+ const tabs = [
74
+ { id: 'overview', label: 'Overview' },
75
+ { id: 'orders', label: 'Orders' },
76
+ { id: 'settings', label: 'Settings' },
77
+ ]
78
+ </script>
79
+
80
+ <template>
81
+ <AzTabs v-model="selected" :tabs="tabs" description="Product sections" />
82
+ </template>
83
+ ```
84
+
85
+ ## Index-based model
86
+
87
+ ```vue
88
+ <script setup>
89
+ import { ref } from 'vue'
90
+ import { AzTabs } from '@ptlm-azulejo/tabs'
91
+
92
+ const selected = ref(0)
93
+ const tabs = [
94
+ { label: 'Overview' },
95
+ { label: 'Orders' },
96
+ { label: 'Settings' },
97
+ ]
98
+ </script>
99
+
100
+ <template>
101
+ <AzTabs v-model="selected" :tabs="tabs" />
102
+ </template>
103
+ ```
104
+
105
+ ## Icons, badges, and alignment
106
+
107
+ ```vue
108
+ <script setup>
109
+ import { ref } from 'vue'
110
+ import { AzTabs } from '@ptlm-azulejo/tabs'
111
+ import '@ptlm-azulejo/icons/style.css'
112
+
113
+ const selected = ref('home')
114
+ const tabs = [
115
+ { id: 'home', label: 'Home', icon: 'az az-ui-home' },
116
+ { id: 'alerts', label: 'Alerts', icon: 'az az-ui-notification', badge: 8 },
117
+ { id: 'settings', label: 'Settings', icon: 'az az-ui-settings', disabled: true },
118
+ ]
119
+ </script>
120
+
121
+ <template>
122
+ <AzTabs v-model="selected" :tabs="tabs" align="center" rounded="m" />
123
+ </template>
124
+ ```
125
+
126
+ `align` accepts `left` (default), `center`, or `right`.
127
+ `rounded` accepts `none`, `s`, `m` (default), `l`, or `full`.
128
+
129
+ ## Accessibility
130
+
131
+ - Uses WAI-ARIA `tablist` / `tab` roles.
132
+ - Keyboard: `ArrowLeft` / `ArrowRight`, `Home`, `End` (skips disabled tabs).
133
+ - Use `description` to label the tab list for assistive tech.
package/dist/index.js ADDED
@@ -0,0 +1,246 @@
1
+ import { Fragment as M, computed as p, createBlock as R, createCommentVNode as j, createElementBlock as x, createElementVNode as C, createVNode as F, defineComponent as O, normalizeClass as u, openBlock as g, ref as P, renderList as U, resolveDynamicComponent as q, toDisplayString as G, toRef as A, unref as h } from "vue";
2
+ import { AzDivider as J } from "@ptlm-azulejo/divider";
3
+ import { AzNumberBadge as Q } from "@ptlm-azulejo/number-badge";
4
+ import "@ptlm-azulejo/divider/style.css";
5
+ import "@ptlm-azulejo/number-badge/style.css";
6
+ import { clsx as W } from "clsx";
7
+ import { extendTailwindMerge as X } from "tailwind-merge";
8
+ var Y = X({ extend: { theme: {
9
+ color: [
10
+ "accent",
11
+ "on-accent",
12
+ "accent-hover",
13
+ "accent-subtle",
14
+ "accent-subtle-hover",
15
+ "accent-strong",
16
+ "accent-strong-hover",
17
+ "fg",
18
+ "fg-accent",
19
+ "surface",
20
+ "surface-muted",
21
+ "edge",
22
+ "danger",
23
+ "danger-hover"
24
+ ],
25
+ radius: ["btn"]
26
+ } } });
27
+ function f(...a) {
28
+ return Y(W(a));
29
+ }
30
+ function B(a, y, r) {
31
+ var o;
32
+ return typeof r == "string" ? (o = y.id) !== null && o !== void 0 ? o : String(a) : a;
33
+ }
34
+ function Z(a) {
35
+ function y(e, t) {
36
+ const s = B(e, t, a.modelValue.value);
37
+ return a.modelValue.value === s;
38
+ }
39
+ function r() {
40
+ const e = a.modelValue.value, t = a.tabs.value;
41
+ return typeof e == "string" ? t.findIndex((s) => s.id === e) : typeof e == "number" && e >= 0 && e < t.length ? e : -1;
42
+ }
43
+ function o() {
44
+ return a.tabs.value.map((e, t) => t).filter((e) => {
45
+ var t;
46
+ return !(!((t = a.tabs.value[e]) === null || t === void 0) && t.disabled);
47
+ });
48
+ }
49
+ function n() {
50
+ const e = o();
51
+ if (e.length === 0)
52
+ return -1;
53
+ const t = r();
54
+ return e.includes(t) ? t : e[0];
55
+ }
56
+ function z(e) {
57
+ const t = n();
58
+ return t < 0 ? -1 : e === t ? 0 : -1;
59
+ }
60
+ function k(e) {
61
+ const t = a.tabs.value[e];
62
+ if (!t || t.disabled)
63
+ return;
64
+ const s = B(e, t, a.modelValue.value);
65
+ s !== a.modelValue.value && a.onSelect(s);
66
+ }
67
+ function I(e, t) {
68
+ var s;
69
+ if (e.key !== "ArrowRight" && e.key !== "ArrowLeft" && e.key !== "Home" && e.key !== "End")
70
+ return;
71
+ const d = o();
72
+ if (d.length === 0)
73
+ return;
74
+ const m = d.indexOf(t);
75
+ let b = null;
76
+ if (e.key === "ArrowRight") {
77
+ if (e.preventDefault(), m < 0)
78
+ return;
79
+ b = (m + 1) % d.length;
80
+ } else if (e.key === "ArrowLeft") {
81
+ if (e.preventDefault(), m < 0)
82
+ return;
83
+ b = (m - 1 + d.length) % d.length;
84
+ } else
85
+ e.key === "Home" ? (e.preventDefault(), b = 0) : e.key === "End" && (e.preventDefault(), b = d.length - 1);
86
+ if (b === null)
87
+ return;
88
+ const w = d[b];
89
+ k(w), (s = a.focusIndex) === null || s === void 0 || s.call(a, w);
90
+ }
91
+ function _(e) {
92
+ return String(e);
93
+ }
94
+ return {
95
+ isSelected: y,
96
+ getTabIndex: z,
97
+ select: k,
98
+ onKeydown: I,
99
+ formatBadge: _
100
+ };
101
+ }
102
+ var ee = ["aria-label"], te = [
103
+ "aria-selected",
104
+ "aria-disabled",
105
+ "disabled",
106
+ "tabindex",
107
+ "onClick",
108
+ "onKeydown"
109
+ ], ae = /* @__PURE__ */ O({
110
+ __name: "index",
111
+ props: {
112
+ description: {},
113
+ size: { default: "m" },
114
+ divider: {
115
+ type: Boolean,
116
+ default: !0
117
+ },
118
+ align: { default: "left" },
119
+ rounded: { default: "m" },
120
+ modelValue: { default: 0 },
121
+ tabs: {},
122
+ ui: { default: () => ({}) }
123
+ },
124
+ emits: ["update:modelValue"],
125
+ setup(a, { emit: y }) {
126
+ const r = {
127
+ root: "relative w-full bg-[color:var(--color-background-primary)] font-sans",
128
+ list: [
129
+ "m-0 flex list-none overflow-x-auto p-0",
130
+ "gap-[length:var(--spacing-100)]",
131
+ "py-[length:var(--spacing-100)] px-[calc(var(--spacing)*0.5)]",
132
+ "md:overflow-hidden"
133
+ ].join(" "),
134
+ item: "",
135
+ tab: [
136
+ "group relative flex cursor-pointer items-center justify-center border-none bg-transparent no-underline outline-none font-sans",
137
+ "text-[color:var(--color-text-tertiary)] gap-[calc(var(--spacing)*0.5)]",
138
+ "hover:not-disabled:bg-[color-mix(in_srgb,var(--color-text-primary)_5%,transparent)]",
139
+ "focus-visible:outline focus-visible:outline-[length:var(--border-m)] focus-visible:outline-[color:var(--color-text-accent)] focus-visible:outline-offset-[length:var(--border-m)]",
140
+ "disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-transparent"
141
+ ].join(" "),
142
+ tabSelected: [
143
+ "font-semibold text-[color:var(--color-text-accent)]",
144
+ "bg-[color:var(--color-background-accent)]",
145
+ "hover:not-disabled:bg-[color-mix(in_srgb,var(--color-background-accent)_88%,#000)]"
146
+ ].join(" "),
147
+ icon: ["inline-flex size-[1em] shrink-0 items-center justify-center leading-none text-current", "[&_svg]:block [&_svg]:size-full"].join(" "),
148
+ label: "pointer-events-none whitespace-nowrap",
149
+ badge: "inline-flex shrink-0 items-center",
150
+ divider: ""
151
+ }, o = {
152
+ size: {
153
+ s: {
154
+ root: "h-10",
155
+ tab: "text-sm h-[length:var(--spacing-300)] px-3",
156
+ icon: "text-[length:calc(var(--spacing)*1.25)]"
157
+ },
158
+ m: {
159
+ root: "h-14",
160
+ tab: "text-base h-[length:var(--spacing-500)] px-4",
161
+ icon: "text-[length:var(--spacing-300)]"
162
+ }
163
+ },
164
+ align: {
165
+ left: { list: "justify-start" },
166
+ center: { list: "justify-center" },
167
+ right: { list: "justify-end" }
168
+ },
169
+ rounded: {
170
+ none: { tab: "rounded-none" },
171
+ s: { tab: "rounded-[length:var(--border-radius-s)]" },
172
+ m: { tab: "rounded-[length:var(--border-radius-m)]" },
173
+ l: { tab: "rounded-[length:var(--border-radius-l)]" },
174
+ full: { tab: "rounded-full" }
175
+ }
176
+ }, n = a, z = y, k = A(n, "tabs"), I = A(n, "modelValue"), _ = P([]), e = (l, c) => {
177
+ _.value[c] = l;
178
+ }, { isSelected: t, getTabIndex: s, select: d, onKeydown: m } = Z({
179
+ tabs: k,
180
+ modelValue: I,
181
+ onSelect: (l) => z("update:modelValue", l),
182
+ focusIndex: (l) => {
183
+ var c;
184
+ return (c = _.value[l]) === null || c === void 0 ? void 0 : c.focus();
185
+ }
186
+ }), b = p(() => f(r.root, o.size[n.size].root, n.ui.root)), w = p(() => f(r.list, o.align[n.align].list, n.ui.list)), D = (l, c) => f(r.tab, o.size[n.size].tab, o.rounded[n.rounded].tab, n.ui.tab, t(l, c) && r.tabSelected, t(l, c) && n.ui.tabSelected), $ = (l) => f(r.icon, o.size[n.size].icon, l, n.ui.icon), T = p(() => f(r.label, n.ui.label)), E = p(() => f(r.badge, n.ui.badge)), K = p(() => f(r.divider, n.ui.divider)), N = p(() => f(r.item, n.ui.item)), L = (l) => typeof l == "number", H = (l) => typeof l == "string";
187
+ return (l, c) => (g(), x("nav", {
188
+ "data-testid": "tabs",
189
+ class: u(b.value)
190
+ }, [C("ul", {
191
+ role: "tablist",
192
+ "data-testid": "tabs-list",
193
+ class: u(w.value),
194
+ "aria-label": a.description
195
+ }, [(g(!0), x(M, null, U(a.tabs, (i, v) => {
196
+ var S;
197
+ return g(), x("li", {
198
+ key: (S = i.id) !== null && S !== void 0 ? S : v,
199
+ role: "presentation",
200
+ "data-testid": "tabs-item",
201
+ class: u(N.value)
202
+ }, [C("button", {
203
+ ref_for: !0,
204
+ ref: (V) => e(V, v),
205
+ type: "button",
206
+ role: "tab",
207
+ "data-testid": "tabs-tab",
208
+ class: u(D(v, i)),
209
+ "aria-selected": h(t)(v, i),
210
+ "aria-disabled": i.disabled || void 0,
211
+ disabled: i.disabled,
212
+ tabindex: h(s)(v),
213
+ onClick: (V) => h(d)(v),
214
+ onKeydown: (V) => h(m)(V, v)
215
+ }, [
216
+ H(i.icon) ? (g(), x("i", {
217
+ key: 0,
218
+ "data-testid": "tabs-icon",
219
+ class: u($(i.icon)),
220
+ "aria-hidden": "true"
221
+ }, null, 2)) : i.icon ? (g(), R(q(i.icon), {
222
+ key: 1,
223
+ "data-testid": "tabs-icon",
224
+ class: u($()),
225
+ "aria-hidden": "true"
226
+ }, null, 8, ["class"])) : j("", !0),
227
+ C("div", {
228
+ "data-testid": "tabs-label",
229
+ class: u(T.value)
230
+ }, [C("span", null, G(i.label), 1)], 2),
231
+ L(i.badge) ? (g(), x("span", {
232
+ key: 2,
233
+ "data-testid": "tabs-badge",
234
+ class: u(E.value)
235
+ }, [F(h(Q), { label: i.badge }, null, 8, ["label"])], 2)) : j("", !0)
236
+ ], 42, te)], 2);
237
+ }), 128))], 10, ee), a.divider ? (g(), R(h(J), {
238
+ key: 0,
239
+ "data-testid": "tabs-divider",
240
+ class: u(K.value)
241
+ }, null, 8, ["class"])) : j("", !0)], 2));
242
+ }
243
+ }), ce = ae;
244
+ export {
245
+ ce as AzTabs
246
+ };
@@ -0,0 +1 @@
1
+ (function(o,e){typeof exports=="object"&&typeof module!="undefined"?e(exports,require("vue"),require("@ptlm-azulejo/divider"),require("@ptlm-azulejo/number-badge"),require("@ptlm-azulejo/divider/style.css"),require("@ptlm-azulejo/number-badge/style.css"),require("clsx"),require("tailwind-merge")):typeof define=="function"&&define.amd?define(["exports","vue","@ptlm-azulejo/divider","@ptlm-azulejo/number-badge","@ptlm-azulejo/divider/style.css","@ptlm-azulejo/number-badge/style.css","clsx","tailwind-merge"],e):(o=typeof globalThis!="undefined"?globalThis:o||self,e(o.tabs={},o.Vue,o.divider,o["number-badge"],o.divider,o["number-badge"],o.clsx,o["tailwind-merge"]))})(this,function(o,e,w,B,P,F,I,S){Object.defineProperty(o,Symbol.toStringTag,{value:"Module"});var E=(0,S.extendTailwindMerge)({extend:{theme:{color:["accent","on-accent","accent-hover","accent-subtle","accent-subtle-hover","accent-strong","accent-strong-hover","fg","fg-accent","surface","surface-muted","edge","danger","danger-hover"],radius:["btn"]}}});function f(...n){return E((0,I.clsx)(n))}function V(n,y,s){var i;return typeof s=="string"?(i=y.id)!==null&&i!==void 0?i:String(n):n}function T(n){function y(t,a){const d=V(t,a,n.modelValue.value);return n.modelValue.value===d}function s(){const t=n.modelValue.value,a=n.tabs.value;return typeof t=="string"?a.findIndex(d=>d.id===t):typeof t=="number"&&t>=0&&t<a.length?t:-1}function i(){return n.tabs.value.map((t,a)=>a).filter(t=>{var a;return!(!((a=n.tabs.value[t])===null||a===void 0)&&a.disabled)})}function l(){const t=i();if(t.length===0)return-1;const a=s();return t.includes(a)?a:t[0]}function _(t){const a=l();return a<0?-1:t===a?0:-1}function h(t){const a=n.tabs.value[t];if(!a||a.disabled)return;const d=V(t,a,n.modelValue.value);d!==n.modelValue.value&&n.onSelect(d)}function z(t,a){var d;if(t.key!=="ArrowRight"&&t.key!=="ArrowLeft"&&t.key!=="Home"&&t.key!=="End")return;const u=i();if(u.length===0)return;const p=u.indexOf(a);let m=null;if(t.key==="ArrowRight"){if(t.preventDefault(),p<0)return;m=(p+1)%u.length}else if(t.key==="ArrowLeft"){if(t.preventDefault(),p<0)return;m=(p-1+u.length)%u.length}else t.key==="Home"?(t.preventDefault(),m=0):t.key==="End"&&(t.preventDefault(),m=u.length-1);if(m===null)return;const x=u[m];h(x),(d=n.focusIndex)===null||d===void 0||d.call(n,x)}function v(t){return String(t)}return{isSelected:y,getTabIndex:_,select:h,onKeydown:z,formatBadge:v}}var N=["aria-label"],R=["aria-selected","aria-disabled","disabled","tabindex","onClick","onKeydown"],$=(0,e.defineComponent)({__name:"index",props:{description:{},size:{default:"m"},divider:{type:Boolean,default:!0},align:{default:"left"},rounded:{default:"m"},modelValue:{default:0},tabs:{},ui:{default:()=>({})}},emits:["update:modelValue"],setup(n,{emit:y}){const s={root:"relative w-full bg-[color:var(--color-background-primary)] font-sans",list:["m-0 flex list-none overflow-x-auto p-0","gap-[length:var(--spacing-100)]","py-[length:var(--spacing-100)] px-[calc(var(--spacing)*0.5)]","md:overflow-hidden"].join(" "),item:"",tab:["group relative flex cursor-pointer items-center justify-center border-none bg-transparent no-underline outline-none font-sans","text-[color:var(--color-text-tertiary)] gap-[calc(var(--spacing)*0.5)]","hover:not-disabled:bg-[color-mix(in_srgb,var(--color-text-primary)_5%,transparent)]","focus-visible:outline focus-visible:outline-[length:var(--border-m)] focus-visible:outline-[color:var(--color-text-accent)] focus-visible:outline-offset-[length:var(--border-m)]","disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-transparent"].join(" "),tabSelected:["font-semibold text-[color:var(--color-text-accent)]","bg-[color:var(--color-background-accent)]","hover:not-disabled:bg-[color-mix(in_srgb,var(--color-background-accent)_88%,#000)]"].join(" "),icon:["inline-flex size-[1em] shrink-0 items-center justify-center leading-none text-current","[&_svg]:block [&_svg]:size-full"].join(" "),label:"pointer-events-none whitespace-nowrap",badge:"inline-flex shrink-0 items-center",divider:""},i={size:{s:{root:"h-10",tab:"text-sm h-[length:var(--spacing-300)] px-3",icon:"text-[length:calc(var(--spacing)*1.25)]"},m:{root:"h-14",tab:"text-base h-[length:var(--spacing-500)] px-4",icon:"text-[length:var(--spacing-300)]"}},align:{left:{list:"justify-start"},center:{list:"justify-center"},right:{list:"justify-end"}},rounded:{none:{tab:"rounded-none"},s:{tab:"rounded-[length:var(--border-radius-s)]"},m:{tab:"rounded-[length:var(--border-radius-m)]"},l:{tab:"rounded-[length:var(--border-radius-l)]"},full:{tab:"rounded-full"}}},l=n,_=y,h=(0,e.toRef)(l,"tabs"),z=(0,e.toRef)(l,"modelValue"),v=(0,e.ref)([]),t=(r,b)=>{v.value[b]=r},{isSelected:a,getTabIndex:d,select:u,onKeydown:p}=T({tabs:h,modelValue:z,onSelect:r=>_("update:modelValue",r),focusIndex:r=>{var b;return(b=v.value[r])===null||b===void 0?void 0:b.focus()}}),m=(0,e.computed)(()=>f(s.root,i.size[l.size].root,l.ui.root)),x=(0,e.computed)(()=>f(s.list,i.align[l.align].list,l.ui.list)),A=(r,b)=>f(s.tab,i.size[l.size].tab,i.rounded[l.rounded].tab,l.ui.tab,a(r,b)&&s.tabSelected,a(r,b)&&l.ui.tabSelected),j=r=>f(s.icon,i.size[l.size].icon,r,l.ui.icon),D=(0,e.computed)(()=>f(s.label,l.ui.label)),K=(0,e.computed)(()=>f(s.badge,l.ui.badge)),L=(0,e.computed)(()=>f(s.divider,l.ui.divider)),M=(0,e.computed)(()=>f(s.item,l.ui.item)),H=r=>typeof r=="number",O=r=>typeof r=="string";return(r,b)=>((0,e.openBlock)(),(0,e.createElementBlock)("nav",{"data-testid":"tabs",class:(0,e.normalizeClass)(m.value)},[(0,e.createElementVNode)("ul",{role:"tablist","data-testid":"tabs-list",class:(0,e.normalizeClass)(x.value),"aria-label":n.description},[((0,e.openBlock)(!0),(0,e.createElementBlock)(e.Fragment,null,(0,e.renderList)(n.tabs,(c,g)=>{var C;return(0,e.openBlock)(),(0,e.createElementBlock)("li",{key:(C=c.id)!==null&&C!==void 0?C:g,role:"presentation","data-testid":"tabs-item",class:(0,e.normalizeClass)(M.value)},[(0,e.createElementVNode)("button",{ref_for:!0,ref:k=>t(k,g),type:"button",role:"tab","data-testid":"tabs-tab",class:(0,e.normalizeClass)(A(g,c)),"aria-selected":(0,e.unref)(a)(g,c),"aria-disabled":c.disabled||void 0,disabled:c.disabled,tabindex:(0,e.unref)(d)(g),onClick:k=>(0,e.unref)(u)(g),onKeydown:k=>(0,e.unref)(p)(k,g)},[O(c.icon)?((0,e.openBlock)(),(0,e.createElementBlock)("i",{key:0,"data-testid":"tabs-icon",class:(0,e.normalizeClass)(j(c.icon)),"aria-hidden":"true"},null,2)):c.icon?((0,e.openBlock)(),(0,e.createBlock)((0,e.resolveDynamicComponent)(c.icon),{key:1,"data-testid":"tabs-icon",class:(0,e.normalizeClass)(j()),"aria-hidden":"true"},null,8,["class"])):(0,e.createCommentVNode)("",!0),(0,e.createElementVNode)("div",{"data-testid":"tabs-label",class:(0,e.normalizeClass)(D.value)},[(0,e.createElementVNode)("span",null,(0,e.toDisplayString)(c.label),1)],2),H(c.badge)?((0,e.openBlock)(),(0,e.createElementBlock)("span",{key:2,"data-testid":"tabs-badge",class:(0,e.normalizeClass)(K.value)},[(0,e.createVNode)((0,e.unref)(B.AzNumberBadge),{label:c.badge},null,8,["label"])],2)):(0,e.createCommentVNode)("",!0)],42,R)],2)}),128))],10,N),n.divider?((0,e.openBlock)(),(0,e.createBlock)((0,e.unref)(w.AzDivider),{key:0,"data-testid":"tabs-divider",class:(0,e.normalizeClass)(L.value)},null,8,["class"])):(0,e.createCommentVNode)("",!0)],2))}}),q=$;o.AzTabs=q});
@@ -0,0 +1,3 @@
1
+ export { default as AzTabs } from './src/index.vue';
2
+ export type { TabItem, TabsSize, TabsAlign, TabsRounded, TabsModelValue, TabsUi, } from './src/types';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,iBAAiB,CAAA;AACnD,YAAY,EACV,OAAO,EACP,QAAQ,EACR,SAAS,EACT,WAAW,EACX,cAAc,EACd,MAAM,GACP,MAAM,aAAa,CAAA"}
@@ -0,0 +1,54 @@
1
+ import { TabItem, TabsAlign, TabsModelValue, TabsRounded, TabsSize, TabsUi } from './types';
2
+ type __VLS_Props = {
3
+ /**
4
+ * Accessible name for the tab list (`aria-label`).
5
+ */
6
+ description?: string;
7
+ /**
8
+ * Size of the tabs. `m` is the default Mozaic medium size.
9
+ */
10
+ size?: TabsSize;
11
+ /**
12
+ * When true, a divider is rendered below the tab list.
13
+ */
14
+ divider?: boolean;
15
+ /**
16
+ * Horizontal alignment of the tab list: `left`, `center`, or `right`.
17
+ */
18
+ align?: TabsAlign;
19
+ /**
20
+ * Border radius of each tab button.
21
+ * Maps to theme tokens: `s` / `m` / `l`, plus `none` and `full`. Defaults to `m`.
22
+ */
23
+ rounded?: TabsRounded;
24
+ /**
25
+ * Currently active tab (`v-model`).
26
+ * - `number` — index of the tab
27
+ * - `string` — must match a tab `id`
28
+ */
29
+ modelValue?: TabsModelValue;
30
+ /**
31
+ * Tab definitions used to render the tab list.
32
+ */
33
+ tabs: TabItem[];
34
+ /**
35
+ * Tailwind class overrides per structural part.
36
+ * Merged with `defaultTabsUi` via `cn()` — conflicting utilities win.
37
+ */
38
+ ui?: TabsUi;
39
+ };
40
+ declare const __VLS_export: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
41
+ "update:modelValue": (value?: TabsModelValue | undefined) => any;
42
+ }, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{
43
+ "onUpdate:modelValue"?: ((value?: TabsModelValue | undefined) => any) | undefined;
44
+ }>, {
45
+ rounded: TabsRounded;
46
+ size: TabsSize;
47
+ divider: boolean;
48
+ align: TabsAlign;
49
+ modelValue: TabsModelValue;
50
+ ui: TabsUi;
51
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
52
+ declare const _default: typeof __VLS_export;
53
+ export default _default;
54
+ //# sourceMappingURL=index.vue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.vue.d.ts","sourceRoot":"","sources":["../../../../src/index.vue"],"names":[],"mappings":"AAwQA,OAAO,iCAAiC,CAAA;AACxC,OAAO,sCAAsC,CAAA;AAG7C,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAmEhG,KAAK,WAAW,GAAG;IACf;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;OAEG;IACH,IAAI,CAAC,EAAE,QAAQ,CAAA;IACf;;OAEG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;OAEG;IACH,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB;;;OAGG;IACH,OAAO,CAAC,EAAE,WAAW,CAAA;IACrB;;;;OAIG;IACH,UAAU,CAAC,EAAE,cAAc,CAAA;IAC3B;;OAEG;IACH,IAAI,EAAE,OAAO,EAAE,CAAA;IACf;;;OAGG;IACH,EAAE,CAAC,EAAE,MAAM,CAAA;CACZ,CAAC;AAkOJ,QAAA,MAAM,YAAY;;;;;aAlPJ,WAAW;UAbd,QAAQ;aAIL,OAAO;WAIT,SAAS;gBAWJ,cAAc;QAStB,MAAM;6EAuOb,CAAC;wBACkB,OAAO,YAAY;AAAxC,wBAAyC"}
@@ -0,0 +1,46 @@
1
+ import { Component } from 'vue';
2
+ export interface TabItem {
3
+ /**
4
+ * Unique identifier for the tab. Required when `modelValue` is a string.
5
+ */
6
+ id?: string;
7
+ /**
8
+ * Icon rendered before the label.
9
+ * Pass a Vue component, or an Azulejo icon class string (e.g. `'az az-ui-home'`).
10
+ */
11
+ icon?: Component | string;
12
+ /**
13
+ * Numeric notification badge shown after the label.
14
+ */
15
+ badge?: number;
16
+ /**
17
+ * Visible tab label.
18
+ */
19
+ label: string;
20
+ /**
21
+ * When true, the tab cannot be selected.
22
+ */
23
+ disabled?: boolean;
24
+ }
25
+ export type TabsSize = 's' | 'm';
26
+ /**
27
+ * Horizontal alignment of the tab list within the available width.
28
+ */
29
+ export type TabsAlign = 'left' | 'center' | 'right';
30
+ /**
31
+ * Border radius of each tab button, mapped to theme `--border-radius-*` tokens.
32
+ */
33
+ export type TabsRounded = 'none' | 's' | 'm' | 'l' | 'full';
34
+ export type TabsModelValue = string | number;
35
+ export interface TabsUi {
36
+ root?: string;
37
+ list?: string;
38
+ item?: string;
39
+ tab?: string;
40
+ tabSelected?: string;
41
+ icon?: string;
42
+ label?: string;
43
+ badge?: string;
44
+ divider?: string;
45
+ }
46
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,KAAK,CAAA;AAEpC,MAAM,WAAW,OAAO;IACtB;;OAEG;IACH,EAAE,CAAC,EAAE,MAAM,CAAA;IACX;;;OAGG;IACH,IAAI,CAAC,EAAE,SAAS,GAAG,MAAM,CAAA;IACzB;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;OAEG;IACH,KAAK,EAAE,MAAM,CAAA;IACb;;OAEG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,MAAM,QAAQ,GAAG,GAAG,GAAG,GAAG,CAAA;AAEhC;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAA;AAEnD;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,MAAM,CAAA;AAE3D,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,MAAM,CAAA;AAE5C,MAAM,WAAW,MAAM;IACrB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB"}
@@ -0,0 +1,15 @@
1
+ import { Ref } from 'vue';
2
+ import { TabItem, TabsModelValue } from './types';
3
+ export declare function useTabs(options: {
4
+ tabs: Ref<TabItem[]>;
5
+ modelValue: Ref<TabsModelValue | undefined>;
6
+ onSelect: (value: TabsModelValue) => void;
7
+ focusIndex?: (index: number) => void;
8
+ }): {
9
+ isSelected: (index: number, tab: TabItem) => boolean;
10
+ getTabIndex: (index: number) => number;
11
+ select: (index: number) => void;
12
+ onKeydown: (event: KeyboardEvent, index: number) => void;
13
+ formatBadge: (badge: number) => string;
14
+ };
15
+ //# sourceMappingURL=useTabs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useTabs.d.ts","sourceRoot":"","sources":["../../../../src/useTabs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AAC9B,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAMtD,wBAAgB,OAAO,CAAC,OAAO,EAAE;IAC/B,IAAI,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC,CAAA;IACpB,UAAU,EAAE,GAAG,CAAC,cAAc,GAAG,SAAS,CAAC,CAAA;IAC3C,QAAQ,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAA;IACzC,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CACrC;wBAC4B,MAAM,OAAO,OAAO,KAAG,OAAO;yBAoC7B,MAAM,KAAG,MAAM;oBAMpB,MAAM,KAAG,IAAI;uBAUV,aAAa,SAAS,MAAM,KAAG,IAAI;yBAuCjC,MAAM,KAAG,MAAM;EAW5C"}
package/dist/style.css ADDED
@@ -0,0 +1,3 @@
1
+ /*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
2
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after{--tw-leading:initial;--tw-font-weight:initial;--tw-shadow:0 0 transparent;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 transparent;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 transparent;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 transparent;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 transparent;--tw-outline-style:solid}::-ms-backdrop{--tw-leading:initial;--tw-font-weight:initial;--tw-shadow:0 0 transparent;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 transparent;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 transparent;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 transparent;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 transparent;--tw-outline-style:solid}::backdrop{--tw-leading:initial;--tw-font-weight:initial;--tw-shadow:0 0 transparent;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 transparent;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 transparent;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 transparent;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 transparent;--tw-outline-style:solid}}}.pointer-events-none{pointer-events:none}.relative{position:relative}.m-0{margin:0}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.size-\[1em\]{width:1em;height:1em}.h-10{height:calc(var(--spacing,.25rem) * 10)}.h-14{height:calc(var(--spacing,.25rem) * 14)}.h-\[length\:var\(--spacing-300\)\]{height:var(--spacing-300)}.h-\[length\:var\(--spacing-500\)\]{height:var(--spacing-500)}.w-full{width:100%}.shrink-0{flex-shrink:0}.cursor-pointer{cursor:pointer}.list-none{list-style-type:none}.items-center{align-items:center}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.justify-start{justify-content:flex-start}.gap-\[calc\(var\(--spacing\)\*0\.5\)\]{gap:calc(var(--spacing) * .5)}.gap-\[length\:var\(--spacing-100\)\]{gap:var(--spacing-100)}.overflow-x-auto{overflow-x:auto}.rounded{border-radius:.25rem}.rounded-\[length\:var\(--border-radius-l\)\]{border-radius:var(--border-radius-l)}.rounded-\[length\:var\(--border-radius-m\)\]{border-radius:var(--border-radius-m)}.rounded-\[length\:var\(--border-radius-s\)\]{border-radius:var(--border-radius-s)}.rounded-full{border-radius:2147483647px}.rounded-none{border-radius:0}.border-none{--tw-border-style:none;border-style:none}.bg-\[color\:var\(--color-background-accent\)\]{background-color:var(--color-background-accent)}.bg-\[color\:var\(--color-background-primary\)\]{background-color:var(--color-background-primary)}.bg-transparent{background-color:transparent}.p-0{padding:0}.px-3{padding-inline:calc(var(--spacing,.25rem) * 3)}.px-4{padding-inline:calc(var(--spacing,.25rem) * 4)}.px-\[calc\(var\(--spacing\)\*0\.5\)\]{padding-inline:calc(var(--spacing) * .5)}.py-\[length\:var\(--spacing-100\)\]{padding-block:var(--spacing-100)}.font-sans{font-family:var(--font-family,ui-sans-serif, system-ui, sans-serif)}.text-base{font-size:var(--font-size-05);line-height:var(--tw-leading,var(--text-base--line-height,calc(1.5 / 1)))}.text-sm{font-size:var(--font-size-04);line-height:var(--tw-leading,var(--text-sm--line-height,calc(1.25 / .875)))}.text-\[length\:calc\(var\(--spacing\)\*1\.25\)\]{font-size:calc(var(--spacing) * 1.25)}.text-\[length\:var\(--spacing-300\)\]{font-size:var(--spacing-300)}.leading-none{--tw-leading:1;line-height:1}.font-semibold{--tw-font-weight:var(--font-weight-semi-bold);font-weight:var(--font-weight-semi-bold)}.whitespace-nowrap{white-space:nowrap}.text-\[color\:var\(--color-text-accent\)\]{color:var(--color-text-accent)}.text-\[color\:var\(--color-text-tertiary\)\]{color:var(--color-text-tertiary)}.text-current{color:currentColor}.no-underline{-webkit-text-decoration-line:none;text-decoration-line:none}.underline{-webkit-text-decoration-line:underline;text-decoration-line:underline}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-edge{--tw-ring-color:var(--color-border-primary)}.outline,.outline-1{outline-style:var(--tw-outline-style);outline-width:1px}.outline-edge{outline-color:var(--color-border-primary)}.outline-none{--tw-outline-style:none;outline-style:none}@media (hover:hover){.hover\:not-disabled\:bg-\[color-mix\(in_srgb\,var\(--color-background-accent\)_88\%\,\#000\)\]:hover:not(:disabled){background-color:var(--color-background-accent)}@supports (color:color-mix(in lab, red, red)){.hover\:not-disabled\:bg-\[color-mix\(in_srgb\,var\(--color-background-accent\)_88\%\,\#000\)\]:hover:not(:disabled){background-color:color-mix(in srgb,var(--color-background-accent) 88%,#000)}}.hover\:not-disabled\:bg-\[color-mix\(in_srgb\,var\(--color-text-primary\)_5\%\,transparent\)\]:hover:not(:disabled){background-color:var(--color-text-primary)}@supports (color:color-mix(in lab, red, red)){.hover\:not-disabled\:bg-\[color-mix\(in_srgb\,var\(--color-text-primary\)_5\%\,transparent\)\]:hover:not(:disabled){background-color:color-mix(in srgb,var(--color-text-primary) 5%,transparent)}}}.focus-visible\:outline:focus-visible{outline-style:var(--tw-outline-style);outline-width:1px}.focus-visible\:outline-\[length\:var\(--border-m\)\]:focus-visible{outline-style:var(--tw-outline-style);outline-width:var(--border-m)}.focus-visible\:outline-offset-\[length\:var\(--border-m\)\]:focus-visible{outline-offset:var(--border-m)}.focus-visible\:outline-\[color\:var\(--color-text-accent\)\]:focus-visible{outline-color:var(--color-text-accent)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}@media (hover:hover){.disabled\:hover\:bg-transparent:disabled:hover{background-color:transparent}}@media (min-width:48rem){.md\:overflow-hidden{overflow:hidden}}.\[\&_svg\]\:block svg{display:block}.\[\&_svg\]\:size-full svg{width:100%;height:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 transparent}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 transparent}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 transparent}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 transparent}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 transparent}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}
3
+ /*$vite$:1*/
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@ptlm-azulejo/tabs",
3
+ "version": "0.0.1-alpha.192",
4
+ "type": "module",
5
+ "main": "./dist/index.umd.cjs",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "require": "./dist/index.umd.cjs"
13
+ },
14
+ "./style.css": "./dist/style.css"
15
+ },
16
+ "scripts": {
17
+ "build": "vite build",
18
+ "test": "vitest --run --passWithNoTests",
19
+ "storybook": "storybook dev -p 6006",
20
+ "build-storybook": "storybook build"
21
+ },
22
+ "publishConfig": {
23
+ "access": "public",
24
+ "registry": "https://registry.npmjs.org"
25
+ },
26
+ "files": [
27
+ "dist/*"
28
+ ],
29
+ "dependencies": {
30
+ "@ptlm-azulejo/divider": "^0.0.1-alpha.32",
31
+ "@ptlm-azulejo/number-badge": "^0.0.1-alpha.33",
32
+ "clsx": "^2.1.1",
33
+ "tailwind-merge": "^3.0.0"
34
+ },
35
+ "devDependencies": {
36
+ "@ptlm-azulejo/themes": "*",
37
+ "@storybook/addon-essentials": "^7.0.0",
38
+ "@storybook/addon-interactions": "^7.0.0",
39
+ "@storybook/test": "^7.0.0",
40
+ "@storybook/vue3": "^7.0.0",
41
+ "@storybook/vue3-vite": "^7.0.0",
42
+ "@tailwindcss/vite": "^4.0.0",
43
+ "storybook": "^7.0.0",
44
+ "tailwindcss": "^4.0.0",
45
+ "vue": "^3.3.4"
46
+ },
47
+ "peerDependencies": {
48
+ "@ptlm-azulejo/themes": ">=1.0.0",
49
+ "vue": ">=3.0.0"
50
+ },
51
+ "peerDependenciesMeta": {
52
+ "@ptlm-azulejo/themes": {
53
+ "optional": true
54
+ }
55
+ }
56
+ }