Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
a2be257
feat(webkit): add small size to Link, aligning its scale with Button
gabriel-lisboa-azion Jul 22, 2026
e527036
fix(webkit): render Popover panel in one teleported subtree, contain …
gabriel-lisboa-azion Jul 22, 2026
27fea84
feat(webkit): make TabView tab bar horizontally scrollable with edge-…
gabriel-lisboa-azion Jul 22, 2026
13ff05e
fix(webkit): scale MiniButton hover ghost and content gap per size
gabriel-lisboa-azion Jul 22, 2026
dcde245
fix(theme): deepen popup scale keyframes to 0.9 for a more pronounced…
gabriel-lisboa-azion Jul 22, 2026
3d241ec
fix(webkit): tween Dialog panel via translate/scale properties under …
gabriel-lisboa-azion Jul 22, 2026
25b695d
chore(webkit): update generated catalog.json
gabriel-lisboa-azion Jul 22, 2026
c599d17
fix(webkit): widen PopoverContent ref-callback type and refresh tab-v…
herbert-julio-azion Jul 23, 2026
6eaa496
test(webkit): drop TabView overflow/edge-fade assertions from the uni…
herbert-julio-azion Jul 23, 2026
d6593b3
test(storybook): regenerate MiniButton, Link, TabView visual baselines
herbert-julio-azion Jul 23, 2026
cf61ee4
Merge branch 'main' into worktree-component-fixes-batch
robsongajunior Jul 23, 2026
452e6dd
Merge branch 'main' into worktree-component-fixes-batch
robsongajunior Jul 23, 2026
d4051bc
Merge branch 'main' into worktree-component-fixes-batch
robsongajunior Jul 23, 2026
ee0f664
refactor(webkit): apply PR #785 review to popover and link spec
gabriel-lisboa-azion Jul 24, 2026
57aa206
Merge branch 'main' into worktree-component-fixes-batch
robsongajunior Jul 26, 2026
d3fa576
Merge branch 'main' into worktree-component-fixes-batch
robsongajunior Jul 27, 2026
3c28b68
Merge branch 'main' into worktree-component-fixes-batch
robsongajunior Jul 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .specs/link.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ name: link
category: navigation
structure: monolithic
status: implemented
spec_version: 1
checksum: 42815455514e846bb1b8f6825c1ab7a00e53d155ae890edd2ffd48898ed9aa0c
spec_version: 2
checksum: 82a01ef41214686b035f6f6f700873cd25b1c0f963f820bbd40220e27ece8c9a
created: 2026-05-22
last_updated: 2026-06-01
last_updated: 2026-07-22
figma:
url: https://www.figma.com/design/t97pXRs7xME3SJDs5iZ5RF/Webkit?node-id=3548-578
node_id: 3548:578
Expand All @@ -22,12 +22,12 @@ Helps users move between views or sections. Migrated from the existing implement
| Prop | Type | Default | Required | JSDoc |
|---|---|---|---|---|
| `label` | `string` | `'Learn More'` | no | Visible label rendered inside the link. |
| `size` | `'large' | 'medium'` | `'large'` | no | Size token; affects height, gap, and typography. |
| `size` | `'small' \| 'medium' \| 'large'` | `'large'` | no | Size token; affects height and typography (large 40px, medium 32px, small 28px), matching Button. |
| `disabled` | `boolean` | `false` | no | Disables interaction and applies disabled tokens. |
| `showIcon` | `boolean` | `true` | no | When true, renders the trailing icon. |
| `icon` | `string` | `'pi pi-external-link'` | no | PrimeIcons class for the trailing icon. |
| `href` | `string` | `'#'` | no | Destination URL for the anchor. |
| `target` | `'_blank' | '_self'` | `'_self'` | no | Link target when navigating. |
| `target` | `'_blank' \| '_self'` | `'_self'` | no | Link target when navigating. |

## Events

Expand Down
16 changes: 8 additions & 8 deletions .specs/popover.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ name: popover
category: overlay
structure: composition
status: implemented
spec_version: 1
spec_version: 2
figma:
url: https://www.figma.com/design/t97pXRs7xME3SJDs5iZ5RF/Webkit?node-id=482-954
node_id: 482:954
checksum: 905f4e9e5d3e34bf91c5321fee72928f6897969a6579a35c86c3d6d0021349dd
checksum: fda32a6bfbd35aeff4521753aefad097adbe249a83f961cb14878b00a6d4db7b
created: 2026-06-30
last_updated: 2026-06-30
last_updated: 2026-07-22
---

# Popover — Component Spec
Expand Down Expand Up @@ -63,13 +63,13 @@ import PopoverFooter from '@aziontech/webkit/popover-footer'

## Sub-components

The root `Popover` owns open/closed state, anchoring/placement, outside-click + `Esc` dismissal, focus return, **and renders the single teleported, animated floating panel** (the panel element carries `role="dialog"`, the surface/border/radius/shadow tokens, the open/close `animate-popup-scale-*`, `--popup-origin`, and `data-state` / `data-placement`). Shared state flows through `provide`/`inject` (`isOpen`, `triggerId`, `contentId`, `titleId`, `descriptionId`, `setOpen`, resolved `placement`, and a panel-body ref the content teleports into), so every sub-component is context-aware and the consumer wires nothing. `Popover.Content` teleports its content into the root's panel — it does not own its own overlay (this mirrors `navigation/dropdown`, where the root owns the floating panel and the sub-components fill it). Member names mirror this component's anatomy; `Trigger` / `Content` are valid here because the popover has a real `data-state="open|closed"`.
The root `Popover` owns open/closed state, anchoring/placement (exposing the computed `panelStyle`), outside-click + `Esc` dismissal, focus containment and return. Shared state flows through `provide`/`inject` (`isOpen`, `triggerId`, `contentId`, `titleId`, `descriptionId`, `setOpen`, resolved `placement`, `width`, `panelStyle`, and the `panelRef` that `Popover.Content` assigns), so every sub-component is context-aware and the consumer wires nothing. **`Popover.Content` renders the single teleported, animated floating panel with its content inline** (the Select / Tooltip pattern — one `<Teleport>` + `<Transition>`, so the scale animation carries the whole panel on enter/leave); the panel element carries `role="dialog"`, the surface/border/radius/shadow tokens, the open/close `animate-popup-scale-*`, `--popup-origin` (via `panelStyle`), and `data-state` / `data-placement`, and assigns itself to the root's `panelRef` so the root's placement/focus logic operates on it. Member names mirror this component's anatomy; `Trigger` / `Content` are valid here because the popover has a real `data-state="open|closed"`.

- `popover-trigger/popover-trigger.vue` — slot-only abstract wrapper. Reads `inject` for `isOpen`, `triggerId`, `contentId`, `setOpen`. Renders a single inline element that toggles the panel on click and exposes `aria-haspopup="dialog"`, `aria-expanded`, `aria-controls`. The popover anchors to this element's bounding box.
- Props: _none_.
- Events: _none_ (the root handles `update:open`).
- Slots: `default` with scope `{ isOpen: boolean }` — the consumer's trigger element. The wrapper attaches aria + click; the consumer must not bind those itself.
- `popover-content/popover-content.vue` — the panel's content region. Teleports its default slot into the root's floating panel (via the injected panel-body ref) and arranges it as a flex column: `Popover.Header` (when present), the free-form body, then `Popover.Footer` (when present). The root — not this component — owns the panel element and its surface/border/radius/shadow, `position: fixed` anchoring, `role="dialog"` + `aria-modal="false"`, `aria-labelledby` / `aria-describedby` (from injected `titleId`/`descriptionId`), the open/close `animate-popup-scale-*`, and `data-state` / `data-placement`. `Popover.Content` adds no overlay or animation of its own.
- `popover-content/popover-content.vue` — renders the floating panel via a single `<Teleport to="body">` + `<Transition>`, with its default slot (`Popover.Header`, the free-form body, `Popover.Footer`) arranged inline as a flex column inside the panel. It owns the panel element and its surface/border/radius/shadow, `position: fixed` anchoring (from the injected `panelStyle`), `role="dialog"` + `aria-modal="false"`, `aria-labelledby` / `aria-describedby` (from injected `titleId`/`descriptionId`), the open/close `animate-popup-scale-in` / `animate-popup-scale-out`, and `data-state` / `data-placement`. It assigns the panel element to the injected `panelRef` (function ref) so the root's placement, focus, `Esc`/`Tab` and outside-click logic operate on it. Because the content lives in the same teleported element as the animation, the scale animation carries the whole panel on enter/leave.
- Props: _none_.
- Events: _none_.
- Slots: `default` — the popover anatomy (`Popover.Header`, body content, `Popover.Footer`).
Expand Down Expand Up @@ -143,7 +143,7 @@ export default Object.assign(Popover, {
| `offset` | `number` | `4` | no | Pixel gap between the trigger and the panel. |
| `disabled` | `boolean` | `false` | no | Prevents the trigger from opening the panel and applies disabled tokens. |
| `width` | `'small' \| 'medium' \| 'large'` | `undefined` | no | Panel width preset mapped to container tokens: `'small'` (`var(--container-xs)`), `'medium'` (`var(--container-sm)`), `'large'` (`var(--container-md)`). When omitted, the panel sizes fluidly between `var(--container-3xs)` and `var(--container-xs)`. |
| `dismissible` | `boolean` | `true` | no | Light-dismiss: when `true`, the panel closes on outside-click, `Esc`, and focus leaving the panel. Set `false` to keep it open until the trigger or `PopoverClose` closes it. |
| `dismissible` | `boolean` | `true` | no | Light-dismiss: when `true`, the panel closes on outside-click and `Esc`. Set `false` to keep it open until the trigger or `PopoverClose` closes it. |
Comment thread
robsongajunior marked this conversation as resolved.

## Events

Expand Down Expand Up @@ -205,8 +205,8 @@ export default Object.assign(Popover, {

- Visible focus: `focus-visible:ring-2 focus-visible:ring-[var(--ring-color)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg-canvas)]` on the trigger and the close button.
- Keyboard map:
- On the trigger: `Enter` / `Space` toggles the panel. When it opens, focus moves into the panel (the first focusable element, else the panel itself).
- Inside the panel: `Esc` closes the panel and returns focus to the trigger; `Tab` / `Shift+Tab` move through the panel's focusable children in DOM order and, since the popover is **non-modal**, continue past the panel without trapping — moving focus out closes the panel.
- On the trigger: `Enter` / `Space` toggles the panel. When it opens, focus **stays on the trigger** — it is not moved into the panel.
- `Esc` closes the panel and returns focus to the trigger, from anywhere (trigger or panel). `Tab` / `Shift+Tab` are **contained** within the trigger and the panel's focusable children: Tab from the trigger moves into the panel, Tab past the last panel child wraps back to the trigger, and Shift+Tab reverses. Tabbing **never** closes the panel — only `Esc` or an outside-click dismiss it.
- Outside-click closes the panel; focus is not forced back to the trigger on outside-click.
- ARIA: trigger exposes `aria-haspopup="dialog"`, `aria-expanded`, `aria-controls`; the floating panel (rendered by the root) uses `role="dialog"` with `aria-modal="false"`, `aria-labelledby` pointing at the `Popover.Title` id and `aria-describedby` pointing at the `Popover.Description` id (when present); `Popover.Close` is an `IconButton` with `aria-label="Close"`.
- Contrast ≥4.5:1 (text) / ≥3:1 (icons), including the disabled trigger state (validated via `--text-muted` / `--text-disabled` tokens).
Expand Down
9 changes: 6 additions & 3 deletions .specs/tab-view.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ name: tab-view
category: navigation
structure: composition
status: implemented
spec_version: 1
spec_version: 2
figma:
url: https://www.figma.com/design/t97pXRs7xME3SJDs5iZ5RF/Webkit?node-id=3374-6191
node_id: 3374:6191
checksum: 31904e8156de30e1675ac0114f0200a0422b240a6a7b28c19d4c16a1243f4fc3
checksum: 9a23a13b0205624a780b6ac76086455a35d38ac2b266757cbf91080258308e6b
created: 2026-05-22
last_updated: 2026-05-22
last_updated: 2026-07-22
---
# Tab View — Component Spec

Expand Down Expand Up @@ -43,13 +43,15 @@ Helps users move between views or sections. Migrated from the existing implement

- Visual states: `default`, `hover`, `focus-visible`, `active`, `disabled`
- `data-state` values: `open` | `closed` (where applicable)
- Responsive: the tab list (`TabView.List`) scrolls **horizontally** when the tabs exceed the available width (mobile / narrow viewports) instead of overflowing or wrapping; the scrollbar is visually hidden and the active indicator stays aligned while scrolling. Tabs keep their intrinsic width (`shrink-0`). A gradient edge-fade overlay on the scrollable side(s) (mirrored on `data-fade-start` / `data-fade-end`) hints at off-screen tabs; it fades in / out with the semantic `animate-fade-in` / `animate-fade-out` utilities, and a non-scrollable edge renders no overlay so the first / last tab is never covered at rest.

## Motion & Animations

| Trigger | Animation / Transition | Token | Reduced-motion fallback |
|---|---|---|---|
| open/close | `animate-fade-in` | semantic | `motion-reduce:animate-none` |
| open/close | `animate-none` | semantic | `motion-reduce:animate-none` |
| scroll edge fade appears / leaves | `animate-fade-in` / `animate-fade-out` | semantic | `motion-reduce:animate-none` |
| state change | `transition-colors duration-150 ease-out` | inline | `motion-reduce:transition-none` |

## Tokens
Expand Down Expand Up @@ -81,6 +83,7 @@ Helps users move between views or sections. Migrated from the existing implement
## Stories (Storybook)

- Default
- Scrollable — many tabs in a width-constrained container; justified because the horizontal-scroll (mobile) behavior of `TabView.List` is a distinct rendered state that the Default story (few tabs, no overflow) does not exercise.

## Constraints — DO NOT

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@ const meta = {
},
size: {
control: 'select',
options: ['large', 'medium'],
description: 'Size token. Affects height, gap, and typography.',
options: ['small', 'medium', 'large'],
description: 'Size token. Affects height and typography, matching Button.',
table: {
category: 'props',
type: { summary: "'large' | 'medium'" },
type: { summary: "'small' | 'medium' | 'large'" },
defaultValue: { summary: "'large'" }
}
},
Expand Down Expand Up @@ -127,6 +127,7 @@ export const Default = {
const SIZES_TEMPLATE = `<div class="flex flex-wrap items-center gap-4">
<Link size="large" label="Learn More" />
<Link size="medium" label="Learn More" />
<Link size="small" label="Learn More" />
</div>`

/** @type {import('@storybook/vue3').StoryObj<typeof Link>} */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,3 +189,91 @@ export const Default = {
}
}
}

const SCROLLABLE_TABS = [
{ value: 'tab-1', label: 'Overview' },
{ value: 'tab-2', label: 'Metrics' },
{ value: 'tab-3', label: 'Logs' },
{ value: 'tab-4', label: 'Settings' },
{ value: 'tab-5', label: 'Security' },
{ value: 'tab-6', label: 'Integrations' },
{ value: 'tab-7', label: 'Billing' },
{ value: 'tab-8', label: 'Advanced' }
]

const SCROLLABLE_TEMPLATE = `
<TabView.Root v-model:value="active" class="w-full max-w-[20rem]">
<TabView.List>
<TabView.Item
v-for="tab in tabs"
:key="tab.value"
:value="tab.value"
:label="tab.label"
/>
</TabView.List>
<TabView.Content class="mt-[var(--spacing-4)]">
<TabView.Panel
v-for="tab in tabs"
:key="tab.value"
:value="tab.value"
class="rounded-[var(--shape-card)] border border-[var(--border-default)] p-[var(--spacing-4)]"
>
<p class="text-body-sm text-[var(--text-default)]">Content for {{ tab.label }}</p>
</TabView.Panel>
</TabView.Content>
</TabView.Root>
`

const SCROLLABLE_SOURCE = `<TabView.Root v-model:value="active" class="w-full max-w-[20rem]">
<TabView.List>
<TabView.Item
v-for="tab in tabs"
:key="tab.value"
:value="tab.value"
:label="tab.label"
/>
</TabView.List>
<TabView.Content class="mt-[var(--spacing-4)]">
<TabView.Panel
v-for="tab in tabs"
:key="tab.value"
:value="tab.value"
class="rounded-[var(--shape-card)] border border-[var(--border-default)] p-[var(--spacing-4)]"
>
<p class="text-body-sm text-[var(--text-default)]">Content for {{ tab.label }}</p>
</TabView.Panel>
</TabView.Content>
</TabView.Root>`

/** @type {import('@storybook/vue3').StoryObj<typeof TabView.Root>} */
export const Scrollable = {
render: () => ({
components,
setup() {
const active = ref('tab-1')
return { active, tabs: SCROLLABLE_TABS }
},
template: SCROLLABLE_TEMPLATE
}),
parameters: {
docs: {
controls: { disable: true },
description: {
story:
'More tabs than fit the container: on narrow (mobile) widths the tab bar scrolls horizontally instead of overflowing or wrapping, the scrollbar is hidden, and the active indicator stays aligned while scrolling.'
},
source: {
code: toSfc(
[
IMPORT,
"import { ref } from 'vue'",
'',
`const tabs = ${JSON.stringify(SCROLLABLE_TABS, null, 2)}`,
"const active = ref('tab-1')"
],
SCROLLABLE_SOURCE
)
}
}
}
}
4 changes: 2 additions & 2 deletions packages/theme/dist/v4/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -981,13 +981,13 @@
}

@keyframes popupScaleIn {
0% { opacity: 0; transform: scale(0.95); }
0% { opacity: 0; transform: scale(0.9); }
100% { opacity: 1; transform: scale(1); }
}

@keyframes popupScaleOut {
0% { opacity: 1; transform: scale(1); }
100% { opacity: 0; transform: scale(0.95); }
100% { opacity: 0; transform: scale(0.9); }
}

@keyframes slideInLeft {
Expand Down
4 changes: 2 additions & 2 deletions packages/theme/dist/v4/globals.scss
Original file line number Diff line number Diff line change
Expand Up @@ -981,13 +981,13 @@
}

@keyframes popupScaleIn {
0% { opacity: 0; transform: scale(0.95); }
0% { opacity: 0; transform: scale(0.9); }
100% { opacity: 1; transform: scale(1); }
}

@keyframes popupScaleOut {
0% { opacity: 1; transform: scale(1); }
100% { opacity: 0; transform: scale(0.95); }
100% { opacity: 0; transform: scale(0.9); }
}

@keyframes slideInLeft {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@ export const keyframes = {
'100%': 'background-position: -200% 0'
},
popupScaleIn: {
'0%': 'opacity: 0; transform: scale(0.95)',
'0%': 'opacity: 0; transform: scale(0.9)',
'100%': 'opacity: 1; transform: scale(1)'
},
popupScaleOut: {
'0%': 'opacity: 1; transform: scale(1)',
'100%': 'opacity: 0; transform: scale(0.95)'
'100%': 'opacity: 0; transform: scale(0.9)'
},
slideInLeft: {
'0%': 'transform: translateX(-100%)',
Expand Down
8 changes: 4 additions & 4 deletions packages/webkit/catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -7843,10 +7843,10 @@
},
{
"name": "size",
"type": "'large' | 'medium'",
"type": "'small' | 'medium' | 'large'",
"default": "'large'",
"required": "no",
"doc": "Size token; affects height, gap, and typography."
"doc": "Size token; affects height and typography (large 40px, medium 32px, small 28px), matching Button."
},
{
"name": "disabled",
Expand Down Expand Up @@ -8436,7 +8436,7 @@
"type": "boolean",
"default": "true",
"required": "no",
"doc": "Light-dismiss: when `true`, the panel closes on outside-click, `Esc`, and focus leaving the panel. Set `false` to keep it open until the trigger or `PopoverClose` closes it."
"doc": "Light-dismiss: when `true`, the panel closes on outside-click and `Esc`. Set `false` to keep it open until the trigger or `PopoverClose` closes it."
}
],
"events": [
Expand Down Expand Up @@ -8505,7 +8505,7 @@
"type": "boolean",
"default": "true",
"required": "no",
"doc": "Light-dismiss: when `true`, the panel closes on outside-click, `Esc`, and focus leaving the panel. Set `false` to keep it open until the trigger or `PopoverClose` closes it."
"doc": "Light-dismiss: when `true`, the panel closes on outside-click and `Esc`. Set `false` to keep it open until the trigger or `PopoverClose` closes it."
}
],
"events": [
Expand Down
Loading
Loading